Added reassign feature

X-Lovable-Edit-ID: edt-d898121a-ad67-4d92-9e10-f71e73a1748d
Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com>
This commit is contained in:
gpt-engineer-app[bot]
2026-04-18 07:04:55 +00:00
co-authored by renee-png
+96 -3
View File
@@ -1,5 +1,5 @@
import { createFileRoute, Link } from "@tanstack/react-router";
import { useEffect, useState, useCallback } from "react";
import { useEffect, useState, useCallback, useMemo } from "react";
import { ProtectedLayout } from "@/components/protected-layout";
import { PageContainer, PageHeader } from "@/components/app-shell";
import { Button } from "@/components/ui/button";
@@ -7,8 +7,10 @@ import { Card, CardContent } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { supabase } from "@/integrations/supabase/client";
import { ArrowLeft, FileText, Clock, DollarSign, Activity, Receipt, Scale, Users, Contact, Phone, Tag, Archive, ArchiveRestore } from "lucide-react";
import { ArrowLeft, FileText, Clock, DollarSign, Activity, Receipt, Scale, Users, Contact, Phone, Tag, Archive, ArchiveRestore, ArrowRightLeft, Search } from "lucide-react";
import { ContactsLinkTab } from "@/components/contacts/contacts-link-tab";
import { CaseCallLogsTab } from "@/components/cases/call-logs-tab";
import { formatCurrency, formatDate, statusBadgeClass } from "@/lib/format";
@@ -38,19 +40,26 @@ function CaseDetail() {
const [data, setData] = useState<any>(null);
const [loading, setLoading] = useState(true);
const [users, setUsers] = useState<any[]>([]);
const [clients, setClients] = useState<any[]>([]);
const [reassignOpen, setReassignOpen] = useState(false);
const [clientSearch, setClientSearch] = useState("");
const [selectedClientId, setSelectedClientId] = useState<string>("");
const [reassigning, setReassigning] = useState(false);
const load = useCallback(async () => {
const [{ data: c, error }, { data: us }] = await Promise.all([
const [{ data: c, error }, { data: us }, { data: cl }] = await Promise.all([
supabase
.from("cases")
.select("*, client:clients(id, name, client_type, annual_interest_rate), assignee:profiles!cases_assigned_attorney_id_fkey(id, full_name, email)")
.eq("id", caseId)
.maybeSingle(),
supabase.from("profiles").select("id, full_name, email"),
supabase.from("clients").select("id, name, client_type").is("archived_at", null).order("name"),
]);
if (error) toast.error("Failed to load case", { description: error.message });
setData(c);
setUsers(us ?? []);
setClients(cl ?? []);
setLoading(false);
}, [caseId]);
@@ -76,6 +85,30 @@ function CaseDetail() {
else { toast.success("Assignee updated"); load(); }
};
const filteredClients = useMemo(() => {
const q = clientSearch.trim().toLowerCase();
if (!q) return clients.slice(0, 50);
return clients.filter((c) => c.name.toLowerCase().includes(q)).slice(0, 50);
}, [clients, clientSearch]);
const reassignClient = async () => {
if (!selectedClientId || selectedClientId === data.client?.id) {
setReassignOpen(false);
return;
}
setReassigning(true);
const { error } = await supabase.from("cases").update({ client_id: selectedClientId }).eq("id", caseId);
setReassigning(false);
if (error) toast.error("Could not reassign", { description: error.message });
else {
toast.success("Case reassigned to new client");
setReassignOpen(false);
setClientSearch("");
setSelectedClientId("");
load();
}
};
if (loading) return <PageContainer><p className="text-muted-foreground">Loading…</p></PageContainer>;
if (!data) {
return (
@@ -134,6 +167,15 @@ function CaseDetail() {
<SelectItem value="closed">Closed</SelectItem>
</SelectContent>
</Select>
<Button
variant="outline"
onClick={() => {
setSelectedClientId(data.client?.id ?? "");
setReassignOpen(true);
}}
>
<ArrowRightLeft className="h-4 w-4 mr-2" /> Reassign client
</Button>
<Button
variant="outline"
onClick={async () => {
@@ -150,6 +192,57 @@ function CaseDetail() {
)}
</div>
<Dialog open={reassignOpen} onOpenChange={setReassignOpen}>
<DialogContent className="max-w-lg">
<DialogHeader>
<DialogTitle>Reassign case to a different client</DialogTitle>
<DialogDescription>
Currently assigned to <span className="font-medium">{data.client?.name ?? "—"}</span>. Pick a new client below.
</DialogDescription>
</DialogHeader>
<div className="space-y-3">
<div className="relative">
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
<Input
placeholder="Search clients…"
value={clientSearch}
onChange={(e) => setClientSearch(e.target.value)}
className="pl-8"
autoFocus
/>
</div>
<div className="max-h-72 overflow-y-auto border rounded-md divide-y">
{filteredClients.length === 0 ? (
<p className="text-sm text-muted-foreground p-3">No clients match.</p>
) : (
filteredClients.map((c) => (
<button
key={c.id}
type="button"
onClick={() => setSelectedClientId(c.id)}
className={`w-full text-left px-3 py-2 text-sm hover:bg-muted flex items-center justify-between ${
selectedClientId === c.id ? "bg-muted" : ""
}`}
>
<span>{c.name}</span>
<span className="text-xs text-muted-foreground uppercase">{c.client_type}</span>
</button>
))
)}
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setReassignOpen(false)}>Cancel</Button>
<Button
onClick={reassignClient}
disabled={reassigning || !selectedClientId || selectedClientId === data.client?.id}
>
{reassigning ? "Reassigning…" : "Reassign"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{data.description && (
<Card className="mb-6 border-border/60">
<CardContent className="p-4">