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,268 @@
import { getMilers, assignMilerToBooking } from '@/api/doormile/endpoints';
import { notifyMiler } from '@/api/doormile/endpoints';
const normMilerName = (n) => (n || '').toString().trim().toLowerCase();
export const buildMilerLookup = (milers) => {
const byUserId = new Map((milers || []).map((m) => [String(m.userid), m]));
const byProfileId = new Map((milers || []).map((m) => [String(m.milerprofileid), m]));
const byName = new Map();
(milers || []).forEach((m) => {
[m.displayname, m.authname].forEach((n) => {
const key = normMilerName(n);
if (key && !byName.has(key)) byName.set(key, m);
});
});
return { byUserId, byProfileId, byName };
};
// ==============================|| Doormile AI — assigning a rider ||============================== //
//
// Fourth write capability. Two endpoints, and which one is correct depends
// entirely on how many orders are being assigned:
//
// ONE order → POST /admin/bookings/:id/assign-miler
// MANY orders → POST /hub/bookings/batch-assign
//
// These are NOT interchangeable. doormile-flow.md §4: batch-assign is "the only
// place stops get ordered" — after assigning it sends each affected rider's
// whole active set to the route optimiser and writes step, per-leg distance and
// ETA back onto `bookingassignments`. Assigning ten orders with ten single
// calls leaves every route unsequenced and riders choosing their own order.
//
// ---- The two rider IDs -----------------------------------------------------
//
// `assign-miler` takes a **mileruserid** in its body. `/admin/milers/:id/notify`
// keys off a **milerprofileid**. Different identity spaces on adjacent
// endpoints — doormile-flow.md calls this out by name. Getting it wrong fails
// quietly in both directions: the assign 404s, or the rider is never told.
// `buildMilerLookup` is the bridge, and it is the SAME one orders.js already
// uses for exactly this translation. Don't grow a second lookup here.
//
// ---- Assignment is normally automatic --------------------------------------
//
// Creating a booking publishes `booking.assignment_requested`; a worker finds a
// rider within 10km via Redis GEO, scores them, and commits — retrying 5 times,
// 2 minutes apart (doormile-flow.md §3). So everything here is an OVERRIDE of a
// decision the backend is already making, which is why the flow re-reads the
// booking's current assignee before offering to change it.
export const ASSIGN_TRIGGER =
/\b(?:re)?assign\s+(?:a\s+|the\s+|another\s+)?(?:rider|miler|driver)\b|\b(?:re)?assign\s+(?:it|this|that|order\b|DM-[A-Za-z0-9-]+)|\bchange\s+(?:the\s+)?rider\b/i;
// Riders, plus the id bridge, in one call.
export const loadRiders = async () => {
const milers = (await getMilers()) || [];
return { milers, lookup: buildMilerLookup(milers) };
};
const riderName = (m) => m.displayname || m.authname || m.name || `Rider #${m.userid}`;
// Available riders first — an operator picking by hand wants the ones who can
// actually take it at the top. Beyond that, alphabetical: any other ordering
// (nearest, least loaded) would need position data this list doesn't carry, and
// a proximity label nobody can stand behind is worse than none.
const isAvailable = (m) => /avail|active|online|free/i.test(String(m.availabilitystatus || ''));
export const riderOptions = (milers) =>
[...(milers || [])]
.sort((a, b) => {
const byAvail = Number(isAvailable(b)) - Number(isAvailable(a));
return byAvail || riderName(a).localeCompare(riderName(b));
})
.map((m) => ({
value: String(m.userid),
label: [riderName(m), m.phone, m.defaultvehicletype, m.availabilitystatus || 'availability unknown'].filter(Boolean).join(' · '),
record: m
}));
// Who currently holds this booking, resolved to a person rather than an id.
export const currentAssignee = (booking, lookup) =>
booking?.assignedmileruserid ? lookup?.byUserId?.get(String(booking.assignedmileruserid)) || null : null;
export const describeRider = (m) => (m ? [riderName(m), m.phone].filter(Boolean).join(' · ') : null);
// ---- one order --------------------------------------------------------------
export const executeAssign = async (booking, rider) => {
const started = Date.now();
const bookingLabel = booking?.bookingno || `#${booking?.bookingid}`;
const call = {
name: 'assignMilerToBooking',
target: `POST /admin/bookings/${booking?.bookingid}/assign-miler`,
stats: `${bookingLabel} → ${riderName(rider)}`
};
try {
// mileruserid, NOT milerprofileid. See the note at the top of this file.
const res = await assignMilerToBooking(booking.bookingid, { mileruserid: Number(rider.userid) });
const duration = `${Date.now() - started}ms`;
if (res && res.success === false) {
return {
ok: false,
message: res.message || 'The server refused the assignment.',
sourceCalls: [{ ...call, duration, status: 'error', errorMessage: res.message }]
};
}
const sourceCalls = [{ ...call, duration, status: 'complete' }];
// CLAUDE.md §9: any mutation that affects a rider is followed by a push.
// It is deliberately NOT allowed to fail the assignment — the order IS
// assigned at this point, and reporting otherwise would be a lie.
// Whether the push ACTUALLY went out, not whether it could have been
// attempted. This used to be reported as `Boolean(rider.milerprofileid)`
// — i.e. "this rider has an id, so assume they were told" — which is a
// different claim entirely. Caught live: notify returned 400 and the
// assistant still said "The rider has been notified."
//
// The assignment itself is unaffected; it had already landed. But a
// dispatcher who believes a rider was pinged does not follow up, and this
// bot's whole contract is that it never states something it has not
// confirmed.
let notified = false;
if (rider.milerprofileid) {
try {
await notifyMiler(rider.milerprofileid);
notified = true;
sourceCalls.push({
name: 'notifyRider',
target: `POST /admin/milers/${rider.milerprofileid}/notify`,
status: 'complete',
stats: 'rider notified'
});
} catch (err) {
sourceCalls.push({
name: 'notifyRider',
target: `POST /admin/milers/${rider.milerprofileid}/notify`,
status: 'error',
errorMessage: err.message || 'notification failed'
});
}
} else {
// No profile id means no push is possible — say so rather than letting
// the operator assume the rider's phone buzzed.
sourceCalls.push({
name: 'notifyRider',
target: '/admin/milers/:id/notify',
status: 'error',
errorMessage: 'this rider has no milerprofileid, so no notification could be sent'
});
}
return { ok: true, rider, bookingLabel, notified, sourceCalls };
} catch (err) {
// doormileAxios rejects with the response BODY; the status rides on
// err.httpStatus.
const status = err.httpStatus;
const message = status
? `POST /admin/bookings/${booking?.bookingid}/assign-miler returned ${status}${
err.message ? ` — ${err.message}` : ''
}. Nothing changed.`
: `${err.message || 'The request failed'} — nothing changed.`;
return {
ok: false,
message,
sourceCalls: [{ ...call, duration: `${Date.now() - started}ms`, status: 'error', errorMessage: message }]
};
}
};
// ---- repeat a run, keeping each order with the rider who ran it last --------
//
// A repeated run is the SAME drops to the SAME doors. The rider who did them
// yesterday already knows the buzzer, the gate code and which side of the
// building to park on, so re-deriving an assignment from scratch throws away
// the one piece of routing knowledge the previous day produced.
//
// Deliberately built on /admin/bookings/:id/assign-miler, one call per order,
// rather than the hub batch endpoint: batch-assign lets the BACKEND choose
// riders, which is the opposite of the intent here, and it is refused to every
// non-hub login anyway (403, confirmed live).
//
// The trade-off this accepts: assigning individually does not sequence a
// rider's stops. Yesterday's run was already sequenced for these same drops,
// so the ordering is not arbitrary — but it is not recomputed either, and the
// caller states that rather than implying a fresh optimisation.
export const executeRepeatAssign = async (createdPairs, rows) => {
const started = Date.now();
// Only rows whose source order actually had a rider. A blank one is not a
// failure — yesterday's copy was never assigned either.
const targets = (createdPairs || [])
.map(({ index, bookingid }) => ({ bookingid, mileruserid: rows?.[index]?.__previousMilerUserId ?? null }))
.filter((t) => t.mileruserid != null);
if (!targets.length) {
return { ok: true, assigned: 0, skipped: (createdPairs || []).length, failures: [], notified: 0, sourceCalls: [] };
}
const { lookup } = await loadRiders().catch(() => ({ lookup: null }));
const assignedRiders = new Set();
const failures = [];
let assigned = 0;
// Sequential on purpose. These are writes against real dispatch records, and
// firing a burst of them concurrently makes a partial failure much harder to
// report accurately — which order did not land, and to whom.
// eslint-disable-next-line no-restricted-syntax
for (const t of targets) {
try {
// eslint-disable-next-line no-await-in-loop
await assignMilerToBooking(t.bookingid, { mileruserid: Number(t.mileruserid) });
assigned += 1;
assignedRiders.add(String(t.mileruserid));
} catch (err) {
failures.push({ bookingid: t.bookingid, reason: err.message || `HTTP ${err.httpStatus || '?'}` });
}
}
const sourceCalls = [
{
name: 'assignMilerToBooking',
target: 'POST /admin/bookings/:id/assign-miler',
duration: `${Date.now() - started}ms`,
status: failures.length ? 'error' : 'complete',
stats: `${assigned} of ${targets.length} re-assigned to yesterday's rider`,
errorMessage: failures.length ? `${failures.length} could not be assigned` : undefined
}
];
// One push per rider, not per order — a rider getting ten of yesterday's
// drops back should feel one buzz, not ten.
let notified = 0;
if (lookup) {
// eslint-disable-next-line no-restricted-syntax
for (const userid of assignedRiders) {
const rider = lookup.byUserId.get(userid);
if (rider?.milerprofileid) {
try {
// eslint-disable-next-line no-await-in-loop
await notifyMiler(rider.milerprofileid);
notified += 1;
} catch {
// Notification failure never fails the assignment — the order IS
// assigned by this point. It is reported, not swallowed.
}
}
}
sourceCalls.push({
name: 'notifyRider',
target: 'POST /admin/milers/:id/notify',
status: notified === assignedRiders.size ? 'complete' : 'error',
stats: `${notified} of ${assignedRiders.size} rider${assignedRiders.size === 1 ? '' : 's'} notified`
});
}
return {
ok: assigned > 0,
assigned,
skipped: (createdPairs || []).length - targets.length,
failures,
notified,
riders: assignedRiders.size,
sourceCalls
};
};