@autobusal/routes-order 1.32.0 → 1.32.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -0
- package/Checkout/Checkout.tsx +35 -1
- package/Step4/Passenger/Passenger.tsx +196 -12
- package/Step4/Passengers/Display.tsx +6 -2
- package/Step4/Passengers/Passengers.tsx +16 -2
- package/Step4/Saved/Choose.tsx +99 -0
- package/Step4/Saved/Remember.tsx +58 -0
- package/Step4/Saved/styles.ts +87 -0
- package/package.json +1 -1
- package/services.ts +75 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.32.1
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **A saved-passenger chooser and a "remember this passenger" checkbox**, per passenger rather than per order, because "remember me but not the person I am buying for" is the ordinary case and one control per booking cannot say it. The list is fetched once for the whole checkout, not once per traveller.
|
|
8
|
+
|
|
9
|
+
Picking applies the whole person, not the filled parts: a passport left from a previous choice is cleared, because the customer has said who is sitting in that seat. Only fields the route actually collects are written - react-hook-form submits values set for names it never registered, so writing the rest would post a WhatsApp number to a route that never asked for one.
|
|
10
|
+
|
|
11
|
+
A journey that needs a field the saved profile lacks says so in the form's own words. `missing` absent means *not measured* and is not treated as "nothing missing".
|
|
12
|
+
|
|
3
13
|
## 1.32.0
|
|
4
14
|
|
|
5
15
|
### Added
|
package/Checkout/Checkout.tsx
CHANGED
|
@@ -24,7 +24,7 @@ import { CouponData } from '@autobusal/providers/types/orders';
|
|
|
24
24
|
import { TotalData } from '../types';
|
|
25
25
|
import { useUserStore } from '@autobusal/providers/stores/user';
|
|
26
26
|
import { useGetSettings } from '@autobusal/providers/services';
|
|
27
|
-
import { usePostOrder, useGetFlex, useGetPayments } from '../services';
|
|
27
|
+
import { usePostOrder, useGetFlex, useGetPayments, useGetSavedPassengers } from '../services';
|
|
28
28
|
import { addon as commerceAddon, beginCheckout, stash } from '@autobusal/providers/Setup/commerce';
|
|
29
29
|
import { item as commerceItem } from '../commerce';
|
|
30
30
|
|
|
@@ -125,6 +125,38 @@ const Checkout = ({ values, step1, step2, step3, isPartner, t, onStore, onBack }
|
|
|
125
125
|
step1.babies ?? 0
|
|
126
126
|
);
|
|
127
127
|
|
|
128
|
+
/**
|
|
129
|
+
* "Book somebody you have booked before" - saved passengers.
|
|
130
|
+
*
|
|
131
|
+
* Edited: Claude - Date: 2026-08-20
|
|
132
|
+
*
|
|
133
|
+
* Fetched ONCE here and drilled down rather than asked for inside each
|
|
134
|
+
* passenger block: a family of five would otherwise be five requests for
|
|
135
|
+
* one list, and the list is the same list.
|
|
136
|
+
*
|
|
137
|
+
* ONLY FOR A SIGNED-IN CUSTOMER. The endpoint is `role:visitor`, so a
|
|
138
|
+
* guest and a member of staff both get a 404 - a deliberate refusal that
|
|
139
|
+
* confirms nothing (see obtapi's Account\PassengersController). Deciding
|
|
140
|
+
* that here, from a fact the app already knows, means the request is never
|
|
141
|
+
* made for them and there is no failed query on a checkout screen to
|
|
142
|
+
* accidentally surface.
|
|
143
|
+
*
|
|
144
|
+
* The price ids go with it so every row comes back saying what THIS
|
|
145
|
+
* journey would still have to ask for - see the chooser.
|
|
146
|
+
*/
|
|
147
|
+
const isCustomer = UserData?.type === 'visitor';
|
|
148
|
+
|
|
149
|
+
const { data: SavedData } = useGetSavedPassengers(
|
|
150
|
+
step2.price.id as number,
|
|
151
|
+
step3?.price.id as (number | undefined),
|
|
152
|
+
isCustomer
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
// undefined until the list has landed, which is also what a guest gets and
|
|
156
|
+
// is exactly the right answer for both: nothing about the feature renders
|
|
157
|
+
// until there is an account behind it with an answer
|
|
158
|
+
const saved = isCustomer ? (SavedData?.data ?? []) : undefined;
|
|
159
|
+
|
|
128
160
|
const addonItems = [
|
|
129
161
|
addonWhatsapp && SettingsData?.addons?.whatsapp.available ? {
|
|
130
162
|
label: t('routes_order.step5.addons.whatsapp.label'),
|
|
@@ -280,6 +312,8 @@ const Checkout = ({ values, step1, step2, step3, isPartner, t, onStore, onBack }
|
|
|
280
312
|
fields={ fields }
|
|
281
313
|
busFrom={ busFrom }
|
|
282
314
|
busTo={ busTo }
|
|
315
|
+
saved={ saved }
|
|
316
|
+
savedMax={ SavedData?.max }
|
|
283
317
|
t={ t }
|
|
284
318
|
errors={ passenger.formState.errors }
|
|
285
319
|
refs={ passenger.register }
|
|
@@ -1,10 +1,13 @@
|
|
|
1
|
-
import { useId } from 'react';
|
|
1
|
+
import { useId, useState } from 'react';
|
|
2
2
|
import { TFunction } from 'i18next';
|
|
3
3
|
import { FieldErrors, FieldValues, UseFormRegister } from 'react-hook-form';
|
|
4
4
|
import { Calendar, Gender, ChooseSeat, Required } from '@autobusal/common';
|
|
5
5
|
import { Validate, Display } from '@autobusal/utilities';
|
|
6
|
+
import Choose from '../Saved/Choose';
|
|
7
|
+
import Remember from '../Saved/Remember';
|
|
8
|
+
import { useGetSavedPassenger } from '../../services';
|
|
6
9
|
import { Container, Title, Notice } from './styles';
|
|
7
|
-
import { PersonData } from '@autobusal/providers/types/persons';
|
|
10
|
+
import { PersonData, SavedPassenger, SavedPassengerRecord } from '@autobusal/providers/types/persons';
|
|
8
11
|
import { OccupiedData } from '@autobusal/providers/types/buses';
|
|
9
12
|
|
|
10
13
|
interface Props {
|
|
@@ -17,6 +20,17 @@ interface Props {
|
|
|
17
20
|
busTo: OccupiedData
|
|
18
21
|
pickedFrom: number[]
|
|
19
22
|
pickedTo: number[]
|
|
23
|
+
/**
|
|
24
|
+
* Edited: Claude - Date: 2026-08-20
|
|
25
|
+
* The travellers this account has saved, or undefined when there is no
|
|
26
|
+
* account to have saved any - a guest, or a member of staff. UNDEFINED IS
|
|
27
|
+
* NOT AN EMPTY LIST: an empty list is a customer who has saved nobody yet
|
|
28
|
+
* and may tick "remember"; undefined is somebody the whole feature does
|
|
29
|
+
* not apply to, and nothing about it renders.
|
|
30
|
+
*/
|
|
31
|
+
saved?: SavedPassenger[]
|
|
32
|
+
/** how many travellers one account may keep (obtapi's own cap) */
|
|
33
|
+
savedMax?: number
|
|
20
34
|
t: TFunction<'public'>
|
|
21
35
|
errors: FieldErrors<FieldValues>
|
|
22
36
|
refs: UseFormRegister<PersonData>
|
|
@@ -30,7 +44,7 @@ interface Props {
|
|
|
30
44
|
// telegram render ONLY when the route requires them - and are then
|
|
31
45
|
// required. Name and date of birth stay mandatory always. `fields` is the
|
|
32
46
|
// union of both legs on a return trip (see Step4).
|
|
33
|
-
const Passenger = ({ type, number, values, fields, withReturn, busFrom, busTo, pickedFrom, pickedTo, t, errors, refs, onSeatSelect, onUpdate }: Props): JSX.Element => {
|
|
47
|
+
const Passenger = ({ type, number, values, fields, withReturn, busFrom, busTo, pickedFrom, pickedTo, saved, savedMax, t, errors, refs, onSeatSelect, onUpdate }: Props): JSX.Element => {
|
|
34
48
|
const name = `${ type }${ number }`;
|
|
35
49
|
|
|
36
50
|
const names = {
|
|
@@ -43,7 +57,12 @@ const Passenger = ({ type, number, values, fields, withReturn, busFrom, busTo, p
|
|
|
43
57
|
whatsapp: `${ name }_wa`,
|
|
44
58
|
telegram: `${ name }_tg`,
|
|
45
59
|
seatDeparture: `${ name }_sd`,
|
|
46
|
-
seatReturn: `${ name }_sr
|
|
60
|
+
seatReturn: `${ name }_sr`,
|
|
61
|
+
|
|
62
|
+
// Edited: Claude - Date: 2026-08-20
|
|
63
|
+
// "Remember this passenger" - obtapi reads `{type}{i}_save` alongside
|
|
64
|
+
// the `_fn` / `_pass` fields this form already posts.
|
|
65
|
+
save: `${ name }_save`
|
|
47
66
|
};
|
|
48
67
|
|
|
49
68
|
const has = (field: string): boolean => fields.includes(field);
|
|
@@ -62,20 +81,161 @@ const Passenger = ({ type, number, values, fields, withReturn, busFrom, busTo, p
|
|
|
62
81
|
|
|
63
82
|
const id = (field: string): string => `${ uid }${ field }`;
|
|
64
83
|
|
|
84
|
+
/**
|
|
85
|
+
* "Book somebody you have booked before" - the saved-passenger chooser.
|
|
86
|
+
*
|
|
87
|
+
* Edited: Claude - Date: 2026-08-20
|
|
88
|
+
*
|
|
89
|
+
* `picked` is the row currently filling these fields; `applied` counts how
|
|
90
|
+
* many times a choice has been applied, and exists only to key the two
|
|
91
|
+
* controls below that cannot be told a new value.
|
|
92
|
+
*
|
|
93
|
+
* WHY A KEY AT ALL. Every field here is uncontrolled - a defaultValue plus
|
|
94
|
+
* a react-hook-form registration - so nothing re-reads its default on a
|
|
95
|
+
* re-render, which is exactly right while somebody is typing and exactly
|
|
96
|
+
* wrong the moment they pick a different traveller. Remounting on a new
|
|
97
|
+
* key is what makes the visible form agree with the form's data, and it is
|
|
98
|
+
* done per field rather than by remounting the passenger, because the seat
|
|
99
|
+
* pickers hold state of their own that a chosen name has nothing to do
|
|
100
|
+
* with.
|
|
101
|
+
*/
|
|
102
|
+
const [ picked, setPicked ] = useState<number>(0);
|
|
103
|
+
const [ applied, setApplied ] = useState<number>(0);
|
|
104
|
+
const [ record, setRecord ] = useState<SavedPassengerRecord | undefined>(undefined);
|
|
105
|
+
|
|
106
|
+
const { mutate: Fetch, isPending: isFetching } = useGetSavedPassenger();
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* What a field starts as: the chosen traveller's value once somebody has
|
|
110
|
+
* chosen one, otherwise whatever was typed before leaving this screen.
|
|
111
|
+
*/
|
|
112
|
+
const start = (field: string, chosen?: (string | number | null)): string => {
|
|
113
|
+
if (record !== undefined) {
|
|
114
|
+
return chosen === null || chosen === undefined ? '' : String(chosen);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return values !== undefined ? String(values[field] ?? '') : '';
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Apply a chosen traveller to this passenger's fields.
|
|
122
|
+
*
|
|
123
|
+
* THE WHOLE PERSON, NOT THE PARTS THAT HAPPEN TO BE FILLED. The customer
|
|
124
|
+
* has said who is sitting in this seat, so a passport left over from the
|
|
125
|
+
* previous choice is not theirs and is cleared rather than kept - the
|
|
126
|
+
* chooser's notice says what is now missing, which is a far better answer
|
|
127
|
+
* than a form quietly carrying one traveller's document under another
|
|
128
|
+
* traveller's name.
|
|
129
|
+
*
|
|
130
|
+
* The values are written BEFORE the remount so react-hook-form and the
|
|
131
|
+
* freshly mounted inputs start from the same answer, whichever of the two
|
|
132
|
+
* the library chooses to trust when a registered field reappears.
|
|
133
|
+
*/
|
|
134
|
+
const apply = (chosen: SavedPassengerRecord): void => {
|
|
135
|
+
onUpdate(names.firstName, chosen.first_name);
|
|
136
|
+
onUpdate(names.lastName, chosen.last_name);
|
|
137
|
+
onUpdate(names.dob, chosen.dob);
|
|
138
|
+
|
|
139
|
+
// ONLY THE FIELDS THIS ROUTE COLLECTS. react-hook-form keeps a value set
|
|
140
|
+
// for a name it never registered and submits it with the rest, so writing
|
|
141
|
+
// all five here would post a WhatsApp number on a route that never asked
|
|
142
|
+
// for one - and obtapi's capture-at-checkout merges whatever it is sent.
|
|
143
|
+
// The form should post exactly what it shows.
|
|
144
|
+
if (has('sex')) {
|
|
145
|
+
// 0 is a perfectly good answer here, so this tests for null rather than
|
|
146
|
+
// for falsiness. A traveller with no stored sex leaves the control at
|
|
147
|
+
// its own default and is reported by `missing` instead.
|
|
148
|
+
onUpdate(names.sex, chosen.sex === null ? 0 : chosen.sex);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (has('passport')) {
|
|
152
|
+
onUpdate(names.passport, chosen.passport ?? '');
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (has('phone')) {
|
|
156
|
+
onUpdate(names.phone, chosen.phone ?? '');
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (has('whatsapp')) {
|
|
160
|
+
onUpdate(names.whatsapp, chosen.whatsapp ?? '');
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (has('telegram')) {
|
|
164
|
+
onUpdate(names.telegram, chosen.telegram ?? '');
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
setRecord(chosen);
|
|
168
|
+
setApplied(applied + 1);
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* A pick is a fetch, because the list does not carry passport numbers -
|
|
173
|
+
* obtapi masks them there and returns the real one for one traveller at a
|
|
174
|
+
* time, at this exact moment. Choosing the blank option clears back to an
|
|
175
|
+
* empty form rather than leaving the last traveller's details behind under
|
|
176
|
+
* a chooser that no longer names them.
|
|
177
|
+
*/
|
|
178
|
+
const onPick = (id: number): void => {
|
|
179
|
+
setPicked(id);
|
|
180
|
+
|
|
181
|
+
if (id === 0) {
|
|
182
|
+
onUpdate(names.firstName, '');
|
|
183
|
+
onUpdate(names.lastName, '');
|
|
184
|
+
onUpdate(names.dob, '');
|
|
185
|
+
|
|
186
|
+
['passport', 'phone', 'whatsapp', 'telegram'].forEach(field => {
|
|
187
|
+
if (has(field)) {
|
|
188
|
+
onUpdate(names[field as keyof typeof names], '');
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
setRecord(undefined);
|
|
193
|
+
setApplied(applied + 1);
|
|
194
|
+
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
Fetch(id, {
|
|
199
|
+
onSuccess: (data) => apply(data)
|
|
200
|
+
});
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
// An account at its cap can still MERGE into a traveller it already has -
|
|
204
|
+
// obtapi only refuses new people - so the box is only closed off when
|
|
205
|
+
// nobody has been chosen to merge into.
|
|
206
|
+
const full = saved !== undefined && savedMax !== undefined && savedMax > 0 && saved.length >= savedMax && picked === 0;
|
|
207
|
+
|
|
65
208
|
return (
|
|
66
209
|
<Container className="box">
|
|
67
210
|
<Title>
|
|
68
211
|
{ `${ t('routes_order.step4.ticket', { no: number }) } (${ t(`data.persons.${ type }.singular`) })` }
|
|
69
212
|
</Title>
|
|
70
213
|
|
|
214
|
+
{ /* Edited: Claude - Date: 2026-08-20
|
|
215
|
+
Only for a signed-in customer who has actually saved somebody -
|
|
216
|
+
`saved` is undefined for a guest or for staff, and an empty list
|
|
217
|
+
is an account with nobody on it yet. Either way nothing renders,
|
|
218
|
+
so the form a first-time buyer sees is the form they saw before. */ }
|
|
219
|
+
{ (saved !== undefined && saved.length > 0) && (
|
|
220
|
+
<Choose
|
|
221
|
+
passengers={ saved }
|
|
222
|
+
picked={ picked }
|
|
223
|
+
id={ id('saved_choose') }
|
|
224
|
+
pending={ isFetching }
|
|
225
|
+
t={ t }
|
|
226
|
+
onPick={ onPick }
|
|
227
|
+
/>
|
|
228
|
+
) }
|
|
229
|
+
|
|
71
230
|
<div className="column">
|
|
72
231
|
<div className="row">
|
|
73
232
|
<Required htmlFor={ id(names.firstName) }>{ t('routes_order.step4.first_name') }</Required>
|
|
74
233
|
|
|
75
234
|
<input
|
|
76
235
|
type="text"
|
|
236
|
+
key={ `${ names.firstName }-${ applied }` }
|
|
77
237
|
id={ id(names.firstName) }
|
|
78
|
-
defaultValue={
|
|
238
|
+
defaultValue={ start(names.firstName, record?.first_name) }
|
|
79
239
|
{ ...refs(names.firstName, Validate('required|min_length:2|max_length:100', t)) }
|
|
80
240
|
/>
|
|
81
241
|
|
|
@@ -87,8 +247,9 @@ const Passenger = ({ type, number, values, fields, withReturn, busFrom, busTo, p
|
|
|
87
247
|
|
|
88
248
|
<input
|
|
89
249
|
type="text"
|
|
250
|
+
key={ `${ names.lastName }-${ applied }` }
|
|
90
251
|
id={ id(names.lastName) }
|
|
91
|
-
defaultValue={
|
|
252
|
+
defaultValue={ start(names.lastName, record?.last_name) }
|
|
92
253
|
{ ...refs(names.lastName, Validate('required|min_length:2|max_length:100', t)) }
|
|
93
254
|
/>
|
|
94
255
|
|
|
@@ -101,10 +262,11 @@ const Passenger = ({ type, number, values, fields, withReturn, busFrom, busTo, p
|
|
|
101
262
|
<Required htmlFor={ id(names.dob) }>{ t('routes_order.step4.dob') }</Required>
|
|
102
263
|
|
|
103
264
|
<Calendar
|
|
265
|
+
key={ `${ names.dob }-${ applied }` }
|
|
104
266
|
type="dob"
|
|
105
267
|
name={ names.dob }
|
|
106
268
|
id={ id(names.dob) }
|
|
107
|
-
defaultValue={ values !== undefined ? String(values[names.dob]) : undefined }
|
|
269
|
+
defaultValue={ record !== undefined ? record.dob : (values !== undefined ? String(values[names.dob]) : undefined) }
|
|
108
270
|
t={ t }
|
|
109
271
|
validation="required"
|
|
110
272
|
refs={ refs }
|
|
@@ -119,8 +281,9 @@ const Passenger = ({ type, number, values, fields, withReturn, busFrom, busTo, p
|
|
|
119
281
|
{ t('routes_order.step4.gender') }
|
|
120
282
|
|
|
121
283
|
<Gender
|
|
284
|
+
key={ `${ names.sex }-${ applied }` }
|
|
122
285
|
name={ names.sex }
|
|
123
|
-
defaultValue={ values !== undefined ? Number(values[names.sex]) : undefined }
|
|
286
|
+
defaultValue={ record !== undefined ? (record.sex ?? undefined) : (values !== undefined ? Number(values[names.sex]) : undefined) }
|
|
124
287
|
t={ t }
|
|
125
288
|
refs={ refs }
|
|
126
289
|
/>
|
|
@@ -138,8 +301,9 @@ const Passenger = ({ type, number, values, fields, withReturn, busFrom, busTo, p
|
|
|
138
301
|
|
|
139
302
|
<input
|
|
140
303
|
type="text"
|
|
304
|
+
key={ `${ names.passport }-${ applied }` }
|
|
141
305
|
id={ id(names.passport) }
|
|
142
|
-
defaultValue={
|
|
306
|
+
defaultValue={ start(names.passport, record?.passport) }
|
|
143
307
|
{ ...refs(names.passport, Validate('required|min_length:2|max_length:50', t)) }
|
|
144
308
|
/>
|
|
145
309
|
|
|
@@ -153,8 +317,9 @@ const Passenger = ({ type, number, values, fields, withReturn, busFrom, busTo, p
|
|
|
153
317
|
|
|
154
318
|
<input
|
|
155
319
|
type="text"
|
|
320
|
+
key={ `${ names.phone }-${ applied }` }
|
|
156
321
|
id={ id(names.phone) }
|
|
157
|
-
defaultValue={
|
|
322
|
+
defaultValue={ start(names.phone, record?.phone) }
|
|
158
323
|
{ ...refs(names.phone, Validate('required|min_length:5|max_length:50', t)) }
|
|
159
324
|
/>
|
|
160
325
|
|
|
@@ -180,8 +345,9 @@ const Passenger = ({ type, number, values, fields, withReturn, busFrom, busTo, p
|
|
|
180
345
|
|
|
181
346
|
<input
|
|
182
347
|
type="text"
|
|
348
|
+
key={ `${ names.whatsapp }-${ applied }` }
|
|
183
349
|
id={ id(names.whatsapp) }
|
|
184
|
-
defaultValue={
|
|
350
|
+
defaultValue={ start(names.whatsapp, record?.whatsapp) }
|
|
185
351
|
{ ...refs(names.whatsapp, Validate('required|min_length:5|max_length:50', t)) }
|
|
186
352
|
/>
|
|
187
353
|
|
|
@@ -195,8 +361,9 @@ const Passenger = ({ type, number, values, fields, withReturn, busFrom, busTo, p
|
|
|
195
361
|
|
|
196
362
|
<input
|
|
197
363
|
type="text"
|
|
364
|
+
key={ `${ names.telegram }-${ applied }` }
|
|
198
365
|
id={ id(names.telegram) }
|
|
199
|
-
defaultValue={
|
|
366
|
+
defaultValue={ start(names.telegram, record?.telegram) }
|
|
200
367
|
{ ...refs(names.telegram, Validate('required|min_length:2|max_length:100', t)) }
|
|
201
368
|
/>
|
|
202
369
|
|
|
@@ -247,6 +414,23 @@ const Passenger = ({ type, number, values, fields, withReturn, busFrom, busTo, p
|
|
|
247
414
|
) }
|
|
248
415
|
</div>
|
|
249
416
|
) }
|
|
417
|
+
|
|
418
|
+
{ /* Edited: Claude - Date: 2026-08-20
|
|
419
|
+
At the foot of the passenger it applies to, so the question and
|
|
420
|
+
the passport number it would store are read in one glance. Only
|
|
421
|
+
for a signed-in customer: a guest has no account to save into, and
|
|
422
|
+
obtapi's Libraries\Passengers\Capture skips staff accounts for a
|
|
423
|
+
different reason - selling to a walk-in traveller must not
|
|
424
|
+
accumulate that person's documents under the agency. */ }
|
|
425
|
+
{ saved !== undefined && (
|
|
426
|
+
<Remember
|
|
427
|
+
name={ names.save }
|
|
428
|
+
id={ id(names.save) }
|
|
429
|
+
full={ full }
|
|
430
|
+
t={ t }
|
|
431
|
+
refs={ refs }
|
|
432
|
+
/>
|
|
433
|
+
) }
|
|
250
434
|
</Container>
|
|
251
435
|
);
|
|
252
436
|
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { TFunction } from 'i18next';
|
|
2
2
|
import { FieldErrors, FieldValues, UseFormRegister } from 'react-hook-form';
|
|
3
3
|
import Passenger from '../Passenger/Passenger';
|
|
4
|
-
import { PersonData } from '@autobusal/providers/types/persons';
|
|
4
|
+
import { PersonData, SavedPassenger } from '@autobusal/providers/types/persons';
|
|
5
5
|
import { OccupiedData } from '@autobusal/providers/types/buses';
|
|
6
6
|
|
|
7
7
|
interface Props {
|
|
@@ -14,6 +14,8 @@ interface Props {
|
|
|
14
14
|
busTo: OccupiedData
|
|
15
15
|
pickedFrom: number[]
|
|
16
16
|
pickedTo: number[]
|
|
17
|
+
saved?: SavedPassenger[]
|
|
18
|
+
savedMax?: number
|
|
17
19
|
t: TFunction<'public'>
|
|
18
20
|
errors: FieldErrors<FieldValues>
|
|
19
21
|
refs: UseFormRegister<PersonData>
|
|
@@ -21,7 +23,7 @@ interface Props {
|
|
|
21
23
|
onUpdate: (name: string, value: (string | number)) => void
|
|
22
24
|
}
|
|
23
25
|
|
|
24
|
-
const Display = ({ type, amount, fields, withReturn, values, busFrom, busTo, pickedFrom, pickedTo, t, errors, refs, onSeatSelect, onUpdate }: Props): JSX.Element[] => {
|
|
26
|
+
const Display = ({ type, amount, fields, withReturn, values, busFrom, busTo, pickedFrom, pickedTo, saved, savedMax, t, errors, refs, onSeatSelect, onUpdate }: Props): JSX.Element[] => {
|
|
25
27
|
const passengers: JSX.Element[] = [];
|
|
26
28
|
|
|
27
29
|
for (let i = 1; i <= amount; i++) {
|
|
@@ -37,6 +39,8 @@ const Display = ({ type, amount, fields, withReturn, values, busFrom, busTo, pic
|
|
|
37
39
|
busTo={ busTo }
|
|
38
40
|
pickedFrom={ pickedFrom }
|
|
39
41
|
pickedTo={ pickedTo }
|
|
42
|
+
saved={ saved }
|
|
43
|
+
savedMax={ savedMax }
|
|
40
44
|
t={ t }
|
|
41
45
|
errors={ errors }
|
|
42
46
|
refs={ refs }
|
|
@@ -4,7 +4,7 @@ import { FieldErrors, FieldValues, UseFormRegister } from 'react-hook-form';
|
|
|
4
4
|
import Display from './Display';
|
|
5
5
|
import { Container } from './styles';
|
|
6
6
|
import { RoutesSearchForm } from '@autobusal/routes-search/types';
|
|
7
|
-
import { PersonData } from '@autobusal/providers/types/persons';
|
|
7
|
+
import { PersonData, SavedPassenger } from '@autobusal/providers/types/persons';
|
|
8
8
|
import { OccupiedData } from '@autobusal/providers/types/buses';
|
|
9
9
|
|
|
10
10
|
interface Props {
|
|
@@ -13,13 +13,21 @@ interface Props {
|
|
|
13
13
|
fields: string[]
|
|
14
14
|
busFrom: OccupiedData
|
|
15
15
|
busTo: OccupiedData
|
|
16
|
+
/**
|
|
17
|
+
* Edited: Claude - Date: 2026-08-20
|
|
18
|
+
* This account's saved travellers, drilled to every passenger block so the
|
|
19
|
+
* list is fetched once for the checkout rather than once per seat.
|
|
20
|
+
* Undefined for a guest or for staff - see Passenger.tsx.
|
|
21
|
+
*/
|
|
22
|
+
saved?: SavedPassenger[]
|
|
23
|
+
savedMax?: number
|
|
16
24
|
t: TFunction<'public'>
|
|
17
25
|
errors: FieldErrors<FieldValues>
|
|
18
26
|
refs: UseFormRegister<PersonData>
|
|
19
27
|
onUpdate: (name: string, value: (string | number)) => void
|
|
20
28
|
}
|
|
21
29
|
|
|
22
|
-
const Passengers = ({ values, step1, fields, busFrom, busTo, t, errors, refs, onUpdate }: Props): JSX.Element => {
|
|
30
|
+
const Passengers = ({ values, step1, fields, busFrom, busTo, saved, savedMax, t, errors, refs, onUpdate }: Props): JSX.Element => {
|
|
23
31
|
const [ pickedFrom, setPickedFrom ] = useState<number[]>([]);
|
|
24
32
|
const [ pickedTo, setPickedTo ] = useState<number[]>([]);
|
|
25
33
|
|
|
@@ -53,6 +61,8 @@ const Passengers = ({ values, step1, fields, busFrom, busTo, t, errors, refs, on
|
|
|
53
61
|
busTo={ busTo }
|
|
54
62
|
pickedFrom={ pickedFrom }
|
|
55
63
|
pickedTo={ pickedTo }
|
|
64
|
+
saved={ saved }
|
|
65
|
+
savedMax={ savedMax }
|
|
56
66
|
t={ t }
|
|
57
67
|
errors={ errors }
|
|
58
68
|
refs={ refs }
|
|
@@ -70,6 +80,8 @@ const Passengers = ({ values, step1, fields, busFrom, busTo, t, errors, refs, on
|
|
|
70
80
|
busTo={ busTo }
|
|
71
81
|
pickedFrom={ pickedFrom }
|
|
72
82
|
pickedTo={ pickedTo }
|
|
83
|
+
saved={ saved }
|
|
84
|
+
savedMax={ savedMax }
|
|
73
85
|
t={ t }
|
|
74
86
|
errors={ errors }
|
|
75
87
|
refs={ refs }
|
|
@@ -87,6 +99,8 @@ const Passengers = ({ values, step1, fields, busFrom, busTo, t, errors, refs, on
|
|
|
87
99
|
busTo={ busTo }
|
|
88
100
|
pickedFrom={ pickedFrom }
|
|
89
101
|
pickedTo={ pickedTo }
|
|
102
|
+
saved={ saved }
|
|
103
|
+
savedMax={ savedMax }
|
|
90
104
|
t={ t }
|
|
91
105
|
errors={ errors }
|
|
92
106
|
refs={ refs }
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { TFunction } from 'i18next';
|
|
2
|
+
import { Container, Label, Missing, Hint } from './styles';
|
|
3
|
+
import { SavedPassenger } from '@autobusal/providers/types/persons';
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
/** this account's saved travellers, already measured against this journey */
|
|
7
|
+
passengers: SavedPassenger[]
|
|
8
|
+
/** the row currently filling the fields below, 0 for "typing from scratch" */
|
|
9
|
+
picked: number
|
|
10
|
+
/** the id to give the select, so the visible label actually points at it */
|
|
11
|
+
id: string
|
|
12
|
+
pending: boolean
|
|
13
|
+
t: TFunction<'common'>
|
|
14
|
+
onPick: (id: number) => void
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The option keys obtapi reports in `missing`, in the order the form asks
|
|
19
|
+
* for them, paired with the label the form already uses for each. One list,
|
|
20
|
+
* so the notice cannot name a field by a word that appears nowhere on the
|
|
21
|
+
* screen it is pointing at.
|
|
22
|
+
*/
|
|
23
|
+
const LABELS: Record<string, string> = {
|
|
24
|
+
sex: 'routes_order.step4.gender',
|
|
25
|
+
passport: 'routes_order.step4.passport',
|
|
26
|
+
phone: 'routes_order.step4.phone',
|
|
27
|
+
whatsapp: 'routes_order.step4.whatsapp',
|
|
28
|
+
telegram: 'routes_order.step4.telegram'
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* "Book somebody you have booked before."
|
|
33
|
+
*
|
|
34
|
+
* Edited: Claude - Date: 2026-08-20
|
|
35
|
+
*
|
|
36
|
+
* WHAT IT IS FOR. The same families ride the same corridors several times a
|
|
37
|
+
* year, and every trip has been another round of typing the same passport
|
|
38
|
+
* number. This turns that into one choice.
|
|
39
|
+
*
|
|
40
|
+
* WHO SEES IT. Only a signed-in customer with at least one saved traveller.
|
|
41
|
+
* The list is not fetched at all for a guest or for staff (see
|
|
42
|
+
* useGetSavedPassengers), so this component is simply never rendered for
|
|
43
|
+
* them - the endpoint's 404 for those accounts never reaches a screen. An
|
|
44
|
+
* account that has saved nobody yet also sees nothing: an empty chooser
|
|
45
|
+
* above an empty form is one more thing to read and nothing to use.
|
|
46
|
+
*
|
|
47
|
+
* A NAMED PERSON IS NOT A COMPLETE ONE, and that is what `missing` is for.
|
|
48
|
+
* Routes collect different fields, so somebody saved on a route that only
|
|
49
|
+
* wanted a name has no passport and must still be offerable on a route that
|
|
50
|
+
* wants one. Rather than quietly filling four of five fields and letting
|
|
51
|
+
* validation explain the fifth after they press pay, the gap is named here,
|
|
52
|
+
* in the fields' own words, at the moment they pick.
|
|
53
|
+
*
|
|
54
|
+
* `missing` may be ABSENT rather than empty - obtapi omits it when no
|
|
55
|
+
* journey was named - so it is tested as an array. An undefined `missing`
|
|
56
|
+
* means "not measured", never "nothing needed", and this says nothing at
|
|
57
|
+
* all in that case rather than promising a completeness it has not checked.
|
|
58
|
+
*/
|
|
59
|
+
const Choose = ({ passengers, picked, id, pending, t, onPick }: Props): JSX.Element => {
|
|
60
|
+
const chosen = passengers.find(item => item.id === picked);
|
|
61
|
+
|
|
62
|
+
const missing = Array.isArray(chosen?.missing) ? chosen.missing : [];
|
|
63
|
+
|
|
64
|
+
const named = missing
|
|
65
|
+
.filter(field => LABELS[field] !== undefined)
|
|
66
|
+
.map(field => t(LABELS[field], { ns: 'common' }));
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<Container>
|
|
70
|
+
<Label htmlFor={ id }>{ t('routes_order.step4.saved.choose', { ns: 'common' }) }</Label>
|
|
71
|
+
|
|
72
|
+
<select
|
|
73
|
+
id={ id }
|
|
74
|
+
value={ picked }
|
|
75
|
+
disabled={ pending }
|
|
76
|
+
onChange={ (event) => onPick(Number(event.target.value)) }
|
|
77
|
+
>
|
|
78
|
+
<option value={ 0 }>{ t('routes_order.step4.saved.none', { ns: 'common' }) }</option>
|
|
79
|
+
|
|
80
|
+
{ passengers.map(item => (
|
|
81
|
+
<option key={ item.id } value={ item.id }>
|
|
82
|
+
{ /* The date of birth is what tells two relatives with the same
|
|
83
|
+
name apart, and it is the one thing on a saved row that is
|
|
84
|
+
never blank. The passport tail is NOT shown here: the list
|
|
85
|
+
is a chooser on a shared screen, and it is enough that the
|
|
86
|
+
person can be named. */ }
|
|
87
|
+
{ `${ item.first_name } ${ item.last_name } (${ item.dob })` }
|
|
88
|
+
</option>
|
|
89
|
+
)) }
|
|
90
|
+
</select>
|
|
91
|
+
|
|
92
|
+
{ named.length > 0
|
|
93
|
+
? <Missing>{ t('routes_order.step4.saved.missing', { fields: named.join(', '), ns: 'common' }) }</Missing>
|
|
94
|
+
: <Hint>{ t('routes_order.step4.saved.hint', { ns: 'common' }) }</Hint> }
|
|
95
|
+
</Container>
|
|
96
|
+
);
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
export default Choose;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { TFunction } from 'i18next';
|
|
2
|
+
import { UseFormRegister } from 'react-hook-form';
|
|
3
|
+
import { Remember as Container } from './styles';
|
|
4
|
+
import { PersonData } from '@autobusal/providers/types/persons';
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
/** the checkout field this posts as - adult1_save, child2_save, ... */
|
|
8
|
+
name: string
|
|
9
|
+
id: string
|
|
10
|
+
/** whether this account is already at its limit, and cannot store another */
|
|
11
|
+
full: boolean
|
|
12
|
+
t: TFunction<'common'>
|
|
13
|
+
refs: UseFormRegister<PersonData>
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* "Remember this passenger for next time."
|
|
18
|
+
*
|
|
19
|
+
* Edited: Claude - Date: 2026-08-20
|
|
20
|
+
*
|
|
21
|
+
* OFF BY DEFAULT, and there is no version of this that is not. What the box
|
|
22
|
+
* stores is a passport number and a date of birth, frequently belonging to
|
|
23
|
+
* somebody who is not the buyer - a family books together, and grandmother
|
|
24
|
+
* did not agree to anything. Nobody is opted into keeping an identity
|
|
25
|
+
* document on file because they did not notice a tick.
|
|
26
|
+
*
|
|
27
|
+
* PER PASSENGER, for the same reason. "Remember me but not the person I am
|
|
28
|
+
* buying a ticket for" is an ordinary thing to want, and one checkbox for
|
|
29
|
+
* the whole order cannot say it. That is why this renders inside the
|
|
30
|
+
* passenger it belongs to rather than beside the paid add-ons.
|
|
31
|
+
*
|
|
32
|
+
* NOT REGISTERED WITH A `value`. react-hook-form hands back the value
|
|
33
|
+
* attribute for a ticked checkbox and `false` for an unticked one; with no
|
|
34
|
+
* value attribute it is a plain true/false, which is exactly the
|
|
35
|
+
* `nullable|boolean` obtapi validates this field as (Orders\Make\Rules) and
|
|
36
|
+
* exactly the set Libraries\Passengers\Capture treats as a yes.
|
|
37
|
+
*
|
|
38
|
+
* Rendered only for a signed-in customer - the caller decides that, from the
|
|
39
|
+
* same fact that decides whether the chooser above appears.
|
|
40
|
+
*/
|
|
41
|
+
const Remember = ({ name, id, full, t, refs }: Props): JSX.Element => (
|
|
42
|
+
<Container>
|
|
43
|
+
<label htmlFor={ id }>
|
|
44
|
+
<input
|
|
45
|
+
type="checkbox"
|
|
46
|
+
id={ id }
|
|
47
|
+
disabled={ full }
|
|
48
|
+
{ ...refs(name) }
|
|
49
|
+
/>
|
|
50
|
+
|
|
51
|
+
{ full
|
|
52
|
+
? t('routes_order.step4.saved.full', { ns: 'common' })
|
|
53
|
+
: t('routes_order.step4.saved.remember', { ns: 'common' }) }
|
|
54
|
+
</label>
|
|
55
|
+
</Container>
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
export default Remember;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import styled from 'styled-components';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* "Book this person again" - the saved-passenger chooser.
|
|
5
|
+
*
|
|
6
|
+
* Edited: Claude - Date: 2026-08-20
|
|
7
|
+
*
|
|
8
|
+
* UNOBTRUSIVE ON PURPOSE. It sits above the fields it fills, and for a
|
|
9
|
+
* first-time buyer - which is most of them - it is not rendered at all. It
|
|
10
|
+
* is a shortcut past a form, not a step in one, so it is quieter than the
|
|
11
|
+
* form: no box of its own, no heading, and it never takes the visual weight
|
|
12
|
+
* of a required field away from the fields that are actually required.
|
|
13
|
+
*/
|
|
14
|
+
export const Container = styled.div`
|
|
15
|
+
display: flex;
|
|
16
|
+
flex-direction: column;
|
|
17
|
+
gap: 4px;
|
|
18
|
+
padding: 10px 12px;
|
|
19
|
+
margin-bottom: 16px;
|
|
20
|
+
border: 1px dashed ${ props => props.theme.background.neutral };
|
|
21
|
+
border-radius: 6px;
|
|
22
|
+
|
|
23
|
+
select {
|
|
24
|
+
max-width: 320px;
|
|
25
|
+
}
|
|
26
|
+
`;
|
|
27
|
+
|
|
28
|
+
export const Label = styled.label`
|
|
29
|
+
font-size: ${ props => props.theme.size.xs };
|
|
30
|
+
font-weight: 600;
|
|
31
|
+
`;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* What this journey still needs from a traveller we already know.
|
|
35
|
+
*
|
|
36
|
+
* Amber rather than red: nothing has gone wrong and nothing has been
|
|
37
|
+
* refused - the person was saved on a route that never asked for a passport
|
|
38
|
+
* and this one does. It is an instruction, not an error, so it does not
|
|
39
|
+
* borrow the styling of the validation messages sitting a few pixels below
|
|
40
|
+
* it.
|
|
41
|
+
*/
|
|
42
|
+
export const Missing = styled.small`
|
|
43
|
+
display: block;
|
|
44
|
+
margin-top: 2px;
|
|
45
|
+
font-size: ${ props => props.theme.size.xxs };
|
|
46
|
+
font-weight: 400;
|
|
47
|
+
line-height: 1.4;
|
|
48
|
+
color: ${ props => props.theme.font.warning };
|
|
49
|
+
`;
|
|
50
|
+
|
|
51
|
+
export const Hint = styled.small`
|
|
52
|
+
display: block;
|
|
53
|
+
font-size: ${ props => props.theme.size.xxs };
|
|
54
|
+
font-weight: 400;
|
|
55
|
+
line-height: 1.4;
|
|
56
|
+
color: ${ props => props.theme.font.faded };
|
|
57
|
+
`;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* "Remember this passenger for next time".
|
|
61
|
+
*
|
|
62
|
+
* Edited: Claude - Date: 2026-08-20
|
|
63
|
+
*
|
|
64
|
+
* At the FOOT of the passenger it applies to rather than beside the paid
|
|
65
|
+
* add-ons, because it is a question about this person and not about the
|
|
66
|
+
* order - and because a checkbox that stores somebody's passport number
|
|
67
|
+
* should be read in the same glance as the passport number it would store.
|
|
68
|
+
*/
|
|
69
|
+
export const Remember = styled.div`
|
|
70
|
+
margin-top: 12px;
|
|
71
|
+
padding-top: 12px;
|
|
72
|
+
border-top: 1px solid ${ props => props.theme.background.neutral };
|
|
73
|
+
|
|
74
|
+
label {
|
|
75
|
+
display: flex;
|
|
76
|
+
align-items: center;
|
|
77
|
+
gap: 8px;
|
|
78
|
+
font-size: ${ props => props.theme.size.xs };
|
|
79
|
+
font-weight: 400;
|
|
80
|
+
cursor: pointer;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
input {
|
|
84
|
+
width: auto;
|
|
85
|
+
margin: 0;
|
|
86
|
+
}
|
|
87
|
+
`;
|
package/package.json
CHANGED
package/services.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { apiClient } from '@autobusal/providers';
|
|
|
4
4
|
import { RoutesSearchForm } from '@autobusal/routes-search/types';
|
|
5
5
|
import { FoundData, FlexOffer } from '@autobusal/providers/types/routes';
|
|
6
6
|
import { CouponData } from '@autobusal/providers/types/orders';
|
|
7
|
-
import { BillingData, PersonData } from '@autobusal/providers/types/persons';
|
|
7
|
+
import { BillingData, PersonData, SavedPassengerRecord, SavedPassengersData } from '@autobusal/providers/types/persons';
|
|
8
8
|
import { FoundDay, ActionData, SaveData, OrderedData, CouponForm, AddonsSelection, AlternativesData, PriceAlertRequest, PriceAlertData } from './types';
|
|
9
9
|
|
|
10
10
|
export const useGetDepartures = (data: RoutesSearchForm): UseQueryResult<FoundData[]> => (
|
|
@@ -313,3 +313,77 @@ export const usePostPriceAlert = (): UseMutationResult<PriceAlertData, AxiosErro
|
|
|
313
313
|
retry: false
|
|
314
314
|
})
|
|
315
315
|
);
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* The travellers this account has saved, measured against THIS journey.
|
|
319
|
+
*
|
|
320
|
+
* Edited: Claude - Date: 2026-08-20
|
|
321
|
+
*
|
|
322
|
+
* The price ids are what make the answer journey-specific: obtapi puts a
|
|
323
|
+
* `missing` array on every row naming the fields these particular routes
|
|
324
|
+
* collect and this particular saved traveller has not got (see
|
|
325
|
+
* Account\PassengersController::browse). Without them the rows come back
|
|
326
|
+
* with no `missing` key at all, which is a different answer from an empty
|
|
327
|
+
* one - so they are always sent from here.
|
|
328
|
+
*
|
|
329
|
+
* `enabled` is the whole gate, and it is not an optimisation. The endpoint
|
|
330
|
+
* is `role:visitor`, so a guest or a member of staff gets a 404 - a correct
|
|
331
|
+
* refusal that confirms nothing, and one this UI must never turn into an
|
|
332
|
+
* error on somebody's checkout. Not asking is the only way to be sure of
|
|
333
|
+
* that; `retry: false` covers the case where the account changes underneath
|
|
334
|
+
* a query already in flight.
|
|
335
|
+
*/
|
|
336
|
+
export const useGetSavedPassengers = (
|
|
337
|
+
departurePriceId: (number | undefined),
|
|
338
|
+
returnPriceId: (number | undefined),
|
|
339
|
+
enabled: boolean
|
|
340
|
+
): UseQueryResult<SavedPassengersData> => (
|
|
341
|
+
useQuery({
|
|
342
|
+
queryKey: ['saved-passengers-checkout', { departurePriceId, returnPriceId }],
|
|
343
|
+
enabled: enabled && departurePriceId !== undefined,
|
|
344
|
+
retry: false,
|
|
345
|
+
queryFn: async () => (
|
|
346
|
+
await apiClient
|
|
347
|
+
.get('/api/account/passengers/browse', {
|
|
348
|
+
params: {
|
|
349
|
+
departure_price_id: departurePriceId,
|
|
350
|
+
return_price_id: returnPriceId
|
|
351
|
+
}
|
|
352
|
+
})
|
|
353
|
+
.then(response => (
|
|
354
|
+
response.data
|
|
355
|
+
))
|
|
356
|
+
)
|
|
357
|
+
})
|
|
358
|
+
);
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* One saved traveller, passport included.
|
|
362
|
+
*
|
|
363
|
+
* Edited: Claude - Date: 2026-08-20
|
|
364
|
+
*
|
|
365
|
+
* A mutation for a GET, deliberately. This is not data the screen renders -
|
|
366
|
+
* it is fetched at the moment somebody picks a name out of the chooser, and
|
|
367
|
+
* what comes back is written into a form rather than displayed. A query
|
|
368
|
+
* would need the chosen id in render state and an effect to notice it
|
|
369
|
+
* landing; `mutate(id, { onSuccess })` is the same fetch with the answer
|
|
370
|
+
* arriving where the decision was made.
|
|
371
|
+
*
|
|
372
|
+
* This is also the ONLY request in the app that returns a stored passport
|
|
373
|
+
* number, which is why it is one traveller at a time and only after a
|
|
374
|
+
* customer has said who is sitting in this seat.
|
|
375
|
+
*/
|
|
376
|
+
export const useGetSavedPassenger = (): UseMutationResult<SavedPassengerRecord, Error, number, unknown> => (
|
|
377
|
+
useMutation({
|
|
378
|
+
mutationKey: ['saved-passenger-record'],
|
|
379
|
+
mutationFn: async (id: number) => (
|
|
380
|
+
await apiClient
|
|
381
|
+
.get('/api/account/passengers/get', {
|
|
382
|
+
params: { id }
|
|
383
|
+
})
|
|
384
|
+
.then(response => (
|
|
385
|
+
response.data
|
|
386
|
+
))
|
|
387
|
+
)
|
|
388
|
+
})
|
|
389
|
+
);
|