Files
Krow-workspace/frontend-web/src/components/scheduling/AutomationEngine.jsx
2025-11-18 21:32:16 -05:00

211 lines
6.7 KiB
JavaScript

import React, { useEffect } from "react";
import { base44 } from "@/api/base44Client";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { useToast } from "@/components/ui/use-toast";
import { hasTimeOverlap, checkDoubleBooking } from "./SmartAssignmentEngine";
import { format, addDays } from "date-fns";
/**
* Automation Engine
* Handles background automations to reduce manual work
*/
export function AutomationEngine() {
const queryClient = useQueryClient();
const { toast } = useToast();
const { data: events } = useQuery({
queryKey: ['events-automation'],
queryFn: () => base44.entities.Event.list(),
initialData: [],
refetchInterval: 30000, // Check every 30s
});
const { data: allStaff } = useQuery({
queryKey: ['staff-automation'],
queryFn: () => base44.entities.Staff.list(),
initialData: [],
refetchInterval: 60000,
});
const { data: existingInvoices } = useQuery({
queryKey: ['invoices-automation'],
queryFn: () => base44.entities.Invoice.list(),
initialData: [],
refetchInterval: 60000,
});
// Auto-create invoice when event is marked as Completed
useEffect(() => {
const autoCreateInvoices = async () => {
const completedEvents = events.filter(e =>
e.status === 'Completed' &&
!e.invoice_id &&
!existingInvoices.some(inv => inv.event_id === e.id)
);
for (const event of completedEvents) {
try {
const invoiceNumber = `INV-${format(new Date(), 'yyMMddHHmmss')}`;
const issueDate = format(new Date(), 'yyyy-MM-dd');
const dueDate = format(addDays(new Date(), 30), 'yyyy-MM-dd'); // Net 30
const invoice = await base44.entities.Invoice.create({
invoice_number: invoiceNumber,
event_id: event.id,
event_name: event.event_name,
business_name: event.business_name || event.client_name,
vendor_name: event.vendor_name,
manager_name: event.client_name,
hub: event.hub,
cost_center: event.cost_center,
amount: event.total || 0,
item_count: event.assigned_staff?.length || 0,
status: 'Open',
issue_date: issueDate,
due_date: dueDate,
notes: `Auto-generated invoice for completed event: ${event.event_name}`
});
// Update event with invoice_id
await base44.entities.Event.update(event.id, {
invoice_id: invoice.id
});
queryClient.invalidateQueries({ queryKey: ['invoices'] });
queryClient.invalidateQueries({ queryKey: ['events'] });
} catch (error) {
console.error('Auto-invoice creation failed:', error);
}
}
};
if (events.length > 0) {
autoCreateInvoices();
}
}, [events, existingInvoices, queryClient]);
// Auto-confirm workers (24 hours before shift)
useEffect(() => {
const autoConfirmWorkers = async () => {
const now = new Date();
const tomorrow = new Date(now.getTime() + 24 * 60 * 60 * 1000);
const upcomingEvents = events.filter(e => {
const eventDate = new Date(e.date);
return eventDate >= now && eventDate <= tomorrow && e.status === 'Assigned';
});
for (const event of upcomingEvents) {
if (event.assigned_staff?.length > 0) {
try {
await base44.entities.Event.update(event.id, {
status: 'Confirmed'
});
// Send confirmation emails
for (const staff of event.assigned_staff) {
await base44.integrations.Core.SendEmail({
to: staff.email,
subject: `Shift Confirmed - ${event.event_name}`,
body: `Your shift at ${event.event_name} on ${event.date} has been confirmed. See you there!`
});
}
} catch (error) {
console.error('Auto-confirm failed:', error);
}
}
}
};
if (events.length > 0) {
autoConfirmWorkers();
}
}, [events]);
// Auto-send reminders (2 hours before shift)
useEffect(() => {
const sendReminders = async () => {
const now = new Date();
const twoHoursLater = new Date(now.getTime() + 2 * 60 * 60 * 1000);
const upcomingEvents = events.filter(e => {
const eventDate = new Date(e.date);
return eventDate >= now && eventDate <= twoHoursLater;
});
for (const event of upcomingEvents) {
if (event.assigned_staff?.length > 0 && event.status === 'Confirmed') {
for (const staff of event.assigned_staff) {
try {
await base44.integrations.Core.SendEmail({
to: staff.email,
subject: `Reminder: Your shift starts in 2 hours`,
body: `Reminder: Your shift at ${event.event_name} starts in 2 hours. Location: ${event.event_location || event.hub}`
});
} catch (error) {
console.error('Reminder failed:', error);
}
}
}
}
};
if (events.length > 0) {
sendReminders();
}
}, [events]);
// Auto-detect overlapping shifts
useEffect(() => {
const detectOverlaps = () => {
const conflicts = [];
allStaff.forEach(staff => {
const staffEvents = events.filter(e =>
e.assigned_staff?.some(s => s.staff_id === staff.id)
);
for (let i = 0; i < staffEvents.length; i++) {
for (let j = i + 1; j < staffEvents.length; j++) {
const e1 = staffEvents[i];
const e2 = staffEvents[j];
const d1 = new Date(e1.date);
const d2 = new Date(e2.date);
if (d1.toDateString() === d2.toDateString()) {
const shift1 = e1.shifts?.[0]?.roles?.[0];
const shift2 = e2.shifts?.[0]?.roles?.[0];
if (shift1 && shift2 && hasTimeOverlap(shift1, shift2)) {
conflicts.push({
staff: staff.employee_name,
event1: e1.event_name,
event2: e2.event_name,
date: e1.date
});
}
}
}
}
});
if (conflicts.length > 0) {
toast({
title: `⚠️ ${conflicts.length} Double-Booking Detected`,
description: `${conflicts[0].staff} has overlapping shifts`,
variant: "destructive",
});
}
};
if (events.length > 0 && allStaff.length > 0) {
detectOverlaps();
}
}, [events, allStaff]);
return null; // Background service
}
export default AutomationEngine;