# Doormile Hub Console — Frontend Integration Guide **Base URL:** `https://api.doormile.com` **Auth:** Bearer token in `Authorization` header on every authenticated request **Content-Type:** `application/json` on all POST / PATCH calls ## How the flow works 1. Staff opens the console → Login page 2. `POST /api/v1/hub/login` → receives JWT + hub context 3. Store token + hub context in localStorage 4. Every page reads hub name/city from stored context (no extra call) 5. Every API call sends `Authorization: Bearer ` 6. On 401 → clear localStorage → redirect to `/login` --- ## 1. Authentication ### POST /api/v1/hub/login No auth header needed. Request body: ```json { "email": "hub.coimbatore@doormile.in", "password": "password123" } ``` Response: ```json { "success": true, "token": "eyJhbGci...", "hub": { "hubid": 1, "hubname": "Coimbatore Jupiter Hub", "hubtype": "sorting_center", "city": "Coimbatore", "capacity": 50 }, "staff": { "displayname": "Coimbatore Hub Staff", "email": "hub.coimbatore@doormile.in", "role": 6 }, "is_doormile_staff": true } ``` What to store in localStorage after login: ```js localStorage.setItem('hub_token', data.token) localStorage.setItem('hub_context', JSON.stringify(data.hub)) localStorage.setItem('hub_staff', JSON.stringify(data.staff)) localStorage.setItem('hub_is_doormile', String(data.is_doormile_staff)) ``` Test accounts: | Email | Password | Hub | Access | |---|---|---|---| | hub.coimbatore@doormile.in | password123 | Coimbatore Jupiter Hub | Full (Doormile staff) | | hub.hyderabad@doormile.in | password123 | Hyderabad hub | Full (Doormile staff) | | hub.bangalore@doormile.in | password123 | Bangalore hub | Full (Doormile staff) | | hub.chennai@doormile.in | password123 | Chennai hub | Full (Doormile staff) | | hub@kpmtravels.in | password123 | Hub 1 | Restricted (partner — no hub management) | --- ## 2. Dashboard ### GET /api/v1/hub/dashboard Returns live KPI stats for the logged-in staff's hub. Response: ```json { "success": true, "data": { "parcels_received_today": 142, "milers_available": 6, "milers_on_duty": 12, "pending_pickups": 8, "batches_sent_today": 3, "exceptions": 2 } } ``` Which UI element uses which field: | Dashboard card | Field | |---|---| | Total Parcels Received | `parcels_received_today` | | Available Milers | `milers_available` | | Milers on Duty | `milers_on_duty` | | Pending Pickups | `pending_pickups` | | Batches Sent Today | `batches_sent_today` | | Needs Checking | `exceptions` | **Note:** Hub name shown in the header/title comes from localStorage (`hub_context.hubname`) — not from this endpoint. No extra call needed. --- ## 3. Inbound — Receive Parcels ### GET /api/v1/hub/inbound/today Returns all parcels inbounded at this hub today. Now returns `sendername`, `originname`, `destinationname`, `temperature` (readable names instead of raw IDs and pincodes). Response: ```json { "success": true, "data": [ { "bookingid": 15, "trackingnumber": "DM-882201", "sendername": "Acme Corp", "originname": "Mumbai Hub", "destinationname": "Dwarka Sec 12, Coimbatore", "weight": "2.4 kg", "condition": "Good", "temperature": "N/A", "shelf": "Zone A (Shelf 1)", "inboundedat": "2026-07-04T09:15:00Z" } ], "total": 1 } ``` ### POST /api/v1/hub/bookings/:id/inbound Scan a parcel in. `:id` is the booking/consignment ID. Request body: ```json { "tracking_id": "DM-882201", "condition": "Good", "temperature": "N/A", "shelf": "Zone A (Shelf 1)", "weight": "2.4 kg" } ``` Condition values: `Good` · `Damaged Box` · `Wet / Crushed` · `Missing Label` Temperature: Free text — "4.1°C" for cold chain items, "N/A" for dry goods Response: ```json { "success": true, "data": { "bookingid": 15, "trackingnumber": "DM-882201", "recommended_shelf": "Zone A (Shelf 1)", "status": "At_Hub" } } ``` Shelf recommendation logic (backend handles this automatically): - Condition contains Damaged / Wet / Missing → Exception Area - Temperature is not N/A → Zone C (Cold Room) - Otherwise → Zone A or Zone B based on destination Error — tracking ID not found: ```json { "success": false, "message": "booking not found" } ``` HTTP 404. --- ## 4. Order Assignment — Pickup Requests ### GET /api/v1/hub/bookings/unassigned Returns all pending pickup requests for this hub's city. Response: ```json { "success": true, "data": [ { "bookingid": 21, "customerName": "Ramesh K.", "pickupaddress": "Gandhipuram, Coimbatore", "deliveryaddress": "Hitech City, Hyderabad", "packagedescription": "Small Box, 2kg", "createdat": "2026-07-04T08:45:00Z", "status": "Pending_Pickup" } ], "total": 3 } ``` For assigning a miler to these bookings, see **Section 14 — Hub Assignment Override**. --- ## 5. Dispatch & Batches ### GET /api/v1/hub/batches Returns all outgoing batches for this hub. Response: ```json { "success": true, "data": [ { "tripsheetid": 1, "batchlabel": "BATCH-9281", "batchkind": "transfer", "destinationlabel": "Mumbai Hub (BOM-02)", "vehicle": "Truck DL-01-BZ-8055", "itemcount": 154, "status": "Draft", "createdat": "2026-07-04T08:00:00Z", "dispatchtime": null } ], "total": 1 } ``` Status values: `Draft` → `Ready` → `Dispatched` Batchkind values: `local` · `transfer` ### POST /api/v1/hub/batches Create a new outgoing batch. Request body: ```json { "route": "Transfer to Mumbai Hub", "destination": "Mumbai Hub (BOM-02)", "vehicle": "Truck DL-01-BZ-8055", "parcels_count": 154, "kind": "transfer" } ``` Response: ```json { "success": true, "data": { "tripsheetid": 2, "batchlabel": "BATCH-9282", "status": "Draft" } } ``` ### PATCH /api/v1/hub/batches/:id/status Move a batch through its status flow. Request body — mark as ready: ```json { "status": "Ready" } ``` Request body — dispatch (send out): ```json { "status": "Dispatched" } ``` Response: ```json { "success": true, "data": { "tripsheetid": 1, "status": "Ready", "dispatchtime": null } } ``` Invalid status transition → 400: ```json { "success": false, "message": "invalid status" } ``` --- ## 6. Milers ### GET /api/v1/hub/milers Returns all milers assigned to this hub, now with operational stats per miler. Response: ```json { "success": true, "data": [ { "userid": 4, "displayname": "Rajesh Kumar", "phone": "+91 98765 43210", "vehicleid": 1, "hubid": 1, "availabilitystatus": "Available", "rating": 4.8, "completedorders": 45, "cancelledorders": 2, "currentlat": 11.0168, "currentlon": 76.9558, "device_token": "fcm_token_here", "zones": ["641001", "641012"], "assignedload": 2, "capacity": 30, "pickupspending": 1, "codcollected": 4500.00, "codpending": 1200.00, "checkinat": "2026-07-04T08:12:00Z", "hoursactive": 6.4, "isverified": true } ], "total": 6 } ``` Availability status values: `Available` · `Assigned` · `On_Break` · `Offline` The `zones`, `assignedload`, `capacity`, `pickupspending`, `codcollected`, `codpending`, `checkinat`, `hoursactive`, `isverified` fields were the empty fields the Milers page was trying to show. Now populated from real data. ### POST /api/v1/admin/milers Create (onboard) a new miler. Hub JWT is accepted here. Request body: ```json { "displayname": "Muthu Kumar", "phone": "+91 90011 22334", "hubid": 1, "vehicleid": 2, "availabilitystatus": "Available" } ``` Response: ```json { "success": true, "data": { "userid": 10, "displayname": "Muthu Kumar" } } ``` ### PATCH /api/v1/admin/milers/:id Update a miler's details or status. Request body (any subset of fields): ```json { "availabilitystatus": "On_Break", "hubid": 2 } ``` ### DELETE /api/v1/admin/milers/:id Remove a miler from the roster. Response: ```json { "success": true, "message": "miler removed" } ``` --- ## 7. Routing — Where Does It Go? ### GET /api/v1/hub/routing/:trackingno Scan a parcel and get its sort destination. `:trackingno` is the tracking number scanned at the sort station. Response: ```json { "success": true, "data": { "trackingno": "DM-TRK-0015", "destination": "Peelamedu, 641004", "recommendedshelf": "Zone B", "nexthop": "Local delivery", "condition": "Good", "iscoldchain": false, "weight": "2.4 kg", "customername": "Ramesh K.", "bookingid": 15 } } ``` `nexthop` values: `Local delivery` · `Transfer to Mumbai Hub` (or whichever destination hub). Use `recommendedshelf` to show the big shelf indicator UI. Error — tracking number not found → 404. Show a "Parcel not found" error state. ```json { "success": false, "message": "parcel not found" } ``` --- ## 8. Rider Routes ### GET /api/v1/hub/rider-routes Returns all milers at this hub with their planned stops today. ### GET /api/v1/hub/milers/:id/route Returns the planned stops for a single miler. `:id` is the miler user ID. Response (same `data[]` shape for both; the single-miler endpoint returns one entry): ```json { "success": true, "data": [ { "mileruserid": 4, "milername": "Rajesh Kumar", "mode": "pickup", "totalstops": 5, "completedstops": 2, "totaldistance_km": 12.4, "stops": [ { "seq": 1, "address": "Gandhipuram, Coimbatore", "lat": 11.0168, "lon": 76.9558, "items": 1, "bookingid": 20, "status": "completed", "eta_minutes": 0 }, { "seq": 2, "address": "RS Puram, Coimbatore", "lat": 11.001, "lon": 76.951, "items": 2, "bookingid": 21, "status": "pending", "eta_minutes": 12 } ] } ] } ``` `mode` values: `pickup` · `delivery` Stop status values: `pending` · `in_progress` · `completed` Pass `stops[].lat/lon` to OSRM to draw the route line — the OSRM call stays client-side. --- ## 9. Live Trucks ### GET /api/v1/hub/tripsheets/in-transit Returns transfer trucks currently in transit, with interpolated live positions. Poll this every 5 seconds alongside miler locations. Response: ```json { "success": true, "data": [ { "tripsheetid": 3, "tripsheetno": "DM-TS-0003", "label": "Chennai Jupiter Hub → Coimbatore Jupiter Hub", "originlat": 13.0827, "originlon": 80.2707, "destlat": 11.0168, "destlon": 76.9558, "currentlat": 12.1, "currentlon": 78.9, "status": "In_Transit", "progresspct": 40, "vehicleno": "TN-04-AX-8822", "itemcount": 154, "eta_minutes": 180 } ] } ``` `currentlat/lon` gives the truck's position on the map. Replace the fake animated truck with these real coordinates. --- ## 10. Arriving Vehicles ### GET /api/v1/hub/inbound/vehicles Returns trucks arriving at this hub (Dashboard → "Trucks Arriving" table). Response: ```json { "success": true, "data": [ { "vehicleno": "TN-04-AX-8822", "origin": "Chennai Jupiter Hub", "tripsheetid": 3, "status": "On the way", "eta": "2 hrs 30 min", "unloadedpct": 0, "itemcount": 154 } ] } ``` Status values: `On the way` · `Unloading` (Dispatched maps to `On the way`, Arrived maps to `Unloading`.) --- ## 11. Activity Feed ### GET /api/v1/hub/activity?limit=10 Returns a real event feed for this hub (UNION across 4 sources: inbound, dispatch, exceptions, sorting). Response: ```json { "success": true, "data": [ { "time": "2026-07-04T11:24:00Z", "type": "exception", "text": "Exception raised: Damaged on DM-TRK-999999" }, { "time": "2026-07-04T11:22:00Z", "type": "inbound", "text": "Received parcel DM-TRK-999999 from Coimbatore Jupiter Hub" }, { "time": "2026-07-04T11:20:00Z", "type": "dispatch", "text": "Dispatched batch DM-TS-0003 to Mumbai Hub" } ], "total": 3 } ``` Type values: `inbound` · `dispatch` · `exception` · `sorting` Use `type` to pick the icon color — same color scheme as the existing mock. --- ## 12. Zones ### GET /api/v1/hub/zones Returns the delivery zone breakdown for this hub (Dashboard → "Delivery Areas"). Response: ```json { "success": true, "data": [ { "zone": "641001", "zonename": "Gandhipuram", "parcels": 12, "milers": 2, "status": "Active" }, { "zone": "641004", "zonename": "Peelamedu", "parcels": 8, "milers": 1, "status": "Active" }, { "zone": "641012", "zonename": "Jupiter Nagar","parcels": 3, "milers": 0, "status": "Need Milers" } ] } ``` Status values: `Active` · `Need Milers` — use this to color the status chip. --- ## 13. Notifications ### GET /api/v1/hub/notifications Returns notifications for the header bell icon, generated from real events. Response: ```json { "success": true, "data": [ { "id": 1, "title": "Exception: Damaged", "type": "exception", "time": "3 min ago", "read": false }, { "id": 2, "title": "Truck arriving from Chennai Hub", "type": "inbound", "time": "5 min ago", "read": false }, { "id": 3, "title": "Pickup waiting 30+ min", "type": "alert", "time": "12 min ago", "read": false } ], "total": 3 } ``` Type values: `exception` · `inbound` · `warning` · `alert` ### PATCH /api/v1/hub/notifications/:id/read Mark a notification as read. Response: ```json { "success": true } ``` On bell click → `GET /hub/notifications`. On item click → `PATCH /hub/notifications/:id/read`, then mark it read in local state. --- ## 14. Hub Assignment Override The old admin assign endpoint (`POST /admin/bookings/:id/assign-miler`) returned **403** for hub staff. Two new hub-scoped endpoints replace it. ### POST /api/v1/hub/bookings/:id/assign-miler Manual assign — hub staff picks the miler. Request body: ```json { "mileruserid": 4 } ``` Response: ```json { "success": true, "data": { "bookingid": 21, "mileruserid": 4, "milername": "Rajesh Kumar", "status": "Miler_Assigned" } } ``` Use this on the **Pickup Requests** page when staff manually selects a miler from the dialog. ### POST /api/v1/hub/bookings/:id/auto-assign Auto-assign — AI engine picks the miler. Request body: ```json {} ``` Response (success): ```json { "success": true, "data": { "mileruserid": 4, "milername": "Rajesh Kumar", "distance_km": 0.4 } } ``` Response (no miler) — HTTP 422: ```json { "success": false, "message": "No eligible miler found in range" } ``` Response (timeout) — HTTP 202: ```json { "success": true, "message": "assignment in progress" } ``` Use this for the **Auto-Assign** bulk button. Loop over selected booking IDs and call this for each one. **Backend implementation notes:** shared `AssignMilerToBooking()` service (`booking_assignment_service.go`), `TryAssignOnce()` single-attempt wrapper on `crm_assignment`, `assignedbyuserid` is nullable for hub-initiated assignments, and `AdminAssignMiler` now sends FCM to the miler. --- ## 15. Hub Management (Doormile staff only) These endpoints return 403 for partner accounts (`is_doormile_staff = false`). Use the `is_doormile_staff` flag from localStorage to show/hide the UI for these. ### GET /api/v1/hub/hubs List all hubs in the logged-in staff's city. Now includes `lat`/`lon` for map pins. Response: ```json { "success": true, "data": [ { "hubid": 1, "hubname": "Coimbatore Jupiter Hub", "hubtype": "sorting_center", "capacity": 50, "status": "active", "has_staff": true, "lat": 11.0168, "lon": 76.9558 }, { "hubid": 2, "hubname": "Coimbatore Spoke 1", "hubtype": "spoke", "capacity": 50, "status": "active", "has_staff": false, "lat": 11.0210, "lon": 76.9610 } ] } ``` Use `lat/lon` to place hub pins on the Live Map. ### POST /api/v1/hub/hubs Create a new hub in the staff's city. Request body: ```json { "hubname": "Coimbatore RS Puram Hub", "hubtype": "spoke", "capacity": 30, "contact": "+91 98765 00000", "address": "RS Puram, Coimbatore", "pincode": "641002" } ``` **Note:** `applocationid` (city) is set automatically from the logged-in staff's hub — cannot be overridden. ### POST /api/v1/hub/staff Create a hub staff login for any hub in the city. Request body: ```json { "hubid": 2, "email": "hub.rspuram@doormile.in", "password": "securepassword", "displayname": "RS Puram Hub Staff", "tenantid": null } ``` Set `tenantid` to the partner's tenant ID for partner accounts, `null` for Doormile staff. Response: ```json { "success": true, "data": { "staffid": 6, "email": "hub.rspuram@doormile.in", "hubid": 2 } } ``` --- ## Error response shape All errors follow the same shape: ```json { "success": false, "message": "human readable error description" } ``` | HTTP code | Meaning | |---|---| | 400 | Bad request — invalid body or status transition | | 401 | Token expired or missing → redirect to /login | | 202 | Accepted — async assignment in progress | | 403 | Forbidden — partner account trying Doormile-only endpoint | | 404 | Resource not found (booking ID, miler ID, tracking no etc.) | | 422 | Unprocessable — no eligible miler found in range | | 500 | Server error — show generic "something went wrong" toast | --- ## Page-to-endpoint map (quick reference) | Page | Endpoints called | |---|---| | Login | POST /hub/login | | Dashboard | GET /hub/dashboard · GET /hub/inbound/vehicles · GET /hub/activity · GET /hub/zones | | Receive Parcels | GET /hub/inbound/today · POST /hub/bookings/:id/inbound | | Pickup Requests | GET /hub/bookings/unassigned · GET /hub/milers · POST /hub/bookings/:id/assign-miler · POST /hub/bookings/:id/auto-assign | | Where Does It Go? (Routing) | GET /hub/routing/:trackingno | | Dispatch | GET /hub/batches · POST /hub/batches · PATCH /hub/batches/:id/status | | Milers | GET /hub/milers · POST /admin/milers · PATCH /admin/milers/:id · DELETE /admin/milers/:id | | Rider Routes | GET /hub/rider-routes · GET /hub/milers/:id/route | | Live Map | GET /admin/milers/locations (poll 5s) · GET /hub/tripsheets/in-transit (poll 5s) · GET /hub/hubs | | Header — Notifications | GET /hub/notifications · PATCH /hub/notifications/:id/read | | Hub Settings | GET /hub/hubs · POST /hub/hubs · POST /hub/staff | --- ## Frontend implementation notes **Token storage:** ```js // On login success: localStorage.setItem('hub_token', response.token) localStorage.setItem('hub_context', JSON.stringify(response.hub)) localStorage.setItem('hub_staff', JSON.stringify(response.staff)) localStorage.setItem('hub_is_doormile', String(response.is_doormile_staff)) // On every API call: headers: { 'Authorization': `Bearer ${localStorage.getItem('hub_token')}` } // On logout or 401: localStorage.clear() navigate('/login') ``` **Show hub name dynamically (not hardcoded):** ```js const hub = JSON.parse(localStorage.getItem('hub_context')) // hub.hubname → "Coimbatore Jupiter Hub" // hub.city → "Coimbatore" // hub.hubid → 1 ``` **Hide hub management UI for partners:** ```js const isDoormile = localStorage.getItem('hub_is_doormile') === 'true' // Show "Add New Hub" / "Add Staff" buttons only if isDoormile === true // KPM partner account gets false → those buttons hidden ``` **On 401 — token expired, force re-login:** ```js if (response.status === 401) { localStorage.clear() window.location.href = '/login' } ``` **Polling intervals:** - Miler locations → every 5 seconds - Truck positions → every 5 seconds (same call cycle) - Dashboard stats → every 30 seconds - Notifications → every 60 seconds --- *All endpoints live at `https://api.doormile.com` · Hub Console Backend v1.0 complete*