From 09b1df87c3a2d62a72814cd9ab5250260fce9383 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: Tue, 27 Oct 2026 16:16:23 +0500 Subject: [PATCH 1/2] =?UTF-8?q?feat(ct):=20=C2=AB=D0=A1=D0=BE=D1=85=D1=80?= =?UTF-8?q?=D0=B0=D0=BD=D0=B8=D1=82=D1=8C=20=D1=87=D0=B5=D1=80=D0=BD=D0=BE?= =?UTF-8?q?=D0=B2=D0=B8=D0=BA=C2=BB=20=D0=BD=D0=B0=20=D0=A8=D0=B0=D0=B3?= =?UTF-8?q?=D0=B0=D1=85=202=20=D0=B8=203=20(#8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Несохранённые правки жили в памяти вкладки и пропадали при её закрытии, а «Сохранить и продолжить» на Шаге 3 не пускает без победителя и километража. Черновик пишет шаг в SharePoint так же, без проверок, и оставляет на шаге; рядом — время последнего сохранения. На Шаге 3 черновик не переносит поставщиков, добавленных на шаге, в общий список: таблица держит их в своём, перенос задвоил бы колонку и обнулил бы суммы. Повторные черновики дублей в SP не создают — SP Id уже запомнены. На Шаге 2 черновик доступен, когда заданы курсы валют: они пишутся в общий список «Курс Валют». Co-Authored-By: Claude Opus 5.5 (1M context) --- .../SaveDraftButton.module.scss | 13 +++++ .../SaveDraftButton/SaveDraftButton.tsx | 35 ++++++++++++ .../Step2CommercialOffers.tsx | 26 +++++++-- .../Step3ComparisonTable.tsx | 8 ++- .../step3SavePatch.test.ts | 55 +++++++++++++++++++ .../Step3ComparisonTable/step3SavePatch.ts | 49 +++++++++++++++++ .../Step3ComparisonTable/useStep3Matrix.ts | 43 +++++++-------- 7 files changed, 200 insertions(+), 29 deletions(-) create mode 100644 src/webparts/ctFormsPortal/forms/ct/components/SaveDraftButton/SaveDraftButton.module.scss create mode 100644 src/webparts/ctFormsPortal/forms/ct/components/SaveDraftButton/SaveDraftButton.tsx create mode 100644 src/webparts/ctFormsPortal/forms/ct/components/Step3ComparisonTable/step3SavePatch.test.ts create mode 100644 src/webparts/ctFormsPortal/forms/ct/components/Step3ComparisonTable/step3SavePatch.ts diff --git a/src/webparts/ctFormsPortal/forms/ct/components/SaveDraftButton/SaveDraftButton.module.scss b/src/webparts/ctFormsPortal/forms/ct/components/SaveDraftButton/SaveDraftButton.module.scss new file mode 100644 index 0000000..b29cfcd --- /dev/null +++ b/src/webparts/ctFormsPortal/forms/ct/components/SaveDraftButton/SaveDraftButton.module.scss @@ -0,0 +1,13 @@ +@import '../common/theme'; + +.root { + display: flex; + align-items: center; + gap: 12px; +} + +.savedAt { + font-size: 12px; + color: $muted; + white-space: nowrap; +} diff --git a/src/webparts/ctFormsPortal/forms/ct/components/SaveDraftButton/SaveDraftButton.tsx b/src/webparts/ctFormsPortal/forms/ct/components/SaveDraftButton/SaveDraftButton.tsx new file mode 100644 index 0000000..082965a --- /dev/null +++ b/src/webparts/ctFormsPortal/forms/ct/components/SaveDraftButton/SaveDraftButton.tsx @@ -0,0 +1,35 @@ +import * as React from 'react'; +import { Button, Spinner } from '@tnsu/ui-kit'; +import styles from './SaveDraftButton.module.scss'; + +interface Props { + onSave: () => void; + isSaving: boolean; + disabled?: boolean; + // Подсказка, почему кнопка недоступна. + disabledReason?: string; + // Время последнего сохранения черновика в этой вкладке. + savedAt?: Date; +} + +const TIME_FORMAT: Intl.DateTimeFormatOptions = { hour: '2-digit', minute: '2-digit' }; + +// «Сохранить черновик» на шагах СТ: записать недоделанный шаг в SharePoint без проверок и остаться +// на шаге — чтобы правки не пропали при закрытии вкладки и были видны на другом компьютере. +export const SaveDraftButton: React.FC = ({ onSave, isSaving, disabled = false, disabledReason, savedAt }) => ( +
+ {savedAt && ( + + Черновик сохранён в {savedAt.toLocaleTimeString('ru-RU', TIME_FORMAT)} + + )} + +
+); diff --git a/src/webparts/ctFormsPortal/forms/ct/components/Step2CommercialOffers/Step2CommercialOffers.tsx b/src/webparts/ctFormsPortal/forms/ct/components/Step2CommercialOffers/Step2CommercialOffers.tsx index 15dbc3c..9811ae1 100644 --- a/src/webparts/ctFormsPortal/forms/ct/components/Step2CommercialOffers/Step2CommercialOffers.tsx +++ b/src/webparts/ctFormsPortal/forms/ct/components/Step2CommercialOffers/Step2CommercialOffers.tsx @@ -13,6 +13,7 @@ import { SuppliersWithoutKp } from './SuppliersWithoutKp'; import { UploadedKpList } from './UploadedKpList'; import { removeKpFileFromComparison } from './kpRemoval'; import { applySupplierCurrency, getSupplierCurrency } from './kpCurrency'; +import { SaveDraftButton } from '../SaveDraftButton/SaveDraftButton'; import styles from './Step2CommercialOffers.module.scss'; interface Props { @@ -50,6 +51,7 @@ export const Step2CommercialOffers: React.FC = ({ ); const [isSaving, setIsSaving] = useState(false); const [saveError, setSaveError] = useState(undefined); + const [draftSavedAt, setDraftSavedAt] = useState(undefined); const kp = useKpProcessing(service, formData, onSave); @@ -136,12 +138,18 @@ export const Step2CommercialOffers: React.FC = ({ kp.handleFileRemove(localId); }; - const handleSave = async (): Promise => { + // 'draft' — записать шаг в SP и остаться: распознанные КП попадают в SP только здесь, и без + // черновика пропадали при закрытии вкладки. + const handleSave = async (mode: 'next' | 'draft' = 'next'): Promise => { setIsSaving(true); setSaveError(undefined); try { const patch = await persistStep2({ service, formData, kpFiles: kp.kpFiles, currencyRates, noOfferSuppliers }); onSave(patch); + if (mode === 'draft') { + setDraftSavedAt(new Date()); + return; + } onStepComplete(2); onNext(); } catch (err) { @@ -212,9 +220,19 @@ export const Step2CommercialOffers: React.FC = ({
{saveError && {saveError}} {canEdit && !isReadOnly && ( - + <> + { handleSave('draft').catch(() => undefined /* ошибку показывает handleSave */); }} + isSaving={isSaving} + // Курсы пишутся в общий список «Курс Валют» — нулевой курс туда писать нельзя. + disabled={!canProceed} + disabledReason="Укажите курсы всех валют" + savedAt={draftSavedAt} + /> + + )}
diff --git a/src/webparts/ctFormsPortal/forms/ct/components/Step3ComparisonTable/Step3ComparisonTable.tsx b/src/webparts/ctFormsPortal/forms/ct/components/Step3ComparisonTable/Step3ComparisonTable.tsx index fc9c0e3..882392a 100644 --- a/src/webparts/ctFormsPortal/forms/ct/components/Step3ComparisonTable/Step3ComparisonTable.tsx +++ b/src/webparts/ctFormsPortal/forms/ct/components/Step3ComparisonTable/Step3ComparisonTable.tsx @@ -19,6 +19,7 @@ import { useAdataCheck } from './useAdataCheck'; import { useRecommendedPick } from './useRecommendedPick'; import { VendorPickerDialog } from './VendorPickerDialog'; import { ExcludedSuppliersList } from './ExcludedSuppliersList'; +import { SaveDraftButton } from '../SaveDraftButton/SaveDraftButton'; // Исключённых поставщиков в режиме просмотра (согласующий, отправленная СТ) пока не показываем — // решение от 2026-10-27 (docs/adr/0001). Данные грузятся и так: включить — поменять на true. @@ -57,7 +58,7 @@ export const Step3ComparisonTable: React.FC = ({ const { isReadOnly, newSupplierName, newSupplierCurrency, setNewSupplierName, setNewSupplierCurrency, handleAddSupplier, handleAddSupplierFromDatabase, translateUnits, setTranslateUnits, isSaving, saveError, setSaveError, - validateBeforeSave, handleSave, + validateBeforeSave, handleSave, draftSavedAt, extraPositions, suppliersWithOffer, columnDisplayNames, allPositions, computedOffers, selectedSupplierId, excludedColumns, handleRestoreSupplier, } = matrix; @@ -226,6 +227,9 @@ export const Step3ComparisonTable: React.FC = ({
{saveError && {saveError}} + {!isReadOnly && ( + { handleSave('draft').catch(() => undefined /* ошибку показывает handleSave */); }} isSaving={isSaving} savedAt={draftSavedAt} /> + )}