diff --git a/provisioning/Provision-Lists.ps1 b/provisioning/Provision-Lists.ps1
index 6046cb2..103a3f2 100644
--- a/provisioning/Provision-Lists.ps1
+++ b/provisioning/Provision-Lists.ps1
@@ -129,13 +129,14 @@ $SITE_COLUMN_GROUP = 'Портал заявок'
# Типы, которые НЕ делаем эталоном (столбцом сайта), а создаём столбцом списка:
# - Lookup (эталон привязан к одному списку, а Ticket у каждого свой);
-# - Number с decimals и UserMulti (создаются через XML; web-level XML в PnP падает
-# NRE, и это одноразовые поля конкретной формы — переиспользовать нечего).
+# - Number с decimals, UserMulti и MultiChoice (создаются через XML; web-level XML
+# в PnP падает NRE, и это одноразовые поля конкретной формы — переиспользовать нечего).
function Test-NeedsListColumn {
param($Col)
return ($Col.type -eq 'Lookup') `
-or ($Col.type -eq 'Number' -and $null -ne $Col.decimals) `
- -or ($Col.type -eq 'User' -and $Col.allowMultiple)
+ -or ($Col.type -eq 'User' -and $Col.allowMultiple) `
+ -or ($Col.type -eq 'Choice' -and $Col.allowMultiple)
}
# Создаёт/обновляет СТОЛБЕЦ САЙТА (эталон) — один раз на весь сайт. Только простые
@@ -184,6 +185,14 @@ function Add-ListColumn {
param([string]$List, $Col)
if (Test-FieldExists -List $List -InternalName $Col.internalName) {
+ # Тип существующей колонки скрипт НЕ меняет (данные!) — одиночный выбор в
+ # множественный переключают вручную: параметры столбца → «флажки».
+ if ($Col.type -eq 'Choice' -and $Col.allowMultiple) {
+ $existingType = (Get-PnPField -List $List -Identity $Col.internalName).TypeAsString
+ if ($existingType -ne 'MultiChoice') {
+ Write-Warning " [!] $($Col.internalName): в списке одиночный выбор ($existingType), а нужен множественный — переключите столбец на «флажки» вручную"
+ }
+ }
if ($Col.type -eq 'Choice' -and $Col.choices) {
try {
Set-PnPField -List $List -Identity $Col.internalName -Values @{ Choices = [string[]]$Col.choices } | Out-Null
@@ -217,7 +226,16 @@ function Add-ListColumn {
Set-PnPField -List $List -Identity $Col.internalName -Values @{ SelectionMode = $mode } | Out-Null
}
}
- 'Choice' { Add-PnPField -List $List -InternalName $Col.internalName -DisplayName $Col.title -Type Choice -Choices $Col.choices -AddToDefaultView | Out-Null }
+ 'Choice' {
+ if ($Col.allowMultiple) {
+ $choicesXml = ($Col.choices | ForEach-Object { "$(ConvertTo-XmlSafe $_)" }) -join ''
+ $xml = "$choicesXml"
+ Add-PnPFieldFromXml -List $List -FieldXml $xml | Out-Null
+ }
+ else {
+ Add-PnPField -List $List -InternalName $Col.internalName -DisplayName $Col.title -Type Choice -Choices $Col.choices -AddToDefaultView | Out-Null
+ }
+ }
'Url' { Add-PnPField -List $List -InternalName $Col.internalName -DisplayName $Col.title -Type URL -AddToDefaultView | Out-Null }
'Number' {
if ($null -ne $Col.decimals) {
diff --git a/src/webparts/requestPortal/forms/RecruitingSelection/sections/ConditionsSection.tsx b/src/webparts/requestPortal/forms/RecruitingSelection/sections/ConditionsSection.tsx
index 0bbc790..8a4f483 100644
--- a/src/webparts/requestPortal/forms/RecruitingSelection/sections/ConditionsSection.tsx
+++ b/src/webparts/requestPortal/forms/RecruitingSelection/sections/ConditionsSection.tsx
@@ -8,8 +8,8 @@ import {
Select,
PeopleField,
PeopleFieldMulti,
+ MultiChoiceField,
} from "../../../shared/ui";
-import { MultiChoiceField } from "./MultiChoiceField";
import {
ISectionProps,
PERSON_PLACEHOLDER,
diff --git a/src/webparts/requestPortal/forms/RecruitingSelection/sections/VacancySection.tsx b/src/webparts/requestPortal/forms/RecruitingSelection/sections/VacancySection.tsx
index a9f9e4c..08caac9 100644
--- a/src/webparts/requestPortal/forms/RecruitingSelection/sections/VacancySection.tsx
+++ b/src/webparts/requestPortal/forms/RecruitingSelection/sections/VacancySection.tsx
@@ -1,8 +1,7 @@
import * as React from 'react';
-import { CollapseSection, FormField, TextInput, TextArea, Select } from '../../../shared/ui';
+import { CollapseSection, FormField, TextInput, TextArea, Select, MultiChoiceField } from '../../../shared/ui';
import { PositionPicker } from '../../../shared/dictionary';
import { LanguagesField } from './LanguagesField';
-import { MultiChoiceField } from './MultiChoiceField';
import {
ISectionProps,
EMPLOYMENT_TYPE_OPTIONS,
diff --git a/src/webparts/requestPortal/forms/Ticket1C/Ticket1CForm.tsx b/src/webparts/requestPortal/forms/Ticket1C/Ticket1CForm.tsx
index 4c57ad5..3d58f6c 100644
--- a/src/webparts/requestPortal/forms/Ticket1C/Ticket1CForm.tsx
+++ b/src/webparts/requestPortal/forms/Ticket1C/Ticket1CForm.tsx
@@ -226,7 +226,7 @@ const Ticket1CForm: React.FC = ({
data.TaskName.trim() &&
data.WorkType &&
data.Organization &&
- data.DB1C &&
+ data.DB1C.length > 0 &&
data.Question.trim()
);
const validate = (): string | undefined => {
@@ -412,7 +412,10 @@ const Ticket1CForm: React.FC = ({
approval.searchUsers(q)}
- emptyText="Маршрут для этого вида работ пока не настроен — обратитесь к администратору маршрутов согласования."
+ // Пустой маршрут — штатная ситуация: для части видов работ согласование
+ // не нужно, заявка сразу уходит в Planner. Сбой загрузки маршрута
+ // показывается отдельно (matrix.error) и блокирует отправку.
+ emptyText="Для этого вида работ согласование не требуется — заявка сразу поступит в отдел 1С."
/>
typeof v === 'string' && v !== '');
+ if (raw && typeof raw === 'object' && Array.isArray((raw as { results?: unknown }).results)) {
+ return toStringArray((raw as { results: unknown[] }).results);
+ }
+ return typeof raw === 'string' && raw !== '' ? [raw] : [];
+}
+
export interface ILoadedRequest {
data: IFormData;
status: string;
@@ -26,7 +40,7 @@ export async function loadTicketRequest(itemId: number, listId?: string): Promis
const list = listId ? getSP().web.getList(listId) : getSP().web.lists.getByTitle(dataListTitle(FORM_ID));
const item = (await list
.items.getById(itemId)
- .select(...SCALAR_FIELDS, 'Title', 'RequestStatus', 'RequestUniqueCode')()) as Record;
+ .select(...SCALAR_FIELDS, 'DB1C', 'Title', 'RequestStatus', 'RequestUniqueCode')()) as Record;
const data: IFormData = { ...EMPTY_DATA };
const writable = data as unknown as Record;
@@ -36,6 +50,7 @@ export async function loadTicketRequest(itemId: number, listId?: string): Promis
writable[key] = value === null || value === undefined ? '' : String(value);
});
data.TaskName = (item.Title as string) || '';
+ data.DB1C = toStringArray(item.DB1C);
return {
data,
diff --git a/src/webparts/requestPortal/forms/Ticket1C/model.ts b/src/webparts/requestPortal/forms/Ticket1C/model.ts
index d12850a..b3aae3b 100644
--- a/src/webparts/requestPortal/forms/Ticket1C/model.ts
+++ b/src/webparts/requestPortal/forms/Ticket1C/model.ts
@@ -16,7 +16,8 @@ export interface IFormData {
TaskName: string;
WorkType: string;
Organization: string;
- DB1C: string;
+ /** Базы 1С — множественный выбор (колонка MultiChoice), минимум одна. */
+ DB1C: string[];
FIO: string;
Email: string;
Phone: string;
@@ -45,7 +46,7 @@ export const EMPTY_DATA: IFormData = {
TaskName: '',
WorkType: '',
Organization: '',
- DB1C: '',
+ DB1C: [],
FIO: '',
Email: '',
Phone: '',
diff --git a/src/webparts/requestPortal/forms/Ticket1C/provisioning.json b/src/webparts/requestPortal/forms/Ticket1C/provisioning.json
index eadb406..26f9524 100644
--- a/src/webparts/requestPortal/forms/Ticket1C/provisioning.json
+++ b/src/webparts/requestPortal/forms/Ticket1C/provisioning.json
@@ -25,7 +25,7 @@
"ОСН Беркут",
"CT Global"
] },
- { "internalName": "DB1C", "title": "База 1С", "type": "Choice", "required": true, "choices": [
+ { "internalName": "DB1C", "title": "База 1С", "type": "Choice", "allowMultiple": true, "required": true, "choices": [
"1С: Зарплата и управление персоналом (ЗУП)",
"1С: Документооборот (ДО)",
"1С: Автотранспортное предприятие (АТП)",
diff --git a/src/webparts/requestPortal/forms/Ticket1C/sections/RequestSection.tsx b/src/webparts/requestPortal/forms/Ticket1C/sections/RequestSection.tsx
index b275f80..db82bd5 100644
--- a/src/webparts/requestPortal/forms/Ticket1C/sections/RequestSection.tsx
+++ b/src/webparts/requestPortal/forms/Ticket1C/sections/RequestSection.tsx
@@ -1,5 +1,5 @@
import * as React from 'react';
-import { CollapseSection, FormField, Select, TextArea, TextInput, ISelectOption } from '../../../shared/ui';
+import { CollapseSection, FormField, MultiChoiceField, Select, TextArea, TextInput, ISelectOption } from '../../../shared/ui';
import { ISectionProps } from '../model';
import { WORK_TYPE_OPTIONS, ORGANIZATION_OPTIONS, DB1C_OPTIONS, QUESTION_MAX } from '../config';
@@ -32,13 +32,12 @@ export const RequestSection: React.FC = ({ data, set }) => (
placeholder="— выберите —"
/>
-
-
diff --git a/src/webparts/requestPortal/models/columnDef.ts b/src/webparts/requestPortal/models/columnDef.ts
index c758d34..719dcb5 100644
--- a/src/webparts/requestPortal/models/columnDef.ts
+++ b/src/webparts/requestPortal/models/columnDef.ts
@@ -38,7 +38,10 @@ export interface IColumnDef {
lookupShowField?: string;
/** Для type === 'User': кого можно выбирать. По умолчанию — как в SharePoint. */
userSelectionMode?: 'PeopleOnly' | 'PeopleAndGroups';
- /** Для type === 'User': разрешить выбор нескольких (создаётся колонка UserMulti). */
+ /**
+ * Разрешить выбор нескольких: для type === 'User' создаётся колонка UserMulti,
+ * для type === 'Choice' — MultiChoice (значение в коде — string[]).
+ */
allowMultiple?: boolean;
}
diff --git a/src/webparts/requestPortal/forms/RecruitingSelection/sections/MultiChoiceField.module.scss b/src/webparts/requestPortal/shared/ui/MultiChoiceField/MultiChoiceField.module.scss
similarity index 100%
rename from src/webparts/requestPortal/forms/RecruitingSelection/sections/MultiChoiceField.module.scss
rename to src/webparts/requestPortal/shared/ui/MultiChoiceField/MultiChoiceField.module.scss
diff --git a/src/webparts/requestPortal/forms/RecruitingSelection/sections/MultiChoiceField.tsx b/src/webparts/requestPortal/shared/ui/MultiChoiceField/MultiChoiceField.tsx
similarity index 98%
rename from src/webparts/requestPortal/forms/RecruitingSelection/sections/MultiChoiceField.tsx
rename to src/webparts/requestPortal/shared/ui/MultiChoiceField/MultiChoiceField.tsx
index caf6398..cfdc24c 100644
--- a/src/webparts/requestPortal/forms/RecruitingSelection/sections/MultiChoiceField.tsx
+++ b/src/webparts/requestPortal/shared/ui/MultiChoiceField/MultiChoiceField.tsx
@@ -1,7 +1,7 @@
import * as React from 'react';
import { useMemo, useState } from 'react';
import styles from './MultiChoiceField.module.scss';
-import { useFormReadOnly, ISelectOption } from '../../../shared/ui';
+import { useFormReadOnly, ISelectOption } from '@tnsu/ui-kit';
export interface IMultiChoiceFieldProps {
value: string[];
diff --git a/src/webparts/requestPortal/shared/ui/index.ts b/src/webparts/requestPortal/shared/ui/index.ts
index 68ab21e..84b3f9e 100644
--- a/src/webparts/requestPortal/shared/ui/index.ts
+++ b/src/webparts/requestPortal/shared/ui/index.ts
@@ -36,6 +36,7 @@ export type { ISelectOption, IConfirmOptions } from '@tnsu/ui-kit';
export { FormField } from './FormField/FormField';
export { FieldChangesProvider } from './FieldChanges/FieldChangesContext';
export { FieldChangesBanner } from './FieldChanges/FieldChangesBanner';
+export { MultiChoiceField } from './MultiChoiceField/MultiChoiceField';
export { ItemsTable } from './ItemsTable/ItemsTable';
export type { IItemsTableProps } from './ItemsTable/ItemsTable';
export type { ILineItem } from './ItemsTable/ILineItem';