feat(ct): карточка INCORE во вложениях СТ, кнопка только после согласования
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+16
@@ -279,3 +279,19 @@
|
|||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
color: $muted;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -266,8 +266,21 @@ export const Step4Approval: React.FC<Props> = (props) => {
|
|||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{a.isOnApproval && a.isInitiator && a.ct?.status !== "approved" && (
|
{/* INCORE: согласованная СТ — финал. Есть карточка во вложении → показываем файл;
|
||||||
<>
|
нет → кнопка формирования. Только для инициатора. */}
|
||||||
|
{a.isInitiator && a.ct?.status === "approved" && !a.isLoadingCard && (
|
||||||
|
a.cardAttachment ? (
|
||||||
|
<a
|
||||||
|
className={styles.cardLink}
|
||||||
|
href={a.cardAttachment.url}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
title="Открыть карточку электронного документа"
|
||||||
|
>
|
||||||
|
<Icon name="download" size="sm" />
|
||||||
|
{a.cardAttachment.name}
|
||||||
|
</a>
|
||||||
|
) : (
|
||||||
<Button variant="secondary" onClick={a.handleSendToIncore} disabled={a.isSendingToIncore}>
|
<Button variant="secondary" onClick={a.handleSendToIncore} disabled={a.isSendingToIncore}>
|
||||||
{a.isSendingToIncore ? (
|
{a.isSendingToIncore ? (
|
||||||
<Spinner size="small" />
|
<Spinner size="small" />
|
||||||
@@ -278,11 +291,15 @@ export const Step4Approval: React.FC<Props> = (props) => {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Перезапуск — пока СТ на согласовании (ещё не согласована) */}
|
||||||
|
{a.isOnApproval && a.isInitiator && a.ct?.status !== "approved" && (
|
||||||
<Button variant="secondary" onClick={a.handleRestart} disabled={a.isSubmitting}>
|
<Button variant="secondary" onClick={a.handleRestart} disabled={a.isSubmitting}>
|
||||||
<Icon name="refresh" size="sm" />
|
<Icon name="refresh" size="sm" />
|
||||||
Перезапустить согласование
|
Перезапустить согласование
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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 { ApprovalMember, ApprovalMemberStatus, FormStep } from '../types/domain';
|
||||||
import { CtService } from '../../services/CtService';
|
import { CtService } from '../../services/CtService';
|
||||||
import { SigexSignService, SigexSignStep } from '../../services/SigexSignService';
|
import { SigexSignService, SigexSignStep } from '../../services/SigexSignService';
|
||||||
@@ -53,6 +53,10 @@ export function useStep4Approval(args: UseStep4ApprovalArgs) {
|
|||||||
const [isSendingToIncore, setIsSendingToIncore] = useState(false);
|
const [isSendingToIncore, setIsSendingToIncore] = useState(false);
|
||||||
const [incoreStep, setIncoreStep] = useState<IncoreStep | null>(null);
|
const [incoreStep, setIncoreStep] = useState<IncoreStep | null>(null);
|
||||||
const [incoreError, setIncoreError] = useState<string | null>(null);
|
const [incoreError, setIncoreError] = useState<string | null>(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 ───
|
// ─── Add member ───
|
||||||
const [showAddRow, setShowAddRow] = useState(false);
|
const [showAddRow, setShowAddRow] = useState(false);
|
||||||
@@ -347,8 +351,12 @@ export function useStep4Approval(args: UseStep4ApprovalArgs) {
|
|||||||
setIncoreStep,
|
setIncoreStep,
|
||||||
);
|
);
|
||||||
|
|
||||||
// 3. Пакет для INCORE: карточка с ЭЦП + файлы КП.
|
// 3. Архивируем карточку во вложения элемента СТ и переключаем UI на показ файла.
|
||||||
service.downloadBlob(cardBlob, `Карточка_CT_${ct.id}.pdf`);
|
const saved = await service.saveCtCardAttachment(ct.id, cardBlob, incoreCardName);
|
||||||
|
setCardAttachment(saved);
|
||||||
|
|
||||||
|
// 4. Пакет для INCORE: карточка с ЭЦП + файлы КП — скачиваем пользователю.
|
||||||
|
service.downloadBlob(cardBlob, incoreCardName);
|
||||||
|
|
||||||
const kpFiles = formData.uploadedKpFiles ?? [];
|
const kpFiles = formData.uploadedKpFiles ?? [];
|
||||||
for (const f of kpFiles) {
|
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'),
|
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 {
|
return {
|
||||||
ct,
|
ct,
|
||||||
chain,
|
chain,
|
||||||
@@ -414,6 +439,8 @@ export function useStep4Approval(args: UseStep4ApprovalArgs) {
|
|||||||
incoreStep,
|
incoreStep,
|
||||||
incoreError,
|
incoreError,
|
||||||
dismissIncoreError: () => setIncoreError(null),
|
dismissIncoreError: () => setIncoreError(null),
|
||||||
|
cardAttachment,
|
||||||
|
isLoadingCard,
|
||||||
isOnApproval,
|
isOnApproval,
|
||||||
showStatuses,
|
showStatuses,
|
||||||
isInitiator,
|
isInitiator,
|
||||||
|
|||||||
@@ -1536,6 +1536,54 @@ export class CtService {
|
|||||||
setTimeout(() => URL.revokeObjectURL(url), 1000);
|
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<void> {
|
async extractWordText(itemId: number): Promise<void> {
|
||||||
const token = await this._getApiToken();
|
const token = await this._getApiToken();
|
||||||
const res = await fetch(`${EXTERNAL_API}/Document/extractWordText`, {
|
const res = await fetch(`${EXTERNAL_API}/Document/extractWordText`, {
|
||||||
|
|||||||
Reference in New Issue
Block a user