Files
acmcc/src/pages/CallLogPage.tsx
T
2026-06-01 20:19:26 -04:00

161 lines
7.9 KiB
TypeScript

import { useState, useEffect } from "react";
import { supabase } from "@/integrations/supabase/client";
import { useToast } from "@/hooks/use-toast";
import { Phone, Plus, Search, Trash2, MoreHorizontal, Download, Upload, Clock, User } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Card, CardContent } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
import { CallLogDialog } from "@/components/CallLogDialog";
const statusStyles: Record<string, string> = {
pending: "bg-amber-100 text-amber-700 border-amber-300",
responded: "bg-emerald-100 text-emerald-700 border-emerald-300",
resolved: "bg-muted text-muted-foreground",
};
export default function CallLogPage() {
const { toast } = useToast();
const [calls, setCalls] = useState<any[]>([]);
const [associations, setAssociations] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
const [search, setSearch] = useState("");
const [statusFilter, setStatusFilter] = useState("all");
const [dialogOpen, setDialogOpen] = useState(false);
const [editing, setEditing] = useState<any>(null);
const fetchData = async () => {
setLoading(true);
const [callRes, assocRes] = await Promise.all([
supabase.from("call_logs").select("*, associations(name)").order("created_at", { ascending: false }),
supabase.from("associations").select("id, name").eq("status", "active").order("name"),
]);
setCalls(callRes.data || []);
setAssociations(assocRes.data || []);
setLoading(false);
};
useEffect(() => { fetchData(); }, []);
const handleDelete = async (id: string) => {
const { error } = await supabase.from("call_logs").delete().eq("id", id);
if (error) toast({ title: "Error", description: error.message, variant: "destructive" });
else { toast({ title: "Call log deleted" }); fetchData(); }
};
const filtered = calls.filter((c) => {
const q = search.toLowerCase();
const matchSearch = !q || c.caller_name.toLowerCase().includes(q) || (c.subject || "").toLowerCase().includes(q) || (c.notes || "").toLowerCase().includes(q) || (c.associations?.name || "").toLowerCase().includes(q);
const status = c.follow_up_required ? (c.call_type === "resolved" ? "resolved" : "pending") : "responded";
const matchStatus = statusFilter === "all" || status === statusFilter;
return matchSearch && matchStatus;
});
return (
<div className="space-y-6">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div>
<h1 className="text-2xl font-bold text-foreground">Communication Logs</h1>
<p className="text-sm text-muted-foreground mt-1">Track and manage client communications.</p>
</div>
<div className="flex gap-2">
<Button variant="outline" className="gap-2"><Download className="h-4 w-4" /> Export</Button>
<Button variant="outline" className="gap-2"><Upload className="h-4 w-4" /> Import</Button>
<Button className="gap-2" onClick={() => { setEditing(null); setDialogOpen(true); }}><Plus className="h-4 w-4" /> Add Log</Button>
</div>
</div>
<div className="flex flex-col sm:flex-row gap-4 items-start sm:items-center">
<div className="relative flex-1 max-w-md">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input placeholder="Search by client, caller, notes..." className="pl-10" value={search} onChange={(e) => setSearch(e.target.value)} />
</div>
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<span>Showing {filtered.length} records</span>
</div>
<div className="ml-auto flex gap-2 items-center text-sm">
<span className="text-muted-foreground">Filter Status:</span>
{["all", "pending", "responded", "resolved"].map(s => (
<Button
key={s}
variant={statusFilter === s ? "default" : "outline"}
size="sm"
onClick={() => setStatusFilter(s)}
className="capitalize"
>
{s === "all" ? "All" : s}
</Button>
))}
</div>
</div>
{loading ? (
<div className="flex justify-center py-12"><div className="h-8 w-8 animate-spin rounded-full border-4 border-primary border-t-transparent" /></div>
) : filtered.length === 0 ? (
<Card><CardContent className="py-12 text-center text-muted-foreground">No call logs found.</CardContent></Card>
) : (
<div className="space-y-4">
{filtered.map((c) => {
const status = c.follow_up_required ? "pending" : "responded";
return (
<Card key={c.id} className="hover:shadow-md transition-shadow">
<CardContent className="p-5">
<div className="flex items-start justify-between">
<div className="flex-1">
<div className="flex items-center gap-3 mb-1">
<Phone className="h-4 w-4 text-muted-foreground" />
<h3 className="font-semibold text-foreground">{c.associations?.name || "Unknown"}</h3>
<Badge variant="outline" className="capitalize text-xs">{c.call_type || "Inbound"} Call</Badge>
</div>
<div className="flex items-center gap-2 text-sm text-muted-foreground mt-1 ml-7">
<User className="h-3 w-3" />
<span>{c.caller_name}</span>
{c.caller_phone && <span>({c.caller_phone})</span>}
</div>
{c.notes && (
<p className="text-sm text-foreground mt-2 ml-7">{c.notes}</p>
)}
{c.subject && (
<p className="text-xs text-muted-foreground mt-1 ml-7 italic">{c.subject}</p>
)}
<div className="flex items-center gap-4 mt-3 ml-7 text-xs text-muted-foreground">
<span className="flex items-center gap-1"><Clock className="h-3 w-3" /> Duration: {c.duration || "—"} mins</span>
<span>Recorded by: {c.taken_by || "—"}</span>
</div>
</div>
<div className="flex flex-col items-end gap-2">
<Badge className={statusStyles[status] || statusStyles.responded} variant="outline">
{status.charAt(0).toUpperCase() + status.slice(1)}
</Badge>
<span className="text-xs text-muted-foreground">
{new Date(c.created_at).toLocaleDateString()}
</span>
<DropdownMenu>
<DropdownMenuTrigger asChild><Button variant="ghost" size="icon" className="h-8 w-8"><MoreHorizontal className="h-4 w-4" /></Button></DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem className="text-destructive" onClick={() => handleDelete(c.id)}><Trash2 className="h-4 w-4 mr-2" /> Delete</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
</CardContent>
</Card>
);
})}
</div>
)}
<CallLogDialog
open={dialogOpen}
onOpenChange={setDialogOpen}
associations={associations}
onSaved={fetchData}
/>
</div>
);
}