134 lines
5.1 KiB
JavaScript
134 lines
5.1 KiB
JavaScript
// ==============================|| Skill: Late Dispatch Agent ||============================== //
|
|
//
|
|
// Flags orders that have been accepted/created but not yet dispatched to a rider
|
|
// within the acceptable dispatch window during business hours.
|
|
// Late dispatch at creation cascades into late delivery at the door.
|
|
|
|
import { parseDoormileTimestamp } from '@/lib/doormileTimestamp';
|
|
|
|
const PENDING_DISPATCH_STATUSES = new Set(['pending']);
|
|
|
|
const minutesBetween = (from, to) => {
|
|
const a = parseDoormileTimestamp(from);
|
|
const b = parseDoormileTimestamp(to);
|
|
if (!a.isValid() || !b.isValid()) return null;
|
|
return b.diff(a, 'minute');
|
|
};
|
|
|
|
const ref = (row) => row?.orderid || row?.bookingno || (row?.bookingid ? `#${row.bookingid}` : '—');
|
|
|
|
export const LateDispatchSkill = {
|
|
id: 'skill_late_dispatch',
|
|
name: 'Late Dispatch Agent',
|
|
description: 'Flags orders accepted but sitting in queue without dispatch for too long. Triggers auto-assignment to prevent cascading delays.',
|
|
category: 'sla_management',
|
|
icon: 'ShieldAlert',
|
|
defaultEnabled: true,
|
|
|
|
thresholds: {
|
|
lateDispatchMinutes: {
|
|
label: 'Dispatch Deadline',
|
|
description: 'Minutes after order creation before flagging as late dispatch.',
|
|
value: 30,
|
|
min: 10,
|
|
max: 90,
|
|
step: 5,
|
|
unit: 'min'
|
|
},
|
|
criticalDispatchMinutes: {
|
|
label: 'Critical Dispatch Deadline',
|
|
description: 'Minutes after order creation before escalating to critical alert.',
|
|
value: 60,
|
|
min: 30,
|
|
max: 180,
|
|
step: 10,
|
|
unit: 'min'
|
|
}
|
|
},
|
|
|
|
instructions: `
|
|
- Orders waiting for dispatch beyond the threshold accumulate delay that can't be recovered at the doorstep.
|
|
- Auto-assign to the nearest available rider immediately.
|
|
- Prioritize orders with the earliest SLA deadlines when multiple are pending.
|
|
`,
|
|
|
|
tools: [
|
|
{
|
|
name: 'trigger_auto_dispatch',
|
|
description: 'Proposes auto-assigning late-pending orders to available riders in zone.',
|
|
parameters: {
|
|
type: 'object',
|
|
properties: {
|
|
bookingIds: { type: 'array', items: { type: 'number' }, description: 'Booking IDs requiring immediate dispatch' },
|
|
reason: { type: 'string', description: 'Dispatch urgency reason' }
|
|
},
|
|
required: ['bookingIds']
|
|
},
|
|
handler: async (args) => ({
|
|
success: true,
|
|
proposal: {
|
|
action: 'auto_dispatch',
|
|
bookingIds: args.bookingIds,
|
|
reason: args.reason || 'Late dispatch threshold exceeded',
|
|
blastRadius: `Auto-assigns ${args.bookingIds?.length || 'late'} order(s) to nearest available zone riders. Rider notifications will be sent.`
|
|
}
|
|
})
|
|
}
|
|
],
|
|
|
|
evaluate: (rows = [], now, currentThresholds = {}) => {
|
|
const lateThreshold = currentThresholds.lateDispatchMinutes ?? 30;
|
|
const criticalThreshold = currentThresholds.criticalDispatchMinutes ?? 60;
|
|
const findings = [];
|
|
|
|
const allPending = rows
|
|
.filter((r) => PENDING_DISPATCH_STATUSES.has(String(r?.orderstatus || '').toLowerCase()))
|
|
.map((r) => ({ row: r, waitMin: minutesBetween(r.orderdate, now) }))
|
|
.filter((m) => m.waitMin !== null && m.waitMin >= lateThreshold)
|
|
.sort((a, b) => b.waitMin - a.waitMin);
|
|
|
|
const critical = allPending.filter((m) => m.waitMin >= criticalThreshold);
|
|
const warning = allPending.filter((m) => m.waitMin >= lateThreshold && m.waitMin < criticalThreshold);
|
|
|
|
if (critical.length) {
|
|
findings.push({
|
|
id: 'late_dispatch_critical',
|
|
skillId: 'skill_late_dispatch',
|
|
severity: 'critical',
|
|
title: `${critical.length} order${critical.length === 1 ? '' : 's'} critically late for dispatch (>${criticalThreshold}m)`,
|
|
why: `Order has been sitting in queue for over ${criticalThreshold} minutes with no rider assigned. Delivery SLA is at severe risk.`,
|
|
count: critical.length,
|
|
rows: critical.map((m) => m.row),
|
|
detail: critical.slice(0, 8).map((m) => `${ref(m.row)} — waiting ${m.waitMin} min for dispatch`),
|
|
proposal: {
|
|
label: 'Auto-dispatch to available riders',
|
|
tool: 'trigger_auto_dispatch',
|
|
scope: critical.map((m) => m.row),
|
|
blastRadius: 'Immediately assigns nearest available riders. Overrides manual queue priority.'
|
|
}
|
|
});
|
|
}
|
|
|
|
if (warning.length) {
|
|
findings.push({
|
|
id: 'late_dispatch_warning',
|
|
skillId: 'skill_late_dispatch',
|
|
severity: 'warning',
|
|
title: `${warning.length} order${warning.length === 1 ? '' : 's'} approaching dispatch deadline (>${lateThreshold}m)`,
|
|
why: `Order has been waiting ${lateThreshold}+ minutes for rider assignment. Act now to prevent SLA breach.`,
|
|
count: warning.length,
|
|
rows: warning.map((m) => m.row),
|
|
detail: warning.slice(0, 8).map((m) => `${ref(m.row)} — waiting ${m.waitMin} min`),
|
|
proposal: {
|
|
label: 'Dispatch now',
|
|
tool: 'trigger_auto_dispatch',
|
|
scope: warning.map((m) => m.row),
|
|
blastRadius: 'Assigns available riders to pending orders based on zone proximity.'
|
|
}
|
|
});
|
|
}
|
|
|
|
return findings;
|
|
}
|
|
};
|