скрипт
This commit is contained in:
@@ -10,6 +10,17 @@
|
|||||||
2) при необходимости переименовывает колонку Title;
|
2) при необходимости переименовывает колонку Title;
|
||||||
3) обеспечивает колонки.
|
3) обеспечивает колонки.
|
||||||
|
|
||||||
|
Имена списков НУМЕРОВАННЫЕ: «<Форма> <N> — данные» / «<Форма> <N> — согласование».
|
||||||
|
Номер собирает сам скрипт из имени в манифесте (см. Get-NumberedListTitle) — в
|
||||||
|
манифестах имена остаются без номера, чтобы номер не дублировался по 14 манифестам
|
||||||
|
и ссылкам lookupList. Нумерация нужна под архивацию: когда список данных упирается
|
||||||
|
в порог элементов, рядом создаётся следующая пара с номером N+1.
|
||||||
|
|
||||||
|
Перед основным проходом идёт разовая МИГРАЦИЯ имён (только при -ListNumber 1):
|
||||||
|
существующие списки со старым безномерным именем переименовываются в «… 1 — …».
|
||||||
|
Переименовывается только Title — server-relative URL списка при этом НЕ меняется,
|
||||||
|
поэтому ссылки на список (request_type.list_id в approval-route) остаются валидными.
|
||||||
|
|
||||||
Колонки — через СТОЛБЦЫ САЙТА (эталоны):
|
Колонки — через СТОЛБЦЫ САЙТА (эталоны):
|
||||||
- не-Lookup колонка сначала создаётся как столбец сайта (группа «Портал заявок»),
|
- не-Lookup колонка сначала создаётся как столбец сайта (группа «Портал заявок»),
|
||||||
один раз на весь сайт, затем ПОДКЛЮЧАЕТСЯ к списку по ссылке. Так одинаковые
|
один раз на весь сайт, затем ПОДКЛЮЧАЕТСЯ к списку по ссылке. Так одинаковые
|
||||||
@@ -40,17 +51,31 @@
|
|||||||
.PARAMETER FormsRoot
|
.PARAMETER FormsRoot
|
||||||
Папка с формами. По умолчанию — src/webparts/requestPortal/forms.
|
Папка с формами. По умолчанию — src/webparts/requestPortal/forms.
|
||||||
|
|
||||||
|
.PARAMETER ListNumber
|
||||||
|
Номер пары списков. 1 — текущие (рабочие) списки; 2, 3 … — следующие пары под
|
||||||
|
архивацию. Миграция безномерных имён выполняется только при 1.
|
||||||
|
|
||||||
|
.PARAMETER SkipRename
|
||||||
|
Не выполнять миграцию имён существующих списков (только создание/колонки).
|
||||||
|
|
||||||
.EXAMPLE
|
.EXAMPLE
|
||||||
./Provision-Lists.ps1 -SiteUrl "https://tnsukz.sharepoint.com/sites/requests" `
|
./Provision-Lists.ps1 -SiteUrl "https://tnsukz.sharepoint.com/sites/requests" `
|
||||||
-ClientId "7459c965-5ed4-4673-8140-0ab8faf9db64" -Tenant "tnsukz.onmicrosoft.com" `
|
-ClientId "7459c965-5ed4-4673-8140-0ab8faf9db64" -Tenant "tnsukz.onmicrosoft.com" `
|
||||||
-Thumbprint "AABBCC...."
|
-Thumbprint "AABBCC...."
|
||||||
|
|
||||||
|
.EXAMPLE
|
||||||
|
# Следующая пара списков под архивацию, без миграции имён.
|
||||||
|
./Provision-Lists.ps1 -SiteUrl "..." -ClientId "..." -Tenant "..." -Thumbprint "..." `
|
||||||
|
-ListNumber 2
|
||||||
#>
|
#>
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory = $true)][string]$SiteUrl,
|
[Parameter(Mandatory = $true)][string]$SiteUrl,
|
||||||
[Parameter(Mandatory = $true)][string]$ClientId,
|
[Parameter(Mandatory = $true)][string]$ClientId,
|
||||||
[Parameter(Mandatory = $true)][string]$Tenant,
|
[Parameter(Mandatory = $true)][string]$Tenant,
|
||||||
[Parameter(Mandatory = $true)][string]$Thumbprint,
|
[Parameter(Mandatory = $true)][string]$Thumbprint,
|
||||||
[string]$FormsRoot = "$PSScriptRoot/../src/webparts/requestPortal/forms"
|
[string]$FormsRoot = "$PSScriptRoot/../src/webparts/requestPortal/forms",
|
||||||
|
[ValidateRange(1, 999)][int]$ListNumber = 1,
|
||||||
|
[switch]$SkipRename
|
||||||
)
|
)
|
||||||
|
|
||||||
$ErrorActionPreference = 'Stop'
|
$ErrorActionPreference = 'Stop'
|
||||||
@@ -61,6 +86,30 @@ function ConvertTo-XmlSafe {
|
|||||||
return $Value.Replace('&', '&').Replace('<', '<').Replace('>', '>').Replace("'", ''').Replace('"', '"')
|
return $Value.Replace('&', '&').Replace('<', '<').Replace('>', '>').Replace("'", ''').Replace('"', '"')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Имя списка по конвенции: «<Форма> — данные» / «<Форма> — согласование».
|
||||||
|
$LIST_TITLE_PATTERN = '^(?<base>.+?)\s*—\s*(?<suffix>данные|согласование)\s*$'
|
||||||
|
|
||||||
|
# Имя в манифесте → нумерованное «<Форма> <N> — <суффикс>».
|
||||||
|
#
|
||||||
|
# Номер ставится ПЕРЕД суффиксом, и это не косметика: и портал
|
||||||
|
# (features/myRequests/myRequests.service.ts), и админка маршрутов
|
||||||
|
# (approval-route/frontend/.../requestManageService.ts) выводят имя списка
|
||||||
|
# согласования из имени списка данных регексом /\s*—\s*данные\s*$/, привязанным к
|
||||||
|
# КОНЦУ строки. «… — данные 1» этот регекс не матчит, и список согласования просто
|
||||||
|
# перестаёт находиться.
|
||||||
|
#
|
||||||
|
# Идемпотентно: уже нумерованное имя не получает второй номер, а перенумеровывается.
|
||||||
|
function Get-NumberedListTitle {
|
||||||
|
param([string]$Title, [int]$Number)
|
||||||
|
|
||||||
|
$m = [regex]::Match($Title, $LIST_TITLE_PATTERN)
|
||||||
|
if (-not $m.Success) {
|
||||||
|
throw "Имя списка «$Title» не по конвенции «<Форма> — данные|согласование» — номер поставить некуда"
|
||||||
|
}
|
||||||
|
$base = $m.Groups['base'].Value -replace '\s+\d+$', ''
|
||||||
|
return "$base $Number — $($m.Groups['suffix'].Value)"
|
||||||
|
}
|
||||||
|
|
||||||
# Есть ли колонка НА СПИСКЕ (list-scope).
|
# Есть ли колонка НА СПИСКЕ (list-scope).
|
||||||
function Test-FieldExists {
|
function Test-FieldExists {
|
||||||
param([string]$List, [string]$InternalName)
|
param([string]$List, [string]$InternalName)
|
||||||
@@ -175,7 +224,12 @@ function Add-ListColumn {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
'Lookup' {
|
'Lookup' {
|
||||||
$src = Get-PnPList -Identity $Col.lookupList -ErrorAction Stop
|
# lookupList в манифесте — безномерное имя, целевой список тоже нумерован.
|
||||||
|
# Кросс-формные lookup'ы (WorkplaceSetup.SourceHire → «Прием на работу … — данные»)
|
||||||
|
# при этом предполагают ОДИН И ТОТ ЖЕ номер у обеих форм — при появлении
|
||||||
|
# реальных архивных пар это место придётся пересмотреть.
|
||||||
|
$lookupTitle = Get-NumberedListTitle -Title $Col.lookupList -Number $ListNumber
|
||||||
|
$src = Get-PnPList -Identity $lookupTitle -ErrorAction Stop
|
||||||
$guid = $src.Id.ToString()
|
$guid = $src.Id.ToString()
|
||||||
$show = if ($Col.lookupShowField) { $Col.lookupShowField } else { 'Title' }
|
$show = if ($Col.lookupShowField) { $Col.lookupShowField } else { 'Title' }
|
||||||
$xml = "<Field Type='Lookup' DisplayName='$(ConvertTo-XmlSafe $Col.title)' Name='$($Col.internalName)' ShowField='$show' List='{$guid}' />"
|
$xml = "<Field Type='Lookup' DisplayName='$(ConvertTo-XmlSafe $Col.title)' Name='$($Col.internalName)' ShowField='$show' List='{$guid}' />"
|
||||||
@@ -239,9 +293,41 @@ if (-not $manifests) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Проход 0: миграция безномерных имён на «… 1 — …». Отдельным проходом ДО основного —
|
||||||
|
# так порядок обхода манифестов не важен: к моменту разрешения Lookup'а (по имени
|
||||||
|
# списка-источника) все списки уже переименованы.
|
||||||
|
#
|
||||||
|
# Переименовывается только Title. URL списка при переименовании Title в SharePoint не
|
||||||
|
# меняется, поэтому request_type.list_id (server-relative URL) в approval-route
|
||||||
|
# трогать не нужно.
|
||||||
|
if ($ListNumber -eq 1 -and -not $SkipRename) {
|
||||||
|
Write-Host "`n=== Миграция имён списков на «… 1 — …» ===" -ForegroundColor Cyan
|
||||||
|
foreach ($manifest in $manifests) {
|
||||||
|
$def = Get-Content -Path $manifest.FullName -Raw -Encoding UTF8 | ConvertFrom-Json
|
||||||
|
$legacyTitle = $def.dataListTitle
|
||||||
|
$targetTitle = Get-NumberedListTitle -Title $legacyTitle -Number 1
|
||||||
|
|
||||||
|
if (Get-PnPList -Identity $targetTitle -ErrorAction SilentlyContinue) {
|
||||||
|
Write-Host " [=] уже нумерован: «$targetTitle»" -ForegroundColor DarkGray
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (-not (Get-PnPList -Identity $legacyTitle -ErrorAction SilentlyContinue)) {
|
||||||
|
Write-Host " [ ] нет списка «$legacyTitle» — создастся ниже как «$targetTitle»" -ForegroundColor DarkGray
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
Set-PnPList -Identity $legacyTitle -Title $targetTitle | Out-Null
|
||||||
|
Write-Host " [~] «$legacyTitle» → «$targetTitle»" -ForegroundColor Yellow
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
Write-Warning " [!] не удалось переименовать «$legacyTitle»: $($_.Exception.Message)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
foreach ($manifest in $manifests) {
|
foreach ($manifest in $manifests) {
|
||||||
$def = Get-Content -Path $manifest.FullName -Raw -Encoding UTF8 | ConvertFrom-Json
|
$def = Get-Content -Path $manifest.FullName -Raw -Encoding UTF8 | ConvertFrom-Json
|
||||||
$listTitle = $def.dataListTitle
|
$listTitle = Get-NumberedListTitle -Title $def.dataListTitle -Number $ListNumber
|
||||||
Write-Host "`n=== Форма: $($manifest.Directory.Name) → список «$listTitle» ===" -ForegroundColor Cyan
|
Write-Host "`n=== Форма: $($manifest.Directory.Name) → список «$listTitle» ===" -ForegroundColor Cyan
|
||||||
|
|
||||||
# 1) Список (идемпотентно)
|
# 1) Список (идемпотентно)
|
||||||
|
|||||||
@@ -24,8 +24,9 @@ import {
|
|||||||
findUnassignedStep
|
findUnassignedStep
|
||||||
} from '../../shared/approval';
|
} from '../../shared/approval';
|
||||||
import { IRequestFormProps } from '../../models/types';
|
import { IRequestFormProps } from '../../models/types';
|
||||||
import { IFormData, EMPTY_DATA, SetField } from './model';
|
import { approvalListTitle, dataListTitle } from '../../shared/listTitles';
|
||||||
import { saveClearanceRequest, clearanceApprovalService, DATA_LIST_TITLE, APPROVAL_LIST_TITLE } from './submit';
|
import { FORM_ID, IFormData, EMPTY_DATA, SetField } from './model';
|
||||||
|
import { saveClearanceRequest, clearanceApprovalService } from './submit';
|
||||||
import { loadClearanceRequest } from './load';
|
import { loadClearanceRequest } from './load';
|
||||||
import { RequestSection } from './sections/RequestSection';
|
import { RequestSection } from './sections/RequestSection';
|
||||||
|
|
||||||
@@ -160,8 +161,8 @@ const ClearanceForm: React.FC<IRequestFormProps> = ({ requestType, context, onCa
|
|||||||
if (status === REQUEST_STATUS.onApproval) {
|
if (status === REQUEST_STATUS.onApproval) {
|
||||||
await notifyActiveStep(context, approval, id, {
|
await notifyActiveStep(context, approval, id, {
|
||||||
requestTypeId: rtId,
|
requestTypeId: rtId,
|
||||||
dataListName: DATA_LIST_TITLE,
|
dataListName: dataListTitle(FORM_ID),
|
||||||
approvalListName: APPROVAL_LIST_TITLE
|
approvalListName: approvalListTitle(FORM_ID)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -196,7 +197,7 @@ const ClearanceForm: React.FC<IRequestFormProps> = ({ requestType, context, onCa
|
|||||||
<FormCard
|
<FormCard
|
||||||
title={requestType.title}
|
title={requestType.title}
|
||||||
subtitle="Категория: Отдел кадров"
|
subtitle="Категория: Отдел кадров"
|
||||||
belowSubtitle={<InitiatorLine listTitle={DATA_LIST_TITLE} itemId={requestId} />}
|
belowSubtitle={<InitiatorLine listTitle={dataListTitle(FORM_ID)} itemId={requestId} />}
|
||||||
headerRight={
|
headerRight={
|
||||||
requestId || requestStatus ? (
|
requestId || requestStatus ? (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -13,14 +13,13 @@ import {
|
|||||||
useRequestTypeId,
|
useRequestTypeId,
|
||||||
APPROVAL_PHASE
|
APPROVAL_PHASE
|
||||||
} from '../../shared/approval';
|
} from '../../shared/approval';
|
||||||
import { IFormData, EMPTY_DATA, SetField } from './model';
|
import { approvalListTitle, dataListTitle } from '../../shared/listTitles';
|
||||||
|
import { FORM_ID, IFormData, EMPTY_DATA, SetField } from './model';
|
||||||
import { loadClearanceRequest } from './load';
|
import { loadClearanceRequest } from './load';
|
||||||
import {
|
import {
|
||||||
clearanceApprovalService,
|
clearanceApprovalService,
|
||||||
updateApproverFields,
|
updateApproverFields,
|
||||||
IApproverEditableFields,
|
IApproverEditableFields
|
||||||
DATA_LIST_TITLE,
|
|
||||||
APPROVAL_LIST_TITLE
|
|
||||||
} from './submit';
|
} from './submit';
|
||||||
import { canEditHrRecordsFields, canEditParkingField } from './approverRights';
|
import { canEditHrRecordsFields, canEditParkingField } from './approverRights';
|
||||||
import { RequestSection } from './sections/RequestSection';
|
import { RequestSection } from './sections/RequestSection';
|
||||||
@@ -51,7 +50,10 @@ const ClearanceView: React.FC<IClearanceViewProps> = ({ context, itemId, formKey
|
|||||||
|
|
||||||
const approval = useMemo(() => clearanceApprovalService(), []);
|
const approval = useMemo(() => clearanceApprovalService(), []);
|
||||||
const { id: requestTypeId } = useRequestTypeId(context, formKey);
|
const { id: requestTypeId } = useRequestTypeId(context, formKey);
|
||||||
const approverService = useMemo(() => new ApproverService(approval, DATA_LIST_TITLE), [approval]);
|
const approverService = useMemo(
|
||||||
|
() => new ApproverService(approval, dataListTitle(FORM_ID)),
|
||||||
|
[approval]
|
||||||
|
);
|
||||||
|
|
||||||
// Матрица маршрута из фактических строк заявки (read-only). decisionSeq в
|
// Матрица маршрута из фактических строк заявки (read-only). decisionSeq в
|
||||||
// reloadKey: после решения строки на сервере меняются — перечитываем матрицу.
|
// reloadKey: после решения строки на сервере меняются — перечитываем матрицу.
|
||||||
@@ -155,8 +157,8 @@ const ClearanceView: React.FC<IClearanceViewProps> = ({ context, itemId, formKey
|
|||||||
if (!requestTypeId) return;
|
if (!requestTypeId) return;
|
||||||
notifyActiveStep(context, approval, itemId, {
|
notifyActiveStep(context, approval, itemId, {
|
||||||
requestTypeId,
|
requestTypeId,
|
||||||
dataListName: DATA_LIST_TITLE,
|
dataListName: dataListTitle(FORM_ID),
|
||||||
approvalListName: APPROVAL_LIST_TITLE
|
approvalListName: approvalListTitle(FORM_ID)
|
||||||
}).catch(() => undefined);
|
}).catch(() => undefined);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -185,7 +187,7 @@ const ClearanceView: React.FC<IClearanceViewProps> = ({ context, itemId, formKey
|
|||||||
<FormCard
|
<FormCard
|
||||||
title="Обходной лист"
|
title="Обходной лист"
|
||||||
subtitle={`№${code || itemId}`}
|
subtitle={`№${code || itemId}`}
|
||||||
belowSubtitle={<InitiatorLine listTitle={DATA_LIST_TITLE} itemId={itemId} />}
|
belowSubtitle={<InitiatorLine listTitle={dataListTitle(FORM_ID)} itemId={itemId} />}
|
||||||
headerRight={status ? <StatusBadge status={status} /> : undefined}
|
headerRight={status ? <StatusBadge status={status} /> : undefined}
|
||||||
>
|
>
|
||||||
<FormModeProvider readOnly>
|
<FormModeProvider readOnly>
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { getSP } from '../../shared/pnp/pnpjs';
|
import { getSP } from '../../shared/pnp/pnpjs';
|
||||||
import { IFormData, EMPTY_DATA } from './model';
|
import { dataListTitle } from '../../shared/listTitles';
|
||||||
|
import { FORM_ID, IFormData, EMPTY_DATA } from './model';
|
||||||
const LIST_TITLE = 'Обходной лист — данные';
|
|
||||||
|
|
||||||
export interface ILoadedRequest {
|
export interface ILoadedRequest {
|
||||||
data: IFormData;
|
data: IFormData;
|
||||||
@@ -15,7 +14,7 @@ export interface ILoadedRequest {
|
|||||||
/** Прочитать обходной лист по id: модель формы + статус (правка / просмотр). */
|
/** Прочитать обходной лист по id: модель формы + статус (правка / просмотр). */
|
||||||
export async function loadClearanceRequest(itemId: number): Promise<ILoadedRequest> {
|
export async function loadClearanceRequest(itemId: number): Promise<ILoadedRequest> {
|
||||||
const item = (await getSP()
|
const item = (await getSP()
|
||||||
.web.lists.getByTitle(LIST_TITLE)
|
.web.lists.getByTitle(dataListTitle(FORM_ID))
|
||||||
.items.getById(itemId)
|
.items.getById(itemId)
|
||||||
.select(
|
.select(
|
||||||
'Id',
|
'Id',
|
||||||
|
|||||||
@@ -1,8 +1,15 @@
|
|||||||
import { WebPartContext } from '@microsoft/sp-webpart-base';
|
import { WebPartContext } from '@microsoft/sp-webpart-base';
|
||||||
|
import { RequestTypeId } from '../../models/types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Тип заявки этой формы — ключ реестра форм (forms/registry.ts). Имена списков
|
||||||
|
* SharePoint выводятся из него, см. shared/listTitles.ts.
|
||||||
|
*/
|
||||||
|
export const FORM_ID: RequestTypeId = 'clearance-form';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Данные формы «Обходной лист». Ключи по возможности = internalName колонок
|
* Данные формы «Обходной лист». Ключи по возможности = internalName колонок
|
||||||
* SP-списка «Обходной лист — данные». Employee хранит e-mail УВОЛЬНЯЕМОГО
|
* списка данных этой формы (см. shared/listTitles.ts). Employee хранит e-mail УВОЛЬНЯЕМОГО
|
||||||
* сотрудника (то, что отдаёт PeopleField) — заявку подаёт HR/руководитель,
|
* сотрудника (то, что отдаёт PeopleField) — заявку подаёт HR/руководитель,
|
||||||
* а не сам увольняемый, поэтому и маршрут согласования резолвится по email
|
* а не сам увольняемый, поэтому и маршрут согласования резолвится по email
|
||||||
* из этого поля, а не по email инициатора (как в «Заявке на отпуск»).
|
* из этого поля, а не по email инициатора (как в «Заявке на отпуск»).
|
||||||
|
|||||||
@@ -1,14 +1,12 @@
|
|||||||
import { getSP } from '../../shared/pnp/pnpjs';
|
import { getSP } from '../../shared/pnp/pnpjs';
|
||||||
import { ApprovalListService, IApproverToWrite, writeRoute } from '../../shared/approval';
|
import { ApprovalListService, IApproverToWrite, writeRoute } from '../../shared/approval';
|
||||||
import { buildRequestUniqueCode } from '../../shared/requestCode';
|
import { buildRequestUniqueCode } from '../../shared/requestCode';
|
||||||
import { IFormData } from './model';
|
import { approvalListTitle, dataListTitle } from '../../shared/listTitles';
|
||||||
|
import { FORM_ID, IFormData } from './model';
|
||||||
export const DATA_LIST_TITLE = 'Обходной лист — данные';
|
|
||||||
export const APPROVAL_LIST_TITLE = 'Обходной лист — согласование';
|
|
||||||
|
|
||||||
/** Экземпляр сервиса списка согласования этой формы. */
|
/** Экземпляр сервиса списка согласования этой формы. */
|
||||||
export function clearanceApprovalService(): ApprovalListService {
|
export function clearanceApprovalService(): ApprovalListService {
|
||||||
return new ApprovalListService(APPROVAL_LIST_TITLE);
|
return new ApprovalListService(approvalListTitle(FORM_ID));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Отметки, которые проставляет не инициатор, а профильный согласующий (см. approverRights.ts). */
|
/** Отметки, которые проставляет не инициатор, а профильный согласующий (см. approverRights.ts). */
|
||||||
@@ -36,7 +34,7 @@ export async function updateApproverFields(
|
|||||||
Number.isFinite(amount) && fields.TrainingAmount.trim() !== '' ? amount : null;
|
Number.isFinite(amount) && fields.TrainingAmount.trim() !== '' ? amount : null;
|
||||||
}
|
}
|
||||||
if (Object.keys(item).length === 0) return;
|
if (Object.keys(item).length === 0) return;
|
||||||
await getSP().web.lists.getByTitle(DATA_LIST_TITLE).items.getById(id).update(item);
|
await getSP().web.lists.getByTitle(dataListTitle(FORM_ID)).items.getById(id).update(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** email / claims-логин → пользователь сайта (Id для Person-поля + Title для заголовка). */
|
/** email / claims-логин → пользователь сайта (Id для Person-поля + Title для заголовка). */
|
||||||
@@ -90,7 +88,7 @@ export async function saveClearanceRequest(
|
|||||||
approvers: IApproverToWrite[],
|
approvers: IApproverToWrite[],
|
||||||
status: string
|
status: string
|
||||||
): Promise<ISaveResult> {
|
): Promise<ISaveResult> {
|
||||||
const list = getSP().web.lists.getByTitle(DATA_LIST_TITLE);
|
const list = getSP().web.lists.getByTitle(dataListTitle(FORM_ID));
|
||||||
const employee = await ensureEmployee(data.Employee);
|
const employee = await ensureEmployee(data.Employee);
|
||||||
const item = buildItem(data, status, employee.Id, employee.Title || data.Employee);
|
const item = buildItem(data, status, employee.Id, employee.Title || data.Employee);
|
||||||
|
|
||||||
|
|||||||
@@ -36,8 +36,9 @@ import {
|
|||||||
} from '../../shared/attachments/attachments.service';
|
} from '../../shared/attachments/attachments.service';
|
||||||
import { effectiveUserEmail } from '../../shared/impersonation';
|
import { effectiveUserEmail } from '../../shared/impersonation';
|
||||||
import { IRequestFormProps } from '../../models/types';
|
import { IRequestFormProps } from '../../models/types';
|
||||||
import { IFormData, EMPTY_DATA, SetField } from './model';
|
import { approvalListTitle, dataListTitle } from '../../shared/listTitles';
|
||||||
import { saveHireRequest, hireApprovalService, DATA_LIST_TITLE, APPROVAL_LIST_TITLE } from './submit';
|
import { FORM_ID, IFormData, EMPTY_DATA, SetField } from './model';
|
||||||
|
import { saveHireRequest, hireApprovalService } from './submit';
|
||||||
import { loadHireRequest } from './load';
|
import { loadHireRequest } from './load';
|
||||||
import { ApplicationSection } from './sections/ApplicationSection';
|
import { ApplicationSection } from './sections/ApplicationSection';
|
||||||
import { CandidateSection } from './sections/CandidateSection';
|
import { CandidateSection } from './sections/CandidateSection';
|
||||||
@@ -66,7 +67,7 @@ const RecruitingHireForm: React.FC<IRequestFormProps> = ({ requestType, context,
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let alive = true;
|
let alive = true;
|
||||||
Promise.all([loadHireRequest(editId), getAttachments(DATA_LIST_TITLE, editId)])
|
Promise.all([loadHireRequest(editId), getAttachments(dataListTitle(FORM_ID), editId)])
|
||||||
.then(([loaded, atts]) => {
|
.then(([loaded, atts]) => {
|
||||||
if (alive) {
|
if (alive) {
|
||||||
setData(loaded.data);
|
setData(loaded.data);
|
||||||
@@ -157,7 +158,7 @@ const RecruitingHireForm: React.FC<IRequestFormProps> = ({ requestType, context,
|
|||||||
if (requestId === undefined) {
|
if (requestId === undefined) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
deleteAttachment(DATA_LIST_TITLE, requestId, fileName)
|
deleteAttachment(dataListTitle(FORM_ID), requestId, fileName)
|
||||||
.then(() => setExistingAttachments((prev) => prev.filter((a) => a.fileName !== fileName)))
|
.then(() => setExistingAttachments((prev) => prev.filter((a) => a.fileName !== fileName)))
|
||||||
.catch(() => undefined);
|
.catch(() => undefined);
|
||||||
};
|
};
|
||||||
@@ -184,9 +185,9 @@ const RecruitingHireForm: React.FC<IRequestFormProps> = ({ requestType, context,
|
|||||||
setRequestCode(code);
|
setRequestCode(code);
|
||||||
}
|
}
|
||||||
if (files.length > 0) {
|
if (files.length > 0) {
|
||||||
await uploadAttachments(DATA_LIST_TITLE, id, files); // файлы — после создания элемента
|
await uploadAttachments(dataListTitle(FORM_ID), id, files); // файлы — после создания элемента
|
||||||
setFiles([]); // выгруженные файлы убираем, чтобы пересохранение не дублировало
|
setFiles([]); // выгруженные файлы убираем, чтобы пересохранение не дублировало
|
||||||
setExistingAttachments(await getAttachments(DATA_LIST_TITLE, id)); // показать добавленные
|
setExistingAttachments(await getAttachments(dataListTitle(FORM_ID), id)); // показать добавленные
|
||||||
}
|
}
|
||||||
if (onSaved) {
|
if (onSaved) {
|
||||||
onSaved();
|
onSaved();
|
||||||
@@ -195,8 +196,8 @@ const RecruitingHireForm: React.FC<IRequestFormProps> = ({ requestType, context,
|
|||||||
if (status === REQUEST_STATUS.onApproval) {
|
if (status === REQUEST_STATUS.onApproval) {
|
||||||
await notifyActiveStep(context, approval, id, {
|
await notifyActiveStep(context, approval, id, {
|
||||||
requestTypeId: rtId,
|
requestTypeId: rtId,
|
||||||
dataListName: DATA_LIST_TITLE,
|
dataListName: dataListTitle(FORM_ID),
|
||||||
approvalListName: APPROVAL_LIST_TITLE,
|
approvalListName: approvalListTitle(FORM_ID),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -236,7 +237,7 @@ const RecruitingHireForm: React.FC<IRequestFormProps> = ({ requestType, context,
|
|||||||
<FormCard
|
<FormCard
|
||||||
title={requestType.title}
|
title={requestType.title}
|
||||||
subtitle="Категория: Отдел рекрутинга"
|
subtitle="Категория: Отдел рекрутинга"
|
||||||
belowSubtitle={<InitiatorLine listTitle={DATA_LIST_TITLE} itemId={requestId} />}
|
belowSubtitle={<InitiatorLine listTitle={dataListTitle(FORM_ID)} itemId={requestId} />}
|
||||||
headerRight={
|
headerRight={
|
||||||
requestId || requestStatus ? (
|
requestId || requestStatus ? (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -15,10 +15,11 @@ import {
|
|||||||
} from '../../shared/approval';
|
} from '../../shared/approval';
|
||||||
import { navigate } from '../../shared/routing';
|
import { navigate } from '../../shared/routing';
|
||||||
import { getAttachments, IAttachmentInfo } from '../../shared/attachments/attachments.service';
|
import { getAttachments, IAttachmentInfo } from '../../shared/attachments/attachments.service';
|
||||||
import { IFormData, EMPTY_DATA, SetField } from './model';
|
import { approvalListTitle, dataListTitle } from '../../shared/listTitles';
|
||||||
|
import { FORM_ID, IFormData, EMPTY_DATA, SetField } from './model';
|
||||||
import { loadHireRequest } from './load';
|
import { loadHireRequest } from './load';
|
||||||
import { confirmDiscardFormChanges } from '../../shared/unsavedChanges';
|
import { confirmDiscardFormChanges } from '../../shared/unsavedChanges';
|
||||||
import { hireApprovalService, DATA_LIST_TITLE, APPROVAL_LIST_TITLE } from './submit';
|
import { hireApprovalService } from './submit';
|
||||||
import { ApplicationSection } from './sections/ApplicationSection';
|
import { ApplicationSection } from './sections/ApplicationSection';
|
||||||
import { CandidateSection } from './sections/CandidateSection';
|
import { CandidateSection } from './sections/CandidateSection';
|
||||||
import { PpeSection } from './sections/PpeSection';
|
import { PpeSection } from './sections/PpeSection';
|
||||||
@@ -53,7 +54,7 @@ const RecruitingHireView: React.FC<IRecruitingHireViewProps> = ({ context, itemI
|
|||||||
|
|
||||||
// Сервис решений согласующего: список согласования + список данных этой формы.
|
// Сервис решений согласующего: список согласования + список данных этой формы.
|
||||||
const approverService = useMemo(
|
const approverService = useMemo(
|
||||||
() => new ApproverService(hireApprovalService(), DATA_LIST_TITLE),
|
() => new ApproverService(hireApprovalService(), dataListTitle(FORM_ID)),
|
||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -90,7 +91,7 @@ const RecruitingHireView: React.FC<IRecruitingHireViewProps> = ({ context, itemI
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let alive = true;
|
let alive = true;
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
Promise.all([loadHireRequest(itemId), getAttachments(DATA_LIST_TITLE, itemId)])
|
Promise.all([loadHireRequest(itemId), getAttachments(dataListTitle(FORM_ID), itemId)])
|
||||||
.then(([loaded, files]) => {
|
.then(([loaded, files]) => {
|
||||||
if (alive) {
|
if (alive) {
|
||||||
setData(loaded.data);
|
setData(loaded.data);
|
||||||
@@ -129,8 +130,8 @@ const RecruitingHireView: React.FC<IRecruitingHireViewProps> = ({ context, itemI
|
|||||||
if (!requestTypeId) return;
|
if (!requestTypeId) return;
|
||||||
notifyActiveStep(context, approval, itemId, {
|
notifyActiveStep(context, approval, itemId, {
|
||||||
requestTypeId,
|
requestTypeId,
|
||||||
dataListName: DATA_LIST_TITLE,
|
dataListName: dataListTitle(FORM_ID),
|
||||||
approvalListName: APPROVAL_LIST_TITLE,
|
approvalListName: approvalListTitle(FORM_ID),
|
||||||
}).catch(() => undefined);
|
}).catch(() => undefined);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -159,7 +160,7 @@ const RecruitingHireView: React.FC<IRecruitingHireViewProps> = ({ context, itemI
|
|||||||
<FormCard
|
<FormCard
|
||||||
title="Заявка на прием"
|
title="Заявка на прием"
|
||||||
subtitle={`№${code || itemId}`}
|
subtitle={`№${code || itemId}`}
|
||||||
belowSubtitle={<InitiatorLine listTitle={DATA_LIST_TITLE} itemId={itemId} />}
|
belowSubtitle={<InitiatorLine listTitle={dataListTitle(FORM_ID)} itemId={itemId} />}
|
||||||
headerRight={status ? <StatusBadge status={status} /> : undefined}
|
headerRight={status ? <StatusBadge status={status} /> : undefined}
|
||||||
>
|
>
|
||||||
<FormModeProvider readOnly>
|
<FormModeProvider readOnly>
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { getSP } from '../../shared/pnp/pnpjs';
|
import { getSP } from '../../shared/pnp/pnpjs';
|
||||||
import { IFormData, EMPTY_DATA } from './model';
|
import { dataListTitle } from '../../shared/listTitles';
|
||||||
|
import { FORM_ID, IFormData, EMPTY_DATA } from './model';
|
||||||
const LIST_TITLE = 'Прием на работу — данные';
|
|
||||||
|
|
||||||
// Скалярные поля (текст/выбор/число/дата) — читаем как есть, приводим к строке.
|
// Скалярные поля (текст/выбор/число/дата) — читаем как есть, приводим к строке.
|
||||||
const SCALAR_FIELDS: (keyof IFormData)[] = [
|
const SCALAR_FIELDS: (keyof IFormData)[] = [
|
||||||
@@ -21,7 +20,7 @@ export interface ILoadedRequest {
|
|||||||
export async function loadHireRequest(itemId: number): Promise<ILoadedRequest> {
|
export async function loadHireRequest(itemId: number): Promise<ILoadedRequest> {
|
||||||
const sp = getSP();
|
const sp = getSP();
|
||||||
const item = (await sp.web.lists
|
const item = (await sp.web.lists
|
||||||
.getByTitle(LIST_TITLE)
|
.getByTitle(dataListTitle(FORM_ID))
|
||||||
.items.getById(itemId)
|
.items.getById(itemId)
|
||||||
.select(...SCALAR_FIELDS, 'RequestStatus', 'RequestUniqueCode')()) as Record<string, unknown>;
|
.select(...SCALAR_FIELDS, 'RequestStatus', 'RequestUniqueCode')()) as Record<string, unknown>;
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,16 @@
|
|||||||
import { WebPartContext } from '@microsoft/sp-webpart-base';
|
import { WebPartContext } from '@microsoft/sp-webpart-base';
|
||||||
import { ISelectOption } from '../../shared/ui';
|
import { ISelectOption } from '../../shared/ui';
|
||||||
|
import { RequestTypeId } from '../../models/types';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Данные формы «Заявка на прием». Ключи = internalName колонок SP-списка
|
* Тип заявки этой формы — ключ реестра форм (forms/registry.ts). Имена списков
|
||||||
* «Прием на работу — данные», чтобы при записи маппинг был 1:1.
|
* SharePoint выводятся из него, см. shared/listTitles.ts.
|
||||||
|
*/
|
||||||
|
export const FORM_ID: RequestTypeId = 'recruiting-hire';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Данные формы «Заявка на прием». Ключи = internalName колонок списка данных
|
||||||
|
* этой формы (см. shared/listTitles.ts), чтобы при записи маппинг был 1:1.
|
||||||
*/
|
*/
|
||||||
export interface IFormData {
|
export interface IFormData {
|
||||||
// Заявка (шапка)
|
// Заявка (шапка)
|
||||||
|
|||||||
@@ -1,17 +1,15 @@
|
|||||||
import { getSP } from '../../shared/pnp/pnpjs';
|
import { getSP } from '../../shared/pnp/pnpjs';
|
||||||
import { ApprovalListService, IApproverToWrite, writeRoute } from '../../shared/approval';
|
import { ApprovalListService, IApproverToWrite, writeRoute } from '../../shared/approval';
|
||||||
import { buildRequestUniqueCode } from '../../shared/requestCode';
|
import { buildRequestUniqueCode } from '../../shared/requestCode';
|
||||||
import { IFormData } from './model';
|
import { approvalListTitle, dataListTitle } from '../../shared/listTitles';
|
||||||
|
import { FORM_ID, IFormData } from './model';
|
||||||
export const DATA_LIST_TITLE = 'Прием на работу — данные';
|
|
||||||
export const APPROVAL_LIST_TITLE = 'Прием на работу — согласование';
|
|
||||||
|
|
||||||
// Числовые поля: пустая строка → null, иначе Number().
|
// Числовые поля: пустая строка → null, иначе Number().
|
||||||
const NUMBER_FIELDS: (keyof IFormData)[] = ['PpeHeight', 'PpeShoeSize', 'SalaryProbation', 'SalaryAfterProbation'];
|
const NUMBER_FIELDS: (keyof IFormData)[] = ['PpeHeight', 'PpeShoeSize', 'SalaryProbation', 'SalaryAfterProbation'];
|
||||||
|
|
||||||
/** Экземпляр сервиса списка согласования этой формы. */
|
/** Экземпляр сервиса списка согласования этой формы. */
|
||||||
export function hireApprovalService(): ApprovalListService {
|
export function hireApprovalService(): ApprovalListService {
|
||||||
return new ApprovalListService(APPROVAL_LIST_TITLE);
|
return new ApprovalListService(approvalListTitle(FORM_ID));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Осмысленный заголовок элемента списка (системная колонка Title). */
|
/** Осмысленный заголовок элемента списка (системная колонка Title). */
|
||||||
@@ -56,7 +54,7 @@ export async function saveHireRequest(
|
|||||||
executors: IApproverToWrite[],
|
executors: IApproverToWrite[],
|
||||||
status: string
|
status: string
|
||||||
): Promise<ISaveResult> {
|
): Promise<ISaveResult> {
|
||||||
const list = getSP().web.lists.getByTitle(DATA_LIST_TITLE);
|
const list = getSP().web.lists.getByTitle(dataListTitle(FORM_ID));
|
||||||
const item = buildItem(data, status);
|
const item = buildItem(data, status);
|
||||||
|
|
||||||
let id: number;
|
let id: number;
|
||||||
|
|||||||
@@ -35,8 +35,9 @@ import {
|
|||||||
} from '../../shared/attachments/attachments.service';
|
} from '../../shared/attachments/attachments.service';
|
||||||
import { effectiveUserEmail } from '../../shared/impersonation';
|
import { effectiveUserEmail } from '../../shared/impersonation';
|
||||||
import { IRequestFormProps } from '../../models/types';
|
import { IRequestFormProps } from '../../models/types';
|
||||||
import { IFormData, EMPTY_DATA, SetField } from './model';
|
import { approvalListTitle, dataListTitle } from '../../shared/listTitles';
|
||||||
import { saveSelectionRequest, selectionApprovalService, DATA_LIST_TITLE, APPROVAL_LIST_TITLE } from './submit';
|
import { FORM_ID, IFormData, EMPTY_DATA, SetField } from './model';
|
||||||
|
import { saveSelectionRequest, selectionApprovalService } from './submit';
|
||||||
import { loadSelectionRequest } from './load';
|
import { loadSelectionRequest } from './load';
|
||||||
import { ApplicationSection } from './sections/ApplicationSection';
|
import { ApplicationSection } from './sections/ApplicationSection';
|
||||||
import { DepartmentSection } from './sections/DepartmentSection';
|
import { DepartmentSection } from './sections/DepartmentSection';
|
||||||
@@ -63,7 +64,7 @@ const RecruitingSelectionForm: React.FC<IRequestFormProps> = ({ requestType, con
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let alive = true;
|
let alive = true;
|
||||||
Promise.all([loadSelectionRequest(editId), getAttachments(DATA_LIST_TITLE, editId)])
|
Promise.all([loadSelectionRequest(editId), getAttachments(dataListTitle(FORM_ID), editId)])
|
||||||
.then(([loaded, atts]) => {
|
.then(([loaded, atts]) => {
|
||||||
if (alive) {
|
if (alive) {
|
||||||
setData(loaded.data);
|
setData(loaded.data);
|
||||||
@@ -131,7 +132,7 @@ const RecruitingSelectionForm: React.FC<IRequestFormProps> = ({ requestType, con
|
|||||||
if (requestId === undefined) {
|
if (requestId === undefined) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
deleteAttachment(DATA_LIST_TITLE, requestId, fileName)
|
deleteAttachment(dataListTitle(FORM_ID), requestId, fileName)
|
||||||
.then(() => setExistingAttachments((prev) => prev.filter((a) => a.fileName !== fileName)))
|
.then(() => setExistingAttachments((prev) => prev.filter((a) => a.fileName !== fileName)))
|
||||||
.catch(() => undefined);
|
.catch(() => undefined);
|
||||||
};
|
};
|
||||||
@@ -151,9 +152,9 @@ const RecruitingSelectionForm: React.FC<IRequestFormProps> = ({ requestType, con
|
|||||||
setRequestCode(code);
|
setRequestCode(code);
|
||||||
}
|
}
|
||||||
if (files.length > 0) {
|
if (files.length > 0) {
|
||||||
await uploadAttachments(DATA_LIST_TITLE, id, files); // файлы — после создания элемента
|
await uploadAttachments(dataListTitle(FORM_ID), id, files); // файлы — после создания элемента
|
||||||
setFiles([]); // выгруженные файлы убираем, чтобы пересохранение не дублировало
|
setFiles([]); // выгруженные файлы убираем, чтобы пересохранение не дублировало
|
||||||
setExistingAttachments(await getAttachments(DATA_LIST_TITLE, id));
|
setExistingAttachments(await getAttachments(dataListTitle(FORM_ID), id));
|
||||||
}
|
}
|
||||||
if (onSaved) {
|
if (onSaved) {
|
||||||
onSaved();
|
onSaved();
|
||||||
@@ -162,8 +163,8 @@ const RecruitingSelectionForm: React.FC<IRequestFormProps> = ({ requestType, con
|
|||||||
if (status === REQUEST_STATUS.onApproval) {
|
if (status === REQUEST_STATUS.onApproval) {
|
||||||
await notifyActiveStep(context, approval, id, {
|
await notifyActiveStep(context, approval, id, {
|
||||||
requestTypeId: rtId,
|
requestTypeId: rtId,
|
||||||
dataListName: DATA_LIST_TITLE,
|
dataListName: dataListTitle(FORM_ID),
|
||||||
approvalListName: APPROVAL_LIST_TITLE
|
approvalListName: approvalListTitle(FORM_ID)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -198,7 +199,7 @@ const RecruitingSelectionForm: React.FC<IRequestFormProps> = ({ requestType, con
|
|||||||
<FormCard
|
<FormCard
|
||||||
title={requestType.title}
|
title={requestType.title}
|
||||||
subtitle="Категория: Отдел рекрутинга"
|
subtitle="Категория: Отдел рекрутинга"
|
||||||
belowSubtitle={<InitiatorLine listTitle={DATA_LIST_TITLE} itemId={requestId} />}
|
belowSubtitle={<InitiatorLine listTitle={dataListTitle(FORM_ID)} itemId={requestId} />}
|
||||||
headerRight={
|
headerRight={
|
||||||
requestId || requestStatus ? (
|
requestId || requestStatus ? (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -13,9 +13,10 @@ import {
|
|||||||
useRequestTypeId
|
useRequestTypeId
|
||||||
} from '../../shared/approval';
|
} from '../../shared/approval';
|
||||||
import { getAttachments, IAttachmentInfo } from '../../shared/attachments/attachments.service';
|
import { getAttachments, IAttachmentInfo } from '../../shared/attachments/attachments.service';
|
||||||
import { IFormData, EMPTY_DATA, SetField } from './model';
|
import { approvalListTitle, dataListTitle } from '../../shared/listTitles';
|
||||||
|
import { FORM_ID, IFormData, EMPTY_DATA, SetField } from './model';
|
||||||
import { loadSelectionRequest } from './load';
|
import { loadSelectionRequest } from './load';
|
||||||
import { selectionApprovalService, DATA_LIST_TITLE, APPROVAL_LIST_TITLE } from './submit';
|
import { selectionApprovalService } from './submit';
|
||||||
import { ApplicationSection } from './sections/ApplicationSection';
|
import { ApplicationSection } from './sections/ApplicationSection';
|
||||||
import { DepartmentSection } from './sections/DepartmentSection';
|
import { DepartmentSection } from './sections/DepartmentSection';
|
||||||
import { VacancySection } from './sections/VacancySection';
|
import { VacancySection } from './sections/VacancySection';
|
||||||
@@ -44,7 +45,7 @@ const RecruitingSelectionView: React.FC<IRecruitingSelectionViewProps> = ({ cont
|
|||||||
|
|
||||||
const approval = useMemo(() => selectionApprovalService(), []);
|
const approval = useMemo(() => selectionApprovalService(), []);
|
||||||
const { id: requestTypeId } = useRequestTypeId(context, formKey);
|
const { id: requestTypeId } = useRequestTypeId(context, formKey);
|
||||||
const approverService = useMemo(() => new ApproverService(approval, DATA_LIST_TITLE), [approval]);
|
const approverService = useMemo(() => new ApproverService(approval, dataListTitle(FORM_ID)), [approval]);
|
||||||
|
|
||||||
// Матрица маршрута из фактических строк заявки (read-only). decisionSeq в
|
// Матрица маршрута из фактических строк заявки (read-only). decisionSeq в
|
||||||
// reloadKey: после решения строки на сервере меняются — перечитываем матрицу.
|
// reloadKey: после решения строки на сервере меняются — перечитываем матрицу.
|
||||||
@@ -57,7 +58,7 @@ const RecruitingSelectionView: React.FC<IRecruitingSelectionViewProps> = ({ cont
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let alive = true;
|
let alive = true;
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
Promise.all([loadSelectionRequest(itemId), getAttachments(DATA_LIST_TITLE, itemId)])
|
Promise.all([loadSelectionRequest(itemId), getAttachments(dataListTitle(FORM_ID), itemId)])
|
||||||
.then(([loaded, files]) => {
|
.then(([loaded, files]) => {
|
||||||
if (alive) {
|
if (alive) {
|
||||||
setData(loaded.data);
|
setData(loaded.data);
|
||||||
@@ -96,8 +97,8 @@ const RecruitingSelectionView: React.FC<IRecruitingSelectionViewProps> = ({ cont
|
|||||||
if (!requestTypeId) return;
|
if (!requestTypeId) return;
|
||||||
notifyActiveStep(context, approval, itemId, {
|
notifyActiveStep(context, approval, itemId, {
|
||||||
requestTypeId,
|
requestTypeId,
|
||||||
dataListName: DATA_LIST_TITLE,
|
dataListName: dataListTitle(FORM_ID),
|
||||||
approvalListName: APPROVAL_LIST_TITLE
|
approvalListName: approvalListTitle(FORM_ID)
|
||||||
}).catch(() => undefined);
|
}).catch(() => undefined);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -111,7 +112,7 @@ const RecruitingSelectionView: React.FC<IRecruitingSelectionViewProps> = ({ cont
|
|||||||
<FormCard
|
<FormCard
|
||||||
title="Заявка на подбор"
|
title="Заявка на подбор"
|
||||||
subtitle={`№${code || itemId}`}
|
subtitle={`№${code || itemId}`}
|
||||||
belowSubtitle={<InitiatorLine listTitle={DATA_LIST_TITLE} itemId={itemId} />}
|
belowSubtitle={<InitiatorLine listTitle={dataListTitle(FORM_ID)} itemId={itemId} />}
|
||||||
headerRight={status ? <StatusBadge status={status} /> : undefined}
|
headerRight={status ? <StatusBadge status={status} /> : undefined}
|
||||||
>
|
>
|
||||||
<FormModeProvider readOnly>
|
<FormModeProvider readOnly>
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { getSP } from '../../shared/pnp/pnpjs';
|
import { getSP } from '../../shared/pnp/pnpjs';
|
||||||
import { IFormData, EMPTY_DATA } from './model';
|
import { dataListTitle } from '../../shared/listTitles';
|
||||||
|
import { FORM_ID, IFormData, EMPTY_DATA } from './model';
|
||||||
const LIST_TITLE = 'Заявка на подбор — данные';
|
|
||||||
|
|
||||||
// Категории полей — определяют, как читать значение из элемента списка.
|
// Категории полей — определяют, как читать значение из элемента списка.
|
||||||
const PERSON_FIELDS: (keyof IFormData)[] = ['Responsible', 'ProposedMentor', 'DirectManager'];
|
const PERSON_FIELDS: (keyof IFormData)[] = ['Responsible', 'ProposedMentor', 'DirectManager'];
|
||||||
@@ -30,7 +29,7 @@ export async function loadSelectionRequest(itemId: number): Promise<ILoadedReque
|
|||||||
const expands = [...PERSON_FIELDS, ...MULTI_PERSON_FIELDS];
|
const expands = [...PERSON_FIELDS, ...MULTI_PERSON_FIELDS];
|
||||||
|
|
||||||
const item = (await getSP()
|
const item = (await getSP()
|
||||||
.web.lists.getByTitle(LIST_TITLE)
|
.web.lists.getByTitle(dataListTitle(FORM_ID))
|
||||||
.items.getById(itemId)
|
.items.getById(itemId)
|
||||||
.select(...scalarSelects, 'RequestStatus', 'RequestUniqueCode', ...personSelects)
|
.select(...scalarSelects, 'RequestStatus', 'RequestUniqueCode', ...personSelects)
|
||||||
.expand(...expands)()) as Record<string, unknown>;
|
.expand(...expands)()) as Record<string, unknown>;
|
||||||
|
|||||||
@@ -1,5 +1,12 @@
|
|||||||
import { WebPartContext } from '@microsoft/sp-webpart-base';
|
import { WebPartContext } from '@microsoft/sp-webpart-base';
|
||||||
import { ISelectOption } from '../../shared/ui';
|
import { ISelectOption } from '../../shared/ui';
|
||||||
|
import { RequestTypeId } from '../../models/types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Тип заявки этой формы — ключ реестра форм (forms/registry.ts). Имена списков
|
||||||
|
* SharePoint выводятся из него, см. shared/listTitles.ts.
|
||||||
|
*/
|
||||||
|
export const FORM_ID: RequestTypeId = 'recruiting-selection';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Данные формы «Заявка на подбор». Ключи = internalName колонок SP-списка
|
* Данные формы «Заявка на подбор». Ключи = internalName колонок SP-списка
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
import { getSP } from '../../shared/pnp/pnpjs';
|
import { getSP } from '../../shared/pnp/pnpjs';
|
||||||
import { ApprovalListService, IApproverToWrite, writeRoute } from '../../shared/approval';
|
import { ApprovalListService, IApproverToWrite, writeRoute } from '../../shared/approval';
|
||||||
import { buildRequestUniqueCode } from '../../shared/requestCode';
|
import { buildRequestUniqueCode } from '../../shared/requestCode';
|
||||||
import { IFormData } from './model';
|
import { approvalListTitle, dataListTitle } from '../../shared/listTitles';
|
||||||
|
import { FORM_ID, IFormData } from './model';
|
||||||
export const DATA_LIST_TITLE = 'Заявка на подбор — данные';
|
|
||||||
export const APPROVAL_LIST_TITLE = 'Заявка на подбор — согласование';
|
|
||||||
|
|
||||||
// Поля-люди (один человек): логин → ensureUser → Id в колонку <Field>Id.
|
// Поля-люди (один человек): логин → ensureUser → Id в колонку <Field>Id.
|
||||||
const PERSON_FIELDS: (keyof IFormData)[] = ['Responsible', 'ProposedMentor', 'DirectManager'];
|
const PERSON_FIELDS: (keyof IFormData)[] = ['Responsible', 'ProposedMentor', 'DirectManager'];
|
||||||
@@ -15,7 +13,7 @@ const NUMBER_FIELDS: (keyof IFormData)[] = ['DepartmentHeadcount', 'RateProbatio
|
|||||||
|
|
||||||
/** Экземпляр сервиса списка согласования этой формы. */
|
/** Экземпляр сервиса списка согласования этой формы. */
|
||||||
export function selectionApprovalService(): ApprovalListService {
|
export function selectionApprovalService(): ApprovalListService {
|
||||||
return new ApprovalListService(APPROVAL_LIST_TITLE);
|
return new ApprovalListService(approvalListTitle(FORM_ID));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Логин → числовой Id пользователя на текущем сайте. */
|
/** Логин → числовой Id пользователя на текущем сайте. */
|
||||||
@@ -78,7 +76,7 @@ export async function saveSelectionRequest(
|
|||||||
approvers: IApproverToWrite[],
|
approvers: IApproverToWrite[],
|
||||||
status: string
|
status: string
|
||||||
): Promise<ISaveResult> {
|
): Promise<ISaveResult> {
|
||||||
const list = getSP().web.lists.getByTitle(DATA_LIST_TITLE);
|
const list = getSP().web.lists.getByTitle(dataListTitle(FORM_ID));
|
||||||
const item = buildScalarBody(data);
|
const item = buildScalarBody(data);
|
||||||
item.RequestStatus = status;
|
item.RequestStatus = status;
|
||||||
await applyPeople(item, data);
|
await applyPeople(item, data);
|
||||||
|
|||||||
@@ -35,14 +35,9 @@ import {
|
|||||||
} from '../../shared/attachments/attachments.service';
|
} from '../../shared/attachments/attachments.service';
|
||||||
import { effectiveUserEmail } from '../../shared/impersonation';
|
import { effectiveUserEmail } from '../../shared/impersonation';
|
||||||
import { IRequestFormProps } from '../../models/types';
|
import { IRequestFormProps } from '../../models/types';
|
||||||
import { IFormData, EMPTY_DATA, SetField } from './model';
|
import { approvalListTitle, dataListTitle } from '../../shared/listTitles';
|
||||||
import {
|
import { FORM_ID, IFormData, EMPTY_DATA, SetField } from './model';
|
||||||
saveTicketRequest,
|
import { saveTicketRequest, setPlannerTaskId, ticket1cApprovalService } from './submit';
|
||||||
setPlannerTaskId,
|
|
||||||
ticket1cApprovalService,
|
|
||||||
DATA_LIST_TITLE,
|
|
||||||
APPROVAL_LIST_TITLE
|
|
||||||
} from './submit';
|
|
||||||
import { loadTicketRequest } from './load';
|
import { loadTicketRequest } from './load';
|
||||||
import { PlannerTicketService } from './services/plannerTicket.service';
|
import { PlannerTicketService } from './services/plannerTicket.service';
|
||||||
import { getApplicantProfile } from './services/profile.service';
|
import { getApplicantProfile } from './services/profile.service';
|
||||||
@@ -86,7 +81,7 @@ const Ticket1CForm: React.FC<IRequestFormProps> = ({ requestType, context, onCan
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let alive = true;
|
let alive = true;
|
||||||
Promise.all([loadTicketRequest(editId), getAttachments(DATA_LIST_TITLE, editId)])
|
Promise.all([loadTicketRequest(editId), getAttachments(dataListTitle(FORM_ID), editId)])
|
||||||
.then(([loaded, atts]) => {
|
.then(([loaded, atts]) => {
|
||||||
if (alive) {
|
if (alive) {
|
||||||
setData(loaded.data);
|
setData(loaded.data);
|
||||||
@@ -193,7 +188,7 @@ const Ticket1CForm: React.FC<IRequestFormProps> = ({ requestType, context, onCan
|
|||||||
if (requestId === undefined) {
|
if (requestId === undefined) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
deleteAttachment(DATA_LIST_TITLE, requestId, fileName)
|
deleteAttachment(dataListTitle(FORM_ID), requestId, fileName)
|
||||||
.then(() => setExistingAttachments((prev) => prev.filter((a) => a.fileName !== fileName)))
|
.then(() => setExistingAttachments((prev) => prev.filter((a) => a.fileName !== fileName)))
|
||||||
.catch(() => undefined);
|
.catch(() => undefined);
|
||||||
};
|
};
|
||||||
@@ -224,9 +219,9 @@ const Ticket1CForm: React.FC<IRequestFormProps> = ({ requestType, context, onCan
|
|||||||
|
|
||||||
let attachments = existingAttachments;
|
let attachments = existingAttachments;
|
||||||
if (files.length > 0) {
|
if (files.length > 0) {
|
||||||
await uploadAttachments(DATA_LIST_TITLE, id, files);
|
await uploadAttachments(dataListTitle(FORM_ID), id, files);
|
||||||
setFiles([]);
|
setFiles([]);
|
||||||
attachments = await getAttachments(DATA_LIST_TITLE, id);
|
attachments = await getAttachments(dataListTitle(FORM_ID), id);
|
||||||
setExistingAttachments(attachments);
|
setExistingAttachments(attachments);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -237,8 +232,8 @@ const Ticket1CForm: React.FC<IRequestFormProps> = ({ requestType, context, onCan
|
|||||||
if (finalStatus === REQUEST_STATUS.onApproval) {
|
if (finalStatus === REQUEST_STATUS.onApproval) {
|
||||||
await notifyActiveStep(context, approval, id, {
|
await notifyActiveStep(context, approval, id, {
|
||||||
requestTypeId: rtId,
|
requestTypeId: rtId,
|
||||||
dataListName: DATA_LIST_TITLE,
|
dataListName: dataListTitle(FORM_ID),
|
||||||
approvalListName: APPROVAL_LIST_TITLE
|
approvalListName: approvalListTitle(FORM_ID)
|
||||||
});
|
});
|
||||||
} else if (finalStatus === REQUEST_STATUS.approved) {
|
} else if (finalStatus === REQUEST_STATUS.approved) {
|
||||||
// Согласование не требуется для этого вида работ — сразу передаём в отдел 1С.
|
// Согласование не требуется для этого вида работ — сразу передаём в отдел 1С.
|
||||||
@@ -289,7 +284,7 @@ const Ticket1CForm: React.FC<IRequestFormProps> = ({ requestType, context, onCan
|
|||||||
<FormCard
|
<FormCard
|
||||||
title={requestType.title}
|
title={requestType.title}
|
||||||
subtitle="Категория: IT"
|
subtitle="Категория: IT"
|
||||||
belowSubtitle={<InitiatorLine listTitle={DATA_LIST_TITLE} itemId={requestId} />}
|
belowSubtitle={<InitiatorLine listTitle={dataListTitle(FORM_ID)} itemId={requestId} />}
|
||||||
headerRight={
|
headerRight={
|
||||||
requestId || requestStatus ? (
|
requestId || requestStatus ? (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -27,15 +27,10 @@ import {
|
|||||||
REQUEST_STATUS
|
REQUEST_STATUS
|
||||||
} from '../../shared/approval';
|
} from '../../shared/approval';
|
||||||
import { getAttachments, IAttachmentInfo } from '../../shared/attachments/attachments.service';
|
import { getAttachments, IAttachmentInfo } from '../../shared/attachments/attachments.service';
|
||||||
import { IFormData, EMPTY_DATA, SetField } from './model';
|
import { approvalListTitle, dataListTitle } from '../../shared/listTitles';
|
||||||
|
import { FORM_ID, IFormData, EMPTY_DATA, SetField } from './model';
|
||||||
import { loadTicketRequest } from './load';
|
import { loadTicketRequest } from './load';
|
||||||
import {
|
import { ticket1cApprovalService, getPlannerTaskId, setPlannerTaskId } from './submit';
|
||||||
ticket1cApprovalService,
|
|
||||||
getPlannerTaskId,
|
|
||||||
setPlannerTaskId,
|
|
||||||
DATA_LIST_TITLE,
|
|
||||||
APPROVAL_LIST_TITLE
|
|
||||||
} from './submit';
|
|
||||||
import { PlannerTicketService } from './services/plannerTicket.service';
|
import { PlannerTicketService } from './services/plannerTicket.service';
|
||||||
import { PLANNER_PLAN_ID, PLANNER_BUCKET_NAME, PLANNER_REJECTED_BUCKET_NAME } from './config';
|
import { PLANNER_PLAN_ID, PLANNER_BUCKET_NAME, PLANNER_REJECTED_BUCKET_NAME } from './config';
|
||||||
import { RequestSection } from './sections/RequestSection';
|
import { RequestSection } from './sections/RequestSection';
|
||||||
@@ -89,7 +84,7 @@ const Ticket1CView: React.FC<ITicket1CViewProps> = ({ context, itemId, formKey }
|
|||||||
const set: SetField = (key, value) => setData((prev) => ({ ...prev, [key]: value }));
|
const set: SetField = (key, value) => setData((prev) => ({ ...prev, [key]: value }));
|
||||||
|
|
||||||
const approverService = useMemo(
|
const approverService = useMemo(
|
||||||
() => new ApproverService(ticket1cApprovalService(), DATA_LIST_TITLE),
|
() => new ApproverService(ticket1cApprovalService(), dataListTitle(FORM_ID)),
|
||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
const approval = useMemo(() => ticket1cApprovalService(), []);
|
const approval = useMemo(() => ticket1cApprovalService(), []);
|
||||||
@@ -105,7 +100,7 @@ const Ticket1CView: React.FC<ITicket1CViewProps> = ({ context, itemId, formKey }
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let alive = true;
|
let alive = true;
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
Promise.all([loadTicketRequest(itemId), getAttachments(DATA_LIST_TITLE, itemId)])
|
Promise.all([loadTicketRequest(itemId), getAttachments(dataListTitle(FORM_ID), itemId)])
|
||||||
.then(([loaded, files]) => {
|
.then(([loaded, files]) => {
|
||||||
if (alive) {
|
if (alive) {
|
||||||
setData(loaded.data);
|
setData(loaded.data);
|
||||||
@@ -148,7 +143,7 @@ const Ticket1CView: React.FC<ITicket1CViewProps> = ({ context, itemId, formKey }
|
|||||||
|
|
||||||
const bucketName =
|
const bucketName =
|
||||||
finalStatus === REQUEST_STATUS.rejected ? PLANNER_REJECTED_BUCKET_NAME : PLANNER_BUCKET_NAME;
|
finalStatus === REQUEST_STATUS.rejected ? PLANNER_REJECTED_BUCKET_NAME : PLANNER_BUCKET_NAME;
|
||||||
const atts = await getAttachments(DATA_LIST_TITLE, itemId);
|
const atts = await getAttachments(dataListTitle(FORM_ID), itemId);
|
||||||
const taskId = await new PlannerTicketService(context).createTask({
|
const taskId = await new PlannerTicketService(context).createTask({
|
||||||
planId: PLANNER_PLAN_ID,
|
planId: PLANNER_PLAN_ID,
|
||||||
bucketName,
|
bucketName,
|
||||||
@@ -179,8 +174,8 @@ const Ticket1CView: React.FC<ITicket1CViewProps> = ({ context, itemId, formKey }
|
|||||||
if (!requestTypeId) return;
|
if (!requestTypeId) return;
|
||||||
notifyActiveStep(context, approval, itemId, {
|
notifyActiveStep(context, approval, itemId, {
|
||||||
requestTypeId,
|
requestTypeId,
|
||||||
dataListName: DATA_LIST_TITLE,
|
dataListName: dataListTitle(FORM_ID),
|
||||||
approvalListName: APPROVAL_LIST_TITLE
|
approvalListName: approvalListTitle(FORM_ID)
|
||||||
}).catch(() => undefined);
|
}).catch(() => undefined);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -203,7 +198,7 @@ const Ticket1CView: React.FC<ITicket1CViewProps> = ({ context, itemId, formKey }
|
|||||||
<FormCard
|
<FormCard
|
||||||
title="Заявка в отдел 1С"
|
title="Заявка в отдел 1С"
|
||||||
subtitle={`№${code || itemId}`}
|
subtitle={`№${code || itemId}`}
|
||||||
belowSubtitle={<InitiatorLine listTitle={DATA_LIST_TITLE} itemId={itemId} />}
|
belowSubtitle={<InitiatorLine listTitle={dataListTitle(FORM_ID)} itemId={itemId} />}
|
||||||
headerRight={status ? <StatusBadge status={status} /> : undefined}
|
headerRight={status ? <StatusBadge status={status} /> : undefined}
|
||||||
>
|
>
|
||||||
<FormModeProvider readOnly>
|
<FormModeProvider readOnly>
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { getSP } from '../../shared/pnp/pnpjs';
|
import { getSP } from '../../shared/pnp/pnpjs';
|
||||||
import { IFormData, EMPTY_DATA } from './model';
|
import { dataListTitle } from '../../shared/listTitles';
|
||||||
|
import { FORM_ID, IFormData, EMPTY_DATA } from './model';
|
||||||
const LIST_TITLE = 'Заявка в отдел 1С — данные';
|
|
||||||
|
|
||||||
// TaskName пишется/читается через системную колонку Title (см. provisioning.json/titleColumnName).
|
// TaskName пишется/читается через системную колонку Title (см. provisioning.json/titleColumnName).
|
||||||
const SCALAR_FIELDS: (keyof IFormData)[] = [
|
const SCALAR_FIELDS: (keyof IFormData)[] = [
|
||||||
@@ -18,7 +17,7 @@ export interface ILoadedRequest {
|
|||||||
/** Прочитать заявку по id: модель формы + статус (для режима чтения). */
|
/** Прочитать заявку по id: модель формы + статус (для режима чтения). */
|
||||||
export async function loadTicketRequest(itemId: number): Promise<ILoadedRequest> {
|
export async function loadTicketRequest(itemId: number): Promise<ILoadedRequest> {
|
||||||
const item = (await getSP()
|
const item = (await getSP()
|
||||||
.web.lists.getByTitle(LIST_TITLE)
|
.web.lists.getByTitle(dataListTitle(FORM_ID))
|
||||||
.items.getById(itemId)
|
.items.getById(itemId)
|
||||||
.select(...SCALAR_FIELDS, 'Title', 'RequestStatus', 'RequestUniqueCode')()) as Record<string, unknown>;
|
.select(...SCALAR_FIELDS, 'Title', 'RequestStatus', 'RequestUniqueCode')()) as Record<string, unknown>;
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,15 @@
|
|||||||
import { WebPartContext } from '@microsoft/sp-webpart-base';
|
import { WebPartContext } from '@microsoft/sp-webpart-base';
|
||||||
|
import { RequestTypeId } from '../../models/types';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Данные формы «Заявка в отдел 1С». Ключи = internalName колонок SP-списка
|
* Тип заявки этой формы — ключ реестра форм (forms/registry.ts). Имена списков
|
||||||
* «Заявка в отдел 1С — данные».
|
* SharePoint выводятся из него, см. shared/listTitles.ts.
|
||||||
|
*/
|
||||||
|
export const FORM_ID: RequestTypeId = 'it-1c-ticket';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Данные формы «Заявка в отдел 1С». Ключи = internalName колонок списка данных
|
||||||
|
* этой формы (см. shared/listTitles.ts).
|
||||||
*/
|
*/
|
||||||
export interface IFormData {
|
export interface IFormData {
|
||||||
/** Пишется в системную колонку Title (см. provisioning.json/titleColumnName). */
|
/** Пишется в системную колонку Title (см. provisioning.json/titleColumnName). */
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import { getSP } from '../../shared/pnp/pnpjs';
|
import { getSP } from '../../shared/pnp/pnpjs';
|
||||||
import { ApprovalListService, IApproverToWrite, writeRoute } from '../../shared/approval';
|
import { ApprovalListService, IApproverToWrite, writeRoute } from '../../shared/approval';
|
||||||
import { buildRequestUniqueCode } from '../../shared/requestCode';
|
import { buildRequestUniqueCode } from '../../shared/requestCode';
|
||||||
import { IFormData } from './model';
|
import { approvalListTitle, dataListTitle } from '../../shared/listTitles';
|
||||||
|
import { FORM_ID, IFormData } from './model';
|
||||||
|
|
||||||
export const DATA_LIST_TITLE = 'Заявка в отдел 1С — данные';
|
|
||||||
export const APPROVAL_LIST_TITLE = 'Заявка в отдел 1С — согласование';
|
|
||||||
// Префикс уникального кода заявки — буквенная метка «1С» вместо числового id
|
// Префикс уникального кода заявки — буквенная метка «1С» вместо числового id
|
||||||
// типа заявки. Числовой RequestTypeId (для резолва маршрута/уведомлений)
|
// типа заявки. Числовой RequestTypeId (для резолва маршрута/уведомлений)
|
||||||
// больше не хардкодится — см. useRequestTypeId в Form.tsx/View.tsx.
|
// больше не хардкодится — см. useRequestTypeId в Form.tsx/View.tsx.
|
||||||
@@ -12,7 +11,7 @@ export const TICKET1C_CODE_PREFIX = '1С';
|
|||||||
|
|
||||||
/** Экземпляр сервиса списка согласования этой формы. */
|
/** Экземпляр сервиса списка согласования этой формы. */
|
||||||
export function ticket1cApprovalService(): ApprovalListService {
|
export function ticket1cApprovalService(): ApprovalListService {
|
||||||
return new ApprovalListService(APPROVAL_LIST_TITLE);
|
return new ApprovalListService(approvalListTitle(FORM_ID));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Поля элемента списка данных из модели формы. TaskName пишется в системную колонку Title. */
|
/** Поля элемента списка данных из модели формы. TaskName пишется в системную колонку Title. */
|
||||||
@@ -46,7 +45,7 @@ export async function saveTicketRequest(
|
|||||||
approvers: IApproverToWrite[],
|
approvers: IApproverToWrite[],
|
||||||
status: string
|
status: string
|
||||||
): Promise<ISaveResult> {
|
): Promise<ISaveResult> {
|
||||||
const list = getSP().web.lists.getByTitle(DATA_LIST_TITLE);
|
const list = getSP().web.lists.getByTitle(dataListTitle(FORM_ID));
|
||||||
const item = buildItem(data, status);
|
const item = buildItem(data, status);
|
||||||
|
|
||||||
let id: number;
|
let id: number;
|
||||||
@@ -73,12 +72,12 @@ export async function saveTicketRequest(
|
|||||||
*/
|
*/
|
||||||
export async function getPlannerTaskId(id: number): Promise<string | undefined> {
|
export async function getPlannerTaskId(id: number): Promise<string | undefined> {
|
||||||
const item = (await getSP()
|
const item = (await getSP()
|
||||||
.web.lists.getByTitle(DATA_LIST_TITLE)
|
.web.lists.getByTitle(dataListTitle(FORM_ID))
|
||||||
.items.getById(id)
|
.items.getById(id)
|
||||||
.select('PlannerTaskId')()) as { PlannerTaskId?: string };
|
.select('PlannerTaskId')()) as { PlannerTaskId?: string };
|
||||||
return item.PlannerTaskId || undefined;
|
return item.PlannerTaskId || undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function setPlannerTaskId(id: number, taskId: string): Promise<void> {
|
export async function setPlannerTaskId(id: number, taskId: string): Promise<void> {
|
||||||
await getSP().web.lists.getByTitle(DATA_LIST_TITLE).items.getById(id).update({ PlannerTaskId: taskId });
|
await getSP().web.lists.getByTitle(dataListTitle(FORM_ID)).items.getById(id).update({ PlannerTaskId: taskId });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,8 +36,9 @@ import {
|
|||||||
} from '../../shared/attachments/attachments.service';
|
} from '../../shared/attachments/attachments.service';
|
||||||
import { effectiveUserEmail } from '../../shared/impersonation';
|
import { effectiveUserEmail } from '../../shared/impersonation';
|
||||||
import { IRequestFormProps } from '../../models/types';
|
import { IRequestFormProps } from '../../models/types';
|
||||||
import { IFormData, EMPTY_DATA, SetField } from './model';
|
import { approvalListTitle, dataListTitle } from '../../shared/listTitles';
|
||||||
import { saveTmcTransferRequest, tmcTransferApprovalService, DATA_LIST_TITLE, APPROVAL_LIST_TITLE } from './submit';
|
import { FORM_ID, IFormData, EMPTY_DATA, SetField } from './model';
|
||||||
|
import { saveTmcTransferRequest, tmcTransferApprovalService } from './submit';
|
||||||
import { loadTmcTransferRequest } from './load';
|
import { loadTmcTransferRequest } from './load';
|
||||||
import { HeaderSection } from './sections/HeaderSection';
|
import { HeaderSection } from './sections/HeaderSection';
|
||||||
import { ItemsSection } from './sections/ItemsSection';
|
import { ItemsSection } from './sections/ItemsSection';
|
||||||
@@ -79,7 +80,7 @@ const TmcTransferForm: React.FC<IRequestFormProps> = ({ requestType, context, on
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let alive = true;
|
let alive = true;
|
||||||
Promise.all([loadTmcTransferRequest(editId), getAttachments(DATA_LIST_TITLE, editId)])
|
Promise.all([loadTmcTransferRequest(editId), getAttachments(dataListTitle(FORM_ID), editId)])
|
||||||
.then(([loaded, atts]) => {
|
.then(([loaded, atts]) => {
|
||||||
if (alive) {
|
if (alive) {
|
||||||
setData(loaded.data);
|
setData(loaded.data);
|
||||||
@@ -173,7 +174,7 @@ const TmcTransferForm: React.FC<IRequestFormProps> = ({ requestType, context, on
|
|||||||
if (requestId === undefined) {
|
if (requestId === undefined) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
deleteAttachment(DATA_LIST_TITLE, requestId, fileName)
|
deleteAttachment(dataListTitle(FORM_ID), requestId, fileName)
|
||||||
.then(() => setExistingAttachments((prev) => prev.filter((a) => a.fileName !== fileName)))
|
.then(() => setExistingAttachments((prev) => prev.filter((a) => a.fileName !== fileName)))
|
||||||
.catch(() => undefined);
|
.catch(() => undefined);
|
||||||
};
|
};
|
||||||
@@ -200,9 +201,9 @@ const TmcTransferForm: React.FC<IRequestFormProps> = ({ requestType, context, on
|
|||||||
setRequestCode(code);
|
setRequestCode(code);
|
||||||
}
|
}
|
||||||
if (files.length > 0) {
|
if (files.length > 0) {
|
||||||
await uploadAttachments(DATA_LIST_TITLE, id, files);
|
await uploadAttachments(dataListTitle(FORM_ID), id, files);
|
||||||
setFiles([]);
|
setFiles([]);
|
||||||
setExistingAttachments(await getAttachments(DATA_LIST_TITLE, id));
|
setExistingAttachments(await getAttachments(dataListTitle(FORM_ID), id));
|
||||||
}
|
}
|
||||||
if (onSaved) {
|
if (onSaved) {
|
||||||
onSaved();
|
onSaved();
|
||||||
@@ -210,8 +211,8 @@ const TmcTransferForm: React.FC<IRequestFormProps> = ({ requestType, context, on
|
|||||||
if (status === REQUEST_STATUS.onApproval) {
|
if (status === REQUEST_STATUS.onApproval) {
|
||||||
await notifyActiveStep(context, approval, id, {
|
await notifyActiveStep(context, approval, id, {
|
||||||
requestTypeId: rtId,
|
requestTypeId: rtId,
|
||||||
dataListName: DATA_LIST_TITLE,
|
dataListName: dataListTitle(FORM_ID),
|
||||||
approvalListName: APPROVAL_LIST_TITLE
|
approvalListName: approvalListTitle(FORM_ID)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -247,7 +248,7 @@ const TmcTransferForm: React.FC<IRequestFormProps> = ({ requestType, context, on
|
|||||||
<FormCard
|
<FormCard
|
||||||
title={requestType.title}
|
title={requestType.title}
|
||||||
subtitle="Категория: Склад / Снабжение"
|
subtitle="Категория: Склад / Снабжение"
|
||||||
belowSubtitle={<InitiatorLine listTitle={DATA_LIST_TITLE} itemId={requestId} />}
|
belowSubtitle={<InitiatorLine listTitle={dataListTitle(FORM_ID)} itemId={requestId} />}
|
||||||
headerRight={
|
headerRight={
|
||||||
requestId || requestStatus ? (
|
requestId || requestStatus ? (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -27,9 +27,10 @@ import {
|
|||||||
APPROVAL_PHASE
|
APPROVAL_PHASE
|
||||||
} from '../../shared/approval';
|
} from '../../shared/approval';
|
||||||
import { getAttachments, IAttachmentInfo } from '../../shared/attachments/attachments.service';
|
import { getAttachments, IAttachmentInfo } from '../../shared/attachments/attachments.service';
|
||||||
import { IFormData, EMPTY_DATA, SetField } from './model';
|
import { approvalListTitle, dataListTitle } from '../../shared/listTitles';
|
||||||
|
import { FORM_ID, IFormData, EMPTY_DATA, SetField } from './model';
|
||||||
import { loadTmcTransferRequest } from './load';
|
import { loadTmcTransferRequest } from './load';
|
||||||
import { tmcTransferApprovalService, DATA_LIST_TITLE, APPROVAL_LIST_TITLE } from './submit';
|
import { tmcTransferApprovalService } from './submit';
|
||||||
import { HeaderSection } from './sections/HeaderSection';
|
import { HeaderSection } from './sections/HeaderSection';
|
||||||
import { ItemsSection } from './sections/ItemsSection';
|
import { ItemsSection } from './sections/ItemsSection';
|
||||||
|
|
||||||
@@ -54,7 +55,7 @@ const TmcTransferView: React.FC<ITmcTransferViewProps> = ({ context, itemId, for
|
|||||||
const set: SetField = () => undefined;
|
const set: SetField = () => undefined;
|
||||||
|
|
||||||
const approverService = useMemo(
|
const approverService = useMemo(
|
||||||
() => new ApproverService(tmcTransferApprovalService(), DATA_LIST_TITLE),
|
() => new ApproverService(tmcTransferApprovalService(), dataListTitle(FORM_ID)),
|
||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -84,7 +85,7 @@ const TmcTransferView: React.FC<ITmcTransferViewProps> = ({ context, itemId, for
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let alive = true;
|
let alive = true;
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
Promise.all([loadTmcTransferRequest(itemId), getAttachments(DATA_LIST_TITLE, itemId)])
|
Promise.all([loadTmcTransferRequest(itemId), getAttachments(dataListTitle(FORM_ID), itemId)])
|
||||||
.then(([loaded, files]) => {
|
.then(([loaded, files]) => {
|
||||||
if (alive) {
|
if (alive) {
|
||||||
setData(loaded.data);
|
setData(loaded.data);
|
||||||
@@ -123,8 +124,8 @@ const TmcTransferView: React.FC<ITmcTransferViewProps> = ({ context, itemId, for
|
|||||||
if (!requestTypeId) return;
|
if (!requestTypeId) return;
|
||||||
notifyActiveStep(context, approval, itemId, {
|
notifyActiveStep(context, approval, itemId, {
|
||||||
requestTypeId,
|
requestTypeId,
|
||||||
dataListName: DATA_LIST_TITLE,
|
dataListName: dataListTitle(FORM_ID),
|
||||||
approvalListName: APPROVAL_LIST_TITLE
|
approvalListName: approvalListTitle(FORM_ID)
|
||||||
}).catch(() => undefined);
|
}).catch(() => undefined);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -147,7 +148,7 @@ const TmcTransferView: React.FC<ITmcTransferViewProps> = ({ context, itemId, for
|
|||||||
<FormCard
|
<FormCard
|
||||||
title="Служебная записка на перемещение ТМЦ"
|
title="Служебная записка на перемещение ТМЦ"
|
||||||
subtitle={`№${code || itemId}`}
|
subtitle={`№${code || itemId}`}
|
||||||
belowSubtitle={<InitiatorLine listTitle={DATA_LIST_TITLE} itemId={itemId} />}
|
belowSubtitle={<InitiatorLine listTitle={dataListTitle(FORM_ID)} itemId={itemId} />}
|
||||||
headerRight={status ? <StatusBadge status={status} /> : undefined}
|
headerRight={status ? <StatusBadge status={status} /> : undefined}
|
||||||
>
|
>
|
||||||
<FormModeProvider readOnly>
|
<FormModeProvider readOnly>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { getSP } from '../../shared/pnp/pnpjs';
|
import { getSP } from '../../shared/pnp/pnpjs';
|
||||||
import { ILineItem } from '../../shared/ui';
|
import { ILineItem } from '../../shared/ui';
|
||||||
import { IFormData, EMPTY_DATA } from './model';
|
import { dataListTitle } from '../../shared/listTitles';
|
||||||
import { DATA_LIST_TITLE } from './submit';
|
import { FORM_ID, IFormData, EMPTY_DATA } from './model';
|
||||||
|
|
||||||
export interface ILoadedRequest {
|
export interface ILoadedRequest {
|
||||||
data: IFormData;
|
data: IFormData;
|
||||||
@@ -24,7 +24,7 @@ function parseItems(raw: unknown): ILineItem[] {
|
|||||||
/** Прочитать заявку «СЗ на перемещение ТМЦ» по id: модель формы + статус + код. */
|
/** Прочитать заявку «СЗ на перемещение ТМЦ» по id: модель формы + статус + код. */
|
||||||
export async function loadTmcTransferRequest(itemId: number): Promise<ILoadedRequest> {
|
export async function loadTmcTransferRequest(itemId: number): Promise<ILoadedRequest> {
|
||||||
const item = (await getSP()
|
const item = (await getSP()
|
||||||
.web.lists.getByTitle(DATA_LIST_TITLE)
|
.web.lists.getByTitle(dataListTitle(FORM_ID))
|
||||||
.items.getById(itemId)
|
.items.getById(itemId)
|
||||||
.select(
|
.select(
|
||||||
'Id',
|
'Id',
|
||||||
|
|||||||
@@ -1,5 +1,12 @@
|
|||||||
import { WebPartContext } from '@microsoft/sp-webpart-base';
|
import { WebPartContext } from '@microsoft/sp-webpart-base';
|
||||||
import { ILineItem } from '../../shared/ui';
|
import { ILineItem } from '../../shared/ui';
|
||||||
|
import { RequestTypeId } from '../../models/types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Тип заявки этой формы — ключ реестра форм (forms/registry.ts). Имена списков
|
||||||
|
* SharePoint выводятся из него, см. shared/listTitles.ts.
|
||||||
|
*/
|
||||||
|
export const FORM_ID: RequestTypeId = 'wh-tmc-transfer';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Данные формы «Служебная записка на перемещение ТМЦ».
|
* Данные формы «Служебная записка на перемещение ТМЦ».
|
||||||
|
|||||||
@@ -2,14 +2,12 @@ import { getSP } from '../../shared/pnp/pnpjs';
|
|||||||
import { ApprovalListService, IApproverToWrite, writeRoute } from '../../shared/approval';
|
import { ApprovalListService, IApproverToWrite, writeRoute } from '../../shared/approval';
|
||||||
import { buildRequestUniqueCode } from '../../shared/requestCode';
|
import { buildRequestUniqueCode } from '../../shared/requestCode';
|
||||||
import { ILineItem } from '../../shared/ui';
|
import { ILineItem } from '../../shared/ui';
|
||||||
import { IFormData } from './model';
|
import { approvalListTitle, dataListTitle } from '../../shared/listTitles';
|
||||||
|
import { FORM_ID, IFormData } from './model';
|
||||||
export const DATA_LIST_TITLE = 'СЗ на перемещение ТМЦ — данные';
|
|
||||||
export const APPROVAL_LIST_TITLE = 'СЗ на перемещение ТМЦ — согласование';
|
|
||||||
|
|
||||||
/** Экземпляр сервиса списка согласования этой формы. */
|
/** Экземпляр сервиса списка согласования этой формы. */
|
||||||
export function tmcTransferApprovalService(): ApprovalListService {
|
export function tmcTransferApprovalService(): ApprovalListService {
|
||||||
return new ApprovalListService(APPROVAL_LIST_TITLE);
|
return new ApprovalListService(approvalListTitle(FORM_ID));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Осмысленный заголовок элемента (системная колонка Title). */
|
/** Осмысленный заголовок элемента (системная колонка Title). */
|
||||||
@@ -55,7 +53,7 @@ export async function saveTmcTransferRequest(
|
|||||||
executors: IApproverToWrite[],
|
executors: IApproverToWrite[],
|
||||||
status: string
|
status: string
|
||||||
): Promise<ISaveResult> {
|
): Promise<ISaveResult> {
|
||||||
const list = getSP().web.lists.getByTitle(DATA_LIST_TITLE);
|
const list = getSP().web.lists.getByTitle(dataListTitle(FORM_ID));
|
||||||
const item = buildItem(data, status);
|
const item = buildItem(data, status);
|
||||||
|
|
||||||
let id: number;
|
let id: number;
|
||||||
|
|||||||
@@ -24,8 +24,9 @@ import {
|
|||||||
findUnassignedStep
|
findUnassignedStep
|
||||||
} from '../../shared/approval';
|
} from '../../shared/approval';
|
||||||
import { IRequestFormProps } from '../../models/types';
|
import { IRequestFormProps } from '../../models/types';
|
||||||
import { IFormData, EMPTY_DATA, SetField } from './model';
|
import { approvalListTitle, dataListTitle } from '../../shared/listTitles';
|
||||||
import { saveVacationRequest, vacationApprovalService, DATA_LIST_TITLE, APPROVAL_LIST_TITLE } from './submit';
|
import { FORM_ID, IFormData, EMPTY_DATA, SetField } from './model';
|
||||||
|
import { saveVacationRequest, vacationApprovalService } from './submit';
|
||||||
import { loadVacationRequest } from './load';
|
import { loadVacationRequest } from './load';
|
||||||
import { RequestSection } from './sections/RequestSection';
|
import { RequestSection } from './sections/RequestSection';
|
||||||
|
|
||||||
@@ -170,8 +171,8 @@ const VacationForm: React.FC<IRequestFormProps> = ({ requestType, context, onCan
|
|||||||
if (status === REQUEST_STATUS.onApproval) {
|
if (status === REQUEST_STATUS.onApproval) {
|
||||||
await notifyActiveStep(context, approval, id, {
|
await notifyActiveStep(context, approval, id, {
|
||||||
requestTypeId: rtId,
|
requestTypeId: rtId,
|
||||||
dataListName: DATA_LIST_TITLE,
|
dataListName: dataListTitle(FORM_ID),
|
||||||
approvalListName: APPROVAL_LIST_TITLE
|
approvalListName: approvalListTitle(FORM_ID)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -206,7 +207,7 @@ const VacationForm: React.FC<IRequestFormProps> = ({ requestType, context, onCan
|
|||||||
<FormCard
|
<FormCard
|
||||||
title={requestType.title}
|
title={requestType.title}
|
||||||
subtitle="Категория: Отдел кадров"
|
subtitle="Категория: Отдел кадров"
|
||||||
belowSubtitle={<InitiatorLine listTitle={DATA_LIST_TITLE} itemId={requestId} />}
|
belowSubtitle={<InitiatorLine listTitle={dataListTitle(FORM_ID)} itemId={requestId} />}
|
||||||
headerRight={
|
headerRight={
|
||||||
requestId || requestStatus ? (
|
requestId || requestStatus ? (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -12,9 +12,10 @@ import {
|
|||||||
notifyActiveStep,
|
notifyActiveStep,
|
||||||
useRequestTypeId
|
useRequestTypeId
|
||||||
} from '../../shared/approval';
|
} from '../../shared/approval';
|
||||||
import { IFormData, EMPTY_DATA, SetField } from './model';
|
import { approvalListTitle, dataListTitle } from '../../shared/listTitles';
|
||||||
|
import { FORM_ID, IFormData, EMPTY_DATA, SetField } from './model';
|
||||||
import { loadVacationRequest } from './load';
|
import { loadVacationRequest } from './load';
|
||||||
import { vacationApprovalService, DATA_LIST_TITLE, APPROVAL_LIST_TITLE } from './submit';
|
import { vacationApprovalService } from './submit';
|
||||||
import { RequestSection } from './sections/RequestSection';
|
import { RequestSection } from './sections/RequestSection';
|
||||||
|
|
||||||
interface IVacationViewProps {
|
interface IVacationViewProps {
|
||||||
@@ -39,7 +40,7 @@ const VacationView: React.FC<IVacationViewProps> = ({ context, itemId, formKey }
|
|||||||
|
|
||||||
const approval = useMemo(() => vacationApprovalService(), []);
|
const approval = useMemo(() => vacationApprovalService(), []);
|
||||||
const { id: requestTypeId } = useRequestTypeId(context, formKey);
|
const { id: requestTypeId } = useRequestTypeId(context, formKey);
|
||||||
const approverService = useMemo(() => new ApproverService(approval, DATA_LIST_TITLE), [approval]);
|
const approverService = useMemo(() => new ApproverService(approval, dataListTitle(FORM_ID)), [approval]);
|
||||||
|
|
||||||
// Матрица маршрута из фактических строк заявки (read-only). decisionSeq в
|
// Матрица маршрута из фактических строк заявки (read-only). decisionSeq в
|
||||||
// reloadKey: после решения строки на сервере меняются — перечитываем матрицу.
|
// reloadKey: после решения строки на сервере меняются — перечитываем матрицу.
|
||||||
@@ -93,8 +94,8 @@ const VacationView: React.FC<IVacationViewProps> = ({ context, itemId, formKey }
|
|||||||
if (!requestTypeId) return;
|
if (!requestTypeId) return;
|
||||||
notifyActiveStep(context, approval, itemId, {
|
notifyActiveStep(context, approval, itemId, {
|
||||||
requestTypeId,
|
requestTypeId,
|
||||||
dataListName: DATA_LIST_TITLE,
|
dataListName: dataListTitle(FORM_ID),
|
||||||
approvalListName: APPROVAL_LIST_TITLE
|
approvalListName: approvalListTitle(FORM_ID)
|
||||||
}).catch(() => undefined);
|
}).catch(() => undefined);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -108,7 +109,7 @@ const VacationView: React.FC<IVacationViewProps> = ({ context, itemId, formKey }
|
|||||||
<FormCard
|
<FormCard
|
||||||
title="Заявка на отпуск"
|
title="Заявка на отпуск"
|
||||||
subtitle={`№${code || itemId}`}
|
subtitle={`№${code || itemId}`}
|
||||||
belowSubtitle={<InitiatorLine listTitle={DATA_LIST_TITLE} itemId={itemId} />}
|
belowSubtitle={<InitiatorLine listTitle={dataListTitle(FORM_ID)} itemId={itemId} />}
|
||||||
headerRight={status ? <StatusBadge status={status} /> : undefined}
|
headerRight={status ? <StatusBadge status={status} /> : undefined}
|
||||||
>
|
>
|
||||||
<FormModeProvider readOnly>
|
<FormModeProvider readOnly>
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { getSP } from '../../shared/pnp/pnpjs';
|
import { getSP } from '../../shared/pnp/pnpjs';
|
||||||
import { IFormData, EMPTY_DATA } from './model';
|
import { dataListTitle } from '../../shared/listTitles';
|
||||||
|
import { FORM_ID, IFormData, EMPTY_DATA } from './model';
|
||||||
const LIST_TITLE = 'Заявка на отпуск — данные';
|
|
||||||
|
|
||||||
export interface ILoadedRequest {
|
export interface ILoadedRequest {
|
||||||
data: IFormData;
|
data: IFormData;
|
||||||
@@ -15,7 +14,7 @@ export interface ILoadedRequest {
|
|||||||
/** Прочитать заявку на отпуск по id: модель формы + статус (правка / просмотр). */
|
/** Прочитать заявку на отпуск по id: модель формы + статус (правка / просмотр). */
|
||||||
export async function loadVacationRequest(itemId: number): Promise<ILoadedRequest> {
|
export async function loadVacationRequest(itemId: number): Promise<ILoadedRequest> {
|
||||||
const item = (await getSP()
|
const item = (await getSP()
|
||||||
.web.lists.getByTitle(LIST_TITLE)
|
.web.lists.getByTitle(dataListTitle(FORM_ID))
|
||||||
.items.getById(itemId)
|
.items.getById(itemId)
|
||||||
.select(
|
.select(
|
||||||
'Id',
|
'Id',
|
||||||
|
|||||||
@@ -1,8 +1,15 @@
|
|||||||
import { WebPartContext } from '@microsoft/sp-webpart-base';
|
import { WebPartContext } from '@microsoft/sp-webpart-base';
|
||||||
|
import { RequestTypeId } from '../../models/types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Тип заявки этой формы — ключ реестра форм (forms/registry.ts). Имена списков
|
||||||
|
* SharePoint выводятся из него, см. shared/listTitles.ts.
|
||||||
|
*/
|
||||||
|
export const FORM_ID: RequestTypeId = 'hr-vacation';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Данные формы «Заявка на отпуск». Ключи по возможности = internalName колонок
|
* Данные формы «Заявка на отпуск». Ключи по возможности = internalName колонок
|
||||||
* SP-списка «Заявка на отпуск — данные». Employee хранит e-mail выбранного
|
* списка данных этой формы (см. shared/listTitles.ts). Employee хранит e-mail выбранного
|
||||||
* сотрудника (то, что отдаёт PeopleField) — по нему же резолвится маршрут
|
* сотрудника (то, что отдаёт PeopleField) — по нему же резолвится маршрут
|
||||||
* согласования, поэтому заявку можно подать и за другого сотрудника.
|
* согласования, поэтому заявку можно подать и за другого сотрудника.
|
||||||
* EndDate в форме не вводится: считается при сохранении как начало + (дни − 1).
|
* EndDate в форме не вводится: считается при сохранении как начало + (дни − 1).
|
||||||
|
|||||||
@@ -1,14 +1,12 @@
|
|||||||
import { getSP } from '../../shared/pnp/pnpjs';
|
import { getSP } from '../../shared/pnp/pnpjs';
|
||||||
import { ApprovalListService, IApproverToWrite, writeRoute } from '../../shared/approval';
|
import { ApprovalListService, IApproverToWrite, writeRoute } from '../../shared/approval';
|
||||||
import { buildRequestUniqueCode } from '../../shared/requestCode';
|
import { buildRequestUniqueCode } from '../../shared/requestCode';
|
||||||
import { IFormData } from './model';
|
import { approvalListTitle, dataListTitle } from '../../shared/listTitles';
|
||||||
|
import { FORM_ID, IFormData } from './model';
|
||||||
export const DATA_LIST_TITLE = 'Заявка на отпуск — данные';
|
|
||||||
export const APPROVAL_LIST_TITLE = 'Заявка на отпуск — согласование';
|
|
||||||
|
|
||||||
/** Экземпляр сервиса списка согласования этой формы. */
|
/** Экземпляр сервиса списка согласования этой формы. */
|
||||||
export function vacationApprovalService(): ApprovalListService {
|
export function vacationApprovalService(): ApprovalListService {
|
||||||
return new ApprovalListService(APPROVAL_LIST_TITLE);
|
return new ApprovalListService(approvalListTitle(FORM_ID));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** email / claims-логин → пользователь сайта (Id для Person-поля + Title для заголовка). */
|
/** email / claims-логин → пользователь сайта (Id для Person-поля + Title для заголовка). */
|
||||||
@@ -67,7 +65,7 @@ export async function saveVacationRequest(
|
|||||||
approvers: IApproverToWrite[],
|
approvers: IApproverToWrite[],
|
||||||
status: string
|
status: string
|
||||||
): Promise<ISaveResult> {
|
): Promise<ISaveResult> {
|
||||||
const list = getSP().web.lists.getByTitle(DATA_LIST_TITLE);
|
const list = getSP().web.lists.getByTitle(dataListTitle(FORM_ID));
|
||||||
const employee = await ensureEmployee(data.Employee);
|
const employee = await ensureEmployee(data.Employee);
|
||||||
const item = buildItem(data, status, employee.Id, employee.Title || data.Employee);
|
const item = buildItem(data, status, employee.Id, employee.Title || data.Employee);
|
||||||
|
|
||||||
|
|||||||
@@ -36,8 +36,9 @@ import {
|
|||||||
} from '../../shared/attachments/attachments.service';
|
} from '../../shared/attachments/attachments.service';
|
||||||
import { effectiveUserEmail } from '../../shared/impersonation';
|
import { effectiveUserEmail } from '../../shared/impersonation';
|
||||||
import { IRequestFormProps } from '../../models/types';
|
import { IRequestFormProps } from '../../models/types';
|
||||||
import { IFormData, EMPTY_DATA, SetField } from './model';
|
import { approvalListTitle, dataListTitle } from '../../shared/listTitles';
|
||||||
import { saveWorkplaceRequest, workplaceApprovalService, DATA_LIST_TITLE, APPROVAL_LIST_TITLE } from './submit';
|
import { FORM_ID, IFormData, EMPTY_DATA, SetField } from './model';
|
||||||
|
import { saveWorkplaceRequest, workplaceApprovalService } from './submit';
|
||||||
import { loadPrefillFromHire } from './prefill';
|
import { loadPrefillFromHire } from './prefill';
|
||||||
import { loadWorkplaceRequest } from './load';
|
import { loadWorkplaceRequest } from './load';
|
||||||
import { EmployeeSection } from './sections/EmployeeSection';
|
import { EmployeeSection } from './sections/EmployeeSection';
|
||||||
@@ -67,7 +68,7 @@ const WorkplaceSetupForm: React.FC<IRequestFormProps> = ({ requestType, context,
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let alive = true;
|
let alive = true;
|
||||||
Promise.all([loadWorkplaceRequest(editId), getAttachments(DATA_LIST_TITLE, editId)])
|
Promise.all([loadWorkplaceRequest(editId), getAttachments(dataListTitle(FORM_ID), editId)])
|
||||||
.then(([loaded, atts]) => {
|
.then(([loaded, atts]) => {
|
||||||
if (alive) {
|
if (alive) {
|
||||||
setData(loaded.data);
|
setData(loaded.data);
|
||||||
@@ -180,7 +181,7 @@ const WorkplaceSetupForm: React.FC<IRequestFormProps> = ({ requestType, context,
|
|||||||
if (requestId === undefined) {
|
if (requestId === undefined) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
deleteAttachment(DATA_LIST_TITLE, requestId, fileName)
|
deleteAttachment(dataListTitle(FORM_ID), requestId, fileName)
|
||||||
.then(() => setExistingAttachments((prev) => prev.filter((a) => a.fileName !== fileName)))
|
.then(() => setExistingAttachments((prev) => prev.filter((a) => a.fileName !== fileName)))
|
||||||
.catch(() => undefined);
|
.catch(() => undefined);
|
||||||
};
|
};
|
||||||
@@ -208,9 +209,9 @@ const WorkplaceSetupForm: React.FC<IRequestFormProps> = ({ requestType, context,
|
|||||||
setRequestCode(code);
|
setRequestCode(code);
|
||||||
}
|
}
|
||||||
if (files.length > 0) {
|
if (files.length > 0) {
|
||||||
await uploadAttachments(DATA_LIST_TITLE, id, files);
|
await uploadAttachments(dataListTitle(FORM_ID), id, files);
|
||||||
setFiles([]); // выгруженные файлы убираем, чтобы пересохранение не дублировало
|
setFiles([]); // выгруженные файлы убираем, чтобы пересохранение не дублировало
|
||||||
setExistingAttachments(await getAttachments(DATA_LIST_TITLE, id)); // показать добавленные
|
setExistingAttachments(await getAttachments(dataListTitle(FORM_ID), id)); // показать добавленные
|
||||||
}
|
}
|
||||||
if (onSaved) {
|
if (onSaved) {
|
||||||
onSaved();
|
onSaved();
|
||||||
@@ -219,8 +220,8 @@ const WorkplaceSetupForm: React.FC<IRequestFormProps> = ({ requestType, context,
|
|||||||
if (status === REQUEST_STATUS.onApproval) {
|
if (status === REQUEST_STATUS.onApproval) {
|
||||||
await notifyActiveStep(context, approval, id, {
|
await notifyActiveStep(context, approval, id, {
|
||||||
requestTypeId: rtId,
|
requestTypeId: rtId,
|
||||||
dataListName: DATA_LIST_TITLE,
|
dataListName: dataListTitle(FORM_ID),
|
||||||
approvalListName: APPROVAL_LIST_TITLE,
|
approvalListName: approvalListTitle(FORM_ID),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -260,7 +261,7 @@ const WorkplaceSetupForm: React.FC<IRequestFormProps> = ({ requestType, context,
|
|||||||
<FormCard
|
<FormCard
|
||||||
title={requestType.title}
|
title={requestType.title}
|
||||||
subtitle="Категория: IT"
|
subtitle="Категория: IT"
|
||||||
belowSubtitle={<InitiatorLine listTitle={DATA_LIST_TITLE} itemId={requestId} />}
|
belowSubtitle={<InitiatorLine listTitle={dataListTitle(FORM_ID)} itemId={requestId} />}
|
||||||
headerRight={
|
headerRight={
|
||||||
requestId || requestStatus ? (
|
requestId || requestStatus ? (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -27,9 +27,10 @@ import {
|
|||||||
APPROVAL_PHASE
|
APPROVAL_PHASE
|
||||||
} from '../../shared/approval';
|
} from '../../shared/approval';
|
||||||
import { getAttachments, IAttachmentInfo } from '../../shared/attachments/attachments.service';
|
import { getAttachments, IAttachmentInfo } from '../../shared/attachments/attachments.service';
|
||||||
import { IFormData, EMPTY_DATA, SetField } from './model';
|
import { approvalListTitle, dataListTitle } from '../../shared/listTitles';
|
||||||
|
import { FORM_ID, IFormData, EMPTY_DATA, SetField } from './model';
|
||||||
import { loadWorkplaceRequest } from './load';
|
import { loadWorkplaceRequest } from './load';
|
||||||
import { workplaceApprovalService, updateReviewFields, DATA_LIST_TITLE, APPROVAL_LIST_TITLE } from './submit';
|
import { workplaceApprovalService, updateReviewFields } from './submit';
|
||||||
import { EmployeeSection } from './sections/EmployeeSection';
|
import { EmployeeSection } from './sections/EmployeeSection';
|
||||||
import { EquipmentSection } from './sections/EquipmentSection';
|
import { EquipmentSection } from './sections/EquipmentSection';
|
||||||
|
|
||||||
@@ -59,7 +60,7 @@ const WorkplaceSetupView: React.FC<IWorkplaceSetupViewProps> = ({ context, itemI
|
|||||||
const set: SetField = (key, value) => setData((prev) => ({ ...prev, [key]: value }));
|
const set: SetField = (key, value) => setData((prev) => ({ ...prev, [key]: value }));
|
||||||
|
|
||||||
const approverService = useMemo(
|
const approverService = useMemo(
|
||||||
() => new ApproverService(workplaceApprovalService(), DATA_LIST_TITLE),
|
() => new ApproverService(workplaceApprovalService(), dataListTitle(FORM_ID)),
|
||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -96,7 +97,7 @@ const WorkplaceSetupView: React.FC<IWorkplaceSetupViewProps> = ({ context, itemI
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let alive = true;
|
let alive = true;
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
Promise.all([loadWorkplaceRequest(itemId), getAttachments(DATA_LIST_TITLE, itemId)])
|
Promise.all([loadWorkplaceRequest(itemId), getAttachments(dataListTitle(FORM_ID), itemId)])
|
||||||
.then(([loaded, files]) => {
|
.then(([loaded, files]) => {
|
||||||
if (alive) {
|
if (alive) {
|
||||||
setData(loaded.data);
|
setData(loaded.data);
|
||||||
@@ -174,8 +175,8 @@ const WorkplaceSetupView: React.FC<IWorkplaceSetupViewProps> = ({ context, itemI
|
|||||||
if (!requestTypeId) return;
|
if (!requestTypeId) return;
|
||||||
notifyActiveStep(context, approval, itemId, {
|
notifyActiveStep(context, approval, itemId, {
|
||||||
requestTypeId,
|
requestTypeId,
|
||||||
dataListName: DATA_LIST_TITLE,
|
dataListName: dataListTitle(FORM_ID),
|
||||||
approvalListName: APPROVAL_LIST_TITLE,
|
approvalListName: approvalListTitle(FORM_ID),
|
||||||
}).catch(() => undefined);
|
}).catch(() => undefined);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -211,7 +212,7 @@ const WorkplaceSetupView: React.FC<IWorkplaceSetupViewProps> = ({ context, itemI
|
|||||||
<FormCard
|
<FormCard
|
||||||
title="Организация рабочего места"
|
title="Организация рабочего места"
|
||||||
subtitle={`№${code || itemId}`}
|
subtitle={`№${code || itemId}`}
|
||||||
belowSubtitle={<InitiatorLine listTitle={DATA_LIST_TITLE} itemId={itemId} />}
|
belowSubtitle={<InitiatorLine listTitle={dataListTitle(FORM_ID)} itemId={itemId} />}
|
||||||
headerRight={status ? <StatusBadge status={status} /> : undefined}
|
headerRight={status ? <StatusBadge status={status} /> : undefined}
|
||||||
>
|
>
|
||||||
<FormModeProvider readOnly>
|
<FormModeProvider readOnly>
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { getSP } from '../../shared/pnp/pnpjs';
|
import { getSP } from '../../shared/pnp/pnpjs';
|
||||||
import { IFormData, EMPTY_DATA } from './model';
|
import { dataListTitle } from '../../shared/listTitles';
|
||||||
|
import { FORM_ID, IFormData, EMPTY_DATA } from './model';
|
||||||
const LIST_TITLE = 'Организация рабочего места — данные';
|
|
||||||
|
|
||||||
const SCALAR_FIELDS: (keyof IFormData)[] = [
|
const SCALAR_FIELDS: (keyof IFormData)[] = [
|
||||||
'FullName', 'NameLatin', 'Phone', 'Position', 'Department', 'DepartmentId', 'WorkKind', 'Location', 'StartDate',
|
'FullName', 'NameLatin', 'Phone', 'Position', 'Department', 'DepartmentId', 'WorkKind', 'Location', 'StartDate',
|
||||||
@@ -18,7 +17,7 @@ export interface ILoadedRequest {
|
|||||||
/** Прочитать заявку по id: модель формы + статус (для режима чтения). */
|
/** Прочитать заявку по id: модель формы + статус (для режима чтения). */
|
||||||
export async function loadWorkplaceRequest(itemId: number): Promise<ILoadedRequest> {
|
export async function loadWorkplaceRequest(itemId: number): Promise<ILoadedRequest> {
|
||||||
const item = (await getSP()
|
const item = (await getSP()
|
||||||
.web.lists.getByTitle(LIST_TITLE)
|
.web.lists.getByTitle(dataListTitle(FORM_ID))
|
||||||
.items.getById(itemId)
|
.items.getById(itemId)
|
||||||
.select(...SCALAR_FIELDS, 'NeedsEquipment', 'RequestStatus', 'RequestUniqueCode')()) as Record<string, unknown>;
|
.select(...SCALAR_FIELDS, 'NeedsEquipment', 'RequestStatus', 'RequestUniqueCode')()) as Record<string, unknown>;
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,11 @@
|
|||||||
import { WebPartContext } from '@microsoft/sp-webpart-base';
|
import { WebPartContext } from '@microsoft/sp-webpart-base';
|
||||||
|
import { RequestTypeId } from '../../models/types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Тип заявки этой формы — ключ реестра форм (forms/registry.ts). Имена списков
|
||||||
|
* SharePoint выводятся из него, см. shared/listTitles.ts.
|
||||||
|
*/
|
||||||
|
export const FORM_ID: RequestTypeId = 'it-workplace-setup';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Данные формы «Организация рабочего места». Ключи = internalName колонок SP-списка
|
* Данные формы «Организация рабочего места». Ключи = internalName колонок SP-списка
|
||||||
|
|||||||
@@ -1,14 +1,12 @@
|
|||||||
import { getSP } from '../../shared/pnp/pnpjs';
|
import { getSP } from '../../shared/pnp/pnpjs';
|
||||||
import { ApprovalListService, IApproverToWrite, writeRoute } from '../../shared/approval';
|
import { ApprovalListService, IApproverToWrite, writeRoute } from '../../shared/approval';
|
||||||
import { buildRequestUniqueCode } from '../../shared/requestCode';
|
import { buildRequestUniqueCode } from '../../shared/requestCode';
|
||||||
import { IFormData } from './model';
|
import { approvalListTitle, dataListTitle } from '../../shared/listTitles';
|
||||||
|
import { FORM_ID, IFormData } from './model';
|
||||||
export const DATA_LIST_TITLE = 'Организация рабочего места — данные';
|
|
||||||
export const APPROVAL_LIST_TITLE = 'Организация рабочего места — согласование';
|
|
||||||
|
|
||||||
/** Экземпляр сервиса списка согласования этой формы. */
|
/** Экземпляр сервиса списка согласования этой формы. */
|
||||||
export function workplaceApprovalService(): ApprovalListService {
|
export function workplaceApprovalService(): ApprovalListService {
|
||||||
return new ApprovalListService(APPROVAL_LIST_TITLE);
|
return new ApprovalListService(approvalListTitle(FORM_ID));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -19,7 +17,7 @@ export async function updateReviewFields(
|
|||||||
id: number,
|
id: number,
|
||||||
fields: { Software: string; AccessRights: string }
|
fields: { Software: string; AccessRights: string }
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await getSP().web.lists.getByTitle(DATA_LIST_TITLE).items.getById(id).update({
|
await getSP().web.lists.getByTitle(dataListTitle(FORM_ID)).items.getById(id).update({
|
||||||
Software: fields.Software === '' ? null : fields.Software,
|
Software: fields.Software === '' ? null : fields.Software,
|
||||||
AccessRights: fields.AccessRights === '' ? null : fields.AccessRights,
|
AccessRights: fields.AccessRights === '' ? null : fields.AccessRights,
|
||||||
});
|
});
|
||||||
@@ -71,7 +69,7 @@ export async function saveWorkplaceRequest(
|
|||||||
status: string,
|
status: string,
|
||||||
sourceHireId?: number
|
sourceHireId?: number
|
||||||
): Promise<ISaveResult> {
|
): Promise<ISaveResult> {
|
||||||
const list = getSP().web.lists.getByTitle(DATA_LIST_TITLE);
|
const list = getSP().web.lists.getByTitle(dataListTitle(FORM_ID));
|
||||||
const item = buildItem(data, status, sourceHireId);
|
const item = buildItem(data, status, sourceHireId);
|
||||||
|
|
||||||
let id: number;
|
let id: number;
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import { RequestTypeId } from '../models/types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ЕДИНЫЙ источник имён списков SharePoint.
|
||||||
|
*
|
||||||
|
* Раньше каждая форма объявляла свои DATA_LIST_TITLE/APPROVAL_LIST_TITLE строковыми
|
||||||
|
* литералами — 20 копий имён по 7 формам. Имена стали нумерованными (под архивацию:
|
||||||
|
* когда список данных упирается в порог элементов, рядом появляется пара с номером
|
||||||
|
* N+1), и с литералами каждый номер пришлось бы править по всем формам руками.
|
||||||
|
*
|
||||||
|
* Конвенция — ровно та же, что у провижинера (provisioning/Provision-Lists.ps1,
|
||||||
|
* функция Get-NumberedListTitle):
|
||||||
|
* «<База> <N> — данные» / «<База> <N> — согласование»
|
||||||
|
*
|
||||||
|
* Номер СТРОГО перед суффиксом: имя списка согласования выводится из имени списка
|
||||||
|
* данных регексом /\s*—\s*данные\s*$/, привязанным к КОНЦУ строки (см.
|
||||||
|
* features/myRequests/myRequests.service.ts и approval-route
|
||||||
|
* frontend/.../approvalAdmin/services/requestManageService.ts). Имя вида
|
||||||
|
* «… — данные 1» этот регекс не матчит, и список согласования перестаёт находиться.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** База имени по типу заявки. Ключи — те же, что в реестре форм (forms/registry.ts). */
|
||||||
|
const LIST_TITLE_BASE: Record<RequestTypeId, string> = {
|
||||||
|
'clearance-form': 'Обходной лист',
|
||||||
|
'hr-vacation': 'Заявка на отпуск',
|
||||||
|
'it-1c-ticket': 'Заявка в отдел 1С',
|
||||||
|
'it-workplace-setup': 'Организация рабочего места',
|
||||||
|
'recruiting-hire': 'Прием на работу',
|
||||||
|
'recruiting-selection': 'Заявка на подбор',
|
||||||
|
'wh-tmc-transfer': 'СЗ на перемещение ТМЦ'
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Номер текущей пары списков. Пока единый для всех типов заявок — архивных пар ещё
|
||||||
|
* нет, все формы работают с парой №1.
|
||||||
|
*
|
||||||
|
* Когда архивация заработает, номер будет приходить из approval-route (эндпоинт
|
||||||
|
* «текущий список по типу заявки») и заполняться один раз при старте web part, до
|
||||||
|
* загрузки чанка любой формы. Сигнатуры функций ниже при этом не меняются — поэтому
|
||||||
|
* вызовы по формам править второй раз не придётся.
|
||||||
|
*/
|
||||||
|
const CURRENT_LIST_NUMBER = 1;
|
||||||
|
|
||||||
|
function listTitleBase(id: RequestTypeId): string {
|
||||||
|
const base = LIST_TITLE_BASE[id];
|
||||||
|
if (!base) {
|
||||||
|
throw new Error(`Неизвестный тип заявки «${id}»: нет базы имени списка в LIST_TITLE_BASE`);
|
||||||
|
}
|
||||||
|
return base;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Имя списка данных заявок этого типа. */
|
||||||
|
export function dataListTitle(id: RequestTypeId): string {
|
||||||
|
return `${listTitleBase(id)} ${CURRENT_LIST_NUMBER} — данные`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Имя списка согласования заявок этого типа. */
|
||||||
|
export function approvalListTitle(id: RequestTypeId): string {
|
||||||
|
return `${listTitleBase(id)} ${CURRENT_LIST_NUMBER} — согласование`;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user