From 9faff36ba2c5e39acbf83c1f4d603a3ebec1a20d 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, 2 Sep 2026 15:27:21 +0500 Subject: [PATCH] =?UTF-8?q?feat(ct):=20=D0=BA=D0=B0=D1=80=D1=82=D0=BE?= =?UTF-8?q?=D1=87=D0=BA=D0=B0=20INCORE=20=D0=B2=D0=BE=20=D0=B2=D0=BB=D0=BE?= =?UTF-8?q?=D0=B6=D0=B5=D0=BD=D0=B8=D1=8F=D1=85=20=D0=A1=D0=A2,=20=D0=BA?= =?UTF-8?q?=D0=BD=D0=BE=D0=BF=D0=BA=D0=B0=20=D1=82=D0=BE=D0=BB=D1=8C=D0=BA?= =?UTF-8?q?=D0=BE=20=D0=BF=D0=BE=D1=81=D0=BB=D0=B5=20=D1=81=D0=BE=D0=B3?= =?UTF-8?q?=D0=BB=D0=B0=D1=81=D0=BE=D0=B2=D0=B0=D0=BD=D0=B8=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 (1M context) --- .../Step4Approval/Step4Approval.module.scss | 16 +++++++ .../Step4Approval/Step4Approval.tsx | 31 +++++++++--- .../Step4Approval/useStep4Approval.ts | 33 +++++++++++-- .../forms/ct/services/CtService.ts | 48 +++++++++++++++++++ 4 files changed, 118 insertions(+), 10 deletions(-) diff --git a/src/webparts/ctFormsPortal/forms/ct/components/Step4Approval/Step4Approval.module.scss b/src/webparts/ctFormsPortal/forms/ct/components/Step4Approval/Step4Approval.module.scss index 707f733..3f2b74b 100644 --- a/src/webparts/ctFormsPortal/forms/ct/components/Step4Approval/Step4Approval.module.scss +++ b/src/webparts/ctFormsPortal/forms/ct/components/Step4Approval/Step4Approval.module.scss @@ -279,3 +279,19 @@ font-size: 13px; color: $muted; } + +.cardLink { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 8px 12px; + font-size: 13px; + color: $ok; + text-decoration: none; + border: 1px solid $ok; + border-radius: 4px; + + &:hover { + background: $ok-bg; + } +} diff --git a/src/webparts/ctFormsPortal/forms/ct/components/Step4Approval/Step4Approval.tsx b/src/webparts/ctFormsPortal/forms/ct/components/Step4Approval/Step4Approval.tsx index e1e41d9..925d031 100644 --- a/src/webparts/ctFormsPortal/forms/ct/components/Step4Approval/Step4Approval.tsx +++ b/src/webparts/ctFormsPortal/forms/ct/components/Step4Approval/Step4Approval.tsx @@ -266,8 +266,21 @@ export const Step4Approval: React.FC = (props) => { )} - {a.isOnApproval && a.isInitiator && a.ct?.status !== "approved" && ( - <> + {/* INCORE: согласованная СТ — финал. Есть карточка во вложении → показываем файл; + нет → кнопка формирования. Только для инициатора. */} + {a.isInitiator && a.ct?.status === "approved" && !a.isLoadingCard && ( + a.cardAttachment ? ( + + + {a.cardAttachment.name} + + ) : ( - - + ) + )} + + {/* Перезапуск — пока СТ на согласовании (ещё не согласована) */} + {a.isOnApproval && a.isInitiator && a.ct?.status !== "approved" && ( + )} diff --git a/src/webparts/ctFormsPortal/forms/ct/components/Step4Approval/useStep4Approval.ts b/src/webparts/ctFormsPortal/forms/ct/components/Step4Approval/useStep4Approval.ts index b1e81f3..1714f0f 100644 --- a/src/webparts/ctFormsPortal/forms/ct/components/Step4Approval/useStep4Approval.ts +++ b/src/webparts/ctFormsPortal/forms/ct/components/Step4Approval/useStep4Approval.ts @@ -1,4 +1,4 @@ -import { useState, useCallback, useRef } from 'react'; +import { useState, useCallback, useRef, useEffect } from 'react'; import { ApprovalMember, ApprovalMemberStatus, FormStep } from '../types/domain'; import { CtService } from '../../services/CtService'; import { SigexSignService, SigexSignStep } from '../../services/SigexSignService'; @@ -53,6 +53,10 @@ export function useStep4Approval(args: UseStep4ApprovalArgs) { const [isSendingToIncore, setIsSendingToIncore] = useState(false); const [incoreStep, setIncoreStep] = useState(null); const [incoreError, setIncoreError] = useState(null); + // Карточка INCORE во вложениях элемента СТ: если она уже есть — показываем файл, а не кнопку. + const [cardAttachment, setCardAttachment] = useState<{ name: string; url: string } | null>(null); + const [isLoadingCard, setIsLoadingCard] = useState(false); + const incoreCardName = ct ? `Карточка_CT_${ct.id}.pdf` : ''; // ─── Add member ─── const [showAddRow, setShowAddRow] = useState(false); @@ -347,8 +351,12 @@ export function useStep4Approval(args: UseStep4ApprovalArgs) { setIncoreStep, ); - // 3. Пакет для INCORE: карточка с ЭЦП + файлы КП. - service.downloadBlob(cardBlob, `Карточка_CT_${ct.id}.pdf`); + // 3. Архивируем карточку во вложения элемента СТ и переключаем UI на показ файла. + const saved = await service.saveCtCardAttachment(ct.id, cardBlob, incoreCardName); + setCardAttachment(saved); + + // 4. Пакет для INCORE: карточка с ЭЦП + файлы КП — скачиваем пользователю. + service.downloadBlob(cardBlob, incoreCardName); const kpFiles = formData.uploadedKpFiles ?? []; for (const f of kpFiles) { @@ -403,6 +411,23 @@ export function useStep4Approval(args: UseStep4ApprovalArgs) { chain.filter((prev) => prev.order < m.order).every((prev) => prev.status !== 'pending'), ); + // Согласованная СТ — финал: если карточка уже во вложениях, показываем файл вместо кнопки. + // Грузим только для инициатора и только в статусе «Согласована». + useEffect(() => { + if (ct?.status !== 'approved' || !isInitiator) { + setCardAttachment(null); + return; + } + let cancelled = false; + setIsLoadingCard(true); + service + .findCtCardAttachment(ct.id, `Карточка_CT_${ct.id}.pdf`) + .then((found) => { if (!cancelled) setCardAttachment(found); }) + .catch(() => { if (!cancelled) setCardAttachment(null); }) + .finally(() => { if (!cancelled) setIsLoadingCard(false); }); + return () => { cancelled = true; }; + }, [ct?.status, ct?.id, isInitiator, service]); + return { ct, chain, @@ -414,6 +439,8 @@ export function useStep4Approval(args: UseStep4ApprovalArgs) { incoreStep, incoreError, dismissIncoreError: () => setIncoreError(null), + cardAttachment, + isLoadingCard, isOnApproval, showStatuses, isInitiator, diff --git a/src/webparts/ctFormsPortal/forms/ct/services/CtService.ts b/src/webparts/ctFormsPortal/forms/ct/services/CtService.ts index 13f6f67..2329851 100644 --- a/src/webparts/ctFormsPortal/forms/ct/services/CtService.ts +++ b/src/webparts/ctFormsPortal/forms/ct/services/CtService.ts @@ -1536,6 +1536,54 @@ export class CtService { setTimeout(() => URL.revokeObjectURL(url), 1000); } + // Ищет карточку электронного документа (INCORE) во вложениях элемента СТ по имени файла. + async findCtCardAttachment( + ctId: string, + fileName: string, + ): Promise<{ name: string; url: string } | null> { + const res = await this.http.get( + `${this._liById(LISTS.CT, ctId)}/AttachmentFiles`, + SPHttpClient.configurations.v1, + ); + if (!res.ok) return null; + const data = await res.json(); + const files: Array<{ FileName: string; ServerRelativeUrl: string }> = data.value ?? []; + const found = files.find((f) => f.FileName === fileName); + return found ? { name: found.FileName, url: found.ServerRelativeUrl } : null; + } + + // Сохраняет карточку во вложения элемента СТ. Одноимённое вложение перезаписывается. + async saveCtCardAttachment( + ctId: string, + blob: Blob, + fileName: string, + ): Promise<{ name: string; url: string }> { + const itemUrl = this._liById(LISTS.CT, ctId); + const encodedName = encodeURIComponent(fileName); + + const existing = await this.findCtCardAttachment(ctId, fileName); + if (existing) { + await this.http.fetch( + `${itemUrl}/AttachmentFiles/getByFileName('${encodedName}')`, + SPHttpClient.configurations.v1, + { method: "POST", headers: { "X-HTTP-Method": "DELETE", "IF-MATCH": "*" } }, + ); + } + + const buffer = await blob.arrayBuffer(); + const res = await this.http.post( + `${itemUrl}/AttachmentFiles/add(FileName='${encodedName}')`, + SPHttpClient.configurations.v1, + { + headers: { Accept: "application/json;odata=nometadata" }, + body: buffer as unknown as string, + }, + ); + if (!res.ok) throw new Error(`Ошибка сохранения карточки во вложения: ${res.status}`); + const data = await res.json(); + return { name: data.FileName ?? fileName, url: data.ServerRelativeUrl }; + } + async extractWordText(itemId: number): Promise { const token = await this._getApiToken(); const res = await fetch(`${EXTERNAL_API}/Document/extractWordText`, {