// ==============================|| 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; } };