feat: город в командировке из справочника на русском и казахском
Город отправления и города точек выбираются из справочника КАТО; в маршруте хранятся оба названия и код (city/cityKz/cityKato, origin/originKz/originKato внутри TripRouteJson — новых колонок нет). Везде, включая TripCity/TripRoute и заголовок, город показывается как «Кульсары (Құлсары)». Введённый вручную город (село, заграница) сохраняется текстом. README дополнен и описанием командирующей организации. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -18,10 +18,26 @@
|
||||
## Особое поведение
|
||||
- **Можно подать за другого сотрудника.** По умолчанию — сам инициатор; при выборе другого
|
||||
подтягиваются его должность и организации (`Employee/ByEmail`, `useTripEmployee`).
|
||||
- **Командирующая организация** (с 10-23, логика — `tripCompany.ts`). Сотрудник может работать
|
||||
в нескольких компаниях — выбирают, от какой он едет (`Select` «Компания — должность»).
|
||||
Одна организация — выбирается сама, поле заблокировано. Несколько — обязательна при отправке,
|
||||
должность пустая до выбора и подставляется из выбранной компании. Сотрудника нет в
|
||||
справочнике — поля нет, отправку не блокирует. Смена сотрудника сбрасывает выбор. Хранение:
|
||||
`TripCompany` (название) + `TripCompanyId` (CompanyId — под будущий маршрут по компании);
|
||||
`Organization` — по-прежнему все организации сотрудника. Заявки до 10-23 в просмотре
|
||||
показывают прежний список организаций.
|
||||
- **Маршрут — лента точек** (`components/route`, логика — `route.ts`; гибрид «маршрут + смета»
|
||||
по образцу Concur/Navan/TripIt). Город отправления → точки (город + объект из `/Project` +
|
||||
даты прибытия/отъезда) → (возвращение — переключатель). Простая поездка — одна точка;
|
||||
«+ Добавить точку маршрута» — для поездок по нескольким объектам.
|
||||
- **Города — из справочника КАТО** (`shared/dictionary/kato`, поле `CityPicker`): 178 городов
|
||||
и посёлков РК из официального КАТО (stat.gov.kz), сгенерированы `scripts/Build-KatoCities.ps1`.
|
||||
Поиск по русскому и казахскому названию с учётом раскладки (Құлсары = Кулсары = Кульсары);
|
||||
набранное целиком однозначное название подставляется само. Выбор сохраняет оба названия и
|
||||
код КАТО (`city`/`cityKz`/`cityKato`, у города отправления — `origin`/`originKz`/`originKato`,
|
||||
всё внутри `TripRouteJson`), показывается везде как «Кульсары (Құлсары)» (`cityLabel`), в том
|
||||
числе в `TripCity`/`TripRoute` и заголовке. Нет в справочнике (село, заграница) — сохраняется
|
||||
введённый текст без казахского названия.
|
||||
- Участки пути не хранятся, а выводятся из точек (`routeLegs`, ключ `откуда>куда`); у
|
||||
каждого участка — вид транспорта плашками (`legTransport`, `TRANSPORT_TYPES`), обязателен.
|
||||
- Объект точки — только выбор из справочника (`projectId`): по объектам разносят затраты.
|
||||
@@ -76,7 +92,7 @@
|
||||
заголовком (см. грабли коллизий internalName).
|
||||
|
||||
## Секции формы
|
||||
EmployeeSection (сотрудник · должность · дата подачи · организация) → RouteSection (маршрут
|
||||
EmployeeSection (сотрудник · должность · дата подачи · командирующая организация) → RouteSection (маршрут
|
||||
лентой + цель) → TasksSection (задачи) → EstimateSection (смета + итог) →
|
||||
ObligationSection (текст обязательства + согласие).
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import styles from './PerDiemFields.module.scss';
|
||||
import { FormField, MoneyInput } from '../../../../shared/ui';
|
||||
import { IEstimateItem, IRouteStop, MRP_VALUE, MRP_YEAR } from '../../model';
|
||||
import { formatAmount, itemAmount } from '../../estimate';
|
||||
import { stopCityLabel } from '../../route';
|
||||
import { fieldKey } from '../../validation';
|
||||
|
||||
interface Props {
|
||||
@@ -23,7 +24,7 @@ export const PerDiemFields: React.FC<Props> = ({ item, stop, onChange }) => {
|
||||
return (
|
||||
<div className={styles.wrap}>
|
||||
<div className={styles.place}>
|
||||
<span className={styles.city}>{stop?.city.trim() || 'Точка маршрута'}</span>
|
||||
<span className={styles.city}>{(stop && stopCityLabel(stop)) || 'Точка маршрута'}</span>
|
||||
<span className={styles.project}>Проект: {project || 'не выбран'}</span>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import * as React from 'react';
|
||||
import type { WebPartContext } from '@microsoft/sp-webpart-base';
|
||||
import styles from './RouteEditor.module.scss';
|
||||
import { FormField, Icon, TextInput, Toggle, useFormReadOnly } from '../../../../shared/ui';
|
||||
import { IRouteStop, ITripRoute } from '../../model';
|
||||
import { IRouteLeg, createStop, routeLegs } from '../../route';
|
||||
import { FormField, Icon, Toggle, useFormReadOnly } from '../../../../shared/ui';
|
||||
import { CityPicker } from '../../../../shared/dictionary';
|
||||
import { CITY_PLACEHOLDER, IRouteStop, ITripRoute } from '../../model';
|
||||
import { IRouteLeg, createStop, originLabel, routeLegs } from '../../route';
|
||||
import { fieldKey } from '../../validation';
|
||||
import { RouteStopCard } from './RouteStopCard';
|
||||
import { RouteLegRow } from './RouteLegRow';
|
||||
@@ -71,11 +72,11 @@ export const RouteEditor: React.FC<Props> = ({ route, onChange, context }) => {
|
||||
<TimelineRow marker="endpoint">
|
||||
<div className={styles.origin}>
|
||||
<FormField label="Откуда (город отправления)" htmlFor={fieldKey.origin} required>
|
||||
<TextInput
|
||||
<CityPicker
|
||||
id={fieldKey.origin}
|
||||
value={route.origin}
|
||||
onChange={(v) => onChange({ ...route, origin: v })}
|
||||
placeholder="Напр.: Алматы"
|
||||
value={{ name: route.origin, nameKz: route.originKz, kato: route.originKato }}
|
||||
onChange={(city) => onChange({ ...route, origin: city.name, originKz: city.nameKz, originKato: city.kato })}
|
||||
placeholder={CITY_PLACEHOLDER}
|
||||
/>
|
||||
</FormField>
|
||||
</div>
|
||||
@@ -111,13 +112,13 @@ export const RouteEditor: React.FC<Props> = ({ route, onChange, context }) => {
|
||||
<div className={styles.end}>
|
||||
{readOnly ? (
|
||||
<span className={styles.endText}>
|
||||
{route.returnToOrigin ? `Возвращение: ${route.origin || 'город отправления'}` : 'Без возвращения в город отправления'}
|
||||
{route.returnToOrigin ? `Возвращение: ${originLabel(route) || 'город отправления'}` : 'Без возвращения в город отправления'}
|
||||
</span>
|
||||
) : (
|
||||
<Toggle
|
||||
checked={route.returnToOrigin}
|
||||
onChange={(returnToOrigin) => onChange({ ...route, returnToOrigin })}
|
||||
onLabel={`Возвращение в ${route.origin.trim() || 'город отправления'}`}
|
||||
onLabel={`Возвращение в ${originLabel(route) || 'город отправления'}`}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as React from 'react';
|
||||
import styles from './RouteScheme.module.scss';
|
||||
import { IEstimateArticle, ITripRoute } from '../../model';
|
||||
import { routeLegs, routePeriod, routeProjects } from '../../route';
|
||||
import { originLabel, routeLegs, routePeriod, routeProjects, stopCityLabel } from '../../route';
|
||||
import { estimateTotal, formatAmount, stopTotals } from '../../estimate';
|
||||
import { plural, tripDays } from '../../tripData';
|
||||
import { ISchemeNode } from './schemeNode';
|
||||
@@ -18,14 +18,14 @@ interface Props {
|
||||
/** Узлы схемы по порядку: город отправления → точки → (возвращение). */
|
||||
function schemeNodes(route: ITripRoute, amounts: Map<string, number>): ISchemeNode[] {
|
||||
const period = routePeriod(route);
|
||||
const origin = route.origin.trim();
|
||||
const origin = originLabel(route);
|
||||
const withDate = (caption: string, iso: string): string => (iso ? `${caption} ${shortDate(iso)}` : caption);
|
||||
|
||||
const nodes: ISchemeNode[] = [
|
||||
{ key: 'origin', kind: 'endpoint', city: origin, caption: withDate('выезд', period.start), amount: 0 }
|
||||
];
|
||||
route.stops.forEach((stop, i) =>
|
||||
nodes.push({ key: stop.id, kind: 'stop', number: i + 1, stop, city: stop.city.trim(), amount: amounts.get(stop.id) || 0 })
|
||||
nodes.push({ key: stop.id, kind: 'stop', number: i + 1, stop, city: stopCityLabel(stop), amount: amounts.get(stop.id) || 0 })
|
||||
);
|
||||
if (route.returnToOrigin) {
|
||||
nodes.push({ key: 'return', kind: 'endpoint', city: origin, caption: withDate('возвращение', period.end), amount: 0 });
|
||||
|
||||
@@ -2,8 +2,8 @@ import * as React from 'react';
|
||||
import type { WebPartContext } from '@microsoft/sp-webpart-base';
|
||||
import styles from './RouteStopCard.module.scss';
|
||||
import { FormField, Icon, TextInput } from '../../../../shared/ui';
|
||||
import { ProjectPicker } from '../../../../shared/dictionary';
|
||||
import { IRouteStop, PROJECT_PLACEHOLDER } from '../../model';
|
||||
import { CityPicker, ProjectPicker } from '../../../../shared/dictionary';
|
||||
import { CITY_PLACEHOLDER, IRouteStop, PROJECT_PLACEHOLDER } from '../../model';
|
||||
import { stopNights } from '../../route';
|
||||
import { plural } from '../../tripData';
|
||||
import { fieldKey } from '../../validation';
|
||||
@@ -41,7 +41,12 @@ export const RouteStopCard: React.FC<Props> = ({ stop, index, context, onChange,
|
||||
</div>
|
||||
<div className={styles.grid}>
|
||||
<FormField label="Город, населённый пункт" htmlFor={fieldKey.stopCity(stop)} required>
|
||||
<TextInput id={fieldKey.stopCity(stop)} value={stop.city} onChange={(v) => onChange({ city: v })} />
|
||||
<CityPicker
|
||||
id={fieldKey.stopCity(stop)}
|
||||
value={{ name: stop.city, nameKz: stop.cityKz, kato: stop.cityKato }}
|
||||
onChange={(city) => onChange({ city: city.name, cityKz: city.nameKz, cityKato: city.kato })}
|
||||
placeholder={CITY_PLACEHOLDER}
|
||||
/>
|
||||
</FormField>
|
||||
<FormField label="Объект" htmlFor={fieldKey.stopProject(stop)} required>
|
||||
<ProjectPicker
|
||||
|
||||
@@ -39,7 +39,7 @@ export const SchemeNodeLabel: React.FC<Props> = ({ node }) => {
|
||||
const dates = stopDates(stop);
|
||||
return (
|
||||
<span className={styles.text}>
|
||||
<span className={styles.city}>{stop.city.trim() || `Город ${NOT_SET}`}</span>
|
||||
<span className={styles.city}>{node.city || `Город ${NOT_SET}`}</span>
|
||||
{/* Названия объектов в справочнике бывают с полным адресом — режем до 2 строк, полное — в подсказке. */}
|
||||
<span className={styles.project} title={stop.project.trim() || undefined}>
|
||||
{stop.project.trim() || 'объект не выбран'}
|
||||
|
||||
@@ -8,7 +8,7 @@ export interface ISchemeNode {
|
||||
number?: number;
|
||||
/** Сама точка — только у точки маршрута. */
|
||||
stop?: IRouteStop;
|
||||
/** Город отправления — у endpoint. */
|
||||
/** Город на двух языках (см. route.ts/stopCityLabel): отправления — у endpoint, точки — у stop. */
|
||||
city: string;
|
||||
/** «выезд 02.10» / «возвращение 05.10» — у endpoint. */
|
||||
caption?: string;
|
||||
|
||||
@@ -17,9 +17,11 @@ import {
|
||||
|
||||
const ROUTE: ITripRoute = {
|
||||
origin: 'Алматы',
|
||||
originKz: '',
|
||||
originKato: '',
|
||||
stops: [
|
||||
{ id: 's1', city: 'Атырау', project: 'Нефтебаза-2', projectId: 'p1', dateFrom: '2026-10-05', dateTo: '2026-10-08' },
|
||||
{ id: 's2', city: 'Актау', project: 'Порт Курык', projectId: 'p2', dateFrom: '2026-10-08', dateTo: '2026-10-10' }
|
||||
{ id: 's1', city: 'Атырау', cityKz: '', cityKato: '', project: 'Нефтебаза-2', projectId: 'p1', dateFrom: '2026-10-05', dateTo: '2026-10-08' },
|
||||
{ id: 's2', city: 'Актау', cityKz: '', cityKato: '', project: 'Порт Курык', projectId: 'p2', dateFrom: '2026-10-08', dateTo: '2026-10-10' }
|
||||
],
|
||||
returnToOrigin: true,
|
||||
legTransport: {}
|
||||
|
||||
@@ -20,7 +20,12 @@ export const FORM_TITLE = 'Заявка на командировку';
|
||||
export interface IRouteStop {
|
||||
/** Стабильный ключ точки: на него ссылаются позиции сметы (IEstimateItem.linkId). */
|
||||
id: string;
|
||||
/** Город на русском — из справочника КАТО (shared/dictionary/kato) или введён вручную. */
|
||||
city: string;
|
||||
/** Город на казахском; '' — введён вручную (в справочнике не нашёлся). */
|
||||
cityKz: string;
|
||||
/** Код КАТО города; '' — введён вручную. */
|
||||
cityKato: string;
|
||||
/** Объект — проект из справочника /Project (см. shared/dictionary/ProjectPicker). */
|
||||
project: string;
|
||||
/** oid проекта; '' — текст введён вручную без выбора из списка (отправка не пропустит). */
|
||||
@@ -38,8 +43,12 @@ export interface IRouteStop {
|
||||
* участка по его ключу.
|
||||
*/
|
||||
export interface ITripRoute {
|
||||
/** Город отправления (обычно город работы). */
|
||||
/** Город отправления (обычно город работы), на русском. */
|
||||
origin: string;
|
||||
/** Город отправления на казахском; '' — введён вручную. */
|
||||
originKz: string;
|
||||
/** Код КАТО города отправления; '' — введён вручную. */
|
||||
originKato: string;
|
||||
stops: IRouteStop[];
|
||||
/** Обратный путь из последней точки в город отправления. */
|
||||
returnToOrigin: boolean;
|
||||
@@ -306,7 +315,9 @@ export const FIRST_STOP_ID = 'stop-1';
|
||||
|
||||
export const EMPTY_ROUTE: ITripRoute = {
|
||||
origin: '',
|
||||
stops: [{ id: FIRST_STOP_ID, city: '', project: '', projectId: '', dateFrom: '', dateTo: '' }],
|
||||
originKz: '',
|
||||
originKato: '',
|
||||
stops: [{ id: FIRST_STOP_ID, city: '', cityKz: '', cityKato: '', project: '', projectId: '', dateFrom: '', dateTo: '' }],
|
||||
returnToOrigin: true,
|
||||
legTransport: {}
|
||||
};
|
||||
@@ -364,4 +375,5 @@ export const OBLIGATION_TEXT =
|
||||
export const OBLIGATION_LABEL = 'Ознакомлен(а) и согласен(на)';
|
||||
|
||||
export const EMPLOYEE_PLACEHOLDER = 'Начните вводить Ф.И.О. сотрудника…';
|
||||
export const CITY_PLACEHOLDER = 'Начните вводить город: Атырау, Құлсары…';
|
||||
export const PROJECT_PLACEHOLDER ='Начните вводить название или код объекта…';
|
||||
|
||||
@@ -14,9 +14,11 @@ import {
|
||||
|
||||
const ROUTE: ITripRoute = {
|
||||
origin: 'Алматы',
|
||||
originKz: '',
|
||||
originKato: '',
|
||||
stops: [
|
||||
{ id: 's1', city: 'Атырау', project: 'Нефтебаза-2', projectId: 'p1', dateFrom: '2026-10-05', dateTo: '2026-10-08' },
|
||||
{ id: 's2', city: 'Актау', project: 'Порт Курык', projectId: 'p2', dateFrom: '2026-10-08', dateTo: '2026-10-10' }
|
||||
{ id: 's1', city: 'Атырау', cityKz: '', cityKato: '', project: 'Нефтебаза-2', projectId: 'p1', dateFrom: '2026-10-05', dateTo: '2026-10-08' },
|
||||
{ id: 's2', city: 'Актау', cityKz: '', cityKato: '', project: 'Порт Курык', projectId: 'p2', dateFrom: '2026-10-08', dateTo: '2026-10-10' }
|
||||
],
|
||||
returnToOrigin: true,
|
||||
legTransport: { 'origin>s1': 'Авиа' }
|
||||
@@ -48,6 +50,18 @@ describe('сводка маршрута', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('город из справочника — на двух языках, если названия различаются', () => {
|
||||
const route: ITripRoute = {
|
||||
...ROUTE,
|
||||
origin: 'Уральск',
|
||||
originKz: 'Орал',
|
||||
stops: [{ ...ROUTE.stops[0], city: 'Кульсары', cityKz: 'Құлсары' }, { ...ROUTE.stops[1], cityKz: 'Актау' }],
|
||||
returnToOrigin: false
|
||||
};
|
||||
expect(routeCities(route)).toBe('Кульсары (Құлсары), Актау');
|
||||
expect(routeLegs(route).map(legLabel)).toEqual(['Уральск (Орал) → Кульсары (Құлсары)', 'Кульсары (Құлсары) → Актау']);
|
||||
});
|
||||
|
||||
it('новая точка начинается с даты отъезда из предыдущей', () => {
|
||||
expect(createStop(ROUTE.stops[1]).dateFrom).toBe('2026-10-10');
|
||||
expect(createStop(undefined).dateFrom).toBe('');
|
||||
@@ -59,6 +73,13 @@ describe('хранение маршрута', () => {
|
||||
expect(parseRoute(serializeRoute(ROUTE))).toEqual(ROUTE);
|
||||
});
|
||||
|
||||
it('заявка до справочника КАТО — город как был, без казахского названия', () => {
|
||||
const old = { origin: 'Алматы', stops: [{ id: 's1', city: 'Атырау', project: '', projectId: '', dateFrom: '', dateTo: '' }] };
|
||||
const route = parseRoute(JSON.stringify(old));
|
||||
expect([route.originKz, route.stops[0].cityKz, route.stops[0].cityKato]).toEqual(['', '', '']);
|
||||
expect(routeCities(route)).toBe('Атырау');
|
||||
});
|
||||
|
||||
it('битый JSON или маршрут без точек — маршрут новой заявки', () => {
|
||||
expect(parseRoute('{oops')).toEqual(EMPTY_ROUTE);
|
||||
expect(parseRoute(JSON.stringify({ origin: 'Алматы', stops: [] }))).toEqual(EMPTY_ROUTE);
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { formatIsoDate } from '../../shared/dateUtils';
|
||||
// Прямой импорт, не через shared/dictionary: индекс тянет сервисы с sp-http, а route.ts тестируется без SPFx.
|
||||
import { cityLabel } from '../../shared/dictionary/kato/cityLabel';
|
||||
import { EMPTY_ROUTE, IRouteStop, ITripRoute, ORIGIN_ID } from './model';
|
||||
import { tripDays } from './tripData';
|
||||
|
||||
@@ -24,8 +26,18 @@ export function newId(prefix: string): string {
|
||||
const ORIGIN_FALLBACK = 'Город отправления';
|
||||
const STOP_FALLBACK = 'Точка маршрута';
|
||||
|
||||
/** Город точки на двух языках: «Кульсары (Құлсары)»; '' — не указан. */
|
||||
export function stopCityLabel(stop: IRouteStop): string {
|
||||
return cityLabel(stop.city, stop.cityKz);
|
||||
}
|
||||
|
||||
/** Город отправления на двух языках; '' — не указан. */
|
||||
export function originLabel(route: ITripRoute): string {
|
||||
return cityLabel(route.origin, route.originKz);
|
||||
}
|
||||
|
||||
function stopCity(stop: IRouteStop): string {
|
||||
return stop.city.trim() || STOP_FALLBACK;
|
||||
return stopCityLabel(stop) || STOP_FALLBACK;
|
||||
}
|
||||
|
||||
/** Подпись точки в выпадающих списках сметы: «Атырау · Нефтебаза-2». */
|
||||
@@ -40,7 +52,7 @@ export function stopLabel(stop: IRouteStop): string {
|
||||
* участки пересобрались сами.
|
||||
*/
|
||||
export function routeLegs(route: ITripRoute): IRouteLeg[] {
|
||||
const origin = { id: ORIGIN_ID, name: route.origin.trim() || ORIGIN_FALLBACK };
|
||||
const origin = { id: ORIGIN_ID, name: originLabel(route) || ORIGIN_FALLBACK };
|
||||
const points = [origin, ...route.stops.map((s) => ({ id: s.id, name: stopCity(s) }))];
|
||||
if (route.returnToOrigin) points.push(origin);
|
||||
|
||||
@@ -91,7 +103,7 @@ const unique = (values: string[]): string[] =>
|
||||
|
||||
/** Города точек через запятую — для колонки TripCity и заголовка элемента. */
|
||||
export function routeCities(route: ITripRoute): string {
|
||||
return unique(route.stops.map((s) => s.city)).join(', ');
|
||||
return unique(route.stops.map(stopCityLabel)).join(', ');
|
||||
}
|
||||
|
||||
/** Объекты точек (без повторов) — для колонки TripProjects. */
|
||||
@@ -106,12 +118,12 @@ function stopDates(stop: IRouteStop): string {
|
||||
|
||||
/** Маршрут читаемым текстом для колонки SP: «Алматы → Атырау (Нефтебаза-2, 05.10.2026–08.10.2026) → Алматы». */
|
||||
export function routeText(route: ITripRoute): string {
|
||||
const parts: string[] = [route.origin.trim() || ORIGIN_FALLBACK];
|
||||
const parts: string[] = [originLabel(route) || ORIGIN_FALLBACK];
|
||||
route.stops.forEach((stop) => {
|
||||
const details = [stop.project.trim(), stopDates(stop)].filter(Boolean).join(', ');
|
||||
parts.push(details ? `${stopCity(stop)} (${details})` : stopCity(stop));
|
||||
});
|
||||
if (route.returnToOrigin) parts.push(route.origin.trim() || ORIGIN_FALLBACK);
|
||||
if (route.returnToOrigin) parts.push(originLabel(route) || ORIGIN_FALLBACK);
|
||||
return parts.join(' → ');
|
||||
}
|
||||
|
||||
@@ -123,6 +135,8 @@ export function createStop(previous: IRouteStop | undefined): IRouteStop {
|
||||
return {
|
||||
id: newId('stop'),
|
||||
city: '',
|
||||
cityKz: '',
|
||||
cityKato: '',
|
||||
project: '',
|
||||
projectId: '',
|
||||
dateFrom: previous?.dateTo || '',
|
||||
@@ -144,6 +158,9 @@ function toStop(value: unknown): IRouteStop | undefined {
|
||||
return {
|
||||
id,
|
||||
city: text(raw.city),
|
||||
// Заявки до справочника КАТО — без этих полей: город остаётся введённым вручную.
|
||||
cityKz: text(raw.cityKz),
|
||||
cityKato: text(raw.cityKato),
|
||||
project: text(raw.project),
|
||||
projectId: text(raw.projectId),
|
||||
dateFrom: text(raw.dateFrom),
|
||||
@@ -177,6 +194,8 @@ export function parseRoute(raw: unknown): ITripRoute {
|
||||
if (stops.length === 0) return EMPTY_ROUTE;
|
||||
return {
|
||||
origin: text(rawRoute.origin),
|
||||
originKz: text(rawRoute.originKz),
|
||||
originKato: text(rawRoute.originKato),
|
||||
stops,
|
||||
returnToOrigin: rawRoute.returnToOrigin !== false,
|
||||
legTransport: toTransport(rawRoute.legTransport)
|
||||
|
||||
@@ -6,7 +6,9 @@ const off = (article: IEstimateArticle): IEstimateArticle => ({ ...article, enab
|
||||
|
||||
const ROUTE: ITripRoute = {
|
||||
origin: 'Алматы',
|
||||
stops: [{ id: 's1', city: 'Атырау', project: 'Нефтебаза-2', projectId: 'p1', dateFrom: '2026-10-05', dateTo: '2026-10-08' }],
|
||||
originKz: '',
|
||||
originKato: '',
|
||||
stops: [{ id: 's1', city: 'Атырау', cityKz: '', cityKato: '', project: 'Нефтебаза-2', projectId: 'p1', dateFrom: '2026-10-05', dateTo: '2026-10-08' }],
|
||||
returnToOrigin: true,
|
||||
legTransport: { 'origin>s1': 'Авиа', 's1>origin': 'Авиа' }
|
||||
};
|
||||
@@ -64,7 +66,7 @@ describe('validation: маршрут', () => {
|
||||
});
|
||||
|
||||
it('прибытие в следующую точку раньше отъезда из предыдущей', () => {
|
||||
const second = { id: 's2', city: 'Актау', 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': 'Авиа' } };
|
||||
expect(fields(submitIssues({ ...FILLED, Route: route }))).toEqual(['stop-s2-from']);
|
||||
});
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { IFieldIssue } from '../../shared/ui';
|
||||
import type { IEmployeeOrganization } from '../../shared/dictionary';
|
||||
import { ESTIMATE_KINDS, IEstimateArticle, IEstimateItem, IFormData, IRouteStop, ITripRoute } from './model';
|
||||
import { filledTasks } from './tripData';
|
||||
import { legLabel, routeLegs } from './route';
|
||||
import { legLabel, routeLegs, stopCityLabel } from './route';
|
||||
import { articleTitle, itemAmount, itemLinkLabel, linkOptions, resolveLinkId } from './estimate';
|
||||
|
||||
/**
|
||||
@@ -26,7 +26,8 @@ export const fieldKey = {
|
||||
|
||||
/** «Точка 2 (Актау)» — как точку видно в форме. */
|
||||
function stopName(stop: IRouteStop, index: number): string {
|
||||
return stop.city.trim() ? `Точка ${index + 1} (${stop.city.trim()})` : `Точка ${index + 1}`;
|
||||
const city = stopCityLabel(stop);
|
||||
return city ? `Точка ${index + 1} (${city})` : `Точка ${index + 1}`;
|
||||
}
|
||||
|
||||
/** Дата окончания раньше начала — блокирует даже черновик: такие даты не сохранить осмысленно. */
|
||||
|
||||
Reference in New Issue
Block a user