feat: командировка не дольше 40 дней
От прибытия в первую точку до отъезда из последней включительно; при отправке подсвечивается отъезд последней точки. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -63,6 +63,8 @@
|
||||
- **Задним числом нельзя** (с 10-23): прибытие в любую точку — не раньше сегодняшнего дня
|
||||
(сегодня можно). Прошедшие дни недоступны в календаре (`min`); набранную вручную или
|
||||
«устаревшую» в черновике дату ловит `validation.ts` при отправке, черновик сохраняется.
|
||||
- **Не дольше 40 дней** (с 10-23, `MAX_TRIP_DAYS`): от прибытия в первую точку до отъезда из
|
||||
последней включительно; при отправке подсвечивается отъезд последней точки.
|
||||
- Хранение: `TripRouteJson`; для отчётов из маршрута пишутся `TripRoute` (текстом),
|
||||
`TripCity` (города), `Project`/`ProjectId` (объект ПЕРВОЙ точки — колонка у всех форм одна),
|
||||
`TripProjects` (все объекты), `TripStartDate`/`TripEndDate`/`TripDaysCount` (весь период).
|
||||
|
||||
@@ -260,6 +260,9 @@ export const ESTIMATE_KIND_ORDER: ReadonlyArray<EstimateKind> = [
|
||||
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;
|
||||
|
||||
|
||||
@@ -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']);
|
||||
|
||||
@@ -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: 'выберите вид транспорта' });
|
||||
|
||||
Reference in New Issue
Block a user