feat(ct): подсветка несоответствия ед. изм. поставщика и заявки на шаге 3
Несоответствие единиц было видно только по незаметной «*» и только при включённом тумблере. Теперь «Ед. изм.» поставщика подсвечивается жёлтым, если отличается от заявки (подсказка «В заявке: кг, у поставщика: т»). С включённым тумблером единица, кол-во и цены подсвечиваются голубым, если пересчитаны в ед. заявки, и красным, если пересчитать нельзя (шт ↔ кг). Статус — чистая функция getUnitStatus; у тумблера — легенда цветов вместо «*».
This commit is contained in:
+6
-5
@@ -3,7 +3,8 @@ import { IconButton, TextInput } from '@tnsu/ui-kit';
|
||||
import { CurrencyRate } from '../types/domain';
|
||||
import { PositionRow } from './matrixTypes';
|
||||
import { Step3Matrix } from './useStep3Matrix';
|
||||
import { convertQuantity, unitsEquivalent } from '../utils/unitConversion';
|
||||
import { getUnitStatus } from '../utils/unitConversion';
|
||||
import { unitStatusClass } from './unitStatusStyles';
|
||||
import { formatKzt, formatNum, formatQty, round2Str, buildOfferKey } from './matrixHelpers';
|
||||
import styles from './Step3ComparisonTable.module.scss';
|
||||
|
||||
@@ -67,8 +68,8 @@ export function ExtraPositionRow({ pos, posIndex, regularPositionsCount, matrix,
|
||||
// Перевод в ед. изм. заявки (тумблер) — как в обычных позициях.
|
||||
const reqUnit = pos.unit;
|
||||
const supUnit = item?.unit || pos.unit;
|
||||
const tRatio = translateUnits && !unitsEquivalent(supUnit, reqUnit) ? convertQuantity(1, supUnit, reqUnit) : null;
|
||||
const cannotTranslate = translateUnits && !unitsEquivalent(supUnit, reqUnit) && tRatio === null;
|
||||
const unitStatus = getUnitStatus(supUnit, reqUnit, translateUnits);
|
||||
const tRatio = unitStatus.ratio ?? null;
|
||||
const dispUnit = tRatio !== null ? reqUnit : (item?.unit || pos.unit);
|
||||
const dispQty = (tRatio !== null && qty !== undefined) ? qty * tRatio : qty;
|
||||
const dispPrice = (tRatio !== null && price !== undefined) ? price / tRatio : price;
|
||||
@@ -91,7 +92,7 @@ export function ExtraPositionRow({ pos, posIndex, regularPositionsCount, matrix,
|
||||
<span className={styles.computedValue}>{item?.supplierProductName || pos.materialName}</span>
|
||||
)}
|
||||
</td>
|
||||
<td className={[styles.dataCell, selCls].filter(Boolean).join(' ')}>
|
||||
<td className={[styles.dataCell, unitStatusClass(unitStatus.status), selCls].filter(Boolean).join(' ')} title={unitStatus.hint}>
|
||||
{showInput ? (
|
||||
<div className={styles.offerInput}>
|
||||
<TextInput
|
||||
@@ -102,7 +103,7 @@ export function ExtraPositionRow({ pos, posIndex, regularPositionsCount, matrix,
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<span className={styles.computedValue}>{(dispUnit || '—') + (cannotTranslate ? ' *' : '')}</span>
|
||||
<span className={styles.computedValue}>{dispUnit || '—'}</span>
|
||||
)}
|
||||
</td>
|
||||
<td className={[styles.dataCell, ep?.isDelivery ? styles.deliveryQtyCell : '', selCls].filter(Boolean).join(' ')}>
|
||||
|
||||
+13
-8
@@ -3,7 +3,8 @@ import { TextInput, Icon } from '@tnsu/ui-kit';
|
||||
import { CurrencyRate } from '../types/domain';
|
||||
import { PositionRow } from './matrixTypes';
|
||||
import { Step3Matrix } from './useStep3Matrix';
|
||||
import { convertQuantity, unitsEquivalent } from '../utils/unitConversion';
|
||||
import { getUnitStatus } from '../utils/unitConversion';
|
||||
import { unitStatusClass } from './unitStatusStyles';
|
||||
import { getSubCols, formatKzt, formatNum, formatQty, round2Str, buildOfferKey } from './matrixHelpers';
|
||||
import styles from './Step3ComparisonTable.module.scss';
|
||||
|
||||
@@ -85,8 +86,12 @@ export function MatrixRowView({ pos, posIndex, matrix, currencyRates, isRestrict
|
||||
// null — единицы совпадают или пересчёт невозможен.
|
||||
const reqUnit = pos.unit;
|
||||
const supUnit = item.unit || pos.unit;
|
||||
const tRatio = translateUnits && !unitsEquivalent(supUnit, reqUnit) ? convertQuantity(1, supUnit, reqUnit) : null;
|
||||
const cannotTranslate = translateUnits && !unitsEquivalent(supUnit, reqUnit) && tRatio === null;
|
||||
const unitStatus = getUnitStatus(supUnit, reqUnit, translateUnits);
|
||||
const tRatio = unitStatus.ratio ?? null;
|
||||
// Подсветка: «Ед. изм.» — при любом несоответствии заявке; кол-во и цены — когда тумблер включён
|
||||
// (они показаны пересчитанными или их нельзя сравнивать напрямую).
|
||||
const unitCls = unitStatusClass(unitStatus.status);
|
||||
const valueCls = translateUnits ? unitCls : '';
|
||||
const dispUnit = tRatio !== null ? reqUnit : (item.unit || pos.unit);
|
||||
const dispQty = (tRatio !== null && qty !== undefined) ? qty * tRatio : qty;
|
||||
const dispPrice = (tRatio !== null && price !== undefined) ? price / tRatio : price;
|
||||
@@ -123,7 +128,7 @@ export function MatrixRowView({ pos, posIndex, matrix, currencyRates, isRestrict
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
<td className={[styles.dataCell, selCls].filter(Boolean).join(' ')}>
|
||||
<td className={[styles.dataCell, unitCls, selCls].filter(Boolean).join(' ')} title={unitStatus.hint}>
|
||||
{showInput ? (
|
||||
<div className={styles.offerInput}>
|
||||
<TextInput
|
||||
@@ -134,10 +139,10 @@ export function MatrixRowView({ pos, posIndex, matrix, currencyRates, isRestrict
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<span className={styles.computedValue}>{(dispUnit || '—') + (cannotTranslate ? ' *' : '')}</span>
|
||||
<span className={styles.computedValue}>{dispUnit || '—'}</span>
|
||||
)}
|
||||
</td>
|
||||
<td className={[styles.dataCell, selCls].filter(Boolean).join(' ')}>
|
||||
<td className={[styles.dataCell, valueCls, selCls].filter(Boolean).join(' ')} title={translateUnits ? unitStatus.hint : undefined}>
|
||||
{showInput ? (
|
||||
<div className={styles.offerInput}>
|
||||
<TextInput
|
||||
@@ -154,7 +159,7 @@ export function MatrixRowView({ pos, posIndex, matrix, currencyRates, isRestrict
|
||||
</td>
|
||||
{!isRestrictedView && isForeign && (
|
||||
<>
|
||||
<td className={[styles.dataCell, selCls].filter(Boolean).join(' ')}>
|
||||
<td className={[styles.dataCell, valueCls, selCls].filter(Boolean).join(' ')}>
|
||||
{showInput ? (
|
||||
<div className={styles.offerInput}>
|
||||
<TextInput
|
||||
@@ -174,7 +179,7 @@ export function MatrixRowView({ pos, posIndex, matrix, currencyRates, isRestrict
|
||||
</td>
|
||||
</>
|
||||
)}
|
||||
{!isRestrictedView && <td className={[styles.dataCell, selCls].filter(Boolean).join(' ')}>
|
||||
{!isRestrictedView && <td className={[styles.dataCell, valueCls, selCls].filter(Boolean).join(' ')}>
|
||||
{showInput ? (
|
||||
<div className={styles.offerInput}>
|
||||
<TextInput
|
||||
|
||||
+29
@@ -493,6 +493,35 @@
|
||||
color: $muted;
|
||||
}
|
||||
|
||||
// Подсветка ед. изм. поставщика (unitStatusStyles): !important — иначе фон перебивает подсветка строки
|
||||
// при наведении и выбранной колонки, и несоответствие пропадает из виду.
|
||||
.dataCell.unitMismatch {
|
||||
background: $warn-bg !important;
|
||||
color: $warn-fg;
|
||||
}
|
||||
|
||||
.dataCell.unitConverted {
|
||||
background: $accent-bg !important;
|
||||
}
|
||||
|
||||
.dataCell.unitImpossible {
|
||||
background: $danger-bg !important;
|
||||
color: $danger;
|
||||
}
|
||||
|
||||
.translateLegendConverted,
|
||||
.translateLegendImpossible {
|
||||
padding: 0 4px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.translateLegendConverted { background: $accent-bg; }
|
||||
|
||||
.translateLegendImpossible {
|
||||
background: $danger-bg;
|
||||
color: $danger;
|
||||
}
|
||||
|
||||
.selectedTotalCell {
|
||||
background: $accent-bg !important;
|
||||
|
||||
|
||||
+3
-1
@@ -142,7 +142,9 @@ export const Step3ComparisonTable: React.FC<Props> = ({
|
||||
</label>
|
||||
{translateUnits && (
|
||||
<span className={styles.translateHint}>
|
||||
кол-во и цена показаны в ед. изм. заявки; «*» — пересчёт невозможен (ред. недоступно)
|
||||
кол-во и цена — в ед. изм. заявки (ред. недоступно):{' '}
|
||||
<span className={styles.translateLegendConverted}>пересчитано</span>,{' '}
|
||||
<span className={styles.translateLegendImpossible}>пересчёт невозможен</span>
|
||||
</span>
|
||||
)}
|
||||
<div className={styles.adataCheckAllGroup}>
|
||||
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
import { UnitStatusKind } from '../utils/unitConversion';
|
||||
import styles from './Step3ComparisonTable.module.scss';
|
||||
|
||||
// Подсветка ячеек по статусу ед. изм. поставщика (см. getUnitStatus): жёлтым — единица отличается
|
||||
// от заявки, голубым — пересчитано в ед. заявки, красным — пересчитать нельзя.
|
||||
const UNIT_STATUS_CLASS: Record<UnitStatusKind, string> = {
|
||||
same: '',
|
||||
mismatch: styles.unitMismatch,
|
||||
converted: styles.unitConverted,
|
||||
impossible: styles.unitImpossible,
|
||||
};
|
||||
|
||||
export function unitStatusClass(status: UnitStatusKind): string {
|
||||
return UNIT_STATUS_CLASS[status];
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { convertQuantity, normalizeUnit, unitsEquivalent } from './unitConversion';
|
||||
import { convertQuantity, getUnitStatus, normalizeUnit, unitsEquivalent } from './unitConversion';
|
||||
|
||||
describe('unitsEquivalent — синонимы одной единицы', () => {
|
||||
// СТ №12: в заявке «Штука», у поставщика «шт.» — тумблер помечал «шт. *» (пересчёт невозможен).
|
||||
@@ -62,3 +62,30 @@ describe('normalizeUnit', () => {
|
||||
expect(normalizeUnit(' шт. ')).toBe(normalizeUnit('Штука'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('getUnitStatus — подсветка несоответствия ед. изм. (п.6)', () => {
|
||||
it('одинаковые единицы — ничего не подсвечиваем', () => {
|
||||
expect(getUnitStatus('шт.', 'Штука', false)).toEqual({ status: 'same' });
|
||||
expect(getUnitStatus('шт.', 'Штука', true)).toEqual({ status: 'same' });
|
||||
});
|
||||
|
||||
// Тумблер выключен: несовпадение видно сразу, а не только по незаметной «*» при включённом тумблере.
|
||||
it('тумблер выключен — разные единицы: несоответствие с подсказкой', () => {
|
||||
expect(getUnitStatus('т', 'кг', false)).toEqual({ status: 'mismatch', hint: 'В заявке: кг, у поставщика: т' });
|
||||
});
|
||||
|
||||
it('тумблер включён — пересчитали: коэффициент и подсказка', () => {
|
||||
expect(getUnitStatus('т', 'кг', true)).toEqual({ status: 'converted', ratio: 1000, hint: 'Пересчитано из т в кг' });
|
||||
});
|
||||
|
||||
it('тумблер включён — пересчитать нельзя: подсказка сравнивать вручную', () => {
|
||||
expect(getUnitStatus('шт', 'кг', true)).toEqual({
|
||||
status: 'impossible',
|
||||
hint: 'Нельзя перевести шт → кг: сравнивайте вручную',
|
||||
});
|
||||
});
|
||||
|
||||
it('пустая единица поставщика — не несоответствие', () => {
|
||||
expect(getUnitStatus('', 'кг', false)).toEqual({ status: 'same' });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -96,3 +96,34 @@ export function convertQuantity(qty: number, fromUnit: string | undefined, toUni
|
||||
if (!from || !to || from.dim !== to.dim) return null;
|
||||
return qty * (from.toBase / to.toBase);
|
||||
}
|
||||
|
||||
// Статус ед. изм. поставщика относительно заявки — для подсветки ячеек на шаге 3:
|
||||
// same — единицы совпадают (или у поставщика не указана) — без подсветки;
|
||||
// mismatch — тумблер выключен, единицы разные — показать, что сравнивать надо внимательно;
|
||||
// converted — тумблер включён, пересчитали в ед. заявки (ratio — коэффициент кол-ва);
|
||||
// impossible — тумблер включён, пересчитать нельзя (шт ↔ кг, шт ↔ компл).
|
||||
export type UnitStatusKind = 'same' | 'mismatch' | 'converted' | 'impossible';
|
||||
|
||||
export interface UnitStatus {
|
||||
status: UnitStatusKind;
|
||||
ratio?: number;
|
||||
hint?: string;
|
||||
}
|
||||
|
||||
export function getUnitStatus(
|
||||
supplierUnit: string | undefined,
|
||||
requestUnit: string | undefined,
|
||||
translateUnits: boolean,
|
||||
): UnitStatus {
|
||||
const supplier = (supplierUnit ?? '').trim();
|
||||
const request = (requestUnit ?? '').trim();
|
||||
if (!supplier || !request || unitsEquivalent(supplier, request)) return { status: 'same' };
|
||||
if (!translateUnits) {
|
||||
return { status: 'mismatch', hint: `В заявке: ${request}, у поставщика: ${supplier}` };
|
||||
}
|
||||
const ratio = convertQuantity(1, supplier, request);
|
||||
if (ratio === null) {
|
||||
return { status: 'impossible', hint: `Нельзя перевести ${supplier} → ${request}: сравнивайте вручную` };
|
||||
}
|
||||
return { status: 'converted', ratio, hint: `Пересчитано из ${supplier} в ${request}` };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user