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:
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user