rider partner page

This commit is contained in:
2026-09-09 15:43:23 +05:30
parent 32c612a10d
commit b760c1a078
18 changed files with 1802 additions and 63 deletions

View File

@@ -33,38 +33,59 @@ export const RIDER_MESSAGE = {
: `${count} orders have been assigned to you. Kindly accept and process the deliveries.`,
} as const;
/**
* Which fleet to ask for. One of these, in this order of preference.
*
* `getriders` scopes by applocation, partner or tenant. It used to be called
* with the region ALONE, which asks "who is on duty in this city" — so a
* merchant's assign picker offered every on-duty rider in Coimbatore, including
* other merchants' own riders and every other partner's.
*
* Measured 2026-09-09: 118 riders across three regions, and 117 of them belong
* to a delivery partner — 75 to partner 44 alone. Exactly one rider on the
* platform is a merchant's own. So the region scope was not a harmless default;
* it was the only thing holding the picker together while the two real scopes
* went unused.
*/
export interface RiderQuery {
/** The merchant's own riders — hired by them, working their branches. */
tenantid?: number;
/** A delivery partner's riders. One partner supplies many merchants. */
partnerid?: number;
/**
* The delivery region, and for now the only scope that finds anybody.
* The delivery region — a CITY, and the fallback for neither of the above.
*
* `tenantid` is accepted too and the query is sound — it just matches nothing
* yet, because `app_users.tenantid` was never filled in for a rider. Riders
* hired through this console DO carry one, so tenant scope starts working the
* moment a merchant has their own.
*
* It is not the scope used here, and that is deliberate: production has 84
* riders on applocation 1 and none of them has a tenant, so switching today
* would empty the picker for everybody. Revisit once merchants have hired
* their own — preferring tenant and falling back to region.
*
* Note what region means: a CITY. Until then an operator is offered every
* on-duty rider in Coimbatore, including other merchants'.
* Kept because a caller with no merchant in hand still has to ask something,
* not because it is the right scope for an assign picker.
*/
applocationid: number;
applocationid?: number;
}
export const deliveriesApi = {
/**
* Riders on duty right now.
* Riders on duty right now, for one OWNER.
*
* "On duty" is the backend's word, not a filter added here: the query wants
* `app_userpools.onduty = 1` and a `riderlogs` row stamped today with
* `logstatus = 0`. So this list empties overnight and refills as riders clock
* on, and an empty answer means nobody has started their shift — not that
* the shop has no riders. The picker has to say which.
*
* ── Why this is scoped and used to not be ─────────────────────────────────
*
* It sent `applocationid` alone, which asks "who is on duty in this city" —
* so a merchant's assign picker listed every on-duty rider in Coimbatore,
* including other merchants' own riders and every partner's. Nobody hit it
* because there is one rider on the platform. `getriders` scopes by
* applocation, partner or tenant, in that order, so the caller names which
* fleet it means and the region is only a fallback for neither.
*/
riders: (query: RiderQuery) =>
api.list<RiderInfo>(`${WEB}/partners/getriders`, { applocationid: query.applocationid }),
api.list<RiderInfo>(`${WEB}/partners/getriders`, {
...(query.tenantid ? { tenantid: query.tenantid } : {}),
...(query.partnerid ? { partnerid: query.partnerid } : {}),
...(query.tenantid || query.partnerid ? {} : { applocationid: query.applocationid }),
}),
/**
* Hand orders to a rider.
@@ -165,7 +186,18 @@ export interface NewRider {
password?: string;
/** The delivery region. Defaulted from the branch — see `RiderDrawer`. */
applocationid: number;
/**
* Whose rider this is — one of these, never both.
*
* `tenantid` is a merchant's own rider; `partnerid` is a delivery partner's,
* who serves several merchants and sits under no single one. The server
* refuses neither and refuses both, so the two can never be confused
* downstream in a directory or an assign picker.
*/
tenantid?: number;
partnerid?: number;
/** The branch an OWN rider works out of. Meaningless for a partner's. */
locationid?: number;
shiftid: number;
identificationno?: string;
vehiclename?: string;
@@ -191,6 +223,9 @@ export interface RiderRosterRow {
contactno?: string;
email?: string;
tenantid?: number;
/** The branch an own rider works out of, and its name. */
locationid?: number;
locationname?: string;
applocationid?: number;
applocation?: string;
partnerid?: number;
@@ -213,9 +248,58 @@ export interface RiderRosterRow {
export interface Partner {
partnerid: number;
partnername?: string;
companyname?: string;
applocationid?: number;
primarycontact?: string;
primaryemail?: string;
contactno?: string;
registrationno?: string;
address?: string;
suburb?: string;
city?: string;
state?: string;
status?: string;
}
/** One region a partner covers — a row of `partnerlocations`. */
export interface PartnerLocation {
partnerlocationid: number;
partnerid: number;
applocationid: number;
applocation?: string;
}
/** A delivery region. `applocationid=0` asks for all of them. */
export interface AppLocation {
applocationid: number;
locationname?: string;
}
/** Everything the console collects to onboard a delivery partner. */
export interface NewPartner {
partnerid?: number;
partnername: string;
companyname?: string;
registrationno?: string;
primarycontact: string;
primaryemail?: string;
contactno?: string;
address?: string;
suburb?: string;
city?: string;
state?: string;
postcode?: number;
status?: string;
/** The district they work — one, never a set. */
applocationid: number;
/**
* The district by NAME, for one Nearle has not opened yet.
*
* Sending it opens the district: the server writes the `app_location` and
* `app_locationconfig` rows every rider query joins through. Ignored when
* `applocationid` is set, which is the ordinary case.
*/
district?: string;
}
export interface RiderShift {
@@ -238,10 +322,27 @@ export const ridersApi = {
roster: (tenantid: number) =>
api.list<RiderRosterRow>(`${WEB}/partners/getriderroster`, { tenantid }),
/** Hire one. `tenantid` travels as a param — the backend ignores it in the body. */
/**
* Hire one for a MERCHANT. `tenantid` travels as a param — the backend takes
* the scope from there rather than trusting the body, so a store admin cannot
* put a rider on another merchant's books by editing a payload.
*/
create: (tenantid: number, rider: NewRider) =>
api.post<{ userid: number }>(`${WEB}/partners/createrider`, rider, { tenantid }),
/**
* Hire one for a delivery PARTNER.
*
* Same endpoint, same rider — what differs is who they ride for. A partner
* has no console of its own, so their riders are added by the platform.
*/
createForPartner: (partnerid: number, rider: NewRider) =>
api.post<{ userid: number }>(`${WEB}/partners/createrider`, rider, { partnerid }),
/** A partner's riders, for the platform's directory. */
partnerRoster: (partnerid: number) =>
api.list<RiderRosterRow>(`${WEB}/partners/getriderroster`, { partnerid }),
update: (rider: NewRider & { userid: number }) =>
api.put<unknown>(`${WEB}/partners/updaterider`, rider),
@@ -253,3 +354,38 @@ export const ridersApi = {
partners: (applocationid: number) =>
api.list<Partner>(`${WEB}/partners/getpartners`, { applocationid }),
};
/**
* Delivery partners — the companies that supply riders.
*
* A partner is onboarded by the platform and then ASSIGNED to merchants; a
* merchant never creates one. That split is why `assign` lives on the tenant
* API and not here, and why `partnerid` is kept out of the merchant-editable
* profile allowlist on the server.
*
* One partner routinely serves many merchants: partner 44 supplies 48 of them
* and partner 60 supplies 63, measured on 2026-09-09.
*/
export const partnersApi = {
/** Every partner in a region. `applocationid` 0 is not accepted here. */
list: (applocationid: number) =>
api.list<Partner>(`${WEB}/partners/getpartners`, { applocationid }),
/** One partner, by id. */
byId: (partnerid: number) =>
api.list<Partner>(`${WEB}/partners/getpartners`, { partnerid }),
create: (partner: NewPartner) =>
api.post<{ partnerid: number }>(`${WEB}/partners/createpartner`, partner),
/**
* Edit a partner. Regions are REPLACED when sent and left alone when not, so
* an edit that changes only a phone number cannot empty the list.
*/
update: (partner: NewPartner & { partnerid: number }) =>
api.put<unknown>(`${WEB}/partners/updatepartner`, partner),
/** The regions one partner covers. */
locations: (partnerid: number) =>
api.list<PartnerLocation>(`${WEB}/partners/getpartnerlocations`, { partnerid }),
};

View File

@@ -1,6 +1,7 @@
/** Tenant and branch endpoints — the Nearle Admin's provisioning surface. */
import { api, WEB } from './client';
import type { AppLocation } from './deliveries';
import type { TenantInfo, TenantLocation } from './types';
/** Everything the tenant-onboarding form collects. */
@@ -172,6 +173,19 @@ export const tenantsApi = {
updateProfile: (body: { tenantid: number } & Partial<TenantInfo>) =>
api.put<unknown>(`${WEB}/tenants/updatetenant`, body),
/**
* Which delivery partner supplies this merchant's riders.
*
* Its own endpoint, not a field on `updateProfile`: `partnerid` is kept out
* of the merchant-editable allowlist on purpose, because a merchant who could
* set it would move themselves under another partner's riders and billing.
*
* `partnerid: 0` is a real instruction — it means "this merchant uses their
* own riders" — and the server reads it as sent rather than as absent.
*/
assignPartner: (tenantid: number, partnerid: number) =>
api.put<unknown>(`${WEB}/tenants/assignpartner`, { tenantid, partnerid }),
/**
* One business, by id — how a store login reads its own record.
*
@@ -267,4 +281,14 @@ export const utilsApi = {
* invisible to onboarding until someone edits the frontend.
*/
appCategories: () => api.list<AppCategory>(`${WEB}/utils/getappcategories`),
/**
* The delivery regions — Coimbatore, Madurai, Nagercoil today.
*
* `applocationid` is REQUIRED by the handler and 0 is how you ask for all of
* them; omitting it answers 400 "Invalid applocationid", which reads as a
* broken request rather than a missing default.
*/
appLocations: (applocationid = 0) =>
api.list<AppLocation>(`${WEB}/utils/getapplocations`, { applocationid }),
};