Changes
Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com>
This commit is contained in:
co-authored by
renee-png
parent
5b31a252e4
commit
2a43b3cea9
@@ -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<FeeItem[]>([]);
|
||||
const [selectedFeeId, setSelectedFeeId] = useState<string>("");
|
||||
const [showImport, setShowImport] = useState(false);
|
||||
const [staff, setStaff] = useState<Array<{ id: string; full_name: string | null; email: string | null }>>([]);
|
||||
const [editItem, setEditItem] = useState<any | null>(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 }) {
|
||||
</Button>
|
||||
)}
|
||||
{!e.invoice_id && <Button variant="ghost" size="icon" onClick={() => del(e)}><Trash2 className="h-4 w-4 text-destructive" /></Button>}
|
||||
{!e.invoice_id && <Button variant="ghost" size="icon" onClick={() => setEditItem(e)} title="Edit"><Pencil className="h-4 w-4" /></Button>}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -373,6 +384,14 @@ export function CaseExpensesTab({ caseId }: { caseId: string }) {
|
||||
clientId={clientId}
|
||||
onImported={load}
|
||||
/>
|
||||
|
||||
<EditExpenseDialog
|
||||
item={editItem}
|
||||
staff={staff}
|
||||
currentUserId={user?.id ?? ""}
|
||||
onClose={() => setEditItem(null)}
|
||||
onSaved={() => { setEditItem(null); load(); }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -385,3 +404,125 @@ function Stat({ label, value }: { label: string; value: string }) {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<Dialog open={!!item} onOpenChange={(o) => { if (!o) onClose(); }}>
|
||||
<DialogContent>
|
||||
<DialogHeader><DialogTitle>Edit expense</DialogTitle></DialogHeader>
|
||||
<div className="space-y-3">
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<Label>Date</Label>
|
||||
<Input type="date" value={form.expense_date} onChange={(e) => setForm({ ...form, expense_date: e.target.value })} />
|
||||
</div>
|
||||
<div>
|
||||
<Label>Entered by</Label>
|
||||
<Select value={form.user_id} onValueChange={(v) => setForm({ ...form, user_id: v })}>
|
||||
<SelectTrigger><SelectValue placeholder="Select staff…" /></SelectTrigger>
|
||||
<SelectContent>
|
||||
{staff.map((s) => (
|
||||
<SelectItem key={s.id} value={s.id}>
|
||||
{(s.full_name || s.email || "Unknown") + (s.id === currentUserId ? " (me)" : "")}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
<div>
|
||||
<Label>Qty</Label>
|
||||
<Input type="number" step="any" min="0" value={form.quantity} onChange={(e) => setForm({ ...form, quantity: e.target.value })} />
|
||||
</div>
|
||||
<div>
|
||||
<Label>Unit price ($)</Label>
|
||||
<Input type="number" step="0.01" min="0" value={form.unit_price} onChange={(e) => setForm({ ...form, unit_price: e.target.value })} />
|
||||
</div>
|
||||
<div>
|
||||
<Label>Total</Label>
|
||||
<Input readOnly tabIndex={-1} className="bg-muted/40"
|
||||
value={`$${((parseFloat(form.quantity) || 0) * (parseFloat(form.unit_price) || 0)).toFixed(2)}`} />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label>Description</Label>
|
||||
<Input value={form.description} onChange={(e) => setForm({ ...form, description: e.target.value })} maxLength={500} />
|
||||
</div>
|
||||
<label className="flex items-center gap-2 text-sm">
|
||||
<Checkbox checked={form.billable} onCheckedChange={(c) => setForm({ ...form, billable: !!c })} />
|
||||
Billable
|
||||
</label>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={onClose}>Cancel</Button>
|
||||
<Button onClick={save} disabled={saving}>
|
||||
{saving && <Loader2 className="h-4 w-4 mr-2 animate-spin" />}Save
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user