From 46f2d14c5dc1adf7db2ab683daac0f3faf2c9f99 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Fri, 17 Apr 2026 02:21:25 +0000 Subject: [PATCH 1/4] Changes Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com> --- src/integrations/supabase/types.ts | 141 ++++++++++++++++++ ...2_8a0edd0a-97dc-4606-9958-f6c247997d3f.sql | 84 +++++++++++ 2 files changed, 225 insertions(+) create mode 100644 supabase/migrations/20260417022122_8a0edd0a-97dc-4606-9958-f6c247997d3f.sql diff --git a/src/integrations/supabase/types.ts b/src/integrations/supabase/types.ts index 99ee050..77e48a7 100644 --- a/src/integrations/supabase/types.ts +++ b/src/integrations/supabase/types.ts @@ -14,6 +14,48 @@ export type Database = { } public: { Tables: { + case_contacts: { + Row: { + case_id: string + contact_id: string + created_at: string + created_by: string | null + id: string + role: string | null + } + Insert: { + case_id: string + contact_id: string + created_at?: string + created_by?: string | null + id?: string + role?: string | null + } + Update: { + case_id?: string + contact_id?: string + created_at?: string + created_by?: string | null + id?: string + role?: string | null + } + Relationships: [ + { + foreignKeyName: "case_contacts_case_id_fkey" + columns: ["case_id"] + isOneToOne: false + referencedRelation: "cases" + referencedColumns: ["id"] + }, + { + foreignKeyName: "case_contacts_contact_id_fkey" + columns: ["contact_id"] + isOneToOne: false + referencedRelation: "contacts" + referencedColumns: ["id"] + }, + ] + } cases: { Row: { assigned_attorney_id: string | null @@ -137,6 +179,48 @@ export type Database = { }, ] } + client_contacts: { + Row: { + client_id: string + contact_id: string + created_at: string + created_by: string | null + id: string + role: string | null + } + Insert: { + client_id: string + contact_id: string + created_at?: string + created_by?: string | null + id?: string + role?: string | null + } + Update: { + client_id?: string + contact_id?: string + created_at?: string + created_by?: string | null + id?: string + role?: string | null + } + Relationships: [ + { + foreignKeyName: "client_contacts_client_id_fkey" + columns: ["client_id"] + isOneToOne: false + referencedRelation: "clients" + referencedColumns: ["id"] + }, + { + foreignKeyName: "client_contacts_contact_id_fkey" + columns: ["contact_id"] + isOneToOne: false + referencedRelation: "contacts" + referencedColumns: ["id"] + }, + ] + } clients: { Row: { address_line1: string | null @@ -524,6 +608,63 @@ export type Database = { }, ] } + contacts: { + Row: { + address_line1: string | null + address_line2: string | null + city: string | null + company: string | null + contact_type: string + created_at: string + created_by: string | null + email: string | null + id: string + name: string + notes: string | null + phone: string | null + postal_code: string | null + state: string | null + title: string | null + updated_at: string + } + Insert: { + address_line1?: string | null + address_line2?: string | null + city?: string | null + company?: string | null + contact_type?: string + created_at?: string + created_by?: string | null + email?: string | null + id?: string + name: string + notes?: string | null + phone?: string | null + postal_code?: string | null + state?: string | null + title?: string | null + updated_at?: string + } + Update: { + address_line1?: string | null + address_line2?: string | null + city?: string | null + company?: string | null + contact_type?: string + created_at?: string + created_by?: string | null + email?: string | null + id?: string + name?: string + notes?: string | null + phone?: string | null + postal_code?: string | null + state?: string | null + title?: string | null + updated_at?: string + } + Relationships: [] + } document_folders: { Row: { case_id: string diff --git a/supabase/migrations/20260417022122_8a0edd0a-97dc-4606-9958-f6c247997d3f.sql b/supabase/migrations/20260417022122_8a0edd0a-97dc-4606-9958-f6c247997d3f.sql new file mode 100644 index 0000000..fe794fc --- /dev/null +++ b/supabase/migrations/20260417022122_8a0edd0a-97dc-4606-9958-f6c247997d3f.sql @@ -0,0 +1,84 @@ + +-- Contacts directory +CREATE TABLE public.contacts ( + id UUID NOT NULL DEFAULT gen_random_uuid() PRIMARY KEY, + name TEXT NOT NULL, + company TEXT, + title TEXT, + email TEXT, + phone TEXT, + address_line1 TEXT, + address_line2 TEXT, + city TEXT, + state TEXT, + postal_code TEXT, + notes TEXT, + contact_type TEXT NOT NULL DEFAULT 'other', + created_by UUID, + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT now() +); + +ALTER TABLE public.contacts ENABLE ROW LEVEL SECURITY; + +CREATE POLICY contacts_select_auth ON public.contacts + FOR SELECT TO authenticated USING (true); +CREATE POLICY contacts_insert_auth ON public.contacts + FOR INSERT TO authenticated WITH CHECK (auth.uid() IS NOT NULL); +CREATE POLICY contacts_update_owner ON public.contacts + FOR UPDATE TO authenticated USING (public.is_admin(auth.uid()) OR created_by = auth.uid()); +CREATE POLICY contacts_delete_owner ON public.contacts + FOR DELETE TO authenticated USING (public.is_admin(auth.uid()) OR created_by = auth.uid()); + +CREATE TRIGGER tg_contacts_updated_at + BEFORE UPDATE ON public.contacts + FOR EACH ROW EXECUTE FUNCTION public.tg_set_updated_at(); + +CREATE INDEX idx_contacts_name ON public.contacts (lower(name)); +CREATE INDEX idx_contacts_type ON public.contacts (contact_type); + +-- Case <-> Contact join +CREATE TABLE public.case_contacts ( + id UUID NOT NULL DEFAULT gen_random_uuid() PRIMARY KEY, + case_id UUID NOT NULL REFERENCES public.cases(id) ON DELETE CASCADE, + contact_id UUID NOT NULL REFERENCES public.contacts(id) ON DELETE CASCADE, + role TEXT, + created_by UUID, + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + UNIQUE (case_id, contact_id) +); + +ALTER TABLE public.case_contacts ENABLE ROW LEVEL SECURITY; + +CREATE POLICY case_contacts_select ON public.case_contacts + FOR SELECT TO authenticated USING (public.can_access_case(case_id, auth.uid())); +CREATE POLICY case_contacts_insert ON public.case_contacts + FOR INSERT TO authenticated WITH CHECK (public.can_access_case(case_id, auth.uid())); +CREATE POLICY case_contacts_delete ON public.case_contacts + FOR DELETE TO authenticated USING (public.can_access_case(case_id, auth.uid())); + +CREATE INDEX idx_case_contacts_case ON public.case_contacts (case_id); +CREATE INDEX idx_case_contacts_contact ON public.case_contacts (contact_id); + +-- Client <-> Contact join +CREATE TABLE public.client_contacts ( + id UUID NOT NULL DEFAULT gen_random_uuid() PRIMARY KEY, + client_id UUID NOT NULL REFERENCES public.clients(id) ON DELETE CASCADE, + contact_id UUID NOT NULL REFERENCES public.contacts(id) ON DELETE CASCADE, + role TEXT, + created_by UUID, + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + UNIQUE (client_id, contact_id) +); + +ALTER TABLE public.client_contacts ENABLE ROW LEVEL SECURITY; + +CREATE POLICY client_contacts_select ON public.client_contacts + FOR SELECT TO authenticated USING (true); +CREATE POLICY client_contacts_insert ON public.client_contacts + FOR INSERT TO authenticated WITH CHECK (auth.uid() IS NOT NULL); +CREATE POLICY client_contacts_delete ON public.client_contacts + FOR DELETE TO authenticated USING (public.is_admin(auth.uid()) OR created_by = auth.uid()); + +CREATE INDEX idx_client_contacts_client ON public.client_contacts (client_id); +CREATE INDEX idx_client_contacts_contact ON public.client_contacts (contact_id); From 1f3fd28159a469a41aab1ce5bd1992eb19b99d0e Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Fri, 17 Apr 2026 02:23:12 +0000 Subject: [PATCH 2/4] Changes Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com> --- .../contacts/contact-form-dialog.tsx | 200 +++++++++++++++ src/components/contacts/contacts-link-tab.tsx | 240 ++++++++++++++++++ src/routeTree.gen.ts | 51 ++++ src/routes/contacts.$contactId.tsx | 188 ++++++++++++++ src/routes/contacts.index.tsx | 201 +++++++++++++++ 5 files changed, 880 insertions(+) create mode 100644 src/components/contacts/contact-form-dialog.tsx create mode 100644 src/components/contacts/contacts-link-tab.tsx create mode 100644 src/routes/contacts.$contactId.tsx create mode 100644 src/routes/contacts.index.tsx diff --git a/src/components/contacts/contact-form-dialog.tsx b/src/components/contacts/contact-form-dialog.tsx new file mode 100644 index 0000000..3eed4be --- /dev/null +++ b/src/components/contacts/contact-form-dialog.tsx @@ -0,0 +1,200 @@ +import { useEffect, useState } from "react"; +import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Button } from "@/components/ui/button"; +import { Textarea } from "@/components/ui/textarea"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { supabase } from "@/integrations/supabase/client"; +import { useAuth } from "@/lib/auth"; +import { toast } from "sonner"; +import { Loader2 } from "lucide-react"; + +export const CONTACT_TYPES = [ + { value: "opposing_counsel", label: "Opposing counsel" }, + { value: "co_counsel", label: "Co-counsel" }, + { value: "expert", label: "Expert" }, + { value: "witness", label: "Witness" }, + { value: "vendor", label: "Vendor" }, + { value: "court", label: "Court / clerk" }, + { value: "process_server", label: "Process server" }, + { value: "client_contact", label: "Client contact" }, + { value: "other", label: "Other" }, +] as const; + +export interface ContactRecord { + id?: string; + name: string; + company?: string | null; + title?: string | null; + email?: string | null; + phone?: string | null; + address_line1?: string | null; + address_line2?: string | null; + city?: string | null; + state?: string | null; + postal_code?: string | null; + notes?: string | null; + contact_type?: string | null; +} + +export function ContactFormDialog({ + open, + onOpenChange, + contact, + onSaved, +}: { + open: boolean; + onOpenChange: (v: boolean) => void; + contact?: ContactRecord | null; + onSaved?: (saved: { id: string; name: string }) => void; +}) { + const { user } = useAuth(); + const [saving, setSaving] = useState(false); + const [form, setForm] = useState({ + name: "", + contact_type: "other", + }); + + useEffect(() => { + if (open) { + setForm( + contact + ? { ...contact } + : { name: "", contact_type: "other" }, + ); + } + }, [open, contact]); + + const set = (k: K, v: ContactRecord[K]) => + setForm((f) => ({ ...f, [k]: v })); + + const onSave = async () => { + if (!form.name.trim()) { + toast.error("Name is required"); + return; + } + setSaving(true); + const payload = { + name: form.name.trim(), + company: form.company || null, + title: form.title || null, + email: form.email || null, + phone: form.phone || null, + address_line1: form.address_line1 || null, + address_line2: form.address_line2 || null, + city: form.city || null, + state: form.state || null, + postal_code: form.postal_code || null, + notes: form.notes || null, + contact_type: form.contact_type || "other", + }; + let saved: { id: string; name: string } | null = null; + if (contact?.id) { + const { data, error } = await supabase + .from("contacts") + .update(payload) + .eq("id", contact.id) + .select("id, name") + .single(); + if (error) { + toast.error(error.message); + setSaving(false); + return; + } + saved = data; + } else { + const { data, error } = await supabase + .from("contacts") + .insert({ ...payload, created_by: user?.id }) + .select("id, name") + .single(); + if (error) { + toast.error(error.message); + setSaving(false); + return; + } + saved = data; + } + setSaving(false); + toast.success(contact?.id ? "Contact updated" : "Contact created"); + onOpenChange(false); + if (saved) onSaved?.(saved); + }; + + return ( + + + + {contact?.id ? "Edit contact" : "New contact"} + + +
+
+ + set("name", e.target.value)} /> +
+
+ + +
+
+ + set("title", e.target.value)} /> +
+
+ + set("company", e.target.value)} /> +
+
+ + set("email", e.target.value)} /> +
+
+ + set("phone", e.target.value)} /> +
+
+ + set("address_line1", e.target.value)} /> +
+
+ + set("address_line2", e.target.value)} /> +
+
+ + set("city", e.target.value)} /> +
+
+ + set("state", e.target.value)} /> +
+
+ + set("postal_code", e.target.value)} /> +
+
+ +