diff --git a/src/webparts/requestPortal/forms/BusinessTrip/README.md b/src/webparts/requestPortal/forms/BusinessTrip/README.md index 0b97f40..0f1e761 100644 --- a/src/webparts/requestPortal/forms/BusinessTrip/README.md +++ b/src/webparts/requestPortal/forms/BusinessTrip/README.md @@ -63,6 +63,8 @@ - **Задним числом нельзя** (с 10-23): прибытие в любую точку — не раньше сегодняшнего дня (сегодня можно). Прошедшие дни недоступны в календаре (`min`); набранную вручную или «устаревшую» в черновике дату ловит `validation.ts` при отправке, черновик сохраняется. + - **Не дольше 40 дней** (с 10-23, `MAX_TRIP_DAYS`): от прибытия в первую точку до отъезда из + последней включительно; при отправке подсвечивается отъезд последней точки. - Хранение: `TripRouteJson`; для отчётов из маршрута пишутся `TripRoute` (текстом), `TripCity` (города), `Project`/`ProjectId` (объект ПЕРВОЙ точки — колонка у всех форм одна), `TripProjects` (все объекты), `TripStartDate`/`TripEndDate`/`TripDaysCount` (весь период). diff --git a/src/webparts/requestPortal/forms/BusinessTrip/model.ts b/src/webparts/requestPortal/forms/BusinessTrip/model.ts index 8ba3b75..56677d5 100644 --- a/src/webparts/requestPortal/forms/BusinessTrip/model.ts +++ b/src/webparts/requestPortal/forms/BusinessTrip/model.ts @@ -260,6 +260,9 @@ export const ESTIMATE_KIND_ORDER: ReadonlyArray = [ export const MRP_VALUE = 4325; export const MRP_YEAR = 2026; +/** Самая длинная командировка, дней включительно (требования кадров 10-23). */ +export const MAX_TRIP_DAYS = 40; + /** Лимит суточных, МРП в день (требования кадров 10-23): за рубежом выше. */ export const MAX_MRP_PER_DAY = { domestic: 6, abroad: 8 } as const; diff --git a/src/webparts/requestPortal/forms/BusinessTrip/validation.test.ts b/src/webparts/requestPortal/forms/BusinessTrip/validation.test.ts index af285c6..c3bb1cd 100644 --- a/src/webparts/requestPortal/forms/BusinessTrip/validation.test.ts +++ b/src/webparts/requestPortal/forms/BusinessTrip/validation.test.ts @@ -84,6 +84,13 @@ describe('validation: маршрут', () => { expect(draftIssues(FILLED)).toEqual([]); }); + it('командировка не дольше 40 дней включительно', () => { + const until = (dateTo: string): ITripRoute => ({ ...ROUTE, stops: [{ ...ROUTE.stops[0], dateTo }] }); + // 05.10 – 13.11 = 40 дней, 05.10 – 14.11 = 41. + expect(submit({ ...FILLED, Route: until('2026-11-13') })).toEqual([]); + expect(fields(submit({ ...FILLED, Route: until('2026-11-14') }))).toEqual(['stop-s1-to']); + }); + it('отъезд раньше прибытия блокирует даже черновик', () => { const route = { ...ROUTE, stops: [{ ...ROUTE.stops[0], dateTo: '2026-10-01' }] }; expect(fields(draftIssues({ ...FILLED, Route: route }))).toEqual(['stop-s1-to']); diff --git a/src/webparts/requestPortal/forms/BusinessTrip/validation.ts b/src/webparts/requestPortal/forms/BusinessTrip/validation.ts index 5a34e1d..7e6845a 100644 --- a/src/webparts/requestPortal/forms/BusinessTrip/validation.ts +++ b/src/webparts/requestPortal/forms/BusinessTrip/validation.ts @@ -1,9 +1,18 @@ import type { IFieldIssue } from '../../shared/ui'; import type { IEmployeeOrganization } from '../../shared/dictionary'; import { todaySubmitDate } from '../../shared/submitDate'; -import { ESTIMATE_KINDS, EstimateKind, IEstimateArticle, IEstimateItem, IFormData, IRouteStop, ITripRoute } from './model'; -import { filledTasks } from './tripData'; -import { legLabel, routeLegs, stopCityLabel } from './route'; +import { + ESTIMATE_KINDS, + EstimateKind, + IEstimateArticle, + IEstimateItem, + IFormData, + IRouteStop, + ITripRoute, + MAX_TRIP_DAYS +} from './model'; +import { filledTasks, plural, tripDays } from './tripData'; +import { legLabel, routeLegs, routePeriod, stopCityLabel } from './route'; import { articleTitle, formatAmount, @@ -78,12 +87,31 @@ function stopIssues(stop: IRouteStop, index: number, previous: IRouteStop | unde return issues; } +/** + * Вся поездка — не дольше MAX_TRIP_DAYS дней (от прибытия в первую точку до отъезда + * из последней, включительно). Подсвечиваем отъезд последней точки — его обычно и правят. + */ +function tripLengthIssues(route: ITripRoute): IFieldIssue[] { + const period = routePeriod(route); + const days = tripDays(period.start, period.end); + const last = route.stops[route.stops.length - 1]; + if (days === undefined || days <= MAX_TRIP_DAYS || !last) return []; + return [ + { + field: fieldKey.stopTo(last), + label: 'Маршрут', + message: `командировка — ${plural(days, ['день', 'дня', 'дней'])}, можно не больше ${MAX_TRIP_DAYS}` + } + ]; +} + /** Маршрут для отправки: город отправления, все точки, вид транспорта на каждом участке. */ function routeIssues(route: ITripRoute, today: string): IFieldIssue[] { const issues: IFieldIssue[] = []; 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], today))); issues.push(...stopDateOrderIssues(route)); + issues.push(...tripLengthIssues(route)); routeLegs(route).forEach((leg) => { if (!route.legTransport[leg.id]) { issues.push({ field: fieldKey.legTransport(leg.id), label: `Участок ${legLabel(leg)}`, message: 'выберите вид транспорта' });