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} /> + )}