@autobusal/routes-order 1.33.6 → 1.33.7

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 CHANGED
@@ -1,5 +1,9 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.33.7 (2026-08-26)
4
+
5
+ - Seat-selection add-on: checkout reads settings addons.seats - picking turns optional with the fee on the CTA and a live summary line; submission strips unpicked (zero) seat fields and sends addon_seats only when a hand-picked seat remains. Fixes the return-leg picked-seat list rebuilding from the departure's on re-pick.
6
+
3
7
  ## 1.33.6
4
8
 
5
9
  ### Fixed
@@ -125,6 +125,17 @@ const Checkout = ({ values, step1, step2, step3, isPartner, t, onStore, onBack }
125
125
  const [ addonSmsNumber, setAddonSmsNumber ] = useState<string>('');
126
126
  const [ addonFlex, setAddonFlex ] = useState<boolean>(false);
127
127
 
128
+ /*
129
+ * Edited: Claude - Date: 2026-08-26
130
+ *
131
+ * The seat-selection add-on. Unlike the checkbox add-ons above there is
132
+ * nothing to tick: wanting it IS picking a seat. `seatsPicked` is the
133
+ * live hand-picked count, reported up by Passengers as choices are made
134
+ * and taken back, and it prices the summary line; the server recomputes
135
+ * the real charge from its own count (Buses\Assign) regardless.
136
+ */
137
+ const [ seatsPicked, setSeatsPicked ] = useState<number>(0);
138
+
128
139
  const { data: UserData } = useUserStore();
129
140
  const { data: SettingsData } = useGetSettings();
130
141
 
@@ -170,6 +181,10 @@ const Checkout = ({ values, step1, step2, step3, isPartner, t, onStore, onBack }
170
181
 
171
182
  const currency = total.display.split(' ')[1] ?? '';
172
183
 
184
+ // the brand sells seat choice: picking turns optional (auto-assign is the
185
+ // free default) and each hand-picked seat costs this
186
+ const seatsOffer = SettingsData?.addons?.seats?.available ? SettingsData.addons.seats : undefined;
187
+
173
188
  // Edited: Ferjolt Ozuni - Date: 2026-08-03
174
189
  // Flexible Ticket, for the routes actually chosen. Asked for BOTH legs at
175
190
  // once: the price is computed on the combined fare and an operator who
@@ -231,6 +246,10 @@ const Checkout = ({ values, step1, step2, step3, isPartner, t, onStore, onBack }
231
246
  addonFlex && FlexData?.available && FlexData.price !== undefined ? {
232
247
  label: t('routes_order.step5.flex.title'),
233
248
  price: FlexData.price
249
+ } : null,
250
+ seatsOffer && seatsPicked > 0 ? {
251
+ label: `${ t('seats.fee_line', { ns: 'common' }) } (${ seatsPicked })`,
252
+ price: seatsOffer.price * seatsPicked
234
253
  } : null
235
254
  ].filter((item): item is { label: string, price: number } => item !== null);
236
255
 
@@ -261,6 +280,7 @@ const Checkout = ({ values, step1, step2, step3, isPartner, t, onStore, onBack }
261
280
  whatsappNumber: addonWhatsappNumber,
262
281
  telegram: addonTelegram,
263
282
  sms: addonSms,
283
+ seats: seatsOffer !== undefined,
264
284
  smsNumber: addonSmsNumber,
265
285
  flex: addonFlex
266
286
  });
@@ -393,6 +413,9 @@ const Checkout = ({ values, step1, step2, step3, isPartner, t, onStore, onBack }
393
413
  busTo={ busTo }
394
414
  saved={ saved }
395
415
  savedMax={ SavedData?.max }
416
+ seatsOptional={ seatsOffer !== undefined }
417
+ seatsFee={ seatsOffer && seatsOffer.price > 0 ? `${ seatsOffer.price } ${ currency }` : undefined }
418
+ onPickedCount={ setSeatsPicked }
396
419
  t={ t }
397
420
  errors={ passenger.formState.errors }
398
421
  refs={ passenger.register }
@@ -31,6 +31,9 @@ interface Props {
31
31
  saved?: SavedPassenger[]
32
32
  /** how many travellers one account may keep (obtapi's own cap) */
33
33
  savedMax?: number
34
+ /** seat-selection add-on: picking optional, `seatsFee` on the CTA */
35
+ seatsOptional?: boolean
36
+ seatsFee?: string
34
37
  t: TFunction<'public'>
35
38
  errors: FieldErrors<FieldValues>
36
39
  refs: UseFormRegister<PersonData>
@@ -44,7 +47,7 @@ interface Props {
44
47
  // telegram render ONLY when the route requires them - and are then
45
48
  // required. Name and date of birth stay mandatory always. `fields` is the
46
49
  // union of both legs on a return trip (see Step4).
47
- const Passenger = ({ type, number, values, fields, withReturn, busFrom, busTo, pickedFrom, pickedTo, saved, savedMax, 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 => {
48
51
  const name = `${ type }${ number }`;
49
52
 
50
53
  const names = {
@@ -376,7 +379,13 @@ const Passenger = ({ type, number, values, fields, withReturn, busFrom, busTo, p
376
379
  { type !== 'baby' && (
377
380
  <div className="column">
378
381
  <div className="row">
379
- <Required>{ t('seats.departure') }</Required>
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> }
380
389
 
381
390
  <ChooseSeat
382
391
  name={ names.seatDeparture }
@@ -384,7 +393,9 @@ const Passenger = ({ type, number, values, fields, withReturn, busFrom, busTo, p
384
393
  defaultValue={ values !== undefined ? Number(values[names.seatDeparture]) : undefined }
385
394
  bus={ busFrom }
386
395
  picked={ pickedFrom }
387
- validation="required|min:1"
396
+ validation={ seatsOptional ? '' : 'required|min:1' }
397
+ optional={ seatsOptional }
398
+ fee={ seatsFee }
388
399
  t={ t }
389
400
  refs={ refs }
390
401
  onUpdate={ onSeatSelect }
@@ -395,7 +406,9 @@ const Passenger = ({ type, number, values, fields, withReturn, busFrom, busTo, p
395
406
 
396
407
  { withReturn && (
397
408
  <div className="row">
398
- <Required>{ t('seats.return') }</Required>
409
+ { seatsOptional
410
+ ? <span>{ t('seats.return') }</span>
411
+ : <Required>{ t('seats.return') }</Required> }
399
412
 
400
413
  <ChooseSeat
401
414
  name={ names.seatReturn }
@@ -403,7 +416,9 @@ const Passenger = ({ type, number, values, fields, withReturn, busFrom, busTo, p
403
416
  defaultValue={ values !== undefined ? Number(values[names.seatReturn]) : undefined }
404
417
  bus={ busTo }
405
418
  picked={ pickedTo }
406
- validation="required|min:1"
419
+ validation={ seatsOptional ? '' : 'required|min:1' }
420
+ optional={ seatsOptional }
421
+ fee={ seatsFee }
407
422
  t={ t }
408
423
  refs={ refs }
409
424
  onUpdate={ onSeatSelect }
@@ -19,11 +19,13 @@ interface Props {
19
19
  t: TFunction<'public'>
20
20
  errors: FieldErrors<FieldValues>
21
21
  refs: UseFormRegister<PersonData>
22
+ seatsOptional?: boolean
23
+ seatsFee?: string
22
24
  onSeatSelect: (name: string, type: ('from' | 'to'), seat: number, previous: number) => void
23
25
  onUpdate: (name: string, value: (string | number)) => void
24
26
  }
25
27
 
26
- const Display = ({ type, amount, fields, withReturn, values, busFrom, busTo, pickedFrom, pickedTo, saved, savedMax, 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[] => {
27
29
  const passengers: JSX.Element[] = [];
28
30
 
29
31
  for (let i = 1; i <= amount; i++) {
@@ -41,6 +43,8 @@ const Display = ({ type, amount, fields, withReturn, values, busFrom, busTo, pic
41
43
  pickedTo={ pickedTo }
42
44
  saved={ saved }
43
45
  savedMax={ savedMax }
46
+ seatsOptional={ seatsOptional }
47
+ seatsFee={ seatsFee }
44
48
  t={ t }
45
49
  errors={ errors }
46
50
  refs={ refs }
@@ -21,13 +21,24 @@ interface Props {
21
21
  */
22
22
  saved?: SavedPassenger[]
23
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
24
35
  t: TFunction<'public'>
25
36
  errors: FieldErrors<FieldValues>
26
37
  refs: UseFormRegister<PersonData>
27
38
  onUpdate: (name: string, value: (string | number)) => void
28
39
  }
29
40
 
30
- const Passengers = ({ values, step1, fields, busFrom, busTo, saved, savedMax, 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 => {
31
42
  const [ pickedFrom, setPickedFrom ] = useState<number[]>([]);
32
43
  const [ pickedTo, setPickedTo ] = useState<number[]>([]);
33
44
 
@@ -35,10 +46,19 @@ const Passengers = ({ values, step1, fields, busFrom, busTo, saved, savedMax, t,
35
46
  let newPicked = type === 'from' ? [...pickedFrom] : [...pickedTo];
36
47
 
37
48
  if (previous > 0) {
38
- 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);
39
55
  }
40
56
 
41
- 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
+ }
42
62
 
43
63
  if (type === 'from') {
44
64
  setPickedFrom(newPicked);
@@ -46,6 +66,15 @@ const Passengers = ({ values, step1, fields, busFrom, busTo, saved, savedMax, t,
46
66
  setPickedTo(newPicked);
47
67
  }
48
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
+
49
78
  onUpdate(name, seat);
50
79
  };
51
80
 
@@ -63,6 +92,8 @@ const Passengers = ({ values, step1, fields, busFrom, busTo, saved, savedMax, t,
63
92
  pickedTo={ pickedTo }
64
93
  saved={ saved }
65
94
  savedMax={ savedMax }
95
+ seatsOptional={ seatsOptional }
96
+ seatsFee={ seatsFee }
66
97
  t={ t }
67
98
  errors={ errors }
68
99
  refs={ refs }
@@ -82,6 +113,8 @@ const Passengers = ({ values, step1, fields, busFrom, busTo, saved, savedMax, t,
82
113
  pickedTo={ pickedTo }
83
114
  saved={ saved }
84
115
  savedMax={ savedMax }
116
+ seatsOptional={ seatsOptional }
117
+ seatsFee={ seatsFee }
85
118
  t={ t }
86
119
  errors={ errors }
87
120
  refs={ refs }
@@ -101,6 +134,8 @@ const Passengers = ({ values, step1, fields, busFrom, busTo, saved, savedMax, t,
101
134
  pickedTo={ pickedTo }
102
135
  saved={ saved }
103
136
  savedMax={ savedMax }
137
+ seatsOptional={ seatsOptional }
138
+ seatsFee={ seatsFee }
104
139
  t={ t }
105
140
  errors={ errors }
106
141
  refs={ refs }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@autobusal/routes-order",
3
- "version": "1.33.6",
3
+ "version": "1.33.7",
4
4
  "author": "Ferjolt Ozuni",
5
5
  "type": "module",
6
6
  "main": "index.ts"
package/services.ts CHANGED
@@ -190,6 +190,36 @@ export const usePostOrder = (
190
190
  order[index] = step4[index];
191
191
  });
192
192
 
193
+ /*
194
+ * Edited: Claude - Date: 2026-08-26
195
+ *
196
+ * Seat fields. A zero means "no choice made": with the seat-selection
197
+ * add-on the picker is optional and 0 is its resting value, and the
198
+ * API auto-assigns ABSENT fields but rejects a literal zero as a
199
+ * taken seat - so unpicked fields must not travel at all. Stripping
200
+ * unconditionally is safe for the legacy mode too: there the fields
201
+ * are validated required|min:1, so a zero cannot reach this line.
202
+ *
203
+ * addon_seats goes over only when the brand sells the add-on AND at
204
+ * least one hand-picked seat survived the strip - the flag is the
205
+ * buyer's opt-in, the server's Buses\Assign counts what it charges.
206
+ */
207
+ let chosenSeats = 0;
208
+
209
+ Object.keys(order).forEach(key => {
210
+ if (/_s[dr]$/.test(key)) {
211
+ if (Number(order[key]) > 0) {
212
+ chosenSeats++;
213
+ } else {
214
+ delete order[key];
215
+ }
216
+ }
217
+ });
218
+
219
+ if (addons.seats && chosenSeats > 0) {
220
+ order.addon_seats = '1';
221
+ }
222
+
193
223
  if (action !== undefined) {
194
224
  order.action = action;
195
225
  }
package/types.ts CHANGED
@@ -87,6 +87,13 @@ export interface AddonsSelection {
87
87
  // price are the server's to decide (Libraries\Orders\Flex), and a client
88
88
  // that sent either would be ignored.
89
89
  flex: boolean
90
+
91
+ // Edited: Claude - Date: 2026-08-26
92
+ // Seat selection is SOLD on this brand (settings addons.seats). What is
93
+ // actually bought is implicit in the seat fields themselves: the
94
+ // submission strips unpicked (zero) fields and sends addon_seats only
95
+ // when at least one hand-picked seat remains - see usePostOrder.
96
+ seats: boolean
90
97
  }
91
98
  /**
92
99
  * "Watch this fare" - see Found/Watch/Watch.tsx.