Fixed time entries & invoicing
X-Lovable-Edit-ID: edt-9ff82267-ec10-490c-bb67-b7db32f1d619 Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com>
This commit is contained in:
@@ -7,12 +7,17 @@ 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 } from "lucide-react";
|
||||
import { Plus, Trash2, Loader2, FilePlus } from "lucide-react";
|
||||
import { formatCurrency, formatDate } from "@/lib/format";
|
||||
import { roundToTenth } from "@/lib/timer";
|
||||
import { toast } from "sonner";
|
||||
|
||||
export function CaseTimeTab({ caseRecord }: { caseRecord: any }) {
|
||||
interface CaseTimeTabProps {
|
||||
caseRecord: any;
|
||||
onInvoice?: () => void;
|
||||
}
|
||||
|
||||
export function CaseTimeTab({ caseRecord, onInvoice }: CaseTimeTabProps) {
|
||||
const { user } = useAuth();
|
||||
const [entries, setEntries] = useState<any[]>([]);
|
||||
const [showForm, setShowForm] = useState(false);
|
||||
@@ -27,12 +32,19 @@ export function CaseTimeTab({ caseRecord }: { caseRecord: any }) {
|
||||
});
|
||||
|
||||
const load = async () => {
|
||||
const { data } = await supabase
|
||||
const { data, error } = await supabase
|
||||
.from("time_entries")
|
||||
.select("*, user:profiles!time_entries_user_id_fkey(full_name, email)")
|
||||
.select("*")
|
||||
.eq("case_id", caseRecord.id)
|
||||
.order("work_date", { ascending: false });
|
||||
setEntries(data ?? []);
|
||||
if (error) { toast.error("Could not load time entries", { description: error.message }); return; }
|
||||
const ids = Array.from(new Set((data ?? []).map((d: any) => d.user_id).filter(Boolean)));
|
||||
let profileMap: Record<string, any> = {};
|
||||
if (ids.length) {
|
||||
const { data: profs } = await supabase.from("profiles").select("id, full_name, email").in("id", ids);
|
||||
profileMap = Object.fromEntries((profs ?? []).map((p: any) => [p.id, p]));
|
||||
}
|
||||
setEntries((data ?? []).map((d: any) => ({ ...d, user: d.user_id ? profileMap[d.user_id] : null })));
|
||||
};
|
||||
|
||||
useEffect(() => { load(); }, [caseRecord.id]);
|
||||
@@ -101,9 +113,16 @@ export function CaseTimeTab({ caseRecord }: { caseRecord: any }) {
|
||||
<Stat label="Billed total" value={formatCurrency(totals.billable)} />
|
||||
<Stat label="Unbilled" value={formatCurrency(totals.unbilled)} />
|
||||
</div>
|
||||
<Button onClick={() => setShowForm((s) => !s)}>
|
||||
<Plus className="h-4 w-4 mr-2" /> {showForm ? "Cancel" : "Log time"}
|
||||
</Button>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{totals.unbilled > 0 && onInvoice && (
|
||||
<Button variant="outline" onClick={onInvoice}>
|
||||
<FilePlus className="h-4 w-4 mr-2" /> Invoice case ({formatCurrency(totals.unbilled)})
|
||||
</Button>
|
||||
)}
|
||||
<Button onClick={() => setShowForm((s) => !s)}>
|
||||
<Plus className="h-4 w-4 mr-2" /> {showForm ? "Cancel" : "Log time"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{showForm && (
|
||||
|
||||
@@ -137,28 +137,7 @@ function CaseDetail() {
|
||||
</Card>
|
||||
)}
|
||||
|
||||
<Tabs defaultValue="litigation">
|
||||
<TabsList className="mb-4 flex-wrap h-auto">
|
||||
<TabsTrigger value="litigation"><Scale className="h-3.5 w-3.5 mr-1.5" />Litigation</TabsTrigger>
|
||||
<TabsTrigger value="overview"><Activity className="h-3.5 w-3.5 mr-1.5" />Status log</TabsTrigger>
|
||||
<TabsTrigger value="documents"><FileText className="h-3.5 w-3.5 mr-1.5" />Documents</TabsTrigger>
|
||||
<TabsTrigger value="time"><Clock className="h-3.5 w-3.5 mr-1.5" />Time</TabsTrigger>
|
||||
<TabsTrigger value="expenses"><DollarSign className="h-3.5 w-3.5 mr-1.5" />Expenses</TabsTrigger>
|
||||
<TabsTrigger value="invoices"><Receipt className="h-3.5 w-3.5 mr-1.5" />Invoices</TabsTrigger>
|
||||
{(data.client?.client_type === "hoa" || data.client?.client_type === "condo") && (
|
||||
<TabsTrigger value="collections"><Users className="h-3.5 w-3.5 mr-1.5" />Collections</TabsTrigger>
|
||||
)}
|
||||
</TabsList>
|
||||
<TabsContent value="litigation"><CaseLitigationTab caseRecord={data} canManage={canManage} onSaved={load} /></TabsContent>
|
||||
<TabsContent value="overview"><CaseStatusTab caseId={caseId} caseLabel={`${data.case_number} — ${data.title}`} /></TabsContent>
|
||||
<TabsContent value="documents"><CaseDocumentsTab caseId={caseId} /></TabsContent>
|
||||
<TabsContent value="time"><CaseTimeTab caseRecord={data} /></TabsContent>
|
||||
<TabsContent value="expenses"><CaseExpensesTab caseId={caseId} /></TabsContent>
|
||||
<TabsContent value="invoices"><CaseInvoicesTab caseRecord={data} /></TabsContent>
|
||||
{(data.client?.client_type === "hoa" || data.client?.client_type === "condo") && (
|
||||
<TabsContent value="collections"><CaseCollectionsTab caseRecord={data} /></TabsContent>
|
||||
)}
|
||||
</Tabs>
|
||||
<CaseTabs data={data} caseId={caseId} canManage={canManage} load={load} />
|
||||
|
||||
<div className="mt-8 grid grid-cols-2 sm:grid-cols-4 gap-3 text-center text-xs">
|
||||
<SmallStat label="Default rate" value={data.default_hourly_rate ? formatCurrency(data.default_hourly_rate) + "/hr" : "—"} />
|
||||
@@ -170,6 +149,34 @@ function CaseDetail() {
|
||||
);
|
||||
}
|
||||
|
||||
function CaseTabs({ data, caseId, canManage, load }: { data: any; caseId: string; canManage: boolean; load: () => void }) {
|
||||
const [tab, setTab] = useState("litigation");
|
||||
return (
|
||||
<Tabs value={tab} onValueChange={setTab}>
|
||||
<TabsList className="mb-4 flex-wrap h-auto">
|
||||
<TabsTrigger value="litigation"><Scale className="h-3.5 w-3.5 mr-1.5" />Litigation</TabsTrigger>
|
||||
<TabsTrigger value="overview"><Activity className="h-3.5 w-3.5 mr-1.5" />Status log</TabsTrigger>
|
||||
<TabsTrigger value="documents"><FileText className="h-3.5 w-3.5 mr-1.5" />Documents</TabsTrigger>
|
||||
<TabsTrigger value="time"><Clock className="h-3.5 w-3.5 mr-1.5" />Time</TabsTrigger>
|
||||
<TabsTrigger value="expenses"><DollarSign className="h-3.5 w-3.5 mr-1.5" />Expenses</TabsTrigger>
|
||||
<TabsTrigger value="invoices"><Receipt className="h-3.5 w-3.5 mr-1.5" />Invoices</TabsTrigger>
|
||||
{(data.client?.client_type === "hoa" || data.client?.client_type === "condo") && (
|
||||
<TabsTrigger value="collections"><Users className="h-3.5 w-3.5 mr-1.5" />Collections</TabsTrigger>
|
||||
)}
|
||||
</TabsList>
|
||||
<TabsContent value="litigation"><CaseLitigationTab caseRecord={data} canManage={canManage} onSaved={load} /></TabsContent>
|
||||
<TabsContent value="overview"><CaseStatusTab caseId={caseId} caseLabel={`${data.case_number} — ${data.title}`} /></TabsContent>
|
||||
<TabsContent value="documents"><CaseDocumentsTab caseId={caseId} /></TabsContent>
|
||||
<TabsContent value="time"><CaseTimeTab caseRecord={data} onInvoice={() => setTab("invoices")} /></TabsContent>
|
||||
<TabsContent value="expenses"><CaseExpensesTab caseId={caseId} /></TabsContent>
|
||||
<TabsContent value="invoices"><CaseInvoicesTab caseRecord={data} /></TabsContent>
|
||||
{(data.client?.client_type === "hoa" || data.client?.client_type === "condo") && (
|
||||
<TabsContent value="collections"><CaseCollectionsTab caseRecord={data} /></TabsContent>
|
||||
)}
|
||||
</Tabs>
|
||||
);
|
||||
}
|
||||
|
||||
function SmallStat({ label, value }: { label: string; value: string }) {
|
||||
return (
|
||||
<div className="border rounded-md py-2 px-3">
|
||||
|
||||
Reference in New Issue
Block a user