mirror of
https://github.com/renee-png/acmcc.git
synced 2026-06-21 09:50:01 +00:00
183fe0a93c
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
161 lines
7.9 KiB
TypeScript
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>
|
|
);
|
|
}
|