@autobusal/routes-order 1.8.0 → 1.9.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 +731 -0
- package/Checkout/Checkout.tsx +519 -0
- package/Checkout/Sidebar/Sidebar.tsx +124 -0
- package/Checkout/Sidebar/styles.ts +52 -0
- package/Facts/Facts.tsx +271 -0
- package/Facts/questions.ts +250 -0
- package/Facts/service.ts +111 -0
- package/Facts/services.ts +19 -0
- package/Facts/styles.ts +112 -0
- package/Facts/types.ts +167 -0
- package/Faq/Faq.tsx +52 -0
- package/Faq/styles.ts +68 -0
- package/Found/Dates/Dates.tsx +95 -5
- package/Found/Dates/styles.ts +173 -9
- package/Found/Empty/Empty.tsx +136 -0
- package/Found/Empty/styles.ts +98 -0
- package/Found/Filters/Filters.tsx +194 -0
- package/Found/Filters/styles.ts +188 -0
- package/Found/Found.tsx +207 -17
- package/Found/Group/Group.tsx +68 -0
- package/Found/Group/styles.ts +44 -0
- package/Found/Header/styles.ts +20 -4
- package/Found/Orders/Orders.tsx +89 -27
- package/Found/Orders/styles.ts +14 -0
- package/Found/Picks/Picks.tsx +75 -0
- package/Found/Picks/styles.ts +106 -0
- package/Found/Route/Route.tsx +211 -33
- package/Found/Route/styles.ts +491 -46
- package/Found/SameDay/SameDay.tsx +35 -0
- package/Found/SameDay/styles.ts +27 -0
- package/Found/Sort/Sort.tsx +40 -0
- package/Found/Sort/styles.ts +87 -0
- package/Found/Summary/Summary.tsx +49 -0
- package/Found/Summary/styles.ts +42 -0
- package/Found/Watch/Watch.tsx +187 -0
- package/Found/Watch/styles.ts +118 -0
- package/Found/refine.ts +652 -0
- package/Links/Links.tsx +91 -0
- package/Links/styles.ts +75 -0
- package/RoutesOrder.tsx +239 -31
- package/Schedule/Schedule.tsx +123 -0
- package/Schedule/styles.ts +71 -0
- package/Sections/Schema.tsx +186 -0
- package/Sections/Sections.tsx +160 -0
- package/Sections/styles.ts +44 -0
- package/Step1/prepare.ts +21 -2
- package/Step2.tsx +34 -1
- package/Step4/Passenger/Passenger.tsx +253 -24
- package/Step4/Passenger/styles.ts +21 -1
- package/Step4/Passengers/Display.tsx +10 -2
- package/Step4/Passengers/Passengers.tsx +53 -4
- package/Step4/Saved/Choose.tsx +99 -0
- package/Step4/Saved/Remember.tsx +58 -0
- package/Step4/Saved/styles.ts +87 -0
- package/Step4/checkAge.ts +15 -4
- package/Step5/Addons/Addons.tsx +146 -4
- package/Step5/Addons/styles.ts +94 -0
- package/Step5/Billing/Billing.tsx +138 -70
- package/Step5/Coupons/Coupons.tsx +28 -18
- package/Step5/Coupons/styles.ts +41 -8
- package/Step5/Payments/Payments.tsx +36 -5
- package/Step5/Payments/styles.ts +16 -7
- package/Step5/Summary/About/About.tsx +99 -27
- package/Step5/Summary/About/styles.ts +118 -1
- package/Step5/Summary/styles.ts +12 -5
- package/Step5/utilities.ts +67 -12
- package/Steps/Steps.tsx +3 -1
- package/commerce.ts +24 -0
- package/package.json +1 -1
- package/services.ts +302 -13
- package/styles.ts +126 -0
- package/types.ts +84 -1
- package/Found/Booking/Booking.tsx +0 -61
- package/Found/Booking/styles.ts +0 -50
- package/Step4/Step4.tsx +0 -114
- package/Step5/Step5.tsx +0 -185
|
@@ -1,9 +1,13 @@
|
|
|
1
|
+
import { useId, useState } from 'react';
|
|
1
2
|
import { TFunction } from 'i18next';
|
|
2
3
|
import { FieldErrors, FieldValues, UseFormRegister } from 'react-hook-form';
|
|
3
|
-
import { Calendar, Gender, ChooseSeat } from '@autobusal/common';
|
|
4
|
+
import { Calendar, Gender, ChooseSeat, Required } from '@autobusal/common';
|
|
4
5
|
import { Validate, Display } from '@autobusal/utilities';
|
|
5
|
-
import
|
|
6
|
-
import
|
|
6
|
+
import Choose from '../Saved/Choose';
|
|
7
|
+
import Remember from '../Saved/Remember';
|
|
8
|
+
import { useGetSavedPassenger } from '../../services';
|
|
9
|
+
import { Container, Title, Notice } from './styles';
|
|
10
|
+
import { PersonData, SavedPassenger, SavedPassengerRecord } from '@autobusal/providers/types/persons';
|
|
7
11
|
import { OccupiedData } from '@autobusal/providers/types/buses';
|
|
8
12
|
|
|
9
13
|
interface Props {
|
|
@@ -16,6 +20,20 @@ interface Props {
|
|
|
16
20
|
busTo: OccupiedData
|
|
17
21
|
pickedFrom: number[]
|
|
18
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
|
|
34
|
+
/** seat-selection add-on: picking optional, `seatsFee` on the CTA */
|
|
35
|
+
seatsOptional?: boolean
|
|
36
|
+
seatsFee?: string
|
|
19
37
|
t: TFunction<'public'>
|
|
20
38
|
errors: FieldErrors<FieldValues>
|
|
21
39
|
refs: UseFormRegister<PersonData>
|
|
@@ -29,7 +47,7 @@ interface Props {
|
|
|
29
47
|
// telegram render ONLY when the route requires them - and are then
|
|
30
48
|
// required. Name and date of birth stay mandatory always. `fields` is the
|
|
31
49
|
// union of both legs on a return trip (see Step4).
|
|
32
|
-
const Passenger = ({ type, number, values, fields, withReturn, busFrom, busTo, pickedFrom, pickedTo, t, errors, refs, onSeatSelect, onUpdate }: Props): JSX.Element => {
|
|
50
|
+
const Passenger = ({ type, number, values, fields, withReturn, busFrom, busTo, pickedFrom, pickedTo, saved, savedMax, seatsOptional, seatsFee, t, errors, refs, onSeatSelect, onUpdate }: Props): JSX.Element => {
|
|
33
51
|
const name = `${ type }${ number }`;
|
|
34
52
|
|
|
35
53
|
const names = {
|
|
@@ -42,24 +60,185 @@ const Passenger = ({ type, number, values, fields, withReturn, busFrom, busTo, p
|
|
|
42
60
|
whatsapp: `${ name }_wa`,
|
|
43
61
|
telegram: `${ name }_tg`,
|
|
44
62
|
seatDeparture: `${ name }_sd`,
|
|
45
|
-
seatReturn: `${ name }_sr
|
|
63
|
+
seatReturn: `${ name }_sr`,
|
|
64
|
+
|
|
65
|
+
// Edited: Claude - Date: 2026-08-20
|
|
66
|
+
// "Remember this passenger" - obtapi reads `{type}{i}_save` alongside
|
|
67
|
+
// the `_fn` / `_pass` fields this form already posts.
|
|
68
|
+
save: `${ name }_save`
|
|
46
69
|
};
|
|
47
70
|
|
|
48
71
|
const has = (field: string): boolean => fields.includes(field);
|
|
49
72
|
|
|
73
|
+
/**
|
|
74
|
+
* Edited: Claude - Date: 2026-08-19
|
|
75
|
+
* Every field here was rendered as a bare <input> beside its label text,
|
|
76
|
+
* with no id and no association - so a screen reader announced the whole
|
|
77
|
+
* passenger block as a run of unnamed "edit text". The field NAMES are
|
|
78
|
+
* already unique per passenger (adult1_fn, adult2_fn), but they belong to
|
|
79
|
+
* the form payload; ids are page-global, so they get a useId prefix -
|
|
80
|
+
* this component renders once per passenger, and two forms on one page
|
|
81
|
+
* would otherwise emit duplicate ids, which is its own defect.
|
|
82
|
+
*/
|
|
83
|
+
const uid = useId();
|
|
84
|
+
|
|
85
|
+
const id = (field: string): string => `${ uid }${ field }`;
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* "Book somebody you have booked before" - the saved-passenger chooser.
|
|
89
|
+
*
|
|
90
|
+
* Edited: Claude - Date: 2026-08-20
|
|
91
|
+
*
|
|
92
|
+
* `picked` is the row currently filling these fields; `applied` counts how
|
|
93
|
+
* many times a choice has been applied, and exists only to key the two
|
|
94
|
+
* controls below that cannot be told a new value.
|
|
95
|
+
*
|
|
96
|
+
* WHY A KEY AT ALL. Every field here is uncontrolled - a defaultValue plus
|
|
97
|
+
* a react-hook-form registration - so nothing re-reads its default on a
|
|
98
|
+
* re-render, which is exactly right while somebody is typing and exactly
|
|
99
|
+
* wrong the moment they pick a different traveller. Remounting on a new
|
|
100
|
+
* key is what makes the visible form agree with the form's data, and it is
|
|
101
|
+
* done per field rather than by remounting the passenger, because the seat
|
|
102
|
+
* pickers hold state of their own that a chosen name has nothing to do
|
|
103
|
+
* with.
|
|
104
|
+
*/
|
|
105
|
+
const [ picked, setPicked ] = useState<number>(0);
|
|
106
|
+
const [ applied, setApplied ] = useState<number>(0);
|
|
107
|
+
const [ record, setRecord ] = useState<SavedPassengerRecord | undefined>(undefined);
|
|
108
|
+
|
|
109
|
+
const { mutate: Fetch, isPending: isFetching } = useGetSavedPassenger();
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* What a field starts as: the chosen traveller's value once somebody has
|
|
113
|
+
* chosen one, otherwise whatever was typed before leaving this screen.
|
|
114
|
+
*/
|
|
115
|
+
const start = (field: string, chosen?: (string | number | null)): string => {
|
|
116
|
+
if (record !== undefined) {
|
|
117
|
+
return chosen === null || chosen === undefined ? '' : String(chosen);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return values !== undefined ? String(values[field] ?? '') : '';
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Apply a chosen traveller to this passenger's fields.
|
|
125
|
+
*
|
|
126
|
+
* THE WHOLE PERSON, NOT THE PARTS THAT HAPPEN TO BE FILLED. The customer
|
|
127
|
+
* has said who is sitting in this seat, so a passport left over from the
|
|
128
|
+
* previous choice is not theirs and is cleared rather than kept - the
|
|
129
|
+
* chooser's notice says what is now missing, which is a far better answer
|
|
130
|
+
* than a form quietly carrying one traveller's document under another
|
|
131
|
+
* traveller's name.
|
|
132
|
+
*
|
|
133
|
+
* The values are written BEFORE the remount so react-hook-form and the
|
|
134
|
+
* freshly mounted inputs start from the same answer, whichever of the two
|
|
135
|
+
* the library chooses to trust when a registered field reappears.
|
|
136
|
+
*/
|
|
137
|
+
const apply = (chosen: SavedPassengerRecord): void => {
|
|
138
|
+
onUpdate(names.firstName, chosen.first_name);
|
|
139
|
+
onUpdate(names.lastName, chosen.last_name);
|
|
140
|
+
onUpdate(names.dob, chosen.dob);
|
|
141
|
+
|
|
142
|
+
// ONLY THE FIELDS THIS ROUTE COLLECTS. react-hook-form keeps a value set
|
|
143
|
+
// for a name it never registered and submits it with the rest, so writing
|
|
144
|
+
// all five here would post a WhatsApp number on a route that never asked
|
|
145
|
+
// for one - and obtapi's capture-at-checkout merges whatever it is sent.
|
|
146
|
+
// The form should post exactly what it shows.
|
|
147
|
+
if (has('sex')) {
|
|
148
|
+
// 0 is a perfectly good answer here, so this tests for null rather than
|
|
149
|
+
// for falsiness. A traveller with no stored sex leaves the control at
|
|
150
|
+
// its own default and is reported by `missing` instead.
|
|
151
|
+
onUpdate(names.sex, chosen.sex === null ? 0 : chosen.sex);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (has('passport')) {
|
|
155
|
+
onUpdate(names.passport, chosen.passport ?? '');
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (has('phone')) {
|
|
159
|
+
onUpdate(names.phone, chosen.phone ?? '');
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (has('whatsapp')) {
|
|
163
|
+
onUpdate(names.whatsapp, chosen.whatsapp ?? '');
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (has('telegram')) {
|
|
167
|
+
onUpdate(names.telegram, chosen.telegram ?? '');
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
setRecord(chosen);
|
|
171
|
+
setApplied(applied + 1);
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* A pick is a fetch, because the list does not carry passport numbers -
|
|
176
|
+
* obtapi masks them there and returns the real one for one traveller at a
|
|
177
|
+
* time, at this exact moment. Choosing the blank option clears back to an
|
|
178
|
+
* empty form rather than leaving the last traveller's details behind under
|
|
179
|
+
* a chooser that no longer names them.
|
|
180
|
+
*/
|
|
181
|
+
const onPick = (id: number): void => {
|
|
182
|
+
setPicked(id);
|
|
183
|
+
|
|
184
|
+
if (id === 0) {
|
|
185
|
+
onUpdate(names.firstName, '');
|
|
186
|
+
onUpdate(names.lastName, '');
|
|
187
|
+
onUpdate(names.dob, '');
|
|
188
|
+
|
|
189
|
+
['passport', 'phone', 'whatsapp', 'telegram'].forEach(field => {
|
|
190
|
+
if (has(field)) {
|
|
191
|
+
onUpdate(names[field as keyof typeof names], '');
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
setRecord(undefined);
|
|
196
|
+
setApplied(applied + 1);
|
|
197
|
+
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
Fetch(id, {
|
|
202
|
+
onSuccess: (data) => apply(data)
|
|
203
|
+
});
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
// An account at its cap can still MERGE into a traveller it already has -
|
|
207
|
+
// obtapi only refuses new people - so the box is only closed off when
|
|
208
|
+
// nobody has been chosen to merge into.
|
|
209
|
+
const full = saved !== undefined && savedMax !== undefined && savedMax > 0 && saved.length >= savedMax && picked === 0;
|
|
210
|
+
|
|
50
211
|
return (
|
|
51
212
|
<Container className="box">
|
|
52
213
|
<Title>
|
|
53
214
|
{ `${ t('routes_order.step4.ticket', { no: number }) } (${ t(`data.persons.${ type }.singular`) })` }
|
|
54
215
|
</Title>
|
|
55
216
|
|
|
217
|
+
{ /* Edited: Claude - Date: 2026-08-20
|
|
218
|
+
Only for a signed-in customer who has actually saved somebody -
|
|
219
|
+
`saved` is undefined for a guest or for staff, and an empty list
|
|
220
|
+
is an account with nobody on it yet. Either way nothing renders,
|
|
221
|
+
so the form a first-time buyer sees is the form they saw before. */ }
|
|
222
|
+
{ (saved !== undefined && saved.length > 0) && (
|
|
223
|
+
<Choose
|
|
224
|
+
passengers={ saved }
|
|
225
|
+
picked={ picked }
|
|
226
|
+
id={ id('saved_choose') }
|
|
227
|
+
pending={ isFetching }
|
|
228
|
+
t={ t }
|
|
229
|
+
onPick={ onPick }
|
|
230
|
+
/>
|
|
231
|
+
) }
|
|
232
|
+
|
|
56
233
|
<div className="column">
|
|
57
234
|
<div className="row">
|
|
58
|
-
{ t('routes_order.step4.first_name') }
|
|
235
|
+
<Required htmlFor={ id(names.firstName) }>{ t('routes_order.step4.first_name') }</Required>
|
|
59
236
|
|
|
60
237
|
<input
|
|
61
238
|
type="text"
|
|
62
|
-
|
|
239
|
+
key={ `${ names.firstName }-${ applied }` }
|
|
240
|
+
id={ id(names.firstName) }
|
|
241
|
+
defaultValue={ start(names.firstName, record?.first_name) }
|
|
63
242
|
{ ...refs(names.firstName, Validate('required|min_length:2|max_length:100', t)) }
|
|
64
243
|
/>
|
|
65
244
|
|
|
@@ -67,11 +246,13 @@ const Passenger = ({ type, number, values, fields, withReturn, busFrom, busTo, p
|
|
|
67
246
|
</div>
|
|
68
247
|
|
|
69
248
|
<div className="row">
|
|
70
|
-
{ t('routes_order.step4.last_name') }
|
|
249
|
+
<Required htmlFor={ id(names.lastName) }>{ t('routes_order.step4.last_name') }</Required>
|
|
71
250
|
|
|
72
251
|
<input
|
|
73
252
|
type="text"
|
|
74
|
-
|
|
253
|
+
key={ `${ names.lastName }-${ applied }` }
|
|
254
|
+
id={ id(names.lastName) }
|
|
255
|
+
defaultValue={ start(names.lastName, record?.last_name) }
|
|
75
256
|
{ ...refs(names.lastName, Validate('required|min_length:2|max_length:100', t)) }
|
|
76
257
|
/>
|
|
77
258
|
|
|
@@ -81,12 +262,14 @@ const Passenger = ({ type, number, values, fields, withReturn, busFrom, busTo, p
|
|
|
81
262
|
|
|
82
263
|
<div className="column">
|
|
83
264
|
<div className="row">
|
|
84
|
-
{ t('routes_order.step4.dob') }
|
|
265
|
+
<Required htmlFor={ id(names.dob) }>{ t('routes_order.step4.dob') }</Required>
|
|
85
266
|
|
|
86
267
|
<Calendar
|
|
268
|
+
key={ `${ names.dob }-${ applied }` }
|
|
87
269
|
type="dob"
|
|
88
270
|
name={ names.dob }
|
|
89
|
-
|
|
271
|
+
id={ id(names.dob) }
|
|
272
|
+
defaultValue={ record !== undefined ? record.dob : (values !== undefined ? String(values[names.dob]) : undefined) }
|
|
90
273
|
t={ t }
|
|
91
274
|
validation="required"
|
|
92
275
|
refs={ refs }
|
|
@@ -101,8 +284,9 @@ const Passenger = ({ type, number, values, fields, withReturn, busFrom, busTo, p
|
|
|
101
284
|
{ t('routes_order.step4.gender') }
|
|
102
285
|
|
|
103
286
|
<Gender
|
|
287
|
+
key={ `${ names.sex }-${ applied }` }
|
|
104
288
|
name={ names.sex }
|
|
105
|
-
defaultValue={ values !== undefined ? Number(values[names.sex]) : undefined }
|
|
289
|
+
defaultValue={ record !== undefined ? (record.sex ?? undefined) : (values !== undefined ? Number(values[names.sex]) : undefined) }
|
|
106
290
|
t={ t }
|
|
107
291
|
refs={ refs }
|
|
108
292
|
/>
|
|
@@ -116,11 +300,13 @@ const Passenger = ({ type, number, values, fields, withReturn, busFrom, busTo, p
|
|
|
116
300
|
<div className="column">
|
|
117
301
|
{ has('passport') && (
|
|
118
302
|
<div className="row">
|
|
119
|
-
{ t('routes_order.step4.passport') }
|
|
303
|
+
<Required htmlFor={ id(names.passport) }>{ t('routes_order.step4.passport') }</Required>
|
|
120
304
|
|
|
121
305
|
<input
|
|
122
306
|
type="text"
|
|
123
|
-
|
|
307
|
+
key={ `${ names.passport }-${ applied }` }
|
|
308
|
+
id={ id(names.passport) }
|
|
309
|
+
defaultValue={ start(names.passport, record?.passport) }
|
|
124
310
|
{ ...refs(names.passport, Validate('required|min_length:2|max_length:50', t)) }
|
|
125
311
|
/>
|
|
126
312
|
|
|
@@ -130,15 +316,25 @@ const Passenger = ({ type, number, values, fields, withReturn, busFrom, busTo, p
|
|
|
130
316
|
|
|
131
317
|
{ has('phone') && (
|
|
132
318
|
<div className="row">
|
|
133
|
-
{ t('routes_order.step4.phone') }
|
|
319
|
+
<Required htmlFor={ id(names.phone) }>{ t('routes_order.step4.phone') }</Required>
|
|
134
320
|
|
|
135
321
|
<input
|
|
136
322
|
type="text"
|
|
137
|
-
|
|
323
|
+
key={ `${ names.phone }-${ applied }` }
|
|
324
|
+
id={ id(names.phone) }
|
|
325
|
+
defaultValue={ start(names.phone, record?.phone) }
|
|
138
326
|
{ ...refs(names.phone, Validate('required|min_length:5|max_length:50', t)) }
|
|
139
327
|
/>
|
|
140
328
|
|
|
141
329
|
{ Display(errors[names.phone]) }
|
|
330
|
+
|
|
331
|
+
{ /* Edited: Ferjolt Ozuni - Date: 2026-08-05
|
|
332
|
+
Said because the use changed. This number was collected
|
|
333
|
+
for the operator to ring; it is now also where a service
|
|
334
|
+
SMS goes if the departure moves. Service messages about
|
|
335
|
+
this booking only - never marketing, which is what makes
|
|
336
|
+
the sentence true. */ }
|
|
337
|
+
<Notice>{ t('routes_order.step4.phone_notice') }</Notice>
|
|
142
338
|
</div>
|
|
143
339
|
) }
|
|
144
340
|
</div>
|
|
@@ -148,11 +344,13 @@ const Passenger = ({ type, number, values, fields, withReturn, busFrom, busTo, p
|
|
|
148
344
|
<div className="column">
|
|
149
345
|
{ has('whatsapp') && (
|
|
150
346
|
<div className="row">
|
|
151
|
-
{ t('routes_order.step4.whatsapp') }
|
|
347
|
+
<Required htmlFor={ id(names.whatsapp) }>{ t('routes_order.step4.whatsapp') }</Required>
|
|
152
348
|
|
|
153
349
|
<input
|
|
154
350
|
type="text"
|
|
155
|
-
|
|
351
|
+
key={ `${ names.whatsapp }-${ applied }` }
|
|
352
|
+
id={ id(names.whatsapp) }
|
|
353
|
+
defaultValue={ start(names.whatsapp, record?.whatsapp) }
|
|
156
354
|
{ ...refs(names.whatsapp, Validate('required|min_length:5|max_length:50', t)) }
|
|
157
355
|
/>
|
|
158
356
|
|
|
@@ -162,11 +360,13 @@ const Passenger = ({ type, number, values, fields, withReturn, busFrom, busTo, p
|
|
|
162
360
|
|
|
163
361
|
{ has('telegram') && (
|
|
164
362
|
<div className="row">
|
|
165
|
-
{ t('routes_order.step4.telegram') }
|
|
363
|
+
<Required htmlFor={ id(names.telegram) }>{ t('routes_order.step4.telegram') }</Required>
|
|
166
364
|
|
|
167
365
|
<input
|
|
168
366
|
type="text"
|
|
169
|
-
|
|
367
|
+
key={ `${ names.telegram }-${ applied }` }
|
|
368
|
+
id={ id(names.telegram) }
|
|
369
|
+
defaultValue={ start(names.telegram, record?.telegram) }
|
|
170
370
|
{ ...refs(names.telegram, Validate('required|min_length:2|max_length:100', t)) }
|
|
171
371
|
/>
|
|
172
372
|
|
|
@@ -179,7 +379,13 @@ const Passenger = ({ type, number, values, fields, withReturn, busFrom, busTo, p
|
|
|
179
379
|
{ type !== 'baby' && (
|
|
180
380
|
<div className="column">
|
|
181
381
|
<div className="row">
|
|
182
|
-
{
|
|
382
|
+
{ /* Edited: Claude - Date: 2026-08-26 - with the seat-selection
|
|
383
|
+
add-on on, a pick is neither required nor free: the label
|
|
384
|
+
drops its required mark and validation, ChooseSeat carries
|
|
385
|
+
the fee and a way back to the free automatic seat. */ }
|
|
386
|
+
{ seatsOptional
|
|
387
|
+
? <span>{ t('seats.departure') }</span>
|
|
388
|
+
: <Required>{ t('seats.departure') }</Required> }
|
|
183
389
|
|
|
184
390
|
<ChooseSeat
|
|
185
391
|
name={ names.seatDeparture }
|
|
@@ -187,7 +393,9 @@ const Passenger = ({ type, number, values, fields, withReturn, busFrom, busTo, p
|
|
|
187
393
|
defaultValue={ values !== undefined ? Number(values[names.seatDeparture]) : undefined }
|
|
188
394
|
bus={ busFrom }
|
|
189
395
|
picked={ pickedFrom }
|
|
190
|
-
validation=
|
|
396
|
+
validation={ seatsOptional ? '' : 'required|min:1' }
|
|
397
|
+
optional={ seatsOptional }
|
|
398
|
+
fee={ seatsFee }
|
|
191
399
|
t={ t }
|
|
192
400
|
refs={ refs }
|
|
193
401
|
onUpdate={ onSeatSelect }
|
|
@@ -198,7 +406,9 @@ const Passenger = ({ type, number, values, fields, withReturn, busFrom, busTo, p
|
|
|
198
406
|
|
|
199
407
|
{ withReturn && (
|
|
200
408
|
<div className="row">
|
|
201
|
-
{
|
|
409
|
+
{ seatsOptional
|
|
410
|
+
? <span>{ t('seats.return') }</span>
|
|
411
|
+
: <Required>{ t('seats.return') }</Required> }
|
|
202
412
|
|
|
203
413
|
<ChooseSeat
|
|
204
414
|
name={ names.seatReturn }
|
|
@@ -206,7 +416,9 @@ const Passenger = ({ type, number, values, fields, withReturn, busFrom, busTo, p
|
|
|
206
416
|
defaultValue={ values !== undefined ? Number(values[names.seatReturn]) : undefined }
|
|
207
417
|
bus={ busTo }
|
|
208
418
|
picked={ pickedTo }
|
|
209
|
-
validation=
|
|
419
|
+
validation={ seatsOptional ? '' : 'required|min:1' }
|
|
420
|
+
optional={ seatsOptional }
|
|
421
|
+
fee={ seatsFee }
|
|
210
422
|
t={ t }
|
|
211
423
|
refs={ refs }
|
|
212
424
|
onUpdate={ onSeatSelect }
|
|
@@ -217,6 +429,23 @@ const Passenger = ({ type, number, values, fields, withReturn, busFrom, busTo, p
|
|
|
217
429
|
) }
|
|
218
430
|
</div>
|
|
219
431
|
) }
|
|
432
|
+
|
|
433
|
+
{ /* Edited: Claude - Date: 2026-08-20
|
|
434
|
+
At the foot of the passenger it applies to, so the question and
|
|
435
|
+
the passport number it would store are read in one glance. Only
|
|
436
|
+
for a signed-in customer: a guest has no account to save into, and
|
|
437
|
+
obtapi's Libraries\Passengers\Capture skips staff accounts for a
|
|
438
|
+
different reason - selling to a walk-in traveller must not
|
|
439
|
+
accumulate that person's documents under the agency. */ }
|
|
440
|
+
{ saved !== undefined && (
|
|
441
|
+
<Remember
|
|
442
|
+
name={ names.save }
|
|
443
|
+
id={ id(names.save) }
|
|
444
|
+
full={ full }
|
|
445
|
+
t={ t }
|
|
446
|
+
refs={ refs }
|
|
447
|
+
/>
|
|
448
|
+
) }
|
|
220
449
|
</Container>
|
|
221
450
|
);
|
|
222
451
|
};
|
|
@@ -7,4 +7,24 @@ export const Container = styled.div`
|
|
|
7
7
|
export const Title = styled.h3`
|
|
8
8
|
padding-bottom: 16px;
|
|
9
9
|
border-bottom: 1px solid ${ props => props.theme.background.neutral };
|
|
10
|
-
`;
|
|
10
|
+
`;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* What the phone number will be used for.
|
|
14
|
+
*
|
|
15
|
+
* Edited: Ferjolt Ozuni - Date: 2026-08-05
|
|
16
|
+
*
|
|
17
|
+
* The number has always been collected so the operator could ring it. It is
|
|
18
|
+
* now also the number a service SMS goes to when a departure changes, and
|
|
19
|
+
* that is a different use of the same data - so it is said here, at the
|
|
20
|
+
* point of collection, rather than assumed. Quiet, but not hidden: a notice
|
|
21
|
+
* nobody can read is not consent.
|
|
22
|
+
*/
|
|
23
|
+
export const Notice = styled.small`
|
|
24
|
+
display: block;
|
|
25
|
+
margin-top: 4px;
|
|
26
|
+
font-size: ${ props => props.theme.size.xxs };
|
|
27
|
+
font-weight: 400;
|
|
28
|
+
line-height: 1.4;
|
|
29
|
+
color: ${ props => props.theme.font.faded };
|
|
30
|
+
`;
|
|
@@ -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,14 +14,18 @@ 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>
|
|
22
|
+
seatsOptional?: boolean
|
|
23
|
+
seatsFee?: string
|
|
20
24
|
onSeatSelect: (name: string, type: ('from' | 'to'), seat: number, previous: number) => void
|
|
21
25
|
onUpdate: (name: string, value: (string | number)) => void
|
|
22
26
|
}
|
|
23
27
|
|
|
24
|
-
const Display = ({ type, amount, fields, withReturn, values, busFrom, busTo, pickedFrom, pickedTo, t, errors, refs, onSeatSelect, onUpdate }: Props): JSX.Element[] => {
|
|
28
|
+
const Display = ({ type, amount, fields, withReturn, values, busFrom, busTo, pickedFrom, pickedTo, saved, savedMax, seatsOptional, seatsFee, t, errors, refs, onSeatSelect, onUpdate }: Props): JSX.Element[] => {
|
|
25
29
|
const passengers: JSX.Element[] = [];
|
|
26
30
|
|
|
27
31
|
for (let i = 1; i <= amount; i++) {
|
|
@@ -37,6 +41,10 @@ const Display = ({ type, amount, fields, withReturn, values, busFrom, busTo, pic
|
|
|
37
41
|
busTo={ busTo }
|
|
38
42
|
pickedFrom={ pickedFrom }
|
|
39
43
|
pickedTo={ pickedTo }
|
|
44
|
+
saved={ saved }
|
|
45
|
+
savedMax={ savedMax }
|
|
46
|
+
seatsOptional={ seatsOptional }
|
|
47
|
+
seatsFee={ seatsFee }
|
|
40
48
|
t={ t }
|
|
41
49
|
errors={ errors }
|
|
42
50
|
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,32 @@ 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
|
|
24
|
+
/**
|
|
25
|
+
* Edited: Claude - Date: 2026-08-26
|
|
26
|
+
* The seat-selection add-on, when this brand sells it: picking becomes
|
|
27
|
+
* optional (auto-assign is the free default) and each hand-picked seat
|
|
28
|
+
* costs `seatsFee` (a display string - the server prices the real one).
|
|
29
|
+
* `onPickedCount` reports how many seats are currently hand-picked, so
|
|
30
|
+
* the checkout can price the summary line live.
|
|
31
|
+
*/
|
|
32
|
+
seatsOptional?: boolean
|
|
33
|
+
seatsFee?: string
|
|
34
|
+
onPickedCount?: (count: number) => void
|
|
16
35
|
t: TFunction<'public'>
|
|
17
36
|
errors: FieldErrors<FieldValues>
|
|
18
37
|
refs: UseFormRegister<PersonData>
|
|
19
38
|
onUpdate: (name: string, value: (string | number)) => void
|
|
20
39
|
}
|
|
21
40
|
|
|
22
|
-
const Passengers = ({ values, step1, fields, busFrom, busTo, t, errors, refs, onUpdate }: Props): JSX.Element => {
|
|
41
|
+
const Passengers = ({ values, step1, fields, busFrom, busTo, saved, savedMax, seatsOptional, seatsFee, onPickedCount, t, errors, refs, onUpdate }: Props): JSX.Element => {
|
|
23
42
|
const [ pickedFrom, setPickedFrom ] = useState<number[]>([]);
|
|
24
43
|
const [ pickedTo, setPickedTo ] = useState<number[]>([]);
|
|
25
44
|
|
|
@@ -27,10 +46,19 @@ const Passengers = ({ values, step1, fields, busFrom, busTo, t, errors, refs, on
|
|
|
27
46
|
let newPicked = type === 'from' ? [...pickedFrom] : [...pickedTo];
|
|
28
47
|
|
|
29
48
|
if (previous > 0) {
|
|
30
|
-
|
|
49
|
+
// Edited: Claude - Date: 2026-08-26
|
|
50
|
+
// Was `pickedFrom.filter(...)` regardless of leg, so re-picking a
|
|
51
|
+
// RETURN seat rebuilt the return list from the departure's - the
|
|
52
|
+
// old choice stayed greyed out on the return map and other
|
|
53
|
+
// passengers' return picks vanished from it.
|
|
54
|
+
newPicked = newPicked.filter(item => item !== previous);
|
|
31
55
|
}
|
|
32
56
|
|
|
33
|
-
|
|
57
|
+
// seat 0 is the add-on's "back to automatic": remove the old pick and
|
|
58
|
+
// push nothing
|
|
59
|
+
if (seat > 0) {
|
|
60
|
+
newPicked.push(seat);
|
|
61
|
+
}
|
|
34
62
|
|
|
35
63
|
if (type === 'from') {
|
|
36
64
|
setPickedFrom(newPicked);
|
|
@@ -38,6 +66,15 @@ const Passengers = ({ values, step1, fields, busFrom, busTo, t, errors, refs, on
|
|
|
38
66
|
setPickedTo(newPicked);
|
|
39
67
|
}
|
|
40
68
|
|
|
69
|
+
// how many seats are hand-picked RIGHT NOW, for the fee line - built
|
|
70
|
+
// from the fresh arrays, since state has not committed yet
|
|
71
|
+
if (onPickedCount) {
|
|
72
|
+
onPickedCount(
|
|
73
|
+
(type === 'from' ? newPicked : pickedFrom).length
|
|
74
|
+
+ (type === 'to' ? newPicked : pickedTo).length
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
41
78
|
onUpdate(name, seat);
|
|
42
79
|
};
|
|
43
80
|
|
|
@@ -53,6 +90,10 @@ const Passengers = ({ values, step1, fields, busFrom, busTo, t, errors, refs, on
|
|
|
53
90
|
busTo={ busTo }
|
|
54
91
|
pickedFrom={ pickedFrom }
|
|
55
92
|
pickedTo={ pickedTo }
|
|
93
|
+
saved={ saved }
|
|
94
|
+
savedMax={ savedMax }
|
|
95
|
+
seatsOptional={ seatsOptional }
|
|
96
|
+
seatsFee={ seatsFee }
|
|
56
97
|
t={ t }
|
|
57
98
|
errors={ errors }
|
|
58
99
|
refs={ refs }
|
|
@@ -70,6 +111,10 @@ const Passengers = ({ values, step1, fields, busFrom, busTo, t, errors, refs, on
|
|
|
70
111
|
busTo={ busTo }
|
|
71
112
|
pickedFrom={ pickedFrom }
|
|
72
113
|
pickedTo={ pickedTo }
|
|
114
|
+
saved={ saved }
|
|
115
|
+
savedMax={ savedMax }
|
|
116
|
+
seatsOptional={ seatsOptional }
|
|
117
|
+
seatsFee={ seatsFee }
|
|
73
118
|
t={ t }
|
|
74
119
|
errors={ errors }
|
|
75
120
|
refs={ refs }
|
|
@@ -87,6 +132,10 @@ const Passengers = ({ values, step1, fields, busFrom, busTo, t, errors, refs, on
|
|
|
87
132
|
busTo={ busTo }
|
|
88
133
|
pickedFrom={ pickedFrom }
|
|
89
134
|
pickedTo={ pickedTo }
|
|
135
|
+
saved={ saved }
|
|
136
|
+
savedMax={ savedMax }
|
|
137
|
+
seatsOptional={ seatsOptional }
|
|
138
|
+
seatsFee={ seatsFee }
|
|
90
139
|
t={ t }
|
|
91
140
|
errors={ errors }
|
|
92
141
|
refs={ refs }
|