Compare commits
4 Commits
435aceb83f
...
40f16c1d22
| Author | SHA1 | Date | |
|---|---|---|---|
| 40f16c1d22 | |||
| d28a316645 | |||
| d433265b5c | |||
| e90cad7e88 |
@@ -15,6 +15,7 @@ import {
|
|||||||
} from '../../shared/approval';
|
} from '../../shared/approval';
|
||||||
import { friendlyErrorMessage } from '../../shared/diagnostics';
|
import { friendlyErrorMessage } from '../../shared/diagnostics';
|
||||||
import { getManagerEmail } from '../../shared/dictionary';
|
import { getManagerEmail } from '../../shared/dictionary';
|
||||||
|
import { effectiveUserEmail } from '../../shared/impersonation';
|
||||||
import {
|
import {
|
||||||
approvalListTitle,
|
approvalListTitle,
|
||||||
approvalListTitleForListId,
|
approvalListTitleForListId,
|
||||||
@@ -146,6 +147,14 @@ const DismissalView: React.FC<IDismissalViewProps> = ({ context, itemId, formKey
|
|||||||
// сейчас активный шаг: getManagerEmail дублирует тот же запрос
|
// сейчас активный шаг: getManagerEmail дублирует тот же запрос
|
||||||
// (Employee/Manager, managerType=chief), что approval-route делает для
|
// (Employee/Manager, managerType=chief), что approval-route делает для
|
||||||
// HeadType.Chief при планировании этого же маршрута (см. README формы).
|
// HeadType.Chief при планировании этого же маршрута (см. README формы).
|
||||||
|
//
|
||||||
|
// Сравниваем с effectiveUserEmail, а не с context.pageContext.user.email
|
||||||
|
// напрямую: getPendingRowForCurrentUser (через ApprovalListService.
|
||||||
|
// getCurrentUserId) уже учитывает отладочную подмену личности (см.
|
||||||
|
// shared/impersonation.ts) — без того же самого здесь «активный шаг есть»
|
||||||
|
// (active=true под подменённым) и «это я» (isManager=false, сравнили с
|
||||||
|
// РЕАЛЬНЫМ логином) расходились бы, и поле оставалось бы заблокированным
|
||||||
|
// даже когда подмена указывает именно на этого руководителя.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!data.Employee) return;
|
if (!data.Employee) return;
|
||||||
let alive = true;
|
let alive = true;
|
||||||
@@ -153,8 +162,7 @@ const DismissalView: React.FC<IDismissalViewProps> = ({ context, itemId, formKey
|
|||||||
.then(([pending, managerEmail]) => {
|
.then(([pending, managerEmail]) => {
|
||||||
if (!alive) return;
|
if (!alive) return;
|
||||||
const active = !!pending && !pending.isDelegator;
|
const active = !!pending && !pending.isDelegator;
|
||||||
const isManager =
|
const isManager = !!managerEmail && managerEmail.toLowerCase() === effectiveUserEmail(context).toLowerCase();
|
||||||
!!managerEmail && managerEmail.toLowerCase() === context.pageContext.user.email.toLowerCase();
|
|
||||||
setAssetsAcceptorEditable(active && isManager);
|
setAssetsAcceptorEditable(active && isManager);
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
@@ -246,11 +254,6 @@ const DismissalView: React.FC<IDismissalViewProps> = ({ context, itemId, formKey
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="rp-no-print">
|
|
||||||
{clearance && <Alert variant="ok">{clearance}</Alert>}
|
|
||||||
{clearanceError && <Alert variant="err">{clearanceError}</Alert>}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="rp-print-root">
|
<div className="rp-print-root">
|
||||||
<FormCard
|
<FormCard
|
||||||
title="Заявление на увольнение"
|
title="Заявление на увольнение"
|
||||||
@@ -271,8 +274,12 @@ const DismissalView: React.FC<IDismissalViewProps> = ({ context, itemId, formKey
|
|||||||
<ApprovalMatrix editor={approvers} search={(q) => approval.searchUsers(q)} />
|
<ApprovalMatrix editor={approvers} search={(q) => approval.searchUsers(q)} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Панель решения — ВНЕ печати и FormModeProvider (комментарий редактируемый). */}
|
{/* Панель решения — ВНЕ печати и FormModeProvider (комментарий редактируемый).
|
||||||
|
Итог автосоздания обходного листа — тут же, а не в шапке: он появляется
|
||||||
|
именно как следствие решения «Согласовать», логичнее видеть его рядом. */}
|
||||||
<div className="rp-no-print">
|
<div className="rp-no-print">
|
||||||
|
{clearance && <Alert variant="ok">{clearance}</Alert>}
|
||||||
|
{clearanceError && <Alert variant="err">{clearanceError}</Alert>}
|
||||||
<DecisionPanel
|
<DecisionPanel
|
||||||
service={approverService}
|
service={approverService}
|
||||||
requestId={itemId}
|
requestId={itemId}
|
||||||
|
|||||||
@@ -28,20 +28,22 @@ RequestTypeId» в `docs/forms.md`). Тип заведён в админке app
|
|||||||
People-picker (`Employee`), маршрут резолвится по e-mail **выбранного** сотрудника,
|
People-picker (`Employee`), маршрут резолвится по e-mail **выбранного** сотрудника,
|
||||||
reloadKey матрицы включает `data.Employee` и должность основной компании (см. ниже).
|
reloadKey матрицы включает `data.Employee` и должность основной компании (см. ниже).
|
||||||
- **Автоподстановка при выборе сотрудника:** вся таблица организаций — из ОДНОГО
|
- **Автоподстановка при выборе сотрудника:** вся таблица организаций — из ОДНОГО
|
||||||
справочника `Employee/ByEmail` (`shared/dictionary`) — см. «Организация ·
|
справочника `Employee/ByEmail` (`shared/dictionary`) — см. ниже.
|
||||||
подразделение · должность».
|
- **Таблица организаций** (`OrganizationsTable`, `forms/Dismissal/sections/`, без
|
||||||
- **«Организация · подразделение · должность»** (`OrganizationsTable`,
|
подписи над собой — у неё уже есть заголовки колонок «Компания» / «Структурное
|
||||||
`forms/Dismissal/sections/`) — ЕДИНСТВЕННОЕ место для компании, департамента и
|
подразделение» / «Должность») — ЕДИНСТВЕННОЕ место для компании, подразделения и
|
||||||
должности, и на форме подачи (редактируемая), и в режиме просмотра (read-only) —
|
должности, и на форме подачи (редактируемая), и в режиме просмотра (read-only) —
|
||||||
отдельных полей для них нет (были бы дублированием). `data.Organization:
|
отдельных полей для них нет (были бы дублированием). `data.Organization:
|
||||||
IOrganizationRecord[]` — массив `{company, department, position}`, компания и
|
IOrganizationRecord[]` — массив `{company, department, position}`; поле называется
|
||||||
департамент по каждой строке read-only; редактируется только должность ПЕРВОЙ
|
`department` (тот же справочник `Section`, что и раньше — см. следующий абзац),
|
||||||
(основной) записи — `primaryPosition(data.Organization)`, используется маршрутом
|
колонка называется «Структурное подразделение», и то и другое read-only. Должность
|
||||||
(условие «IsManager») и обязательна валидацией; должности остальных компаний
|
редактируема В ЛЮБОЙ строке (справочник не всегда точен), но маршрут (условие
|
||||||
(совместитель) — информационные. Справочник ничего не вернул (сотрудник не найден
|
«IsManager») и обязательность по валидации завязаны ТОЛЬКО на должность ПЕРВОЙ
|
||||||
в api.tnsu.kz) — таблица всё равно рендерится одной строкой с прочерками вместо
|
(основной) записи — `primaryPosition(data.Organization)`; должности остальных
|
||||||
компании/департамента и редактируемой должностью: иначе такому человеку нечем было
|
компаний правятся, но ни на что не влияют. Справочник ничего не вернул (сотрудник
|
||||||
бы заполнить обязательное поле.
|
не найден в api.tnsu.kz) — таблица всё равно рендерится одной строкой с прочерками
|
||||||
|
вместо компании/подразделения и редактируемой должностью: иначе такому человеку
|
||||||
|
нечем было бы заполнить обязательное поле.
|
||||||
|
|
||||||
**Хранение в SharePoint — три ПАРАЛЛЕЛЬНЫХ JSON-столбца**, не один общий блок:
|
**Хранение в SharePoint — три ПАРАЛЛЕЛЬНЫХ JSON-столбца**, не один общий блок:
|
||||||
`Organization`/`Department`/`Position` — каждый содержит JSON-массив строк
|
`Organization`/`Department`/`Position` — каждый содержит JSON-массив строк
|
||||||
|
|||||||
@@ -1,5 +1,13 @@
|
|||||||
@import '../../../../../styles/design-tokens';
|
@import '../../../../../styles/design-tokens';
|
||||||
|
|
||||||
|
// Занять обе колонки сетки формы — то же, что даёт проп full у FormField
|
||||||
|
// (._full_1wk7n_8{grid-column:1/-1} в @tnsu/ui-kit), без самого FormField:
|
||||||
|
// подпись над таблицей не нужна, у неё уже есть заголовки колонок.
|
||||||
|
.wrapper {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.table {
|
.table {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { FormField, Icon, TextInput } from '../../../shared/ui';
|
import { Icon, TextInput } from '../../../shared/ui';
|
||||||
import { IOrganizationRecord } from '../model';
|
import { IOrganizationRecord } from '../model';
|
||||||
import styles from './OrganizationsTable.module.scss';
|
import styles from './OrganizationsTable.module.scss';
|
||||||
|
|
||||||
interface IOrganizationsTableProps {
|
interface IOrganizationsTableProps {
|
||||||
records: IOrganizationRecord[];
|
records: IOrganizationRecord[];
|
||||||
/**
|
/**
|
||||||
* Должность ПЕРВОЙ (основной) компании редактируема — единственная ячейка
|
* Должность любой строки можно поправить — справочник не всегда точен, а
|
||||||
* таблицы, у которой это так: маршрут согласования резолвится по НЕЙ (см.
|
* условие маршрута «IsManager» резолвится по должности ПЕРВОЙ (основной)
|
||||||
* DismissalForm — fieldValues.Position), должности остальных компаний
|
* компании (см. DismissalForm — primaryPosition/fieldValues.Position), так
|
||||||
* (совместитель) — только для просмотра, ставить им что-то в соответствие
|
* что расхождение со справочником там, где оно есть, важно уметь исправить.
|
||||||
* незачем. Не задано — вся таблица read-only (режим просмотра, DismissalView).
|
* Не задано — вся таблица read-only (режим просмотра, DismissalView).
|
||||||
*/
|
*/
|
||||||
onChangePosition?: (value: string) => void;
|
onChangePosition?: (index: number, value: string) => void;
|
||||||
/**
|
/**
|
||||||
* Убрать компанию из заявления — есть только на форме подачи (см.
|
* Убрать компанию из заявления — есть только на форме подачи (см.
|
||||||
* DismissalForm.handleRemoveOrganization), не в режиме просмотра: там состав
|
* DismissalForm.handleRemoveOrganization), не в режиме просмотра: там состав
|
||||||
@@ -22,7 +22,7 @@ interface IOrganizationsTableProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Организации сотрудника — ЕДИНСТВЕННОЕ место для компании/департамента/
|
* Организации сотрудника — ЕДИНСТВЕННОЕ место для компании/подразделения/
|
||||||
* должности, и на форме подачи (редактируемая — см. onChangePosition/onRemove),
|
* должности, и на форме подачи (редактируемая — см. onChangePosition/onRemove),
|
||||||
* и в режиме просмотра заявки (read-only, те же данные из сохранённой заявки,
|
* и в режиме просмотра заявки (read-only, те же данные из сохранённой заявки,
|
||||||
* а не live-запрос к справочнику — см. DismissalView). Раньше это были
|
* а не live-запрос к справочнику — см. DismissalView). Раньше это были
|
||||||
@@ -42,17 +42,21 @@ interface IOrganizationsTableProps {
|
|||||||
* редактируемой должностью: без этого заявление на человека без карточки в
|
* редактируемой должностью: без этого заявление на человека без карточки в
|
||||||
* справочнике нельзя было бы подать вовсе (некуда вписать должность вручную —
|
* справочнике нельзя было бы подать вовсе (некуда вписать должность вручную —
|
||||||
* а маршрут согласования без неё не резолвит условие «IsManager»).
|
* а маршрут согласования без неё не резолвит условие «IsManager»).
|
||||||
|
*
|
||||||
|
* Без обёртки FormField/подписи над таблицей намеренно — у нёе уже есть
|
||||||
|
* заголовки колонок, дублировать их общей подписью незачем; full-ширина взята
|
||||||
|
* напрямую (см. .wrapper), а не через FormField (там label обязателен).
|
||||||
*/
|
*/
|
||||||
export const OrganizationsTable: React.FC<IOrganizationsTableProps> = ({ records, onChangePosition, onRemove }) => {
|
export const OrganizationsTable: React.FC<IOrganizationsTableProps> = ({ records, onChangePosition, onRemove }) => {
|
||||||
const rows = records.length > 0 ? records : [undefined];
|
const rows = records.length > 0 ? records : [undefined];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormField label="Организация · подразделение · должность" required={!!onChangePosition} full>
|
<div className={styles.wrapper}>
|
||||||
<table className={styles.table}>
|
<table className={styles.table}>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th className={styles.company}>Компания</th>
|
<th className={styles.company}>Компания</th>
|
||||||
<th className={styles.department}>Департамент</th>
|
<th className={styles.department}>Структурное подразделение</th>
|
||||||
<th className={styles.position}>Должность</th>
|
<th className={styles.position}>Должность</th>
|
||||||
{onRemove && <th className={styles.action} />}
|
{onRemove && <th className={styles.action} />}
|
||||||
</tr>
|
</tr>
|
||||||
@@ -62,9 +66,13 @@ export const OrganizationsTable: React.FC<IOrganizationsTableProps> = ({ records
|
|||||||
<tr key={r?.company || 'manual'}>
|
<tr key={r?.company || 'manual'}>
|
||||||
<td className={styles.company}>{r?.company || '—'}</td>
|
<td className={styles.company}>{r?.company || '—'}</td>
|
||||||
<td className={styles.department}>{r?.department || '—'}</td>
|
<td className={styles.department}>{r?.department || '—'}</td>
|
||||||
{i === 0 && onChangePosition ? (
|
{onChangePosition ? (
|
||||||
<td className={styles.positionInput}>
|
<td className={styles.positionInput}>
|
||||||
<TextInput value={r?.position || ''} onChange={onChangePosition} placeholder="Введите должность" />
|
<TextInput
|
||||||
|
value={r?.position || ''}
|
||||||
|
onChange={(v) => onChangePosition(i, v)}
|
||||||
|
placeholder="Введите должность"
|
||||||
|
/>
|
||||||
</td>
|
</td>
|
||||||
) : (
|
) : (
|
||||||
<td className={styles.position}>{r?.position || '—'}</td>
|
<td className={styles.position}>{r?.position || '—'}</td>
|
||||||
@@ -88,6 +96,6 @@ export const OrganizationsTable: React.FC<IOrganizationsTableProps> = ({ records
|
|||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</FormField>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ export const RequestSection: React.FC<IRequestSectionProps> = ({
|
|||||||
placeholder={EMPLOYEE_PLACEHOLDER}
|
placeholder={EMPLOYEE_PLACEHOLDER}
|
||||||
/>
|
/>
|
||||||
</FormField>
|
</FormField>
|
||||||
{/* ЕДИНСТВЕННОЕ место для компании/департамента/должности — и на форме
|
{/* ЕДИНСТВЕННОЕ место для компании/подразделения/должности — и на форме
|
||||||
подачи (редактируемая, onRemoveOrganization задан), и в просмотре
|
подачи (редактируемая, onRemoveOrganization задан), и в просмотре
|
||||||
(read-only, ничего не передано). Отдельных полей для них нет — были бы
|
(read-only, ничего не передано). Отдельных полей для них нет — были бы
|
||||||
дублированием того, что уже показывает эта таблица. */}
|
дублированием того, что уже показывает эта таблица. */}
|
||||||
@@ -45,11 +45,11 @@ export const RequestSection: React.FC<IRequestSectionProps> = ({
|
|||||||
records={data.Organization}
|
records={data.Organization}
|
||||||
onChangePosition={
|
onChangePosition={
|
||||||
onRemoveOrganization &&
|
onRemoveOrganization &&
|
||||||
((v) =>
|
((i, v) =>
|
||||||
set(
|
set(
|
||||||
'Organization',
|
'Organization',
|
||||||
data.Organization.length > 0
|
data.Organization.length > 0
|
||||||
? data.Organization.map((r, i) => (i === 0 ? { ...r, position: v } : r))
|
? data.Organization.map((r, ri) => (ri === i ? { ...r, position: v } : r))
|
||||||
: [{ company: '', department: '', position: v }]
|
: [{ company: '', department: '', position: v }]
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
@@ -90,7 +90,7 @@ export const RequestSection: React.FC<IRequestSectionProps> = ({
|
|||||||
context={context}
|
context={context}
|
||||||
value={data.AssetsAcceptor}
|
value={data.AssetsAcceptor}
|
||||||
onChange={(v) => set('AssetsAcceptor', v)}
|
onChange={(v) => set('AssetsAcceptor', v)}
|
||||||
placeholder="Начните вводить Ф.И.О. сотрудника…"
|
placeholder={assetsAcceptorEditable ? 'Начните вводить Ф.И.О. сотрудника…' : undefined}
|
||||||
readOnly={assetsAcceptorEditable ? false : true}
|
readOnly={assetsAcceptorEditable ? false : true}
|
||||||
/>
|
/>
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|||||||
Reference in New Issue
Block a user