Skip to content

Commit cf82bfa

Browse files
committed
Add hybrid stop create logic and components
1 parent 19348e0 commit cf82bfa

7 files changed

Lines changed: 549 additions & 4 deletions

File tree

Lines changed: 291 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,291 @@
1+
import {
2+
Combobox,
3+
ComboboxButton,
4+
ComboboxInput,
5+
ComboboxOption,
6+
ComboboxOptions,
7+
} from '@headlessui/react';
8+
import debounce from 'lodash/debounce';
9+
import { FC, useEffect, useMemo, useState } from 'react';
10+
import { useTranslation } from 'react-i18next';
11+
import { MdOutlineSearch } from 'react-icons/md';
12+
import { StopRegistryTransportModeType } from '../../../../../generated/graphql';
13+
import { mapStopRegistryTransportModeTypeToUiName } from '../../../../../i18n/uiNameMappings';
14+
import { StopWithDetails } from '../../../../../types';
15+
import {
16+
JoreListbox,
17+
ListboxOptionItem,
18+
Modal,
19+
ModalBody,
20+
ModalHeader,
21+
NewModalFooter,
22+
SimpleButton,
23+
comboboxStyles,
24+
} from '../../../../../uiComponents';
25+
import { parseVehicleMode } from '../../../../../utils';
26+
import {
27+
showDangerToastWithError,
28+
showSuccessToast,
29+
} from '../../../../../utils/toastService';
30+
import { StopModalStopAreaFormSchema } from '../../../../forms/stop/types';
31+
import {
32+
formatIsoDateString,
33+
useFindStopAreas,
34+
} from '../../../../forms/stop/utils';
35+
import { useCreateMirrorQuay } from './useCreateMirrorQuay';
36+
37+
const testIds = {
38+
modal: 'MakeHybridStopModal',
39+
transportModeDropdown: 'MakeHybridStopModal::transportMode',
40+
stopAreaInput: 'MakeHybridStopModal::stopAreaInput',
41+
stopAreaOption: (code: string) => `MakeHybridStopModal::stopArea::${code}`,
42+
confirmButton: 'MakeHybridStopModal::confirm',
43+
cancelButton: 'MakeHybridStopModal::cancel',
44+
};
45+
46+
const SUPPORTED_TRANSPORT_MODES = [
47+
StopRegistryTransportModeType.Bus,
48+
StopRegistryTransportModeType.Tram,
49+
] as const;
50+
51+
type MakeHybridStopModalProps = {
52+
readonly isOpen: boolean;
53+
readonly onClose: () => void;
54+
readonly parentStop: StopWithDetails | null;
55+
};
56+
57+
export const MakeHybridStopModal: FC<MakeHybridStopModalProps> = ({
58+
isOpen,
59+
onClose,
60+
parentStop,
61+
}) => {
62+
const { t } = useTranslation();
63+
const { createMirrorQuay, loading: saving } = useCreateMirrorQuay();
64+
65+
const currentTransportMode = parentStop?.stop_place?.transportMode ?? null;
66+
67+
const availableModes = useMemo(
68+
() =>
69+
SUPPORTED_TRANSPORT_MODES.filter((mode) => mode !== currentTransportMode),
70+
[currentTransportMode],
71+
);
72+
73+
const transportModeOptions: ReadonlyArray<
74+
ListboxOptionItem<StopRegistryTransportModeType>
75+
> = useMemo(
76+
() =>
77+
availableModes.map((mode) => ({
78+
value: mode,
79+
content: mapStopRegistryTransportModeTypeToUiName(t, mode),
80+
})),
81+
[availableModes, t],
82+
);
83+
84+
const [selectedMode, setSelectedMode] = useState<
85+
StopRegistryTransportModeType | undefined
86+
>(undefined);
87+
88+
const vehicleMode = selectedMode ? parseVehicleMode(selectedMode) : undefined;
89+
90+
const [query, setQuery] = useState('');
91+
const [queryDebounced, setQueryDebounced] = useState(false);
92+
const { areas, loading: loadingAreas } = useFindStopAreas(query, vehicleMode);
93+
94+
const [selectedStopArea, setSelectedStopArea] =
95+
useState<StopModalStopAreaFormSchema | null>(null);
96+
97+
useEffect(() => {
98+
if (!loadingAreas) {
99+
setQueryDebounced(false);
100+
}
101+
}, [loadingAreas]);
102+
103+
const onQueryChange = useMemo(() => {
104+
const debouncedSetQuery = debounce(setQuery, 500);
105+
return (newQuery: string) => {
106+
if (newQuery === '') {
107+
debouncedSetQuery.cancel();
108+
setQueryDebounced(false);
109+
setQuery('');
110+
} else {
111+
setQueryDebounced(true);
112+
debouncedSetQuery(newQuery);
113+
}
114+
};
115+
}, []);
116+
117+
const handleModeChange = (mode: StopRegistryTransportModeType) => {
118+
setSelectedMode(mode);
119+
setSelectedStopArea(null);
120+
setQuery('');
121+
};
122+
123+
const resetState = () => {
124+
setSelectedMode(undefined);
125+
setSelectedStopArea(null);
126+
setQuery('');
127+
setQueryDebounced(false);
128+
};
129+
130+
const handleClose = () => {
131+
resetState();
132+
onClose();
133+
};
134+
135+
const handleConfirm = async () => {
136+
if (!parentStop || !selectedStopArea || !selectedMode || !vehicleMode) {
137+
return;
138+
}
139+
140+
try {
141+
const success = await createMirrorQuay({
142+
targetStopPlaceId: selectedStopArea.netexId,
143+
parentStop,
144+
vehicleMode,
145+
});
146+
147+
if (success) {
148+
showSuccessToast(t(($) => $.stopDetails.hybrid.success));
149+
handleClose();
150+
}
151+
} catch (err) {
152+
showDangerToastWithError(
153+
t(($) => $.stopDetails.hybrid.error),
154+
err,
155+
);
156+
}
157+
};
158+
159+
const areaSearchLoading = loadingAreas || queryDebounced;
160+
const canConfirm = !!selectedMode && !!selectedStopArea && !saving;
161+
162+
return (
163+
<Modal
164+
isOpen={isOpen}
165+
onClose={handleClose}
166+
contentClassName="w-1/3"
167+
testId={testIds.modal}
168+
>
169+
<ModalHeader
170+
onClose={handleClose}
171+
heading={t(($) => $.stopDetails.hybrid.title)}
172+
/>
173+
<ModalBody className="space-y-4">
174+
<div>
175+
<label className="mb-1 block text-sm font-bold">
176+
{t(($) => $.stopDetails.hybrid.transportMode)}
177+
</label>
178+
<JoreListbox<StopRegistryTransportModeType>
179+
buttonContent={
180+
selectedMode
181+
? mapStopRegistryTransportModeTypeToUiName(t, selectedMode)
182+
: t(($) => $.stopDetails.hybrid.selectTransportMode)
183+
}
184+
options={transportModeOptions}
185+
value={selectedMode}
186+
onChange={handleModeChange}
187+
testId={testIds.transportModeDropdown}
188+
/>
189+
</div>
190+
191+
<div>
192+
<label className="mb-1 block text-sm font-bold">
193+
{t(($) => $.stopDetails.hybrid.stopArea)}
194+
</label>
195+
<Combobox
196+
as="div"
197+
className={comboboxStyles.root('flex flex-col')}
198+
value={selectedStopArea}
199+
onChange={setSelectedStopArea}
200+
disabled={!selectedMode}
201+
>
202+
<div className="flex h-(--input-height)">
203+
<ComboboxInput<StopModalStopAreaFormSchema>
204+
className={comboboxStyles.input(
205+
'grow border-r-0 outline-0 ui-not-open:rounded-tr-none ui-not-open:rounded-br-none',
206+
'ui-open:rounded-bl-none',
207+
)}
208+
onChange={(e) => onQueryChange(e.target.value)}
209+
displayValue={(it) => it?.nameFin ?? ''}
210+
autoComplete="off"
211+
placeholder={t(($) => $.stopDetails.hybrid.searchStopArea)}
212+
data-testid={testIds.stopAreaInput}
213+
/>
214+
<ComboboxButton
215+
disabled={!query}
216+
className={comboboxStyles.button(
217+
'static flex h-(--input-height) w-(--input-height) justify-center rounded-tr-[5px] rounded-br-[5px] bg-tweaked-brand text-xl',
218+
'ui-open:rounded-br-none',
219+
)}
220+
>
221+
<MdOutlineSearch color="white" />
222+
</ComboboxButton>
223+
</div>
224+
<ComboboxOptions
225+
anchor="bottom start"
226+
className={comboboxStyles.options(
227+
'min-w-[calc(var(--button-width)+var(--input-width))]',
228+
)}
229+
transition
230+
>
231+
{areaSearchLoading && (
232+
<ComboboxOption
233+
className={comboboxStyles.option()}
234+
value={null}
235+
disabled
236+
>
237+
{t(($) => $.stops.stopArea.label)}
238+
</ComboboxOption>
239+
)}
240+
241+
{areas.map((area) => (
242+
<ComboboxOption
243+
className={comboboxStyles.option()}
244+
key={area.netexId}
245+
value={area}
246+
data-testid={testIds.stopAreaOption(area.privateCode)}
247+
>
248+
<span className="shrink-0 self-start font-bold">
249+
{area.privateCode}
250+
</span>
251+
<div className="mx-2 flex grow flex-col">
252+
<span>{area.nameFin ?? area.nameSwe}</span>
253+
<span className="font-bold">
254+
{`${formatIsoDateString(area.validityStart)} - ${formatIsoDateString(area.validityEnd)}`}
255+
</span>
256+
</div>
257+
</ComboboxOption>
258+
))}
259+
260+
{!query && !areaSearchLoading && (
261+
<ComboboxOption
262+
className={comboboxStyles.option()}
263+
value={null}
264+
disabled
265+
>
266+
{t(($) => $.stops.stopArea.help)}
267+
</ComboboxOption>
268+
)}
269+
</ComboboxOptions>
270+
</Combobox>
271+
</div>
272+
</ModalBody>
273+
<NewModalFooter>
274+
<SimpleButton
275+
inverted
276+
onClick={handleClose}
277+
testId={testIds.cancelButton}
278+
>
279+
{t(($) => $.stopDetails.hybrid.cancel)}
280+
</SimpleButton>
281+
<SimpleButton
282+
onClick={handleConfirm}
283+
disabled={!canConfirm}
284+
testId={testIds.confirmButton}
285+
>
286+
{saving ? '...' : t(($) => $.stopDetails.hybrid.confirm)}
287+
</SimpleButton>
288+
</NewModalFooter>
289+
</Modal>
290+
);
291+
};
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { MakeHybridStopModal } from './MakeHybridStopModal';

0 commit comments

Comments
 (0)