From 2eaa75e876e3aabe32da181c7835eb726a2d9415 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A2=D0=B8=D0=BC=D1=83=D1=80=20=D0=93=D0=BE=D1=80=D0=BE?= =?UTF-8?q?=D0=B4=D0=B5=D1=86=D0=BA=D0=B8=D0=B9?= Date: Wed, 28 Oct 2026 08:59:10 +0500 Subject: [PATCH] =?UTF-8?q?feat(ct):=20=D0=B7=D0=B0=D0=BC=D0=B5=D1=82?= =?UTF-8?q?=D0=BD=D0=BE=D0=B5=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D0=B5=20=D0=BF=D0=BE=D1=82=D0=B5=D0=BD=D1=86=D0=B8?= =?UTF-8?q?=D0=B0=D0=BB=D1=8C=D0=BD=D0=BE=D0=B3=D0=BE=20=D0=BF=D0=BE=D1=81?= =?UTF-8?q?=D1=82=D0=B0=D0=B2=D1=89=D0=B8=D0=BA=D0=B0=20=D0=BD=D0=B0=20?= =?UTF-8?q?=D0=A8=D0=B0=D0=B3=D0=B5=203=20(#12)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Блок добавления не замечали: поле без подписи и серая заблокированная кнопка читались как «не работает», а сам блок стоял в зоне настроек. Теперь у блока заголовок и подсказка, кнопка «Добавить» не блокируется (с пустым названием подсказывает, что ввести), «Из базы поставщиков» рядом. И вторая точка входа там, где ищут, — колонка «+ Добавить поставщика» в шапке таблицы с окном «вручную или из базы». Добавление вручную — одна функция на обе кнопки. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../Step3ComparisonTable/AddExtraRow.tsx | 2 + .../AddSupplierDialog.tsx | 69 ++++++++++++ .../AddSupplierToolbar.tsx | 82 ++++++++++++++ .../Step3ComparisonTable/ComparisonMatrix.tsx | 21 +++- .../Step3ComparisonTable/ExtraPositionRow.tsx | 2 + .../Step3ComparisonTable/MatrixRowView.tsx | 2 + .../Step3ComparisonTable.module.scss | 105 +++++++++++++++--- .../Step3ComparisonTable.tsx | 64 +++++------ .../Step3ComparisonTable/useStep3Matrix.ts | 18 ++- 9 files changed, 313 insertions(+), 52 deletions(-) create mode 100644 src/webparts/ctFormsPortal/forms/ct/components/Step3ComparisonTable/AddSupplierDialog.tsx create mode 100644 src/webparts/ctFormsPortal/forms/ct/components/Step3ComparisonTable/AddSupplierToolbar.tsx diff --git a/src/webparts/ctFormsPortal/forms/ct/components/Step3ComparisonTable/AddExtraRow.tsx b/src/webparts/ctFormsPortal/forms/ct/components/Step3ComparisonTable/AddExtraRow.tsx index 47361fa..14a83a0 100644 --- a/src/webparts/ctFormsPortal/forms/ct/components/Step3ComparisonTable/AddExtraRow.tsx +++ b/src/webparts/ctFormsPortal/forms/ct/components/Step3ComparisonTable/AddExtraRow.tsx @@ -59,6 +59,8 @@ export function AddExtraRow({ matrix, isRestrictedView, colSpan }: Props): JSX.E {suppliersWithOffer.map(s => ( ))} + {/* Под колонкой «+ Добавить поставщика» (видна там же, где эта строка, — не в режиме просмотра). */} + ); } diff --git a/src/webparts/ctFormsPortal/forms/ct/components/Step3ComparisonTable/AddSupplierDialog.tsx b/src/webparts/ctFormsPortal/forms/ct/components/Step3ComparisonTable/AddSupplierDialog.tsx new file mode 100644 index 0000000..933faf0 --- /dev/null +++ b/src/webparts/ctFormsPortal/forms/ct/components/Step3ComparisonTable/AddSupplierDialog.tsx @@ -0,0 +1,69 @@ +import * as React from 'react'; +import { useState } from 'react'; +import { Button, Dialog, Icon, TextInput } from '@tnsu/ui-kit'; +import styles from './Step3ComparisonTable.module.scss'; + +interface Props { + currencyOptions: string[]; + onAddManual: (name: string, currency: string) => void; + onOpenDatabase: () => void; + onClose: () => void; +} + +const DEFAULT_CURRENCY = 'KZT'; + +// Окно кнопки «+ Добавить поставщика» в шапке таблицы: вручную (название и валюта) или из базы +// поставщиков. Добавление — те же функции, что у блока над таблицей. +export const AddSupplierDialog: React.FC = ({ currencyOptions, onAddManual, onOpenDatabase, onClose }) => { + const [name, setName] = useState(''); + const [currency, setCurrency] = useState(DEFAULT_CURRENCY); + const canAdd = name.trim().length > 0; + + const handleAdd = (): void => { + if (!canAdd) return; + onAddManual(name, currency); + onClose(); + }; + + return ( + Отмена} + > +
+
+
+ { if (e.key === 'Enter') handleAdd(); }} + /> +
+ + +
+
или
+ +
+
+ ); +}; diff --git a/src/webparts/ctFormsPortal/forms/ct/components/Step3ComparisonTable/AddSupplierToolbar.tsx b/src/webparts/ctFormsPortal/forms/ct/components/Step3ComparisonTable/AddSupplierToolbar.tsx new file mode 100644 index 0000000..b0ee3d7 --- /dev/null +++ b/src/webparts/ctFormsPortal/forms/ct/components/Step3ComparisonTable/AddSupplierToolbar.tsx @@ -0,0 +1,82 @@ +import * as React from 'react'; +import { useState } from 'react'; +import { Button, Icon, TextInput } from '@tnsu/ui-kit'; +import styles from './Step3ComparisonTable.module.scss'; + +interface Props { + name: string; + currency: string; + currencyOptions: string[]; + onNameChange: (value: string) => void; + onCurrencyChange: (value: string) => void; + onAdd: () => void; + onOpenDatabase: () => void; +} + +const NAME_INPUT_ID = 'step3-new-supplier-name'; + +// Блок «Потенциальные поставщики» над таблицей. Раньше это были поле, выпадашка и серая кнопка без +// подписи — их не замечали: заблокированная кнопка читалась как «не работает». Теперь у блока есть +// заголовок, кнопка не блокируется: с пустым названием подсказывает, что ввести. +export const AddSupplierToolbar: React.FC = ({ + name, currency, currencyOptions, onNameChange, onCurrencyChange, onAdd, onOpenDatabase, +}) => { + const [isNameMissing, setIsNameMissing] = useState(false); + + const handleAdd = (): void => { + if (!name.trim()) { + setIsNameMissing(true); + // Поле — в ui-kit, ref он не отдаёт; id задаём сами. + document.getElementById(NAME_INPUT_ID)?.focus(); + return; + } + setIsNameMissing(false); + onAdd(); + }; + + const handleNameChange = (value: string): void => { + if (value.trim()) setIsNameMissing(false); + onNameChange(value); + }; + + return ( +
+
+

Потенциальные поставщики

+ + Добавьте поставщика, у которого нет загруженного КП, — вручную или из базы поставщиков + +
+
+
+ { if (e.key === 'Enter') handleAdd(); }} + /> +
+ + + или + +
+ {isNameMissing && Введите наименование поставщика} +
+ ); +}; diff --git a/src/webparts/ctFormsPortal/forms/ct/components/Step3ComparisonTable/ComparisonMatrix.tsx b/src/webparts/ctFormsPortal/forms/ct/components/Step3ComparisonTable/ComparisonMatrix.tsx index d9cfa79..648b995 100644 --- a/src/webparts/ctFormsPortal/forms/ct/components/Step3ComparisonTable/ComparisonMatrix.tsx +++ b/src/webparts/ctFormsPortal/forms/ct/components/Step3ComparisonTable/ComparisonMatrix.tsx @@ -19,12 +19,14 @@ interface Props { isRestrictedView: boolean; adata: UseAdataCheckResult; recommendedId?: string; // победитель формулы 80% цена / 20% условия оплаты (useRecommendedPick) + // Кнопка «+ Добавить поставщика» в последней колонке шапки. + onAddSupplierClick: () => void; } // Таблица сравнения целиком: шапка (фикс-колонки + заголовки поставщиков) + строки позиций // (MatrixRowView/ExtraPositionRow) + итоговые строки + строка добавления (AddExtraRow). // Перенесено 1:1 из Step3ComparisonTable (§стайл-гайд: чистая вёрстка, логика — в useStep3Matrix). -export function ComparisonMatrix({ matrix, currencyRates, isRestrictedView, adata, recommendedId }: Props): JSX.Element { +export function ComparisonMatrix({ matrix, currencyRates, isRestrictedView, adata, recommendedId, onAddSupplierClick }: Props): JSX.Element { const { isReadOnly, suppliersWithOffer, supplierCurrencies, selectedSupplierId, supplierStatuses, nameColWidth, regularPositions, extraPositionsRows, allPositions, computedOffers, @@ -37,7 +39,12 @@ export function ComparisonMatrix({ matrix, currencyRates, isRestrictedView, adat // Как в проверке перед сохранением (getStep3SaveError): выбор на исключённом — не победитель. const hasWinner = suppliersWithOffer.some(s => s.id === selectedSupplierId); - const totalColSpan = FIXED_COLS + suppliersWithOffer.reduce((sum, s) => sum + getSubCols(supplierCurrencies[s.id] ?? 'KZT', isRestrictedView), 0); + // Последняя колонка «+ Добавить поставщика» — там, где ищут, как добавить колонку. Во всех строках + // под ней пустая ячейка (addSupplierFillerCell), иначе строки таблицы были бы разной длины. + const showAddColumn = !isReadOnly; + const totalColSpan = FIXED_COLS + + suppliersWithOffer.reduce((sum, s) => sum + getSubCols(supplierCurrencies[s.id] ?? 'KZT', isRestrictedView), 0) + + (showAddColumn ? 1 : 0); // Высота row1 (шапка поставщика) переменная (теги валют, выбор поставщика, состояние Adata), // а row2 прибит sticky сразу под ней. Измеряем реальную высоту и отдаём в CSS-переменную, @@ -142,6 +149,13 @@ export function ComparisonMatrix({ matrix, currencyRates, isRestrictedView, adat ); })} + {showAddColumn && ( + + + + )} {/* Row 2: sub-headers — 4 cols for KZT, 6 cols for foreign currency */} @@ -206,6 +220,7 @@ export function ComparisonMatrix({ matrix, currencyRates, isRestrictedView, adat ); })} + {showAddColumn && } {/* Доп. расходы positions — single row each (no bundle support needed) */} @@ -242,6 +257,7 @@ export function ComparisonMatrix({ matrix, currencyRates, isRestrictedView, adat ); })} + {showAddColumn && } )} @@ -265,6 +281,7 @@ export function ComparisonMatrix({ matrix, currencyRates, isRestrictedView, adat ); })} + {showAddColumn && } {/* Inline add-position row */} diff --git a/src/webparts/ctFormsPortal/forms/ct/components/Step3ComparisonTable/ExtraPositionRow.tsx b/src/webparts/ctFormsPortal/forms/ct/components/Step3ComparisonTable/ExtraPositionRow.tsx index 0eb1c98..2180a4f 100644 --- a/src/webparts/ctFormsPortal/forms/ct/components/Step3ComparisonTable/ExtraPositionRow.tsx +++ b/src/webparts/ctFormsPortal/forms/ct/components/Step3ComparisonTable/ExtraPositionRow.tsx @@ -234,6 +234,8 @@ export function ExtraPositionRow({ pos, posIndex, regularPositionsCount, matrix, ); })} + {/* Под колонкой «+ Добавить поставщика». */} + {!isReadOnly && } ); } diff --git a/src/webparts/ctFormsPortal/forms/ct/components/Step3ComparisonTable/MatrixRowView.tsx b/src/webparts/ctFormsPortal/forms/ct/components/Step3ComparisonTable/MatrixRowView.tsx index 3a57132..a2301e7 100644 --- a/src/webparts/ctFormsPortal/forms/ct/components/Step3ComparisonTable/MatrixRowView.tsx +++ b/src/webparts/ctFormsPortal/forms/ct/components/Step3ComparisonTable/MatrixRowView.tsx @@ -218,6 +218,8 @@ export function MatrixRowView({ pos, posIndex, matrix, currencyRates, isRestrict ); })} + {/* Под колонкой «+ Добавить поставщика»: одна ячейка на позицию, включая строки комплекта. */} + {rowIdx === 0 && !isReadOnly && } ); } diff --git a/src/webparts/ctFormsPortal/forms/ct/components/Step3ComparisonTable/Step3ComparisonTable.module.scss b/src/webparts/ctFormsPortal/forms/ct/components/Step3ComparisonTable/Step3ComparisonTable.module.scss index 41e967f..0a4cf1e 100644 --- a/src/webparts/ctFormsPortal/forms/ct/components/Step3ComparisonTable/Step3ComparisonTable.module.scss +++ b/src/webparts/ctFormsPortal/forms/ct/components/Step3ComparisonTable/Step3ComparisonTable.module.scss @@ -866,20 +866,6 @@ $delivery-km-min-width: 76px; color: $text; } -.supplierToolbar { - display: flex; - align-items: center; - gap: 8px; - flex-wrap: wrap; - padding: 4px 0 8px; - max-width: 30%; - - > :first-child { - flex: 1 1 0; - min-width: 160px; - } -} - .addRow { display: flex; align-items: flex-end; @@ -1369,3 +1355,94 @@ $delivery-km-min-width: 76px; border-color: $danger; color: $danger; } + +// ─── Добавление потенциального поставщика ─────────────────────────────────── +// Блок над таблицей (AddSupplierToolbar): раньше без заголовка и с серой кнопкой — его не замечали. + +.addSupplierBlock { + display: flex; + flex-direction: column; + gap: 8px; + padding: 12px 16px; + border: 1px solid $accent-border; + border-radius: 4px; + background: $accent-bg; +} + +.addSupplierHeading { + display: flex; + flex-wrap: wrap; + align-items: baseline; + gap: 4px 12px; +} + +.addSupplierTitle { + margin: 0; + font-size: 14px; + font-weight: 600; + color: $accent-dark; +} + +.addSupplierHint { + font-size: 12px; + color: $muted; +} + +.addSupplierRow { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 8px; +} + +.addSupplierOr { + font-size: 12px; + color: $muted; +} + +.addSupplierError { + font-size: 12px; + color: $danger; +} + +.addSupplierDialogBody { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 12px; + + > .addSupplierRow { align-self: stretch; } +} + +.addSupplierDivider { + font-size: 12px; + color: $muted; +} + +// Последняя колонка таблицы «+ Добавить поставщика» — там, где ищут, как добавить колонку. +.addSupplierHeaderCell { + min-width: 140px; + vertical-align: middle; + text-align: center; + background: $head-bg; + border-left: 2px dashed $accent-border !important; +} + +.addSupplierHeaderBtn { + padding: 8px 12px; + border: 1px dashed $accent; + border-radius: 4px; + background: $surface; + color: $accent; + font-family: inherit; + font-size: 13px; + font-weight: 600; + white-space: nowrap; + cursor: pointer; + + &:hover { background: $accent-bg; } +} + +.addSupplierFillerCell { + border-left: 2px dashed $accent-border !important; +} diff --git a/src/webparts/ctFormsPortal/forms/ct/components/Step3ComparisonTable/Step3ComparisonTable.tsx b/src/webparts/ctFormsPortal/forms/ct/components/Step3ComparisonTable/Step3ComparisonTable.tsx index 5b44902..3144c8a 100644 --- a/src/webparts/ctFormsPortal/forms/ct/components/Step3ComparisonTable/Step3ComparisonTable.tsx +++ b/src/webparts/ctFormsPortal/forms/ct/components/Step3ComparisonTable/Step3ComparisonTable.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import { useMemo, useState } from 'react'; -import { Button, Alert, TextInput, Toggle, Spinner, Dialog, Icon } from '@tnsu/ui-kit'; +import { Button, Alert, Toggle, Spinner, Dialog, Icon } from '@tnsu/ui-kit'; import { FormStep } from '../types/domain'; import { CtService } from '../../services/CtService'; import { VendorDatabaseService, IVendorRecord } from '../../services/VendorDatabaseService'; @@ -19,6 +19,8 @@ import { useAdataCheck } from './useAdataCheck'; import { useRecommendedPick } from './useRecommendedPick'; import { VendorPickerDialog } from './VendorPickerDialog'; import { ExcludedSuppliersList } from './ExcludedSuppliersList'; +import { AddSupplierToolbar } from './AddSupplierToolbar'; +import { AddSupplierDialog } from './AddSupplierDialog'; import { SaveDraftButton } from '../SaveDraftButton/SaveDraftButton'; import { withoutPlaceholders } from '../utils/placeholderKpFile'; @@ -58,7 +60,7 @@ export const Step3ComparisonTable: React.FC = ({ const matrix = useStep3Matrix({ service, formData, isReadOnly: !canEdit, onSave, onNext, onStepComplete }); const { isReadOnly, newSupplierName, newSupplierCurrency, setNewSupplierName, setNewSupplierCurrency, - handleAddSupplier, handleAddSupplierFromDatabase, translateUnits, setTranslateUnits, isSaving, saveError, setSaveError, + addManualSupplier, handleAddSupplier, handleAddSupplierFromDatabase, translateUnits, setTranslateUnits, isSaving, saveError, setSaveError, validateBeforeSave, handleSave, draftSavedAt, extraPositions, suppliersWithOffer, columnDisplayNames, allPositions, computedOffers, selectedSupplierId, excludedColumns, handleRestoreSupplier, @@ -70,6 +72,8 @@ export const Step3ComparisonTable: React.FC = ({ // Модалка выбора поставщика из «База данных поставщиков» (см. VendorPickerDialog). const [showVendorPicker, setShowVendorPicker] = useState(false); + // Окно кнопки «+ Добавить поставщика» в шапке таблицы. + const [showAddSupplierDialog, setShowAddSupplierDialog] = useState(false); const vendorDbService = useMemo(() => new VendorDatabaseService(service.webPartContext), [service]); const handleAddVendorFromDatabase = (vendor: IVendorRecord): void => { const id = handleAddSupplierFromDatabase(vendor); @@ -117,35 +121,17 @@ export const Step3ComparisonTable: React.FC = ({ return (
- {/* Toolbar: add supplier — always visible above the table */} + {/* Добавление потенциального поставщика: блок над таблицей + кнопка в шапке таблицы. */} {!isReadOnly && ( -
-
- { if (e.key === 'Enter') handleAddSupplier(); }} - /> -
- - - -
+ setShowVendorPicker(true)} + /> )} {/* Тумблер пересчёта единиц измерения поставщика в ед. изм. заявки */} @@ -185,7 +171,14 @@ export const Step3ComparisonTable: React.FC = ({ {recPick.pickError && {recPick.pickError}} - + setShowAddSupplierDialog(true)} + /> @@ -269,6 +262,15 @@ export const Step3ComparisonTable: React.FC = ({
+ {showAddSupplierDialog && ( + setShowVendorPicker(true)} + onClose={() => setShowAddSupplierDialog(false)} + /> + )} + {showVendorPicker && ( { - if (!newSupplierName.trim()) return; + // Поставщик, добавленный на Шаге 3 вручную (без КП). Общая точка для обеих кнопок добавления: + // блока над таблицей и колонки «+ Добавить поставщика» в самой таблице. + const addManualSupplier = (name: string, currency: string): void => { + if (isReadOnly || !name.trim()) return; const id = `extra_sup_${++extraIdCounter}`; - const newEntry = { id, name: newSupplierName.trim(), currency: newSupplierCurrency }; - const updated = [...extraSuppliers, newEntry]; + const updated = [...extraSuppliers, { id, name: name.trim(), currency }]; setExtraSuppliers(updated); setSupplierStatuses(prev => ({ ...prev, [id]: '' })); + onSave({ step3ExtraSuppliers: updated }); + }; + + // Блок над таблицей: добавить из своих полей и очистить их. + const handleAddSupplier = (): void => { + if (!newSupplierName.trim()) return; + addManualSupplier(newSupplierName, newSupplierCurrency); setNewSupplierName(''); setNewSupplierCurrency('KZT'); - onSave({ step3ExtraSuppliers: updated }); }; // Добавление поставщика из VendorPickerDialog («База данных поставщиков»): та же механика @@ -1000,6 +1007,7 @@ export function useStep3Matrix(args: UseStep3MatrixArgs) { handleRemoveCustomCriteria, handleNameColResizeMouseDown, handleStatusChange, + addManualSupplier, handleAddSupplier, handleAddSupplierFromDatabase, handleAddPosition,