@autobusal/routes-order 1.5.0 → 1.6.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 +32 -0
- package/Found/Route/Route.tsx +25 -12
- package/Step4/Step4.tsx +7 -2
- package/Step5/Addons/Addons.tsx +87 -0
- package/Step5/Addons/styles.ts +32 -0
- package/Step5/Coupons/Coupons.tsx +6 -1
- package/Step5/Step5.tsx +56 -2
- package/Step5/Summary/Amount/Amount.tsx +4 -3
- package/Step5/Summary/Amount/styles.ts +1 -1
- package/Step5/Summary/Summary.tsx +11 -1
- package/package.json +1 -1
- package/services.ts +15 -2
- package/types.ts +8 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,38 @@
|
|
|
3
3
|
All notable changes to this package are documented here. This project follows
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/) and [Semantic Versioning](https://semver.org/).
|
|
5
5
|
|
|
6
|
+
## [1.6.1] - 2026-07-27
|
|
7
|
+
|
|
8
|
+
### Added
|
|
9
|
+
|
|
10
|
+
- **External-provider "Book on {provider}" redirect (Mode A: search +
|
|
11
|
+
redirect).** `Found/Route/Route.tsx` now detects `data.external?.mode ===
|
|
12
|
+
'redirect'` (obtapi's `Libraries\External\Search\Merge`) and swaps the
|
|
13
|
+
usual "Select Route" button for a `t('routes_order.step2.route.book_on')`
|
|
14
|
+
button that opens the click-out endpoint
|
|
15
|
+
(`{VITE_API_OBT}/api/external/go/{offer_id}`) in a new tab instead of
|
|
16
|
+
calling `onSave` - external offers use a synthetic id the local checkout
|
|
17
|
+
pipeline can't resolve, so "select" is never wired for them. The operator
|
|
18
|
+
logo and route-code links are also repointed to the same click-out URL
|
|
19
|
+
(not the raw provider deep link) so every outbound path is logged for the
|
|
20
|
+
search->click funnel, and the local-only "Read Operator Policy" button is
|
|
21
|
+
hidden for external rows (no local policy content exists for them).
|
|
22
|
+
Requires `@autobusal/providers` ^1.3.7.
|
|
23
|
+
|
|
24
|
+
## [1.6.0] - 2026-07-25
|
|
25
|
+
|
|
26
|
+
### Added
|
|
27
|
+
|
|
28
|
+
- **Checkout addon selection (Step5).** Buyers can now add WhatsApp and/or
|
|
29
|
+
Telegram notification addons at checkout, if the label has them enabled
|
|
30
|
+
(`SettingsData.addons` from `@autobusal/providers`). WhatsApp collects a
|
|
31
|
+
phone number inline; Telegram is a "connect after purchase" flow (a bot
|
|
32
|
+
can't message a bare phone number cold). Selected addons render as line
|
|
33
|
+
items in the order summary and are folded into the grand total shown
|
|
34
|
+
before payment - pricing/availability is re-verified server-side, this is
|
|
35
|
+
only what the buyer requests. `usePostOrder` now takes an `AddonsSelection`
|
|
36
|
+
and posts `addon_whatsapp`/`addon_whatsapp_number`/`addon_telegram`.
|
|
37
|
+
|
|
6
38
|
## [1.5.0] - 2026-07-25
|
|
7
39
|
|
|
8
40
|
### Added
|
package/Found/Route/Route.tsx
CHANGED
|
@@ -27,6 +27,17 @@ const Route = ({ data, type, t, onSave }: Props): JSX.Element => {
|
|
|
27
27
|
<RouteFeature key={ index } item={ item } />
|
|
28
28
|
));
|
|
29
29
|
|
|
30
|
+
// Edited: Ferjolt Ozuni - Date: 2026-07-27
|
|
31
|
+
// Mode A (search + redirect): this row is an external-provider offer,
|
|
32
|
+
// not a local route - "select" would try to check out with a synthetic
|
|
33
|
+
// "ext:..." id, which the local pipeline can't resolve. Every outbound
|
|
34
|
+
// click goes through the click-out endpoint (not the raw deep link
|
|
35
|
+
// directly) so it's logged for the search->click funnel.
|
|
36
|
+
const isExternalRedirect = data.external?.mode === 'redirect';
|
|
37
|
+
const bookOnUrl = data.external
|
|
38
|
+
? `${ import.meta.env.VITE_API_OBT }/api/external/go/${ data.external.offer_id }`
|
|
39
|
+
: undefined;
|
|
40
|
+
|
|
30
41
|
return (
|
|
31
42
|
<Container className="box" $type={ type }>
|
|
32
43
|
{ data.price.offer === true && <SpecialOffer>{ t('routes_order.step2.route.offer') }</SpecialOffer> }
|
|
@@ -34,7 +45,7 @@ const Route = ({ data, type, t, onSave }: Props): JSX.Element => {
|
|
|
34
45
|
|
|
35
46
|
<Trip>
|
|
36
47
|
<Company>
|
|
37
|
-
<LinkCompany to={ data.operator.link } target="_blank" rel="noopener noreferrer" title={ data.operator.company }><img src={ data.operator.logo_url || LOGO_FALLBACK } onError={ (e) => { e.currentTarget.onerror = null; e.currentTarget.src = LOGO_FALLBACK; } } alt={ data.operator.company } /></LinkCompany>
|
|
48
|
+
<LinkCompany to={ bookOnUrl ?? data.operator.link } target="_blank" rel="noopener noreferrer" title={ data.operator.company }><img src={ data.operator.logo_url || LOGO_FALLBACK } onError={ (e) => { e.currentTarget.onerror = null; e.currentTarget.src = LOGO_FALLBACK; } } alt={ data.operator.company } /></LinkCompany>
|
|
38
49
|
</Company>
|
|
39
50
|
|
|
40
51
|
<Location>
|
|
@@ -62,10 +73,10 @@ const Route = ({ data, type, t, onSave }: Props): JSX.Element => {
|
|
|
62
73
|
text={
|
|
63
74
|
<>
|
|
64
75
|
<RiBus2Line />
|
|
65
|
-
{ t('routes_order.step2.route.select') }
|
|
76
|
+
{ isExternalRedirect ? t('routes_order.step2.route.book_on', { provider: data.operator.company }) : t('routes_order.step2.route.select') }
|
|
66
77
|
</>
|
|
67
78
|
}
|
|
68
|
-
onClick={ () => onSave(data) }
|
|
79
|
+
onClick={ () => (isExternalRedirect && bookOnUrl ? window.open(bookOnUrl, '_blank', 'noopener,noreferrer') : onSave(data)) }
|
|
69
80
|
/>
|
|
70
81
|
</Price>
|
|
71
82
|
</Trip>
|
|
@@ -73,7 +84,7 @@ const Route = ({ data, type, t, onSave }: Props): JSX.Element => {
|
|
|
73
84
|
<Details>
|
|
74
85
|
<Detail>
|
|
75
86
|
<TitleDetail>{ t('routes_order.step2.route.name') }</TitleDetail>
|
|
76
|
-
<LinkRoute to={ data.link } target="_blank" rel="noopener noreferrer">{ data.code }</LinkRoute>
|
|
87
|
+
<LinkRoute to={ bookOnUrl ?? data.link } target="_blank" rel="noopener noreferrer">{ data.code }</LinkRoute>
|
|
77
88
|
</Detail>
|
|
78
89
|
|
|
79
90
|
{ customs && (
|
|
@@ -90,14 +101,16 @@ const Route = ({ data, type, t, onSave }: Props): JSX.Element => {
|
|
|
90
101
|
</Detail>
|
|
91
102
|
) }
|
|
92
103
|
|
|
93
|
-
|
|
94
|
-
<
|
|
104
|
+
{ !data.external && (
|
|
105
|
+
<Detail>
|
|
106
|
+
<TitleDetail>{ t('routes_order.step2.route.policies.title') }</TitleDetail>
|
|
95
107
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
108
|
+
<ButtonPolicy type="button" onClick={ () => setPolicy(true) }>
|
|
109
|
+
<HiOutlineDocumentText />
|
|
110
|
+
{ t('routes_order.step2.route.policies.view') }
|
|
111
|
+
</ButtonPolicy>
|
|
112
|
+
</Detail>
|
|
113
|
+
) }
|
|
101
114
|
</Details>
|
|
102
115
|
|
|
103
116
|
{ data.trip.features_data.length > 0 && (
|
|
@@ -110,7 +123,7 @@ const Route = ({ data, type, t, onSave }: Props): JSX.Element => {
|
|
|
110
123
|
</Features>
|
|
111
124
|
) }
|
|
112
125
|
|
|
113
|
-
{ policy && <Policy id={ data.id } t={ t } onClose={ () => setPolicy(false) } /> }
|
|
126
|
+
{ policy && typeof data.id === 'number' && <Policy id={ data.id } t={ t } onClose={ () => setPolicy(false) } /> }
|
|
114
127
|
</Container>
|
|
115
128
|
);
|
|
116
129
|
};
|
package/Step4/Step4.tsx
CHANGED
|
@@ -40,8 +40,13 @@ const Step4 = ({ values, step1, step2, step3, isPartner, t, onSave, onBack }: Pr
|
|
|
40
40
|
...(step3 !== undefined ? (step3.trip?.passenger_fields ?? DEFAULT_FIELDS) : [])
|
|
41
41
|
]));
|
|
42
42
|
|
|
43
|
-
|
|
44
|
-
|
|
43
|
+
// Edited: Ferjolt Ozuni - Date: 2026-07-27
|
|
44
|
+
// FoundData.id is widened to (number | string) for external-provider
|
|
45
|
+
// offers (Mode A: search + redirect), but those never reach Step4 -
|
|
46
|
+
// Found/Route.tsx's CTA opens the provider's click-out link for them
|
|
47
|
+
// instead of calling onSave, so a real local numeric id is guaranteed here.
|
|
48
|
+
const busFrom = useBusOccupied(step1.departure, step2.id as number);
|
|
49
|
+
const busTo = useBusOccupied(step1._return, step3?.id as (number | undefined));
|
|
45
50
|
|
|
46
51
|
if (busFrom === undefined || busTo === undefined) {
|
|
47
52
|
return <Loading t={ t } />;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { TFunction } from 'i18next';
|
|
2
|
+
import { RiWhatsappLine, RiTelegramLine } from 'react-icons/ri';
|
|
3
|
+
import { SubTitle } from '../../styles';
|
|
4
|
+
import { Container, Option, Notice } from './styles';
|
|
5
|
+
|
|
6
|
+
interface AddonsData {
|
|
7
|
+
whatsapp: { available: boolean, price: number }
|
|
8
|
+
telegram: { available: boolean, price: number }
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface Props {
|
|
12
|
+
data?: AddonsData
|
|
13
|
+
currency: string
|
|
14
|
+
whatsapp: boolean
|
|
15
|
+
whatsappNumber: string
|
|
16
|
+
telegram: boolean
|
|
17
|
+
t: TFunction<'common'>
|
|
18
|
+
onChangeWhatsapp: (value: boolean) => void
|
|
19
|
+
onChangeWhatsappNumber: (value: string) => void
|
|
20
|
+
onChangeTelegram: (value: boolean) => void
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Edited: Ferjolt Ozuni - Date: 2026-07-25
|
|
24
|
+
// Paid checkout addons - hidden entirely if the label has neither channel
|
|
25
|
+
// enabled (an empty box would be confusing). The WhatsApp addon collects
|
|
26
|
+
// the number here; Telegram is a "connect after purchase" flow (a bot
|
|
27
|
+
// 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
|
+
if (!data || (!data.whatsapp.available && !data.telegram.available)) {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<Container className="box">
|
|
35
|
+
<SubTitle>
|
|
36
|
+
{ t('routes_order.step5.addons.title') }
|
|
37
|
+
</SubTitle>
|
|
38
|
+
|
|
39
|
+
{ data.whatsapp.available && (
|
|
40
|
+
<Option>
|
|
41
|
+
<label>
|
|
42
|
+
<input
|
|
43
|
+
type="checkbox"
|
|
44
|
+
checked={ whatsapp }
|
|
45
|
+
onChange={ (event) => onChangeWhatsapp(event.target.checked) }
|
|
46
|
+
/>
|
|
47
|
+
|
|
48
|
+
<RiWhatsappLine />
|
|
49
|
+
|
|
50
|
+
{ t('routes_order.step5.addons.whatsapp.label') } (+{ data.whatsapp.price } { currency })
|
|
51
|
+
</label>
|
|
52
|
+
|
|
53
|
+
<Notice>{ t('routes_order.step5.addons.whatsapp.description') }</Notice>
|
|
54
|
+
|
|
55
|
+
{ whatsapp && (
|
|
56
|
+
<input
|
|
57
|
+
type="text"
|
|
58
|
+
placeholder={ t('routes_order.step5.addons.whatsapp.phone_placeholder') }
|
|
59
|
+
value={ whatsappNumber }
|
|
60
|
+
onChange={ (event) => onChangeWhatsappNumber(event.target.value) }
|
|
61
|
+
/>
|
|
62
|
+
) }
|
|
63
|
+
</Option>
|
|
64
|
+
) }
|
|
65
|
+
|
|
66
|
+
{ data.telegram.available && (
|
|
67
|
+
<Option>
|
|
68
|
+
<label>
|
|
69
|
+
<input
|
|
70
|
+
type="checkbox"
|
|
71
|
+
checked={ telegram }
|
|
72
|
+
onChange={ (event) => onChangeTelegram(event.target.checked) }
|
|
73
|
+
/>
|
|
74
|
+
|
|
75
|
+
<RiTelegramLine />
|
|
76
|
+
|
|
77
|
+
{ t('routes_order.step5.addons.telegram.label') } (+{ data.telegram.price } { currency })
|
|
78
|
+
</label>
|
|
79
|
+
|
|
80
|
+
<Notice>{ t('routes_order.step5.addons.telegram.description') }</Notice>
|
|
81
|
+
</Option>
|
|
82
|
+
) }
|
|
83
|
+
</Container>
|
|
84
|
+
);
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
export default Addons;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import styled from 'styled-components';
|
|
2
|
+
|
|
3
|
+
export const Container = styled.div`
|
|
4
|
+
margin-top: 25px;
|
|
5
|
+
`;
|
|
6
|
+
|
|
7
|
+
export const Option = styled.div`
|
|
8
|
+
margin-top: 15px;
|
|
9
|
+
|
|
10
|
+
&:first-child {
|
|
11
|
+
margin-top: 0;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
label {
|
|
15
|
+
display: flex;
|
|
16
|
+
align-items: center;
|
|
17
|
+
gap: 6px;
|
|
18
|
+
cursor: pointer;
|
|
19
|
+
font-weight: 700;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
input[type="text"] {
|
|
23
|
+
margin-top: 10px;
|
|
24
|
+
}
|
|
25
|
+
`;
|
|
26
|
+
|
|
27
|
+
export const Notice = styled.div`
|
|
28
|
+
margin-top: 4px;
|
|
29
|
+
margin-left: 22px;
|
|
30
|
+
font-size: ${ props => props.theme.size.xs };
|
|
31
|
+
color: ${ props => props.theme.font.faded };
|
|
32
|
+
`;
|
|
@@ -21,7 +21,12 @@ interface Props {
|
|
|
21
21
|
const Coupons = ({ current, step2, t, total, onApplied }: Props): (JSX.Element | null) => {
|
|
22
22
|
const { register, handleSubmit, formState: { errors } } = useForm<CouponForm>();
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
// Edited: Ferjolt Ozuni - Date: 2026-07-27
|
|
25
|
+
// step2.id is only a synthetic string for external-provider offers
|
|
26
|
+
// (Mode A: search + redirect), and those never reach Step5/checkout -
|
|
27
|
+
// Found/Route.tsx's CTA redirects to the provider instead of calling
|
|
28
|
+
// onSave, so a real local numeric id is guaranteed here.
|
|
29
|
+
const { mutate: SearchCoupons, isPending } = useGetCoupon(step2.id as number, total.value);
|
|
25
30
|
|
|
26
31
|
const onSubmit = (data: CouponForm): void => {
|
|
27
32
|
SearchCoupons(data, {
|
package/Step5/Step5.tsx
CHANGED
|
@@ -9,6 +9,7 @@ import { Button } from '@autobusal/common';
|
|
|
9
9
|
import Steps from '../Steps/Steps';
|
|
10
10
|
import Summary from './Summary/Summary';
|
|
11
11
|
import Coupons from './Coupons/Coupons';
|
|
12
|
+
import Addons from './Addons/Addons';
|
|
12
13
|
import Billing from './Billing/Billing';
|
|
13
14
|
import Payments from './Payments/Payments';
|
|
14
15
|
import { getTotal, convertCoupon } from './utilities';
|
|
@@ -19,6 +20,7 @@ import { CouponData } from '@autobusal/providers/types/orders';
|
|
|
19
20
|
import { RoutesSearchForm } from '@autobusal/routes-search/types';
|
|
20
21
|
import { TotalData } from '../types';
|
|
21
22
|
import { useUserStore } from '@autobusal/providers/stores/user';
|
|
23
|
+
import { useGetSettings } from '@autobusal/providers/services';
|
|
22
24
|
import { usePostOrder } from '../services';
|
|
23
25
|
|
|
24
26
|
interface Props {
|
|
@@ -38,13 +40,46 @@ const Step5 = ({ step1, step2, step3, step4, isPartner, t, onBack }: Props): JSX
|
|
|
38
40
|
);
|
|
39
41
|
const [ action, setAction ] = useState<string | undefined>(undefined);
|
|
40
42
|
|
|
43
|
+
const [ addonWhatsapp, setAddonWhatsapp ] = useState<boolean>(false);
|
|
44
|
+
const [ addonWhatsappNumber, setAddonWhatsappNumber ] = useState<string>('');
|
|
45
|
+
const [ addonTelegram, setAddonTelegram ] = useState<boolean>(false);
|
|
46
|
+
|
|
41
47
|
const { data: UserData } = useUserStore();
|
|
48
|
+
const { data: SettingsData } = useGetSettings();
|
|
42
49
|
|
|
43
50
|
const navigate = useNavigate();
|
|
44
51
|
|
|
45
52
|
const { register, handleSubmit, formState: { errors } } = useForm<BillingData>();
|
|
46
53
|
|
|
47
|
-
|
|
54
|
+
// Edited: Ferjolt Ozuni - Date: 2026-07-25
|
|
55
|
+
// Paid addons are priced/verified server-side (see obtapi's
|
|
56
|
+
// App\Libraries\Orders\Make\Addons) - this is only for the checkout
|
|
57
|
+
// summary display and the grand total shown before payment.
|
|
58
|
+
const currency = total.display.split(' ')[1] ?? '';
|
|
59
|
+
|
|
60
|
+
const addonItems = [
|
|
61
|
+
addonWhatsapp && SettingsData?.addons?.whatsapp.available ? {
|
|
62
|
+
label: t('routes_order.step5.addons.whatsapp.label'),
|
|
63
|
+
price: SettingsData.addons.whatsapp.price
|
|
64
|
+
} : null,
|
|
65
|
+
addonTelegram && SettingsData?.addons?.telegram.available ? {
|
|
66
|
+
label: t('routes_order.step5.addons.telegram.label'),
|
|
67
|
+
price: SettingsData.addons.telegram.price
|
|
68
|
+
} : null
|
|
69
|
+
].filter((item): item is { label: string, price: number } => item !== null);
|
|
70
|
+
|
|
71
|
+
const addonsValue = addonItems.reduce((sum, item) => sum + item.price, 0);
|
|
72
|
+
|
|
73
|
+
const grandTotal: TotalData = {
|
|
74
|
+
display: `${ total.value + addonsValue } ${ currency }`,
|
|
75
|
+
value: total.value + addonsValue
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const { mutate: OrderSend, isPending } = usePostOrder(step1, step2, step3, step4, coupon, action, {
|
|
79
|
+
whatsapp: addonWhatsapp,
|
|
80
|
+
whatsappNumber: addonWhatsappNumber,
|
|
81
|
+
telegram: addonTelegram
|
|
82
|
+
});
|
|
48
83
|
|
|
49
84
|
const onApplyCoupon = (data: CouponData): void => {
|
|
50
85
|
setTotal(
|
|
@@ -59,6 +94,12 @@ const Step5 = ({ step1, step2, step3, step4, isPartner, t, onBack }: Props): JSX
|
|
|
59
94
|
}, []);
|
|
60
95
|
|
|
61
96
|
const onSendForm = async (): Promise<void> => {
|
|
97
|
+
if (addonWhatsapp && addonWhatsappNumber.trim().length < 5) {
|
|
98
|
+
alert(t('routes_order.step5.addons.whatsapp.phone_error'));
|
|
99
|
+
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
62
103
|
await handleSubmit(onSubmit)();
|
|
63
104
|
};
|
|
64
105
|
|
|
@@ -91,12 +132,25 @@ const Step5 = ({ step1, step2, step3, step4, isPartner, t, onBack }: Props): JSX
|
|
|
91
132
|
step3={ step3 }
|
|
92
133
|
step4={ step4 }
|
|
93
134
|
coupon={ coupon }
|
|
94
|
-
|
|
135
|
+
addons={ addonItems.map(item => ({ label: item.label, value: `${ item.price } ${ currency }` })) }
|
|
136
|
+
total={ grandTotal }
|
|
95
137
|
t={ t }
|
|
96
138
|
/>
|
|
97
139
|
|
|
98
140
|
<Coupons current={ coupon } step2={ step2 } total={ total } t={ t } onApplied={ onApplyCoupon } />
|
|
99
141
|
|
|
142
|
+
<Addons
|
|
143
|
+
data={ SettingsData?.addons }
|
|
144
|
+
currency={ currency }
|
|
145
|
+
whatsapp={ addonWhatsapp }
|
|
146
|
+
whatsappNumber={ addonWhatsappNumber }
|
|
147
|
+
telegram={ addonTelegram }
|
|
148
|
+
t={ t }
|
|
149
|
+
onChangeWhatsapp={ setAddonWhatsapp }
|
|
150
|
+
onChangeWhatsappNumber={ setAddonWhatsappNumber }
|
|
151
|
+
onChangeTelegram={ setAddonTelegram }
|
|
152
|
+
/>
|
|
153
|
+
|
|
100
154
|
{ UserData === undefined && <Billing t={ t } errors={ errors } refs={ register } /> }
|
|
101
155
|
|
|
102
156
|
<Payments
|
|
@@ -2,15 +2,16 @@ import { TFunction } from 'i18next';
|
|
|
2
2
|
import { Container, Value, Code } from './styles';
|
|
3
3
|
|
|
4
4
|
interface Props {
|
|
5
|
-
type: 'total' | 'discount'
|
|
5
|
+
type: 'total' | 'discount' | 'addon'
|
|
6
6
|
value: string
|
|
7
7
|
code?: string
|
|
8
|
+
label?: string
|
|
8
9
|
t: TFunction<'common'>
|
|
9
10
|
}
|
|
10
11
|
|
|
11
|
-
const Amount = ({ type, value, code, t }: Props): JSX.Element => (
|
|
12
|
+
const Amount = ({ type, value, code, label, t }: Props): JSX.Element => (
|
|
12
13
|
<Container $type={ type }>
|
|
13
|
-
{ t(`routes_order.step5.summary.${ type }`) }
|
|
14
|
+
{ label ?? t(`routes_order.step5.summary.${ type }`) }
|
|
14
15
|
|
|
15
16
|
{ code && <Code>{ code }</Code> }
|
|
16
17
|
|
|
@@ -11,17 +11,23 @@ import { PersonData } from '@autobusal/providers/types/persons';
|
|
|
11
11
|
import { CouponData } from '@autobusal/providers/types/orders';
|
|
12
12
|
import { TotalData } from '../../types';
|
|
13
13
|
|
|
14
|
+
interface AddonItem {
|
|
15
|
+
label: string
|
|
16
|
+
value: string
|
|
17
|
+
}
|
|
18
|
+
|
|
14
19
|
interface Props {
|
|
15
20
|
step1: RoutesSearchForm
|
|
16
21
|
step2: FoundData
|
|
17
22
|
step3?: FoundData
|
|
18
23
|
step4: PersonData
|
|
19
24
|
coupon?: CouponData
|
|
25
|
+
addons?: AddonItem[]
|
|
20
26
|
total: TotalData
|
|
21
27
|
t: TFunction<'common'>
|
|
22
28
|
}
|
|
23
29
|
|
|
24
|
-
const Summary = ({ step1, step2, step3, step4, coupon, total, t }: Props): JSX.Element => (
|
|
30
|
+
const Summary = ({ step1, step2, step3, step4, coupon, addons, total, t }: Props): JSX.Element => (
|
|
25
31
|
<div className="box">
|
|
26
32
|
<SubTitle>
|
|
27
33
|
<AiOutlineInfoCircle />
|
|
@@ -35,6 +41,10 @@ const Summary = ({ step1, step2, step3, step4, coupon, total, t }: Props): JSX.E
|
|
|
35
41
|
|
|
36
42
|
<Tickets step1={ step1 } step4={ step4 } t={ t } />
|
|
37
43
|
|
|
44
|
+
{ addons?.map(item => (
|
|
45
|
+
<Amount key={ item.label } type="addon" label={ item.label } value={ item.value } t={ t } />
|
|
46
|
+
)) }
|
|
47
|
+
|
|
38
48
|
{ coupon && <Amount type="discount" code={ coupon.code } value={ coupon.discount } t={ t } /> }
|
|
39
49
|
|
|
40
50
|
<Amount type="total" value={ total.display } t={ t } />
|
package/package.json
CHANGED
package/services.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { RoutesSearchForm } from '@autobusal/routes-search/types';
|
|
|
4
4
|
import { FoundData } 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
|
-
import { FoundDay, ActionData, SaveData, OrderedData, CouponForm } from './types';
|
|
7
|
+
import { FoundDay, ActionData, SaveData, OrderedData, CouponForm, AddonsSelection } from './types';
|
|
8
8
|
|
|
9
9
|
export const useGetDepartures = (data: RoutesSearchForm): UseQueryResult<FoundData[]> => (
|
|
10
10
|
useQuery({
|
|
@@ -74,7 +74,8 @@ export const usePostOrder = (
|
|
|
74
74
|
step3: (FoundData | undefined),
|
|
75
75
|
step4: PersonData,
|
|
76
76
|
coupon: (CouponData | undefined),
|
|
77
|
-
action: (string | undefined)
|
|
77
|
+
action: (string | undefined),
|
|
78
|
+
addons: AddonsSelection
|
|
78
79
|
): UseMutationResult<OrderedData, Error, BillingData, unknown> => (
|
|
79
80
|
useMutation({
|
|
80
81
|
mutationKey: ['save-order'],
|
|
@@ -95,6 +96,18 @@ export const usePostOrder = (
|
|
|
95
96
|
order.coupon = coupon.code;
|
|
96
97
|
}
|
|
97
98
|
|
|
99
|
+
// Edited: Ferjolt Ozuni - Date: 2026-07-25
|
|
100
|
+
// Paid addons - obtapi re-verifies availability/price server-side
|
|
101
|
+
// against the label's own settings, this is just the buyer's request
|
|
102
|
+
if (addons.whatsapp && addons.whatsappNumber) {
|
|
103
|
+
order.addon_whatsapp = '1';
|
|
104
|
+
order.addon_whatsapp_number = addons.whatsappNumber;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (addons.telegram) {
|
|
108
|
+
order.addon_telegram = '1';
|
|
109
|
+
}
|
|
110
|
+
|
|
98
111
|
Object.keys(step4).forEach(index => {
|
|
99
112
|
order[index] = step4[index];
|
|
100
113
|
});
|
package/types.ts
CHANGED
|
@@ -33,4 +33,12 @@ export interface OrderedData {
|
|
|
33
33
|
export interface TotalData {
|
|
34
34
|
display: string
|
|
35
35
|
value: number
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Edited: Ferjolt Ozuni - Date: 2026-07-25
|
|
39
|
+
// The buyer's paid-addon selection at checkout - see Step5/Addons/Addons.tsx
|
|
40
|
+
export interface AddonsSelection {
|
|
41
|
+
whatsapp: boolean
|
|
42
|
+
whatsappNumber: string
|
|
43
|
+
telegram: boolean
|
|
36
44
|
}
|