@autobusal/routes-order 1.9.3 → 1.11.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.
@@ -1,6 +1,6 @@
1
1
  import { TFunction } from 'i18next';
2
2
  import { FieldErrors, FieldValues, UseFormRegister } from 'react-hook-form';
3
- import { Calendar, Gender, ChooseSeat } from '@autobusal/common';
3
+ import { Calendar, Gender, ChooseSeat, Required } from '@autobusal/common';
4
4
  import { Validate, Display } from '@autobusal/utilities';
5
5
  import { Container, Title } from './styles';
6
6
  import { PersonData } from '@autobusal/providers/types/persons';
@@ -55,7 +55,7 @@ const Passenger = ({ type, number, values, fields, withReturn, busFrom, busTo, p
55
55
 
56
56
  <div className="column">
57
57
  <div className="row">
58
- { t('routes_order.step4.first_name') }
58
+ <Required>{ t('routes_order.step4.first_name') }</Required>
59
59
 
60
60
  <input
61
61
  type="text"
@@ -67,7 +67,7 @@ const Passenger = ({ type, number, values, fields, withReturn, busFrom, busTo, p
67
67
  </div>
68
68
 
69
69
  <div className="row">
70
- { t('routes_order.step4.last_name') }
70
+ <Required>{ t('routes_order.step4.last_name') }</Required>
71
71
 
72
72
  <input
73
73
  type="text"
@@ -81,7 +81,7 @@ const Passenger = ({ type, number, values, fields, withReturn, busFrom, busTo, p
81
81
 
82
82
  <div className="column">
83
83
  <div className="row">
84
- { t('routes_order.step4.dob') }
84
+ <Required>{ t('routes_order.step4.dob') }</Required>
85
85
 
86
86
  <Calendar
87
87
  type="dob"
@@ -116,7 +116,7 @@ const Passenger = ({ type, number, values, fields, withReturn, busFrom, busTo, p
116
116
  <div className="column">
117
117
  { has('passport') && (
118
118
  <div className="row">
119
- { t('routes_order.step4.passport') }
119
+ <Required>{ t('routes_order.step4.passport') }</Required>
120
120
 
121
121
  <input
122
122
  type="text"
@@ -130,7 +130,7 @@ const Passenger = ({ type, number, values, fields, withReturn, busFrom, busTo, p
130
130
 
131
131
  { has('phone') && (
132
132
  <div className="row">
133
- { t('routes_order.step4.phone') }
133
+ <Required>{ t('routes_order.step4.phone') }</Required>
134
134
 
135
135
  <input
136
136
  type="text"
@@ -148,7 +148,7 @@ const Passenger = ({ type, number, values, fields, withReturn, busFrom, busTo, p
148
148
  <div className="column">
149
149
  { has('whatsapp') && (
150
150
  <div className="row">
151
- { t('routes_order.step4.whatsapp') }
151
+ <Required>{ t('routes_order.step4.whatsapp') }</Required>
152
152
 
153
153
  <input
154
154
  type="text"
@@ -162,7 +162,7 @@ const Passenger = ({ type, number, values, fields, withReturn, busFrom, busTo, p
162
162
 
163
163
  { has('telegram') && (
164
164
  <div className="row">
165
- { t('routes_order.step4.telegram') }
165
+ <Required>{ t('routes_order.step4.telegram') }</Required>
166
166
 
167
167
  <input
168
168
  type="text"
@@ -179,7 +179,7 @@ const Passenger = ({ type, number, values, fields, withReturn, busFrom, busTo, p
179
179
  { type !== 'baby' && (
180
180
  <div className="column">
181
181
  <div className="row">
182
- { t('seats.departure') }
182
+ <Required>{ t('seats.departure') }</Required>
183
183
 
184
184
  <ChooseSeat
185
185
  name={ names.seatDeparture }
@@ -198,7 +198,7 @@ const Passenger = ({ type, number, values, fields, withReturn, busFrom, busTo, p
198
198
 
199
199
  { withReturn && (
200
200
  <div className="row">
201
- { t('seats.return') }
201
+ <Required>{ t('seats.return') }</Required>
202
202
 
203
203
  <ChooseSeat
204
204
  name={ names.seatReturn }
@@ -1,5 +1,6 @@
1
1
  import { useState } from 'react';
2
2
  import { TFunction } from 'i18next';
3
+ import { Required } from '@autobusal/common';
3
4
  import { UseFormRegister, FieldErrors } from 'react-hook-form';
4
5
  import { RiPencilFill } from 'react-icons/ri';
5
6
  import { AiOutlineInfoCircle } from 'react-icons/ai';
@@ -42,7 +43,7 @@ const Billing = ({ errors, t, refs }: Props): JSX.Element => {
42
43
 
43
44
  <div className="column">
44
45
  <div className="row">
45
- { t('routes_order.step5.billing.first_name') }
46
+ <Required>{ t('routes_order.step5.billing.first_name') }</Required>
46
47
 
47
48
  <input type="text" { ...refs('bill_fn', Validate('required|min_length:2|max_length:100', t)) } />
48
49
 
@@ -50,7 +51,7 @@ const Billing = ({ errors, t, refs }: Props): JSX.Element => {
50
51
  </div>
51
52
 
52
53
  <div className="row">
53
- { t('routes_order.step5.billing.last_name') }
54
+ <Required>{ t('routes_order.step5.billing.last_name') }</Required>
54
55
 
55
56
  <input type="text" { ...refs('bill_ln', Validate('required|min_length:2|max_length:100', t)) } />
56
57
 
@@ -60,7 +61,7 @@ const Billing = ({ errors, t, refs }: Props): JSX.Element => {
60
61
 
61
62
  <div className="column">
62
63
  <div className="row">
63
- { t('routes_order.step5.billing.email') }
64
+ <Required>{ t('routes_order.step5.billing.email') }</Required>
64
65
 
65
66
  <input type="email" { ...refs('email', Validate('required|email|max_length:255', t)) } />
66
67
 
@@ -72,7 +73,7 @@ const Billing = ({ errors, t, refs }: Props): JSX.Element => {
72
73
  </div>
73
74
 
74
75
  <div className="row">
75
- { t('routes_order.step5.billing.phone') }
76
+ <Required>{ t('routes_order.step5.billing.phone') }</Required>
76
77
 
77
78
  <input type="text" { ...refs('bill_phone', Validate('required|min_length:10|max_length:20', t)) } />
78
79
 
@@ -81,7 +82,7 @@ const Billing = ({ errors, t, refs }: Props): JSX.Element => {
81
82
  </div>
82
83
 
83
84
  <div className="row">
84
- { t('routes_order.step5.billing.address') }
85
+ <Required>{ t('routes_order.step5.billing.address') }</Required>
85
86
 
86
87
  <textarea { ...refs('bill_address', Validate('required|min_length:2', t)) }></textarea>
87
88
 
@@ -90,7 +91,7 @@ const Billing = ({ errors, t, refs }: Props): JSX.Element => {
90
91
 
91
92
  <div className="column">
92
93
  <div className="row">
93
- { t('routes_order.step5.billing.city') }
94
+ <Required>{ t('routes_order.step5.billing.city') }</Required>
94
95
 
95
96
  <input type="text" { ...refs('bill_city', Validate('required|min_length:2|max_length:100', t)) } />
96
97
 
@@ -98,7 +99,7 @@ const Billing = ({ errors, t, refs }: Props): JSX.Element => {
98
99
  </div>
99
100
 
100
101
  <div className="row">
101
- { t('routes_order.step5.billing.country') }
102
+ <Required>{ t('routes_order.step5.billing.country') }</Required>
102
103
 
103
104
  <select { ...refs('bill_country', Validate('required|min:1', t)) }>
104
105
  <option value={ 0 }>{ t('routes_order.step5.billing.country_choose') }</option>
@@ -1,20 +1,27 @@
1
1
  import styled from 'styled-components';
2
2
 
3
3
  export const Container = styled.div`
4
- margin-top: 25px;
4
+ margin-top: 0;
5
5
  `;
6
6
 
7
+ /**
8
+ * Edited: Ferjolt Ozuni - Date: 2026-08-01
9
+ *
10
+ * Wraps on its OWN width rather than switching layout at a viewport
11
+ * breakpoint. This now sits in a ~290px sidebar on a 1440px screen, so the
12
+ * old `@media (min-width: 480px)` put the field and the button side by side
13
+ * in a column too narrow for either - the input came out 86px wide.
14
+ *
15
+ * flex-basis plus wrap is container-responsive without container queries:
16
+ * they sit side by side where there is room and stack where there is not.
17
+ */
7
18
  export const ContainerForm = styled.div`
8
19
  display: flex;
9
- flex-direction: column;
10
- gap: 15px;
20
+ flex-wrap: wrap;
21
+ gap: 10px;
11
22
  max-width: 450px;
12
-
13
- @media (min-width: 480px) {
14
- flex-direction: row;
15
- }
16
23
  `;
17
24
 
18
25
  export const ContainerInput = styled.div`
19
- flex: 1;
20
- `;
26
+ flex: 1 1 165px;
27
+ `;
@@ -1,14 +1,14 @@
1
1
  import styled, { css } from 'styled-components';
2
2
 
3
3
  export const Container = styled.div`
4
- margin-top: 25px;
4
+ margin-top: 0;
5
5
  `;
6
6
 
7
7
  export const ContainerPayments = styled.div`
8
8
  display: flex;
9
9
  flex-direction: row;
10
10
  flex-wrap: wrap;
11
- gap: 15px;
11
+ gap: 10px;
12
12
  justify-content: center;
13
13
  `;
14
14
 
@@ -17,8 +17,11 @@ export const ButtonPayment = styled.button<{ $selected: boolean }>`
17
17
  flex-direction: column;
18
18
  align-items: center;
19
19
  justify-content: center;
20
- gap: 10px;
21
- flex-basis: 150px;
20
+ gap: 8px;
21
+ // Edited: Ferjolt Ozuni - Date: 2026-08-01
22
+ // grow-and-wrap rather than a fixed 150px basis, so two fit per row in
23
+ // the checkout sidebar instead of one card per line down a tall column
24
+ flex: 1 1 110px;
22
25
  padding: 10px;
23
26
  text-align: center;
24
27
  font-weight: 700;
@@ -1,6 +1,7 @@
1
1
  import { TFunction } from 'i18next';
2
2
  import { FaArrowLeft, FaArrowRight } from 'react-icons/fa';
3
- import { Container, ContainerIcon, Icon, Information, Title, SubTitle, Operator } from './styles';
3
+ import { FaCircleDot, FaLocationDot } from 'react-icons/fa6';
4
+ import { Container, ContainerIcon, Icon, Information, Title, Operator, Logo, Leg, Point, PointIcon, PointCity, Country, PointMeta } from './styles';
4
5
  import { RoutesSearchForm } from '@autobusal/routes-search/types';
5
6
  import { FoundData } from '@autobusal/providers/types/routes';
6
7
 
@@ -11,24 +12,28 @@ interface Props {
11
12
  t: TFunction<'public'>
12
13
  }
13
14
 
15
+ /**
16
+ * One leg of the trip, both ends of it.
17
+ *
18
+ * Edited: Ferjolt Ozuni - Date: 2026-08-01
19
+ *
20
+ * This used to render a single line - the departure point on the outbound
21
+ * leg, the arrival point on the return one - so a one-way buyer reached the
22
+ * payment screen having never been shown where the bus actually drops them.
23
+ * Both ends are now stated, origin above destination.
24
+ *
25
+ * The country is named in full rather than coded: this is the last screen
26
+ * before someone pays, and "Thessaloniki, Greece" settles a doubt that
27
+ * "(GRC)" only half answers.
28
+ */
14
29
  const About = ({ type, step1, data, t }: Props): (JSX.Element | null) => {
15
- const icon = type === 'departure' ? <FaArrowRight /> : <FaArrowLeft />;
16
-
17
30
  if (!data) {
18
31
  return null;
19
32
  }
20
33
 
21
- const details = type === 'departure' ? {
22
- date: step1.departure,
23
- hour: data.locations.from.departure,
24
- stop: data.locations.from.stop?.name,
25
- city: data.locations.from.city.name
26
- } : {
27
- date: step1._return,
28
- hour: data.locations.to.arrival,
29
- stop: data.locations.to.stop?.name,
30
- city: data.locations.to.city.name
31
- };
34
+ const icon = type === 'departure' ? <FaArrowRight /> : <FaArrowLeft />;
35
+
36
+ const date = type === 'departure' ? step1.departure : step1._return;
32
37
 
33
38
  return (
34
39
  <Container>
@@ -38,11 +43,56 @@ const About = ({ type, step1, data, t }: Props): (JSX.Element | null) => {
38
43
 
39
44
  <Information>
40
45
  <Title>{ data.name } ({ data.code })</Title>
41
- <SubTitle>{ t(`routes_order.step5.summary.${ type }`, details) }</SubTitle>
46
+
47
+ { data.operator.logo_url && (
48
+ <Logo src={ data.operator.logo_url } alt={ data.operator.company } />
49
+ ) }
50
+
51
+ <Leg>
52
+ <Point>
53
+ <PointIcon><FaCircleDot /></PointIcon>
54
+
55
+ <PointCity>
56
+ { data.locations.from.city.name }
57
+ { data.locations.from.city.country?.name && (
58
+ <Country>, { data.locations.from.city.country.name }</Country>
59
+ ) }
60
+ </PointCity>
61
+
62
+ { /* the date and time are what a buyer double-checks before
63
+ paying, so they carry the weight here */ }
64
+ <PointMeta $strong>
65
+ { t('routes_order.step5.summary.departs', {
66
+ date,
67
+ hour: data.locations.from.departure,
68
+ stop: data.locations.from.stop?.name
69
+ }) }
70
+ </PointMeta>
71
+ </Point>
72
+
73
+ <Point>
74
+ <PointIcon><FaLocationDot /></PointIcon>
75
+
76
+ <PointCity>
77
+ { data.locations.to.city.name }
78
+ { data.locations.to.city.country?.name && (
79
+ <Country>, { data.locations.to.city.country.name }</Country>
80
+ ) }
81
+ </PointCity>
82
+
83
+ <PointMeta>
84
+ { t('routes_order.step5.summary.arrives', {
85
+ hour: data.locations.to.arrival,
86
+ stop: data.locations.to.stop?.name
87
+ }) }
88
+ </PointMeta>
89
+ </Point>
90
+ </Leg>
91
+
42
92
  <Operator>{ data.operator.company }</Operator>
43
93
  </Information>
44
94
  </Container>
45
95
  );
46
96
  };
47
97
 
48
- export default About;
98
+ export default About;
@@ -42,6 +42,87 @@ export const SubTitle = styled.div`
42
42
  border-radius: ${ props => props.theme.borderRadius };
43
43
  `;
44
44
 
45
+ /**
46
+ * Edited: Ferjolt Ozuni - Date: 2026-08-01
47
+ *
48
+ * Origin above destination, each with a city line and a detail line.
49
+ * Replaces the single pill that carried only one end of the leg.
50
+ */
51
+ /**
52
+ * Edited: Ferjolt Ozuni - Date: 2026-08-01
53
+ *
54
+ * The line runs down the Leg itself, not down each Point, so it reads as
55
+ * one continuous journey rather than two separate bars. Each point's icon
56
+ * then sits ON that line - centred over it with a background matching the
57
+ * card, so the line appears to pass behind the icon.
58
+ */
59
+ export const Leg = styled.div`
60
+ position: relative;
61
+ display: flex;
62
+ flex-direction: column;
63
+ gap: 12px;
64
+ margin: 10px 0;
65
+ padding-left: 26px;
66
+
67
+ &::before {
68
+ content: '';
69
+ position: absolute;
70
+ left: 8px;
71
+ top: 6px;
72
+ bottom: 6px;
73
+ width: 2px;
74
+ background: ${ props => props.theme.background.neutral };
75
+ }
76
+ `;
77
+
78
+ export const Point = styled.div`
79
+ position: relative;
80
+ display: flex;
81
+ flex-direction: column;
82
+ `;
83
+
84
+ export const PointIcon = styled.span`
85
+ position: absolute;
86
+ left: -26px;
87
+ top: 0;
88
+ display: flex;
89
+ align-items: center;
90
+ justify-content: center;
91
+ width: 18px;
92
+ height: 18px;
93
+ font-size: 11px;
94
+ color: ${ props => props.theme.primary.contrast };
95
+ background: ${ props => props.theme.primary.normal };
96
+ border-radius: 100px;
97
+ `;
98
+
99
+ export const PointCity = styled.div`
100
+ font-weight: 700;
101
+ `;
102
+
103
+ export const Country = styled.span`
104
+ font-weight: 400;
105
+ color: ${ props => props.theme.font.faded };
106
+ `;
107
+
108
+ export const PointMeta = styled.div<{ $strong?: boolean }>`
109
+ font-size: calc(${ props => props.theme.size.s } - 1px);
110
+ font-weight: ${ props => props.$strong ? 700 : 400 };
111
+ color: ${ props => props.$strong ? props.theme.font.normal : props.theme.font.faded };
112
+ `;
113
+
114
+ // Edited: Ferjolt Ozuni - Date: 2026-08-01
115
+ // The operator's mark under the route name - who the buyer is actually
116
+ // travelling with, which a company name alone conveys less quickly.
117
+ export const Logo = styled.img`
118
+ display: block;
119
+ max-width: 110px;
120
+ max-height: 34px;
121
+ object-fit: contain;
122
+ object-position: left;
123
+ margin: 6px 0;
124
+ `;
125
+
45
126
  export const Operator = styled.div`
46
127
  padding-left: 10px;
47
128
  font-size: calc(${ props => props.theme.size.s } - 1px);
package/Steps/Steps.tsx CHANGED
@@ -8,7 +8,9 @@ interface Props {
8
8
  const Steps = ({ value }: Props): JSX.Element => {
9
9
  const items: JSX.Element[] = [];
10
10
 
11
- for (let i = 1; i <= 5; i++) {
11
+ // Edited: Ferjolt Ozuni - Date: 2026-08-01
12
+ // Four, not five: passenger details and checkout are one screen now.
13
+ for (let i = 1; i <= 4; i++) {
12
14
  items.push(
13
15
  <Item key={ i }>
14
16
  <Content $selected={ value > i }>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@autobusal/routes-order",
3
- "version": "1.9.3",
3
+ "version": "1.11.1",
4
4
  "author": "Ferjolt Ozuni",
5
5
  "type": "module",
6
6
  "main": "index.ts"
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, AddonsSelection } from './types';
7
+ import { FoundDay, ActionData, SaveData, OrderedData, CouponForm, AddonsSelection, AlternativesData } from './types';
8
8
 
9
9
  export const useGetDepartures = (data: RoutesSearchForm): UseQueryResult<FoundData[]> => (
10
10
  useQuery({
@@ -21,6 +21,32 @@ export const useGetDepartures = (data: RoutesSearchForm): UseQueryResult<FoundDa
21
21
  })
22
22
  );
23
23
 
24
+ /**
25
+ * What to offer when a search finds nothing.
26
+ *
27
+ * Edited: Ferjolt Ozuni - Date: 2026-08-01
28
+ *
29
+ * `enabled` is the whole point: this only runs once the departure search
30
+ * has come back empty, so a search that succeeded never pays for it. On
31
+ * obtapi's side it costs a handful of real searches, which is why it is
32
+ * not simply folded into /departure.
33
+ */
34
+ export const useGetAlternatives = (data: RoutesSearchForm, enabled: boolean): UseQueryResult<AlternativesData> => (
35
+ useQuery({
36
+ queryKey: ['search-alternatives', { data }],
37
+ enabled,
38
+ queryFn: async () => (
39
+ await apiClient
40
+ .get('/api/routes/search/alternatives', {
41
+ params: data
42
+ })
43
+ .then(response => (
44
+ response?.data ?? { from: [], to: [], dates: [] }
45
+ ))
46
+ )
47
+ })
48
+ );
49
+
24
50
  export const useGetReturns = (data: RoutesSearchForm, departure: FoundData): UseQueryResult<FoundData[]> => (
25
51
  useQuery({
26
52
  queryKey: ['search-routes-step3', { data }],
@@ -74,18 +100,34 @@ export const useGetDates = (step1: RoutesSearchForm, type: ('departure' | '_retu
74
100
  })
75
101
  };
76
102
 
103
+ /**
104
+ * Edited: Ferjolt Ozuni - Date: 2026-08-01
105
+ *
106
+ * The passenger details moved from a hook argument into the mutation
107
+ * variables. On the merged checkout screen they are only known at the
108
+ * moment the buyer presses pay - validated right then, in the same handler
109
+ * - so baking them into the hook's closure at render time would have sent
110
+ * whatever was in state one render earlier.
111
+ */
112
+ export interface OrderSubmission {
113
+ billing: BillingData
114
+ person: PersonData
115
+ }
116
+
77
117
  export const usePostOrder = (
78
118
  step1: RoutesSearchForm,
79
119
  step2: FoundData,
80
120
  step3: (FoundData | undefined),
81
- step4: PersonData,
82
121
  coupon: (CouponData | undefined),
83
122
  action: (string | undefined),
84
123
  addons: AddonsSelection
85
- ): UseMutationResult<OrderedData, Error, BillingData, unknown> => (
124
+ ): UseMutationResult<OrderedData, Error, OrderSubmission, unknown> => (
86
125
  useMutation({
87
126
  mutationKey: ['save-order'],
88
- mutationFn: async (data: BillingData) => {
127
+ mutationFn: async ({ billing, person }: OrderSubmission) => {
128
+ const data = billing;
129
+ const step4 = person;
130
+
89
131
  const type = step1.type ?? 'departure';
90
132
 
91
133
  const order: SaveData = {};
package/styles.ts CHANGED
@@ -23,6 +23,19 @@ export const Actions = styled.div`
23
23
  margin-top: 25px;
24
24
  `;
25
25
 
26
+ /**
27
+ * The search form kept on screen while browsing results.
28
+ *
29
+ * Edited: Ferjolt Ozuni - Date: 2026-08-01
30
+ *
31
+ * Changing a date or a city used to mean going Back to Search and losing
32
+ * your place. The form stays available above the results so an adjustment
33
+ * is one edit rather than a round trip.
34
+ */
35
+ export const SearchAgain = styled.div`
36
+ margin-bottom: 20px;
37
+ `;
38
+
26
39
  /**
27
40
  * Results layout: filters beside the list, not above it.
28
41
  *
package/types.ts CHANGED
@@ -10,6 +10,30 @@ export interface FoundDay {
10
10
  from_price_display?: string | null
11
11
  }
12
12
 
13
+ /**
14
+ * Edited: Ferjolt Ozuni - Date: 2026-08-01
15
+ * Alternatives offered when a search finds nothing - see obtapi's
16
+ * Routes\Search\Nearby.
17
+ */
18
+ export interface NearbyCity {
19
+ city: {
20
+ id: number
21
+ name: string
22
+ slug: string
23
+ }
24
+ /** kilometres from the city the passenger actually asked for */
25
+ distance: number
26
+ routes: number
27
+ from_price: number
28
+ from_price_display: string
29
+ }
30
+
31
+ export interface AlternativesData {
32
+ from: NearbyCity[]
33
+ to: NearbyCity[]
34
+ dates: FoundDay[]
35
+ }
36
+
13
37
  export interface CouponForm {
14
38
  code: string
15
39
  }
package/Step4/Step4.tsx DELETED
@@ -1,114 +0,0 @@
1
- import { TFunction } from 'i18next';
2
- import { useForm } from 'react-hook-form';
3
- import { HiUsers } from 'react-icons/hi';
4
- import { RiShoppingCartLine } from 'react-icons/ri';
5
- import { AiOutlineCaretLeft } from 'react-icons/ai';
6
- import { useBusOccupied } from '@autobusal/hooks';
7
- import { Button } from '@autobusal/common';
8
- import Steps from '../Steps/Steps';
9
- import Loading from './Loading';
10
- import Passengers from './Passengers/Passengers';
11
- import checkAge from './checkAge';
12
- import { Title, Actions } from '../styles';
13
- import { RoutesSearchForm } from '@autobusal/routes-search/types';
14
- import { PersonData } from '@autobusal/providers/types/persons';
15
- import { FoundData } from '@autobusal/providers/types/routes';
16
-
17
- interface Props {
18
- values?: PersonData
19
- step1: RoutesSearchForm
20
- step2: FoundData
21
- step3?: FoundData
22
- isPartner?: boolean
23
- t: TFunction<'common'>
24
- onSave: (data: PersonData) => void
25
- onBack: (step: number) => void
26
- }
27
-
28
- // Edited: Ferjolt Ozuni - Date: 2026-07-25
29
- // Routes without a passenger_fields config keep the pre-feature form
30
- const DEFAULT_FIELDS = ['sex', 'passport'];
31
-
32
- const Step4 = ({ values, step1, step2, step3, isPartner, t, onSave, onBack }: Props): JSX.Element => {
33
- const { register, handleSubmit, formState: { errors }, setValue, setError } = useForm<PersonData>();
34
-
35
- // Edited: Ferjolt Ozuni - Date: 2026-07-25
36
- // one form serves both legs of a return trip, so it collects the UNION of
37
- // what either route requires (mirrored by the backend rules)
38
- const fields = Array.from(new Set([
39
- ...(step2.trip?.passenger_fields ?? DEFAULT_FIELDS),
40
- ...(step3 !== undefined ? (step3.trip?.passenger_fields ?? DEFAULT_FIELDS) : [])
41
- ]));
42
-
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));
50
-
51
- if (busFrom === undefined || busTo === undefined) {
52
- return <Loading t={ t } />;
53
- }
54
-
55
- const onSubmit = (data: PersonData): void => {
56
- // if all the ages are ok, we move forward
57
- if (checkAge(step1, data, t)) {
58
- onSave(data);
59
- }
60
- };
61
-
62
- const onUpdate = (name: string, value: (string | number)): void => {
63
- setValue(name, value);
64
- setError(name, {});
65
- };
66
-
67
- const onSendForm = (): void => {
68
- handleSubmit(onSubmit)();
69
- };
70
-
71
- return (
72
- <>
73
- <Title>
74
- <HiUsers />
75
- { t('routes_order.step4.title') }
76
- </Title>
77
-
78
- <Steps value={ 4 } />
79
-
80
- <Passengers
81
- values={ values }
82
- step1={ step1 }
83
- fields={ fields }
84
- busFrom={ busFrom }
85
- busTo={ busTo }
86
- t={ t }
87
- errors={ errors }
88
- refs={ register }
89
- onUpdate={ onUpdate }
90
- />
91
-
92
- <Actions>
93
- { isPartner === undefined && (
94
- <Button
95
- type="button"
96
- subtype="secondary"
97
- text={ <><AiOutlineCaretLeft /> { t('routes_order.step4.back') }</> }
98
- noMargin
99
- onClick={ () => onBack(step1._return ? 3 : 2) }
100
- />
101
- ) }
102
-
103
- <Button
104
- type="button"
105
- text={ <><RiShoppingCartLine /> { t('routes_order.step4.next') }</> }
106
- noMargin
107
- onClick={ onSendForm }
108
- />
109
- </Actions>
110
- </>
111
- );
112
- };
113
-
114
- export default Step4;