@autobusal/operator-routes 1.10.3 → 1.10.5

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.
@@ -54,6 +54,7 @@ const Browse = ({ id, type, data, name, t, onReload }: Props): JSX.Element => {
54
54
  id={ id }
55
55
  type={ type }
56
56
  group={ group }
57
+ others={ groups.length > 1 }
57
58
  name={ name }
58
59
  t={ t }
59
60
  onReload={ onReload }
@@ -3,18 +3,21 @@ import { TFunction } from 'i18next';
3
3
  import { useForm } from 'react-hook-form';
4
4
  import { BiEditAlt } from 'react-icons/bi';
5
5
  import { IoArrowDown } from 'react-icons/io5';
6
- import { FaRegCopy } from 'react-icons/fa';
6
+ import { FaRegCopy, FaArrowDown } from 'react-icons/fa';
7
7
  import { Button } from '@autobusal/common';
8
8
  import { Validate, Display, success } from '@autobusal/utilities';
9
9
  import { Group as Container, Header, Origin, Country, Tools, Dirty, Copy, Columns, Column, Fill, Row, Destination, Changed } from './styles';
10
10
  import { FareColumn, GroupForm, OriginGroup } from '../types';
11
11
  import { COLUMNS, field, toForm, toPrices } from '../utilities';
12
- import { usePostPrices, usePostCopy } from '../services';
12
+ import { usePostPrices, usePostCopy, usePostSpread } from '../services';
13
13
 
14
14
  interface Props {
15
15
  id: number
16
16
  type: 'normal' | 'alternate'
17
17
  group: OriginGroup
18
+
19
+ /** whether the route has more than this one origin - nothing to spread to otherwise */
20
+ others: boolean
18
21
  name: string
19
22
  t: TFunction<'normal'>
20
23
  onReload: () => void
@@ -35,7 +38,7 @@ interface Props {
35
38
  * that changed carry a dot - on a twelve-destination group, pressing Save
36
39
  * should not be a guess about what you edited near the top.
37
40
  */
38
- const Group = ({ id, type, group, name, t, onReload }: Props): JSX.Element => {
41
+ const Group = ({ id, type, group, others, name, t, onReload }: Props): JSX.Element => {
39
42
  const defaults = useMemo(() => toForm(group.prices), [ group.prices ]);
40
43
 
41
44
  const { register, handleSubmit, setValue, getValues, reset, formState: { errors, isDirty, dirtyFields } } = useForm<GroupForm>({
@@ -54,6 +57,7 @@ const Group = ({ id, type, group, name, t, onReload }: Props): JSX.Element => {
54
57
 
55
58
  const { mutate: Save, isPending } = usePostPrices(id, type, name);
56
59
  const { mutate: CopyPrices, isPending: copying } = usePostCopy(id, type, name);
60
+ const { mutate: Spread, isPending: spreading } = usePostSpread(id, type, name);
57
61
 
58
62
  const city = group.from.city;
59
63
 
@@ -76,8 +80,27 @@ const Group = ({ id, type, group, name, t, onReload }: Props): JSX.Element => {
76
80
  success(t('prices_manage.messages.saved_group', { city: city.name, ns: 'common' }));
77
81
 
78
82
  // the values just written are the new baseline, so the group goes
79
- // clean without a refetch
83
+ // clean immediately rather than waiting for the round trip below
80
84
  reset(data);
85
+
86
+ /*
87
+ * Claude - 2026-08-30: AND THE CACHE HAS TO FOLLOW, or the next
88
+ * refetch is a no-op.
89
+ *
90
+ * Resetting the form alone left react-query holding the fares from
91
+ * page load. A later copy writes on the server and refetches - and
92
+ * if what comes back happens to equal that stale cache, structural
93
+ * sharing hands back the SAME object reference, the memo does not
94
+ * recompute, the effect above never fires, and the screen keeps
95
+ * showing the pre-copy fares while the database holds the copied
96
+ * ones. Pressing Update then writes the stale figures back over the
97
+ * copy.
98
+ *
99
+ * Measured, not theorised: copied Tirana's first fare over a
100
+ * destination saved at 99 and watched the row stay at 99 with the
101
+ * group marked clean, while the table said 50.
102
+ */
103
+ onReload();
81
104
  }
82
105
  });
83
106
  };
@@ -96,6 +119,29 @@ const Group = ({ id, type, group, name, t, onReload }: Props): JSX.Element => {
96
119
  });
97
120
  };
98
121
 
122
+ /**
123
+ * Hand this origin's fares to every other origin, destination by
124
+ * destination.
125
+ *
126
+ * Claude - 2026-08-30. The confirm spells out what it will do rather than
127
+ * asking "are you sure": this overwrites every other group on the route and
128
+ * there is no undo, so the sentence has to be the thing the operator checks
129
+ * before agreeing, not a formality they click through.
130
+ */
131
+ const onSpread = (): void => {
132
+ if (!window.confirm(t('prices_manage.group.spread_ask', { city: city.name, ns: 'common' }))) {
133
+ return;
134
+ }
135
+
136
+ Spread({ location_from: group.from.id }, {
137
+ onSuccess: () => {
138
+ success(t('prices_manage.messages.spread', { city: city.name, ns: 'common' }));
139
+
140
+ onReload();
141
+ }
142
+ });
143
+ };
144
+
99
145
  /**
100
146
  * Fill one column down the group from its first destination.
101
147
  *
@@ -138,6 +184,18 @@ const Group = ({ id, type, group, name, t, onReload }: Props): JSX.Element => {
138
184
  </Copy>
139
185
  ) }
140
186
 
187
+ { others && (
188
+ <Copy
189
+ type="button"
190
+ disabled={ spreading }
191
+ onClick={ onSpread }
192
+ title={ t('prices_manage.group.spread_hint', { ns: 'common' }) }
193
+ >
194
+ <FaArrowDown />
195
+ { t('prices_manage.group.spread', { ns: 'common' }) }
196
+ </Copy>
197
+ ) }
198
+
141
199
  <Button
142
200
  type="submit"
143
201
  active={ isDirty }
@@ -72,6 +72,33 @@ export const usePostPrices = (id: number, type: ('normal' | 'alternate'), name:
72
72
  })
73
73
  );
74
74
 
75
+ /**
76
+ * Give every other origin this one's fares, matched destination by
77
+ * destination.
78
+ *
79
+ * Claude - 2026-08-30. Not the same question as usePostCopy below: that one
80
+ * flattens a set of fares to a single figure, this one carries the SHAPE of
81
+ * an origin across - whatever it charges to Thessaloniki becomes what every
82
+ * other stop charges to Thessaloniki.
83
+ */
84
+ export const usePostSpread = (id: number, type: ('normal' | 'alternate'), name: string): UseMutationResult<{ copied: number }, Error, { location_from: number }, unknown> => (
85
+ useMutation({
86
+ mutationKey: ['operator-prices-spread', { id, type, name }],
87
+ mutationFn: async (data: { location_from: number }) => (
88
+ await apiClient
89
+ .post('/api/prices/operator/copy-origins', {
90
+ ...data,
91
+ id,
92
+ type,
93
+ name
94
+ })
95
+ .then(response => (
96
+ response.data
97
+ ))
98
+ )
99
+ })
100
+ );
101
+
75
102
  /**
76
103
  * Copy the first fare over the rest.
77
104
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@autobusal/operator-routes",
3
- "version": "1.10.3",
3
+ "version": "1.10.5",
4
4
  "author": "Ferjolt Ozuni",
5
5
  "type": "module",
6
6
  "main": "index.ts"