feat: предпочтительное время выезда на участках командировки
У каждого участка маршрута рядом с видом транспорта — «Утро / День / Вечер / Ночь» (необязательно, повторный клик снимает выбор). Хранится в TripRouteJson (legDepartureTime) — новых колонок нет. На схеме — только в подсказке плашки транспорта. Стили плашек вынесены в общий ChoiceChips.module.scss. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -49,6 +49,9 @@
|
|||||||
- Участки пути не хранятся, а выводятся из точек (`routeLegs`, ключ `откуда>куда`); у
|
- Участки пути не хранятся, а выводятся из точек (`routeLegs`, ключ `откуда>куда`); у
|
||||||
каждого участка — вид транспорта плашками (`legTransport`, `TRANSPORT_TYPES`: авиа, ж/д,
|
каждого участка — вид транспорта плашками (`legTransport`, `TRANSPORT_TYPES`: авиа, ж/д,
|
||||||
такси, служебный и личный автотранспорт; «Автобус» заменён на «Такси» 10-23), обязателен.
|
такси, служебный и личный автотранспорт; «Автобус» заменён на «Такси» 10-23), обязателен.
|
||||||
|
Там же **предпочтительное время выезда** (с 10-23): «Утро / День / Вечер / Ночь»
|
||||||
|
(`legDepartureTime`, `DEPARTURE_TIMES`), необязательно — повторный клик снимает выбор. На
|
||||||
|
схеме — только в подсказке плашки транспорта, вёрстку схемы не трогали.
|
||||||
- Объект точки по Казахстану — только выбор из справочника (`projectId`): по объектам
|
- Объект точки по Казахстану — только выбор из справочника (`projectId`): по объектам
|
||||||
разносят затраты. У зарубежной точки поле «Объект / место назначения» принимает и свой
|
разносят затраты. У зарубежной точки поле «Объект / место назначения» принимает и свой
|
||||||
текст (завод, выставка — в справочнике их нет), обязательно лишь непустое значение.
|
текст (завод, выставка — в справочнике их нет), обязательно лишь непустое значение.
|
||||||
|
|||||||
+1
@@ -1,4 +1,5 @@
|
|||||||
@import '../../../../../styles/design-tokens';
|
@import '../../../../../styles/design-tokens';
|
||||||
|
// Плашки выбора в один клик: вид транспорта (TransportChips) и время выезда (DepartureTimeChips).
|
||||||
|
|
||||||
.chips {
|
.chips {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import styles from './ChoiceChips.module.scss';
|
||||||
|
import { useFormReadOnly } from '../../../shared/ui';
|
||||||
|
import { DEPARTURE_TIMES } from '../model';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
/** Значение из DEPARTURE_TIMES; '' — не указано. */
|
||||||
|
value: string;
|
||||||
|
onChange: (value: string) => void;
|
||||||
|
/** Подпись группы для экранных дикторов, напр. «Время выезда: Алматы → Атырау». */
|
||||||
|
ariaLabel: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Предпочтительное время выезда плашками. Поле необязательное: повторный клик по
|
||||||
|
* выбранной плашке снимает выбор. В режиме чтения — только выбранная плашка
|
||||||
|
* (не указано — ничего не показываем).
|
||||||
|
*/
|
||||||
|
export const DepartureTimeChips: React.FC<Props> = ({ value, onChange, ariaLabel }) => {
|
||||||
|
const readOnly = useFormReadOnly();
|
||||||
|
|
||||||
|
if (readOnly) {
|
||||||
|
return value ? <span className={`${styles.chip} ${styles.selected} ${styles.static}`}>{value}</span> : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.chips} role="radiogroup" aria-label={ariaLabel}>
|
||||||
|
{DEPARTURE_TIMES.map((time) => {
|
||||||
|
const selected = time === value;
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={time}
|
||||||
|
type="button"
|
||||||
|
role="radio"
|
||||||
|
aria-checked={selected}
|
||||||
|
className={selected ? `${styles.chip} ${styles.selected}` : styles.chip}
|
||||||
|
onClick={() => onChange(selected ? '' : time)}
|
||||||
|
>
|
||||||
|
{time}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import styles from './TransportChips.module.scss';
|
import styles from './ChoiceChips.module.scss';
|
||||||
import { useFormReadOnly } from '../../../shared/ui';
|
import { useFormReadOnly } from '../../../shared/ui';
|
||||||
import { TRANSPORT_TYPES } from '../model';
|
import { TRANSPORT_TYPES } from '../model';
|
||||||
import { TransportIcon } from './TransportIcon';
|
import { TransportIcon } from './TransportIcon';
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import styles from './RouteEditor.module.scss';
|
|||||||
import { FormField, Icon, Toggle, useFormReadOnly } from '../../../../shared/ui';
|
import { FormField, Icon, Toggle, useFormReadOnly } from '../../../../shared/ui';
|
||||||
import { CityPicker } from '../../../../shared/dictionary';
|
import { CityPicker } from '../../../../shared/dictionary';
|
||||||
import { CITY_PLACEHOLDER, IRouteStop, ITripRoute } from '../../model';
|
import { CITY_PLACEHOLDER, IRouteStop, ITripRoute } from '../../model';
|
||||||
import { IRouteLeg, createStop, originLabel, routeLegs } from '../../route';
|
import { IRouteLeg, createStop, originLabel, routeLegs, withoutStopLegs } from '../../route';
|
||||||
import { fieldKey } from '../../validation';
|
import { fieldKey } from '../../validation';
|
||||||
import { RouteStopCard } from './RouteStopCard';
|
import { RouteStopCard } from './RouteStopCard';
|
||||||
import { RouteLegRow } from './RouteLegRow';
|
import { RouteLegRow } from './RouteLegRow';
|
||||||
@@ -47,23 +47,29 @@ export const RouteEditor: React.FC<Props> = ({ route, onChange, context }) => {
|
|||||||
onChange({ ...route, stops: route.stops.map((s) => (s.id === id ? { ...s, ...patch } : s)) });
|
onChange({ ...route, stops: route.stops.map((s) => (s.id === id ? { ...s, ...patch } : s)) });
|
||||||
const addStop = (): void =>
|
const addStop = (): void =>
|
||||||
onChange({ ...route, stops: [...route.stops, createStop(route.stops[route.stops.length - 1])] });
|
onChange({ ...route, stops: [...route.stops, createStop(route.stops[route.stops.length - 1])] });
|
||||||
const removeStop = (id: string): void => {
|
// Транспорт и время выезда участков, которые шли через удалённую точку, больше ни к чему не относятся.
|
||||||
// Виды транспорта участков, которые шли через удалённую точку, больше ни к чему не относятся.
|
const removeStop = (id: string): void =>
|
||||||
const legTransport: Record<string, string> = {};
|
onChange({
|
||||||
Object.keys(route.legTransport)
|
...route,
|
||||||
.filter((key) => key.split('>').indexOf(id) === -1)
|
stops: route.stops.filter((s) => s.id !== id),
|
||||||
.forEach((key) => {
|
legTransport: withoutStopLegs(route.legTransport, id),
|
||||||
legTransport[key] = route.legTransport[key];
|
legDepartureTime: withoutStopLegs(route.legDepartureTime, id)
|
||||||
});
|
});
|
||||||
onChange({ ...route, stops: route.stops.filter((s) => s.id !== id), legTransport });
|
|
||||||
};
|
|
||||||
const setTransport = (id: string, value: string): void =>
|
const setTransport = (id: string, value: string): void =>
|
||||||
onChange({ ...route, legTransport: { ...route.legTransport, [id]: value } });
|
onChange({ ...route, legTransport: { ...route.legTransport, [id]: value } });
|
||||||
|
const setDepartureTime = (id: string, value: string): void =>
|
||||||
|
onChange({ ...route, legDepartureTime: { ...route.legDepartureTime, [id]: value } });
|
||||||
|
|
||||||
const legRow = (leg: IRouteLeg | undefined): React.ReactNode =>
|
const legRow = (leg: IRouteLeg | undefined): React.ReactNode =>
|
||||||
leg && (
|
leg && (
|
||||||
<TimelineRow marker="line">
|
<TimelineRow marker="line">
|
||||||
<RouteLegRow leg={leg} transport={route.legTransport[leg.id] || ''} onTransportChange={(v) => setTransport(leg.id, v)} />
|
<RouteLegRow
|
||||||
|
leg={leg}
|
||||||
|
transport={route.legTransport[leg.id] || ''}
|
||||||
|
onTransportChange={(v) => setTransport(leg.id, v)}
|
||||||
|
departureTime={route.legDepartureTime[leg.id] || ''}
|
||||||
|
onDepartureTimeChange={(v) => setDepartureTime(leg.id, v)}
|
||||||
|
/>
|
||||||
</TimelineRow>
|
</TimelineRow>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
+17
@@ -2,5 +2,22 @@
|
|||||||
|
|
||||||
// Участок — без карточки: это «переезд» между точками, он лежит прямо на линии маршрута.
|
// Участок — без карточки: это «переезд» между точками, он лежит прямо на линии маршрута.
|
||||||
.leg {
|
.leg {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: $sp-2;
|
||||||
padding-block: $sp-2;
|
padding-block: $sp-2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Время выезда — второстепенно: подпись слева, плашки в строку; на узком экране переносятся.
|
||||||
|
.time {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: $sp-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeLabel {
|
||||||
|
font-family: $font-sans;
|
||||||
|
font-size: $fs-label;
|
||||||
|
color: $gray-500;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,25 +1,43 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import styles from './RouteLegRow.module.scss';
|
import styles from './RouteLegRow.module.scss';
|
||||||
import { FormField } from '../../../../shared/ui';
|
import { FormField, useFormReadOnly } from '../../../../shared/ui';
|
||||||
import { IRouteLeg, legLabel } from '../../route';
|
import { IRouteLeg, legLabel } from '../../route';
|
||||||
import { fieldKey } from '../../validation';
|
import { fieldKey } from '../../validation';
|
||||||
import { TransportChips } from '../TransportChips';
|
import { TransportChips } from '../TransportChips';
|
||||||
|
import { DepartureTimeChips } from '../DepartureTimeChips';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
leg: IRouteLeg;
|
leg: IRouteLeg;
|
||||||
transport: string;
|
transport: string;
|
||||||
onTransportChange: (value: string) => void;
|
onTransportChange: (value: string) => void;
|
||||||
|
/** Предпочтительное время выезда; '' — не указано. */
|
||||||
|
departureTime: string;
|
||||||
|
onDepartureTimeChange: (value: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Участок пути между точками на линии маршрута: «Алматы → Атырау» и вид транспорта. */
|
/**
|
||||||
export const RouteLegRow: React.FC<Props> = ({ leg, transport, onTransportChange }) => (
|
* Участок пути между точками на линии маршрута: «Алматы → Атырау», вид транспорта
|
||||||
|
* и предпочтительное время выезда (необязательно).
|
||||||
|
*/
|
||||||
|
export const RouteLegRow: React.FC<Props> = ({ leg, transport, onTransportChange, departureTime, onDepartureTimeChange }) => {
|
||||||
|
const readOnly = useFormReadOnly();
|
||||||
|
|
||||||
|
return (
|
||||||
<div className={styles.leg}>
|
<div className={styles.leg}>
|
||||||
<FormField label={legLabel(leg)} fieldKey={fieldKey.legTransport(leg.id)}>
|
<FormField label={legLabel(leg)} fieldKey={fieldKey.legTransport(leg.id)}>
|
||||||
<TransportChips
|
<TransportChips value={transport} onChange={onTransportChange} ariaLabel={`Вид транспорта: ${legLabel(leg)}`} />
|
||||||
value={transport}
|
|
||||||
onChange={onTransportChange}
|
|
||||||
ariaLabel={`Вид транспорта: ${legLabel(leg)}`}
|
|
||||||
/>
|
|
||||||
</FormField>
|
</FormField>
|
||||||
|
{/* В просмотре без указанного времени строка не нужна. */}
|
||||||
|
{(!readOnly || departureTime) && (
|
||||||
|
<div className={styles.time}>
|
||||||
|
<span className={styles.timeLabel}>Выезд</span>
|
||||||
|
<DepartureTimeChips
|
||||||
|
value={departureTime}
|
||||||
|
onChange={onDepartureTimeChange}
|
||||||
|
ariaLabel={`Время выезда: ${legLabel(leg)}`}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ export const RouteScheme: React.FC<Props> = ({ route, articles, onSelectStop })
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.scheme}>
|
<div className={styles.scheme}>
|
||||||
<SchemeTrack nodes={nodes} legs={legs} legTransport={route.legTransport} onSelectStop={onSelectStop} />
|
<SchemeTrack nodes={nodes} legs={legs} legTransport={route.legTransport} legDepartureTime={route.legDepartureTime} onSelectStop={onSelectStop} />
|
||||||
<div className={styles.stats}>{stats.join(' · ')}</div>
|
<div className={styles.stats}>{stats.join(' · ')}</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ interface Props {
|
|||||||
/** Участки между соседними узлами: legs[i] — от nodes[i] к nodes[i + 1]. */
|
/** Участки между соседними узлами: legs[i] — от nodes[i] к nodes[i + 1]. */
|
||||||
legs: IRouteLeg[];
|
legs: IRouteLeg[];
|
||||||
legTransport: Record<string, string>;
|
legTransport: Record<string, string>;
|
||||||
|
/** Время выезда по ключу участка — только в подсказке плашки, вёрстку схемы не меняет. */
|
||||||
|
legDepartureTime: Record<string, string>;
|
||||||
onSelectStop?: (stopId: string) => void;
|
onSelectStop?: (stopId: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,7 +79,7 @@ const NodeCell: React.FC<{ node: ISchemeNode; onSelectStop?: (stopId: string) =>
|
|||||||
};
|
};
|
||||||
|
|
||||||
/** Широкий экран: ветка слева направо — SVG по координатам + сетка подписей с теми же колонками. */
|
/** Широкий экран: ветка слева направо — SVG по координатам + сетка подписей с теми же колонками. */
|
||||||
const HorizontalTrack: React.FC<Props> = ({ nodes, legs, legTransport, onSelectStop }) => {
|
const HorizontalTrack: React.FC<Props> = ({ nodes, legs, legTransport, legDepartureTime, onSelectStop }) => {
|
||||||
const count = nodes.length;
|
const count = nodes.length;
|
||||||
return (
|
return (
|
||||||
<div className={styles.hTrack} style={{ minWidth: count * MIN_COLUMN_WIDTH }}>
|
<div className={styles.hTrack} style={{ minWidth: count * MIN_COLUMN_WIDTH }}>
|
||||||
@@ -116,7 +118,7 @@ const HorizontalTrack: React.FC<Props> = ({ nodes, legs, legTransport, onSelectS
|
|||||||
</svg>
|
</svg>
|
||||||
{legs.map((leg, i) => (
|
{legs.map((leg, i) => (
|
||||||
<span key={leg.id} className={styles.hBadge} style={{ left: `${((i + 1) / count) * 100}%` }}>
|
<span key={leg.id} className={styles.hBadge} style={{ left: `${((i + 1) / count) * 100}%` }}>
|
||||||
<TransportBadge leg={leg} transport={legTransport[leg.id] || ''} />
|
<TransportBadge leg={leg} transport={legTransport[leg.id] || ''} departureTime={legDepartureTime[leg.id] || ''} />
|
||||||
</span>
|
</span>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -130,7 +132,7 @@ const HorizontalTrack: React.FC<Props> = ({ nodes, legs, legTransport, onSelectS
|
|||||||
};
|
};
|
||||||
|
|
||||||
/** Телефон: ветка сверху вниз; линия — фоном колонки маркеров, без вычислений геометрии. */
|
/** Телефон: ветка сверху вниз; линия — фоном колонки маркеров, без вычислений геометрии. */
|
||||||
const VerticalTrack: React.FC<Props> = ({ nodes, legs, legTransport, onSelectStop }) => (
|
const VerticalTrack: React.FC<Props> = ({ nodes, legs, legTransport, legDepartureTime, onSelectStop }) => (
|
||||||
<div className={styles.vTrack}>
|
<div className={styles.vTrack}>
|
||||||
{nodes.map((node, i) => {
|
{nodes.map((node, i) => {
|
||||||
const leg = legs[i];
|
const leg = legs[i];
|
||||||
@@ -151,7 +153,7 @@ const VerticalTrack: React.FC<Props> = ({ nodes, legs, legTransport, onSelectSto
|
|||||||
<div className={styles.vRow}>
|
<div className={styles.vRow}>
|
||||||
<span className={styles.vRail} />
|
<span className={styles.vRail} />
|
||||||
<span className={styles.vLeg}>
|
<span className={styles.vLeg}>
|
||||||
<TransportBadge leg={leg} transport={legTransport[leg.id] || ''} />
|
<TransportBadge leg={leg} transport={legTransport[leg.id] || ''} departureTime={legDepartureTime[leg.id] || ''} />
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -7,13 +7,20 @@ import { TransportIcon } from '../TransportIcon';
|
|||||||
interface Props {
|
interface Props {
|
||||||
leg: IRouteLeg;
|
leg: IRouteLeg;
|
||||||
transport: string;
|
transport: string;
|
||||||
|
/** Время выезда; '' — не указано. */
|
||||||
|
departureTime: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Плашка вида транспорта на линии схемы: иконка + короткая подпись; полная — в подсказке. */
|
/**
|
||||||
export const TransportBadge: React.FC<Props> = ({ leg, transport }) => (
|
* Плашка вида транспорта на линии схемы: иконка + короткая подпись; полная и время
|
||||||
|
* выезда — в подсказке (на линию не помещаются).
|
||||||
|
*/
|
||||||
|
export const TransportBadge: React.FC<Props> = ({ leg, transport, departureTime }) => (
|
||||||
<span
|
<span
|
||||||
className={transport ? styles.transport : `${styles.transport} ${styles.missing}`}
|
className={transport ? styles.transport : `${styles.transport} ${styles.missing}`}
|
||||||
title={`${leg.from} → ${leg.to}${transport ? `: ${transport}` : ''}`}
|
title={[`${leg.from} → ${leg.to}`, transport, departureTime && `выезд: ${departureTime.toLowerCase()}`]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(', ')}
|
||||||
>
|
>
|
||||||
<TransportIcon type={transport} className={styles.transportIcon} />
|
<TransportIcon type={transport} className={styles.transportIcon} />
|
||||||
{transport ? TRANSPORT_SHORT[transport] || transport : 'транспорт?'}
|
{transport ? TRANSPORT_SHORT[transport] || transport : 'транспорт?'}
|
||||||
|
|||||||
@@ -35,7 +35,8 @@ const ROUTE: ITripRoute = {
|
|||||||
{ id: 's2', abroad: false, country: '', city: 'Актау', cityKz: '', cityKato: '', project: 'Порт Курык', projectId: 'p2', dateFrom: '2026-10-08', dateTo: '2026-10-10' }
|
{ id: 's2', abroad: false, country: '', city: 'Актау', cityKz: '', cityKato: '', project: 'Порт Курык', projectId: 'p2', dateFrom: '2026-10-08', dateTo: '2026-10-10' }
|
||||||
],
|
],
|
||||||
returnToOrigin: true,
|
returnToOrigin: true,
|
||||||
legTransport: {}
|
legTransport: {},
|
||||||
|
legDepartureTime: {}
|
||||||
};
|
};
|
||||||
|
|
||||||
const item = (patch: Partial<IEstimateItem>): IEstimateItem => ({
|
const item = (patch: Partial<IEstimateItem>): IEstimateItem => ({
|
||||||
|
|||||||
@@ -65,8 +65,13 @@ export interface ITripRoute {
|
|||||||
returnToOrigin: boolean;
|
returnToOrigin: boolean;
|
||||||
/** Вид транспорта по ключу участка (ключ — legId из route.ts). */
|
/** Вид транспорта по ключу участка (ключ — legId из route.ts). */
|
||||||
legTransport: Record<string, string>;
|
legTransport: Record<string, string>;
|
||||||
|
/** Предпочтительное время выезда по ключу участка (DEPARTURE_TIMES); нет ключа — не указано. */
|
||||||
|
legDepartureTime: Record<string, string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Предпочтительное время выезда на участке (требования кадров 10-23); необязательно. */
|
||||||
|
export const DEPARTURE_TIMES: ReadonlyArray<string> = ['Утро', 'День', 'Вечер', 'Ночь'];
|
||||||
|
|
||||||
/** Ключ «точки» города отправления в ключах участков. */
|
/** Ключ «точки» города отправления в ключах участков. */
|
||||||
export const ORIGIN_ID = 'origin';
|
export const ORIGIN_ID = 'origin';
|
||||||
|
|
||||||
@@ -357,7 +362,8 @@ export const EMPTY_ROUTE: ITripRoute = {
|
|||||||
{ id: FIRST_STOP_ID, abroad: false, country: '', city: '', cityKz: '', cityKato: '', project: '', projectId: '', dateFrom: '', dateTo: '' }
|
{ id: FIRST_STOP_ID, abroad: false, country: '', city: '', cityKz: '', cityKato: '', project: '', projectId: '', dateFrom: '', dateTo: '' }
|
||||||
],
|
],
|
||||||
returnToOrigin: true,
|
returnToOrigin: true,
|
||||||
legTransport: {}
|
legTransport: {},
|
||||||
|
legDepartureTime: {}
|
||||||
};
|
};
|
||||||
|
|
||||||
function emptyItem(kind: EstimateKind, linkId: string): IEstimateItem {
|
function emptyItem(kind: EstimateKind, linkId: string): IEstimateItem {
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ import {
|
|||||||
serializeRoute,
|
serializeRoute,
|
||||||
stopLocationPatch,
|
stopLocationPatch,
|
||||||
stopNights,
|
stopNights,
|
||||||
tripKind
|
tripKind,
|
||||||
|
withoutStopLegs
|
||||||
} from './route';
|
} from './route';
|
||||||
|
|
||||||
const ROUTE: ITripRoute = {
|
const ROUTE: ITripRoute = {
|
||||||
@@ -23,7 +24,8 @@ const ROUTE: ITripRoute = {
|
|||||||
{ id: 's2', abroad: false, country: '', city: 'Актау', cityKz: '', cityKato: '', project: 'Порт Курык', projectId: 'p2', dateFrom: '2026-10-08', dateTo: '2026-10-10' }
|
{ id: 's2', abroad: false, country: '', city: 'Актау', cityKz: '', cityKato: '', project: 'Порт Курык', projectId: 'p2', dateFrom: '2026-10-08', dateTo: '2026-10-10' }
|
||||||
],
|
],
|
||||||
returnToOrigin: true,
|
returnToOrigin: true,
|
||||||
legTransport: { 'origin>s1': 'Авиа' }
|
legTransport: { 'origin>s1': 'Авиа' },
|
||||||
|
legDepartureTime: {}
|
||||||
};
|
};
|
||||||
|
|
||||||
describe('участки пути', () => {
|
describe('участки пути', () => {
|
||||||
@@ -79,6 +81,11 @@ describe('сводка маршрута', () => {
|
|||||||
expect([createStop(beijing).abroad, createStop(beijing).country]).toEqual([true, 'Китай']);
|
expect([createStop(beijing).abroad, createStop(beijing).country]).toEqual([true, 'Китай']);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('удалили точку — транспорт и время выезда её участков уходят', () => {
|
||||||
|
const times = { 'origin>s1': 'Утро', 's1>s2': 'Вечер', 's2>origin': 'Ночь' };
|
||||||
|
expect(withoutStopLegs(times, 's2')).toEqual({ 'origin>s1': 'Утро' });
|
||||||
|
});
|
||||||
|
|
||||||
it('смена «Казахстан / другая страна» сбрасывает чужие поля, город остаётся', () => {
|
it('смена «Казахстан / другая страна» сбрасывает чужие поля, город остаётся', () => {
|
||||||
expect(stopLocationPatch(true)).toEqual({ abroad: true, cityKz: '', cityKato: '' });
|
expect(stopLocationPatch(true)).toEqual({ abroad: true, cityKz: '', cityKato: '' });
|
||||||
expect(stopLocationPatch(false)).toEqual({ abroad: false, country: '' });
|
expect(stopLocationPatch(false)).toEqual({ abroad: false, country: '' });
|
||||||
@@ -95,6 +102,7 @@ describe('хранение маршрута', () => {
|
|||||||
const route = parseRoute(JSON.stringify(old));
|
const route = parseRoute(JSON.stringify(old));
|
||||||
expect([route.originKz, route.stops[0].cityKz, route.stops[0].cityKato]).toEqual(['', '', '']);
|
expect([route.originKz, route.stops[0].cityKz, route.stops[0].cityKato]).toEqual(['', '', '']);
|
||||||
expect([route.stops[0].abroad, route.stops[0].country]).toEqual([false, '']);
|
expect([route.stops[0].abroad, route.stops[0].country]).toEqual([false, '']);
|
||||||
|
expect(route.legDepartureTime).toEqual({});
|
||||||
expect(routeCities(route)).toBe('Атырау');
|
expect(routeCities(route)).toBe('Атырау');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -192,12 +192,24 @@ function toStop(value: unknown): IRouteStop | undefined {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function toTransport(value: unknown): Record<string, string> {
|
/** Словарь «ключ участка → значение» (вид транспорта, время выезда); не строки отбрасываются. */
|
||||||
|
function toLegRecord(value: unknown): Record<string, string> {
|
||||||
if (!value || typeof value !== 'object') return {};
|
if (!value || typeof value !== 'object') return {};
|
||||||
const result: Record<string, string> = {};
|
const result: Record<string, string> = {};
|
||||||
Object.keys(value as object).forEach((key) => {
|
Object.keys(value as object).forEach((key) => {
|
||||||
const type = (value as Record<string, unknown>)[key];
|
const entry = (value as Record<string, unknown>)[key];
|
||||||
if (typeof type === 'string') result[key] = type;
|
if (typeof entry === 'string') result[key] = entry;
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Убрать из словаря участков те, что шли через точку stopId (её удалили из маршрута). */
|
||||||
|
export function withoutStopLegs(record: Record<string, string>, stopId: string): Record<string, string> {
|
||||||
|
const result: Record<string, string> = {};
|
||||||
|
Object.keys(record)
|
||||||
|
.filter((key) => key.split('>').indexOf(stopId) === -1)
|
||||||
|
.forEach((key) => {
|
||||||
|
result[key] = record[key];
|
||||||
});
|
});
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
@@ -222,6 +234,8 @@ export function parseRoute(raw: unknown): ITripRoute {
|
|||||||
originKato: text(rawRoute.originKato),
|
originKato: text(rawRoute.originKato),
|
||||||
stops,
|
stops,
|
||||||
returnToOrigin: rawRoute.returnToOrigin !== false,
|
returnToOrigin: rawRoute.returnToOrigin !== false,
|
||||||
legTransport: toTransport(rawRoute.legTransport)
|
legTransport: toLegRecord(rawRoute.legTransport),
|
||||||
|
// Заявки до времени выезда — без этого поля: время просто не указано.
|
||||||
|
legDepartureTime: toLegRecord(rawRoute.legDepartureTime)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,8 @@ const ROUTE: ITripRoute = {
|
|||||||
originKato: '',
|
originKato: '',
|
||||||
stops: [{ id: 's1', abroad: false, country: '', city: 'Атырау', cityKz: '', cityKato: '', project: 'Нефтебаза-2', projectId: 'p1', dateFrom: '2026-10-05', dateTo: '2026-10-08' }],
|
stops: [{ id: 's1', abroad: false, country: '', city: 'Атырау', cityKz: '', cityKato: '', project: 'Нефтебаза-2', projectId: 'p1', dateFrom: '2026-10-05', dateTo: '2026-10-08' }],
|
||||||
returnToOrigin: true,
|
returnToOrigin: true,
|
||||||
legTransport: { 'origin>s1': 'Авиа', 's1>origin': 'Авиа' }
|
legTransport: { 'origin>s1': 'Авиа', 's1>origin': 'Авиа' },
|
||||||
|
legDepartureTime: {}
|
||||||
};
|
};
|
||||||
|
|
||||||
const FILLED: IFormData = {
|
const FILLED: IFormData = {
|
||||||
|
|||||||
Reference in New Issue
Block a user