From 50a1ae2ce3e07efaa6f79d933e64b3f39f10f228 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A2=D0=B8=D0=BC=D1=83=D1=80=20=D0=93=D0=BE=D1=80=D0=BE?= =?UTF-8?q?=D0=B4=D0=B5=D1=86=D0=BA=D0=B8=D0=B9?= Date: Fri, 23 Oct 2026 15:14:45 +0500 Subject: [PATCH] =?UTF-8?q?feat:=20=D0=BA=D0=BE=D0=BC=D0=B0=D0=BD=D0=B4?= =?UTF-8?q?=D0=B8=D1=80=D0=BE=D0=B2=D0=BA=D0=B0=20=D0=BD=D0=B5=20=D0=B4?= =?UTF-8?q?=D0=BE=D0=BB=D1=8C=D1=88=D0=B5=2040=20=D0=B4=D0=BD=D0=B5=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit От прибытия в первую точку до отъезда из последней включительно; при отправке подсвечивается отъезд последней точки. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../forms/BusinessTrip/README.md | 2 ++ .../requestPortal/forms/BusinessTrip/model.ts | 3 ++ .../forms/BusinessTrip/validation.test.ts | 7 ++++ .../forms/BusinessTrip/validation.ts | 34 +++++++++++++++++-- 4 files changed, 43 insertions(+), 3 deletions(-) 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: 'выберите вид транспорта' });