shift
This commit is contained in:
@@ -7,7 +7,7 @@
|
|||||||
* could edit.
|
* could edit.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { api, WEB } from './client';
|
import { api, errorMessage, WEB } from './client';
|
||||||
|
|
||||||
/** One tool the assistant ran, so an answer can show its working. */
|
/** One tool the assistant ran, so an answer can show its working. */
|
||||||
export interface AssistantStep {
|
export interface AssistantStep {
|
||||||
@@ -81,7 +81,13 @@ export async function assistantAvailable(): Promise<boolean> {
|
|||||||
console.warn(`[nearle] Buddy is off: ${status.reason}`);
|
console.warn(`[nearle] Buddy is off: ${status.reason}`);
|
||||||
}
|
}
|
||||||
return status?.available === true;
|
return status?.available === true;
|
||||||
} catch {
|
} catch (cause) {
|
||||||
|
// A failed check and a configured-off server both disable the composer, and
|
||||||
|
// both used to leave exactly the same trace: nothing. So "Not connected yet"
|
||||||
|
// was read as "the deployment has no model" when it could equally have been
|
||||||
|
// a 500, an expired session or a blocked request — three problems with three
|
||||||
|
// different fixes, and no way to tell them apart from the screen.
|
||||||
|
console.warn('[nearle] Buddy status check failed:', errorMessage(cause));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -310,6 +310,22 @@ export interface RiderShift {
|
|||||||
shifthours?: number;
|
shifthours?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A new working window.
|
||||||
|
*
|
||||||
|
* Times go as `HH:MM`; the server normalises `9:00` and `09:00:00` to the same
|
||||||
|
* thing, because the dropdown labels a shift by concatenating the two columns
|
||||||
|
* and the rows inserted by hand over the years use every spelling.
|
||||||
|
*/
|
||||||
|
export interface NewRiderShift {
|
||||||
|
applocationid: number;
|
||||||
|
starttime: string;
|
||||||
|
endtime: string;
|
||||||
|
basefare?: number;
|
||||||
|
additionalcharges?: number;
|
||||||
|
fuelcharge?: number;
|
||||||
|
}
|
||||||
|
|
||||||
export const ridersApi = {
|
export const ridersApi = {
|
||||||
/**
|
/**
|
||||||
* The directory — everyone, working today or not.
|
* The directory — everyone, working today or not.
|
||||||
@@ -350,6 +366,21 @@ export const ridersApi = {
|
|||||||
shifts: (applocationid: number) =>
|
shifts: (applocationid: number) =>
|
||||||
api.list<RiderShift>(`${WEB}/partners/getridershifts`, { applocationid }),
|
api.list<RiderShift>(`${WEB}/partners/getridershifts`, { applocationid }),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open a shift window in a region.
|
||||||
|
*
|
||||||
|
* A rider cannot be hired without a shift, and this table could only be read
|
||||||
|
* until now — so a region that shipped with no shift rows was a region no
|
||||||
|
* rider could ever be added to, from anywhere in the product. The drawer
|
||||||
|
* showed "No shifts set up for this region" and that was the end of it.
|
||||||
|
*
|
||||||
|
* `shifthours` is deliberately not sent. The server works it out from the two
|
||||||
|
* times, because it feeds rider pay and is the one field a person gets wrong
|
||||||
|
* with nothing downstream to catch it.
|
||||||
|
*/
|
||||||
|
createShift: (shift: NewRiderShift) =>
|
||||||
|
api.post<RiderShift>(`${WEB}/partners/createridershift`, shift),
|
||||||
|
|
||||||
/** Delivery partners a rider can ride for. */
|
/** Delivery partners a rider can ride for. */
|
||||||
partners: (applocationid: number) =>
|
partners: (applocationid: number) =>
|
||||||
api.list<Partner>(`${WEB}/partners/getpartners`, { applocationid }),
|
api.list<Partner>(`${WEB}/partners/getpartners`, { applocationid }),
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import { Bike, Building2, CircleDot } from 'lucide-react';
|
import { Bike, Building2, CircleDot, Clock } from 'lucide-react';
|
||||||
import { errorMessage } from '@/api/client';
|
import { errorMessage } from '@/api/client';
|
||||||
import { ridersApi, type RiderRosterRow } from '@/api/deliveries';
|
import { ridersApi, type RiderRosterRow } from '@/api/deliveries';
|
||||||
import type { TenantLocation } from '@/api/types';
|
import type { TenantLocation } from '@/api/types';
|
||||||
@@ -112,6 +112,43 @@ export function RiderDrawer({ row, owner, branch, onClose }: RiderDrawerProps) {
|
|||||||
const set = (key: keyof typeof form) => (value: string) =>
|
const set = (key: keyof typeof form) => (value: string) =>
|
||||||
setForm((prev) => ({ ...prev, [key]: value }));
|
setForm((prev) => ({ ...prev, [key]: value }));
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Opening a shift window, from inside the form that needs one.
|
||||||
|
*
|
||||||
|
* A rider cannot be hired without a shift, and `ridershifts` could only be
|
||||||
|
* READ until now — so a region that shipped with no rows was a region no
|
||||||
|
* rider could ever be added to. The drawer said "No shifts set up for this
|
||||||
|
* region" and there was nothing anybody could do about it from the product;
|
||||||
|
* it took somebody with database access.
|
||||||
|
*
|
||||||
|
* It sits here rather than on a settings screen because this is where the
|
||||||
|
* wall is hit. A person hiring their first rider in a new city should not
|
||||||
|
* have to find out that shifts are a separate concept somewhere else.
|
||||||
|
*/
|
||||||
|
const [newShift, setNewShift] = useState({ starttime: '09:00', endtime: '17:00' });
|
||||||
|
const setShiftField = (key: keyof typeof newShift) => (value: string) =>
|
||||||
|
setNewShift((prev) => ({ ...prev, [key]: value }));
|
||||||
|
|
||||||
|
const addShift = useMutation({
|
||||||
|
mutationFn: () =>
|
||||||
|
ridersApi.createShift({
|
||||||
|
applocationid,
|
||||||
|
starttime: newShift.starttime.trim(),
|
||||||
|
endtime: newShift.endtime.trim(),
|
||||||
|
}),
|
||||||
|
onSuccess: async (shift) => {
|
||||||
|
setProblem(null);
|
||||||
|
await client.invalidateQueries({ queryKey: queryKeys.insights.riderShifts(applocationid) });
|
||||||
|
// Selected straight away. Somebody who just typed these hours meant to put
|
||||||
|
// this rider on them, and making them open the dropdown to choose the only
|
||||||
|
// option is a step that exists for no reason.
|
||||||
|
if (shift?.shiftid) setForm((prev) => ({ ...prev, shiftid: String(shift.shiftid) }));
|
||||||
|
},
|
||||||
|
onError: (error) => setProblem(errorMessage(error)),
|
||||||
|
});
|
||||||
|
|
||||||
|
const hasNoShifts = !shifts.isLoading && (shifts.data ?? []).length === 0;
|
||||||
|
|
||||||
const save = useMutation({
|
const save = useMutation({
|
||||||
mutationFn: () => {
|
mutationFn: () => {
|
||||||
const payload = {
|
const payload = {
|
||||||
@@ -267,6 +304,36 @@ export function RiderDrawer({ row, owner, branch, onClose }: RiderDrawerProps) {
|
|||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
|
|
||||||
|
{/* The way out of the dead end. Shown only when the region genuinely has
|
||||||
|
none — a shift creator on a screen that already has shifts is a way
|
||||||
|
to end up with four overlapping windows nobody can tell apart. */}
|
||||||
|
{hasNoShifts && applocationid > 0 ? (
|
||||||
|
<DrawerCard tone="subtle">
|
||||||
|
<Note>
|
||||||
|
No shift windows exist in this region yet, and a rider cannot be hired without
|
||||||
|
one. Set the hours they work — every rider in this region can then be put on it.
|
||||||
|
</Note>
|
||||||
|
<FieldRow>
|
||||||
|
<Field label="Starts" isRequired>
|
||||||
|
<TextField value={newShift.starttime} onChange={setShiftField('starttime')} />
|
||||||
|
</Field>
|
||||||
|
<Field label="Ends" isRequired>
|
||||||
|
<TextField value={newShift.endtime} onChange={setShiftField('endtime')} />
|
||||||
|
</Field>
|
||||||
|
</FieldRow>
|
||||||
|
<DrawerButton
|
||||||
|
label={addShift.isPending ? 'Adding…' : 'Add this shift'}
|
||||||
|
icon={<Clock size={15} />}
|
||||||
|
isDisabled={
|
||||||
|
addShift.isPending ||
|
||||||
|
newShift.starttime.trim() === '' ||
|
||||||
|
newShift.endtime.trim() === ''
|
||||||
|
}
|
||||||
|
onClick={() => addShift.mutate()}
|
||||||
|
/>
|
||||||
|
</DrawerCard>
|
||||||
|
) : null}
|
||||||
|
|
||||||
{/* A merchant's rider works out of one of that merchant's branches.
|
{/* A merchant's rider works out of one of that merchant's branches.
|
||||||
A partner's does not — a partner supplies several merchants and is
|
A partner's does not — a partner supplies several merchants and is
|
||||||
tied to none of their outlets — so the question is only asked
|
tied to none of their outlets — so the question is only asked
|
||||||
|
|||||||
Reference in New Issue
Block a user