new temporal folder to test
This commit is contained in:
200
frontend-web-free/src/components/invoices/CreateInvoiceModal.jsx
Normal file
200
frontend-web-free/src/components/invoices/CreateInvoiceModal.jsx
Normal file
@@ -0,0 +1,200 @@
|
||||
import React, { useState } from "react";
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { useToast } from "@/components/ui/use-toast";
|
||||
import { base44 } from "@/api/base44Client";
|
||||
import { useMutation, useQueryClient, useQuery } from "@tanstack/react-query";
|
||||
import { format, addDays } from "date-fns";
|
||||
import { Plus, Trash2, FileEdit } from "lucide-react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { createPageUrl } from "@/utils";
|
||||
|
||||
export default function CreateInvoiceModal({ open, onClose }) {
|
||||
const { toast } = useToast();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { data: events = [] } = useQuery({
|
||||
queryKey: ['events-for-invoice'],
|
||||
queryFn: () => base44.entities.Event.list(),
|
||||
enabled: open,
|
||||
});
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleAdvancedEditor = () => {
|
||||
onClose();
|
||||
navigate(createPageUrl('InvoiceEditor'));
|
||||
};
|
||||
|
||||
const createMutation = useMutation({
|
||||
mutationFn: async (data) => {
|
||||
const selectedEvent = events.find(e => e.id === data.event_id);
|
||||
if (!selectedEvent) throw new Error("Event not found");
|
||||
|
||||
// Generate roles and staff entries from event
|
||||
const roleGroups = {};
|
||||
|
||||
if (selectedEvent.assigned_staff && selectedEvent.shifts) {
|
||||
selectedEvent.shifts.forEach(shift => {
|
||||
shift.roles?.forEach(role => {
|
||||
const assignedForRole = selectedEvent.assigned_staff.filter(
|
||||
s => s.role === role.role
|
||||
);
|
||||
|
||||
if (!roleGroups[role.role]) {
|
||||
roleGroups[role.role] = {
|
||||
role_name: role.role,
|
||||
staff_entries: [],
|
||||
role_subtotal: 0
|
||||
};
|
||||
}
|
||||
|
||||
assignedForRole.forEach(staff => {
|
||||
const workedHours = role.hours || 8;
|
||||
const baseRate = role.cost_per_hour || role.rate_per_hour || 0;
|
||||
|
||||
const regularHours = Math.min(workedHours, 8);
|
||||
const otHours = Math.max(0, Math.min(workedHours - 8, 4));
|
||||
const dtHours = Math.max(0, workedHours - 12);
|
||||
|
||||
const regularRate = baseRate;
|
||||
const otRate = baseRate * 1.5;
|
||||
const dtRate = baseRate * 2;
|
||||
|
||||
const regularValue = regularHours * regularRate;
|
||||
const otValue = otHours * otRate;
|
||||
const dtValue = dtHours * dtRate;
|
||||
const total = regularValue + otValue + dtValue;
|
||||
|
||||
const entry = {
|
||||
staff_name: staff.staff_name,
|
||||
staff_id: staff.staff_id,
|
||||
date: selectedEvent.date,
|
||||
position: role.role,
|
||||
check_in: role.start_time || "09:00 AM",
|
||||
check_out: role.end_time || "05:00 PM",
|
||||
worked_hours: workedHours,
|
||||
regular_hours: regularHours,
|
||||
ot_hours: otHours,
|
||||
dt_hours: dtHours,
|
||||
regular_rate: regularRate,
|
||||
ot_rate: otRate,
|
||||
dt_rate: dtRate,
|
||||
regular_value: regularValue,
|
||||
ot_value: otValue,
|
||||
dt_value: dtValue,
|
||||
rate: baseRate,
|
||||
total: total
|
||||
};
|
||||
|
||||
roleGroups[role.role].staff_entries.push(entry);
|
||||
roleGroups[role.role].role_subtotal += total;
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const roles = Object.values(roleGroups);
|
||||
const subtotal = roles.reduce((sum, role) => sum + role.role_subtotal, 0);
|
||||
const otherCharges = parseFloat(data.other_charges) || 0;
|
||||
const total = subtotal + otherCharges;
|
||||
|
||||
const invoiceNumber = `INV-${Math.floor(Math.random() * 10000)}`;
|
||||
|
||||
const vendorInfo = {
|
||||
name: selectedEvent.vendor_name || "Legendary",
|
||||
address: "848 E Gish Rd Ste 1, San Jose, CA 95112",
|
||||
email: "orders@legendaryeventstaff.com",
|
||||
phone: "(408) 936-0180"
|
||||
};
|
||||
|
||||
const clientInfo = {
|
||||
name: selectedEvent.business_name || "Client Company",
|
||||
address: selectedEvent.event_location || "Address",
|
||||
email: selectedEvent.client_email || "",
|
||||
manager: selectedEvent.client_name || selectedEvent.manager_name || "Manager",
|
||||
phone: selectedEvent.client_phone || "",
|
||||
vendor_id: "Vendor #"
|
||||
};
|
||||
|
||||
return base44.entities.Invoice.create({
|
||||
invoice_number: invoiceNumber,
|
||||
event_id: selectedEvent.id,
|
||||
event_name: selectedEvent.event_name,
|
||||
event_date: selectedEvent.date,
|
||||
po_reference: data.po_reference || selectedEvent.po_reference,
|
||||
from_company: vendorInfo,
|
||||
to_company: clientInfo,
|
||||
business_name: selectedEvent.business_name,
|
||||
manager_name: selectedEvent.client_name || selectedEvent.business_name,
|
||||
vendor_name: selectedEvent.vendor_name,
|
||||
vendor_id: selectedEvent.vendor_id,
|
||||
hub: selectedEvent.hub,
|
||||
cost_center: data.po_reference || selectedEvent.po_reference,
|
||||
roles: roles,
|
||||
subtotal: subtotal,
|
||||
other_charges: otherCharges,
|
||||
amount: total,
|
||||
status: "Draft",
|
||||
issue_date: format(new Date(), 'yyyy-MM-dd'),
|
||||
due_date: format(addDays(new Date(), 30), 'yyyy-MM-dd'),
|
||||
is_auto_generated: false,
|
||||
notes: data.notes,
|
||||
});
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['invoices'] });
|
||||
toast({
|
||||
title: "✅ Invoice Created",
|
||||
description: "Invoice has been created successfully",
|
||||
});
|
||||
onClose();
|
||||
setFormData({ event_id: "", po_reference: "", other_charges: 0, notes: "" });
|
||||
},
|
||||
});
|
||||
|
||||
const handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
if (!formData.event_id) {
|
||||
toast({
|
||||
title: "Error",
|
||||
description: "Please select an event",
|
||||
variant: "destructive",
|
||||
});
|
||||
return;
|
||||
}
|
||||
createMutation.mutate(formData);
|
||||
};
|
||||
|
||||
const completedEvents = events.filter(e => e.status === "Completed");
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onClose}>
|
||||
<DialogContent className="max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Create New Invoice</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="py-6 text-center">
|
||||
<div className="w-16 h-16 bg-blue-100 rounded-full mx-auto mb-4 flex items-center justify-center">
|
||||
<FileEdit className="w-8 h-8 text-blue-600" />
|
||||
</div>
|
||||
<h3 className="text-lg font-semibold mb-2">Ready to create an invoice?</h3>
|
||||
<p className="text-slate-600 mb-6">Use the advanced editor to create a detailed invoice with full control.</p>
|
||||
|
||||
<Button
|
||||
onClick={handleAdvancedEditor}
|
||||
className="w-full bg-gradient-to-r from-blue-600 to-blue-700 hover:from-blue-700 hover:to-blue-800 text-white font-semibold h-12"
|
||||
>
|
||||
<FileEdit className="w-5 h-5 mr-2" />
|
||||
Open Invoice Editor
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user