@autobusal/operator-routes 1.6.1 → 1.10.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 +9 -0
- package/Locations/Add/Add.tsx +18 -7
- package/Locations/utilities.tsx +37 -38
- package/Manage.tsx +5 -3
- package/Schedule/Manage.tsx +26 -5
- package/Unavailable/Manage.tsx +12 -5
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.10.0 (2026-08-29)
|
|
4
|
+
|
|
5
|
+
- Editors offer Save & Close alongside Save & Stay, so correcting a record no longer bounces you back to its list every time (opt-in per page, editing only).
|
|
6
|
+
|
|
7
|
+
## 1.9.0 (2026-08-29)
|
|
8
|
+
|
|
9
|
+
- Route schedules and unavailability periods can be planned five years ahead, not one - the picker's ceiling was the only limit.
|
|
10
|
+
- The 'add a stop' picker is searchable: every station on the platform was one native select, so finding your own city meant scrolling hundreds of options in an order nothing sorted for you.
|
|
11
|
+
|
|
3
12
|
## 1.6.1
|
|
4
13
|
|
|
5
14
|
### Fixed
|
package/Locations/Add/Add.tsx
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { TFunction } from 'i18next';
|
|
2
2
|
import { useForm } from 'react-hook-form';
|
|
3
3
|
import { FiPlus } from 'react-icons/fi';
|
|
4
|
-
import { Button } from '@autobusal/common';
|
|
5
|
-
import {
|
|
4
|
+
import { Button, SearchableSelect } from '@autobusal/common';
|
|
5
|
+
import { Display, success, notify } from '@autobusal/utilities';
|
|
6
6
|
import Loading from './Loading';
|
|
7
|
-
import {
|
|
7
|
+
import { getStopOptions } from '../utilities';
|
|
8
8
|
import { Container, Title, ContainerAdd, Choose, Notice } from './styles';
|
|
9
9
|
import { AddForm } from '../types';
|
|
10
10
|
import { useGetStops, useAddLocation } from '../services';
|
|
@@ -16,7 +16,7 @@ interface Props {
|
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
const Add = ({ routeId, t, onReload }: Props): JSX.Element => {
|
|
19
|
-
const { register, handleSubmit, formState: { errors } } = useForm<AddForm>();
|
|
19
|
+
const { register, handleSubmit, setValue, formState: { errors } } = useForm<AddForm>();
|
|
20
20
|
|
|
21
21
|
const { data, isLoading } = useGetStops(routeId);
|
|
22
22
|
|
|
@@ -60,9 +60,20 @@ const Add = ({ routeId, t, onReload }: Props): JSX.Element => {
|
|
|
60
60
|
<form onSubmit={ handleSubmit(onSubmit) }>
|
|
61
61
|
<ContainerAdd>
|
|
62
62
|
<Choose>
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
63
|
+
{ /* Edited: Claude - Date: 2026-08-29: typeable. Every station on
|
|
64
|
+
the platform was listed in one native select, so an operator
|
|
65
|
+
looking for their own city scrolled hundreds of options in an
|
|
66
|
+
order nothing sorted for them. */ }
|
|
67
|
+
<SearchableSelect
|
|
68
|
+
name="group_id"
|
|
69
|
+
options={ getStopOptions(t, data) }
|
|
70
|
+
placeholder={ t('locations_manage.add_city.choose', { ns: 'common' }) }
|
|
71
|
+
empty={ t('locations_manage.add_city.empty', { ns: 'common' }) }
|
|
72
|
+
validation="required"
|
|
73
|
+
t={ t }
|
|
74
|
+
refs={ register }
|
|
75
|
+
onUpdate={ setValue }
|
|
76
|
+
/>
|
|
66
77
|
|
|
67
78
|
{ Display(errors.group_id) }
|
|
68
79
|
</Choose>
|
package/Locations/utilities.tsx
CHANGED
|
@@ -1,46 +1,45 @@
|
|
|
1
1
|
import { TFunction } from 'i18next';
|
|
2
|
-
import {
|
|
2
|
+
import { CountryData } from '@autobusal/providers/types/locations';
|
|
3
|
+
import { SearchableOption } from '@autobusal/common/SearchableSelect/types';
|
|
3
4
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
/**
|
|
6
|
+
* The stations an operator may add to a route, as searchable options.
|
|
7
|
+
*
|
|
8
|
+
* Edited: Claude - Date: 2026-08-29
|
|
9
|
+
*
|
|
10
|
+
* Was a list of <option>/<optgroup> elements for a native select. It is a
|
|
11
|
+
* DATA list now, for SearchableSelect - every station on the platform is in
|
|
12
|
+
* here (hundreds of them, grouped by country), and until this became
|
|
13
|
+
* typeable the only way to find one's own city was to scroll the lot.
|
|
14
|
+
* `search` carries the country name too, so typing "greece" narrows to
|
|
15
|
+
* Greek stations even though the option itself shows only "city > stop".
|
|
16
|
+
*/
|
|
17
|
+
export const getStopOptions = (t: TFunction<'normal'>, countries?: CountryData[]): SearchableOption[] => {
|
|
18
|
+
const options: SearchableOption[] = [];
|
|
8
19
|
|
|
9
20
|
countries?.forEach(country => {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
cities?.forEach(city => {
|
|
24
|
-
city.stops?.forEach(stop => {
|
|
25
|
-
const value = `${ city.id }#${ stop.id }`;
|
|
26
|
-
|
|
27
|
-
/*
|
|
28
|
-
* Edited: Claude - Date: 2026-08-21
|
|
29
|
-
*
|
|
30
|
-
* A stop an operator proposed is listed but DISABLED until an admin
|
|
31
|
-
* approves it. Leaving it out entirely would look like the proposal
|
|
32
|
-
* had been lost - and obtapi refuses it anyway (Locations\
|
|
33
|
-
* OperatorController::add scopes to verified()), so an enabled option
|
|
34
|
-
* would just be an error waiting to happen.
|
|
35
|
-
*/
|
|
36
|
-
const pending = stop.verified === false;
|
|
21
|
+
country.cities?.forEach(city => {
|
|
22
|
+
city.stops?.forEach(stop => {
|
|
23
|
+
/*
|
|
24
|
+
* Edited: Claude - Date: 2026-08-21
|
|
25
|
+
*
|
|
26
|
+
* A stop an operator proposed is listed but DISABLED until an admin
|
|
27
|
+
* approves it. Leaving it out entirely would look like the proposal
|
|
28
|
+
* had been lost - and obtapi refuses it anyway (Locations\
|
|
29
|
+
* OperatorController::add scopes to verified()), so an enabled option
|
|
30
|
+
* would just be an error waiting to happen.
|
|
31
|
+
*/
|
|
32
|
+
const pending = stop.verified === false;
|
|
37
33
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
{ city.name }
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
34
|
+
options.push({
|
|
35
|
+
value: `${ city.id }#${ stop.id }`,
|
|
36
|
+
label: `${ city.name } \u203a ${ stop.name }`,
|
|
37
|
+
group: country.name,
|
|
38
|
+
disabled: pending,
|
|
39
|
+
note: pending ? t('locations_manage.add_city.pending', { ns: 'common' }) : undefined,
|
|
40
|
+
search: [ country.name, city.name, stop.name ]
|
|
41
|
+
});
|
|
42
|
+
});
|
|
44
43
|
});
|
|
45
44
|
});
|
|
46
45
|
|
package/Manage.tsx
CHANGED
|
@@ -46,12 +46,14 @@ const Manage = ({ url, t }: Props): JSX.Element => {
|
|
|
46
46
|
|
|
47
47
|
const drivers = useOperatorDrivers('id');
|
|
48
48
|
|
|
49
|
-
const onSave = (data: RouteData): void => {
|
|
49
|
+
const onSave = (data: RouteData, stay?: boolean): void => {
|
|
50
50
|
Update(data, {
|
|
51
51
|
onSuccess: () => {
|
|
52
52
|
success(t('operator_routes.manage.messages.saved', { ns: 'common' }));
|
|
53
53
|
|
|
54
|
-
|
|
54
|
+
if (!stay) {
|
|
55
|
+
navigate(url);
|
|
56
|
+
}
|
|
55
57
|
}
|
|
56
58
|
});
|
|
57
59
|
};
|
|
@@ -282,7 +284,7 @@ const Manage = ({ url, t }: Props): JSX.Element => {
|
|
|
282
284
|
pending={ isPendingSave || isPendingDelete }
|
|
283
285
|
t={ t }
|
|
284
286
|
actions={ [
|
|
285
|
-
'save', 'delete'
|
|
287
|
+
'save', 'save_stay', 'delete'
|
|
286
288
|
]}
|
|
287
289
|
onSave={ onSave }
|
|
288
290
|
onDelete={ onDelete }
|
package/Schedule/Manage.tsx
CHANGED
|
@@ -12,6 +12,20 @@ interface Props {
|
|
|
12
12
|
t: TFunction<'common'>
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
/**
|
|
16
|
+
* How far ahead a route schedule may be planned.
|
|
17
|
+
*
|
|
18
|
+
* Edited: Claude - Date: 2026-08-29
|
|
19
|
+
*
|
|
20
|
+
* The picker allowed one year - the ceiling every booking calendar has -
|
|
21
|
+
* so an operator running a stable seasonal line had to come back and redo
|
|
22
|
+
* the same dates every twelve months. obtapi never capped this (its rules
|
|
23
|
+
* are date_format + after:start_date), so the limit was only ever the
|
|
24
|
+
* calendar's. Six months before the end date, `schedules:expiring` now
|
|
25
|
+
* emails the operator and raises the alert on their dashboard.
|
|
26
|
+
*/
|
|
27
|
+
const SCHEDULE_YEARS = 5;
|
|
28
|
+
|
|
15
29
|
const Manage = ({ url, t }: Props): JSX.Element => {
|
|
16
30
|
const params = useParams();
|
|
17
31
|
|
|
@@ -42,12 +56,14 @@ const Manage = ({ url, t }: Props): JSX.Element => {
|
|
|
42
56
|
return <Navigate to={ url } />;
|
|
43
57
|
}
|
|
44
58
|
|
|
45
|
-
const onSave = (data: AvailableData): void => {
|
|
59
|
+
const onSave = (data: AvailableData, stay?: boolean): void => {
|
|
46
60
|
Update(data, {
|
|
47
61
|
onSuccess: () => {
|
|
48
62
|
success(t('schedules_manage.messages.saved', { ns: 'common' }));
|
|
49
63
|
|
|
50
|
-
|
|
64
|
+
if (!stay) {
|
|
65
|
+
navigate(url);
|
|
66
|
+
}
|
|
51
67
|
}
|
|
52
68
|
});
|
|
53
69
|
};
|
|
@@ -63,13 +79,18 @@ const Manage = ({ url, t }: Props): JSX.Element => {
|
|
|
63
79
|
name: 'start_date',
|
|
64
80
|
type: 'picker',
|
|
65
81
|
value: data?.available.start_date,
|
|
66
|
-
rules: 'required'
|
|
82
|
+
rules: 'required',
|
|
83
|
+
// Edited: Claude - Date: 2026-08-29: a schedule is planned in
|
|
84
|
+
// seasons, not in the one-year window seats are sold in - both
|
|
85
|
+
// ends of it now reach five years out (see Calendar's maxYears)
|
|
86
|
+
maxYears: SCHEDULE_YEARS
|
|
67
87
|
}, {
|
|
68
88
|
label: t('schedules_manage.end_date', { ns: 'common' }),
|
|
69
89
|
name: 'end_date',
|
|
70
90
|
type: 'picker',
|
|
71
91
|
value: data?.available.end_date,
|
|
72
|
-
rules: 'required'
|
|
92
|
+
rules: 'required',
|
|
93
|
+
maxYears: SCHEDULE_YEARS
|
|
73
94
|
}, {}, {
|
|
74
95
|
label: t('schedules_manage.schedule', { ns: 'common' }),
|
|
75
96
|
name: 'schedule',
|
|
@@ -81,7 +102,7 @@ const Manage = ({ url, t }: Props): JSX.Element => {
|
|
|
81
102
|
pending={ isPending }
|
|
82
103
|
t={ t }
|
|
83
104
|
actions={ [
|
|
84
|
-
'update'
|
|
105
|
+
'update', 'save_stay'
|
|
85
106
|
]}
|
|
86
107
|
onSave={ onSave }
|
|
87
108
|
/>
|
package/Unavailable/Manage.tsx
CHANGED
|
@@ -47,12 +47,14 @@ const Manage = ({ url, t }: Props): JSX.Element => {
|
|
|
47
47
|
return <Navigate to={ url } />;
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
const onSave = (data: UnavailableData): void => {
|
|
50
|
+
const onSave = (data: UnavailableData, stay?: boolean): void => {
|
|
51
51
|
Update(data, {
|
|
52
52
|
onSuccess: () => {
|
|
53
53
|
success(t('unavailable.manage.messages.saved', { ns: 'common' }));
|
|
54
54
|
|
|
55
|
-
|
|
55
|
+
if (!stay) {
|
|
56
|
+
navigate(url);
|
|
57
|
+
}
|
|
56
58
|
}
|
|
57
59
|
});
|
|
58
60
|
};
|
|
@@ -80,19 +82,24 @@ const Manage = ({ url, t }: Props): JSX.Element => {
|
|
|
80
82
|
name: 'start_inactive',
|
|
81
83
|
type: 'picker',
|
|
82
84
|
value: unavailable?.start_inactive,
|
|
83
|
-
rules: 'required'
|
|
85
|
+
rules: 'required',
|
|
86
|
+
// Edited: Claude - Date: 2026-08-29: an unavailability falls INSIDE
|
|
87
|
+
// the route's schedule, which now runs up to five years out - a
|
|
88
|
+
// one-year picker could not describe a closure in year three
|
|
89
|
+
maxYears: 5
|
|
84
90
|
}, {
|
|
85
91
|
label: t('unavailable.manage.end_inactive', { ns: 'common' }),
|
|
86
92
|
name: 'end_inactive',
|
|
87
93
|
type: 'picker',
|
|
88
94
|
value: unavailable?.end_inactive,
|
|
89
|
-
rules: 'required'
|
|
95
|
+
rules: 'required',
|
|
96
|
+
maxYears: 5
|
|
90
97
|
}] }
|
|
91
98
|
fetching={ isFetching }
|
|
92
99
|
pending={ isPendingSave || isPendingDelete }
|
|
93
100
|
t={ t }
|
|
94
101
|
actions={ [
|
|
95
|
-
'save', 'delete'
|
|
102
|
+
'save', 'save_stay', 'delete'
|
|
96
103
|
]}
|
|
97
104
|
onSave={ onSave }
|
|
98
105
|
onDelete={ onDelete }
|