feat: командировку нельзя оформить задним числом
Прибытие в любую точку маршрута — не раньше сегодняшнего дня: прошедшие дни закрыты в календаре, а набранная вручную или устаревшая в черновике дата подсвечивается при отправке (черновик сохраняется). «Сегодня» передаётся в submitIssues параметром — тесты не зависят от текущей даты. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+1
-1
@@ -49,7 +49,7 @@
|
|||||||
| [`clearance-form`](../src/webparts/requestPortal/forms/Clearance/README.md) | Обходной лист | Отдел кадров | да | нет | да | подаётся HR за увольняемого (People-picker) либо создаётся сам по согласованному «Заявлению на увольнение»; условие «IsManager» резолвится по должности из самой заявки; да/нет-поля (парковка, СКУД); 5 независимых сумм к списанию компактной сеткой (ОС, СИЗ, подотчётная сумма, НМА, ТМЦ — обучение не входит, у него своё поле) — вводит любой активный согласующий, блокируется навсегда после первого ввода; заполнил хоть одну — стандартный комментарий решения (в блоке «Ваше решение») становится обязательным на любое решение, не только возврат/отклонение | ✅ |
|
| [`clearance-form`](../src/webparts/requestPortal/forms/Clearance/README.md) | Обходной лист | Отдел кадров | да | нет | да | подаётся HR за увольняемого (People-picker) либо создаётся сам по согласованному «Заявлению на увольнение»; условие «IsManager» резолвится по должности из самой заявки; да/нет-поля (парковка, СКУД); 5 независимых сумм к списанию компактной сеткой (ОС, СИЗ, подотчётная сумма, НМА, ТМЦ — обучение не входит, у него своё поле) — вводит любой активный согласующий, блокируется навсегда после первого ввода; заполнил хоть одну — стандартный комментарий решения (в блоке «Ваше решение») становится обязательным на любое решение, не только возврат/отклонение | ✅ |
|
||||||
| [`hr-dismissal`](../src/webparts/requestPortal/forms/Dismissal/README.md) | Заявление на увольнение | Отдел кадров | да | нет | да | совместители — таблица компаний с удалением строки (снимает и согласующего из матрицы); должность/организация из справочника; **после согласования сам создаёт обходной лист**; печать заблокирована до согласования обходного листа; «Лицо, принимающее ОС/НМА» — заполняет только непосредственный руководитель | ✅ |
|
| [`hr-dismissal`](../src/webparts/requestPortal/forms/Dismissal/README.md) | Заявление на увольнение | Отдел кадров | да | нет | да | совместители — таблица компаний с удалением строки (снимает и согласующего из матрицы); должность/организация из справочника; **после согласования сам создаёт обходной лист**; печать заблокирована до согласования обходного листа; «Лицо, принимающее ОС/НМА» — заполняет только непосредственный руководитель | ✅ |
|
||||||
| [`hr-skud-explanation`](../src/webparts/requestPortal/forms/SkudExplanation/README.md) | Объяснительная об отсутствии отметок в СКУД | Отдел кадров | да | нет | да | только «на себя»; дни выбираются кликом в календаре «Мой СКУД» (проходы Hikvision текущего пользователя), сохраняются снимком для согласующего; текст записки собирается автоматически, печать — бланк объяснительной | ⏳ код готов, списки/тип/маршрут не заведены |
|
| [`hr-skud-explanation`](../src/webparts/requestPortal/forms/SkudExplanation/README.md) | Объяснительная об отсутствии отметок в СКУД | Отдел кадров | да | нет | да | только «на себя»; дни выбираются кликом в календаре «Мой СКУД» (проходы Hikvision текущего пользователя), сохраняются снимком для согласующего; текст записки собирается автоматически, печать — бланк объяснительной | ⏳ код готов, списки/тип/маршрут не заведены |
|
||||||
| [`hr-business-trip`](../src/webparts/requestPortal/forms/BusinessTrip/README.md) | Заявка на командировку | Отдел кадров | да | да | да | можно за другого сотрудника; маршрут лентой (точки = город + объект из справочника + даты, участки с видом транспорта); задание — список задач; смета: статьи с позициями, привязанными к точкам/участкам (суммой или кол-во × цена — всё вручную), итог с долями и по объектам; обязательство по авансовому отчёту — флажок; печать без шапки адресата | ⏳ код готов, списки/тип/маршрут не заведены |
|
| [`hr-business-trip`](../src/webparts/requestPortal/forms/BusinessTrip/README.md) | Заявка на командировку | Отдел кадров | да | да | да | можно за другого сотрудника; командирующая организация — выбор из организаций сотрудника; маршрут лентой (точки = город из справочника КАТО на русском и казахском + объект из справочника + даты не задним числом, участки с видом транспорта); задание — список задач; смета: статьи с позициями, привязанными к точкам/участкам (суммой или кол-во × цена — всё вручную), итог с долями и по объектам; обязательство по авансовому отчёту — флажок; печать без шапки адресата | ⏳ код готов, списки/тип/маршрут не заведены |
|
||||||
| `hr-transfer` | Перемещение сотрудников | Отдел кадров | — | — | — | — | ⏳ заглушка (плитка есть, форма не сделана) |
|
| `hr-transfer` | Перемещение сотрудников | Отдел кадров | — | — | — | — | ⏳ заглушка (плитка есть, форма не сделана) |
|
||||||
| [`wh-service-request`](../src/webparts/requestPortal/forms/ServiceRequest/README.md) | Заявка на услуги | Склад / Снабжение | да | нет | да | маршрут по проекту (руководитель проекта/ПТО); табличные части «Приложения» и «Смета» (JSON-колонки); сумма по смете считается на лету (qty × цена), итог дублируется числовой колонкой для отчётов | ✅ |
|
| [`wh-service-request`](../src/webparts/requestPortal/forms/ServiceRequest/README.md) | Заявка на услуги | Склад / Снабжение | да | нет | да | маршрут по проекту (руководитель проекта/ПТО); табличные части «Приложения» и «Смета» (JSON-колонки); сумма по смете считается на лету (qty × цена), итог дублируется числовой колонкой для отчётов | ✅ |
|
||||||
|
|
||||||
|
|||||||
@@ -50,6 +50,9 @@
|
|||||||
узкий — сверху вниз. Сумму под точками Тимур может попросить убрать, если будет громоздко.
|
узкий — сверху вниз. Сумму под точками Тимур может попросить убрать, если будет громоздко.
|
||||||
- Новая точка начинается с даты отъезда из предыдущей (то, что человек уже ввёл);
|
- Новая точка начинается с даты отъезда из предыдущей (то, что человек уже ввёл);
|
||||||
прибытие раньше отъезда из предыдущей точки — ошибка.
|
прибытие раньше отъезда из предыдущей точки — ошибка.
|
||||||
|
- **Задним числом нельзя** (с 10-23): прибытие в любую точку — не раньше сегодняшнего дня
|
||||||
|
(сегодня можно). Прошедшие дни недоступны в календаре (`min`); набранную вручную или
|
||||||
|
«устаревшую» в черновике дату ловит `validation.ts` при отправке, черновик сохраняется.
|
||||||
- Хранение: `TripRouteJson`; для отчётов из маршрута пишутся `TripRoute` (текстом),
|
- Хранение: `TripRouteJson`; для отчётов из маршрута пишутся `TripRoute` (текстом),
|
||||||
`TripCity` (города), `Project`/`ProjectId` (объект ПЕРВОЙ точки — колонка у всех форм одна),
|
`TripCity` (города), `Project`/`ProjectId` (объект ПЕРВОЙ точки — колонка у всех форм одна),
|
||||||
`TripProjects` (все объекты), `TripStartDate`/`TripEndDate`/`TripDaysCount` (весь период).
|
`TripProjects` (все объекты), `TripStartDate`/`TripEndDate`/`TripDaysCount` (весь период).
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { CityPicker, ProjectPicker } from '../../../../shared/dictionary';
|
|||||||
import { CITY_PLACEHOLDER, IRouteStop, PROJECT_PLACEHOLDER } from '../../model';
|
import { CITY_PLACEHOLDER, IRouteStop, PROJECT_PLACEHOLDER } from '../../model';
|
||||||
import { stopNights } from '../../route';
|
import { stopNights } from '../../route';
|
||||||
import { plural } from '../../tripData';
|
import { plural } from '../../tripData';
|
||||||
|
import { todaySubmitDate } from '../../../../shared/submitDate';
|
||||||
import { fieldKey } from '../../validation';
|
import { fieldKey } from '../../validation';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -63,6 +64,8 @@ export const RouteStopCard: React.FC<Props> = ({ stop, index, context, onChange,
|
|||||||
type="date"
|
type="date"
|
||||||
value={stop.dateFrom}
|
value={stop.dateFrom}
|
||||||
onChange={(v) => onChange({ dateFrom: v })}
|
onChange={(v) => onChange({ dateFrom: v })}
|
||||||
|
// Прошедшие дни в календаре недоступны; набранную вручную дату ловит validation.ts.
|
||||||
|
min={todaySubmitDate()}
|
||||||
/>
|
/>
|
||||||
</FormField>
|
</FormField>
|
||||||
<FormField label="Отъезд" htmlFor={fieldKey.stopTo(stop)} required>
|
<FormField label="Отъезд" htmlFor={fieldKey.stopTo(stop)} required>
|
||||||
@@ -71,7 +74,7 @@ export const RouteStopCard: React.FC<Props> = ({ stop, index, context, onChange,
|
|||||||
type="date"
|
type="date"
|
||||||
value={stop.dateTo}
|
value={stop.dateTo}
|
||||||
onChange={(v) => onChange({ dateTo: v })}
|
onChange={(v) => onChange({ dateTo: v })}
|
||||||
min={stop.dateFrom || undefined}
|
min={stop.dateFrom || todaySubmitDate()}
|
||||||
/>
|
/>
|
||||||
</FormField>
|
</FormField>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { EMPTY_DATA, IEstimateArticle, IFormData, ITripRoute } from './model';
|
import { EMPTY_DATA, IEstimateArticle, IFormData, ITripRoute } from './model';
|
||||||
import { createArticle } from './estimate';
|
import { createArticle } from './estimate';
|
||||||
|
import type { IEmployeeOrganization } from '../../shared/dictionary';
|
||||||
|
import type { IFieldIssue } from '../../shared/ui';
|
||||||
import { draftIssues, submitIssues } from './validation';
|
import { draftIssues, submitIssues } from './validation';
|
||||||
|
|
||||||
const off = (article: IEstimateArticle): IEstimateArticle => ({ ...article, enabled: false });
|
const off = (article: IEstimateArticle): IEstimateArticle => ({ ...article, enabled: false });
|
||||||
@@ -24,19 +26,23 @@ const FILLED: IFormData = {
|
|||||||
TripObligationAccepted: true
|
TripObligationAccepted: true
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Фиксированное «сегодня» — раньше дат маршрута в фикстурах, чтобы тесты не зависели от текущей даты.
|
||||||
|
const TODAY = '2026-10-01';
|
||||||
|
const submit = (data: IFormData, organizations: IEmployeeOrganization[] = []): IFieldIssue[] => submitIssues(data, organizations, TODAY);
|
||||||
|
|
||||||
const fields = (issues: { field: string }[]): string[] => issues.map((i) => i.field);
|
const fields = (issues: { field: string }[]): string[] => issues.map((i) => i.field);
|
||||||
|
|
||||||
describe('validation: форма', () => {
|
describe('validation: форма', () => {
|
||||||
it('заполненная заявка — без проблем', () => {
|
it('заполненная заявка — без проблем', () => {
|
||||||
expect(submitIssues(FILLED)).toEqual([]);
|
expect(submit(FILLED)).toEqual([]);
|
||||||
expect(draftIssues(FILLED)).toEqual([]);
|
expect(draftIssues(FILLED)).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('командирующая организация обязательна, только если есть из чего выбрать', () => {
|
it('командирующая организация обязательна, только если есть из чего выбрать', () => {
|
||||||
const orgs = [{ id: '1', fio: '', email: '', iin: '', company: 'ТОО А', companyId: 'a', section: '', departmentId: '', position: '', mobile: '' }];
|
const orgs = [{ id: '1', fio: '', email: '', iin: '', company: 'ТОО А', companyId: 'a', section: '', departmentId: '', position: '', mobile: '' }];
|
||||||
expect(fields(submitIssues(FILLED, orgs))).toEqual(['TripCompany']);
|
expect(fields(submit(FILLED, orgs))).toEqual(['TripCompany']);
|
||||||
expect(submitIssues({ ...FILLED, TripCompany: 'ТОО А', TripCompanyId: 'a' }, orgs)).toEqual([]);
|
expect(submit({ ...FILLED, TripCompany: 'ТОО А', TripCompanyId: 'a' }, orgs)).toEqual([]);
|
||||||
expect(submitIssues(FILLED, [])).toEqual([]);
|
expect(submit(FILLED, [])).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('черновику нужен только сотрудник', () => {
|
it('черновику нужен только сотрудник', () => {
|
||||||
@@ -45,19 +51,25 @@ describe('validation: форма', () => {
|
|||||||
|
|
||||||
it('пустые задачи и обязательство', () => {
|
it('пустые задачи и обязательство', () => {
|
||||||
const data = { ...FILLED, Tasks: [' '], TripObligationAccepted: false };
|
const data = { ...FILLED, Tasks: [' '], TripObligationAccepted: false };
|
||||||
expect(fields(submitIssues(data))).toEqual(['Tasks', 'TripObligationAccepted']);
|
expect(fields(submit(data))).toEqual(['Tasks', 'TripObligationAccepted']);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('validation: маршрут', () => {
|
describe('validation: маршрут', () => {
|
||||||
it('объект только из справочника', () => {
|
it('объект только из справочника', () => {
|
||||||
const route = { ...ROUTE, stops: [{ ...ROUTE.stops[0], projectId: '' }] };
|
const route = { ...ROUTE, stops: [{ ...ROUTE.stops[0], projectId: '' }] };
|
||||||
expect(fields(submitIssues({ ...FILLED, Route: route }))).toEqual(['stop-s1-project']);
|
expect(fields(submit({ ...FILLED, Route: route }))).toEqual(['stop-s1-project']);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('у каждого участка нужен вид транспорта', () => {
|
it('у каждого участка нужен вид транспорта', () => {
|
||||||
const route = { ...ROUTE, legTransport: { 'origin>s1': 'Авиа' } };
|
const route = { ...ROUTE, legTransport: { 'origin>s1': 'Авиа' } };
|
||||||
expect(fields(submitIssues({ ...FILLED, Route: route }))).toEqual(['leg-s1>origin']);
|
expect(fields(submit({ ...FILLED, Route: route }))).toEqual(['leg-s1>origin']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('прибытие задним числом не отправить, сегодня — можно; черновик сохраняется', () => {
|
||||||
|
expect(fields(submitIssues(FILLED, [], '2026-10-06'))).toEqual(['stop-s1-from']);
|
||||||
|
expect(submitIssues(FILLED, [], '2026-10-05')).toEqual([]);
|
||||||
|
expect(draftIssues(FILLED)).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('отъезд раньше прибытия блокирует даже черновик', () => {
|
it('отъезд раньше прибытия блокирует даже черновик', () => {
|
||||||
@@ -68,7 +80,7 @@ describe('validation: маршрут', () => {
|
|||||||
it('прибытие в следующую точку раньше отъезда из предыдущей', () => {
|
it('прибытие в следующую точку раньше отъезда из предыдущей', () => {
|
||||||
const second = { id: 's2', city: 'Актау', cityKz: '', cityKato: '', project: 'Порт', projectId: 'p2', dateFrom: '2026-10-06', dateTo: '2026-10-09' };
|
const second = { id: 's2', city: 'Актау', cityKz: '', cityKato: '', project: 'Порт', projectId: 'p2', dateFrom: '2026-10-06', dateTo: '2026-10-09' };
|
||||||
const route = { ...ROUTE, stops: [...ROUTE.stops, second], legTransport: { 'origin>s1': 'Авиа', 's1>s2': 'Авиа', 's2>origin': 'Авиа' } };
|
const route = { ...ROUTE, stops: [...ROUTE.stops, second], legTransport: { 'origin>s1': 'Авиа', 's1>s2': 'Авиа', 's2>origin': 'Авиа' } };
|
||||||
expect(fields(submitIssues({ ...FILLED, Route: route }))).toEqual(['stop-s2-from']);
|
expect(fields(submit({ ...FILLED, Route: route }))).toEqual(['stop-s2-from']);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -78,27 +90,27 @@ describe('validation: смета', () => {
|
|||||||
it('включённая статья без суммы — подсвечивается пустое поле', () => {
|
it('включённая статья без суммы — подсвечивается пустое поле', () => {
|
||||||
const filledQty = { ...accommodation, items: [{ ...accommodation.items[0], linkId: 's1', quantity: '3' }] };
|
const filledQty = { ...accommodation, items: [{ ...accommodation.items[0], linkId: 's1', quantity: '3' }] };
|
||||||
const data = { ...FILLED, Estimate: [off(perDiem), off(travel), filledQty] };
|
const data = { ...FILLED, Estimate: [off(perDiem), off(travel), filledQty] };
|
||||||
expect(fields(submitIssues(data))).toEqual([`item-${accommodation.items[0].id}-price`]);
|
expect(fields(submit(data))).toEqual([`item-${accommodation.items[0].id}-price`]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('суточные: дни из маршрута, от человека — только кол-во МРП', () => {
|
it('суточные: дни из маршрута, от человека — только кол-во МРП', () => {
|
||||||
const noMrp = { ...perDiem, items: [{ ...perDiem.items[0], linkId: 's1', quantity: '4' }] };
|
const noMrp = { ...perDiem, items: [{ ...perDiem.items[0], linkId: 's1', quantity: '4' }] };
|
||||||
const withMrp = { ...perDiem, items: [{ ...noMrp.items[0], mrpCount: '6' }] };
|
const withMrp = { ...perDiem, items: [{ ...noMrp.items[0], mrpCount: '6' }] };
|
||||||
const estimate = (article: IEstimateArticle): IEstimateArticle[] => [article, off(travel), off(accommodation)];
|
const estimate = (article: IEstimateArticle): IEstimateArticle[] => [article, off(travel), off(accommodation)];
|
||||||
expect(fields(submitIssues({ ...FILLED, Estimate: estimate(noMrp) }))).toEqual([`item-${perDiem.items[0].id}-mrp`]);
|
expect(fields(submit({ ...FILLED, Estimate: estimate(noMrp) }))).toEqual([`item-${perDiem.items[0].id}-mrp`]);
|
||||||
expect(submitIssues({ ...FILLED, Estimate: estimate(withMrp) })).toEqual([]);
|
expect(submit({ ...FILLED, Estimate: estimate(withMrp) })).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('при нескольких вариантах позицию нужно привязать', () => {
|
it('при нескольких вариантах позицию нужно привязать', () => {
|
||||||
// В маршруте «туда и обратно» у проезда два участка — без привязки не понять, какой оплачен.
|
// В маршруте «туда и обратно» у проезда два участка — без привязки не понять, какой оплачен.
|
||||||
const unlinked = { ...travel, items: [{ ...travel.items[0], linkId: '', amount: '50000' }] };
|
const unlinked = { ...travel, items: [{ ...travel.items[0], linkId: '', amount: '50000' }] };
|
||||||
const data = { ...FILLED, Estimate: [off(perDiem), unlinked, off(accommodation)] };
|
const data = { ...FILLED, Estimate: [off(perDiem), unlinked, off(accommodation)] };
|
||||||
expect(fields(submitIssues(data))).toEqual([`item-${travel.items[0].id}-link`]);
|
expect(fields(submit(data))).toEqual([`item-${travel.items[0].id}-link`]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('другой расход требует название и сумму', () => {
|
it('другой расход требует название и сумму', () => {
|
||||||
const other = createArticle('other', ROUTE);
|
const other = createArticle('other', ROUTE);
|
||||||
const data = { ...FILLED, Estimate: [...FILLED.Estimate, other] };
|
const data = { ...FILLED, Estimate: [...FILLED.Estimate, other] };
|
||||||
expect(fields(submitIssues(data))).toEqual([`article-${other.id}-title`, `item-${other.items[0].id}-amount`]);
|
expect(fields(submit(data))).toEqual([`article-${other.id}-title`, `item-${other.items[0].id}-amount`]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import type { IFieldIssue } from '../../shared/ui';
|
import type { IFieldIssue } from '../../shared/ui';
|
||||||
import type { IEmployeeOrganization } from '../../shared/dictionary';
|
import type { IEmployeeOrganization } from '../../shared/dictionary';
|
||||||
|
import { todaySubmitDate } from '../../shared/submitDate';
|
||||||
import { ESTIMATE_KINDS, IEstimateArticle, IEstimateItem, IFormData, IRouteStop, ITripRoute } from './model';
|
import { ESTIMATE_KINDS, IEstimateArticle, IEstimateItem, IFormData, IRouteStop, ITripRoute } from './model';
|
||||||
import { filledTasks } from './tripData';
|
import { filledTasks } from './tripData';
|
||||||
import { legLabel, routeLegs, stopCityLabel } from './route';
|
import { legLabel, routeLegs, stopCityLabel } from './route';
|
||||||
@@ -42,13 +43,17 @@ function stopDateOrderIssues(route: ITripRoute): IFieldIssue[] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Проблемы одной точки маршрута (кроме порядка дат внутри точки — см. stopDateOrderIssues). */
|
/** Проблемы одной точки маршрута (кроме порядка дат внутри точки — см. stopDateOrderIssues). */
|
||||||
function stopIssues(stop: IRouteStop, index: number, previous: IRouteStop | undefined): IFieldIssue[] {
|
function stopIssues(stop: IRouteStop, index: number, previous: IRouteStop | undefined, today: string): IFieldIssue[] {
|
||||||
const label = stopName(stop, index);
|
const label = stopName(stop, index);
|
||||||
const issues: IFieldIssue[] = [];
|
const issues: IFieldIssue[] = [];
|
||||||
if (!stop.city.trim()) issues.push({ field: fieldKey.stopCity(stop), label, message: 'укажите город' });
|
if (!stop.city.trim()) issues.push({ field: fieldKey.stopCity(stop), label, message: 'укажите город' });
|
||||||
// Объект — именно из справочника (projectId): по нему потом разносят затраты по проектам.
|
// Объект — именно из справочника (projectId): по нему потом разносят затраты по проектам.
|
||||||
if (!stop.projectId) issues.push({ field: fieldKey.stopProject(stop), label, message: 'выберите объект из списка' });
|
if (!stop.projectId) issues.push({ field: fieldKey.stopProject(stop), label, message: 'выберите объект из списка' });
|
||||||
if (!stop.dateFrom) issues.push({ field: fieldKey.stopFrom(stop), label, message: 'укажите дату прибытия' });
|
if (!stop.dateFrom) issues.push({ field: fieldKey.stopFrom(stop), label, message: 'укажите дату прибытия' });
|
||||||
|
// Задним числом командировку не оформляют. Сегодня — можно: выезд в день подачи.
|
||||||
|
if (stop.dateFrom && stop.dateFrom < today) {
|
||||||
|
issues.push({ field: fieldKey.stopFrom(stop), label, message: 'дата прибытия уже прошла' });
|
||||||
|
}
|
||||||
if (!stop.dateTo) issues.push({ field: fieldKey.stopTo(stop), label, message: 'укажите дату отъезда' });
|
if (!stop.dateTo) issues.push({ field: fieldKey.stopTo(stop), label, message: 'укажите дату отъезда' });
|
||||||
if (previous?.dateTo && stop.dateFrom && stop.dateFrom < previous.dateTo) {
|
if (previous?.dateTo && stop.dateFrom && stop.dateFrom < previous.dateTo) {
|
||||||
issues.push({ field: fieldKey.stopFrom(stop), label, message: 'прибытие раньше отъезда из предыдущей точки' });
|
issues.push({ field: fieldKey.stopFrom(stop), label, message: 'прибытие раньше отъезда из предыдущей точки' });
|
||||||
@@ -57,10 +62,10 @@ function stopIssues(stop: IRouteStop, index: number, previous: IRouteStop | unde
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Маршрут для отправки: город отправления, все точки, вид транспорта на каждом участке. */
|
/** Маршрут для отправки: город отправления, все точки, вид транспорта на каждом участке. */
|
||||||
function routeIssues(route: ITripRoute): IFieldIssue[] {
|
function routeIssues(route: ITripRoute, today: string): IFieldIssue[] {
|
||||||
const issues: IFieldIssue[] = [];
|
const issues: IFieldIssue[] = [];
|
||||||
if (!route.origin.trim()) issues.push({ field: fieldKey.origin, label: 'Маршрут', message: 'укажите город отправления' });
|
if (!route.origin.trim()) issues.push({ field: fieldKey.origin, label: 'Маршрут', message: 'укажите город отправления' });
|
||||||
route.stops.forEach((stop, i) => issues.push(...stopIssues(stop, i, route.stops[i - 1])));
|
route.stops.forEach((stop, i) => issues.push(...stopIssues(stop, i, route.stops[i - 1], today)));
|
||||||
issues.push(...stopDateOrderIssues(route));
|
issues.push(...stopDateOrderIssues(route));
|
||||||
routeLegs(route).forEach((leg) => {
|
routeLegs(route).forEach((leg) => {
|
||||||
if (!route.legTransport[leg.id]) {
|
if (!route.legTransport[leg.id]) {
|
||||||
@@ -121,8 +126,13 @@ export function draftIssues(data: IFormData): IFieldIssue[] {
|
|||||||
/**
|
/**
|
||||||
* Всё, что мешает отправке на согласование, в порядке формы (пусто — отправлять можно).
|
* Всё, что мешает отправке на согласование, в порядке формы (пусто — отправлять можно).
|
||||||
* organizations — организации сотрудника (см. useTripEmployee): из них выбирают командирующую.
|
* organizations — организации сотрудника (см. useTripEmployee): из них выбирают командирующую.
|
||||||
|
* today — YYYY-MM-DD; параметром, чтобы тесты не зависели от текущей даты.
|
||||||
*/
|
*/
|
||||||
export function submitIssues(data: IFormData, organizations: IEmployeeOrganization[] = []): IFieldIssue[] {
|
export function submitIssues(
|
||||||
|
data: IFormData,
|
||||||
|
organizations: IEmployeeOrganization[] = [],
|
||||||
|
today: string = todaySubmitDate()
|
||||||
|
): IFieldIssue[] {
|
||||||
const issues: IFieldIssue[] = [];
|
const issues: IFieldIssue[] = [];
|
||||||
if (!data.Employee) issues.push({ field: 'Employee', label: 'Сотрудник' });
|
if (!data.Employee) issues.push({ field: 'Employee', label: 'Сотрудник' });
|
||||||
if (!data.Position.trim()) issues.push({ field: 'Position', label: 'Должность' });
|
if (!data.Position.trim()) issues.push({ field: 'Position', label: 'Должность' });
|
||||||
@@ -130,7 +140,7 @@ export function submitIssues(data: IFormData, organizations: IEmployeeOrganizati
|
|||||||
if (organizations.length > 0 && !data.TripCompany) {
|
if (organizations.length > 0 && !data.TripCompany) {
|
||||||
issues.push({ field: 'TripCompany', label: 'Командирующая организация', message: 'выберите, от какой организации командировка' });
|
issues.push({ field: 'TripCompany', label: 'Командирующая организация', message: 'выберите, от какой организации командировка' });
|
||||||
}
|
}
|
||||||
issues.push(...routeIssues(data.Route));
|
issues.push(...routeIssues(data.Route, today));
|
||||||
if (!data.TripPurpose.trim()) issues.push({ field: 'TripPurpose', label: 'Цель' });
|
if (!data.TripPurpose.trim()) issues.push({ field: 'TripPurpose', label: 'Цель' });
|
||||||
if (filledTasks(data.Tasks).length === 0) {
|
if (filledTasks(data.Tasks).length === 0) {
|
||||||
issues.push({ field: 'Tasks', label: 'Задание на командировку', message: 'добавьте хотя бы одну задачу' });
|
issues.push({ field: 'Tasks', label: 'Задание на командировку', message: 'добавьте хотя бы одну задачу' });
|
||||||
|
|||||||
Reference in New Issue
Block a user