@autobusal/routes-order 1.24.0 → 1.26.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 +15 -0
- package/Checkout/Checkout.tsx +41 -2
- package/Found/Found.tsx +14 -1
- package/RoutesOrder.tsx +5 -0
- package/Sections/Sections.tsx +21 -0
- package/commerce.ts +24 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.26.0
|
|
4
|
+
|
|
5
|
+
**Funnel events on every step**: the result list, the coach chosen, checkout reached, the Flexible Ticket ticked or unticked, and the value stashed for the purchase event.
|
|
6
|
+
|
|
7
|
+
- `view_item_list` is keyed on the **result set**, not on render — this component re-renders on every filter tick and sort change, and announcing again each time would report a dozen searches for one.
|
|
8
|
+
- `begin_checkout` lives in `Checkout` rather than `RoutesOrder`, because that screen already derives the currency from its own formatted total (the public settings payload deliberately carries none) and it mounts exactly once per arrival.
|
|
9
|
+
- The **add-on attach rate** is the one input the Flexible Ticket pricing decision has been waiting on — it cannot be banded by window without knowing how often each window is actually bought.
|
|
10
|
+
- One `item()` mapping shared by every event, keyed on the **route code** rather than the numeric id, so a report can be reconciled against something a human recognises.
|
|
11
|
+
|
|
12
|
+
## 1.25.0
|
|
13
|
+
|
|
14
|
+
**Reviews on the route-pair page.** A new section between the timetable and the internal links, with its own nav anchor.
|
|
15
|
+
|
|
16
|
+
Renders nothing below the display threshold, so a pair without enough reviews does not gain an empty box.
|
|
17
|
+
|
|
3
18
|
## 1.24.0
|
|
4
19
|
|
|
5
20
|
**The booking wizard has real URLs.** The step now lives in the URL and `current` is derived from it, rather than the reverse.
|
package/Checkout/Checkout.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useCallback, useState } from 'react';
|
|
1
|
+
import { useCallback, useState, useEffect } from 'react';
|
|
2
2
|
import { TFunction } from 'i18next';
|
|
3
3
|
import { useForm } from 'react-hook-form';
|
|
4
4
|
import { useNavigate } from 'react-router-dom';
|
|
@@ -25,6 +25,8 @@ import { TotalData } from '../types';
|
|
|
25
25
|
import { useUserStore } from '@autobusal/providers/stores/user';
|
|
26
26
|
import { useGetSettings } from '@autobusal/providers/services';
|
|
27
27
|
import { usePostOrder, useGetFlex } from '../services';
|
|
28
|
+
import { addon as commerceAddon, beginCheckout, stash } from '@autobusal/providers/Setup/commerce';
|
|
29
|
+
import { item as commerceItem } from '../commerce';
|
|
28
30
|
|
|
29
31
|
interface Props {
|
|
30
32
|
values?: PersonData
|
|
@@ -129,6 +131,21 @@ const Checkout = ({ values, step1, step2, step3, isPartner, t, onStore, onBack }
|
|
|
129
131
|
value: total.value + addonsValue
|
|
130
132
|
};
|
|
131
133
|
|
|
134
|
+
/**
|
|
135
|
+
* Edited: Ferjolt Ozuni - Date: 2026-08-05
|
|
136
|
+
*
|
|
137
|
+
* Here rather than in RoutesOrder because this screen already derives the
|
|
138
|
+
* currency from its own formatted total - the public settings payload
|
|
139
|
+
* carries none - and because it mounts exactly once per arrival at
|
|
140
|
+
* checkout, which is the cadence this event wants.
|
|
141
|
+
*/
|
|
142
|
+
useEffect(() => {
|
|
143
|
+
const items = [commerceItem(step2), ...(step3 ? [commerceItem(step3)] : [])];
|
|
144
|
+
|
|
145
|
+
beginCheckout(total.value, items, currency);
|
|
146
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
147
|
+
}, []);
|
|
148
|
+
|
|
132
149
|
const { mutate: OrderSend, isPending } = usePostOrder(step1, step2, step3, coupon, action, {
|
|
133
150
|
whatsapp: addonWhatsapp,
|
|
134
151
|
whatsappNumber: addonWhatsappNumber,
|
|
@@ -200,6 +217,21 @@ const Checkout = ({ values, step1, step2, step3, isPartner, t, onStore, onBack }
|
|
|
200
217
|
|
|
201
218
|
const hash = step1.type === 'return' ? data.return : data.departure;
|
|
202
219
|
|
|
220
|
+
// Edited: Ferjolt Ozuni - Date: 2026-08-05
|
|
221
|
+
//
|
|
222
|
+
// Remembered here because the confirmation page cannot work it out.
|
|
223
|
+
// A card payment leaves the site for the bank and returns on a URL
|
|
224
|
+
// carrying nothing but this hash, and most buyers are guests with no
|
|
225
|
+
// account the order could be read back from. Keyed on the SAME hash
|
|
226
|
+
// the confirmation is reached with, so it cannot be attributed to a
|
|
227
|
+
// different order.
|
|
228
|
+
stash(
|
|
229
|
+
String(hash),
|
|
230
|
+
grandTotal.value,
|
|
231
|
+
[commerceItem(step2), ...(step3 ? [commerceItem(step3)] : [])],
|
|
232
|
+
currency
|
|
233
|
+
);
|
|
234
|
+
|
|
203
235
|
navigate(`/orders/process/${ action }/${ hash }`);
|
|
204
236
|
}
|
|
205
237
|
});
|
|
@@ -244,7 +276,14 @@ const Checkout = ({ values, step1, step2, step3, isPartner, t, onStore, onBack }
|
|
|
244
276
|
onChangeWhatsapp={ setAddonWhatsapp }
|
|
245
277
|
onChangeWhatsappNumber={ setAddonWhatsappNumber }
|
|
246
278
|
onChangeTelegram={ setAddonTelegram }
|
|
247
|
-
onChangeFlex={
|
|
279
|
+
onChangeFlex={ (value) => {
|
|
280
|
+
setAddonFlex(value);
|
|
281
|
+
|
|
282
|
+
// The attach rate this records is the one input the Flexible
|
|
283
|
+
// Ticket pricing decision is waiting on - it cannot be banded
|
|
284
|
+
// by window without knowing how often each is actually bought.
|
|
285
|
+
commerceAddon(value, 'Flexible Ticket', FlexData?.price ?? 0, currency);
|
|
286
|
+
} }
|
|
248
287
|
/>
|
|
249
288
|
|
|
250
289
|
{ UserData === undefined && (
|
package/Found/Found.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useMemo, useState } from 'react';
|
|
1
|
+
import { useMemo, useState, useEffect } from 'react';
|
|
2
2
|
import { useSearchParams } from 'react-router-dom';
|
|
3
3
|
import { TFunction } from 'i18next';
|
|
4
4
|
import Dates from './Dates/Dates';
|
|
@@ -13,6 +13,8 @@ import { Results, Sidebar, Listing } from '../styles';
|
|
|
13
13
|
import { useGetAlternatives } from '../services';
|
|
14
14
|
import { RoutesSearchForm } from '@autobusal/routes-search/types';
|
|
15
15
|
import { FoundData } from '@autobusal/providers/types/routes';
|
|
16
|
+
import { viewItemList } from '@autobusal/providers/Setup/commerce';
|
|
17
|
+
import { item as commerceItem } from '../commerce';
|
|
16
18
|
import { Refinement, BucketKey, fromParams, toParams, filterItems, buildFacets, priceRange, currencyOf, isRefined } from './refine';
|
|
17
19
|
|
|
18
20
|
interface Props {
|
|
@@ -31,6 +33,17 @@ interface Props {
|
|
|
31
33
|
const OWNED = [ 'sort', 'operators', 'times', 'direct', 'from_stops', 'to_stops', 'features', 'max_price' ];
|
|
32
34
|
|
|
33
35
|
const Found = ({ type, loading, data, step1, preferredStop, t, onSearch, onSave }: Props): JSX.Element => {
|
|
36
|
+
/**
|
|
37
|
+
* Edited: Ferjolt Ozuni - Date: 2026-08-05
|
|
38
|
+
*
|
|
39
|
+
* Keyed on the RESULT SET, not on every render. This component re-renders
|
|
40
|
+
* on each filter tick and sort change, and announcing the list again every
|
|
41
|
+
* time would report a dozen searches for one.
|
|
42
|
+
*/
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
viewItemList((data ?? []).map(commerceItem), currencyOf(data ?? []));
|
|
45
|
+
}, [data]);
|
|
46
|
+
|
|
34
47
|
const [ searchParams, setSearchParams ] = useSearchParams();
|
|
35
48
|
const [ open, setOpen ] = useState<boolean>(false);
|
|
36
49
|
|
package/RoutesOrder.tsx
CHANGED
|
@@ -5,6 +5,9 @@ import { AiOutlineSearch, AiOutlineCaretDown, AiOutlineCaretUp } from 'react-ico
|
|
|
5
5
|
import { Meta } from '@autobusal/common';
|
|
6
6
|
import { useSystemTheme } from '@autobusal/hooks';
|
|
7
7
|
import { useGetSettings } from '@autobusal/providers/services';
|
|
8
|
+
import { selectItem } from '@autobusal/providers/Setup/commerce';
|
|
9
|
+
import { item as commerceItem } from './commerce';
|
|
10
|
+
import { currencyOf } from './Found/refine';
|
|
8
11
|
import RoutesSearch from '@autobusal/routes-search';
|
|
9
12
|
import { useGetSuggestions } from '@autobusal/routes-search/services';
|
|
10
13
|
import { SearchAgain, SearchInner, SearchToggle, SearchSummary, SearchPanel } from './styles';
|
|
@@ -142,6 +145,8 @@ const RoutesOrder = ({ t }: Props): JSX.Element => {
|
|
|
142
145
|
const onStep2 = (data: FoundData): void => {
|
|
143
146
|
setStep2(data);
|
|
144
147
|
|
|
148
|
+
selectItem(commerceItem(data), currencyOf([data]));
|
|
149
|
+
|
|
145
150
|
goto(step1?.type === 'return' ? 'return' : 'checkout');
|
|
146
151
|
};
|
|
147
152
|
|
package/Sections/Sections.tsx
CHANGED
|
@@ -2,6 +2,7 @@ import { TFunction } from 'i18next';
|
|
|
2
2
|
import Facts from '../Facts/Facts';
|
|
3
3
|
import Schedule from '../Schedule/Schedule';
|
|
4
4
|
import Links from '../Links/Links';
|
|
5
|
+
import { Reviews } from '@autobusal/reviews';
|
|
5
6
|
import Schema from './Schema';
|
|
6
7
|
import { useGetFacts } from '../Facts/services';
|
|
7
8
|
import { Nav, Anchor, Anchored } from './styles';
|
|
@@ -16,6 +17,7 @@ const TRIPS = 'trips';
|
|
|
16
17
|
const FACTS = 'facts';
|
|
17
18
|
const SCHEDULE = 'schedule';
|
|
18
19
|
const LINKS = 'links';
|
|
20
|
+
const REVIEWS = 'reviews';
|
|
19
21
|
|
|
20
22
|
/**
|
|
21
23
|
* Everything on a route-pair page that is not the booking wizard.
|
|
@@ -63,6 +65,8 @@ const Sections = ({ from, to, t }: Props): (JSX.Element | null) => {
|
|
|
63
65
|
{ schedule.length > 0 && (
|
|
64
66
|
<Anchor href={ `#${ SCHEDULE }` }>{ t('routes_order.sections.schedule') }</Anchor>
|
|
65
67
|
) }
|
|
68
|
+
|
|
69
|
+
<Anchor href={ `#${ REVIEWS }` }>{ t('routes_order.sections.reviews') }</Anchor>
|
|
66
70
|
</Nav>
|
|
67
71
|
|
|
68
72
|
{ /* Edited: Ferjolt Ozuni - Date: 2026-08-03
|
|
@@ -78,6 +82,23 @@ const Sections = ({ from, to, t }: Props): (JSX.Element | null) => {
|
|
|
78
82
|
<Schedule id={ SCHEDULE } from={ facts.from } to={ facts.to } rows={ schedule } t={ t } />
|
|
79
83
|
</Anchored>
|
|
80
84
|
|
|
85
|
+
{ /* Edited: Ferjolt Ozuni - Date: 2026-08-03
|
|
86
|
+
Reviews for the PAIR, not for one coach on it. A review belongs to
|
|
87
|
+
a route, but a single route rarely has enough published reviews to
|
|
88
|
+
clear the display threshold - the pair it runs on often does, so
|
|
89
|
+
aggregating across the routes that sell this leg is what makes the
|
|
90
|
+
block appear at all rather than a nicety.
|
|
91
|
+
|
|
92
|
+
Renders nothing below the threshold, so a pair without enough
|
|
93
|
+
reviews does not gain an empty box. */ }
|
|
94
|
+
<Anchored>
|
|
95
|
+
{ /* the anchor lives on a wrapper: Reviews takes the SUBJECT's id,
|
|
96
|
+
which a pair does not have - it is named by its two ends */ }
|
|
97
|
+
<div id={ REVIEWS }>
|
|
98
|
+
<Reviews type="pair" id={ 0 } pair={ { from: from ?? '', to: to ?? '' } } t={ t } />
|
|
99
|
+
</div>
|
|
100
|
+
</Anchored>
|
|
101
|
+
|
|
81
102
|
<Links
|
|
82
103
|
id={ LINKS }
|
|
83
104
|
from={ links.from }
|
package/commerce.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { CommerceItem } from '@autobusal/providers/Setup/commerce';
|
|
2
|
+
import { FoundData } from '@autobusal/providers/types/routes';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A coach, as the analytics layer sees it.
|
|
6
|
+
*
|
|
7
|
+
* Edited: Ferjolt Ozuni - Date: 2026-08-05
|
|
8
|
+
*
|
|
9
|
+
* One mapping used by every event, so a route cannot be identified one way
|
|
10
|
+
* in `select_item` and another in `purchase` - which is exactly how a funnel
|
|
11
|
+
* ends up unable to join its own steps together.
|
|
12
|
+
*
|
|
13
|
+
* The ROUTE CODE is the id rather than the numeric key: it is what appears
|
|
14
|
+
* on the ticket, in the timetable and on the operator's own paperwork, so a
|
|
15
|
+
* report built on it can be reconciled against something a human recognises.
|
|
16
|
+
*/
|
|
17
|
+
export const item = (route: FoundData): CommerceItem => ({
|
|
18
|
+
item_id: String(route.code ?? route.id),
|
|
19
|
+
item_name: `${ route.locations?.from?.city?.name ?? '' } - ${ route.locations?.to?.city?.name ?? '' }`.trim(),
|
|
20
|
+
item_brand: route.operator?.company,
|
|
21
|
+
price: Number(route.price?.value ?? 0),
|
|
22
|
+
quantity: 1
|
|
23
|
+
});
|
|
24
|
+
|