feat(ct): выбор валюты у уже загруженных КП на шаге 2
После возврата с шага 3 очередь загрузки пуста, и КП, по умолчанию сохранённое в тенге, нельзя было перевести в валюту без перезагрузки файла. Теперь валюта меняется в «Загруженные КП»: обновляются supplier.currency и пересчёт офферов в тенге; в режиме просмотра показывается код валюты. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+24
@@ -109,6 +109,30 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Размеры совпадают с селектом валюты в очереди загрузки (FileUploadArea), чтобы строки выглядели одинаково.
|
||||||
|
.kpCurrencySelect {
|
||||||
|
font-size: 11px;
|
||||||
|
padding: 2px 6px;
|
||||||
|
border: 1px solid $border;
|
||||||
|
border-radius: 3px;
|
||||||
|
background: $surface;
|
||||||
|
color: $text;
|
||||||
|
font-family: inherit;
|
||||||
|
cursor: pointer;
|
||||||
|
outline: none;
|
||||||
|
flex-shrink: 0;
|
||||||
|
height: 26px;
|
||||||
|
|
||||||
|
&:focus { border-color: $accent; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.kpCurrencyCode {
|
||||||
|
flex-shrink: 0;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: $muted;
|
||||||
|
}
|
||||||
|
|
||||||
.noOfferBadge {
|
.noOfferBadge {
|
||||||
background: $danger-bg;
|
background: $danger-bg;
|
||||||
color: $danger;
|
color: $danger;
|
||||||
|
|||||||
+22
-1
@@ -11,6 +11,7 @@ import { useKpProcessing } from './useKpProcessing';
|
|||||||
import { persistStep2 } from './step2Persist';
|
import { persistStep2 } from './step2Persist';
|
||||||
import { SuppliersWithoutKp } from './SuppliersWithoutKp';
|
import { SuppliersWithoutKp } from './SuppliersWithoutKp';
|
||||||
import { UploadedKpList } from './UploadedKpList';
|
import { UploadedKpList } from './UploadedKpList';
|
||||||
|
import { applySupplierCurrency, getSupplierCurrency } from './kpCurrency';
|
||||||
import styles from './Step2CommercialOffers.module.scss';
|
import styles from './Step2CommercialOffers.module.scss';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -72,6 +73,23 @@ export const Step2CommercialOffers: React.FC<Props> = ({
|
|||||||
onSave({ suppliers: [...withOffer, ...withoutOffer] });
|
onSave({ suppliers: [...withOffer, ...withoutOffer] });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Смена валюты уже загруженного КП (список «Загруженные КП»). Если файл ещё в очереди этой сессии,
|
||||||
|
// синхронизируем и её: иначе persistStep2 пересоберёт поставщика из kpFiles со старой валютой.
|
||||||
|
const handleUploadedCurrencyChange = (fileSpId: number, currencyCode: string): void => {
|
||||||
|
const uploaded = formData.uploadedKpFiles.find(u => u.spId === fileSpId);
|
||||||
|
if (!uploaded?.supplierName) return;
|
||||||
|
onSave(applySupplierCurrency(
|
||||||
|
formData.suppliers ?? [],
|
||||||
|
formData.offers ?? [],
|
||||||
|
uploaded.supplierName,
|
||||||
|
currencyCode,
|
||||||
|
currencyRates,
|
||||||
|
));
|
||||||
|
kp.kpFiles
|
||||||
|
.filter(f => f.supplierName === uploaded.supplierName)
|
||||||
|
.forEach(f => kp.handleCurrencyChange(f.localId, currencyCode));
|
||||||
|
};
|
||||||
|
|
||||||
const handleSave = async (): Promise<void> => {
|
const handleSave = async (): Promise<void> => {
|
||||||
setIsSaving(true);
|
setIsSaving(true);
|
||||||
setSaveError(undefined);
|
setSaveError(undefined);
|
||||||
@@ -133,8 +151,11 @@ export const Step2CommercialOffers: React.FC<Props> = ({
|
|||||||
<UploadedKpList
|
<UploadedKpList
|
||||||
files={formData.uploadedKpFiles}
|
files={formData.uploadedKpFiles}
|
||||||
service={service}
|
service={service}
|
||||||
|
currencyOptions={currencyOptions}
|
||||||
|
getCurrency={supplierName => getSupplierCurrency(formData.suppliers ?? [], supplierName)}
|
||||||
|
onCurrencyChange={handleUploadedCurrencyChange}
|
||||||
onRemove={(spId) => onSave({ uploadedKpFiles: formData.uploadedKpFiles.filter(u => u.spId !== spId) })}
|
onRemove={(spId) => onSave({ uploadedKpFiles: formData.uploadedKpFiles.filter(u => u.spId !== spId) })}
|
||||||
disabled={isReadOnly}
|
disabled={isNotificationLink || isReadOnly}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
+30
-3
@@ -7,12 +7,24 @@ import styles from './Step2CommercialOffers.module.scss';
|
|||||||
interface Props {
|
interface Props {
|
||||||
files: CTFormData['uploadedKpFiles'];
|
files: CTFormData['uploadedKpFiles'];
|
||||||
service: CtService;
|
service: CtService;
|
||||||
|
currencyOptions: string[];
|
||||||
|
getCurrency: (supplierName: string | undefined) => string;
|
||||||
|
onCurrencyChange: (spId: number, currencyCode: string) => void;
|
||||||
onRemove: (spId: number) => void;
|
onRemove: (spId: number) => void;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Секция «Загруженные КП» (постоянный список из formData): ссылка на файл + удаление из списка.
|
// Секция «Загруженные КП» (постоянный список из formData): ссылка на файл, валюта КП и удаление.
|
||||||
export const UploadedKpList: React.FC<Props> = ({ files, service, onRemove, disabled }) => (
|
// Валюту здесь можно поправить и после возврата с Шага 3 — очередь загрузки к этому моменту уже пуста.
|
||||||
|
export const UploadedKpList: React.FC<Props> = ({
|
||||||
|
files,
|
||||||
|
service,
|
||||||
|
currencyOptions,
|
||||||
|
getCurrency,
|
||||||
|
onCurrencyChange,
|
||||||
|
onRemove,
|
||||||
|
disabled,
|
||||||
|
}) => (
|
||||||
<section className={styles.section}>
|
<section className={styles.section}>
|
||||||
<h3 className={styles.sectionTitle}>Загруженные КП</h3>
|
<h3 className={styles.sectionTitle}>Загруженные КП</h3>
|
||||||
<ul className={styles.noOfferList}>
|
<ul className={styles.noOfferList}>
|
||||||
@@ -27,12 +39,27 @@ export const UploadedKpList: React.FC<Props> = ({ files, service, onRemove, disa
|
|||||||
>
|
>
|
||||||
{f.name}
|
{f.name}
|
||||||
</a>
|
</a>
|
||||||
|
{disabled ? (
|
||||||
|
<span className={styles.kpCurrencyCode}>{getCurrency(f.supplierName)}</span>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<select
|
||||||
|
className={styles.kpCurrencySelect}
|
||||||
|
value={getCurrency(f.supplierName)}
|
||||||
|
onChange={e => onCurrencyChange(f.spId, e.target.value)}
|
||||||
|
// Валюта хранится на поставщике — без него менять нечего.
|
||||||
|
disabled={!f.supplierName}
|
||||||
|
aria-label={`Валюта КП ${f.name}`}
|
||||||
|
>
|
||||||
|
{currencyOptions.map(c => <option key={c} value={c}>{c}</option>)}
|
||||||
|
</select>
|
||||||
<IconButton
|
<IconButton
|
||||||
icon="trash"
|
icon="trash"
|
||||||
title="Удалить КП из списка"
|
title="Удалить КП из списка"
|
||||||
onClick={() => onRemove(f.spId)}
|
onClick={() => onRemove(f.spId)}
|
||||||
disabled={disabled}
|
|
||||||
/>
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import { CurrencyRate, Supplier, SupplierOffer } from '../types/domain';
|
||||||
|
|
||||||
|
const BASE_CURRENCY = 'KZT';
|
||||||
|
|
||||||
|
// Валюта хранится на поставщике (Шаг 3 читает supplier.currency), а офферы несут цену в валюте КП
|
||||||
|
// и её пересчёт в тенге. При смене валюты уже загруженного КП обновляем и то и другое, чтобы
|
||||||
|
// таблица сравнения и сохранение офферов в SP (valutaId) видели одну и ту же валюту.
|
||||||
|
export function applySupplierCurrency(
|
||||||
|
suppliers: Supplier[],
|
||||||
|
offers: SupplierOffer[],
|
||||||
|
supplierName: string,
|
||||||
|
currencyCode: string,
|
||||||
|
currencyRates: CurrencyRate[],
|
||||||
|
): { suppliers: Supplier[]; offers: SupplierOffer[] } {
|
||||||
|
const supplierIds = new Set(
|
||||||
|
suppliers.filter(s => s.hasOffer && s.name === supplierName).map(s => s.id),
|
||||||
|
);
|
||||||
|
const rate = currencyCode === BASE_CURRENCY
|
||||||
|
? 1
|
||||||
|
: (currencyRates.find(r => r.code === currencyCode)?.rateToKzt ?? 1);
|
||||||
|
|
||||||
|
const nextSuppliers = suppliers.map(s =>
|
||||||
|
supplierIds.has(s.id) ? { ...s, currency: currencyCode } : s,
|
||||||
|
);
|
||||||
|
const nextOffers = offers.map(o => {
|
||||||
|
if (!supplierIds.has(o.supplierId)) return o;
|
||||||
|
const pricePerUnitKzt = o.pricePerUnit * rate;
|
||||||
|
const totalPriceKzt = pricePerUnitKzt * o.quantity;
|
||||||
|
return { ...o, currencyCode, pricePerUnitKzt, totalPriceKzt, totalWithExtraKzt: totalPriceKzt };
|
||||||
|
});
|
||||||
|
|
||||||
|
return { suppliers: nextSuppliers, offers: nextOffers };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getSupplierCurrency(suppliers: Supplier[], supplierName: string | undefined): string {
|
||||||
|
const supplier = suppliers.find(s => s.hasOffer && s.name === supplierName);
|
||||||
|
return supplier?.currency ?? BASE_CURRENCY;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user