updates on the ui changes and changed into doormile

This commit is contained in:
2026-08-24 18:10:47 +05:30
parent dcf9770ada
commit aa42d9ee81
409 changed files with 49950 additions and 66532 deletions

View File

@@ -0,0 +1,93 @@
import axios from 'axios';
/**
* The Doormile Express admin API client.
*
* One axios instance for `api.doormile.com/api/v1/admin/*`. Every request needs
* a JWT Bearer token, so the token is attached here rather than at each call
* site, and an expired one logs the console out the same way a manual sign-out
* does — a half-authenticated shell that renders but 401s on every fetch is
* worse than being sent back to the login page.
*
* Ported from the Express Console's `utils/doormileAxios.js`. The only change
* is the environment seam: Vite exposes `import.meta.env`, not `process.env`.
*/
export const DOORMILE_TOKEN_KEY = 'doormileToken';
export const DOORMILE_USER_KEY = 'doormileUser';
/** The backend, for every `/admin/*` call the console makes. */
export const DOORMILE_API_URL = 'https://api.doormile.com/api/v1';
/**
* `VITE_DOORMILE_URL` exists to point a local build at a staging or mock
* backend. It is never set in production: `.dockerignore` keeps every `.env`
* file out of the image, so a container build always resolves to the real API.
*
* An override is announced loudly in development, because a console silently
* talking to the wrong backend looks exactly like a console talking to the
* right one with no data in it.
*/
export const DOORMILE_BASE_URL = import.meta.env.VITE_DOORMILE_URL || DOORMILE_API_URL;
if (import.meta.env.DEV && DOORMILE_BASE_URL !== DOORMILE_API_URL) {
console.warn(
`[Doormile] API overridden to ${DOORMILE_BASE_URL} — the real backend is ${DOORMILE_API_URL}. ` +
'Unset VITE_DOORMILE_URL to use it.'
);
}
const doormileAxios = axios.create({ baseURL: DOORMILE_BASE_URL });
doormileAxios.interceptors.request.use((config) => {
const token = localStorage.getItem(DOORMILE_TOKEN_KEY);
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
doormileAxios.interceptors.response.use(
(response) => response,
(error) => {
if (error.response?.status === 401 && !window.location.href.includes('/login')) {
localStorage.removeItem(DOORMILE_TOKEN_KEY);
localStorage.removeItem(DOORMILE_USER_KEY);
window.location.replace('/login');
}
/* Reject with the server's own body — callers read `err.message` — but keep
the HTTP status reachable as `err.httpStatus`. Without this the status is
lost entirely and a 404, a 500 and a validation failure all collapse into
the same generic failure, which is exactly the distinction the write
paths need. Non-enumerable so nothing that spreads or stringifies the
body starts carrying it around. */
const body = error.response?.data;
if (body === undefined || body === null) return Promise.reject(error);
const payload = typeof body === 'object' ? body : { message: String(body) };
return Promise.reject(
Object.defineProperty(payload, 'httpStatus', {
value: error.response?.status,
enumerable: false,
})
);
}
);
/** Reads the signed-in admin back out of storage, or null when signed out. */
export const readStoredUser = () => {
try {
const raw = localStorage.getItem(DOORMILE_USER_KEY);
return raw ? JSON.parse(raw) : null;
} catch {
return null;
}
};
export const readStoredToken = () => localStorage.getItem(DOORMILE_TOKEN_KEY);
/** The message a failed call should show — server text first, then the throw. */
export const errorMessage = (err, fallback = 'Something went wrong') =>
err?.message || err?.response?.data?.message || fallback;
export default doormileAxios;

View File

@@ -0,0 +1,655 @@
import doormileAxios, { DOORMILE_TOKEN_KEY, DOORMILE_USER_KEY } from './client';
// The Doormile Express admin API — one named async export per
// `api.doormile.com/api/v1/admin/*` endpoint.
//
// Ported verbatim from the Express Console's `pages/api/doormileApi.js`, since
// this is the contract the backend actually speaks; only the client import
// changed. Every function unwraps the envelope for its caller: list/GET
// functions return `response.data.data` directly, mutations return the full
// `response.data` (`{ success, data | message }`) so callers can check
// `res.success`.
//
// Pages consume these through the React Query hooks in `@/lib/doormileHooks`
// rather than calling them directly, so cache keys stay coherent across the app.
const buildQuery = (params = {}) => {
const usp = new URLSearchParams();
Object.entries(params).forEach(([key, value]) => {
if (value !== undefined && value !== null && value !== '') usp.append(key, value);
});
const qs = usp.toString();
return qs ? `?${qs}` : '';
};
// ==============================|| Auth ||============================== //
export const loginAdmin = async (email, password) => {
const response = await doormileAxios.post('/admin/login', { email, password });
if (response.data.success) {
localStorage.setItem(DOORMILE_TOKEN_KEY, response.data.token);
localStorage.setItem(DOORMILE_USER_KEY, JSON.stringify(response.data.user));
}
return response.data;
};
export const logoutAdmin = () => {
localStorage.removeItem(DOORMILE_TOKEN_KEY);
localStorage.removeItem(DOORMILE_USER_KEY);
};
// Drains a paginated list endpoint instead of taking whatever the server's
// default page size happens to be.
//
// GET /admin/customers defaults to `pagesize: 20` — confirmed live against an
// account with 37 customers, which came back as
// { pageno: 1, pagesize: 20, pages: 2, total: 37 }. Every caller of
// getAdminCustomers was therefore seeing 20 of 37 and had no way to know:
// the Customers page reported "Total Customers 20", and the customer lookups
// on the orders pages silently missed anyone on page 2.
//
// (express-console-api.md claims the default is 500. It is not, at least not
// for this endpoint. Trust the envelope, not the doc.)
const PAGE_SIZE = 100; // the doc's own example value — safer than assuming the 1000 cap holds
const MAX_PAGES = 12;
const fetchAllPages = async (path) => {
const first = await doormileAxios.get(`${path}${buildQuery({ pageno: 1, pagesize: PAGE_SIZE })}`);
const body = first.data || {};
const rows = body.data || [];
const total = Number(body.total);
if (!Number.isFinite(total) || rows.length >= total) return rows;
const pageCount = Math.min(Math.ceil(total / PAGE_SIZE), MAX_PAGES);
for (let page = 2; page <= pageCount; page += 1) {
const next = await doormileAxios.get(`${path}${buildQuery({ pageno: page, pagesize: PAGE_SIZE })}`);
const more = next.data?.data || [];
if (!more.length) break;
rows.push(...more);
}
return rows;
};
// ==============================|| Dashboard, profile, reports ||============================== //
export const getDashboard = async () => {
const response = await doormileAxios.get('/admin/dashboard');
return response.data.data;
};
// jupiter used fromdate/todate + applocationid (city); Doormile uses from/to
// and no city filter at all (filters by hubid instead — see
// jupiter2doormile.md §2.3/§6). Response carries by_location/by_hub/by_tenant/
// by_rider blocks per that doc, not documented in express-console-api.md.
export const getReports = async (from, to, tenantid, locationid, hubid) => {
const response = await doormileAxios.get(`/admin/reports${buildQuery({ from, to, tenantid, locationid, hubid })}`);
return response.data.data;
};
// Confirmed against jupiter2doormile.md (Status: Done) as the replacement for
// jupiter's getlocationsummary — per-tenant-site report breakdown. Not in
// express-console-api.md.
export const getLocationsSummary = async (tenantid, locationid, from, to) => {
const response = await doormileAxios.get(`/admin/locations/summary${buildQuery({ tenantid, locationid, from, to })}`);
return response.data.data;
};
export const getProfile = async () => {
const response = await doormileAxios.get('/admin/profile');
return response.data.data;
};
export const updateProfilePassword = async (current_password, new_password) => {
const response = await doormileAxios.put('/admin/profile/password', { current_password, new_password });
return response.data;
};
// ==============================|| App users (staff logins) ||============================== //
export const getAppUsers = async () => {
const response = await doormileAxios.get('/admin/users');
return response.data.data;
};
export const createAppUser = async (data) => {
const response = await doormileAxios.post('/admin/users', data);
return response.data;
};
export const updateAppUser = async (id, data) => {
const response = await doormileAxios.put(`/admin/users/${id}`, data);
return response.data;
};
export const deleteAppUser = async (id) => {
const response = await doormileAxios.delete(`/admin/users/${id}`);
return response.data;
};
// ==============================|| Partners (fleet / rider suppliers) ||============================== //
export const getPartners = async () => {
const response = await doormileAxios.get('/admin/partners');
return response.data.data;
};
export const createPartner = async (data) => {
const response = await doormileAxios.post('/admin/partners', data);
return response.data;
};
export const getPartner = async (id) => {
const response = await doormileAxios.get(`/admin/partners/${id}`);
return response.data.data;
};
export const updatePartner = async (id, data) => {
const response = await doormileAxios.put(`/admin/partners/${id}`, data);
return response.data;
};
export const deletePartner = async (id) => {
const response = await doormileAxios.delete(`/admin/partners/${id}`);
return response.data;
};
// ==============================|| Tenants (client companies) ||============================== //
export const getAdminTenants = async () => {
const response = await doormileAxios.get('/admin/tenants');
return response.data.data;
};
export const createAdminTenant = async (data) => {
const response = await doormileAxios.post('/admin/tenants', data);
return response.data;
};
export const getAdminTenant = async (id) => {
const response = await doormileAxios.get(`/admin/tenants/${id}`);
return response.data.data;
};
export const updateAdminTenant = async (id, data) => {
// requiredeliveryotp is a pointer server-side — omit the key entirely to leave the setting alone.
const response = await doormileAxios.put(`/admin/tenants/${id}`, data);
return response.data;
};
export const deleteAdminTenant = async (id) => {
const response = await doormileAxios.delete(`/admin/tenants/${id}`);
return response.data;
};
export const getTenantLocations = async (tenantId) => {
const response = await doormileAxios.get(`/admin/tenants/${tenantId}/locations`);
return response.data.data;
};
export const createTenantLocation = async (tenantId, data) => {
const response = await doormileAxios.post(`/admin/tenants/${tenantId}/locations`, data);
return response.data;
};
export const updateTenantLocation = async (locationId, data) => {
// Not nested under the tenant — PUT /admin/tenantlocations/:id.
const response = await doormileAxios.put(`/admin/tenantlocations/${locationId}`, data);
return response.data;
};
// ==============================|| Tenant customers (a client's own end customers) ||============================== //
export const getTenantCustomers = async () => fetchAllPages('/admin/tenantcustomers');
export const createTenantCustomer = async (data) => {
const response = await doormileAxios.post('/admin/tenantcustomers', data);
return response.data;
};
export const getTenantCustomer = async (id) => {
const response = await doormileAxios.get(`/admin/tenantcustomers/${id}`);
return response.data.data;
};
export const updateTenantCustomer = async (id, data) => {
const response = await doormileAxios.put(`/admin/tenantcustomers/${id}`, data);
return response.data;
};
export const deleteTenantCustomer = async (id) => {
const response = await doormileAxios.delete(`/admin/tenantcustomers/${id}`);
return response.data;
};
// ==============================|| B2C app customers ||============================== //
export const getAdminCustomers = async () => fetchAllPages('/admin/customers');
// POST /admin/customers.
//
// express-console-api.md lists this resource as GET + PATCH only, and states
// that a customer there comes into existence through a booking. Added on
// explicit instruction to create into the store the Customers page actually
// reads. If the route genuinely does not exist the call returns 404/405, and
// the assistant surfaces that verbatim with the endpoint name rather than
// failing silently — so an unsupported route is immediately visible.
//
// Body mirrors this resource's own GET shape.
export const createAdminCustomer = async (data) => {
const response = await doormileAxios.post('/admin/customers', data);
return response.data;
};
export const updateAdminCustomer = async (id, data) => {
const response = await doormileAxios.patch(`/admin/customers/${id}`, data);
return response.data;
};
// ==============================|| Hubs ||============================== //
export const getHubs = async () => {
const response = await doormileAxios.get('/admin/hubs');
return response.data.data;
};
export const createHub = async (data) => {
const response = await doormileAxios.post('/admin/hubs', data);
return response.data;
};
export const getHub = async (id) => {
const response = await doormileAxios.get(`/admin/hubs/${id}`);
return response.data.data;
};
export const updateHub = async (id, data) => {
const response = await doormileAxios.put(`/admin/hubs/${id}`, data);
return response.data;
};
export const deleteHub = async (id) => {
// Soft delete server-side.
const response = await doormileAxios.delete(`/admin/hubs/${id}`);
return response.data;
};
// ==============================|| Vehicles ||============================== //
export const getVehicles = async () => {
const response = await doormileAxios.get('/admin/vehicles');
return response.data.data;
};
export const createVehicle = async (data) => {
const response = await doormileAxios.post('/admin/vehicles', data);
return response.data;
};
export const getVehicle = async (id) => {
const response = await doormileAxios.get(`/admin/vehicles/${id}`);
return response.data.data;
};
export const updateVehicle = async (id, data) => {
const response = await doormileAxios.put(`/admin/vehicles/${id}`, data);
return response.data;
};
export const deleteVehicle = async (id) => {
// Soft delete server-side.
const response = await doormileAxios.delete(`/admin/vehicles/${id}`);
return response.data;
};
// ==============================|| Milers (riders) ||============================== //
export const getMilers = async () => {
// Tenant-scoped server-side from the token — no tenantid param needed/accepted.
const response = await doormileAxios.get('/admin/milers');
return response.data.data;
};
export const createMiler = async (data) => {
// configid defaults to 1001 server-side (the miler-app login partition) — do not override it.
const response = await doormileAxios.post('/admin/milers', data);
return response.data;
};
export const getMiler = async (id) => {
const response = await doormileAxios.get(`/admin/milers/${id}`);
return response.data.data;
};
export const updateMiler = async (id, data) => {
const response = await doormileAxios.put(`/admin/milers/${id}`, data);
return response.data;
};
export const blockMiler = async (id, data) => {
const response = await doormileAxios.put(`/admin/milers/${id}/block`, data);
return response.data;
};
export const assignMilerVehicle = async (id, data) => {
const response = await doormileAxios.put(`/admin/milers/${id}/assign-vehicle`, data);
return response.data;
};
export const notifyMiler = async (milerProfileId, title, message) => {
const response = await doormileAxios.post(`/admin/milers/${milerProfileId}/notify`, { title, message });
return response.data;
};
// Confirmed against jupiter2doormile.md (Status: Done — tested against a real
// request), not in express-console-api.md. jupiter used fromdate/todate;
// Doormile uses from/to (both YYYY-MM-DD). Aggregate rider KPI counts +
// current position (currentlatitude/currentlongitude/lastpingat per the doc —
// unconfirmed beyond those three field names).
export const getMilerSummary = async (applocationid, from, to, tenantid, hubid) => {
const response = await doormileAxios.get(`/admin/milers/summary${buildQuery({ applocationid, from, to, tenantid, hubid })}`);
return response.data.data;
};
// Replaces jupiter's `riderlogs` table — the rider's GPS/status ping history.
export const getMilerLogs = async (id, from, to, limit) => {
const response = await doormileAxios.get(`/admin/milers/${id}/logs${buildQuery({ from, to, limit })}`);
return response.data.data;
};
// No jupiter equivalent — new in Doormile.
export const getMilerActivity = async (id, from, to) => {
const response = await doormileAxios.get(`/admin/milers/${id}/activity${buildQuery({ from, to })}`);
return response.data.data;
};
// ==============================|| Bookings ||============================== //
export const getBookings = async (pageno, pagesize) => {
const response = await doormileAxios.get(`/admin/bookings${buildQuery({ pageno, pagesize })}`);
return response.data.data;
};
// Same request as getBookings, but keeps the envelope's `total`/`page` instead
// of throwing them away.
//
// getBookings returns `response.data.data` — just the rows — which means no
// caller can tell a full result from a truncated one. `pagesize` is capped at
// 1000 server-side (express-console-api.md, "Conventions": default 500, cap
// 1000), so any account with more than 1000 bookings silently gets a partial
// list from a single call. That's tolerable for a paginated table (it fetches
// the next page as you scroll) but NOT for anything that counts or sums, which
// would report a confidently wrong number.
//
// Kept as a separate export rather than changing getBookings' return shape:
// four callers (fetchDeliveries, fetchCountAPI, orders.js, the assistant's own
// per-order lookup) already destructure it as a bare array.
export const getBookingsPage = async (pageno, pagesize) => {
const response = await doormileAxios.get(`/admin/bookings${buildQuery({ pageno, pagesize })}`);
const body = response.data || {};
return {
rows: body.data || [],
// `total` is documented as present on list responses; fall back to the row
// count so a backend that omits it degrades to "this is everything" rather
// than to NaN-driven pagination.
total: Number.isFinite(Number(body.total)) ? Number(body.total) : (body.data || []).length,
page: Number(body.page) || pageno
};
};
export const createExpressBooking = async (data) => {
// tenantid is forced to the caller's own tenant server-side on client logins.
// CityGate applies: pickup pincode prefix must be an open city (641/600/560/500/629).
const response = await doormileAxios.post('/admin/expressbooking', data);
return response.data;
};
export const createExpressBookingBulk = async (bookings) => {
// Max 200 per call; response carries per-row results.
const response = await doormileAxios.post('/admin/expressbooking/bulk', { bookings });
return response.data;
};
export const getBooking = async (id) => {
const response = await doormileAxios.get(`/admin/bookings/${id}`);
return response.data.data;
};
export const assignMilerToBooking = async (id, data) => {
const response = await doormileAxios.post(`/admin/bookings/${id}/assign-miler`, data);
return response.data;
};
// Doormile-native bulk assign: picks real milers via Redis GEO + AI scoring, no
// external solver involved. Commits server-side in this one call — unlike the
// workolik solver flow, there's no separate preview/reconcile/commit step.
// Sequencing (POST /optimization/doormile/sequence) is not deployed yet, so
// riders with more than one stop come back unsequenced (step: 0) — see
// doormile-flow.md's State table.
export const batchAssignBookings = async (bookingIds, maxPerRider = 5) => {
const response = await doormileAxios.post('/hub/bookings/batch-assign', {
bookingids: bookingIds,
max_per_rider: maxPerRider
});
return response.data;
};
export const assignVehicleToBooking = async (id, data) => {
const response = await doormileAxios.post(`/admin/bookings/${id}/assign-vehicle`, data);
return response.data;
};
export const updateBookingStatus = async (id, data) => {
const response = await doormileAxios.put(`/admin/bookings/${id}/status`, data);
return response.data;
};
export const cancelBooking = async (id, data) => {
const response = await doormileAxios.post(`/admin/bookings/${id}/cancel`, data);
return response.data;
};
export const bulkCancelBookings = async (bookingIds) => {
const response = await doormileAxios.post('/admin/bookings/bulk-cancel', { bookingIds });
return response.data;
};
// Confirmed against jupiter2doormile.md (Status: Done), not in
// express-console-api.md.
export const getBookingTrack = async (id) => {
const response = await doormileAxios.get(`/admin/bookings/${id}/track`);
return response.data.data;
};
// ==============================|| Consignments ||============================== //
export const getConsignments = async () => {
const response = await doormileAxios.get('/admin/consignments');
return response.data.data;
};
// Confirmed against jupiter2doormile.md (Status: Done) — the actual GPS/status
// trail for a consignment, not in express-console-api.md.
export const getConsignmentLogs = async (id) => {
const response = await doormileAxios.get(`/admin/consignments/${id}/logs`);
return response.data.data;
};
export const getConsignment = async (id) => {
const response = await doormileAxios.get(`/admin/consignments/${id}`);
return response.data.data;
};
export const trackConsignment = async (trackingno) => {
const response = await doormileAxios.get(`/admin/consignments/track/${trackingno}`);
return response.data.data;
};
export const updateConsignmentStatus = async (id, data) => {
const response = await doormileAxios.put(`/admin/consignments/${id}/status`, data);
return response.data;
};
// ==============================|| Tripsheets (hub-to-hub transport) ||============================== //
export const getTripsheets = async () => {
const response = await doormileAxios.get('/admin/tripsheets');
return response.data.data;
};
export const createTripsheet = async (data) => {
const response = await doormileAxios.post('/admin/tripsheets', data);
return response.data;
};
export const getTripsheet = async (id) => {
const response = await doormileAxios.get(`/admin/tripsheets/${id}`);
return response.data.data;
};
export const addTripsheetItem = async (id, consignmentid) => {
const response = await doormileAxios.post(`/admin/tripsheets/${id}/items`, { consignmentid });
return response.data;
};
export const removeTripsheetItem = async (id, itemId) => {
const response = await doormileAxios.delete(`/admin/tripsheets/${id}/items/${itemId}`);
return response.data;
};
export const dispatchTripsheet = async (id) => {
const response = await doormileAxios.put(`/admin/tripsheets/${id}/dispatch`);
return response.data;
};
export const arriveTripsheet = async (id) => {
const response = await doormileAxios.put(`/admin/tripsheets/${id}/arrive`);
return response.data;
};
// ==============================|| Pricing ||============================== //
export const getAdminPricing = async () => {
const response = await doormileAxios.get('/admin/pricing');
return response.data.data;
};
export const createAdminPricing = async (data) => {
const response = await doormileAxios.post('/admin/pricing', data);
return response.data;
};
export const updateAdminPricing = async (id, data) => {
const response = await doormileAxios.put(`/admin/pricing/${id}`, data);
return response.data;
};
export const deleteAdminPricing = async (id) => {
const response = await doormileAxios.delete(`/admin/pricing/${id}`);
return response.data;
};
export const simulatePricing = async (data) => {
const response = await doormileAxios.post('/admin/pricing/simulate', data);
return response.data;
};
export const quotePricing = async (data) => {
// Same handler as simulate.
const response = await doormileAxios.post('/admin/pricing/quote', data);
return response.data;
};
export const getDoormilePricing = async () => {
const response = await doormileAxios.get('/admin/doormile-pricing');
return response.data.data;
};
export const createDoormilePricing = async (data) => {
const response = await doormileAxios.post('/admin/doormile-pricing', data);
return response.data;
};
export const updateDoormilePricing = async (id, data) => {
const response = await doormileAxios.put(`/admin/doormile-pricing/${id}`, data);
return response.data;
};
export const deleteDoormilePricing = async (id) => {
// Soft delete server-side.
const response = await doormileAxios.delete(`/admin/doormile-pricing/${id}`);
return response.data;
};
// ==============================|| Exceptions ||============================== //
export const getExceptions = async () => {
const response = await doormileAxios.get('/admin/exceptions');
return response.data.data;
};
export const createException = async (data) => {
const response = await doormileAxios.post('/admin/exceptions', data);
return response.data;
};
export const getException = async (id) => {
const response = await doormileAxios.get(`/admin/exceptions/${id}`);
return response.data.data;
};
export const updateExceptionStatus = async (id, status, resolution) => {
// status: 'Resolved' | 'Closed'.
const response = await doormileAxios.put(`/admin/exceptions/${id}/status`, { status, resolution });
return response.data;
};
// ==============================|| Competitive intel ||============================== //
// Confirmed live: this resource paginates on page/limit, not the
// pageno/pagesize convention used everywhere else in the API — and defaults
// to only 50 rows (there are 339+ competitor branches on a real tenant), so
// callers must page through it explicitly rather than trusting one fetch to
// return everything. Returns the full envelope (data/limit/page/total) so
// the caller can build pagination controls.
export const getCompetitorBranches = async (page, limit) => {
const response = await doormileAxios.get(`/admin/competitor-branches${buildQuery({ page, limit })}`);
return response.data;
};
export const createCompetitorBranch = async (data) => {
const response = await doormileAxios.post('/admin/competitor-branches', data);
return response.data;
};
export const updateCompetitorBranch = async (id, data) => {
const response = await doormileAxios.put(`/admin/competitor-branches/${id}`, data);
return response.data;
};
export const deleteCompetitorBranch = async (id) => {
const response = await doormileAxios.delete(`/admin/competitor-branches/${id}`);
return response.data;
};
// Same page/limit pagination convention as competitor-branches (confirmed
// live) — 368+ rows on a real tenant, default page returns only 100.
export const getCarrierPricing = async (page, limit) => {
const response = await doormileAxios.get(`/admin/carrier-pricing${buildQuery({ page, limit })}`);
return response.data;
};
export const createCarrierPricing = async (data) => {
const response = await doormileAxios.post('/admin/carrier-pricing', data);
return response.data;
};
export const updateCarrierPricing = async (id, data) => {
const response = await doormileAxios.put(`/admin/carrier-pricing/${id}`, data);
return response.data;
};
export const deleteCarrierPricing = async (id) => {
const response = await doormileAxios.delete(`/admin/carrier-pricing/${id}`);
return response.data;
};

29
src/api/doormile/index.js Normal file
View File

@@ -0,0 +1,29 @@
/**
* The Doormile Express data layer — the single import surface for pages.
*
* import { getHubs, fetchDeliveries } from '@/api/doormile';
*
* Three files sit underneath:
*
* - `client.js` — the axios instance, the bearer token, the 401 handling.
* - `endpoints.js` — one function per `api.doormile.com/api/v1/admin/*` route.
* - `queries.js` — the page-facing layer: joins, status mapping, the solver
* calls, and the safe empty defaults for the resources the
* admin API has no equivalent for.
*
* Pages should generally reach for the React Query hooks in
* `@/lib/doormileHooks` instead, so cache keys stay coherent across the app.
*/
export { default as doormileAxios } from './client';
export {
DOORMILE_BASE_URL,
DOORMILE_TOKEN_KEY,
DOORMILE_USER_KEY,
errorMessage,
readStoredToken,
readStoredUser,
} from './client';
export { OpenToast, messageOf } from './notify';
export * from './endpoints';
export * from './queries';

View File

@@ -0,0 +1,22 @@
import { toast } from '@/components/ds/toast';
/**
* The data layer's toast seam.
*
* The Express Console signals API failures with `OpenToast(message, variant,
* duration)`. Keeping that call signature means the ported query functions read
* exactly like their originals — the variants just resolve to the KROW design
* system's toast instead of notistack, so a failed fetch looks like every other
* notification in this app rather than importing a second toast system.
*/
export const OpenToast = (message, variant = 'error', duration) => {
const text = String(message || 'Something went wrong');
const options = duration ? { duration } : undefined;
if (variant === 'success') return toast.success(text, options);
if (variant === 'warning' || variant === 'info') return toast.info(text, options);
return toast.error(text, options);
};
/** The message a rejected Doormile call should show. */
export const messageOf = (err, fallback = 'Something went wrong') =>
err?.response?.data?.message || err?.message || fallback;

1107
src/api/doormile/queries.js Normal file

File diff suppressed because it is too large Load Diff