211 lines
6.7 KiB
JavaScript
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; |