From aca6344cc919411d2709771b0830e0fc557517e2 Mon Sep 17 00:00:00 2001 From: abhishek Date: Thu, 24 Sep 2026 15:52:27 +0530 Subject: [PATCH] shift --- src/api/assistant.ts | 10 +++- src/api/deliveries.ts | 31 +++++++++++ src/features/store-admin/RiderDrawer.tsx | 69 +++++++++++++++++++++++- 3 files changed, 107 insertions(+), 3 deletions(-) diff --git a/src/api/assistant.ts b/src/api/assistant.ts index 668d97c..50e93fa 100644 --- a/src/api/assistant.ts +++ b/src/api/assistant.ts @@ -7,7 +7,7 @@ * 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. */ export interface AssistantStep { @@ -81,7 +81,13 @@ export async function assistantAvailable(): Promise { console.warn(`[nearle] Buddy is off: ${status.reason}`); } 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; } } diff --git a/src/api/deliveries.ts b/src/api/deliveries.ts index 9ecd466..db5421d 100644 --- a/src/api/deliveries.ts +++ b/src/api/deliveries.ts @@ -310,6 +310,22 @@ export interface RiderShift { 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 = { /** * The directory — everyone, working today or not. @@ -350,6 +366,21 @@ export const ridersApi = { shifts: (applocationid: number) => api.list(`${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(`${WEB}/partners/createridershift`, shift), + /** Delivery partners a rider can ride for. */ partners: (applocationid: number) => api.list(`${WEB}/partners/getpartners`, { applocationid }), diff --git a/src/features/store-admin/RiderDrawer.tsx b/src/features/store-admin/RiderDrawer.tsx index f332495..0d74aaa 100644 --- a/src/features/store-admin/RiderDrawer.tsx +++ b/src/features/store-admin/RiderDrawer.tsx @@ -1,6 +1,6 @@ import { useState } from 'react'; 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 { ridersApi, type RiderRosterRow } from '@/api/deliveries'; 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) => 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({ mutationFn: () => { const payload = { @@ -267,6 +304,36 @@ export function RiderDrawer({ row, owner, branch, onClose }: RiderDrawerProps) { /> + {/* 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 ? ( + + + 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. + + + + + + + + + + } + isDisabled={ + addShift.isPending || + newShift.starttime.trim() === '' || + newShift.endtime.trim() === '' + } + onClick={() => addShift.mutate()} + /> + + ) : null} + {/* 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 tied to none of their outlets — so the question is only asked