Changes
Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com>
This commit is contained in:
co-authored by
renee-png
parent
5d3ae5c467
commit
6fa3188fe7
@@ -0,0 +1,296 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useAuth } from "@/lib/auth";
|
||||
import { supabase } from "@/integrations/supabase/client";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { SearchableSelect } from "@/components/ui/searchable-select";
|
||||
import { Phone, Loader2 } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
interface ClientOpt {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
interface CaseOpt {
|
||||
id: string;
|
||||
case_number: string;
|
||||
title: string;
|
||||
client_id: string | null;
|
||||
}
|
||||
interface ContactOpt {
|
||||
id: string;
|
||||
name: string;
|
||||
phone: string | null;
|
||||
}
|
||||
|
||||
export function QuickAddCallLog() {
|
||||
const { user } = useAuth();
|
||||
const [open, setOpen] = useState(false);
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
||||
const [clients, setClients] = useState<ClientOpt[]>([]);
|
||||
const [cases, setCases] = useState<CaseOpt[]>([]);
|
||||
const [contacts, setContacts] = useState<ContactOpt[]>([]);
|
||||
|
||||
const [clientId, setClientId] = useState("");
|
||||
const [caseId, setCaseId] = useState("");
|
||||
const [contactId, setContactId] = useState("");
|
||||
const [callerName, setCallerName] = useState("");
|
||||
const [callerPhone, setCallerPhone] = useState("");
|
||||
const [direction, setDirection] = useState<"inbound" | "outbound">("outbound");
|
||||
const [subject, setSubject] = useState("");
|
||||
const [notes, setNotes] = useState("");
|
||||
const [duration, setDuration] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
(async () => {
|
||||
const [{ data: cs }, { data: ks }, { data: ct }] = await Promise.all([
|
||||
supabase.from("clients").select("id, name").is("archived_at", null).order("name"),
|
||||
supabase
|
||||
.from("cases")
|
||||
.select("id, case_number, title, client_id")
|
||||
.is("archived_at", null)
|
||||
.order("case_number", { ascending: false }),
|
||||
supabase
|
||||
.from("contacts")
|
||||
.select("id, name, phone")
|
||||
.is("archived_at", null)
|
||||
.order("name"),
|
||||
]);
|
||||
setClients(cs ?? []);
|
||||
setCases((ks ?? []) as CaseOpt[]);
|
||||
setContacts((ct ?? []) as ContactOpt[]);
|
||||
})();
|
||||
}, [open]);
|
||||
|
||||
const filteredCases = useMemo(
|
||||
() => (clientId ? cases.filter((k) => k.client_id === clientId) : cases),
|
||||
[cases, clientId],
|
||||
);
|
||||
|
||||
const reset = () => {
|
||||
setClientId("");
|
||||
setCaseId("");
|
||||
setContactId("");
|
||||
setCallerName("");
|
||||
setCallerPhone("");
|
||||
setDirection("outbound");
|
||||
setSubject("");
|
||||
setNotes("");
|
||||
setDuration("");
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!user) return;
|
||||
if (!subject.trim() && !notes.trim()) {
|
||||
toast.error("Please enter a subject or notes");
|
||||
return;
|
||||
}
|
||||
setSaving(true);
|
||||
try {
|
||||
// call_logs requires case_id (NOT NULL). If no case selected, we can't save to call_logs;
|
||||
// fall back to creating a status update / comment? For now, require case OR refuse.
|
||||
if (!caseId) {
|
||||
toast.error("A case is required to save a call log. Pick a case (or create one first).");
|
||||
setSaving(false);
|
||||
return;
|
||||
}
|
||||
const { error } = await supabase.from("call_logs").insert({
|
||||
case_id: caseId,
|
||||
contact_id: contactId || null,
|
||||
caller_name: callerName || null,
|
||||
caller_phone: callerPhone || null,
|
||||
direction,
|
||||
subject: subject || "(no subject)",
|
||||
notes: notes || null,
|
||||
duration_minutes: duration ? Number(duration) : null,
|
||||
call_date: new Date().toISOString(),
|
||||
created_by: user.id,
|
||||
} as never);
|
||||
if (error) throw error;
|
||||
toast.success("Call log saved");
|
||||
reset();
|
||||
setOpen(false);
|
||||
} catch (e: any) {
|
||||
toast.error(e.message ?? "Could not save call log");
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
aria-label="Log a call"
|
||||
title="Log a call"
|
||||
>
|
||||
<Phone className="h-4 w-4" />
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Log a call</DialogTitle>
|
||||
<DialogDescription>Record an incoming or outgoing call.</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="grid gap-4 py-2">
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<Label>Direction</Label>
|
||||
<Select value={direction} onValueChange={(v) => setDirection(v as "inbound" | "outbound")}>
|
||||
<SelectTrigger><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="outbound">Outgoing</SelectItem>
|
||||
<SelectItem value="inbound">Incoming</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div>
|
||||
<Label>Duration (min)</Label>
|
||||
<Input
|
||||
type="number"
|
||||
min="0"
|
||||
value={duration}
|
||||
onChange={(e) => setDuration(e.target.value)}
|
||||
placeholder="optional"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label>Client (optional)</Label>
|
||||
<SearchableSelect
|
||||
value={clientId}
|
||||
onValueChange={(id) => {
|
||||
setClientId(id);
|
||||
setCaseId("");
|
||||
}}
|
||||
placeholder="Select client"
|
||||
searchPlaceholder="Search clients…"
|
||||
emptyText="No clients found."
|
||||
options={[
|
||||
{ value: "", label: "— None —", keywords: "none" },
|
||||
...clients.map((c) => ({ value: c.id, label: c.name, keywords: c.name })),
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label>Case (optional)</Label>
|
||||
<SearchableSelect
|
||||
value={caseId}
|
||||
onValueChange={setCaseId}
|
||||
placeholder="Select case"
|
||||
searchPlaceholder="Search cases…"
|
||||
emptyText="No cases found."
|
||||
options={[
|
||||
{ value: "", label: "— None —", keywords: "none" },
|
||||
...filteredCases.map((k) => ({
|
||||
value: k.id,
|
||||
label: `${k.case_number} — ${k.title}`,
|
||||
keywords: `${k.case_number} ${k.title}`,
|
||||
})),
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label>Contact (optional)</Label>
|
||||
<SearchableSelect
|
||||
value={contactId}
|
||||
onValueChange={(id) => {
|
||||
setContactId(id);
|
||||
const c = contacts.find((x) => x.id === id);
|
||||
if (c) {
|
||||
if (!callerName) setCallerName(c.name);
|
||||
if (!callerPhone && c.phone) setCallerPhone(c.phone);
|
||||
}
|
||||
}}
|
||||
placeholder="Select contact or enter manually below"
|
||||
searchPlaceholder="Search contacts…"
|
||||
emptyText="No contacts found."
|
||||
options={[
|
||||
{ value: "", label: "— None —", keywords: "none" },
|
||||
...contacts.map((c) => ({
|
||||
value: c.id,
|
||||
label: c.phone ? `${c.name} (${c.phone})` : c.name,
|
||||
keywords: `${c.name} ${c.phone ?? ""}`,
|
||||
})),
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<Label>Caller name</Label>
|
||||
<Input
|
||||
value={callerName}
|
||||
onChange={(e) => setCallerName(e.target.value)}
|
||||
placeholder="Manual entry"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label>Caller phone</Label>
|
||||
<Input
|
||||
value={callerPhone}
|
||||
onChange={(e) => setCallerPhone(e.target.value)}
|
||||
placeholder="Manual entry"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label>Subject</Label>
|
||||
<Input
|
||||
value={subject}
|
||||
onChange={(e) => setSubject(e.target.value)}
|
||||
placeholder="Brief summary"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label>Description / Notes</Label>
|
||||
<Textarea
|
||||
value={notes}
|
||||
onChange={(e) => setNotes(e.target.value)}
|
||||
rows={4}
|
||||
placeholder="What was discussed…"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="ghost" onClick={() => setOpen(false)} disabled={saving}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleSave} disabled={saving}>
|
||||
{saving && <Loader2 className="h-4 w-4 mr-2 animate-spin" />}
|
||||
Save call log
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user