feat: ручной ввод базы 1С при выборе варианта «Другое»

Если нужной базы нет в списке, заявитель отмечает «Другое» и вписывает
название в обязательное поле. Текст хранится в отдельной колонке DB1COther,
показывается в просмотре и попадает в карточку Planner.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Тимур Городецкий
2026-09-25 17:08:29 +05:00
parent ebc74dc46d
commit 8a6edf9d35
6 changed files with 44 additions and 9 deletions
@@ -45,7 +45,7 @@ import {
dataListTitle,
dataListTitleForListId
} from '../../shared/listTitles';
import { FORM_ID, IFormData, EMPTY_DATA, SetField } from './model';
import { FORM_ID, IFormData, EMPTY_DATA, SetField, hasOtherDatabase } from './model';
import { saveTicketRequest, ticket1cApprovalService } from './submit';
import { loadTicketRequest } from './load';
import { getApplicantProfile } from './services/profile.service';
@@ -222,17 +222,20 @@ const Ticket1CForm: React.FC<IRequestFormProps> = ({
);
const matrixEditor = useMemo(() => ({ ...matrix, rows: matrixRows }), [matrix, matrixRows]);
const isValid = !!(
const isOtherDatabaseMissing = hasOtherDatabase(data) && !data.DB1COther.trim();
const areRequiredFilled = !!(
data.TaskName.trim() &&
data.WorkType &&
data.Organization &&
data.DB1C.length > 0 &&
data.Question.trim()
);
const isValid = areRequiredFilled && !isOtherDatabaseMissing;
const validate = (): string | undefined => {
if (!isValid) {
if (!areRequiredFilled) {
return 'Заполните обязательные поля: наименование задачи, вид работ, организация, база 1С, вопрос.';
}
if (isOtherDatabaseMissing) return 'Выбрана база 1С «Другое» — укажите её название.';
if (requestTypeIdState.error) return `Не удалось определить тип заявки: ${requestTypeIdState.error}`;
if (!requestTypeId) return 'Тип заявки ещё определяется — подождите немного и повторите.';
return undefined;
@@ -34,7 +34,7 @@ import {
dataListTitle,
dataListTitleForListId
} from '../../shared/listTitles';
import { FORM_ID, IFormData, EMPTY_DATA, SetField } from './model';
import { FORM_ID, IFormData, EMPTY_DATA, SetField, hasOtherDatabase } from './model';
import { loadTicketRequest } from './load';
import { ticket1cApprovalService, getPlannerTaskId, setPlannerTaskId } from './submit';
import { PlannerTicketService } from './services/plannerTicket.service';
@@ -69,6 +69,7 @@ function buildPlannerNotes(
lines.push(`Отдел: ${data.Department}`);
lines.push(`Должность: ${data.Position}`);
lines.push(`База 1С: ${data.DB1C.join(', ')}`);
if (hasOtherDatabase(data)) lines.push(`Другая база 1С: ${data.DB1COther}`);
lines.push(`Виды работ: ${data.WorkType}`);
lines.push('');
lines.push('Вопрос:');
@@ -5,7 +5,7 @@ import { FORM_ID, IFormData, EMPTY_DATA } from './model';
// TaskName пишется/читается через системную колонку Title (см. provisioning.json/titleColumnName).
// DB1C — отдельно, это множественный выбор (см. toStringArray).
const SCALAR_FIELDS: (keyof IFormData)[] = [
'WorkType', 'Organization', 'FIO', 'Email', 'Phone', 'Department', 'Position', 'Manager', 'Question',
'WorkType', 'Organization', 'DB1COther', 'FIO', 'Email', 'Phone', 'Department', 'Position', 'Manager', 'Question',
];
/**
@@ -18,6 +18,8 @@ export interface IFormData {
Organization: string;
/** Базы 1С — множественный выбор (колонка MultiChoice), минимум одна. */
DB1C: string[];
/** Название базы вручную — обязательно, только если в DB1C отмечено «Другое». */
DB1COther: string;
FIO: string;
Email: string;
Phone: string;
@@ -32,6 +34,16 @@ export interface IFormData {
Question: string;
}
/**
* Вариант колонки «База 1С», при выборе которого заявитель вписывает базу сам
* (поле DB1COther). Должен дословно совпадать с вариантом в колонке SP.
*/
export const DB1C_OTHER_OPTION = 'Другое';
export function hasOtherDatabase(data: IFormData): boolean {
return data.DB1C.indexOf(DB1C_OTHER_OPTION) !== -1;
}
/** Обновление одного поля формы с сохранением типа значения. */
export type SetField = <K extends keyof IFormData>(key: K, value: IFormData[K]) => void;
@@ -47,6 +59,7 @@ export const EMPTY_DATA: IFormData = {
WorkType: '',
Organization: '',
DB1C: [],
DB1COther: '',
FIO: '',
Email: '',
Phone: '',
@@ -39,8 +39,10 @@
"1С: Бухгалтерия ГеоТрек",
"1С: Бухгалтерия Монтажник-Т",
"1С: Бухгалтерия ОСН Беркут",
"1С: Бухгалтерия CT Global"
"1С: Бухгалтерия CT Global",
"Другое"
] },
{ "internalName": "DB1COther", "title": "База 1С (другое)", "type": "Text" },
{ "internalName": "FIO", "title": "Ф.И.О.", "type": "Text" },
{ "internalName": "Email", "title": "Email", "type": "Text" },
@@ -2,7 +2,7 @@ import * as React from 'react';
import { Alert, CollapseSection, FormField, MultiChoiceField, Select, TextArea, TextInput, ISelectOption } from '../../../shared/ui';
import { useFieldChoices, IFieldChoicesState } from '../../../shared/fieldChoices';
import { dataListTitle } from '../../../shared/listTitles';
import { FORM_ID, ISectionProps } from '../model';
import { DB1C_OTHER_OPTION, FORM_ID, ISectionProps, hasOtherDatabase } from '../model';
import {
QUESTION_MAX,
CHOICES_LOADING_PLACEHOLDER,
@@ -55,14 +55,30 @@ export const RequestSection: React.FC<ISectionProps> = ({ data, set }) => {
placeholder={choicesPlaceholder(organizations, '— выберите —')}
/>
</FormField>
<FormField label="База 1С" fieldKey="DB1C" required>
{/* На всю ширину: чипы выбранных баз растят поле по высоте, и соседнее
поле в той же строке сетки съезжало бы относительно него. */}
<FormField label="База 1С" fieldKey="DB1C" required full>
<MultiChoiceField
value={data.DB1C}
onChange={(v) => set('DB1C', v)}
onChange={(v) => {
set('DB1C', v);
// Сняли «Другое» — вписанное название больше не относится к заявке.
if (v.indexOf(DB1C_OTHER_OPTION) === -1) set('DB1COther', '');
}}
options={databases.options}
placeholder={choicesPlaceholder(databases, 'Выберите одну или несколько баз')}
/>
</FormField>
{hasOtherDatabase(data) && (
<FormField label="Другая база 1С" htmlFor="DB1COther" required full>
<TextInput
id="DB1COther"
value={data.DB1COther}
onChange={(v) => set('DB1COther', v)}
placeholder="Укажите название базы"
/>
</FormField>
)}
<FormField label={`Вопрос (${data.Question.length}/${QUESTION_MAX})`} htmlFor="Question" required full>
<TextArea
id="Question"