@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 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
@@ -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 { Validate, Display, success, notify } from '@autobusal/utilities';
4
+ import { Button, SearchableSelect } from '@autobusal/common';
5
+ import { Display, success, notify } from '@autobusal/utilities';
6
6
  import Loading from './Loading';
7
- import { getStops } from '../utilities';
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
- <select { ...register('group_id', Validate('required', t)) }>
64
- { getStops(t, data) }
65
- </select>
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>
@@ -1,46 +1,45 @@
1
1
  import { TFunction } from 'i18next';
2
- import { CityData, CountryData } from '@autobusal/providers/types/locations';
2
+ import { CountryData } from '@autobusal/providers/types/locations';
3
+ import { SearchableOption } from '@autobusal/common/SearchableSelect/types';
3
4
 
4
- export const getStops = (t: TFunction<'normal'>, countries?: CountryData[]): JSX.Element[] => {
5
- const options: JSX.Element[] = [
6
- <option key="-none=" value="">{ t('locations_manage.add_city.choose', { ns: 'common' }) }</option>
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
- options.push(
11
- <optgroup key={ country.id } label={ country.name }>
12
- { getOptions(country.cities, t) }
13
- </optgroup>
14
- );
15
- });
16
-
17
- return options;
18
- };
19
-
20
- const getOptions = (cities?: CityData[], t?: TFunction<'normal'>): JSX.Element[] => {
21
- const options: JSX.Element[] = [];
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
- options.push(
39
- <option key={ value } value={ value } disabled={ pending }>
40
- { city.name } &rsaquo; { stop.name }
41
- { pending && t ? ` (${ t('locations_manage.add_city.pending', { ns: 'common' }) })` : '' }
42
- </option>
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
- navigate(url);
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 }
@@ -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
- navigate(url);
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
  />
@@ -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
- navigate(url);
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 }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@autobusal/operator-routes",
3
- "version": "1.6.1",
3
+ "version": "1.10.0",
4
4
  "author": "Ferjolt Ozuni",
5
5
  "type": "module",
6
6
  "main": "index.ts"