869 lines
38 KiB
JavaScript
869 lines
38 KiB
JavaScript
import React, { useState } from "react";
|
|
import { useNavigate } from "react-router-dom";
|
|
import { base44 } from "@/api/base44Client";
|
|
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
|
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 { Card } from "@/components/ui/card";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { ArrowLeft, Plus, Trash2, Clock } from "lucide-react";
|
|
import { createPageUrl } from "@/utils";
|
|
import { useToast } from "@/components/ui/use-toast";
|
|
import { format, addDays } from "date-fns";
|
|
import {
|
|
Popover,
|
|
PopoverContent,
|
|
PopoverTrigger,
|
|
} from "@/components/ui/popover";
|
|
|
|
export default function InvoiceEditor() {
|
|
const navigate = useNavigate();
|
|
const { toast } = useToast();
|
|
const queryClient = useQueryClient();
|
|
const urlParams = new URLSearchParams(window.location.search);
|
|
const invoiceId = urlParams.get('id');
|
|
const isEdit = !!invoiceId;
|
|
|
|
const { data: user } = useQuery({
|
|
queryKey: ['current-user-invoice-editor'],
|
|
queryFn: () => base44.auth.me(),
|
|
});
|
|
|
|
const { data: invoices = [] } = useQuery({
|
|
queryKey: ['invoices'],
|
|
queryFn: () => base44.entities.Invoice.list(),
|
|
enabled: isEdit,
|
|
});
|
|
|
|
const { data: events = [] } = useQuery({
|
|
queryKey: ['events-for-invoice'],
|
|
queryFn: () => base44.entities.Event.list(),
|
|
});
|
|
|
|
const existingInvoice = invoices.find(inv => inv.id === invoiceId);
|
|
|
|
const [formData, setFormData] = useState({
|
|
invoice_number: existingInvoice?.invoice_number || `INV-G00G${Math.floor(Math.random() * 100000)}`,
|
|
event_id: existingInvoice?.event_id || "",
|
|
event_name: existingInvoice?.event_name || "",
|
|
invoice_date: existingInvoice?.issue_date || format(new Date(), 'yyyy-MM-dd'),
|
|
due_date: existingInvoice?.due_date || format(addDays(new Date(), 30), 'yyyy-MM-dd'),
|
|
payment_terms: existingInvoice?.payment_terms || "30",
|
|
hub: existingInvoice?.hub || "",
|
|
manager: existingInvoice?.manager_name || "",
|
|
vendor_id: existingInvoice?.vendor_id || "",
|
|
department: existingInvoice?.department || "",
|
|
po_reference: existingInvoice?.po_reference || "",
|
|
from_company: existingInvoice?.from_company || {
|
|
name: "Legendary Event Staffing",
|
|
address: "848 E Gish Rd Ste 1, San Jose, CA 95112",
|
|
phone: "(408) 936-0180",
|
|
email: "order@legendaryeventstaff.com"
|
|
},
|
|
to_company: existingInvoice?.to_company || {
|
|
name: "Thinkloops",
|
|
phone: "4086702861",
|
|
email: "mohsin@thikloops.com",
|
|
address: "Dublin St, San Francisco, CA 94112, USA",
|
|
manager_name: "Manager Name",
|
|
hub_name: "Hub Name",
|
|
vendor_id: "Vendor #"
|
|
},
|
|
staff_entries: existingInvoice?.roles?.[0]?.staff_entries || [],
|
|
charges: existingInvoice?.charges || [],
|
|
other_charges: existingInvoice?.other_charges || 0,
|
|
notes: existingInvoice?.notes || "",
|
|
});
|
|
|
|
const [timePickerOpen, setTimePickerOpen] = useState(null);
|
|
const [selectedTime, setSelectedTime] = useState({ hours: "09", minutes: "00", period: "AM" });
|
|
|
|
const saveMutation = useMutation({
|
|
mutationFn: async (data) => {
|
|
// Calculate totals
|
|
const staffTotal = data.staff_entries.reduce((sum, entry) => sum + (entry.total || 0), 0);
|
|
const chargesTotal = data.charges.reduce((sum, charge) => sum + ((charge.qty * charge.rate) || 0), 0);
|
|
const subtotal = staffTotal + chargesTotal;
|
|
const total = subtotal + (parseFloat(data.other_charges) || 0);
|
|
|
|
const roles = data.staff_entries.length > 0 ? [{
|
|
role_name: "Mixed",
|
|
staff_entries: data.staff_entries,
|
|
role_subtotal: staffTotal
|
|
}] : [];
|
|
|
|
const invoiceData = {
|
|
invoice_number: data.invoice_number,
|
|
event_id: data.event_id,
|
|
event_name: data.event_name,
|
|
event_date: data.invoice_date,
|
|
po_reference: data.po_reference,
|
|
from_company: data.from_company,
|
|
to_company: data.to_company,
|
|
business_name: data.to_company.name,
|
|
manager_name: data.manager,
|
|
vendor_name: data.from_company.name,
|
|
vendor_id: data.vendor_id,
|
|
hub: data.hub,
|
|
department: data.department,
|
|
cost_center: data.po_reference,
|
|
roles: roles,
|
|
charges: data.charges,
|
|
subtotal: subtotal,
|
|
other_charges: parseFloat(data.other_charges) || 0,
|
|
amount: total,
|
|
status: existingInvoice?.status || "Draft",
|
|
issue_date: data.invoice_date,
|
|
due_date: data.due_date,
|
|
payment_terms: data.payment_terms,
|
|
is_auto_generated: false,
|
|
notes: data.notes,
|
|
};
|
|
|
|
if (isEdit) {
|
|
return base44.entities.Invoice.update(invoiceId, invoiceData);
|
|
} else {
|
|
return base44.entities.Invoice.create(invoiceData);
|
|
}
|
|
},
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ['invoices'] });
|
|
toast({
|
|
title: isEdit ? "✅ Invoice Updated" : "✅ Invoice Created",
|
|
description: isEdit ? "Invoice has been updated successfully" : "Invoice has been created successfully",
|
|
});
|
|
navigate(createPageUrl('Invoices'));
|
|
},
|
|
});
|
|
|
|
const handleAddStaffEntry = () => {
|
|
setFormData({
|
|
...formData,
|
|
staff_entries: [
|
|
...formData.staff_entries,
|
|
{
|
|
name: "Mohsin",
|
|
date: format(new Date(), 'MM/dd/yyyy'),
|
|
position: "Bartender",
|
|
check_in: "hh:mm",
|
|
lunch: 0,
|
|
check_out: "",
|
|
worked_hours: 0,
|
|
regular_hours: 0,
|
|
ot_hours: 0,
|
|
dt_hours: 0,
|
|
rate: 52.68,
|
|
regular_value: 0,
|
|
ot_value: 0,
|
|
dt_value: 0,
|
|
total: 0
|
|
}
|
|
]
|
|
});
|
|
};
|
|
|
|
const handleAddCharge = () => {
|
|
setFormData({
|
|
...formData,
|
|
charges: [
|
|
...formData.charges,
|
|
{
|
|
name: "Gas Compensation",
|
|
qty: 7.30,
|
|
rate: 0,
|
|
price: 0
|
|
}
|
|
]
|
|
});
|
|
};
|
|
|
|
const handleStaffChange = (index, field, value) => {
|
|
const newEntries = [...formData.staff_entries];
|
|
newEntries[index] = { ...newEntries[index], [field]: value };
|
|
|
|
// Recalculate totals if time-related fields change
|
|
if (['worked_hours', 'regular_hours', 'ot_hours', 'dt_hours', 'rate'].includes(field)) {
|
|
const entry = newEntries[index];
|
|
entry.regular_value = (entry.regular_hours || 0) * (entry.rate || 0);
|
|
entry.ot_value = (entry.ot_hours || 0) * (entry.rate || 0) * 1.5;
|
|
entry.dt_value = (entry.dt_hours || 0) * (entry.rate || 0) * 2;
|
|
entry.total = entry.regular_value + entry.ot_value + entry.dt_value;
|
|
}
|
|
|
|
setFormData({ ...formData, staff_entries: newEntries });
|
|
};
|
|
|
|
const handleChargeChange = (index, field, value) => {
|
|
const newCharges = [...formData.charges];
|
|
newCharges[index] = { ...newCharges[index], [field]: value };
|
|
|
|
if (['qty', 'rate'].includes(field)) {
|
|
newCharges[index].price = (newCharges[index].qty || 0) * (newCharges[index].rate || 0);
|
|
}
|
|
|
|
setFormData({ ...formData, charges: newCharges });
|
|
};
|
|
|
|
const handleRemoveStaff = (index) => {
|
|
setFormData({
|
|
...formData,
|
|
staff_entries: formData.staff_entries.filter((_, i) => i !== index)
|
|
});
|
|
};
|
|
|
|
const handleRemoveCharge = (index) => {
|
|
setFormData({
|
|
...formData,
|
|
charges: formData.charges.filter((_, i) => i !== index)
|
|
});
|
|
};
|
|
|
|
const handleTimeSelect = (entryIndex, field) => {
|
|
const timeString = `${selectedTime.hours}:${selectedTime.minutes} ${selectedTime.period}`;
|
|
handleStaffChange(entryIndex, field, timeString);
|
|
setTimePickerOpen(null);
|
|
};
|
|
|
|
const calculateTotals = () => {
|
|
const staffTotal = formData.staff_entries.reduce((sum, entry) => sum + (entry.total || 0), 0);
|
|
const chargesTotal = formData.charges.reduce((sum, charge) => sum + (charge.price || 0), 0);
|
|
const subtotal = staffTotal + chargesTotal;
|
|
const otherCharges = parseFloat(formData.other_charges) || 0;
|
|
const grandTotal = subtotal + otherCharges;
|
|
|
|
return { subtotal, otherCharges, grandTotal };
|
|
};
|
|
|
|
const totals = calculateTotals();
|
|
|
|
return (
|
|
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-slate-50 p-6">
|
|
<div className="max-w-7xl mx-auto">
|
|
<div className="mb-6 flex items-center justify-between">
|
|
<div className="flex items-center gap-4">
|
|
<Button variant="outline" onClick={() => navigate(createPageUrl('Invoices'))} className="bg-white">
|
|
<ArrowLeft className="w-4 h-4 mr-2" />
|
|
Back to Invoices
|
|
</Button>
|
|
<div>
|
|
<h1 className="text-2xl font-bold text-slate-900">{isEdit ? 'Edit Invoice' : 'Create New Invoice'}</h1>
|
|
<p className="text-sm text-slate-600">Complete all invoice details below</p>
|
|
</div>
|
|
</div>
|
|
<Badge className="bg-blue-100 text-blue-700 text-sm px-3 py-1">
|
|
{existingInvoice?.status || "Draft"}
|
|
</Badge>
|
|
</div>
|
|
|
|
<Card className="p-8 bg-white shadow-lg border-blue-100">
|
|
{/* Invoice Details Header */}
|
|
<div className="flex items-start justify-between mb-6 pb-6 border-b border-blue-100">
|
|
<div className="flex-1">
|
|
<div className="flex items-center gap-3 mb-6">
|
|
<div className="w-12 h-12 bg-gradient-to-br from-blue-500 to-blue-600 rounded-lg flex items-center justify-center">
|
|
<span className="text-white font-bold text-lg">📄</span>
|
|
</div>
|
|
<div>
|
|
<h2 className="text-xl font-bold text-slate-900">Invoice Details</h2>
|
|
<p className="text-sm text-slate-500">Event: {formData.event_name || "Internal Support"}</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="bg-gradient-to-r from-blue-50 to-blue-100 p-4 rounded-lg mb-4">
|
|
<div className="text-xs text-blue-600 font-semibold mb-1">Invoice Number</div>
|
|
<div className="font-bold text-2xl text-blue-900">{formData.invoice_number}</div>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-2 gap-4 mb-4">
|
|
<div>
|
|
<Label className="text-xs font-semibold text-slate-700">Invoice Date</Label>
|
|
<Input
|
|
type="date"
|
|
value={formData.invoice_date}
|
|
onChange={(e) => setFormData({ ...formData, invoice_date: e.target.value })}
|
|
className="mt-1 border-blue-200 focus:border-blue-500"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<Label className="text-xs font-semibold text-slate-700">Due Date</Label>
|
|
<Input
|
|
type="date"
|
|
value={formData.due_date}
|
|
onChange={(e) => setFormData({ ...formData, due_date: e.target.value })}
|
|
className="mt-1 border-blue-200 focus:border-blue-500"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="mb-4">
|
|
<Label className="text-xs">Hub</Label>
|
|
<Input
|
|
value={formData.hub}
|
|
onChange={(e) => setFormData({ ...formData, hub: e.target.value })}
|
|
placeholder="Hub"
|
|
className="mt-1"
|
|
/>
|
|
</div>
|
|
|
|
<div className="mb-4">
|
|
<Label className="text-xs">Manager</Label>
|
|
<Input
|
|
value={formData.manager}
|
|
onChange={(e) => setFormData({ ...formData, manager: e.target.value })}
|
|
placeholder="Manager Name"
|
|
className="mt-1"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<Label className="text-xs">Vendor #</Label>
|
|
<Input
|
|
value={formData.vendor_id}
|
|
onChange={(e) => setFormData({ ...formData, vendor_id: e.target.value })}
|
|
placeholder="Vendor #"
|
|
className="mt-1"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex-1 text-right">
|
|
<div className="mb-4">
|
|
<Label className="text-xs font-semibold text-slate-700 block mb-2">Payment Terms</Label>
|
|
<div className="flex gap-2 justify-end">
|
|
<Badge
|
|
className={`cursor-pointer transition-all ${formData.payment_terms === "30" ? "bg-blue-600 text-white" : "bg-white border-2 border-slate-200 text-slate-700 hover:border-blue-300"}`}
|
|
onClick={() => setFormData({ ...formData, payment_terms: "30", due_date: format(addDays(new Date(formData.invoice_date), 30), 'yyyy-MM-dd') })}
|
|
>
|
|
30 days
|
|
</Badge>
|
|
<Badge
|
|
className={`cursor-pointer transition-all ${formData.payment_terms === "45" ? "bg-blue-600 text-white" : "bg-white border-2 border-slate-200 text-slate-700 hover:border-blue-300"}`}
|
|
onClick={() => setFormData({ ...formData, payment_terms: "45", due_date: format(addDays(new Date(formData.invoice_date), 45), 'yyyy-MM-dd') })}
|
|
>
|
|
45 days
|
|
</Badge>
|
|
<Badge
|
|
className={`cursor-pointer transition-all ${formData.payment_terms === "60" ? "bg-blue-600 text-white" : "bg-white border-2 border-slate-200 text-slate-700 hover:border-blue-300"}`}
|
|
onClick={() => setFormData({ ...formData, payment_terms: "60", due_date: format(addDays(new Date(formData.invoice_date), 60), 'yyyy-MM-dd') })}
|
|
>
|
|
60 days
|
|
</Badge>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="mt-4 space-y-2">
|
|
<div className="flex items-center gap-2">
|
|
<span className="text-sm text-slate-500">Department:</span>
|
|
<Input
|
|
value={formData.department}
|
|
onChange={(e) => setFormData({ ...formData, department: e.target.value })}
|
|
placeholder="INV-G00G20242"
|
|
className="h-8 w-48"
|
|
/>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<span className="text-sm text-slate-500">PO#:</span>
|
|
<Input
|
|
value={formData.po_reference}
|
|
onChange={(e) => setFormData({ ...formData, po_reference: e.target.value })}
|
|
placeholder="INV-G00G20242"
|
|
className="h-8 w-48"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* From and To */}
|
|
<div className="grid grid-cols-2 gap-6 mb-6">
|
|
<div className="bg-gradient-to-br from-blue-50 to-blue-100 p-5 rounded-xl border border-blue-200">
|
|
<h3 className="font-bold mb-4 flex items-center gap-2 text-blue-900">
|
|
<div className="w-8 h-8 bg-blue-600 rounded-lg flex items-center justify-center text-white text-sm font-bold shadow-md">F</div>
|
|
From (Vendor):
|
|
</h3>
|
|
<div className="space-y-2 text-sm">
|
|
<Input
|
|
value={formData.from_company.name}
|
|
onChange={(e) => setFormData({
|
|
...formData,
|
|
from_company: { ...formData.from_company, name: e.target.value }
|
|
})}
|
|
className="font-semibold mb-2"
|
|
/>
|
|
<Input
|
|
value={formData.from_company.address}
|
|
onChange={(e) => setFormData({
|
|
...formData,
|
|
from_company: { ...formData.from_company, address: e.target.value }
|
|
})}
|
|
className="text-sm"
|
|
/>
|
|
<Input
|
|
value={formData.from_company.phone}
|
|
onChange={(e) => setFormData({
|
|
...formData,
|
|
from_company: { ...formData.from_company, phone: e.target.value }
|
|
})}
|
|
className="text-sm"
|
|
/>
|
|
<Input
|
|
value={formData.from_company.email}
|
|
onChange={(e) => setFormData({
|
|
...formData,
|
|
from_company: { ...formData.from_company, email: e.target.value }
|
|
})}
|
|
className="text-sm"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="bg-gradient-to-br from-slate-50 to-slate-100 p-5 rounded-xl border border-slate-200">
|
|
<h3 className="font-bold mb-4 flex items-center gap-2 text-slate-900">
|
|
<div className="w-8 h-8 bg-slate-600 rounded-lg flex items-center justify-center text-white text-sm font-bold shadow-md">T</div>
|
|
To (Client):
|
|
</h3>
|
|
<div className="space-y-2 text-sm">
|
|
<div className="flex items-center gap-2">
|
|
<span className="text-slate-500 w-32">Company:</span>
|
|
<Input
|
|
value={formData.to_company.name}
|
|
onChange={(e) => setFormData({
|
|
...formData,
|
|
to_company: { ...formData.to_company, name: e.target.value }
|
|
})}
|
|
className="flex-1"
|
|
/>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<span className="text-slate-500 w-32">Phone:</span>
|
|
<Input
|
|
value={formData.to_company.phone}
|
|
onChange={(e) => setFormData({
|
|
...formData,
|
|
to_company: { ...formData.to_company, phone: e.target.value }
|
|
})}
|
|
className="flex-1"
|
|
/>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<span className="text-slate-500 w-32">Manager Name:</span>
|
|
<Input
|
|
value={formData.to_company.manager_name}
|
|
onChange={(e) => setFormData({
|
|
...formData,
|
|
to_company: { ...formData.to_company, manager_name: e.target.value }
|
|
})}
|
|
className="flex-1"
|
|
/>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<span className="text-slate-500 w-32">Email:</span>
|
|
<Input
|
|
value={formData.to_company.email}
|
|
onChange={(e) => setFormData({
|
|
...formData,
|
|
to_company: { ...formData.to_company, email: e.target.value }
|
|
})}
|
|
className="flex-1"
|
|
/>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<span className="text-slate-500 w-32">Hub Name:</span>
|
|
<Input
|
|
value={formData.to_company.hub_name}
|
|
onChange={(e) => setFormData({
|
|
...formData,
|
|
to_company: { ...formData.to_company, hub_name: e.target.value }
|
|
})}
|
|
className="flex-1"
|
|
/>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<span className="text-slate-500 w-32">Address:</span>
|
|
<Input
|
|
value={formData.to_company.address}
|
|
onChange={(e) => setFormData({
|
|
...formData,
|
|
to_company: { ...formData.to_company, address: e.target.value }
|
|
})}
|
|
className="flex-1"
|
|
/>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<span className="text-slate-500 w-32">Vendor #:</span>
|
|
<Input
|
|
value={formData.to_company.vendor_id}
|
|
onChange={(e) => setFormData({
|
|
...formData,
|
|
to_company: { ...formData.to_company, vendor_id: e.target.value }
|
|
})}
|
|
className="flex-1"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Staff Table */}
|
|
<div className="mb-6">
|
|
<div className="flex items-center justify-between mb-4 p-4 bg-gradient-to-r from-blue-50 to-blue-100 rounded-lg">
|
|
<div className="flex items-center gap-3">
|
|
<div className="w-10 h-10 bg-blue-600 rounded-lg flex items-center justify-center">
|
|
<span className="text-white font-bold text-lg">👥</span>
|
|
</div>
|
|
<div>
|
|
<h3 className="font-bold text-blue-900">Staff Entries</h3>
|
|
<p className="text-xs text-blue-700">{formData.staff_entries.length} entries</p>
|
|
</div>
|
|
</div>
|
|
<Button size="sm" onClick={handleAddStaffEntry} className="bg-blue-600 hover:bg-blue-700 text-white shadow-md">
|
|
<Plus className="w-4 h-4 mr-1" />
|
|
Add Staff Entry
|
|
</Button>
|
|
</div>
|
|
|
|
<div className="overflow-x-auto border rounded-lg">
|
|
<table className="w-full text-sm">
|
|
<thead className="bg-slate-50">
|
|
<tr>
|
|
<th className="p-2 text-left">#</th>
|
|
<th className="p-2 text-left">Name</th>
|
|
<th className="p-2 text-left">ClockIn</th>
|
|
<th className="p-2 text-left">Lunch</th>
|
|
<th className="p-2 text-left">Checkout</th>
|
|
<th className="p-2 text-left">Worked H</th>
|
|
<th className="p-2 text-left">Reg H</th>
|
|
<th className="p-2 text-left">OT Hours</th>
|
|
<th className="p-2 text-left">DT Hours</th>
|
|
<th className="p-2 text-left">Rate</th>
|
|
<th className="p-2 text-left">Reg Value</th>
|
|
<th className="p-2 text-left">OT Value</th>
|
|
<th className="p-2 text-left">DT Value</th>
|
|
<th className="p-2 text-left">Total</th>
|
|
<th className="p-2">Action</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{formData.staff_entries.map((entry, idx) => (
|
|
<tr key={idx} className="border-t hover:bg-slate-50">
|
|
<td className="p-2">{idx + 1}</td>
|
|
<td className="p-2">
|
|
<Input
|
|
value={entry.name}
|
|
onChange={(e) => handleStaffChange(idx, 'name', e.target.value)}
|
|
className="h-8 w-24"
|
|
/>
|
|
</td>
|
|
<td className="p-2">
|
|
<Popover open={timePickerOpen === `checkin-${idx}`} onOpenChange={(open) => setTimePickerOpen(open ? `checkin-${idx}` : null)}>
|
|
<PopoverTrigger asChild>
|
|
<Button variant="outline" size="sm" className="h-8 w-24 justify-start font-normal">
|
|
<Clock className="w-3 h-3 mr-1" />
|
|
{entry.check_in}
|
|
</Button>
|
|
</PopoverTrigger>
|
|
<PopoverContent className="w-auto p-3">
|
|
<div className="space-y-2">
|
|
<div className="flex gap-2">
|
|
<Input
|
|
type="number"
|
|
min="01"
|
|
max="12"
|
|
value={selectedTime.hours}
|
|
onChange={(e) => setSelectedTime({ ...selectedTime, hours: e.target.value.padStart(2, '0') })}
|
|
className="w-16"
|
|
placeholder="HH"
|
|
/>
|
|
<span className="text-2xl">:</span>
|
|
<Input
|
|
type="number"
|
|
min="00"
|
|
max="59"
|
|
value={selectedTime.minutes}
|
|
onChange={(e) => setSelectedTime({ ...selectedTime, minutes: e.target.value.padStart(2, '0') })}
|
|
className="w-16"
|
|
placeholder="MM"
|
|
/>
|
|
<Select value={selectedTime.period} onValueChange={(val) => setSelectedTime({ ...selectedTime, period: val })}>
|
|
<SelectTrigger className="w-20">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="AM">AM</SelectItem>
|
|
<SelectItem value="PM">PM</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
<Button size="sm" onClick={() => handleTimeSelect(idx, 'check_in')} className="w-full">
|
|
Set Time
|
|
</Button>
|
|
</div>
|
|
</PopoverContent>
|
|
</Popover>
|
|
</td>
|
|
<td className="p-2">
|
|
<Input
|
|
type="number"
|
|
value={entry.lunch}
|
|
onChange={(e) => handleStaffChange(idx, 'lunch', parseFloat(e.target.value))}
|
|
className="h-8 w-16"
|
|
/>
|
|
</td>
|
|
<td className="p-2">
|
|
<Popover open={timePickerOpen === `checkout-${idx}`} onOpenChange={(open) => setTimePickerOpen(open ? `checkout-${idx}` : null)}>
|
|
<PopoverTrigger asChild>
|
|
<Button variant="outline" size="sm" className="h-8 w-24 justify-start font-normal">
|
|
<Clock className="w-3 h-3 mr-1" />
|
|
{entry.check_out || "hh:mm"}
|
|
</Button>
|
|
</PopoverTrigger>
|
|
<PopoverContent className="w-auto p-3">
|
|
<div className="space-y-2">
|
|
<div className="flex gap-2">
|
|
<Input
|
|
type="number"
|
|
min="01"
|
|
max="12"
|
|
value={selectedTime.hours}
|
|
onChange={(e) => setSelectedTime({ ...selectedTime, hours: e.target.value.padStart(2, '0') })}
|
|
className="w-16"
|
|
placeholder="HH"
|
|
/>
|
|
<span className="text-2xl">:</span>
|
|
<Input
|
|
type="number"
|
|
min="00"
|
|
max="59"
|
|
value={selectedTime.minutes}
|
|
onChange={(e) => setSelectedTime({ ...selectedTime, minutes: e.target.value.padStart(2, '0') })}
|
|
className="w-16"
|
|
placeholder="MM"
|
|
/>
|
|
<Select value={selectedTime.period} onValueChange={(val) => setSelectedTime({ ...selectedTime, period: val })}>
|
|
<SelectTrigger className="w-20">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="AM">AM</SelectItem>
|
|
<SelectItem value="PM">PM</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
<Button size="sm" onClick={() => handleTimeSelect(idx, 'check_out')} className="w-full">
|
|
Set Time
|
|
</Button>
|
|
</div>
|
|
</PopoverContent>
|
|
</Popover>
|
|
</td>
|
|
<td className="p-2">
|
|
<Input
|
|
type="number"
|
|
step="0.1"
|
|
value={entry.worked_hours}
|
|
onChange={(e) => handleStaffChange(idx, 'worked_hours', parseFloat(e.target.value))}
|
|
className="h-8 w-16"
|
|
/>
|
|
</td>
|
|
<td className="p-2">
|
|
<Input
|
|
type="number"
|
|
step="0.1"
|
|
value={entry.regular_hours}
|
|
onChange={(e) => handleStaffChange(idx, 'regular_hours', parseFloat(e.target.value))}
|
|
className="h-8 w-16"
|
|
/>
|
|
</td>
|
|
<td className="p-2">
|
|
<Input
|
|
type="number"
|
|
step="0.1"
|
|
value={entry.ot_hours}
|
|
onChange={(e) => handleStaffChange(idx, 'ot_hours', parseFloat(e.target.value))}
|
|
className="h-8 w-16"
|
|
/>
|
|
</td>
|
|
<td className="p-2">
|
|
<Input
|
|
type="number"
|
|
step="0.1"
|
|
value={entry.dt_hours}
|
|
onChange={(e) => handleStaffChange(idx, 'dt_hours', parseFloat(e.target.value))}
|
|
className="h-8 w-16"
|
|
/>
|
|
</td>
|
|
<td className="p-2">
|
|
<Input
|
|
type="number"
|
|
step="0.01"
|
|
value={entry.rate}
|
|
onChange={(e) => handleStaffChange(idx, 'rate', parseFloat(e.target.value))}
|
|
className="h-8 w-20"
|
|
/>
|
|
</td>
|
|
<td className="p-2 text-right">${entry.regular_value?.toFixed(2) || "0.00"}</td>
|
|
<td className="p-2 text-right">${entry.ot_value?.toFixed(2) || "0.00"}</td>
|
|
<td className="p-2 text-right">${entry.dt_value?.toFixed(2) || "0.00"}</td>
|
|
<td className="p-2 text-right font-semibold">${entry.total?.toFixed(2) || "0.00"}</td>
|
|
<td className="p-2 text-center">
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => handleRemoveStaff(idx)}
|
|
className="h-8 w-8 p-0 text-red-600 hover:bg-red-50"
|
|
>
|
|
<Trash2 className="w-4 h-4" />
|
|
</Button>
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Charges */}
|
|
<div className="mb-6">
|
|
<div className="flex items-center justify-between mb-4 p-4 bg-gradient-to-r from-green-50 to-emerald-100 rounded-lg">
|
|
<div className="flex items-center gap-3">
|
|
<div className="w-10 h-10 bg-emerald-600 rounded-lg flex items-center justify-center">
|
|
<span className="text-white font-bold text-lg">💰</span>
|
|
</div>
|
|
<div>
|
|
<h3 className="font-bold text-emerald-900">Additional Charges</h3>
|
|
<p className="text-xs text-emerald-700">{formData.charges.length} charges</p>
|
|
</div>
|
|
</div>
|
|
<Button size="sm" onClick={handleAddCharge} className="bg-emerald-600 hover:bg-emerald-700 text-white shadow-md">
|
|
<Plus className="w-4 h-4 mr-1" />
|
|
Add Charge
|
|
</Button>
|
|
</div>
|
|
|
|
<div className="overflow-x-auto border rounded-lg">
|
|
<table className="w-full text-sm">
|
|
<thead className="bg-slate-50">
|
|
<tr>
|
|
<th className="p-2 text-left">#</th>
|
|
<th className="p-2 text-left">Name</th>
|
|
<th className="p-2 text-left">QTY</th>
|
|
<th className="p-2 text-left">Rate</th>
|
|
<th className="p-2 text-left">Price</th>
|
|
<th className="p-2">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{formData.charges.map((charge, idx) => (
|
|
<tr key={idx} className="border-t hover:bg-slate-50">
|
|
<td className="p-2">{idx + 1}</td>
|
|
<td className="p-2">
|
|
<Input
|
|
value={charge.name}
|
|
onChange={(e) => handleChargeChange(idx, 'name', e.target.value)}
|
|
className="h-8"
|
|
/>
|
|
</td>
|
|
<td className="p-2">
|
|
<Input
|
|
type="number"
|
|
step="0.01"
|
|
value={charge.qty}
|
|
onChange={(e) => handleChargeChange(idx, 'qty', parseFloat(e.target.value))}
|
|
className="h-8 w-20"
|
|
/>
|
|
</td>
|
|
<td className="p-2">
|
|
<Input
|
|
type="number"
|
|
step="0.01"
|
|
value={charge.rate}
|
|
onChange={(e) => handleChargeChange(idx, 'rate', parseFloat(e.target.value))}
|
|
className="h-8 w-20"
|
|
/>
|
|
</td>
|
|
<td className="p-2">${charge.price?.toFixed(2) || "0.00"}</td>
|
|
<td className="p-2 text-center">
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => handleRemoveCharge(idx)}
|
|
className="h-8 w-8 p-0 text-red-600 hover:bg-red-50"
|
|
>
|
|
<Trash2 className="w-4 h-4" />
|
|
</Button>
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Totals */}
|
|
<div className="flex justify-end mb-6">
|
|
<div className="w-96 bg-gradient-to-br from-blue-50 to-blue-100 p-6 rounded-xl border-2 border-blue-200 shadow-lg">
|
|
<div className="space-y-4">
|
|
<div className="flex justify-between text-sm">
|
|
<span className="text-slate-600">Sub total:</span>
|
|
<span className="font-semibold text-slate-900">${totals.subtotal.toFixed(2)}</span>
|
|
</div>
|
|
<div className="flex justify-between items-center">
|
|
<span className="text-sm text-slate-600">Other charges:</span>
|
|
<Input
|
|
type="number"
|
|
step="0.01"
|
|
value={formData.other_charges}
|
|
onChange={(e) => setFormData({ ...formData, other_charges: e.target.value })}
|
|
className="h-9 w-32 text-right border-blue-300 focus:border-blue-500 bg-white"
|
|
/>
|
|
</div>
|
|
<div className="flex justify-between text-xl font-bold pt-4 border-t-2 border-blue-300">
|
|
<span className="text-blue-900">Grand total:</span>
|
|
<span className="text-blue-900">${totals.grandTotal.toFixed(2)}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Notes */}
|
|
<div className="mb-6">
|
|
<Label className="mb-2 block">Notes</Label>
|
|
<Textarea
|
|
value={formData.notes}
|
|
onChange={(e) => setFormData({ ...formData, notes: e.target.value })}
|
|
placeholder="Enter your notes here..."
|
|
rows={3}
|
|
/>
|
|
</div>
|
|
|
|
{/* Actions */}
|
|
<div className="flex justify-between items-center pt-6 border-t-2 border-blue-100">
|
|
<Button variant="outline" onClick={() => navigate(createPageUrl('Invoices'))} className="border-slate-300">
|
|
Cancel
|
|
</Button>
|
|
<div className="flex gap-3">
|
|
<Button
|
|
variant="outline"
|
|
onClick={() => saveMutation.mutate({ ...formData, status: "Draft" })}
|
|
disabled={saveMutation.isPending}
|
|
className="border-blue-300 text-blue-700 hover:bg-blue-50"
|
|
>
|
|
Save as Draft
|
|
</Button>
|
|
<Button
|
|
onClick={() => saveMutation.mutate(formData)}
|
|
disabled={saveMutation.isPending}
|
|
className="bg-gradient-to-r from-blue-600 to-blue-700 hover:from-blue-700 hover:to-blue-800 text-white font-semibold px-8 shadow-lg"
|
|
>
|
|
{saveMutation.isPending ? "Saving..." : isEdit ? "Update Invoice" : "Create Invoice"}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
</div>
|
|
);
|
|
} |