@autobusal/routes-order 1.33.5 → 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 +10 -0
- package/Checkout/Checkout.tsx +23 -0
- package/Step4/Passenger/Passenger.tsx +20 -5
- package/Step4/Passengers/Display.tsx +5 -1
- package/Step4/Passengers/Passengers.tsx +38 -3
- package/package.json +1 -1
- package/services.ts +35 -1
- package/types.ts +7 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
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
|
+
|
|
7
|
+
## 1.33.6
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- **The return date strip asks the API for return-leg pricing** (new leg parameter) - it used to quote one-way fares over days the buyer pays the round-trip difference for. (Audit BOOK-02-b; needs obtapi >= 2.4.63.)
|
|
12
|
+
|
|
3
13
|
## 1.33.5
|
|
4
14
|
|
|
5
15
|
### Fixed
|
package/Checkout/Checkout.tsx
CHANGED
|
@@ -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
|
-
|
|
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=
|
|
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
|
-
|
|
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=
|
|
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
|
-
|
|
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
|
-
|
|
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
package/services.ts
CHANGED
|
@@ -104,7 +104,11 @@ export const useGetDates = (step1: RoutesSearchForm, type: ('departure' | '_retu
|
|
|
104
104
|
.get('/api/routes/search/dates', {
|
|
105
105
|
params: {
|
|
106
106
|
...step,
|
|
107
|
-
number
|
|
107
|
+
number,
|
|
108
|
+
// which half of a round trip this strip prices (BOOK-02-b):
|
|
109
|
+
// the from/to swap above handles the direction, this handles
|
|
110
|
+
// the FARE - a return leg sells at adult_return
|
|
111
|
+
leg: type === '_return' ? 'return' : 'departure'
|
|
108
112
|
}
|
|
109
113
|
})
|
|
110
114
|
.then(response => (
|
|
@@ -186,6 +190,36 @@ export const usePostOrder = (
|
|
|
186
190
|
order[index] = step4[index];
|
|
187
191
|
});
|
|
188
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
|
+
|
|
189
223
|
if (action !== undefined) {
|
|
190
224
|
order.action = action;
|
|
191
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.
|