first commit
This commit is contained in:
125
base44/functions/fillEscalationMonitor/entry.ts
Normal file
125
base44/functions/fillEscalationMonitor/entry.ts
Normal file
@@ -0,0 +1,125 @@
|
||||
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(),
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user