diff --git a/src/components/cases/expenses-tab.tsx b/src/components/cases/expenses-tab.tsx index 7cb78f3..2181233 100644 --- a/src/components/cases/expenses-tab.tsx +++ b/src/components/cases/expenses-tab.tsx @@ -6,7 +6,7 @@ import { Label } from "@/components/ui/label"; import { Checkbox } from "@/components/ui/checkbox"; import { supabase } from "@/integrations/supabase/client"; import { useAuth } from "@/lib/auth"; -import { Plus, Trash2, Loader2, Receipt as ReceiptIcon, CheckCircle2, Undo2, Download, RefreshCw } from "lucide-react"; +import { Plus, Trash2, Loader2, Receipt as ReceiptIcon, CheckCircle2, Undo2, Download, RefreshCw, Pencil } from "lucide-react"; import { formatCurrency, formatDate } from "@/lib/format"; import { ensureMarkedInvoicedPlaceholder } from "@/lib/invoice-generation"; import { toast } from "sonner"; @@ -19,6 +19,7 @@ import { SelectValue, } from "@/components/ui/select"; import { SearchableSelect } from "@/components/ui/searchable-select"; +import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog"; interface FeeItem { id: string; @@ -47,6 +48,15 @@ export function CaseExpensesTab({ caseId }: { caseId: string }) { const [fees, setFees] = useState([]); const [selectedFeeId, setSelectedFeeId] = useState(""); const [showImport, setShowImport] = useState(false); + const [staff, setStaff] = useState>([]); + const [editItem, setEditItem] = useState(null); + + useEffect(() => { + (async () => { + const { data } = await supabase.from("profiles").select("id, full_name, email").order("full_name"); + setStaff((data ?? []) as any); + })(); + }, []); const load = async () => { const { data, error } = await supabase @@ -356,6 +366,7 @@ export function CaseExpensesTab({ caseId }: { caseId: string }) { )} {!e.invoice_id && } + {!e.invoice_id && } @@ -373,6 +384,14 @@ export function CaseExpensesTab({ caseId }: { caseId: string }) { clientId={clientId} onImported={load} /> + + setEditItem(null)} + onSaved={() => { setEditItem(null); load(); }} + /> ); } @@ -385,3 +404,125 @@ function Stat({ label, value }: { label: string; value: string }) { ); } + +function EditExpenseDialog({ + item, + staff, + currentUserId, + onClose, + onSaved, +}: { + item: any | null; + staff: Array<{ id: string; full_name: string | null; email: string | null }>; + currentUserId: string; + onClose: () => void; + onSaved: () => void; +}) { + const [form, setForm] = useState({ + expense_date: "", + user_id: "", + description: "", + quantity: "1", + unit_price: "", + billable: true, + }); + const [saving, setSaving] = useState(false); + + useEffect(() => { + if (item) { + setForm({ + expense_date: item.expense_date ?? "", + user_id: item.user_id ?? currentUserId, + description: item.description ?? "", + quantity: String(item.quantity ?? 1), + unit_price: String(item.unit_price ?? item.amount ?? ""), + billable: !!item.billable, + }); + } + }, [item, currentUserId]); + + const save = async () => { + if (!item) return; + const quantity = parseFloat(form.quantity); + const unit_price = parseFloat(form.unit_price); + if (isNaN(quantity) || quantity <= 0) { toast.error("Quantity must be > 0"); return; } + if (isNaN(unit_price) || unit_price < 0) { toast.error("Invalid unit price"); return; } + if (!form.description.trim()) { toast.error("Description required"); return; } + const amount = +(quantity * unit_price).toFixed(2); + setSaving(true); + const { error } = await supabase + .from("expenses") + .update({ + expense_date: form.expense_date, + user_id: form.user_id, + description: form.description.trim(), + quantity, + unit_price, + amount, + billable: form.billable, + }) + .eq("id", item.id); + setSaving(false); + if (error) { toast.error(error.message); return; } + toast.success("Expense updated"); + onSaved(); + }; + + return ( + { if (!o) onClose(); }}> + + Edit expense +
+
+
+ + setForm({ ...form, expense_date: e.target.value })} /> +
+
+ + +
+
+
+
+ + setForm({ ...form, quantity: e.target.value })} /> +
+
+ + setForm({ ...form, unit_price: e.target.value })} /> +
+
+ + +
+
+
+ + setForm({ ...form, description: e.target.value })} maxLength={500} /> +
+ +
+ + + + +
+
+ ); +} diff --git a/src/components/cases/time-tab.tsx b/src/components/cases/time-tab.tsx index 14404df..7f2a4d4 100644 --- a/src/components/cases/time-tab.tsx +++ b/src/components/cases/time-tab.tsx @@ -7,7 +7,7 @@ import { Textarea } from "@/components/ui/textarea"; import { Checkbox } from "@/components/ui/checkbox"; import { supabase } from "@/integrations/supabase/client"; import { useAuth } from "@/lib/auth"; -import { Plus, Trash2, Loader2, FilePlus, PlusCircle, CheckCircle2, Undo2, Download } from "lucide-react"; +import { Plus, Trash2, Loader2, FilePlus, PlusCircle, CheckCircle2, Undo2, Download, Pencil } from "lucide-react"; import { formatCurrency, formatDate } from "@/lib/format"; import { ensureMarkedInvoicedPlaceholder } from "@/lib/invoice-generation"; import { roundToTenth } from "@/lib/timer"; @@ -22,6 +22,7 @@ import { } from "@/components/ui/select"; import { SearchableSelect } from "@/components/ui/searchable-select"; import { NewFeeItemDialog } from "@/components/fees/new-fee-item-dialog"; +import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog"; interface FeeItem { id: string; @@ -61,6 +62,7 @@ export function CaseTimeTab({ caseRecord, onInvoice }: CaseTimeTabProps) { const [selectedFeeId, setSelectedFeeId] = useState(""); const [showNewFee, setShowNewFee] = useState(false); const [showImport, setShowImport] = useState(false); + const [editEntry, setEditEntry] = useState(null); useEffect(() => { (async () => { @@ -354,6 +356,7 @@ export function CaseTimeTab({ caseRecord, onInvoice }: CaseTimeTabProps) { )} {!e.invoice_id && } + {!e.invoice_id && } @@ -393,6 +396,14 @@ export function CaseTimeTab({ caseRecord, onInvoice }: CaseTimeTabProps) { clientId={caseRecord.client_id ?? null} onImported={load} /> + + setEditEntry(null)} + onSaved={() => { setEditEntry(null); load(); }} + /> ); } @@ -405,3 +416,117 @@ function Stat({ label, value }: { label: string; value: string }) { ); } + +function EditTimeEntryDialog({ + entry, + staff, + currentUserId, + onClose, + onSaved, +}: { + entry: any | null; + staff: Array<{ id: string; full_name: string | null; email: string | null; hourly_rate: number | null }>; + currentUserId: string; + onClose: () => void; + onSaved: () => void; +}) { + const [form, setForm] = useState({ + work_date: "", + user_id: "", + hours: "", + hourly_rate: "", + description: "", + billable: true, + }); + const [saving, setSaving] = useState(false); + + useEffect(() => { + if (entry) { + setForm({ + work_date: entry.work_date ?? "", + user_id: entry.user_id ?? currentUserId, + hours: String(entry.hours ?? ""), + hourly_rate: String(entry.hourly_rate ?? ""), + description: entry.description ?? "", + billable: !!entry.billable, + }); + } + }, [entry, currentUserId]); + + const save = async () => { + if (!entry) return; + const hours = roundToTenth(parseFloat(form.hours || "0") || 0); + const rate = parseFloat(form.hourly_rate || "0") || 0; + if (hours <= 0) { toast.error("Hours must be greater than 0"); return; } + if (!form.description.trim()) { toast.error("Description required"); return; } + setSaving(true); + const { error } = await supabase + .from("time_entries") + .update({ + work_date: form.work_date, + user_id: form.user_id, + hours, + hourly_rate: rate, + description: form.description.trim(), + billable: form.billable, + }) + .eq("id", entry.id); + setSaving(false); + if (error) { toast.error(error.message); return; } + toast.success("Time entry updated"); + onSaved(); + }; + + return ( + { if (!o) onClose(); }}> + + Edit time entry +
+
+
+ + setForm({ ...form, work_date: e.target.value })} /> +
+
+ + +
+
+
+
+ + setForm({ ...form, hours: e.target.value })} /> +
+
+ + setForm({ ...form, hourly_rate: e.target.value })} /> +
+
+
+ +