2 Commits

Author SHA1 Message Date
Тимур Городецкий 82a9a68fbc style: заметный комментарий согласующего в маршруте
Tests / frontend-test (push) Successful in 1m31s
Мелкий серый текст под именем согласующего пользователи не замечали,
а там бывает причина возврата или отклонения. Теперь это плашка:
светлый фон, полоса слева, обычный размер шрифта. Действует во всех формах.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-28 10:05:03 +05:00
Тимур Городецкий ac633c767e feat: комментарий инициатора в заявке на отпуск
Необязательное поле «Комментарий» (VacationComment, Note) последним в
секции: инициатор может пояснить заявку для согласующих. В просмотре
пустой комментарий не показывается, в DOCX для ЭДО не идёт.
Колонка уже провизионирована.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-28 10:05:02 +05:00
7 changed files with 97 additions and 65 deletions
@@ -28,6 +28,9 @@
- **Даты окончания нет**: отпуск задаётся началом и количеством дней, чтобы не считать - **Даты окончания нет**: отпуск задаётся началом и количеством дней, чтобы не считать
праздники и выходные. Колонка `EndDate` не используется: из провижининга, типа контента и представления она убрана (само системное поле SharePoint `FromBaseType` удалить нельзя). праздники и выходные. Колонка `EndDate` не используется: из провижининга, типа контента и представления она убрана (само системное поле SharePoint `FromBaseType` удалить нельзя).
- **Заголовок элемента** (`Title`): `Отпуск — {ФИО} — дд.мм.гггг`. - **Заголовок элемента** (`Title`): `Отпуск — {ФИО} — дд.мм.гггг`.
- **Комментарий** (с 10-28, `VacationComment`, Note) — необязательный, заполняет инициатор,
последним полем секции; в просмотре пустой не показывается. В DOCX для ЭДО (`vacationDocument.ts`) не идёт; при печати
просмотра виден, как остальные поля секции.
## Подписание в ЭДО ## Подписание в ЭДО
> **Сейчас выключено** (`VACATION_EDO_SIGNING_ENABLED = false` в `shared/config.ts`): решение последнего > **Сейчас выключено** (`VACATION_EDO_SIGNING_ENABLED = false` в `shared/config.ts`): решение последнего
@@ -71,7 +74,7 @@
SLA на бэкенде approval-route этот код не вызывает — такое заявление на подпись не уйдёт. SLA на бэкенде approval-route этот код не вызывает — такое заявление на подпись не уйдёт.
## Секции формы ## Секции формы
RequestSection (сотрудник · должность · дата начала · количество дней) RequestSection (сотрудник · должность · дата начала · количество дней · замещающий · комментарий)
## Точки входа ## Точки входа
- rptype: `hr-vacation` - rptype: `hr-vacation`
@@ -34,6 +34,7 @@ export async function loadVacationRequest(itemId: number, listId?: string): Prom
'DepartmentId', 'DepartmentId',
'Project', 'Project',
'ProjectId', 'ProjectId',
'VacationComment',
'RequestStatus', 'RequestStatus',
'RequestUniqueCode', 'RequestUniqueCode',
'Employee/Title', 'Employee/Title',
@@ -58,7 +59,8 @@ export async function loadVacationRequest(itemId: number, listId?: string): Prom
Department: (item.Department as string) || '', Department: (item.Department as string) || '',
DepartmentId: (item.DepartmentId as string) || '', DepartmentId: (item.DepartmentId as string) || '',
Project: (item.Project as string) || '', Project: (item.Project as string) || '',
ProjectId: (item.ProjectId as string) || '' ProjectId: (item.ProjectId as string) || '',
VacationComment: (item.VacationComment as string) || ''
}; };
return { return {
@@ -44,6 +44,8 @@ export interface IFormData {
Project: string; Project: string;
/** oid проекта ('' — если введён вручную без выбора из списка). */ /** oid проекта ('' — если введён вручную без выбора из списка). */
ProjectId: string; ProjectId: string;
/** Комментарий инициатора к заявке — необязательный, для согласующих. */
VacationComment: string;
} }
/** Обновление одного поля формы с сохранением типа значения. */ /** Обновление одного поля формы с сохранением типа значения. */
@@ -73,7 +75,8 @@ export const EMPTY_DATA: IFormData = {
Department: '', Department: '',
DepartmentId: '', DepartmentId: '',
Project: '', Project: '',
ProjectId: '' ProjectId: '',
VacationComment: ''
}; };
/** Плейсхолдер для поля выбора сотрудника. */ /** Плейсхолдер для поля выбора сотрудника. */
@@ -13,6 +13,7 @@
{ "internalName": "Department", "title": "Подразделение", "type": "Text" }, { "internalName": "Department", "title": "Подразделение", "type": "Text" },
{ "internalName": "DepartmentId", "title": "Id подразделения (справочник)", "type": "Text" }, { "internalName": "DepartmentId", "title": "Id подразделения (справочник)", "type": "Text" },
{ "internalName": "Project", "title": "Проект", "type": "Text" }, { "internalName": "Project", "title": "Проект", "type": "Text" },
{ "internalName": "ProjectId", "title": "Id проекта (справочник)", "type": "Text" } { "internalName": "ProjectId", "title": "Id проекта (справочник)", "type": "Text" },
{ "internalName": "VacationComment", "title": "Комментарий", "type": "Note" }
] ]
} }
@@ -1,60 +1,75 @@
import * as React from 'react'; import * as React from 'react';
import { FormGrid, FormField, TextInput, PeopleField, OrganizationTable } from '../../../shared/ui'; import { FormGrid, FormField, TextInput, TextArea, PeopleField, OrganizationTable, useFormReadOnly } from '../../../shared/ui';
import { ProjectPicker } from '../../../shared/dictionary'; import { ProjectPicker } from '../../../shared/dictionary';
import { ISectionProps, EMPLOYEE_PLACEHOLDER, SUBSTITUTE_PLACEHOLDER, MIN_NOTICE_DAYS, MAX_DAYS } from '../model'; import { ISectionProps, EMPLOYEE_PLACEHOLDER, SUBSTITUTE_PLACEHOLDER, MIN_NOTICE_DAYS, MAX_DAYS } from '../model';
import { isoDatePlusDays } from '../../../shared/dateUtils'; import { isoDatePlusDays } from '../../../shared/dateUtils';
/** Единственная секция формы «Заявка на отпуск»: сотрудник (должность и подразделение подтягиваются из справочника), даты. */ /** Единственная секция формы «Заявка на отпуск»: сотрудник (должность и подразделение подтягиваются из справочника), даты. */
export const RequestSection: React.FC<ISectionProps> = ({ data, set, context, onEmployeeChange, printOrgIndex }) => ( export const RequestSection: React.FC<ISectionProps> = ({ data, set, context, onEmployeeChange, printOrgIndex }) => {
<FormGrid> const readOnly = useFormReadOnly();
<FormField label="Сотрудник" htmlFor="Employee" required full>
<PeopleField return (
context={context} <FormGrid>
value={data.Employee} <FormField label="Сотрудник" htmlFor="Employee" required full>
onChange={onEmployeeChange ?? ((v) => set('Employee', v))} <PeopleField
placeholder={EMPLOYEE_PLACEHOLDER} context={context}
/> value={data.Employee}
</FormField> onChange={onEmployeeChange ?? ((v) => set('Employee', v))}
<OrganizationTable value={data.Organization} printIndex={printOrgIndex} /> placeholder={EMPLOYEE_PLACEHOLDER}
<FormField label="Проект" htmlFor="Project"> />
<ProjectPicker </FormField>
id="Project" <OrganizationTable value={data.Organization} printIndex={printOrgIndex} />
context={context} <FormField label="Проект" htmlFor="Project">
value={data.Project} <ProjectPicker
onChange={(name, id) => { id="Project"
set('Project', name); context={context}
set('ProjectId', id); value={data.Project}
}} onChange={(name, id) => {
placeholder="Начните вводить название или код проекта…" set('Project', name);
/> set('ProjectId', id);
</FormField> }}
<FormField label="Дата начала" htmlFor="StartDate" required> placeholder="Начните вводить название или код проекта…"
<TextInput />
id="StartDate" </FormField>
type="date" <FormField label="Дата начала" htmlFor="StartDate" required>
value={data.StartDate} <TextInput
onChange={(v) => set('StartDate', v)} id="StartDate"
min={isoDatePlusDays(MIN_NOTICE_DAYS)} type="date"
/> value={data.StartDate}
</FormField> onChange={(v) => set('StartDate', v)}
<FormField label="Количество дней" htmlFor="DaysCount" required> min={isoDatePlusDays(MIN_NOTICE_DAYS)}
<TextInput />
id="DaysCount" </FormField>
type="number" <FormField label="Количество дней" htmlFor="DaysCount" required>
value={data.DaysCount} <TextInput
onChange={(v) => set('DaysCount', v)} id="DaysCount"
min={1} type="number"
max={MAX_DAYS} value={data.DaysCount}
placeholder={`от 1 до ${MAX_DAYS}`} onChange={(v) => set('DaysCount', v)}
/> min={1}
</FormField> max={MAX_DAYS}
<FormField label="Лицо, замещающее на время отсутствия" htmlFor="Substitute" full> placeholder={`от 1 до ${MAX_DAYS}`}
<PeopleField />
context={context} </FormField>
value={data.Substitute} <FormField label="Лицо, замещающее на время отсутствия" htmlFor="Substitute" full>
onChange={(v) => set('Substitute', v)} <PeopleField
placeholder={SUBSTITUTE_PLACEHOLDER} context={context}
/> value={data.Substitute}
</FormField> onChange={(v) => set('Substitute', v)}
</FormGrid> placeholder={SUBSTITUTE_PLACEHOLDER}
); />
</FormField>
{/* Пустой комментарий в просмотре не показываем — пустое поле только мешает читать. */}
{(!readOnly || data.VacationComment.trim()) && (
<FormField label="Комментарий" htmlFor="VacationComment" full>
<TextArea
id="VacationComment"
value={data.VacationComment}
onChange={(v) => set('VacationComment', v)}
rows={3}
/>
</FormField>
)}
</FormGrid>
);
};
@@ -43,7 +43,8 @@ function buildItem(
Department: data.Department || null, Department: data.Department || null,
DepartmentId: data.DepartmentId || null, DepartmentId: data.DepartmentId || null,
Project: data.Project || null, Project: data.Project || null,
ProjectId: data.ProjectId || null ProjectId: data.ProjectId || null,
VacationComment: data.VacationComment.trim() || null
}; };
} }
@@ -157,20 +157,27 @@
} }
} }
// Комментарий решения — мелким текстом под именем согласующего (в той же ячейке, // Комментарий решения — плашкой под именем согласующего (в той же ячейке,
// а не отдельной строкой: иначе ломается rowSpan колонки «Шаг» у параллельных). // а не отдельной строкой: иначе ломается rowSpan колонки «Шаг» у параллельных).
// Плашка, а не мелкий серый текст: пользователи его не замечали, а там бывает
// причина возврата/отклонения.
.commentInline { .commentInline {
display: block; display: block;
margin-top: 3px; margin-top: $sp-1;
font-size: $fs-caption; padding: 6px 10px;
color: $gray-500; border-left: 3px solid $navy-600;
border-radius: 0 $r-sm $r-sm 0;
background: $navy-100;
font-size: $fs-sm;
color: $gray-900;
line-height: 1.45; line-height: 1.45;
white-space: pre-wrap; // сохранить переносы строк из комментария white-space: pre-wrap; // сохранить переносы строк из комментария
overflow-wrap: anywhere; // длинная ссылка без пробелов не распирает ячейку
} }
.commentLabel { .commentLabel {
color: $gray-400; color: $navy-800;
font-weight: 500; font-weight: 600;
} }
// Пометка «доп. согласующий, добавил: …» — под именем согласующего. Золотой // Пометка «доп. согласующий, добавил: …» — под именем согласующего. Золотой