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(), }); });