@autobusal/operator-routes 1.6.0 → 1.9.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,16 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.9.0 (2026-08-29)
4
+
5
+ - Route schedules and unavailability periods can be planned five years ahead, not one - the picker's ceiling was the only limit.
6
+ - 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.
7
+
8
+ ## 1.6.1
9
+
10
+ ### Fixed
11
+
12
+ - **A copied or reversed route could not be opened.** The manage page read `data.drivers.registered` with the optional chain stopping one level short, so a route whose `drivers` relation was null crashed the whole page. Guarded. (Platform audit RT-09e; the backend now also copies the drivers row.)
13
+
3
14
  ## 1.6.0
4
15
 
5
16
  ### Added
@@ -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
@@ -218,7 +218,7 @@ const Manage = ({ url, t }: Props): JSX.Element => {
218
218
  label: t('operator_routes.manage.drivers', { ns: 'common' }),
219
219
  name: 'drivers',
220
220
  type: 'checkbox-list-all',
221
- selected: data?.drivers.registered ?? [],
221
+ selected: data?.drivers?.registered ?? [],
222
222
  values: drivers
223
223
  }, {}, {
224
224
  label: t('operator_routes.manage.searchable', { ns: 'common' }),
@@ -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
 
@@ -63,13 +77,18 @@ const Manage = ({ url, t }: Props): JSX.Element => {
63
77
  name: 'start_date',
64
78
  type: 'picker',
65
79
  value: data?.available.start_date,
66
- rules: 'required'
80
+ rules: 'required',
81
+ // Edited: Claude - Date: 2026-08-29: a schedule is planned in
82
+ // seasons, not in the one-year window seats are sold in - both
83
+ // ends of it now reach five years out (see Calendar's maxYears)
84
+ maxYears: SCHEDULE_YEARS
67
85
  }, {
68
86
  label: t('schedules_manage.end_date', { ns: 'common' }),
69
87
  name: 'end_date',
70
88
  type: 'picker',
71
89
  value: data?.available.end_date,
72
- rules: 'required'
90
+ rules: 'required',
91
+ maxYears: SCHEDULE_YEARS
73
92
  }, {}, {
74
93
  label: t('schedules_manage.schedule', { ns: 'common' }),
75
94
  name: 'schedule',
@@ -80,13 +80,18 @@ const Manage = ({ url, t }: Props): JSX.Element => {
80
80
  name: 'start_inactive',
81
81
  type: 'picker',
82
82
  value: unavailable?.start_inactive,
83
- rules: 'required'
83
+ rules: 'required',
84
+ // Edited: Claude - Date: 2026-08-29: an unavailability falls INSIDE
85
+ // the route's schedule, which now runs up to five years out - a
86
+ // one-year picker could not describe a closure in year three
87
+ maxYears: 5
84
88
  }, {
85
89
  label: t('unavailable.manage.end_inactive', { ns: 'common' }),
86
90
  name: 'end_inactive',
87
91
  type: 'picker',
88
92
  value: unavailable?.end_inactive,
89
- rules: 'required'
93
+ rules: 'required',
94
+ maxYears: 5
90
95
  }] }
91
96
  fetching={ isFetching }
92
97
  pending={ isPendingSave || isPendingDelete }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@autobusal/operator-routes",
3
- "version": "1.6.0",
3
+ "version": "1.9.0",
4
4
  "author": "Ferjolt Ozuni",
5
5
  "type": "module",
6
6
  "main": "index.ts"