export base44 - Nov 18
This commit is contained in:
211
frontend-web/src/components/scheduling/AutomationEngine.jsx
Normal file
211
frontend-web/src/components/scheduling/AutomationEngine.jsx
Normal file
@@ -0,0 +1,211 @@
|
||||
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;
|
||||
Reference in New Issue
Block a user