feat(ct): выбор валюты у уже загруженных КП на шаге 2

После возврата с шага 3 очередь загрузки пуста, и КП, по умолчанию
сохранённое в тенге, нельзя было перевести в валюту без перезагрузки файла.
Теперь валюта меняется в «Загруженные КП»: обновляются supplier.currency
и пересчёт офферов в тенге; в режиме просмотра показывается код валюты.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Тимур Городецкий
2026-09-23 15:32:43 +05:00
parent a45807f669
commit e4b6582e3c
4 changed files with 119 additions and 9 deletions
@@ -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 {
background: $danger-bg;
color: $danger;
@@ -11,6 +11,7 @@ import { useKpProcessing } from './useKpProcessing';
import { persistStep2 } from './step2Persist';
import { SuppliersWithoutKp } from './SuppliersWithoutKp';
import { UploadedKpList } from './UploadedKpList';
import { applySupplierCurrency, getSupplierCurrency } from './kpCurrency';
import styles from './Step2CommercialOffers.module.scss';
interface Props {
@@ -72,6 +73,23 @@ export const Step2CommercialOffers: React.FC<Props> = ({
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> => {
setIsSaving(true);
setSaveError(undefined);
@@ -133,8 +151,11 @@ export const Step2CommercialOffers: React.FC<Props> = ({
<UploadedKpList
files={formData.uploadedKpFiles}
service={service}
currencyOptions={currencyOptions}
getCurrency={supplierName => getSupplierCurrency(formData.suppliers ?? [], supplierName)}
onCurrencyChange={handleUploadedCurrencyChange}
onRemove={(spId) => onSave({ uploadedKpFiles: formData.uploadedKpFiles.filter(u => u.spId !== spId) })}
disabled={isReadOnly}
disabled={isNotificationLink || isReadOnly}
/>
)}
@@ -7,12 +7,24 @@ import styles from './Step2CommercialOffers.module.scss';
interface Props {
files: CTFormData['uploadedKpFiles'];
service: CtService;
currencyOptions: string[];
getCurrency: (supplierName: string | undefined) => string;
onCurrencyChange: (spId: number, currencyCode: string) => void;
onRemove: (spId: number) => void;
disabled?: boolean;
}
// Секция «Загруженные КП» (постоянный список из formData): ссылка на файл + удаление из списка.
export const UploadedKpList: React.FC<Props> = ({ files, service, onRemove, disabled }) => (
// Секция «Загруженные КП» (постоянный список из formData): ссылка на файл, валюта КП и удаление.
// Валюту здесь можно поправить и после возврата с Шага 3 — очередь загрузки к этому моменту уже пуста.
export const UploadedKpList: React.FC<Props> = ({
files,
service,
currencyOptions,
getCurrency,
onCurrencyChange,
onRemove,
disabled,
}) => (
<section className={styles.section}>
<h3 className={styles.sectionTitle}>Загруженные КП</h3>
<ul className={styles.noOfferList}>
@@ -27,12 +39,27 @@ export const UploadedKpList: React.FC<Props> = ({ files, service, onRemove, disa
>
{f.name}
</a>
<IconButton
icon="trash"
title="Удалить КП из списка"
onClick={() => onRemove(f.spId)}
disabled={disabled}
/>
{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
icon="trash"
title="Удалить КП из списка"
onClick={() => onRemove(f.spId)}
/>
</>
)}
</li>
))}
</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;
}