@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.
Files changed (76) hide show
  1. package/CHANGELOG.md +731 -0
  2. package/Checkout/Checkout.tsx +519 -0
  3. package/Checkout/Sidebar/Sidebar.tsx +124 -0
  4. package/Checkout/Sidebar/styles.ts +52 -0
  5. package/Facts/Facts.tsx +271 -0
  6. package/Facts/questions.ts +250 -0
  7. package/Facts/service.ts +111 -0
  8. package/Facts/services.ts +19 -0
  9. package/Facts/styles.ts +112 -0
  10. package/Facts/types.ts +167 -0
  11. package/Faq/Faq.tsx +52 -0
  12. package/Faq/styles.ts +68 -0
  13. package/Found/Dates/Dates.tsx +95 -5
  14. package/Found/Dates/styles.ts +173 -9
  15. package/Found/Empty/Empty.tsx +136 -0
  16. package/Found/Empty/styles.ts +98 -0
  17. package/Found/Filters/Filters.tsx +194 -0
  18. package/Found/Filters/styles.ts +188 -0
  19. package/Found/Found.tsx +207 -17
  20. package/Found/Group/Group.tsx +68 -0
  21. package/Found/Group/styles.ts +44 -0
  22. package/Found/Header/styles.ts +20 -4
  23. package/Found/Orders/Orders.tsx +89 -27
  24. package/Found/Orders/styles.ts +14 -0
  25. package/Found/Picks/Picks.tsx +75 -0
  26. package/Found/Picks/styles.ts +106 -0
  27. package/Found/Route/Route.tsx +211 -33
  28. package/Found/Route/styles.ts +491 -46
  29. package/Found/SameDay/SameDay.tsx +35 -0
  30. package/Found/SameDay/styles.ts +27 -0
  31. package/Found/Sort/Sort.tsx +40 -0
  32. package/Found/Sort/styles.ts +87 -0
  33. package/Found/Summary/Summary.tsx +49 -0
  34. package/Found/Summary/styles.ts +42 -0
  35. package/Found/Watch/Watch.tsx +187 -0
  36. package/Found/Watch/styles.ts +118 -0
  37. package/Found/refine.ts +652 -0
  38. package/Links/Links.tsx +91 -0
  39. package/Links/styles.ts +75 -0
  40. package/RoutesOrder.tsx +239 -31
  41. package/Schedule/Schedule.tsx +123 -0
  42. package/Schedule/styles.ts +71 -0
  43. package/Sections/Schema.tsx +186 -0
  44. package/Sections/Sections.tsx +160 -0
  45. package/Sections/styles.ts +44 -0
  46. package/Step1/prepare.ts +21 -2
  47. package/Step2.tsx +34 -1
  48. package/Step4/Passenger/Passenger.tsx +253 -24
  49. package/Step4/Passenger/styles.ts +21 -1
  50. package/Step4/Passengers/Display.tsx +10 -2
  51. package/Step4/Passengers/Passengers.tsx +53 -4
  52. package/Step4/Saved/Choose.tsx +99 -0
  53. package/Step4/Saved/Remember.tsx +58 -0
  54. package/Step4/Saved/styles.ts +87 -0
  55. package/Step4/checkAge.ts +15 -4
  56. package/Step5/Addons/Addons.tsx +146 -4
  57. package/Step5/Addons/styles.ts +94 -0
  58. package/Step5/Billing/Billing.tsx +138 -70
  59. package/Step5/Coupons/Coupons.tsx +28 -18
  60. package/Step5/Coupons/styles.ts +41 -8
  61. package/Step5/Payments/Payments.tsx +36 -5
  62. package/Step5/Payments/styles.ts +16 -7
  63. package/Step5/Summary/About/About.tsx +99 -27
  64. package/Step5/Summary/About/styles.ts +118 -1
  65. package/Step5/Summary/styles.ts +12 -5
  66. package/Step5/utilities.ts +67 -12
  67. package/Steps/Steps.tsx +3 -1
  68. package/commerce.ts +24 -0
  69. package/package.json +1 -1
  70. package/services.ts +302 -13
  71. package/styles.ts +126 -0
  72. package/types.ts +84 -1
  73. package/Found/Booking/Booking.tsx +0 -61
  74. package/Found/Booking/styles.ts +0 -50
  75. package/Step4/Step4.tsx +0 -114
  76. 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 { Container, Title } from './styles';
6
- import { PersonData } from '@autobusal/providers/types/persons';
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
- defaultValue={ values !== undefined ? values[names.firstName] : '' }
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
- defaultValue={ values !== undefined ? values[names.lastName] : '' }
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
- defaultValue={ values !== undefined ? String(values[names.dob]) : undefined }
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
- defaultValue={ values !== undefined ? values[names.passport] : '' }
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
- defaultValue={ values !== undefined ? values[names.phone] : '' }
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
- defaultValue={ values !== undefined ? values[names.whatsapp] : '' }
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
- defaultValue={ values !== undefined ? values[names.telegram] : '' }
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
- { t('seats.departure') }
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="required|min:1"
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
- { t('seats.return') }
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="required|min:1"
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
- newPicked = pickedFrom.filter(item => item !== previous);
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
- newPicked.push(seat);
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 }