@autobusal/routes-order 1.22.0 → 1.23.0
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 +11 -0
- package/Checkout/Checkout.tsx +24 -2
- package/Found/Route/Route.tsx +6 -6
- package/Step5/Addons/Addons.tsx +67 -4
- package/Step5/Addons/styles.ts +20 -0
- package/package.json +1 -1
- package/services.ts +46 -2
- package/types.ts +6 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.23.0
|
|
4
|
+
|
|
5
|
+
**Flexible Ticket at checkout.**
|
|
6
|
+
|
|
7
|
+
- New `useGetFlex(departurePriceId, returnPriceId, adults, children)`. Its own request rather than a field on the search result, because the answer depends on **both legs at once**: an operator who will not amend one half removes the add-on from the whole booking, and the price is computed on the combined fare.
|
|
8
|
+
- `Addons` renders the option below passenger details, where it already sat. It shows the **default first** — "without it, this ticket cannot be cancelled or changed" — then only the capabilities this booking actually includes, then that the fee itself is not refunded, then that statutory rights when the *operator* cancels are untouched. Nothing renders at all when the offer is unavailable: a checkbox that unlocks nothing is worse than no checkbox.
|
|
9
|
+
- `AddonsSelection` gains `flex`; the order payload gains `addon_flex`.
|
|
10
|
+
- `Found/Route` reads `routes_order.step2.route.information.*` after the `policies` rename.
|
|
11
|
+
|
|
12
|
+
Copy is **Flexible Ticket** / **Biletë Fleksibël**, never insurance — the distinction decides how the line is taxed and whether selling it needs a licence.
|
|
13
|
+
|
|
3
14
|
All notable changes to this package are documented here. This project follows
|
|
4
15
|
[Keep a Changelog](https://keepachangelog.com/) and [Semantic Versioning](https://semver.org/).
|
|
5
16
|
|
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 } from '../services';
|
|
27
|
+
import { usePostOrder, useGetFlex } from '../services';
|
|
28
28
|
|
|
29
29
|
interface Props {
|
|
30
30
|
values?: PersonData
|
|
@@ -75,6 +75,7 @@ const Checkout = ({ values, step1, step2, step3, isPartner, t, onStore, onBack }
|
|
|
75
75
|
const [ addonWhatsapp, setAddonWhatsapp ] = useState<boolean>(false);
|
|
76
76
|
const [ addonWhatsappNumber, setAddonWhatsappNumber ] = useState<string>('');
|
|
77
77
|
const [ addonTelegram, setAddonTelegram ] = useState<boolean>(false);
|
|
78
|
+
const [ addonFlex, setAddonFlex ] = useState<boolean>(false);
|
|
78
79
|
|
|
79
80
|
const { data: UserData } = useUserStore();
|
|
80
81
|
const { data: SettingsData } = useGetSettings();
|
|
@@ -93,6 +94,19 @@ const Checkout = ({ values, step1, step2, step3, isPartner, t, onStore, onBack }
|
|
|
93
94
|
|
|
94
95
|
const currency = total.display.split(' ')[1] ?? '';
|
|
95
96
|
|
|
97
|
+
// Edited: Ferjolt Ozuni - Date: 2026-08-03
|
|
98
|
+
// Flexible Ticket, for the routes actually chosen. Asked for BOTH legs at
|
|
99
|
+
// once: the price is computed on the combined fare and an operator who
|
|
100
|
+
// will not amend one half removes the offer from the whole booking, so a
|
|
101
|
+
// per-leg question could not be answered honestly.
|
|
102
|
+
const { data: FlexData } = useGetFlex(
|
|
103
|
+
step2.price.id as number,
|
|
104
|
+
step3?.price.id as (number | undefined),
|
|
105
|
+
step1.adults ?? 1,
|
|
106
|
+
step1.children ?? 0,
|
|
107
|
+
step1.babies ?? 0
|
|
108
|
+
);
|
|
109
|
+
|
|
96
110
|
const addonItems = [
|
|
97
111
|
addonWhatsapp && SettingsData?.addons?.whatsapp.available ? {
|
|
98
112
|
label: t('routes_order.step5.addons.whatsapp.label'),
|
|
@@ -101,6 +115,10 @@ const Checkout = ({ values, step1, step2, step3, isPartner, t, onStore, onBack }
|
|
|
101
115
|
addonTelegram && SettingsData?.addons?.telegram.available ? {
|
|
102
116
|
label: t('routes_order.step5.addons.telegram.label'),
|
|
103
117
|
price: SettingsData.addons.telegram.price
|
|
118
|
+
} : null,
|
|
119
|
+
addonFlex && FlexData?.available && FlexData.price !== undefined ? {
|
|
120
|
+
label: t('routes_order.step5.flex.title'),
|
|
121
|
+
price: FlexData.price
|
|
104
122
|
} : null
|
|
105
123
|
].filter((item): item is { label: string, price: number } => item !== null);
|
|
106
124
|
|
|
@@ -114,7 +132,8 @@ const Checkout = ({ values, step1, step2, step3, isPartner, t, onStore, onBack }
|
|
|
114
132
|
const { mutate: OrderSend, isPending } = usePostOrder(step1, step2, step3, coupon, action, {
|
|
115
133
|
whatsapp: addonWhatsapp,
|
|
116
134
|
whatsappNumber: addonWhatsappNumber,
|
|
117
|
-
telegram: addonTelegram
|
|
135
|
+
telegram: addonTelegram,
|
|
136
|
+
flex: addonFlex
|
|
118
137
|
});
|
|
119
138
|
|
|
120
139
|
const onApplyCoupon = (data: CouponData): void => {
|
|
@@ -215,14 +234,17 @@ const Checkout = ({ values, step1, step2, step3, isPartner, t, onStore, onBack }
|
|
|
215
234
|
|
|
216
235
|
<Addons
|
|
217
236
|
data={ SettingsData?.addons }
|
|
237
|
+
flexOffer={ FlexData }
|
|
218
238
|
currency={ currency }
|
|
219
239
|
whatsapp={ addonWhatsapp }
|
|
220
240
|
whatsappNumber={ addonWhatsappNumber }
|
|
221
241
|
telegram={ addonTelegram }
|
|
242
|
+
flex={ addonFlex }
|
|
222
243
|
t={ t }
|
|
223
244
|
onChangeWhatsapp={ setAddonWhatsapp }
|
|
224
245
|
onChangeWhatsappNumber={ setAddonWhatsappNumber }
|
|
225
246
|
onChangeTelegram={ setAddonTelegram }
|
|
247
|
+
onChangeFlex={ setAddonFlex }
|
|
226
248
|
/>
|
|
227
249
|
|
|
228
250
|
{ UserData === undefined && (
|
package/Found/Route/Route.tsx
CHANGED
|
@@ -4,7 +4,7 @@ import { HiOutlineArrowNarrowRight, HiOutlineDocumentText, HiChevronDown, HiChev
|
|
|
4
4
|
import { BiHourglass } from 'react-icons/bi';
|
|
5
5
|
import { RiBus2Line } from 'react-icons/ri';
|
|
6
6
|
import { formatDuration, splitPrice } from '../refine';
|
|
7
|
-
import { Button, RouteFeature,
|
|
7
|
+
import { Button, RouteFeature, Information, Rating } from '@autobusal/common';
|
|
8
8
|
import { Container, ButtonDetails, SpecialOffer, FavoriteStop, Trip, Company, Location, Stop, Iso, Time, Price, Amount, Decimals, Currency, PriceNotice, LinkCompany, Details, Detail, ButtonPolicy, TitleDetail, LinkRoute, FeaturesItems, Leaves } from './styles';
|
|
9
9
|
import { FoundData } from '@autobusal/providers/types/routes';
|
|
10
10
|
|
|
@@ -25,7 +25,7 @@ interface Props {
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
const Route = ({ data, type, passengers, t, onSave, children }: Props): JSX.Element => {
|
|
28
|
-
const [
|
|
28
|
+
const [ information, setInformation ] = useState<boolean>(false);
|
|
29
29
|
|
|
30
30
|
// Edited: Ferjolt Ozuni - Date: 2026-08-03
|
|
31
31
|
// Closed to begin with. Amenities, route code, transit and policies are
|
|
@@ -203,17 +203,17 @@ const Route = ({ data, type, passengers, t, onSave, children }: Props): JSX.Elem
|
|
|
203
203
|
|
|
204
204
|
{ !data.external && (
|
|
205
205
|
<Detail>
|
|
206
|
-
<TitleDetail>{ t('routes_order.step2.route.
|
|
206
|
+
<TitleDetail>{ t('routes_order.step2.route.information.title') }</TitleDetail>
|
|
207
207
|
|
|
208
|
-
<ButtonPolicy type="button" onClick={ () =>
|
|
208
|
+
<ButtonPolicy type="button" onClick={ () => setInformation(true) }>
|
|
209
209
|
<HiOutlineDocumentText />
|
|
210
|
-
{ t('routes_order.step2.route.
|
|
210
|
+
{ t('routes_order.step2.route.information.view') }
|
|
211
211
|
</ButtonPolicy>
|
|
212
212
|
</Detail>
|
|
213
213
|
) }
|
|
214
214
|
</Details>
|
|
215
215
|
|
|
216
|
-
{
|
|
216
|
+
{ information && typeof data.id === 'number' && <Information id={ data.id } t={ t } onClose={ () => setInformation(false) } /> }
|
|
217
217
|
</Container>
|
|
218
218
|
);
|
|
219
219
|
};
|
package/Step5/Addons/Addons.tsx
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { TFunction } from 'i18next';
|
|
2
|
-
import { RiWhatsappLine, RiTelegramLine } from 'react-icons/ri';
|
|
2
|
+
import { RiWhatsappLine, RiTelegramLine, RiCalendarCheckLine } from 'react-icons/ri';
|
|
3
|
+
import { FlexOffer } from '@autobusal/providers/types/routes';
|
|
3
4
|
import { SubTitle } from '../../styles';
|
|
4
|
-
import { Container, Option, Notice } from './styles';
|
|
5
|
+
import { Container, Option, Notice, Terms, Term } from './styles';
|
|
5
6
|
|
|
6
7
|
interface AddonsData {
|
|
7
8
|
whatsapp: { available: boolean, price: number }
|
|
@@ -10,14 +11,17 @@ interface AddonsData {
|
|
|
10
11
|
|
|
11
12
|
interface Props {
|
|
12
13
|
data?: AddonsData
|
|
14
|
+
flexOffer?: FlexOffer
|
|
13
15
|
currency: string
|
|
14
16
|
whatsapp: boolean
|
|
15
17
|
whatsappNumber: string
|
|
16
18
|
telegram: boolean
|
|
19
|
+
flex: boolean
|
|
17
20
|
t: TFunction<'common'>
|
|
18
21
|
onChangeWhatsapp: (value: boolean) => void
|
|
19
22
|
onChangeWhatsappNumber: (value: string) => void
|
|
20
23
|
onChangeTelegram: (value: boolean) => void
|
|
24
|
+
onChangeFlex: (value: boolean) => void
|
|
21
25
|
}
|
|
22
26
|
|
|
23
27
|
// Edited: Ferjolt Ozuni - Date: 2026-07-25
|
|
@@ -25,8 +29,17 @@ interface Props {
|
|
|
25
29
|
// enabled (an empty box would be confusing). The WhatsApp addon collects
|
|
26
30
|
// the number here; Telegram is a "connect after purchase" flow (a bot
|
|
27
31
|
// can't message a bare phone number cold), so it's just a checkbox.
|
|
28
|
-
const Addons = ({ data, currency, whatsapp, whatsappNumber, telegram, t, onChangeWhatsapp, onChangeWhatsappNumber, onChangeTelegram }: Props): (JSX.Element | null) => {
|
|
29
|
-
|
|
32
|
+
const Addons = ({ data, flexOffer, currency, whatsapp, whatsappNumber, telegram, flex, t, onChangeWhatsapp, onChangeWhatsappNumber, onChangeTelegram, onChangeFlex }: Props): (JSX.Element | null) => {
|
|
33
|
+
// Edited: Ferjolt Ozuni - Date: 2026-08-03
|
|
34
|
+
// Flexible Ticket is only on offer where the operators behind the chosen
|
|
35
|
+
// routes have agreed to honour it, which is a per-booking answer the
|
|
36
|
+
// settings payload cannot give - hence the separate offer (see
|
|
37
|
+
// useGetFlex). When it is unavailable nothing renders for it at all,
|
|
38
|
+
// rather than a disabled row: a checkbox that unlocks nothing is worse
|
|
39
|
+
// than no checkbox.
|
|
40
|
+
const hasFlex = flexOffer?.available === true;
|
|
41
|
+
|
|
42
|
+
if (!data || (!data.whatsapp.available && !data.telegram.available && !hasFlex)) {
|
|
30
43
|
return null;
|
|
31
44
|
}
|
|
32
45
|
|
|
@@ -80,6 +93,56 @@ const Addons = ({ data, currency, whatsapp, whatsappNumber, telegram, t, onChang
|
|
|
80
93
|
<Notice>{ t('routes_order.step5.addons.telegram.description') }</Notice>
|
|
81
94
|
</Option>
|
|
82
95
|
) }
|
|
96
|
+
|
|
97
|
+
{ hasFlex && (
|
|
98
|
+
<Option>
|
|
99
|
+
<label>
|
|
100
|
+
<input
|
|
101
|
+
type="checkbox"
|
|
102
|
+
checked={ flex }
|
|
103
|
+
onChange={ (event) => onChangeFlex(event.target.checked) }
|
|
104
|
+
/>
|
|
105
|
+
|
|
106
|
+
<RiCalendarCheckLine />
|
|
107
|
+
|
|
108
|
+
{ t('routes_order.step5.flex.add', { price: flexOffer?.price_display }) }
|
|
109
|
+
</label>
|
|
110
|
+
|
|
111
|
+
{/*
|
|
112
|
+
* The default is stated FIRST and plainly. The passenger is being
|
|
113
|
+
* asked to pay for flexibility, so what they get without paying
|
|
114
|
+
* has to be the thing they read before the price, not a
|
|
115
|
+
* disclosure buried under it.
|
|
116
|
+
*/}
|
|
117
|
+
<Notice>{ t('routes_order.step5.flex.default') }</Notice>
|
|
118
|
+
|
|
119
|
+
<Terms>
|
|
120
|
+
{ flexOffer?.refund_hours != null && (
|
|
121
|
+
<Term>{ t('routes_order.step5.flex.refund', { hours: flexOffer.refund_hours }) }</Term>
|
|
122
|
+
) }
|
|
123
|
+
|
|
124
|
+
{/*
|
|
125
|
+
* Either half can be absent on its own. An operator who will
|
|
126
|
+
* move a booking but not refund it is a real case, and listing
|
|
127
|
+
* only what this booking actually includes is the difference
|
|
128
|
+
* between an offer and a misrepresentation.
|
|
129
|
+
*/}
|
|
130
|
+
{ flexOffer?.reschedule_hours != null && (
|
|
131
|
+
<Term>{ t('routes_order.step5.flex.reschedule', { hours: flexOffer.reschedule_hours }) }</Term>
|
|
132
|
+
) }
|
|
133
|
+
</Terms>
|
|
134
|
+
|
|
135
|
+
<Notice>{ t('routes_order.step5.flex.nonrefundable') }</Notice>
|
|
136
|
+
|
|
137
|
+
{/*
|
|
138
|
+
* Statutory rights when the OPERATOR cancels are untouched by
|
|
139
|
+
* this and by anything else sold here. Saying so at the point of
|
|
140
|
+
* sale is both accurate and reassuring; leaving it out would
|
|
141
|
+
* imply the add-on is what grants them.
|
|
142
|
+
*/}
|
|
143
|
+
<Notice>{ t('routes_order.step5.flex.carrier') }</Notice>
|
|
144
|
+
</Option>
|
|
145
|
+
) }
|
|
83
146
|
</Container>
|
|
84
147
|
);
|
|
85
148
|
};
|
package/Step5/Addons/styles.ts
CHANGED
|
@@ -30,3 +30,23 @@ export const Notice = styled.div`
|
|
|
30
30
|
font-size: ${ props => props.theme.size.xs };
|
|
31
31
|
color: ${ props => props.theme.font.faded };
|
|
32
32
|
`;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* What the add-on actually unlocks, as a short list.
|
|
36
|
+
*
|
|
37
|
+
* Edited: Ferjolt Ozuni - Date: 2026-08-03
|
|
38
|
+
* A list rather than a sentence because the two windows can differ, and
|
|
39
|
+
* because either can be absent - prose would have to be rewritten for each
|
|
40
|
+
* combination, a list simply gets shorter.
|
|
41
|
+
*/
|
|
42
|
+
export const Terms = styled.ul`
|
|
43
|
+
margin: 8px 0 10px;
|
|
44
|
+
padding: 0 0 0 18px;
|
|
45
|
+
list-style: disc;
|
|
46
|
+
`;
|
|
47
|
+
|
|
48
|
+
export const Term = styled.li`
|
|
49
|
+
margin: 2px 0;
|
|
50
|
+
font-size: 14px;
|
|
51
|
+
line-height: 1.5;
|
|
52
|
+
`;
|
package/package.json
CHANGED
package/services.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useMutation, UseMutationResult, useQuery, UseQueryResult } from '@tanstack/react-query';
|
|
2
2
|
import { apiClient } from '@autobusal/providers';
|
|
3
3
|
import { RoutesSearchForm } from '@autobusal/routes-search/types';
|
|
4
|
-
import { FoundData } from '@autobusal/providers/types/routes';
|
|
4
|
+
import { FoundData, FlexOffer } from '@autobusal/providers/types/routes';
|
|
5
5
|
import { CouponData } from '@autobusal/providers/types/orders';
|
|
6
6
|
import { BillingData, PersonData } from '@autobusal/providers/types/persons';
|
|
7
7
|
import { FoundDay, ActionData, SaveData, OrderedData, CouponForm, AddonsSelection, AlternativesData } from './types';
|
|
@@ -156,6 +156,10 @@ export const usePostOrder = (
|
|
|
156
156
|
order.addon_telegram = '1';
|
|
157
157
|
}
|
|
158
158
|
|
|
159
|
+
if (addons.flex) {
|
|
160
|
+
order.addon_flex = '1';
|
|
161
|
+
}
|
|
162
|
+
|
|
159
163
|
Object.keys(step4).forEach(index => {
|
|
160
164
|
order[index] = step4[index];
|
|
161
165
|
});
|
|
@@ -207,4 +211,44 @@ export const useGetPayments = (): UseQueryResult<ActionData> => (
|
|
|
207
211
|
))
|
|
208
212
|
)
|
|
209
213
|
})
|
|
210
|
-
);
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* The Flexible Ticket offer for the routes actually chosen.
|
|
218
|
+
*
|
|
219
|
+
* Edited: Ferjolt Ozuni - Date: 2026-08-03
|
|
220
|
+
*
|
|
221
|
+
* Its own request rather than a field on the search result, because the
|
|
222
|
+
* answer depends on BOTH legs of a return trip at once: an operator who will
|
|
223
|
+
* not amend one half removes the add-on from the whole booking, and the
|
|
224
|
+
* price is computed on the combined fare. A per-route field could not say
|
|
225
|
+
* either of those things.
|
|
226
|
+
*
|
|
227
|
+
* Disabled until there is a departure price to ask about, so it never fires
|
|
228
|
+
* on a half-built booking.
|
|
229
|
+
*/
|
|
230
|
+
export const useGetFlex = (
|
|
231
|
+
departurePriceId: (number | undefined),
|
|
232
|
+
returnPriceId: (number | undefined),
|
|
233
|
+
adults: number,
|
|
234
|
+
children: number,
|
|
235
|
+
babies: number
|
|
236
|
+
): UseQueryResult<FlexOffer> => (
|
|
237
|
+
useQuery({
|
|
238
|
+
queryKey: ['flex-offer', { departurePriceId, returnPriceId, adults, children, babies }],
|
|
239
|
+
enabled: departurePriceId !== undefined,
|
|
240
|
+
queryFn: async () => (
|
|
241
|
+
await apiClient
|
|
242
|
+
.get('/api/orders/flex', {
|
|
243
|
+
params: {
|
|
244
|
+
departure_price_id: departurePriceId,
|
|
245
|
+
return_price_id: returnPriceId,
|
|
246
|
+
adults,
|
|
247
|
+
children,
|
|
248
|
+
babies
|
|
249
|
+
}
|
|
250
|
+
})
|
|
251
|
+
.then(response => response.data)
|
|
252
|
+
)
|
|
253
|
+
})
|
|
254
|
+
);
|
package/types.ts
CHANGED
|
@@ -71,4 +71,10 @@ export interface AddonsSelection {
|
|
|
71
71
|
whatsapp: boolean
|
|
72
72
|
whatsappNumber: string
|
|
73
73
|
telegram: boolean
|
|
74
|
+
|
|
75
|
+
// Edited: Ferjolt Ozuni - Date: 2026-08-03
|
|
76
|
+
// Flexible Ticket. A bare boolean, like Telegram - the window and the
|
|
77
|
+
// price are the server's to decide (Libraries\Orders\Flex), and a client
|
|
78
|
+
// that sent either would be ignored.
|
|
79
|
+
flex: boolean
|
|
74
80
|
}
|