From 911ccfbcf44f8d00a37b900d79b3a2d0ca3af5d3 Mon Sep 17 00:00:00 2001 From: "gorbenko.pavel" Date: Fri, 2 Oct 2026 17:00:43 +0500 Subject: [PATCH] =?UTF-8?q?=D0=9E=D1=82=D0=BF=D1=83=D1=81=D0=BA:=20=D0=B4?= =?UTF-8?q?=D0=BE=D0=BA=D1=83=D0=BC=D0=B5=D0=BD=D1=82=D1=8B=20=D0=BA=D0=BE?= =?UTF-8?q?=D0=BC=D0=BF=D0=B0=D0=BD=D0=B8=D0=B9=20=D0=B1=D0=B5=D0=B7=20?= =?UTF-8?q?=D1=81=D0=B5=D1=80=D1=82=D0=B8=D1=84=D0=B8=D0=BA=D0=B0=D1=82?= =?UTF-8?q?=D0=B0=20Sigex=20=D1=81=D0=BE=D1=85=D1=80=D0=B0=D0=BD=D1=8F?= =?UTF-8?q?=D1=8E=D1=82=D1=81=D1=8F=20=D0=B2=20=D0=AD=D0=94=D0=9E=20=D0=B8?= =?UTF-8?q?=20=D0=B6=D0=B4=D1=83=D1=82=20=D0=B5=D0=B3=D0=BE=20=D0=B7=D0=B0?= =?UTF-8?q?=D0=B3=D1=80=D1=83=D0=B7=D0=BA=D0=B8,=20=D1=81=D0=BE=D0=BE?= =?UTF-8?q?=D0=B1=D1=89=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=BE=D0=B1=20=D1=8D?= =?UTF-8?q?=D1=82=D0=BE=D0=BC=20=D0=B2=20=C2=AB=D0=A0=D0=B5=D1=88=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D0=B5=20=D1=81=D0=BE=D1=85=D1=80=D0=B0=D0=BD=D0=B5?= =?UTF-8?q?=D0=BD=D0=BE=C2=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 5.5 --- .../requestPortal/forms/Vacation/README.md | 4 +++- .../forms/Vacation/VacationView.tsx | 3 ++- .../forms/Vacation/sendToSign.ts | 15 ++++++++++-- .../forms/Vacation/signingMessage.ts | 23 ++++++++++++++++++ .../requestPortal/shared/edo/edo.service.ts | 18 +++++++++++--- .../requestPortal/shared/edo/edo.test.ts | 24 +++++++++++++++++++ 6 files changed, 80 insertions(+), 7 deletions(-) create mode 100644 src/webparts/requestPortal/forms/Vacation/signingMessage.ts diff --git a/src/webparts/requestPortal/forms/Vacation/README.md b/src/webparts/requestPortal/forms/Vacation/README.md index 33e4cd6..2f9db15 100644 --- a/src/webparts/requestPortal/forms/Vacation/README.md +++ b/src/webparts/requestPortal/forms/Vacation/README.md @@ -45,7 +45,9 @@ массиве `file_company_bins`, общий `company_bin` не передаётся. Подписант один — сам сотрудник, от имени физического лица; БИН нужны ЭДО только чтобы выбрать сертификат Sigex (mTLS) нужной компании. Запрос один на все файлы: ЭДО принимает его целиком или отклоняет целиком (например, если у какой-то - компании нет сертификата), поэтому повтор безопасен. + компании не распознан), поэтому повтор безопасен. Если у компании нет сертификата Sigex, ЭДО всё равно + сохраняет её документ (`awaiting_certificate: true`), но подписать его нельзя, пока администратор не + загрузит сертификат — тогда подписание откроется само. В зелёном сообщении такие компании названы отдельно. - **ИИН** берётся из `GET /Employee/ByEmail` (поле `Iin`), **БИН компаний** — из `GET /Dictionary/Companies` по `CompanyId` каждой записи сотрудника (`shared/edo/signingTarget.ts`). Компания без БИН в справочнике пропускается и показывается в сообщении; остальные уходят. diff --git a/src/webparts/requestPortal/forms/Vacation/VacationView.tsx b/src/webparts/requestPortal/forms/Vacation/VacationView.tsx index 77fe59a..073b0b2 100644 --- a/src/webparts/requestPortal/forms/Vacation/VacationView.tsx +++ b/src/webparts/requestPortal/forms/Vacation/VacationView.tsx @@ -28,6 +28,7 @@ import { FORM_ID, IFormData, EMPTY_DATA, SetField } from './model'; import { loadVacationRequest } from './load'; import { vacationApprovalService } from './submit'; import { IVacationSigningResult, sendVacationForSigning } from './sendToSign'; +import { describeSigning } from './signingMessage'; import { RequestSection } from './sections/RequestSection'; import { useStableCallback } from '../../shared/useStableCallback'; @@ -154,7 +155,7 @@ const VacationView: React.FC = ({ context, itemId, formKey, return sendVacationForSigning(context, { data, employeeName, itemId, code }) .then((result): string | undefined => { setSigningResult(result); - return `Документы отправлены на подписание в ЭДО (${result.sent.join(', ')}): ссылка для подписания ушла на ${data.Employee}.`; + return describeSigning(result, data.Employee); }) .catch((e): string | undefined => { setSigningError( diff --git a/src/webparts/requestPortal/forms/Vacation/sendToSign.ts b/src/webparts/requestPortal/forms/Vacation/sendToSign.ts index 42b8d48..414ccfd 100644 --- a/src/webparts/requestPortal/forms/Vacation/sendToSign.ts +++ b/src/webparts/requestPortal/forms/Vacation/sendToSign.ts @@ -21,6 +21,11 @@ export interface IVacationToSign { export interface IVacationSigningResult { /** Названия компаний, по которым заявление отправлено на подпись. */ sent: string[]; + /** + * Компании, у которых в ЭДО нет сертификата Sigex: заявление там сохранено, но подписать его нельзя, + * пока администратор не загрузит сертификат. Подписание откроется само — это подмножество `sent`. + */ + awaitingCertificate: string[]; /** Компании сотрудника, по которым документ не собрать (нет БИН в справочнике), и почему. */ skipped: ISkippedCompany[]; } @@ -100,11 +105,17 @@ export async function sendVacationForSigning( })) ); - await submitSigningRequest(context, { + const accepted = await submitSigningRequest(context, { recipient: { iin: targets.iin, email: data.Employee, name: targets.name }, initiatorEmail: data.Employee, files }); - return { sent: targets.companies.map((c) => c.name), skipped: targets.skipped }; + const waitingBins = accepted.documents.filter((d) => d.awaitingCertificate).map((d) => d.companyBin); + + return { + sent: targets.companies.map((c) => c.name), + awaitingCertificate: targets.companies.filter((c) => waitingBins.indexOf(c.bin) >= 0).map((c) => c.name), + skipped: targets.skipped + }; } diff --git a/src/webparts/requestPortal/forms/Vacation/signingMessage.ts b/src/webparts/requestPortal/forms/Vacation/signingMessage.ts new file mode 100644 index 0000000..6c556b7 --- /dev/null +++ b/src/webparts/requestPortal/forms/Vacation/signingMessage.ts @@ -0,0 +1,23 @@ +import type { IVacationSigningResult } from './sendToSign'; + +/** + * Фраза для зелёного сообщения «Решение сохранено…»: что ушло на подпись, а что сохранено и ждёт + * сертификата Sigex своей компании (подписание откроется после его загрузки). + */ +export function describeSigning(result: IVacationSigningResult, employeeEmail: string): string { + const ready = result.sent.filter((name) => result.awaitingCertificate.indexOf(name) < 0); + const parts: string[] = []; + + if (ready.length > 0) { + parts.push(`Документы отправлены на подписание в ЭДО (${ready.join(', ')}): ссылка для подписания ушла на ${employeeEmail}.`); + } + + if (result.awaitingCertificate.length > 0) { + parts.push( + `Документы сохранены в ЭДО, но ждут загрузки сертификата Sigex по компаниям: ${result.awaitingCertificate.join(', ')}. ` + + `Подписание откроется само, когда администратор загрузит сертификат; ссылка придёт на ${employeeEmail}.` + ); + } + + return parts.join(' '); +} diff --git a/src/webparts/requestPortal/shared/edo/edo.service.ts b/src/webparts/requestPortal/shared/edo/edo.service.ts index 2fc4d0f..7034ce9 100644 --- a/src/webparts/requestPortal/shared/edo/edo.service.ts +++ b/src/webparts/requestPortal/shared/edo/edo.service.ts @@ -28,13 +28,20 @@ export interface IEdoSigningRequest { export interface IEdoSigningResult { /** Кабинет получателя без ключа: войти по ЭЦП или через Microsoft. */ cabinetUrl: string; - documents: Array<{ id: string; fileName: string }>; + documents: Array<{ + id: string; + fileName: string; + /** БИН компании файла. */ + companyBin: string; + /** У компании нет сертификата Sigex: файл сохранён, но подписать его нельзя до загрузки сертификата. */ + awaitingCertificate: boolean; + }>; } /** Ответ ЭДО в snake_case — только поля, которые читаем. */ interface IEdoResponseBody { cabinet_url?: string; - documents?: Array<{ id?: string; file_name?: string }>; + documents?: Array<{ id?: string; file_name?: string; company_bin?: string; awaiting_certificate?: boolean }>; } /** @@ -84,6 +91,11 @@ export async function submitSigningRequest( const body = (await response.json()) as IEdoResponseBody; return { cabinetUrl: body.cabinet_url || '', - documents: (body.documents || []).map((d) => ({ id: d.id || '', fileName: d.file_name || '' })) + documents: (body.documents || []).map((d) => ({ + id: d.id || '', + fileName: d.file_name || '', + companyBin: d.company_bin || '', + awaitingCertificate: d.awaiting_certificate === true + })) }; } diff --git a/src/webparts/requestPortal/shared/edo/edo.test.ts b/src/webparts/requestPortal/shared/edo/edo.test.ts index 4174a45..6437667 100644 --- a/src/webparts/requestPortal/shared/edo/edo.test.ts +++ b/src/webparts/requestPortal/shared/edo/edo.test.ts @@ -3,6 +3,7 @@ import { IEmployeeOrganization } from '../dictionary/models/IEmployeeOrganizatio import { describeEdoError } from './edoErrors'; import { splitFio } from './fio'; import { resolveSigningTargets } from './signingTarget'; +import { describeSigning } from '../../forms/Vacation/signingMessage'; const TANSU_ID = '9de064a4-44c8-88ef-11ec-8284930fad96'; @@ -150,3 +151,26 @@ describe('текст ошибки ЭДО', () => { expect(describeEdoError(403, '')).toContain('403'); }); }); + +describe('сообщение об отправке в ЭДО', () => { + it('всё ушло на подпись', () => { + const text = describeSigning({ sent: ['ТОО А', 'ТОО Б'], awaitingCertificate: [], skipped: [] }, 'a@b.kz'); + + expect(text).toContain('отправлены на подписание в ЭДО (ТОО А, ТОО Б)'); + expect(text).not.toContain('сертификат'); + }); + + it('у части компаний нет сертификата — документы сохранены и ждут', () => { + const text = describeSigning({ sent: ['ТОО А', 'ТОО Б'], awaitingCertificate: ['ТОО Б'], skipped: [] }, 'a@b.kz'); + + expect(text).toContain('отправлены на подписание в ЭДО (ТОО А)'); + expect(text).toContain('ждут загрузки сертификата Sigex по компаниям: ТОО Б'); + }); + + it('сертификата нет ни у кого', () => { + const text = describeSigning({ sent: ['ТОО А'], awaitingCertificate: ['ТОО А'], skipped: [] }, 'a@b.kz'); + + expect(text).not.toContain('отправлены на подписание'); + expect(text).toContain('ждут загрузки сертификата Sigex'); + }); +});