updates on the ui changes and changed into doormile
This commit is contained in:
93
src/api/doormile/client.js
Normal file
93
src/api/doormile/client.js
Normal 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;
|
||||
655
src/api/doormile/endpoints.js
Normal file
655
src/api/doormile/endpoints.js
Normal 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
29
src/api/doormile/index.js
Normal 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';
|
||||
22
src/api/doormile/notify.js
Normal file
22
src/api/doormile/notify.js
Normal 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
1107
src/api/doormile/queries.js
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user