Files
doormilxpress_astryx/src/lib/doormileHooks.js

772 lines
31 KiB
JavaScript

import { useMutation, useQueries, useQuery, useQueryClient } from '@tanstack/react-query';
import { kalmanSmoothGps } from '@/lib/dispatchShared';
import { parseDoormileTimestamp } from '@/lib/doormileTimestamp';
import * as api from '@/api/doormile';
import { OpenToast, messageOf } from '@/api/doormile/notify';
/**
* React Query hooks over the Doormile Express admin API.
*
* Every page reads through here rather than calling `@/api/doormile` directly,
* so a resource has exactly one cache key across the app and a write on one
* page invalidates the list another page is showing. The keys are declared once
* in `KEYS` for the same reason — a hand-written key that drifts by a character
* is a cache that silently never invalidates.
*/
export const KEYS = {
dashboard: ['doormile', 'dashboard'],
profile: ['doormile', 'profile'],
reports: (from, to, tenantid, locationid, hubid) => ['doormile', 'reports', from, to, tenantid, locationid, hubid],
locationsSummary: (tenantid, locationid, from, to) => ['doormile', 'locations-summary', tenantid, locationid, from, to],
appUsers: ['doormile', 'app-users'],
partners: ['doormile', 'partners'],
partner: (id) => ['doormile', 'partner', id],
tenants: ['doormile', 'tenants'],
tenant: (id) => ['doormile', 'tenant', id],
tenantLocations: (tenantId) => ['doormile', 'tenant-locations', tenantId],
tenantCustomers: ['doormile', 'tenant-customers'],
tenantCustomer: (id) => ['doormile', 'tenant-customer', id],
customers: ['doormile', 'customers'],
hubs: ['doormile', 'hubs'],
hub: (id) => ['doormile', 'hub', id],
vehicles: ['doormile', 'vehicles'],
vehicle: (id) => ['doormile', 'vehicle', id],
milers: ['doormile', 'milers'],
miler: (id) => ['doormile', 'miler', id],
milerSummary: (hubid, from, to) => ['doormile', 'miler-summary', hubid, from, to],
milerLogs: (id, from, to) => ['doormile', 'miler-logs', id, from, to],
milerActivity: (id, from, to) => ['doormile', 'miler-activity', id, from, to],
riderSummaryCounts: ['doormile', 'rider-summary-counts'],
bookings: (pageno, pagesize) => ['doormile', 'bookings', pageno, pagesize],
booking: (id) => ['doormile', 'booking', id],
bookingTrack: (id) => ['doormile', 'booking-track', id],
consignments: ['doormile', 'consignments'],
consignment: (id) => ['doormile', 'consignment', id],
consignmentLogs: (id) => ['doormile', 'consignment-logs', id],
deliveries: (status, from, to, pageSize) => ['doormile', 'deliveries', status, from, to, pageSize],
deliveryCounts: ['doormile', 'delivery-counts'],
tripsheets: ['doormile', 'tripsheets'],
tripsheet: (id) => ['doormile', 'tripsheet', id],
pricing: ['doormile', 'pricing'],
doormilePricing: ['doormile', 'doormile-pricing'],
exceptions: ['doormile', 'exceptions'],
exception: (id) => ['doormile', 'exception', id],
competitorBranches: (page, limit) => ['doormile', 'competitor-branches', page, limit],
carrierPricing: (page, limit) => ['doormile', 'carrier-pricing', page, limit],
appLocations: ['doormile', 'app-locations'],
};
/* ── Shared helpers ───────────────────────────────────────────────────────── */
/**
* A mutation that toasts on both outcomes and invalidates the keys it touched.
*
* The Doormile API answers `{ success: false, message }` with HTTP 200 on some
* write paths, so "resolved" is not the same as "worked" — a mutation that only
* watched for a thrown error would report a refused write as a success.
*/
function useDoormileMutation({ mutationFn, invalidates = [], successMessage, onDone }) {
const queryClient = useQueryClient();
return useMutation({
mutationFn,
onSuccess: (result) => {
if (result && result.success === false) {
OpenToast(result.message || 'The change was refused', 'warning', 4000);
return;
}
invalidates.forEach((key) => queryClient.invalidateQueries({ queryKey: key }));
if (successMessage) OpenToast(successMessage, 'success');
onDone?.(result);
},
onError: (err) => OpenToast(messageOf(err), 'error', 4000),
});
}
/** Every key a booking write can invalidate — orders, deliveries and counts. */
const BOOKING_KEYS = [
['doormile', 'bookings'],
['doormile', 'deliveries'],
['doormile', 'delivery-counts'],
];
/* ── Dashboard, reports, profile ──────────────────────────────────────────── */
export const useDashboard = (options) =>
useQuery({ queryKey: KEYS.dashboard, queryFn: api.getDashboard, staleTime: 30_000, ...options });
export const useReports = (from, to, tenantid, locationid, hubid, options) =>
useQuery({
queryKey: KEYS.reports(from, to, tenantid, locationid, hubid),
queryFn: () => api.getReports(from, to, tenantid, locationid, hubid),
staleTime: 30_000,
...options,
});
export const useLocationsSummary = (tenantid, locationid, from, to, options) =>
useQuery({
queryKey: KEYS.locationsSummary(tenantid, locationid, from, to),
queryFn: () => api.getLocationsSummary(tenantid, locationid, from, to),
staleTime: 30_000,
...options,
});
export const useProfile = (options) =>
useQuery({ queryKey: KEYS.profile, queryFn: api.getusers, staleTime: 60_000, ...options });
export const useUpdatePassword = () =>
useDoormileMutation({
mutationFn: ({ current_password, new_password }) => api.updateProfilePassword(current_password, new_password),
successMessage: 'Password updated',
});
/* ── App users ────────────────────────────────────────────────────────────── */
export const useAppUsers = (options) =>
useQuery({ queryKey: KEYS.appUsers, queryFn: api.getAppUsers, staleTime: 30_000, ...options });
export const useCreateAppUser = () =>
useDoormileMutation({ mutationFn: api.createAppUser, invalidates: [KEYS.appUsers], successMessage: 'User created' });
export const useUpdateAppUser = () =>
useDoormileMutation({
mutationFn: ({ id, data }) => api.updateAppUser(id, data),
invalidates: [KEYS.appUsers],
successMessage: 'User updated',
});
export const useDeleteAppUser = () =>
useDoormileMutation({ mutationFn: api.deleteAppUser, invalidates: [KEYS.appUsers], successMessage: 'User removed' });
/* ── Partners (fleet suppliers) ───────────────────────────────────────────── */
export const usePartners = (options) =>
useQuery({ queryKey: KEYS.partners, queryFn: api.getPartners, staleTime: 60_000, ...options });
export const useCreatePartner = () =>
useDoormileMutation({ mutationFn: api.createPartner, invalidates: [KEYS.partners], successMessage: 'Partner created' });
export const useUpdatePartner = () =>
useDoormileMutation({
mutationFn: ({ id, data }) => api.updatePartner(id, data),
invalidates: [KEYS.partners],
successMessage: 'Partner updated',
});
export const useDeletePartner = () =>
useDoormileMutation({ mutationFn: api.deletePartner, invalidates: [KEYS.partners], successMessage: 'Partner removed' });
/* ── Tenants (client companies) ───────────────────────────────────────────── */
export const useTenants = (options) =>
useQuery({ queryKey: KEYS.tenants, queryFn: api.getalltenants, staleTime: 60_000, ...options });
export const useTenant = (id, options) =>
useQuery({ queryKey: KEYS.tenant(id), queryFn: () => api.getAdminTenant(id), enabled: !!id, ...options });
export const useCreateTenant = () =>
useDoormileMutation({ mutationFn: api.createAdminTenant, invalidates: [KEYS.tenants], successMessage: 'Client created' });
export const useUpdateTenant = () =>
useDoormileMutation({
mutationFn: ({ id, data }) => api.updateAdminTenant(id, data),
invalidates: [KEYS.tenants],
successMessage: 'Client updated',
});
export const useDeleteTenant = () =>
useDoormileMutation({ mutationFn: api.deleteAdminTenant, invalidates: [KEYS.tenants], successMessage: 'Client removed' });
export const useTenantLocations = (tenantId, options) =>
useQuery({
queryKey: KEYS.tenantLocations(tenantId),
queryFn: () => api.gettenantlocations(tenantId),
enabled: !!tenantId,
staleTime: 60_000,
...options,
});
export const useCreateTenantLocation = () =>
useDoormileMutation({
mutationFn: ({ tenantId, data }) => api.createTenantLocation(tenantId, data),
invalidates: [KEYS.tenants],
successMessage: 'Branch created',
});
export const useUpdateTenantLocation = () =>
useDoormileMutation({
mutationFn: ({ locationId, data }) => api.updateTenantLocation(locationId, data),
invalidates: [KEYS.tenants],
successMessage: 'Branch updated',
});
/* ── Customers ────────────────────────────────────────────────────────────── */
export const useCustomers = (options) =>
useQuery({ queryKey: KEYS.customers, queryFn: api.getAdminCustomers, staleTime: 60_000, ...options });
export const useCreateCustomer = () =>
useDoormileMutation({ mutationFn: api.createAdminCustomer, invalidates: [KEYS.customers], successMessage: 'Customer created' });
export const useUpdateCustomer = () =>
useDoormileMutation({
mutationFn: ({ id, data }) => api.updateAdminCustomer(id, data),
invalidates: [KEYS.customers],
successMessage: 'Customer updated',
});
export const useTenantCustomers = (options) =>
useQuery({ queryKey: KEYS.tenantCustomers, queryFn: api.getTenantCustomers, staleTime: 60_000, ...options });
export const useCreateTenantCustomer = () =>
useDoormileMutation({
mutationFn: api.createTenantCustomer,
invalidates: [KEYS.tenantCustomers],
successMessage: 'Customer created',
});
export const useUpdateTenantCustomer = () =>
useDoormileMutation({
mutationFn: ({ id, data }) => api.updateTenantCustomer(id, data),
invalidates: [KEYS.tenantCustomers],
successMessage: 'Customer updated',
});
export const useDeleteTenantCustomer = () =>
useDoormileMutation({
mutationFn: api.deleteTenantCustomer,
invalidates: [KEYS.tenantCustomers],
successMessage: 'Customer removed',
});
/* ── Hubs ─────────────────────────────────────────────────────────────────── */
export const useHubs = (options) =>
useQuery({ queryKey: KEYS.hubs, queryFn: api.getHubs, staleTime: 60_000, ...options });
export const useHub = (id, options) =>
useQuery({ queryKey: KEYS.hub(id), queryFn: () => api.getHub(id), enabled: !!id, ...options });
export const useCreateHub = () =>
useDoormileMutation({ mutationFn: api.createHub, invalidates: [KEYS.hubs], successMessage: 'Hub created' });
export const useUpdateHub = () =>
useDoormileMutation({
mutationFn: ({ id, data }) => api.updateHub(id, data),
invalidates: [KEYS.hubs],
successMessage: 'Hub updated',
});
export const useDeleteHub = () =>
useDoormileMutation({ mutationFn: api.deleteHub, invalidates: [KEYS.hubs], successMessage: 'Hub removed' });
/**
* The zone picker. There is no zones resource on this API — `applocationid`
* only exists as a field on a hub — so the list is derived from the distinct
* cities across `GET /admin/hubs`.
*/
export const useAppLocations = (options) =>
useQuery({ queryKey: KEYS.appLocations, queryFn: api.fetchAppLocations, staleTime: 300_000, ...options });
/* ── Vehicles ─────────────────────────────────────────────────────────────── */
export const useVehicles = (options) =>
useQuery({ queryKey: KEYS.vehicles, queryFn: api.getVehicles, staleTime: 60_000, ...options });
export const useCreateVehicle = () =>
useDoormileMutation({ mutationFn: api.createVehicle, invalidates: [KEYS.vehicles], successMessage: 'Vehicle created' });
export const useUpdateVehicle = () =>
useDoormileMutation({
mutationFn: ({ id, data }) => api.updateVehicle(id, data),
invalidates: [KEYS.vehicles],
successMessage: 'Vehicle updated',
});
export const useDeleteVehicle = () =>
useDoormileMutation({ mutationFn: api.deleteVehicle, invalidates: [KEYS.vehicles], successMessage: 'Vehicle removed' });
/* ── Milers (riders) ──────────────────────────────────────────────────────── */
export const useMilers = (options) =>
useQuery({ queryKey: KEYS.milers, queryFn: api.getallriders, staleTime: 30_000, ...options });
export const useMiler = (id, options) =>
useQuery({ queryKey: KEYS.miler(id), queryFn: () => api.getMiler(id), enabled: !!id, ...options });
/** Riders shaped for a dropdown — `label` is "name | phone". */
export const useRiderOptions = (options) =>
useQuery({ queryKey: [...KEYS.milers, 'options'], queryFn: api.fetchRidersList, staleTime: 30_000, ...options });
/** Total / active / available / on-delivery counts for the Riders KPI strip. */
export const useRiderSummaryCounts = (options) =>
useQuery({ queryKey: KEYS.riderSummaryCounts, queryFn: api.getallridersummary, staleTime: 30_000, ...options });
export const useMilerSummary = (hubid, from, to, options) =>
useQuery({
queryKey: KEYS.milerSummary(hubid, from, to),
queryFn: () => api.getMilerSummary(hubid, from, to),
staleTime: 30_000,
...options,
});
export const useMilerLogs = (id, from, to, limit, options) =>
useQuery({
queryKey: KEYS.milerLogs(id, from, to),
queryFn: () => api.getMilerLogs(id, from, to, limit),
enabled: !!id,
...options,
});
export const useMilerActivity = (id, from, to, options) =>
useQuery({
queryKey: KEYS.milerActivity(id, from, to),
queryFn: () => api.getMilerActivity(id, from, to),
enabled: !!id,
...options,
});
export const useCreateMiler = () =>
useDoormileMutation({ mutationFn: api.createMiler, invalidates: [KEYS.milers], successMessage: 'Rider created' });
export const useUpdateMiler = () =>
useDoormileMutation({
mutationFn: ({ id, data }) => api.updateMiler(id, data),
invalidates: [KEYS.milers],
successMessage: 'Rider updated',
});
export const useBlockMiler = () =>
useDoormileMutation({
mutationFn: ({ id, data }) => api.blockMiler(id, data),
invalidates: [KEYS.milers],
successMessage: 'Rider updated',
});
export const useAssignMilerVehicle = () =>
useDoormileMutation({
mutationFn: ({ id, data }) => api.assignMilerVehicle(id, data),
invalidates: [KEYS.milers, KEYS.vehicles],
successMessage: 'Vehicle assigned',
});
/* Notify takes a miler PROFILE id, not a userid and not an FCM token — the
server looks the device token up itself. */
export const useNotifyMiler = () =>
useDoormileMutation({
mutationFn: ({ milerProfileId, title, message }) => api.notifyRider(milerProfileId, title, message),
successMessage: 'Rider notified',
});
/* ── Bookings (orders) ────────────────────────────────────────────────────── */
/** One page of bookings, with the envelope's `total` kept for the pager. */
export const useBookingsPage = (pageno, pagesize, options) =>
useQuery({
queryKey: KEYS.bookings(pageno, pagesize),
queryFn: () => api.getBookingsPage(pageno, pagesize),
placeholderData: (previous) => previous,
staleTime: 15_000,
...options,
});
export const useBooking = (id, options) =>
useQuery({ queryKey: KEYS.booking(id), queryFn: () => api.getorderdetails(id), enabled: !!id, ...options });
export const useBookingTrack = (id, options) =>
useQuery({ queryKey: KEYS.bookingTrack(id), queryFn: () => api.getBookingTrack(id), enabled: !!id, ...options });
export const useCreateBooking = () =>
useDoormileMutation({ mutationFn: api.createExpressBooking, invalidates: BOOKING_KEYS, successMessage: 'Order created' });
export const useCreateBookingsBulk = () =>
useDoormileMutation({ mutationFn: api.createExpressBookingBulk, invalidates: BOOKING_KEYS });
/* assign-miler wants the miler's `userid` — a different identity space than the
`milerprofileid` every other /admin/milers route keys on. */
export const useAssignMilerToBooking = () =>
useDoormileMutation({
mutationFn: ({ id, mileruserid }) => api.assignMilerToBooking(id, { mileruserid }),
invalidates: BOOKING_KEYS,
successMessage: 'Rider assigned',
});
export const useBatchAssignBookings = () =>
useDoormileMutation({
mutationFn: ({ bookingIds, maxPerRider }) => api.batchAssignBookings(bookingIds, maxPerRider),
invalidates: BOOKING_KEYS,
});
export const useAssignVehicleToBooking = () =>
useDoormileMutation({
mutationFn: ({ id, data }) => api.assignVehicleToBooking(id, data),
invalidates: BOOKING_KEYS,
successMessage: 'Vehicle assigned',
});
export const useUpdateBookingStatus = () =>
useDoormileMutation({
mutationFn: ({ id, data }) => api.updateBookingStatus(id, data),
invalidates: BOOKING_KEYS,
successMessage: 'Status updated',
});
export const useCancelBooking = () =>
useDoormileMutation({
mutationFn: ({ id, reason }) => api.cancelBooking(id, { reason }),
invalidates: BOOKING_KEYS,
successMessage: 'Order cancelled',
});
export const useBulkCancelBookings = () =>
useDoormileMutation({
mutationFn: api.bulkCancelBookings,
invalidates: BOOKING_KEYS,
successMessage: 'Orders cancelled',
});
/* ── Deliveries ───────────────────────────────────────────────────────────── */
/**
* The Deliveries table — bookings that have moved past merely being created.
*
* Built from `/admin/bookings` rather than `/admin/consignments`, which has no
* documented response schema; see `fetchDeliveries` for the full reasoning and
* for which columns are deliberately left blank instead of invented.
*/
export const useDeliveries = ({ page = 1, pageSize = 50, status = '', from = '', to = '' } = {}, options) =>
useQuery({
queryKey: [...KEYS.deliveries(status, from, to, pageSize), page],
queryFn: () =>
api.fetchDeliveries({ pageParam: page, queryKey: ['deliveries', '', '', status, from, to, pageSize] }),
placeholderData: (previous) => previous,
staleTime: 15_000,
...options,
});
export const useDeliveryCounts = (options) =>
useQuery({ queryKey: KEYS.deliveryCounts, queryFn: api.fetchCountAPI, staleTime: 15_000, ...options });
export const useChangeDeliveryRider = () =>
useDoormileMutation({
mutationFn: ({ rider, row }) => api.changeRiderAPI(rider, row),
invalidates: BOOKING_KEYS,
successMessage: 'Rider changed',
});
/* Refuses statuses the consignment enum cannot express, with the reason — see
`updateDeliveryAPI`. Those come back as `{ success: false, message }`, which
`useDoormileMutation` surfaces as a warning rather than a false success. */
export const useUpdateDeliveryStatus = () =>
useDoormileMutation({ mutationFn: api.updateDeliveryAPI, invalidates: BOOKING_KEYS, successMessage: 'Status updated' });
export const useCancelDelivery = () =>
useDoormileMutation({
mutationFn: ({ row, reason }) => api.cancelDeliveryAPI(row, reason),
invalidates: BOOKING_KEYS,
successMessage: 'Delivery cancelled',
});
/* ── Consignments ─────────────────────────────────────────────────────────── */
export const useConsignments = (options) =>
useQuery({ queryKey: KEYS.consignments, queryFn: api.getConsignments, staleTime: 30_000, ...options });
export const useConsignment = (id, options) =>
useQuery({ queryKey: KEYS.consignment(id), queryFn: () => api.getConsignment(id), enabled: !!id, ...options });
export const useConsignmentLogs = (id, options) =>
useQuery({ queryKey: KEYS.consignmentLogs(id), queryFn: () => api.getConsignmentLogs(id), enabled: !!id, ...options });
/**
* The GPS trail for each of a rider's deliveries, as parallel queries.
*
* This is what the dispatch board's plan-vs-actual comparison is built on:
* `GET /admin/consignments/:id/logs` is the confirmed per-consignment trail,
* and the comparison is per stop, not per rider — so one query per delivery is
* the shape, not a single call.
*
* Pings come back in no guaranteed order and carry raw GPS jitter, so each
* trail is sorted chronologically and run through the Kalman/RTS smoother
* before it reaches a renderer. Without the sort a polyline zig-zags between
* consecutive points; without the smoother it wanders at every stop.
*
* A `deliveryid` on this board has never been confirmed to be a consignment id.
* If it is really a booking id the call 404s and the trail is simply empty —
* degraded, not broken, which is why the error is swallowed per query.
*/
export const useDeliveryTracks = (deliveryIds = [], enabled = true) =>
useQueries({
queries: deliveryIds.map((deliveryid) => ({
queryKey: KEYS.consignmentLogs(deliveryid),
queryFn: async () => {
let rows = [];
try {
rows = (await api.getConsignmentLogs(deliveryid)) || [];
if (!Array.isArray(rows)) rows = [];
} catch {
rows = [];
}
const sorted = rows
.map((row) => {
const at = row?.logdate ? parseDoormileTimestamp(row.logdate) : null;
return {
lat: parseFloat(row?.latitude ?? row?.lat),
lng: parseFloat(row?.longitude ?? row?.lng ?? row?.lon),
logdate: row?.logdate,
_ts: at && at.isValid() ? at.valueOf() : Number.MAX_SAFE_INTEGER,
};
})
.filter((point) => Number.isFinite(point.lat) && Number.isFinite(point.lng))
.sort((a, b) => a._ts - b._ts);
/* The smoother needs `_ts` to compute per-step dt, so it runs before
the timestamp is stripped on the way out. */
return kalmanSmoothGps(sorted).map(({ _ts, ...point }) => point);
},
enabled: enabled && deliveryid != null,
staleTime: 5 * 60 * 1000,
refetchOnWindowFocus: false,
retry: 1,
})),
});
/**
* A rider's most recent periodic log — position, status, battery.
*
* Polled while the rider-info view is open, and only then: this is a live
* snapshot, and polling it from a background tab burns requests for a panel
* nobody is looking at.
*/
export const useRiderPeriodicLog = (milerProfileId, options) =>
useQuery({
queryKey: ['doormile', 'rider-periodic-log', milerProfileId],
queryFn: () => api.getRiderPeriodicLogs(milerProfileId),
enabled: !!milerProfileId,
...options,
});
/* ── Tripsheets ───────────────────────────────────────────────────────────── */
export const useTripsheets = (options) =>
useQuery({ queryKey: KEYS.tripsheets, queryFn: api.getTripsheets, staleTime: 30_000, ...options });
export const useTripsheet = (id, options) =>
useQuery({ queryKey: KEYS.tripsheet(id), queryFn: () => api.getTripsheet(id), enabled: !!id, ...options });
export const useCreateTripsheet = () =>
useDoormileMutation({ mutationFn: api.createTripsheet, invalidates: [KEYS.tripsheets], successMessage: 'Tripsheet created' });
export const useAddTripsheetItem = () =>
useDoormileMutation({
mutationFn: ({ id, consignmentid }) => api.addTripsheetItem(id, consignmentid),
invalidates: [KEYS.tripsheets],
successMessage: 'Consignment added',
});
export const useRemoveTripsheetItem = () =>
useDoormileMutation({
mutationFn: ({ id, itemId }) => api.removeTripsheetItem(id, itemId),
invalidates: [KEYS.tripsheets],
successMessage: 'Consignment removed',
});
export const useDispatchTripsheet = () =>
useDoormileMutation({
mutationFn: api.dispatchTripsheet,
invalidates: [KEYS.tripsheets],
successMessage: 'Tripsheet dispatched',
});
export const useArriveTripsheet = () =>
useDoormileMutation({
mutationFn: api.arriveTripsheet,
invalidates: [KEYS.tripsheets],
successMessage: 'Tripsheet marked arrived',
});
/* ── Pricing ──────────────────────────────────────────────────────────────── */
/* Two separate resources, deliberately: `/admin/pricing` is per-tenant client
pricing; `/admin/doormile-pricing` is Doormile's own internal banding and has
no tenant concept at all. */
export const usePricing = (options) =>
useQuery({ queryKey: KEYS.pricing, queryFn: api.getallpricing, staleTime: 60_000, ...options });
export const useCreatePricing = () =>
useDoormileMutation({ mutationFn: api.createAdminPricing, invalidates: [KEYS.pricing], successMessage: 'Pricing rule created' });
export const useUpdatePricing = () =>
useDoormileMutation({
mutationFn: ({ id, data }) => api.updateAdminPricing(id, data),
invalidates: [KEYS.pricing],
successMessage: 'Pricing rule updated',
});
export const useDeletePricing = () =>
useDoormileMutation({ mutationFn: api.deleteAdminPricing, invalidates: [KEYS.pricing], successMessage: 'Pricing rule removed' });
export const useSimulatePricing = () => useDoormileMutation({ mutationFn: api.simulatePricing });
export const useDoormilePricing = (options) =>
useQuery({ queryKey: KEYS.doormilePricing, queryFn: api.getDoormilePricing, staleTime: 60_000, ...options });
export const useCreateDoormilePricing = () =>
useDoormileMutation({
mutationFn: api.createDoormilePricing,
invalidates: [KEYS.doormilePricing],
successMessage: 'Band created',
});
export const useUpdateDoormilePricing = () =>
useDoormileMutation({
mutationFn: ({ id, data }) => api.updateDoormilePricing(id, data),
invalidates: [KEYS.doormilePricing],
successMessage: 'Band updated',
});
export const useDeleteDoormilePricing = () =>
useDoormileMutation({
mutationFn: api.deleteDoormilePricing,
invalidates: [KEYS.doormilePricing],
successMessage: 'Band removed',
});
/* ── Exceptions ───────────────────────────────────────────────────────────── */
export const useExceptions = (options) =>
useQuery({ queryKey: KEYS.exceptions, queryFn: api.getExceptions, staleTime: 30_000, ...options });
export const useException = (id, options) =>
useQuery({ queryKey: KEYS.exception(id), queryFn: () => api.getException(id), enabled: !!id, ...options });
export const useCreateException = () =>
useDoormileMutation({ mutationFn: api.createException, invalidates: [KEYS.exceptions], successMessage: 'Exception raised' });
export const useUpdateExceptionStatus = () =>
useDoormileMutation({
mutationFn: ({ id, status, resolution }) => api.updateExceptionStatus(id, status, resolution),
invalidates: [KEYS.exceptions],
successMessage: 'Exception updated',
});
/* ── Competitive intelligence ─────────────────────────────────────────────── */
/* These two resources paginate on page/limit, not the pageno/pagesize
convention used everywhere else, and return the whole envelope so the page
can build its own pager. A single fetch is NOT the full set. */
export const useCompetitorBranches = (page = 1, limit = 100, options) =>
useQuery({
queryKey: KEYS.competitorBranches(page, limit),
queryFn: () => api.getCompetitorBranches(page, limit),
placeholderData: (previous) => previous,
staleTime: 60_000,
...options,
});
export const useCreateCompetitorBranch = () =>
useDoormileMutation({
mutationFn: api.createCompetitorBranch,
invalidates: [['doormile', 'competitor-branches']],
successMessage: 'Branch added',
});
export const useUpdateCompetitorBranch = () =>
useDoormileMutation({
mutationFn: ({ id, data }) => api.updateCompetitorBranch(id, data),
invalidates: [['doormile', 'competitor-branches']],
successMessage: 'Branch updated',
});
export const useDeleteCompetitorBranch = () =>
useDoormileMutation({
mutationFn: api.deleteCompetitorBranch,
invalidates: [['doormile', 'competitor-branches']],
successMessage: 'Branch removed',
});
export const useCarrierPricing = (page = 1, limit = 100, options) =>
useQuery({
queryKey: KEYS.carrierPricing(page, limit),
queryFn: () => api.getCarrierPricing(page, limit),
placeholderData: (previous) => previous,
staleTime: 60_000,
...options,
});
export const useCreateCarrierPricing = () =>
useDoormileMutation({
mutationFn: api.createCarrierPricing,
invalidates: [['doormile', 'carrier-pricing']],
successMessage: 'Carrier rate added',
});
export const useUpdateCarrierPricing = () =>
useDoormileMutation({
mutationFn: ({ id, data }) => api.updateCarrierPricing(id, data),
invalidates: [['doormile', 'carrier-pricing']],
successMessage: 'Carrier rate updated',
});
export const useDeleteCarrierPricing = () =>
useDoormileMutation({
mutationFn: api.deleteCarrierPricing,
invalidates: [['doormile', 'carrier-pricing']],
successMessage: 'Carrier rate removed',
});
/* ── Reports ──────────────────────────────────────────────────────────────── */
export const useReportSummary = (appId, tenantid, locationid, startdate, enddate, options) =>
useQuery({
queryKey: ['doormile', 'report-summary', appId, tenantid, locationid, startdate, enddate],
queryFn: () => api.getreportsummary({ queryKey: [appId, tenantid, locationid, startdate, enddate] }),
staleTime: 30_000,
...options,
});
export const useReportLocationSummary = (appId, tenantid, locationid, startdate, enddate, options) =>
useQuery({
queryKey: ['doormile', 'report-location-summary', appId, tenantid, locationid, startdate, enddate],
queryFn: () => api.getreportlocationsummary({ queryKey: [appId, tenantid, locationid, startdate, enddate] }),
staleTime: 30_000,
...options,
});
export const useRidersSummary = (appId, startdate, enddate, options) =>
useQuery({
queryKey: ['doormile', 'riders-summary', appId, startdate, enddate],
queryFn: () => api.fetchRidersSummary({ queryKey: ['ridersSummary', appId, startdate, enddate] }),
staleTime: 30_000,
...options,
});
/** Live rider positions for the dispatch map. Polled, so it never toasts. */
export const useRiderLogs = (appId, options) =>
useQuery({
queryKey: ['doormile', 'rider-logs', appId],
queryFn: () => api.fetchRidersLogs({ queryKey: [appId] }),
...options,
});
export const useOrderPercentages = (startdate, enddate, options) =>
useQuery({
queryKey: ['doormile', 'order-percentages', startdate, enddate],
queryFn: () => api.fetchPercentageData({ queryKey: ['pct', '', startdate, enddate] }),
staleTime: 30_000,
...options,
});