@autobusal/routes-order 1.27.0 → 1.30.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 CHANGED
@@ -1,5 +1,51 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.30.0
4
+
5
+ ### Added
6
+
7
+ - **The SMS booking-confirmation add-on at checkout**, with its own number
8
+ field prefilled from the passenger phone already collected — somebody
9
+ booking for a relative wants the text to reach the traveller. The
10
+ description says the free route alerts are not what is being sold here.
11
+ - **A line under the passenger phone field** saying what the number is used
12
+ for: reaching you about this trip, and service messages if the departure
13
+ changes — never marketing. The number was always collected so a driver
14
+ could ring it; texting it is a different use of the same data, and saying
15
+ so at the point of collection is what makes it consent rather than an
16
+ assumption.
17
+ ## 1.29.0
18
+
19
+ ### Removed
20
+
21
+ - **The reviews block on the route-pair page.** A review wall sat in the
22
+ middle of a page whose job is to compare departures. What a reader needs
23
+ there about reputation is one number per operator, which the result cards
24
+ already carry; the reviews themselves belong on the operator's own page,
25
+ where somebody who wants to read them has gone looking for them. The `pair`
26
+ mode of the reviews API is untouched — nothing calls it from here now.
27
+ - The "Reviews" entry in the in-page nav, with it.
28
+
29
+ ## 1.28.0
30
+
31
+ ### Added
32
+
33
+ - **Overnight trips now say so.** A result card that arrives on a later day
34
+ prints the calendar day under BOTH ends and a `+1` against the arrival
35
+ time; the pair-page timetable carries the same marker. "22:30 → 06:00" is
36
+ the one thing on a result card that can be read as the exact opposite of
37
+ what it means, and the duration beside it does not fix that because nobody
38
+ reads a duration to work out a date.
39
+ - Nothing is added to a same-day card. A date under every arrival would make
40
+ the overnight ones stop standing out, which is the only reason this exists.
41
+
42
+ ### Changed
43
+
44
+ - The day comes from the timetable's own midnight markers, computed
45
+ server-side, never from "the arrival looks earlier than the departure" —
46
+ which is wrong for exactly the cross-border journeys whose two ends keep
47
+ different clocks.
48
+
3
49
  ## 1.27.0
4
50
 
5
51
  **`reloadDocument` is gone — the results follow the URL.**
@@ -77,6 +77,8 @@ const Checkout = ({ values, step1, step2, step3, isPartner, t, onStore, onBack }
77
77
  const [ addonWhatsapp, setAddonWhatsapp ] = useState<boolean>(false);
78
78
  const [ addonWhatsappNumber, setAddonWhatsappNumber ] = useState<string>('');
79
79
  const [ addonTelegram, setAddonTelegram ] = useState<boolean>(false);
80
+ const [ addonSms, setAddonSms ] = useState<boolean>(false);
81
+ const [ addonSmsNumber, setAddonSmsNumber ] = useState<string>('');
80
82
  const [ addonFlex, setAddonFlex ] = useState<boolean>(false);
81
83
 
82
84
  const { data: UserData } = useUserStore();
@@ -118,6 +120,10 @@ const Checkout = ({ values, step1, step2, step3, isPartner, t, onStore, onBack }
118
120
  label: t('routes_order.step5.addons.telegram.label'),
119
121
  price: SettingsData.addons.telegram.price
120
122
  } : null,
123
+ addonSms && SettingsData?.addons?.sms?.available ? {
124
+ label: t('routes_order.step5.addons.sms.label'),
125
+ price: SettingsData.addons.sms.price
126
+ } : null,
121
127
  addonFlex && FlexData?.available && FlexData.price !== undefined ? {
122
128
  label: t('routes_order.step5.flex.title'),
123
129
  price: FlexData.price
@@ -150,6 +156,8 @@ const Checkout = ({ values, step1, step2, step3, isPartner, t, onStore, onBack }
150
156
  whatsapp: addonWhatsapp,
151
157
  whatsappNumber: addonWhatsappNumber,
152
158
  telegram: addonTelegram,
159
+ sms: addonSms,
160
+ smsNumber: addonSmsNumber,
153
161
  flex: addonFlex
154
162
  });
155
163
 
@@ -271,11 +279,15 @@ const Checkout = ({ values, step1, step2, step3, isPartner, t, onStore, onBack }
271
279
  whatsapp={ addonWhatsapp }
272
280
  whatsappNumber={ addonWhatsappNumber }
273
281
  telegram={ addonTelegram }
282
+ sms={ addonSms }
283
+ smsNumber={ addonSmsNumber }
274
284
  flex={ addonFlex }
275
285
  t={ t }
276
286
  onChangeWhatsapp={ setAddonWhatsapp }
277
287
  onChangeWhatsappNumber={ setAddonWhatsappNumber }
278
288
  onChangeTelegram={ setAddonTelegram }
289
+ onChangeSms={ setAddonSms }
290
+ onChangeSmsNumber={ setAddonSmsNumber }
279
291
  onChangeFlex={ (value) => {
280
292
  setAddonFlex(value);
281
293
 
package/Facts/types.ts CHANGED
@@ -37,6 +37,13 @@ export interface ScheduleRow {
37
37
  rating: { average: number, count: number } | null
38
38
  departure: string | null
39
39
  arrival: string | null
40
+ /**
41
+ * Edited: Ferjolt Ozuni - Date: 2026-08-05
42
+ * Days between boarding and arriving - 0 same day, 1 the next morning.
43
+ * A timetable of two clock times per row cannot show an overnight service
44
+ * without it.
45
+ */
46
+ offset?: number
40
47
  minutes: number | null
41
48
  // 0 is a direct service
42
49
  stops: number
@@ -8,6 +8,7 @@ import { FoundData } from '@autobusal/providers/types/routes';
8
8
  interface Props {
9
9
  items: FoundData[]
10
10
  type: 'favorite' | 'normal'
11
+ date?: string
11
12
  passengers: number
12
13
  t: TFunction<'common'>
13
14
  onSave: (data: FoundData) => void
@@ -23,7 +24,7 @@ interface Props {
23
24
  * booked, so there is no separate "now choose a time" step to forget: the
24
25
  * card always represents a bookable trip, starting with the earliest.
25
26
  */
26
- const Group = ({ items, type, passengers, t, onSave }: Props): JSX.Element => {
27
+ const Group = ({ items, type, date, passengers, t, onSave }: Props): JSX.Element => {
27
28
  // chronological for the picker, regardless of how the list itself is
28
29
  // sorted - a row of times out of time order is just confusing
29
30
  const departures = useMemo(() => (
@@ -38,6 +39,7 @@ const Group = ({ items, type, passengers, t, onSave }: Props): JSX.Element => {
38
39
  <Route
39
40
  type={ type }
40
41
  data={ current }
42
+ date={ date }
41
43
  passengers={ passengers }
42
44
  t={ t }
43
45
  onSave={ onSave }
@@ -71,6 +71,16 @@ const Orders = ({ loading, data, empty, refined, sort, type, passengers, preferr
71
71
  * Grouping runs AFTER sorting: groupItems keeps input order, so a
72
72
  * collapsed set of shuttles lands wherever its best member sorted to.
73
73
  */
74
+ /**
75
+ * Edited: Ferjolt Ozuni - Date: 2026-08-05
76
+ *
77
+ * The day these results board on, for the cards that arrive on a later
78
+ * one. It is the date that was SEARCHED - the same value the strip above
79
+ * highlights - rather than anything the server sends back, so the two can
80
+ * never disagree about which day a card is for.
81
+ */
82
+ const date = step1[type];
83
+
74
84
  const render = (list: FoundData[], kind: 'favorite' | 'normal'): JSX.Element[] => (
75
85
  groupItems(sortItems(list, sort)).map(group => (
76
86
  group.length > 1
@@ -79,6 +89,7 @@ const Orders = ({ loading, data, empty, refined, sort, type, passengers, preferr
79
89
  key={ group[0].id }
80
90
  items={ group }
81
91
  type={ kind }
92
+ date={ date }
82
93
  passengers={ passengers }
83
94
  t={ t }
84
95
  onSave={ onSave }
@@ -89,6 +100,7 @@ const Orders = ({ loading, data, empty, refined, sort, type, passengers, preferr
89
100
  key={ group[0].id }
90
101
  type={ kind }
91
102
  data={ group[0] }
103
+ date={ date }
92
104
  passengers={ passengers }
93
105
  t={ t }
94
106
  onSave={ onSave }
@@ -6,7 +6,8 @@ import { RiBus2Line } from 'react-icons/ri';
6
6
  import { MdOutlineAltRoute } from 'react-icons/md';
7
7
  import { formatDuration, splitPrice } from '../refine';
8
8
  import { Button, RouteFeature, Information, Rating } from '@autobusal/common';
9
- import { Container, ButtonDetails, SpecialOffer, FavoriteStop, Trip, Company, Location, Stop, Iso, Time, Price, Amount, Decimals, Currency, PriceNotice, LinkCompany, CodeCompany, Details, Detail, ButtonPolicy, TitleDetail, FeaturesItems, Leaves } from './styles';
9
+ import { createDate, getDayParts } from '@autobusal/utilities';
10
+ import { Container, ButtonDetails, SpecialOffer, FavoriteStop, Trip, Company, Location, Stop, Iso, Time, Price, Amount, Decimals, Currency, PriceNotice, LinkCompany, CodeCompany, Details, Detail, ButtonPolicy, TitleDetail, FeaturesItems, Leaves, Day, NextDay } from './styles';
10
11
  import { FoundData } from '@autobusal/providers/types/routes';
11
12
 
12
13
  // neutral placeholder so a missing/broken operator logo doesn't render the
@@ -16,6 +17,11 @@ const LOGO_FALLBACK = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/s
16
17
  interface Props {
17
18
  data: FoundData
18
19
  type: 'favorite' | 'normal'
20
+ // Edited: Ferjolt Ozuni - Date: 2026-08-05
21
+ // The searched boarding day, dd/mm/yyyy. Only used to print the two dates
22
+ // on an overnight trip - a card that arrives the same day says nothing
23
+ // about days at all, which is why this can be absent.
24
+ date?: string
19
25
  passengers: number
20
26
  t: TFunction<'common'>
21
27
  onSave: (data: FoundData) => void
@@ -25,7 +31,7 @@ interface Props {
25
31
  children?: React.ReactNode
26
32
  }
27
33
 
28
- const Route = ({ data, type, passengers, t, onSave, children }: Props): JSX.Element => {
34
+ const Route = ({ data, type, date, passengers, t, onSave, children }: Props): JSX.Element => {
29
35
  const [ information, setInformation ] = useState<boolean>(false);
30
36
 
31
37
  // Edited: Ferjolt Ozuni - Date: 2026-08-03
@@ -63,6 +69,53 @@ const Route = ({ data, type, passengers, t, onSave, children }: Props): JSX.Elem
63
69
 
64
70
  const soon = leaves !== null && leaves < 60;
65
71
 
72
+ /**
73
+ * Edited: Ferjolt Ozuni - Date: 2026-08-05
74
+ *
75
+ * Overnight trips. "22:30 -> 06:00" is the one thing on this card that
76
+ * can be read as the exact opposite of what it means - a seven-and-a-half
77
+ * hour journey looks like a sixteen-hour one run backwards - and the
78
+ * duration beside it does not fix that, because nobody reads a duration
79
+ * to work out a date. So when the coach arrives on a later day, BOTH ends
80
+ * print their day and the arrival carries a +1.
81
+ *
82
+ * Nothing is added to a same-day card: a date under every arrival on a
83
+ * page of same-day results is noise that makes the overnight ones stop
84
+ * standing out, which is the only reason any of this is here.
85
+ *
86
+ * The offset comes from the timetable's own midnight markers, computed
87
+ * server-side (Libraries\Routes\Search\Locations::offset) - it is not
88
+ * inferred from "arrival looks earlier than departure", which is wrong
89
+ * for exactly the cross-border journeys where the two ends keep
90
+ * different clocks.
91
+ */
92
+ const offset = data.arrival_offset ?? 0;
93
+
94
+ const days = ((): { departure: string, arrival: string } | null => {
95
+ if (offset <= 0 || !date) {
96
+ return null;
97
+ }
98
+
99
+ const departs = createDate(date);
100
+
101
+ // an invalid or unexpected date string must not print "NaN Invalid" on
102
+ // a result card - showing no day is better than showing a wrong one
103
+ if (Number.isNaN(departs.getTime())) {
104
+ return null;
105
+ }
106
+
107
+ const arrives = new Date(departs);
108
+ arrives.setUTCDate(arrives.getUTCDate() + offset);
109
+
110
+ const format = (value: Date): string => {
111
+ const parts = getDayParts(value, t as any);
112
+
113
+ return `${ parts.day } ${ parts.month }`;
114
+ };
115
+
116
+ return { departure: format(departs), arrival: format(arrives) };
117
+ })();
118
+
66
119
  return (
67
120
  <Container className="box" $type={ type }>
68
121
  { data.price.offer === true && <SpecialOffer>{ t('routes_order.step2.route.offer') }</SpecialOffer> }
@@ -116,6 +169,8 @@ const Route = ({ data, type, passengers, t, onSave, children }: Props): JSX.Elem
116
169
  { data.locations.from.departure }
117
170
  </h2>
118
171
 
172
+ { days && <Day>{ days.departure }</Day> }
173
+
119
174
  <Stop>{ data.locations.from.stop?.name }</Stop>
120
175
  </Location>
121
176
 
@@ -144,8 +199,15 @@ const Route = ({ data, type, passengers, t, onSave, children }: Props): JSX.Elem
144
199
  <Iso>({ data.locations.to.city.country?.iso_code })</Iso>
145
200
  { ' ' }
146
201
  { data.locations.to.arrival }
202
+ { offset > 0 && (
203
+ <NextDay title={ t('routes_order.step2.route.next_day', { count: offset }) }>
204
+ +{ offset }
205
+ </NextDay>
206
+ ) }
147
207
  </h2>
148
208
 
209
+ { days && <Day>{ days.arrival }</Day> }
210
+
149
211
  <Stop>{ data.locations.to.stop?.name }</Stop>
150
212
  </Location>
151
213
 
@@ -609,3 +609,44 @@ export const Leaves = styled.div<{ $soon: boolean }>`
609
609
  background: ${ props => (props.$soon ? props.theme.font.error : props.theme.primary.neutral) };
610
610
  `;
611
611
 
612
+ /**
613
+ * The +1 on an overnight arrival.
614
+ *
615
+ * Edited: Ferjolt Ozuni - Date: 2026-08-05
616
+ *
617
+ * Sits INSIDE the h2, right against the clock time it qualifies, because
618
+ * that is the number it changes the meaning of. Raised and shrunk rather
619
+ * than boxed: it reads as an annotation on the time, the way a footnote
620
+ * marker does, and a second pill on a card that already carries an offer
621
+ * badge and a countdown would compete with both.
622
+ *
623
+ * The title attribute carries the sentence - "arrives the next day" - so
624
+ * the meaning is available to anyone for whom "+1" is not self-evident,
625
+ * which on a first visit is most people.
626
+ */
627
+ export const NextDay = styled.sup`
628
+ margin-left: 4px;
629
+ padding: 1px 4px;
630
+ border-radius: 4px;
631
+ font-size: ${ props => props.theme.size.xs };
632
+ font-weight: 700;
633
+ white-space: nowrap;
634
+ vertical-align: super;
635
+ cursor: help;
636
+ color: ${ props => props.theme.font.normal };
637
+ background: ${ props => props.theme.primary.neutral };
638
+ `;
639
+
640
+ /**
641
+ * The calendar day under one end of an overnight trip.
642
+ *
643
+ * Rendered on BOTH ends or neither: a date shown only over the arrival
644
+ * leaves the reader working out what day the departure was, which is the
645
+ * question the whole thing exists to answer.
646
+ */
647
+ export const Day = styled.span`
648
+ font-size: ${ props => props.theme.size.xs };
649
+ font-weight: 700;
650
+ white-space: nowrap;
651
+ color: ${ props => props.theme.primary.normal };
652
+ `;
@@ -1,6 +1,6 @@
1
1
  import { TFunction } from 'i18next';
2
2
  import { Rating } from '@autobusal/common';
3
- import { Container, Table, Time, Faded, Price, Operator } from './styles';
3
+ import { Container, Table, Time, Faded, Price, Operator, NextDay } from './styles';
4
4
  import { ScheduleRow } from '../Facts/types';
5
5
 
6
6
  interface Props {
@@ -69,7 +69,20 @@ const Schedule = ({ id, from, to, rows, t }: Props): (JSX.Element | null) => {
69
69
  { rows.map((row, index) => (
70
70
  <tr key={ index }>
71
71
  <td><Time>{ row.departure ?? '-' }</Time></td>
72
- <td><Time>{ row.arrival ?? '-' }</Time></td>
72
+ { /* Edited: Ferjolt Ozuni - Date: 2026-08-05
73
+ The same +1 the result cards carry. A timetable is read
74
+ precisely to plan around a time, so a row whose arrival
75
+ is the following morning and does not say so is the one
76
+ row on the page somebody will actually act on wrongly. */ }
77
+ <td>
78
+ <Time>{ row.arrival ?? '-' }</Time>
79
+
80
+ { (row.offset ?? 0) > 0 && (
81
+ <NextDay title={ t('routes_order.step2.route.next_day', { count: row.offset }) }>
82
+ +{ row.offset }
83
+ </NextDay>
84
+ ) }
85
+ </td>
73
86
  <td><Faded>{ duration(row.minutes) }</Faded></td>
74
87
 
75
88
  <td>
@@ -51,3 +51,21 @@ export const Operator = styled.div`
51
51
  flex-direction: column;
52
52
  gap: 2px;
53
53
  `;
54
+
55
+ /**
56
+ * The +1 on an overnight arrival, matching the result cards.
57
+ *
58
+ * Edited: Ferjolt Ozuni - Date: 2026-08-05
59
+ */
60
+ export const NextDay = styled.sup`
61
+ margin-left: 4px;
62
+ padding: 1px 4px;
63
+ border-radius: 4px;
64
+ font-size: ${ props => props.theme.size.xs };
65
+ font-weight: 700;
66
+ white-space: nowrap;
67
+ vertical-align: super;
68
+ cursor: help;
69
+ color: ${ props => props.theme.font.normal };
70
+ background: ${ props => props.theme.primary.neutral };
71
+ `;
@@ -2,7 +2,6 @@ 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';
6
5
  import Schema from './Schema';
7
6
  import { useGetFacts } from '../Facts/services';
8
7
  import { Nav, Anchor, Anchored } from './styles';
@@ -17,7 +16,6 @@ const TRIPS = 'trips';
17
16
  const FACTS = 'facts';
18
17
  const SCHEDULE = 'schedule';
19
18
  const LINKS = 'links';
20
- const REVIEWS = 'reviews';
21
19
 
22
20
  /**
23
21
  * Everything on a route-pair page that is not the booking wizard.
@@ -65,8 +63,6 @@ const Sections = ({ from, to, t }: Props): (JSX.Element | null) => {
65
63
  { schedule.length > 0 && (
66
64
  <Anchor href={ `#${ SCHEDULE }` }>{ t('routes_order.sections.schedule') }</Anchor>
67
65
  ) }
68
-
69
- <Anchor href={ `#${ REVIEWS }` }>{ t('routes_order.sections.reviews') }</Anchor>
70
66
  </Nav>
71
67
 
72
68
  { /* Edited: Ferjolt Ozuni - Date: 2026-08-03
@@ -82,22 +78,16 @@ const Sections = ({ from, to, t }: Props): (JSX.Element | null) => {
82
78
  <Schedule id={ SCHEDULE } from={ facts.from } to={ facts.to } rows={ schedule } t={ t } />
83
79
  </Anchored>
84
80
 
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>
81
+ { /* Edited: Ferjolt Ozuni - Date: 2026-08-05
82
+ NO reviews block here any more. A pair-level review wall sat in
83
+ the middle of a page whose job is to compare departures, where
84
+ what a reader needs about reputation is one number per operator -
85
+ and the result cards already carry exactly that.
86
+
87
+ The reviews themselves live on the operator's own page, which is
88
+ where somebody who wants to read them has gone looking for them.
89
+ The `pair` mode of Reviews\BrowseController is untouched and
90
+ still works; nothing calls it from here. */ }
101
91
 
102
92
  <Links
103
93
  id={ LINKS }
@@ -2,7 +2,7 @@ import { TFunction } from 'i18next';
2
2
  import { FieldErrors, FieldValues, UseFormRegister } from 'react-hook-form';
3
3
  import { Calendar, Gender, ChooseSeat, Required } from '@autobusal/common';
4
4
  import { Validate, Display } from '@autobusal/utilities';
5
- import { Container, Title } from './styles';
5
+ import { Container, Title, Notice } from './styles';
6
6
  import { PersonData } from '@autobusal/providers/types/persons';
7
7
  import { OccupiedData } from '@autobusal/providers/types/buses';
8
8
 
@@ -139,6 +139,14 @@ const Passenger = ({ type, number, values, fields, withReturn, busFrom, busTo, p
139
139
  />
140
140
 
141
141
  { Display(errors[names.phone]) }
142
+
143
+ { /* Edited: Ferjolt Ozuni - Date: 2026-08-05
144
+ Said because the use changed. This number was collected
145
+ for the operator to ring; it is now also where a service
146
+ SMS goes if the departure moves. Service messages about
147
+ this booking only - never marketing, which is what makes
148
+ the sentence true. */ }
149
+ <Notice>{ t('routes_order.step4.phone_notice') }</Notice>
142
150
  </div>
143
151
  ) }
144
152
  </div>
@@ -7,4 +7,24 @@ export const Container = styled.div`
7
7
  export const Title = styled.h3`
8
8
  padding-bottom: 16px;
9
9
  border-bottom: 1px solid ${ props => props.theme.background.neutral };
10
- `;
10
+ `;
11
+
12
+ /**
13
+ * What the phone number will be used for.
14
+ *
15
+ * Edited: Ferjolt Ozuni - Date: 2026-08-05
16
+ *
17
+ * The number has always been collected so the operator could ring it. It is
18
+ * now also the number a service SMS goes to when a departure changes, and
19
+ * that is a different use of the same data - so it is said here, at the
20
+ * point of collection, rather than assumed. Quiet, but not hidden: a notice
21
+ * nobody can read is not consent.
22
+ */
23
+ export const Notice = styled.small`
24
+ display: block;
25
+ margin-top: 4px;
26
+ font-size: ${ props => props.theme.size.xxs };
27
+ font-weight: 400;
28
+ line-height: 1.4;
29
+ color: ${ props => props.theme.font.faded };
30
+ `;
@@ -1,6 +1,6 @@
1
1
  import { TFunction } from 'i18next';
2
2
  import { useState } from 'react';
3
- import { RiWhatsappLine, RiTelegramLine, RiCalendarCheckLine, RiArrowUpSLine, RiArrowDownSLine } from 'react-icons/ri';
3
+ import { RiWhatsappLine, RiTelegramLine, RiMessage2Line, RiCalendarCheckLine, RiArrowUpSLine, RiArrowDownSLine } from 'react-icons/ri';
4
4
  import { FlexOffer } from '@autobusal/providers/types/routes';
5
5
  import { SubTitle } from '../../styles';
6
6
  import { Container, Option, Notice, Terms, Term, FlexHeader, FlexTitle, FlexToggle, FlexPrice, FlexAdd, FlexDetails } from './styles';
@@ -8,6 +8,14 @@ import { Container, Option, Notice, Terms, Term, FlexHeader, FlexTitle, FlexTogg
8
8
  interface AddonsData {
9
9
  whatsapp: { available: boolean, price: number }
10
10
  telegram: { available: boolean, price: number }
11
+ /**
12
+ * Edited: Ferjolt Ozuni - Date: 2026-08-05
13
+ * The paid per-order CONFIRMATION by SMS. Route alerts over SMS are free
14
+ * and reach every passenger who gave a phone number - this is only the
15
+ * text that confirms the booking. Optional so an older settings payload
16
+ * does not crash the checkout.
17
+ */
18
+ sms?: { available: boolean, price: number }
11
19
  }
12
20
 
13
21
  interface Props {
@@ -17,11 +25,15 @@ interface Props {
17
25
  whatsapp: boolean
18
26
  whatsappNumber: string
19
27
  telegram: boolean
28
+ sms: boolean
29
+ smsNumber: string
20
30
  flex: boolean
21
31
  t: TFunction<'common'>
22
32
  onChangeWhatsapp: (value: boolean) => void
23
33
  onChangeWhatsappNumber: (value: string) => void
24
34
  onChangeTelegram: (value: boolean) => void
35
+ onChangeSms: (value: boolean) => void
36
+ onChangeSmsNumber: (value: string) => void
25
37
  onChangeFlex: (value: boolean) => void
26
38
  }
27
39
 
@@ -30,7 +42,7 @@ interface Props {
30
42
  // enabled (an empty box would be confusing). The WhatsApp addon collects
31
43
  // the number here; Telegram is a "connect after purchase" flow (a bot
32
44
  // can't message a bare phone number cold), so it's just a checkbox.
33
- const Addons = ({ data, flexOffer, currency, whatsapp, whatsappNumber, telegram, flex, t, onChangeWhatsapp, onChangeWhatsappNumber, onChangeTelegram, onChangeFlex }: Props): (JSX.Element | null) => {
45
+ const Addons = ({ data, flexOffer, currency, whatsapp, whatsappNumber, telegram, sms, smsNumber, flex, t, onChangeWhatsapp, onChangeWhatsappNumber, onChangeTelegram, onChangeSms, onChangeSmsNumber, onChangeFlex }: Props): (JSX.Element | null) => {
34
46
  // Edited: Ferjolt Ozuni - Date: 2026-08-03
35
47
  // Flexible Ticket is only on offer where the operators behind the chosen
36
48
  // routes have agreed to honour it, which is a per-booking answer the
@@ -45,7 +57,9 @@ const Addons = ({ data, flexOffer, currency, whatsapp, whatsappNumber, telegram,
45
57
  // check before making it.
46
58
  const [ open, setOpen ] = useState<boolean>(false);
47
59
 
48
- if (!data || (!data.whatsapp.available && !data.telegram.available && !hasFlex)) {
60
+ const hasSms = data?.sms?.available === true;
61
+
62
+ if (!data || (!data.whatsapp.available && !data.telegram.available && !hasSms && !hasFlex)) {
49
63
  return null;
50
64
  }
51
65
 
@@ -100,6 +114,43 @@ const Addons = ({ data, flexOffer, currency, whatsapp, whatsappNumber, telegram,
100
114
  </Option>
101
115
  ) }
102
116
 
117
+ { /* Edited: Ferjolt Ozuni - Date: 2026-08-05
118
+ The CONFIRMATION text, not route alerts - those are free and go to
119
+ every passenger who left a phone number, whether or not anything
120
+ was bought here. The description says so, because "SMS
121
+ notifications" as a paid line would otherwise read as buying the
122
+ delay warnings too.
123
+
124
+ Prefilled from the passenger phone the form above already
125
+ collected, and still editable: somebody booking for a relative
126
+ wants the text to reach the traveller. */ }
127
+ { hasSms && (
128
+ <Option>
129
+ <label>
130
+ <input
131
+ type="checkbox"
132
+ checked={ sms }
133
+ onChange={ (event) => onChangeSms(event.target.checked) }
134
+ />
135
+
136
+ <RiMessage2Line />
137
+
138
+ { t('routes_order.step5.addons.sms.label') } (+{ data.sms?.price } { currency })
139
+ </label>
140
+
141
+ <Notice>{ t('routes_order.step5.addons.sms.description') }</Notice>
142
+
143
+ { sms && (
144
+ <input
145
+ type="text"
146
+ placeholder={ t('routes_order.step5.addons.sms.phone_placeholder') }
147
+ value={ smsNumber }
148
+ onChange={ (event) => onChangeSmsNumber(event.target.value) }
149
+ />
150
+ ) }
151
+ </Option>
152
+ ) }
153
+
103
154
  { hasFlex && (
104
155
  <Option>
105
156
  <FlexHeader>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@autobusal/routes-order",
3
- "version": "1.27.0",
3
+ "version": "1.30.0",
4
4
  "author": "Ferjolt Ozuni",
5
5
  "type": "module",
6
6
  "main": "index.ts"
package/services.ts CHANGED
@@ -156,6 +156,14 @@ export const usePostOrder = (
156
156
  order.addon_telegram = '1';
157
157
  }
158
158
 
159
+ // The number may be blank here: obtapi falls back to the first
160
+ // passenger phone the form already collected (Orders\Make\Addons),
161
+ // so ticking the box on a route that asks for a phone is enough.
162
+ if (addons.sms) {
163
+ order.addon_sms = '1';
164
+ order.addon_sms_number = addons.smsNumber;
165
+ }
166
+
159
167
  if (addons.flex) {
160
168
  order.addon_flex = '1';
161
169
  }
package/types.ts CHANGED
@@ -72,6 +72,15 @@ export interface AddonsSelection {
72
72
  whatsappNumber: string
73
73
  telegram: boolean
74
74
 
75
+ /**
76
+ * Edited: Ferjolt Ozuni - Date: 2026-08-05
77
+ * The paid per-order CONFIRMATION by SMS - route alerts over SMS are free
78
+ * and need nothing here. Carries its own number like WhatsApp does, since
79
+ * somebody booking for a relative wants the text to reach the traveller.
80
+ */
81
+ sms: boolean
82
+ smsNumber: string
83
+
75
84
  // Edited: Ferjolt Ozuni - Date: 2026-08-03
76
85
  // Flexible Ticket. A bare boolean, like Telegram - the window and the
77
86
  // price are the server's to decide (Libraries\Orders\Flex), and a client