Files
krowworkforce_controltower/base44/functions/fillEscalationMonitor/entry.ts
2026-08-17 19:45:29 +05:30

125 lines
4.5 KiB
TypeScript

import { createClientFromRequest } from 'npm:@base44/sdk@0.8.23';
/**
* Fill Escalation Monitor
* Runs every 15 minutes. Detects events at risk of not being filled
* and creates escalation alerts in ActivityLog.
*
* Fill probability formula:
* score = (assigned / requested) * 60
* + timeBuffer * 20 (more time = more chance to fill)
* + dayOfWeek * 20 (based on historical fill rate for that day)
*
* If fillProbability < 60% → escalation alert
*/
Deno.serve(async (req) => {
const base44 = createClientFromRequest(req);
// Allow scheduled/service calls without user auth
const now = new Date();
const in24h = new Date(now.getTime() + 24 * 60 * 60 * 1000);
const in4h = new Date(now.getTime() + 4 * 60 * 60 * 1000);
// Fetch all upcoming active events
const allEvents = await base44.asServiceRole.entities.Event.list();
const upcoming = allEvents.filter(e => {
if (!e.date) return false;
const eventDate = new Date(e.date);
return eventDate > now && eventDate <= in24h &&
["Active", "Pending", "Confirmed", "Draft"].includes(e.status);
});
// Historical fill rate by day of week
const completed = allEvents.filter(e => e.status === "Completed" && e.date);
const dayFillRate = Array(7).fill(null).map((_, dow) => {
const dayEvents = completed.filter(e => new Date(e.date).getDay() === dow);
if (dayEvents.length === 0) return 0.75; // default 75%
const filled = dayEvents.filter(e => (e.assigned_staff?.length || 0) >= (e.requested || 1));
return filled.length / dayEvents.length;
});
const escalations = [];
const alerts = [];
for (const event of upcoming) {
const assigned = event.assigned_staff?.length || 0;
const requested = event.requested || 0;
if (requested === 0) continue;
const fillRatio = assigned / requested;
const eventDate = new Date(event.date);
const hoursUntil = (eventDate - now) / (1000 * 60 * 60);
const dow = eventDate.getDay();
// Time buffer score: 0 = within 2h (critical), 1 = 24h out (comfortable)
const timeBuffer = Math.min(hoursUntil / 24, 1);
// Fill probability 0-100
const fillProbability = Math.round(
fillRatio * 60 +
timeBuffer * 20 +
dayFillRate[dow] * 20
);
const isCritical = hoursUntil <= 4 && fillRatio < 1;
const isAtRisk = fillProbability < 60;
if (isCritical || isAtRisk) {
escalations.push({
event_id: event.id,
event_name: event.event_name,
hours_until: Math.round(hoursUntil),
fill_ratio: fillRatio,
fill_probability: fillProbability,
assigned,
requested,
is_critical: isCritical,
});
// Create ActivityLog alert (avoid duplicates — check if one exists in last 2h)
const existingAlerts = await base44.asServiceRole.entities.ActivityLog.filter({
related_entity_id: event.id,
activity_type: "event_updated",
});
const recentAlert = existingAlerts.find(a => {
const created = new Date(a.created_date);
return (now - created) < 2 * 60 * 60 * 1000; // within 2h
});
if (!recentAlert) {
const severity = isCritical ? "🚨 CRITICAL" : "⚠️ AT RISK";
const gapCount = requested - assigned;
const description = isCritical
? `${event.event_name} starts in ${Math.round(hoursUntil)}h — ${gapCount} position${gapCount > 1 ? "s" : ""} still open. Fill immediately.`
: `${event.event_name} has a ${fillProbability}% fill probability. ${gapCount} position${gapCount > 1 ? "s" : ""} unfilled with ${Math.round(hoursUntil)}h remaining.`;
alerts.push(
base44.asServiceRole.entities.ActivityLog.create({
title: `${severity}: ${event.event_name} needs staffing`,
description,
activity_type: "event_updated",
related_entity_type: "event",
related_entity_id: event.id,
action_link: `EventDetail?id=${event.id}`,
action_label: "Assign Staff Now",
user_id: event.created_by || "system",
is_read: false,
icon_type: "alert",
icon_color: isCritical ? "red" : "yellow",
})
);
}
}
}
await Promise.all(alerts);
return Response.json({
checked: upcoming.length,
escalations: escalations.length,
details: escalations,
timestamp: now.toISOString(),
});
});