772 lines
31 KiB
JavaScript
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,
|
|
});
|