@autobusal/operator-routes 1.5.2 → 1.6.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,24 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.6.1
4
+
5
+ ### Fixed
6
+
7
+ - **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.)
8
+
9
+ ## 1.6.0
10
+
11
+ ### Added
12
+
13
+ - **The stops taken off a route can be put back.** Withdrawal never destroyed
14
+ anything - the stop's row, its price rows and the fares on them all survive
15
+ - and both the API endpoint and this package's `useRestoreLocation` hook
16
+ had existed since it shipped. Nothing rendered them: the locations response
17
+ filters on `active`, so a withdrawn stop was never mentioned and no screen
18
+ could offer to bring one back. From the operator's side a withdrawal was
19
+ invisible and permanent, which is the one thing it was built not to be.
20
+ The section appears only when the route has something to restore.
21
+
3
22
  ## 1.5.2
4
23
 
5
24
  ### Removed
@@ -5,6 +5,7 @@ import { usePage, useBreadcrumbs } from '@autobusal/hooks';
5
5
  import Add from './Add/Add';
6
6
  import Propose from './Propose/Propose';
7
7
  import Browse from './Browse/Browse';
8
+ import Withdrawn from './Withdrawn/Withdrawn';
8
9
  import { useGetLocations, useGetStops } from './services';
9
10
 
10
11
  interface Props {
@@ -61,6 +62,13 @@ const Manage = ({ url, t }: Props): JSX.Element => {
61
62
  <Report margin={ 15 } question={ t('locations_manage.report', { ns: 'common' }) } type="location" t={ t } />
62
63
 
63
64
  <Browse routeId={ id } data={ data?.locations } t={ t } onReload={ onReload } />
65
+
66
+ { /* Claude - 2026-08-22: the stops taken off this route. Withdrawal
67
+ keeps everything - the row, the price rows, the fares - so it is
68
+ reversible, and the endpoint and hook to reverse it already
69
+ existed. Nothing rendered them, so a withdrawn stop was invisible
70
+ and permanent from here. Renders nothing when there are none. */ }
71
+ <Withdrawn routeId={ id } data={ data?.withdrawn } t={ t } onReload={ onReload } />
64
72
  </Meta>
65
73
  );
66
74
  };
@@ -0,0 +1,74 @@
1
+ import { TFunction } from 'i18next';
2
+ import { FiRotateCcw } from 'react-icons/fi';
3
+ import { success } from '@autobusal/utilities';
4
+ import { LocationData } from '@autobusal/providers/types/locations';
5
+ import { useRestoreLocation } from '../services';
6
+ import { Section, Lead, Row, Where, Restore } from './styles';
7
+
8
+ interface Props {
9
+ routeId: number
10
+ data?: LocationData[]
11
+ t: TFunction<'normal'>
12
+ onReload: () => void
13
+ }
14
+
15
+ /**
16
+ * The stops taken off this route, and the way to put one back.
17
+ *
18
+ * Claude - Date: 2026-08-22
19
+ *
20
+ * Withdrawal never destroys anything - the stop's row, its price rows and
21
+ * the fares on them all survive, which is what lets a ticket sold through it
22
+ * still say where its passenger boarded. That makes it reversible by
23
+ * construction, and obtapi has had the endpoint and this package has had the
24
+ * `useRestoreLocation` hook since the day withdrawal shipped.
25
+ *
26
+ * Nothing rendered them. `locations` filters on `active`, so the response
27
+ * never mentioned a withdrawn stop and no screen could offer to bring one
28
+ * back: from the operator's side a withdrawal was invisible and permanent,
29
+ * which is the one thing it was built not to be. This is the missing half.
30
+ *
31
+ * Shown only when there is something to show - an operator who has never
32
+ * withdrawn a stop should not be told about a concept they have not met.
33
+ */
34
+ const Withdrawn = ({ routeId, data, t, onReload }: Props): JSX.Element | null => {
35
+ const { mutate: Restoring, isPending } = useRestoreLocation(routeId);
36
+
37
+ if (!data?.length) {
38
+ return null;
39
+ }
40
+
41
+ const restore = (id: number): void => {
42
+ Restoring(id, {
43
+ onSuccess: () => {
44
+ success(t('locations_manage.withdrawn.restored', { ns: 'common' }));
45
+
46
+ onReload();
47
+ }
48
+ });
49
+ };
50
+
51
+ return (
52
+ <Section className="box">
53
+ <h4>{ t('locations_manage.withdrawn.title', { ns: 'common' }) }</h4>
54
+
55
+ <Lead>{ t('locations_manage.withdrawn.lead', { ns: 'common' }) }</Lead>
56
+
57
+ { data.map(item => (
58
+ <Row key={ item.id }>
59
+ <Where>
60
+ <strong>{ item.city?.name }</strong>
61
+ <span>{ item.stop?.name }</span>
62
+ </Where>
63
+
64
+ <Restore type="button" disabled={ isPending } onClick={ () => restore(item.id) }>
65
+ <FiRotateCcw />
66
+ { t('locations_manage.withdrawn.restore', { ns: 'common' }) }
67
+ </Restore>
68
+ </Row>
69
+ )) }
70
+ </Section>
71
+ );
72
+ };
73
+
74
+ export default Withdrawn;
@@ -0,0 +1,55 @@
1
+ import styled from 'styled-components';
2
+
3
+ export const Section = styled.div`
4
+ margin-bottom: 15px;
5
+ `;
6
+
7
+ export const Lead = styled.p`
8
+ margin-bottom: 12px;
9
+ font-size: ${ props => props.theme.size.xs };
10
+ opacity: .8;
11
+ `;
12
+
13
+ export const Row = styled.div`
14
+ display: flex;
15
+ flex-wrap: wrap;
16
+ align-items: center;
17
+ justify-content: space-between;
18
+ gap: 10px;
19
+ padding: 8px 0;
20
+ border-top: 1px solid ${ props => props.theme.background.neutral };
21
+ `;
22
+
23
+ export const Where = styled.div`
24
+ font-size: ${ props => props.theme.size.xs };
25
+ min-width: 0;
26
+
27
+ strong {
28
+ display: block;
29
+ }
30
+
31
+ span {
32
+ display: block;
33
+ opacity: .7;
34
+ word-break: break-word;
35
+ }
36
+ `;
37
+
38
+ export const Restore = styled.button`
39
+ background: none;
40
+ border: 1px solid ${ props => props.theme.background.neutral };
41
+ border-radius: 3px;
42
+ padding: 5px 12px;
43
+ cursor: pointer;
44
+ font-size: ${ props => props.theme.size.xs };
45
+
46
+ svg {
47
+ margin-right: 6px;
48
+ vertical-align: -2px;
49
+ }
50
+
51
+ &:disabled {
52
+ opacity: .5;
53
+ cursor: default;
54
+ }
55
+ `;
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' }),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@autobusal/operator-routes",
3
- "version": "1.5.2",
3
+ "version": "1.6.1",
4
4
  "author": "Ferjolt Ozuni",
5
5
  "type": "module",
6
6
  "main": "index.ts"