@autobusal/operator-routes 1.5.0 → 1.5.2
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 +13 -0
- package/Locations/Browse/Actions.tsx +24 -6
- package/Locations/Withdraw/Withdraw.tsx +70 -18
- package/Locations/Withdraw/styles.ts +28 -0
- package/Locations/services.ts +27 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.5.2
|
|
4
|
+
|
|
5
|
+
### Removed
|
|
6
|
+
|
|
7
|
+
- **The confirm dialog in front of removing a stop.** It asked the question
|
|
8
|
+
the withdrawal panel already asks - the panel names the stop, lists
|
|
9
|
+
everybody still travelling through it and has its own button - and it
|
|
10
|
+
answered one part wrongly, warning that the fares would have to be entered
|
|
11
|
+
again. That stopped being true when calibration began reconciling price
|
|
12
|
+
rows instead of rebuilding them: measured on a ten-stop route, withdrawing
|
|
13
|
+
a stop keeps fourteen of sixteen fares and drops only the two legs through
|
|
14
|
+
the stop itself.
|
|
15
|
+
|
|
3
16
|
## 1.3.3
|
|
4
17
|
|
|
5
18
|
### Fixed
|
|
@@ -24,11 +24,29 @@ const Actions = ({ item, loading, t, onMove, onDelete }: Props): JSX.Element =>
|
|
|
24
24
|
onMove(order);
|
|
25
25
|
};
|
|
26
26
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
27
|
+
/*
|
|
28
|
+
* Edited: Claude - Date: 2026-08-22
|
|
29
|
+
*
|
|
30
|
+
* No window.confirm here any more. Removing a stop opens the withdrawal
|
|
31
|
+
* panel, which names the stop, lists everybody still travelling through
|
|
32
|
+
* it, and has its own explicit button - a far better confirmation than a
|
|
33
|
+
* browser dialog, and the dialog now stood in front of it saying "YOU WILL
|
|
34
|
+
* NEED TO ADD THE PRICES AGAIN", which stopped being true when
|
|
35
|
+
* Locations\Calibration started reconciling price rows instead of
|
|
36
|
+
* rebuilding them. Measured: withdrawing a stop from a ten-stop route kept
|
|
37
|
+
* fourteen of sixteen fares, losing only the two legs through the stop
|
|
38
|
+
* itself.
|
|
39
|
+
*
|
|
40
|
+
* So it was a second gate that asked the same question twice and got the
|
|
41
|
+
* answer wrong once. The panel asks it properly.
|
|
42
|
+
*
|
|
43
|
+
* `locations_manage.actions.delete` is deliberately still in obtapi's
|
|
44
|
+
* language files. obtapi deploys BEFORE the frontends, so for the length
|
|
45
|
+
* of that window the previous bundle is still calling it - and a missing
|
|
46
|
+
* key renders as the key itself, which is what the dialog showed when it
|
|
47
|
+
* was removed. It now says something true and is simply read by nobody
|
|
48
|
+
* once this bundle lands.
|
|
49
|
+
*/
|
|
32
50
|
|
|
33
51
|
return (
|
|
34
52
|
<ContainerActions>
|
|
@@ -80,7 +98,7 @@ const Actions = ({ item, loading, t, onMove, onDelete }: Props): JSX.Element =>
|
|
|
80
98
|
</>
|
|
81
99
|
}
|
|
82
100
|
noMargin
|
|
83
|
-
onClick={
|
|
101
|
+
onClick={ onDelete }
|
|
84
102
|
/>
|
|
85
103
|
</Group>
|
|
86
104
|
</ContainerActions>
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { useState } from 'react';
|
|
2
2
|
import { TFunction } from 'i18next';
|
|
3
|
-
import { FiAlertTriangle, FiCheck,
|
|
3
|
+
import { FiAlertTriangle, FiCheck, FiSend, FiUsers } from 'react-icons/fi';
|
|
4
4
|
import { Button } from '@autobusal/common';
|
|
5
5
|
import { success, notify } from '@autobusal/utilities';
|
|
6
6
|
import { LocationData } from '@autobusal/providers/types/locations';
|
|
7
|
-
import { useWithdrawCheck, useMoveOptions, useMovePassenger, AffectedPassenger } from '../services';
|
|
7
|
+
import { useWithdrawCheck, useMoveOptions, useMovePassenger, useNotifyAffected, AffectedPassenger } from '../services';
|
|
8
8
|
import {
|
|
9
9
|
Overlay, Panel, Title, Lead, Table, Row, Cell, MoveOptions, Draft, DraftField,
|
|
10
|
-
Actions, Empty, Close
|
|
10
|
+
Actions, Empty, Close, Channels
|
|
11
11
|
} from './styles';
|
|
12
12
|
|
|
13
13
|
interface Props {
|
|
@@ -30,29 +30,62 @@ interface Props {
|
|
|
30
30
|
* works through it: move each passenger to another stop, or cancel them, and
|
|
31
31
|
* then withdraw.
|
|
32
32
|
*
|
|
33
|
-
* THE MESSAGE IS DRAFTED, NOT SENT. The wording is written in
|
|
34
|
-
* own language and shown here to be read and edited;
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
33
|
+
* THE MESSAGE IS DRAFTED, NOT SENT AUTOMATICALLY. The wording is written in
|
|
34
|
+
* the route's own language and shown here to be read and edited; it goes out
|
|
35
|
+
* only when the operator presses send. Only they know what is replacing the
|
|
36
|
+
* stop and whether a given passenger is being moved or refunded, so mailing
|
|
37
|
+
* people the moment a stop was withdrawn would be writing an apology in
|
|
38
|
+
* their name.
|
|
39
|
+
*
|
|
40
|
+
* IT GOES TO THE AFFECTED BOOKINGS AND NOBODY ELSE. The recipient list is
|
|
41
|
+
* never posted from here - obtapi resolves it from the stop, so the people
|
|
42
|
+
* told are exactly the people in the table above. Routing this through the
|
|
43
|
+
* broadcast composer instead would address a route and a date, reaching
|
|
44
|
+
* passengers boarding two towns away whose journey has not changed, and
|
|
45
|
+
* telling them a stop had changed would be a support call and doubt about a
|
|
46
|
+
* ticket that was never affected.
|
|
39
47
|
*/
|
|
40
48
|
const Withdraw = ({ routeId, location, t, onClose, onWithdraw, onReload }: Props): JSX.Element => {
|
|
41
49
|
const { data, isLoading, refetch } = useWithdrawCheck(routeId, location.id, true);
|
|
42
50
|
|
|
43
51
|
const [ subject, setSubject ] = useState<string | null>(null);
|
|
44
52
|
const [ message, setMessage ] = useState<string | null>(null);
|
|
53
|
+
const [ channels, setChannels ] = useState<string[]>([]);
|
|
54
|
+
const [ notified, setNotified ] = useState<boolean>(false);
|
|
45
55
|
|
|
46
56
|
const passengers = data?.passengers ?? [];
|
|
47
57
|
const draft = data?.draft ?? null;
|
|
58
|
+
const reach = data?.reach ?? {};
|
|
59
|
+
|
|
60
|
+
const { mutate: Notify, isPending: isSending } = useNotifyAffected(routeId);
|
|
61
|
+
|
|
62
|
+
const toggle = (key: string): void => {
|
|
63
|
+
setChannels(current => current.includes(key)
|
|
64
|
+
? current.filter(item => item !== key)
|
|
65
|
+
: [ ...current, key ]);
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const send = (): void => {
|
|
69
|
+
if (channels.length === 0) {
|
|
70
|
+
notify(t('locations_manage.withdraw.draft.pick_channel', { ns: 'common' }));
|
|
48
71
|
|
|
49
|
-
|
|
50
|
-
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
51
74
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
75
|
+
Notify({
|
|
76
|
+
location_id: location.id,
|
|
77
|
+
subject: subject ?? draft?.subject ?? '',
|
|
78
|
+
message: message ?? draft?.message ?? '',
|
|
79
|
+
channels
|
|
80
|
+
}, {
|
|
81
|
+
onSuccess: (results) => {
|
|
82
|
+
const sent = Object.values(results ?? {}).reduce((total, row) => total + (row?.sent ?? 0), 0);
|
|
83
|
+
|
|
84
|
+
success(t('locations_manage.withdraw.draft.sent', { ns: 'common', count: sent }));
|
|
85
|
+
|
|
86
|
+
setNotified(true);
|
|
87
|
+
}
|
|
88
|
+
});
|
|
56
89
|
};
|
|
57
90
|
|
|
58
91
|
return (
|
|
@@ -132,9 +165,28 @@ const Withdraw = ({ routeId, location, t, onClose, onWithdraw, onReload }: Props
|
|
|
132
165
|
/>
|
|
133
166
|
</DraftField>
|
|
134
167
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
168
|
+
{ /* how many of the affected passengers each channel can
|
|
169
|
+
actually reach, so the operator sees that WhatsApp would
|
|
170
|
+
get two of nine BEFORE choosing it, not after */ }
|
|
171
|
+
<Channels>
|
|
172
|
+
{ Object.entries(reach).map(([ key, row ]) => (
|
|
173
|
+
<label key={ key }>
|
|
174
|
+
<input
|
|
175
|
+
type="checkbox"
|
|
176
|
+
checked={ channels.includes(key) }
|
|
177
|
+
onChange={ () => toggle(key) }
|
|
178
|
+
/>
|
|
179
|
+
{ t(`admin_label.manage.options_notifications.${ key }`, { ns: 'common', defaultValue: key }) }
|
|
180
|
+
<span>{ t('locations_manage.withdraw.draft.reach', { ns: 'common', count: row.count }) }</span>
|
|
181
|
+
</label>
|
|
182
|
+
)) }
|
|
183
|
+
</Channels>
|
|
184
|
+
|
|
185
|
+
<button type="button" disabled={ isSending || notified } onClick={ send }>
|
|
186
|
+
<FiSend />
|
|
187
|
+
{ notified
|
|
188
|
+
? t('locations_manage.withdraw.draft.already_sent', { ns: 'common' })
|
|
189
|
+
: t('locations_manage.withdraw.draft.send', { ns: 'common', count: passengers.length }) }
|
|
138
190
|
</button>
|
|
139
191
|
|
|
140
192
|
<span>
|
|
@@ -194,6 +194,34 @@ export const DraftField = styled.div`
|
|
|
194
194
|
}
|
|
195
195
|
`;
|
|
196
196
|
|
|
197
|
+
/*
|
|
198
|
+
* The channel tick boxes, each showing how many of the AFFECTED passengers
|
|
199
|
+
* it can reach - so an operator can see that WhatsApp would get two of nine
|
|
200
|
+
* before choosing it rather than after.
|
|
201
|
+
*/
|
|
202
|
+
export const Channels = styled.div`
|
|
203
|
+
display: flex;
|
|
204
|
+
flex-wrap: wrap;
|
|
205
|
+
gap: 14px;
|
|
206
|
+
margin-bottom: 12px;
|
|
207
|
+
|
|
208
|
+
label {
|
|
209
|
+
display: flex;
|
|
210
|
+
align-items: center;
|
|
211
|
+
gap: 6px;
|
|
212
|
+
font-size: ${ props => props.theme.size.xs };
|
|
213
|
+
cursor: pointer;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
input {
|
|
217
|
+
margin: 0;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
span {
|
|
221
|
+
opacity: .65;
|
|
222
|
+
}
|
|
223
|
+
`;
|
|
224
|
+
|
|
197
225
|
export const Actions = styled.div`
|
|
198
226
|
display: flex;
|
|
199
227
|
align-items: center;
|
package/Locations/services.ts
CHANGED
|
@@ -210,7 +210,7 @@ export interface AffectedPassenger {
|
|
|
210
210
|
export interface WithdrawDraft {
|
|
211
211
|
subject: string
|
|
212
212
|
message: string
|
|
213
|
-
/** travel dates the affected bookings fall on
|
|
213
|
+
/** travel dates the affected bookings fall on */
|
|
214
214
|
dates: string[]
|
|
215
215
|
recipients: { order_id: number, order_number: string, name: string | null, email: string | null, phone: string | null, date_travel: string | null }[]
|
|
216
216
|
}
|
|
@@ -223,7 +223,10 @@ export interface MoveOption {
|
|
|
223
223
|
fare: string | number
|
|
224
224
|
}
|
|
225
225
|
|
|
226
|
-
|
|
226
|
+
/** how many of the affected passengers each channel can actually reach */
|
|
227
|
+
export type ChannelReach = Record<string, { count: number, subject: boolean }>
|
|
228
|
+
|
|
229
|
+
export const useWithdrawCheck = (routeId: number, locationId: number, enabled: boolean): UseQueryResult<{ passengers: AffectedPassenger[], draft: WithdrawDraft | null, reach: ChannelReach }> => (
|
|
227
230
|
useQuery({
|
|
228
231
|
queryKey: ['operator-location-passengers', { routeId, locationId }],
|
|
229
232
|
enabled,
|
|
@@ -268,3 +271,25 @@ export const useRestoreLocation = (routeId: number): UseMutationResult<void, Err
|
|
|
268
271
|
)
|
|
269
272
|
})
|
|
270
273
|
);
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Send the approved message to the affected passengers - and only them.
|
|
277
|
+
*
|
|
278
|
+
* Claude - Date: 2026-08-21
|
|
279
|
+
*
|
|
280
|
+
* The recipient list is NOT sent: obtapi resolves it from the stop, so the
|
|
281
|
+
* people told are exactly the people the panel listed when the operator read
|
|
282
|
+
* the wording. Deliberately not the broadcast composer, which addresses a
|
|
283
|
+
* route and a date and would also reach passengers boarding two towns away
|
|
284
|
+
* whose journey has not changed.
|
|
285
|
+
*/
|
|
286
|
+
export const useNotifyAffected = (routeId: number): UseMutationResult<Record<string, { sent: number, failed: number }>, Error, { location_id: number, subject: string, message: string, channels: string[] }, unknown> => (
|
|
287
|
+
useMutation({
|
|
288
|
+
mutationKey: ['operator-location-notify', { routeId }],
|
|
289
|
+
mutationFn: async (data) => (
|
|
290
|
+
await apiClient
|
|
291
|
+
.post('/api/locations/operator/notify', { id: routeId, ...data })
|
|
292
|
+
.then(response => response.data)
|
|
293
|
+
)
|
|
294
|
+
})
|
|
295
|
+
);
|