From 564921d56d076c5aa909c30c57774cac1a57703e 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: Fri, 11 Sep 2026 12:20:31 +0500 Subject: [PATCH] =?UTF-8?q?fix:=20=D0=B7=D0=B0=D1=8F=D0=B2=D0=BA=D0=B0=20?= =?UTF-8?q?=D0=B2=201=D0=A1=20=D0=B2=D1=81=D0=B5=D0=B3=D0=B4=D0=B0=20?= =?UTF-8?q?=D1=83=D1=85=D0=BE=D0=B4=D0=B8=D1=82=20=D0=BD=D0=B0=20=D1=81?= =?UTF-8?q?=D0=BE=D0=B3=D0=BB=D0=B0=D1=81=D0=BE=D0=B2=D0=B0=D0=BD=D0=B8?= =?UTF-8?q?=D0=B5=20+=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8=D1=82=D1=8C?= =?UTF-8?q?=20=D0=BF=D0=BE=D0=BB=D0=B5=20=C2=AB=D0=A0=D1=83=D0=BA=D0=BE?= =?UTF-8?q?=D0=B2=D0=BE=D0=B4=D0=B8=D1=82=D0=B5=D0=BB=D1=8C=C2=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../forms/Ticket1C/Ticket1CForm.tsx | 82 ++++++++----------- .../requestPortal/forms/Ticket1C/load.ts | 2 +- .../requestPortal/forms/Ticket1C/model.ts | 3 + .../forms/Ticket1C/provisioning.json | 1 + .../Ticket1C/sections/ContactSection.tsx | 3 + 5 files changed, 42 insertions(+), 49 deletions(-) diff --git a/src/webparts/requestPortal/forms/Ticket1C/Ticket1CForm.tsx b/src/webparts/requestPortal/forms/Ticket1C/Ticket1CForm.tsx index ea46e50..433fbac 100644 --- a/src/webparts/requestPortal/forms/Ticket1C/Ticket1CForm.tsx +++ b/src/webparts/requestPortal/forms/Ticket1C/Ticket1CForm.tsx @@ -35,6 +35,7 @@ import { IAttachmentInfo } from '../../shared/attachments/attachments.service'; import { effectiveUserEmail } from '../../shared/impersonation'; +import { getManagerEmail, getEmployeeOrganizations } from '../../shared/dictionary'; import { friendlyErrorMessage } from '../../shared/diagnostics'; import { IRequestFormProps } from '../../models/types'; import { @@ -44,33 +45,13 @@ import { dataListTitleForListId } from '../../shared/listTitles'; import { FORM_ID, IFormData, EMPTY_DATA, SetField } from './model'; -import { saveTicketRequest, setPlannerTaskId, ticket1cApprovalService } from './submit'; +import { saveTicketRequest, ticket1cApprovalService } from './submit'; import { loadTicketRequest } from './load'; -import { PlannerTicketService } from './services/plannerTicket.service'; import { getApplicantProfile } from './services/profile.service'; -import { PLANNER_PLAN_ID, PLANNER_BUCKET_NAME } from './config'; import { RequestSection } from './sections/RequestSection'; import { ContactSection } from './sections/ContactSection'; import { useStableCallback } from '../../shared/useStableCallback'; -/** Текст карточки Planner из данных заявки (соответствует старому buildPlannerNotes). */ -function buildPlannerNotes(data: IFormData, code: string): string { - const lines: string[] = []; - if (code) lines.push(`Код заявки: ${code}`); - lines.push(`Организация: ${data.Organization}`); - lines.push(`ФИО: ${data.FIO}`); - lines.push(`Email: ${data.Email}`); - lines.push(`Телефон: ${data.Phone}`); - lines.push(`Отдел: ${data.Department}`); - lines.push(`Должность: ${data.Position}`); - lines.push(`База 1С: ${data.DB1C}`); - lines.push(`Виды работ: ${data.WorkType}`); - lines.push(''); - lines.push('Вопрос:'); - lines.push(data.Question); - return lines.join('\n'); -} - const Ticket1CForm: React.FC = ({ requestType, context, @@ -145,6 +126,23 @@ const Ticket1CForm: React.FC = ({ })); }) .catch(() => undefined); + // Независимо от профиля Graph — непосредственный руководитель (тот же + // источник, что у Dismissal/Vacation, см. shared/dictionary/employeeLookup). + // Ф.И.О. руководителя своего поля не возвращает — доп. запрос в тот же + // Employee/ByEmail (см. getEmployeeOrganizations), которым форма уже + // резолвит ФИО/организацию заявителя. + getManagerEmail(context, context.pageContext.user.email, 'chief') + .then((email) => { + if (!alive || !email) return; + setData((prev) => ({ ...prev, Manager: prev.Manager || email })); + getEmployeeOrganizations(context, email) + .then((records) => { + const fio = records[0]?.fio; + if (alive && fio) setData((prev) => ({ ...prev, Manager: `${fio} · ${email}` })); + }) + .catch(() => undefined); + }) + .catch(() => undefined); return () => { alive = false; }; @@ -229,14 +227,12 @@ const Ticket1CForm: React.FC = ({ if (!rtId) { throw new Error('Тип заявки не определён — обновите страницу и повторите.'); } - // Пустой маршрут — согласование не требуется по правилам этого вида работ. - const hasApprovers = matrix.rows.length > 0; - const finalStatus = - intendedStatus === REQUEST_STATUS.draft - ? REQUEST_STATUS.draft - : hasApprovers - ? REQUEST_STATUS.onApproval - : REQUEST_STATUS.approved; + // Маршрут запрашивается для ЛЮБОГО вида работ и используется как есть — + // без авто-approve, если админ ещё не настроил шаги для конкретного вида + // работ (раньше в этом случае заявка сразу становилась «Согласовано» в + // обход согласования; теперь это просто «На согласовании» с пустой + // матрицей — сигнал администратору маршрутов донастроить условие). + const finalStatus = intendedStatus === REQUEST_STATUS.draft ? REQUEST_STATUS.draft : REQUEST_STATUS.onApproval; // requestId уже задан → редактируем существующую заявку, пишем в ту же // партицию (editListId). Иначе — создаём новую, и партицию для записи @@ -260,18 +256,19 @@ const Ticket1CForm: React.FC = ({ setRequestCode(code); } - let attachments = existingAttachments; if (files.length > 0) { await uploadAttachments(writeListId || dataListTitle(FORM_ID), id, files); setFiles([]); - attachments = await getAttachments(writeListId || dataListTitle(FORM_ID), id); - setExistingAttachments(attachments); + setExistingAttachments(await getAttachments(writeListId || dataListTitle(FORM_ID), id)); } if (onSaved) { onSaved(); } + // Передача в Planner теперь происходит только после реального решения + // согласующего (см. Ticket1CView.handoffToPlanner) — здесь заявка всегда + // отправляется на согласование, минуя Planner напрямую. if (finalStatus === REQUEST_STATUS.onApproval) { // Имена списков — той же партиции, куда фактически записали (writeListId), // не обязательно текущей на момент рендера формы: поток ищет строку @@ -281,24 +278,13 @@ const Ticket1CForm: React.FC = ({ dataListName: writeListId ? dataListTitleForListId(FORM_ID, writeListId) : dataListTitle(FORM_ID), approvalListName: writeListId ? approvalListTitleForListId(FORM_ID, writeListId) : approvalListTitle(FORM_ID) }); - } else if (finalStatus === REQUEST_STATUS.approved) { - // Согласование не требуется для этого вида работ — сразу передаём в отдел 1С. - const taskId = await new PlannerTicketService(context).createTask({ - planId: PLANNER_PLAN_ID, - bucketName: PLANNER_BUCKET_NAME, - title: data.TaskName, - notes: buildPlannerNotes(data, code || String(id)), - attachments, - webAbsoluteUrl: context.pageContext.web.absoluteUrl - }); - await setPlannerTaskId(id, taskId, writeListId); } }; - // На отправку маршрут должен быть либо пустым (согласование не нужно), либо - // полностью укомплектован. Ошибку резолва маршрута (напр. тип заявки ещё не - // настроен в админке маршрутов) не путаем с «согласование не требуется» — - // иначе заявка молча уйдёт в Planner в обход согласования. + // На отправку матрица не обязана быть непустой (админ мог ещё не настроить + // шаги для этого вида работ — заявка всё равно уходит «На согласовании», см. + // save() выше), но если шаг ЕСТЬ, он должен быть назначен на конкретного + // человека — иначе решать по нему будет некому. const validateSubmit = (): string | undefined => { const formError = validate(); if (formError) return formError; @@ -374,7 +360,7 @@ const Ticket1CForm: React.FC = ({ approval.searchUsers(q)} - emptyText="Согласование не требуется для этого вида работ." + emptyText="Маршрут для этого вида работ пока не настроен — обратитесь к администратору маршрутов согласования." /> = ({ data, set }) => ( set('Position', v)} /> + + set('Manager', v)} /> + );