106 lines
3.8 KiB
JavaScript
106 lines
3.8 KiB
JavaScript
// ==============================|| Skill: Hub Congestion Agent ||============================== //
|
|
//
|
|
// Detects parcels that have been sitting at a hub for too long without
|
|
// being picked up by a rider. These represent blocked capacity at the hub
|
|
// and direct SLA risk for the customer. This is critical for hub operations
|
|
|
|
|
|
|
|
import { parseDoormileTimestamp } from '@/lib/doormileTimestamp';
|
|
|
|
const HUB_STATUSES = new Set(['pending', 'accepted']); // at hub, not yet picked
|
|
|
|
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 HubCongestionSkill = {
|
|
id: 'skill_hub_congestion',
|
|
name: 'Hub Congestion Agent',
|
|
description: 'Detects parcels sitting at a hub without rider pickup beyond the dwell threshold. Dispatches nearest idle rider.',
|
|
category: 'sla_management',
|
|
icon: 'ShieldAlert',
|
|
defaultEnabled: true,
|
|
|
|
thresholds: {
|
|
hubDwellMinutes: {
|
|
label: 'Hub Dwell Threshold',
|
|
description: 'Minutes a parcel can sit at hub without a rider pickup before raising a congestion alert.',
|
|
value: 45,
|
|
min: 15,
|
|
max: 120,
|
|
step: 5,
|
|
unit: 'min'
|
|
}
|
|
},
|
|
|
|
instructions: `
|
|
- Parcels sitting at a hub beyond the dwell threshold block capacity for the next batch.
|
|
- Identify the nearest idle rider in the same zone and dispatch immediately.
|
|
- Prefer riders with the fewest active orders to avoid overloading.
|
|
`,
|
|
|
|
tools: [
|
|
{
|
|
name: 'dispatch_hub_idle_parcels',
|
|
description: 'Proposes dispatching a nearby idle rider to collect stalled hub parcels.',
|
|
parameters: {
|
|
type: 'object',
|
|
properties: {
|
|
hubId: { type: 'string', description: 'Hub identifier where parcels are stalled' },
|
|
bookingIds: { type: 'array', items: { type: 'number' }, description: 'Booking IDs to dispatch' },
|
|
milerId: { type: 'number', description: 'Target idle rider user ID' }
|
|
},
|
|
required: ['bookingIds']
|
|
},
|
|
handler: async (args) => ({
|
|
success: true,
|
|
proposal: {
|
|
action: 'dispatch_hub_parcels',
|
|
hubId: args.hubId || 'unknown',
|
|
bookingIds: args.bookingIds,
|
|
milerId: args.milerId,
|
|
blastRadius: `Dispatches rider to collect ${args.bookingIds?.length || 'stalled'} parcel(s) from hub. Updates parcel status to In-Transit.`
|
|
}
|
|
})
|
|
}
|
|
],
|
|
|
|
evaluate: (rows = [], now, currentThresholds = {}) => {
|
|
const dwellLimit = currentThresholds.hubDwellMinutes ?? 45;
|
|
const findings = [];
|
|
|
|
const stalled = rows
|
|
.filter((r) => HUB_STATUSES.has(String(r?.orderstatus || '').toLowerCase()))
|
|
.map((r) => ({ row: r, dwellMin: minutesBetween(r.orderdate, now) }))
|
|
.filter((m) => m.dwellMin !== null && m.dwellMin >= dwellLimit)
|
|
.sort((a, b) => b.dwellMin - a.dwellMin);
|
|
|
|
if (stalled.length) {
|
|
findings.push({
|
|
id: 'hub_parcel_stalled',
|
|
skillId: 'skill_hub_congestion',
|
|
severity: 'critical',
|
|
title: `${stalled.length} parcel${stalled.length === 1 ? '' : 's'} stalled at hub for >${dwellLimit}m`,
|
|
why: 'Parcel has been at the hub beyond the acceptable dwell time with no rider assigned for pickup.',
|
|
count: stalled.length,
|
|
rows: stalled.map((m) => m.row),
|
|
detail: stalled.slice(0, 8).map((m) => `${ref(m.row)} — at hub for ${m.dwellMin} min`),
|
|
proposal: {
|
|
label: 'Dispatch idle rider to hub',
|
|
tool: 'dispatch_hub_idle_parcels',
|
|
scope: stalled.map((m) => m.row),
|
|
blastRadius: 'Assigns nearest idle zone rider to collect and dispatch all stalled parcels.'
|
|
}
|
|
});
|
|
}
|
|
|
|
return findings;
|
|
}
|
|
};
|