From f524dc8fd1ab6798263dc85699589ac8abeef2da 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:51:56 +0000 Subject: [PATCH 1/4] Changes Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com> --- src/integrations/supabase/types.ts | 391 ++++++++++++++++++ ...3_cddbb003-f4b0-4e29-a2e5-6a0be42892ea.sql | 239 +++++++++++ 2 files changed, 630 insertions(+) create mode 100644 supabase/migrations/20260417025153_cddbb003-f4b0-4e29-a2e5-6a0be42892ea.sql diff --git a/src/integrations/supabase/types.ts b/src/integrations/supabase/types.ts index 1638011..43a0065 100644 --- a/src/integrations/supabase/types.ts +++ b/src/integrations/supabase/types.ts @@ -690,6 +690,47 @@ export type Database = { }, ] } + comments: { + Row: { + author_id: string + body: string + case_id: string | null + created_at: string + edited_at: string | null + entity_id: string + entity_type: Database["public"]["Enums"]["comment_entity"] + id: string + } + Insert: { + author_id: string + body?: string + case_id?: string | null + created_at?: string + edited_at?: string | null + entity_id: string + entity_type: Database["public"]["Enums"]["comment_entity"] + id?: string + } + Update: { + author_id?: string + body?: string + case_id?: string | null + created_at?: string + edited_at?: string | null + entity_id?: string + entity_type?: Database["public"]["Enums"]["comment_entity"] + id?: string + } + Relationships: [ + { + foreignKeyName: "comments_case_id_fkey" + columns: ["case_id"] + isOneToOne: false + referencedRelation: "cases" + referencedColumns: ["id"] + }, + ] + } contacts: { Row: { address_line1: string | null @@ -1517,6 +1558,73 @@ export type Database = { }, ] } + notifications: { + Row: { + body: string | null + case_id: string | null + conversation_id: string | null + created_at: string + created_by: string | null + id: string + kind: Database["public"]["Enums"]["notification_kind"] + link: string | null + read_at: string | null + task_id: string | null + title: string + user_id: string + } + Insert: { + body?: string | null + case_id?: string | null + conversation_id?: string | null + created_at?: string + created_by?: string | null + id?: string + kind: Database["public"]["Enums"]["notification_kind"] + link?: string | null + read_at?: string | null + task_id?: string | null + title: string + user_id: string + } + Update: { + body?: string | null + case_id?: string | null + conversation_id?: string | null + created_at?: string + created_by?: string | null + id?: string + kind?: Database["public"]["Enums"]["notification_kind"] + link?: string | null + read_at?: string | null + task_id?: string | null + title?: string + user_id?: string + } + Relationships: [ + { + foreignKeyName: "notifications_case_id_fkey" + columns: ["case_id"] + isOneToOne: false + referencedRelation: "cases" + referencedColumns: ["id"] + }, + { + foreignKeyName: "notifications_conversation_id_fkey" + columns: ["conversation_id"] + isOneToOne: false + referencedRelation: "conversations" + referencedColumns: ["id"] + }, + { + foreignKeyName: "notifications_task_id_fkey" + columns: ["task_id"] + isOneToOne: false + referencedRelation: "tasks" + referencedColumns: ["id"] + }, + ] + } payment_plan_installments: { Row: { amount: number @@ -1701,6 +1809,184 @@ export type Database = { }, ] } + task_assignees: { + Row: { + assigned_by: string | null + created_at: string + id: string + task_id: string + user_id: string + } + Insert: { + assigned_by?: string | null + created_at?: string + id?: string + task_id: string + user_id: string + } + Update: { + assigned_by?: string | null + created_at?: string + id?: string + task_id?: string + user_id?: string + } + Relationships: [ + { + foreignKeyName: "task_assignees_task_id_fkey" + columns: ["task_id"] + isOneToOne: false + referencedRelation: "tasks" + referencedColumns: ["id"] + }, + ] + } + task_comments: { + Row: { + author_id: string + body: string + created_at: string + edited_at: string | null + id: string + task_id: string + } + Insert: { + author_id: string + body?: string + created_at?: string + edited_at?: string | null + id?: string + task_id: string + } + Update: { + author_id?: string + body?: string + created_at?: string + edited_at?: string | null + id?: string + task_id?: string + } + Relationships: [ + { + foreignKeyName: "task_comments_task_id_fkey" + columns: ["task_id"] + isOneToOne: false + referencedRelation: "tasks" + referencedColumns: ["id"] + }, + ] + } + task_history: { + Row: { + actor_id: string | null + created_at: string + detail: Json + event: string + id: string + task_id: string + } + Insert: { + actor_id?: string | null + created_at?: string + detail?: Json + event: string + id?: string + task_id: string + } + Update: { + actor_id?: string | null + created_at?: string + detail?: Json + event?: string + id?: string + task_id?: string + } + Relationships: [ + { + foreignKeyName: "task_history_task_id_fkey" + columns: ["task_id"] + isOneToOne: false + referencedRelation: "tasks" + referencedColumns: ["id"] + }, + ] + } + tasks: { + Row: { + case_id: string | null + completed_at: string | null + completed_by: string | null + created_at: string + created_by: string | null + description: string | null + due_date: string | null + id: string + parent_task_id: string | null + priority: Database["public"]["Enums"]["task_priority"] + sort_order: number + status: Database["public"]["Enums"]["task_status"] + title: string + updated_at: string + workflow_template_id: string | null + } + Insert: { + case_id?: string | null + completed_at?: string | null + completed_by?: string | null + created_at?: string + created_by?: string | null + description?: string | null + due_date?: string | null + id?: string + parent_task_id?: string | null + priority?: Database["public"]["Enums"]["task_priority"] + sort_order?: number + status?: Database["public"]["Enums"]["task_status"] + title: string + updated_at?: string + workflow_template_id?: string | null + } + Update: { + case_id?: string | null + completed_at?: string | null + completed_by?: string | null + created_at?: string + created_by?: string | null + description?: string | null + due_date?: string | null + id?: string + parent_task_id?: string | null + priority?: Database["public"]["Enums"]["task_priority"] + sort_order?: number + status?: Database["public"]["Enums"]["task_status"] + title?: string + updated_at?: string + workflow_template_id?: string | null + } + Relationships: [ + { + foreignKeyName: "tasks_case_id_fkey" + columns: ["case_id"] + isOneToOne: false + referencedRelation: "cases" + referencedColumns: ["id"] + }, + { + foreignKeyName: "tasks_parent_task_id_fkey" + columns: ["parent_task_id"] + isOneToOne: false + referencedRelation: "tasks" + referencedColumns: ["id"] + }, + { + foreignKeyName: "tasks_workflow_template_id_fkey" + columns: ["workflow_template_id"] + isOneToOne: false + referencedRelation: "workflow_templates" + referencedColumns: ["id"] + }, + ] + } time_entries: { Row: { billable: boolean @@ -1779,6 +2065,80 @@ export type Database = { } Relationships: [] } + workflow_template_tasks: { + Row: { + created_at: string + days_from_start: number + default_assignee_id: string | null + description: string | null + id: string + priority: Database["public"]["Enums"]["task_priority"] + sort_order: number + template_id: string + title: string + updated_at: string + } + Insert: { + created_at?: string + days_from_start?: number + default_assignee_id?: string | null + description?: string | null + id?: string + priority?: Database["public"]["Enums"]["task_priority"] + sort_order?: number + template_id: string + title: string + updated_at?: string + } + Update: { + created_at?: string + days_from_start?: number + default_assignee_id?: string | null + description?: string | null + id?: string + priority?: Database["public"]["Enums"]["task_priority"] + sort_order?: number + template_id?: string + title?: string + updated_at?: string + } + Relationships: [ + { + foreignKeyName: "workflow_template_tasks_template_id_fkey" + columns: ["template_id"] + isOneToOne: false + referencedRelation: "workflow_templates" + referencedColumns: ["id"] + }, + ] + } + workflow_templates: { + Row: { + created_at: string + created_by: string | null + description: string | null + id: string + name: string + updated_at: string + } + Insert: { + created_at?: string + created_by?: string | null + description?: string | null + id?: string + name: string + updated_at?: string + } + Update: { + created_at?: string + created_by?: string | null + description?: string | null + id?: string + name?: string + updated_at?: string + } + Relationships: [] + } } Views: { [_ in never]: never @@ -1788,6 +2148,10 @@ export type Database = { Args: { _case_id: string; _user_id: string } Returns: boolean } + can_access_task: { + Args: { _task_id: string; _user_id: string } + Returns: boolean + } has_role: { Args: { _role: Database["public"]["Enums"]["app_role"] @@ -1811,7 +2175,20 @@ export type Database = { | "closed_lost" | "closed" client_type: "hoa" | "individual" | "business" | "condo" + comment_entity: "case" | "document" invoice_status: "draft" | "sent" | "paid" | "overdue" | "void" + notification_kind: + | "task_assigned" + | "task_mention" + | "task_comment" + | "task_due" + | "task_completed" + | "case_mention" + | "document_mention" + | "message_mention" + | "message" + task_priority: "low" | "normal" | "high" | "urgent" + task_status: "incomplete" | "complete" } CompositeTypes: { [_ in never]: never @@ -1949,7 +2326,21 @@ export const Constants = { "closed", ], client_type: ["hoa", "individual", "business", "condo"], + comment_entity: ["case", "document"], invoice_status: ["draft", "sent", "paid", "overdue", "void"], + notification_kind: [ + "task_assigned", + "task_mention", + "task_comment", + "task_due", + "task_completed", + "case_mention", + "document_mention", + "message_mention", + "message", + ], + task_priority: ["low", "normal", "high", "urgent"], + task_status: ["incomplete", "complete"], }, }, } as const diff --git a/supabase/migrations/20260417025153_cddbb003-f4b0-4e29-a2e5-6a0be42892ea.sql b/supabase/migrations/20260417025153_cddbb003-f4b0-4e29-a2e5-6a0be42892ea.sql new file mode 100644 index 0000000..f164f7e --- /dev/null +++ b/supabase/migrations/20260417025153_cddbb003-f4b0-4e29-a2e5-6a0be42892ea.sql @@ -0,0 +1,239 @@ +-- Enums +CREATE TYPE public.task_priority AS ENUM ('low', 'normal', 'high', 'urgent'); +CREATE TYPE public.task_status AS ENUM ('incomplete', 'complete'); +CREATE TYPE public.comment_entity AS ENUM ('case', 'document'); +CREATE TYPE public.notification_kind AS ENUM ('task_assigned', 'task_mention', 'task_comment', 'task_due', 'task_completed', 'case_mention', 'document_mention', 'message_mention', 'message'); + +-- Workflow templates +CREATE TABLE public.workflow_templates ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + name text NOT NULL, + description text, + created_by uuid, + created_at timestamptz NOT NULL DEFAULT now(), + updated_at timestamptz NOT NULL DEFAULT now() +); + +CREATE TABLE public.workflow_template_tasks ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + template_id uuid NOT NULL REFERENCES public.workflow_templates(id) ON DELETE CASCADE, + title text NOT NULL, + description text, + priority public.task_priority NOT NULL DEFAULT 'normal', + days_from_start integer NOT NULL DEFAULT 7, + default_assignee_id uuid, + sort_order integer NOT NULL DEFAULT 0, + created_at timestamptz NOT NULL DEFAULT now(), + updated_at timestamptz NOT NULL DEFAULT now() +); + +-- Tasks +CREATE TABLE public.tasks ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + title text NOT NULL, + description text, + status public.task_status NOT NULL DEFAULT 'incomplete', + priority public.task_priority NOT NULL DEFAULT 'normal', + due_date date, + case_id uuid REFERENCES public.cases(id) ON DELETE CASCADE, + parent_task_id uuid REFERENCES public.tasks(id) ON DELETE CASCADE, + workflow_template_id uuid REFERENCES public.workflow_templates(id) ON DELETE SET NULL, + created_by uuid, + completed_by uuid, + completed_at timestamptz, + sort_order integer NOT NULL DEFAULT 0, + created_at timestamptz NOT NULL DEFAULT now(), + updated_at timestamptz NOT NULL DEFAULT now() +); + +CREATE INDEX idx_tasks_case_id ON public.tasks(case_id); +CREATE INDEX idx_tasks_parent ON public.tasks(parent_task_id); +CREATE INDEX idx_tasks_due_date ON public.tasks(due_date); +CREATE INDEX idx_tasks_status ON public.tasks(status); + +-- Task assignees +CREATE TABLE public.task_assignees ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + task_id uuid NOT NULL REFERENCES public.tasks(id) ON DELETE CASCADE, + user_id uuid NOT NULL, + assigned_by uuid, + created_at timestamptz NOT NULL DEFAULT now(), + UNIQUE (task_id, user_id) +); +CREATE INDEX idx_task_assignees_user ON public.task_assignees(user_id); + +-- Task comments +CREATE TABLE public.task_comments ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + task_id uuid NOT NULL REFERENCES public.tasks(id) ON DELETE CASCADE, + author_id uuid NOT NULL, + body text NOT NULL DEFAULT '', + edited_at timestamptz, + created_at timestamptz NOT NULL DEFAULT now() +); +CREATE INDEX idx_task_comments_task ON public.task_comments(task_id); + +-- Task history (activity log) +CREATE TABLE public.task_history ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + task_id uuid NOT NULL REFERENCES public.tasks(id) ON DELETE CASCADE, + actor_id uuid, + event text NOT NULL, + detail jsonb NOT NULL DEFAULT '{}'::jsonb, + created_at timestamptz NOT NULL DEFAULT now() +); +CREATE INDEX idx_task_history_task ON public.task_history(task_id); + +-- Generic comments (cases / documents) +CREATE TABLE public.comments ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + entity_type public.comment_entity NOT NULL, + entity_id uuid NOT NULL, + case_id uuid REFERENCES public.cases(id) ON DELETE CASCADE, + author_id uuid NOT NULL, + body text NOT NULL DEFAULT '', + edited_at timestamptz, + created_at timestamptz NOT NULL DEFAULT now() +); +CREATE INDEX idx_comments_entity ON public.comments(entity_type, entity_id); +CREATE INDEX idx_comments_case ON public.comments(case_id); + +-- Notifications (global inbox) +CREATE TABLE public.notifications ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + user_id uuid NOT NULL, + kind public.notification_kind NOT NULL, + title text NOT NULL, + body text, + link text, + task_id uuid REFERENCES public.tasks(id) ON DELETE CASCADE, + case_id uuid REFERENCES public.cases(id) ON DELETE CASCADE, + conversation_id uuid REFERENCES public.conversations(id) ON DELETE CASCADE, + read_at timestamptz, + created_by uuid, + created_at timestamptz NOT NULL DEFAULT now() +); +CREATE INDEX idx_notifications_user_unread ON public.notifications(user_id, read_at); + +-- Triggers for updated_at +CREATE TRIGGER trg_tasks_updated_at BEFORE UPDATE ON public.tasks FOR EACH ROW EXECUTE FUNCTION public.tg_set_updated_at(); +CREATE TRIGGER trg_workflow_templates_updated_at BEFORE UPDATE ON public.workflow_templates FOR EACH ROW EXECUTE FUNCTION public.tg_set_updated_at(); +CREATE TRIGGER trg_workflow_template_tasks_updated_at BEFORE UPDATE ON public.workflow_template_tasks FOR EACH ROW EXECUTE FUNCTION public.tg_set_updated_at(); + +-- Helper: can_access_task +CREATE OR REPLACE FUNCTION public.can_access_task(_task_id uuid, _user_id uuid) +RETURNS boolean +LANGUAGE sql +STABLE SECURITY DEFINER +SET search_path = public +AS $$ + SELECT + public.is_admin(_user_id) + OR EXISTS ( + SELECT 1 FROM public.tasks t + WHERE t.id = _task_id + AND ( + t.created_by = _user_id + OR (t.case_id IS NOT NULL AND public.can_access_case(t.case_id, _user_id)) + OR EXISTS (SELECT 1 FROM public.task_assignees ta WHERE ta.task_id = t.id AND ta.user_id = _user_id) + ) + ) +$$; + +-- Enable RLS +ALTER TABLE public.tasks ENABLE ROW LEVEL SECURITY; +ALTER TABLE public.task_assignees ENABLE ROW LEVEL SECURITY; +ALTER TABLE public.task_comments ENABLE ROW LEVEL SECURITY; +ALTER TABLE public.task_history ENABLE ROW LEVEL SECURITY; +ALTER TABLE public.comments ENABLE ROW LEVEL SECURITY; +ALTER TABLE public.notifications ENABLE ROW LEVEL SECURITY; +ALTER TABLE public.workflow_templates ENABLE ROW LEVEL SECURITY; +ALTER TABLE public.workflow_template_tasks ENABLE ROW LEVEL SECURITY; + +-- Tasks policies +CREATE POLICY tasks_select ON public.tasks FOR SELECT TO authenticated +USING (public.can_access_task(id, auth.uid())); +CREATE POLICY tasks_insert ON public.tasks FOR INSERT TO authenticated +WITH CHECK ( + auth.uid() IS NOT NULL + AND (case_id IS NULL OR public.can_access_case(case_id, auth.uid())) +); +CREATE POLICY tasks_update ON public.tasks FOR UPDATE TO authenticated +USING (public.can_access_task(id, auth.uid())); +CREATE POLICY tasks_delete ON public.tasks FOR DELETE TO authenticated +USING (public.is_admin(auth.uid()) OR created_by = auth.uid()); + +-- Task assignees policies +CREATE POLICY task_assignees_select ON public.task_assignees FOR SELECT TO authenticated +USING (public.can_access_task(task_id, auth.uid())); +CREATE POLICY task_assignees_insert ON public.task_assignees FOR INSERT TO authenticated +WITH CHECK (public.can_access_task(task_id, auth.uid())); +CREATE POLICY task_assignees_delete ON public.task_assignees FOR DELETE TO authenticated +USING (public.can_access_task(task_id, auth.uid())); + +-- Task comments policies +CREATE POLICY task_comments_select ON public.task_comments FOR SELECT TO authenticated +USING (public.can_access_task(task_id, auth.uid())); +CREATE POLICY task_comments_insert ON public.task_comments FOR INSERT TO authenticated +WITH CHECK (author_id = auth.uid() AND public.can_access_task(task_id, auth.uid())); +CREATE POLICY task_comments_update ON public.task_comments FOR UPDATE TO authenticated +USING (author_id = auth.uid()); +CREATE POLICY task_comments_delete ON public.task_comments FOR DELETE TO authenticated +USING (author_id = auth.uid() OR public.is_admin(auth.uid())); + +-- Task history policies +CREATE POLICY task_history_select ON public.task_history FOR SELECT TO authenticated +USING (public.can_access_task(task_id, auth.uid())); +CREATE POLICY task_history_insert ON public.task_history FOR INSERT TO authenticated +WITH CHECK (public.can_access_task(task_id, auth.uid())); + +-- Generic comments policies +CREATE POLICY comments_select ON public.comments FOR SELECT TO authenticated +USING ( + public.is_admin(auth.uid()) + OR (case_id IS NOT NULL AND public.can_access_case(case_id, auth.uid())) + OR author_id = auth.uid() +); +CREATE POLICY comments_insert ON public.comments FOR INSERT TO authenticated +WITH CHECK ( + author_id = auth.uid() + AND (case_id IS NULL OR public.can_access_case(case_id, auth.uid())) +); +CREATE POLICY comments_update ON public.comments FOR UPDATE TO authenticated +USING (author_id = auth.uid()); +CREATE POLICY comments_delete ON public.comments FOR DELETE TO authenticated +USING (author_id = auth.uid() OR public.is_admin(auth.uid())); + +-- Notifications policies (private to user) +CREATE POLICY notifications_select ON public.notifications FOR SELECT TO authenticated +USING (user_id = auth.uid()); +CREATE POLICY notifications_insert ON public.notifications FOR INSERT TO authenticated +WITH CHECK (auth.uid() IS NOT NULL); +CREATE POLICY notifications_update ON public.notifications FOR UPDATE TO authenticated +USING (user_id = auth.uid()); +CREATE POLICY notifications_delete ON public.notifications FOR DELETE TO authenticated +USING (user_id = auth.uid()); + +-- Workflow templates policies +CREATE POLICY wft_select ON public.workflow_templates FOR SELECT TO authenticated USING (true); +CREATE POLICY wft_insert ON public.workflow_templates FOR INSERT TO authenticated WITH CHECK (auth.uid() IS NOT NULL); +CREATE POLICY wft_update ON public.workflow_templates FOR UPDATE TO authenticated +USING (created_by = auth.uid() OR public.is_admin(auth.uid())); +CREATE POLICY wft_delete ON public.workflow_templates FOR DELETE TO authenticated +USING (created_by = auth.uid() OR public.is_admin(auth.uid())); + +CREATE POLICY wftt_select ON public.workflow_template_tasks FOR SELECT TO authenticated USING (true); +CREATE POLICY wftt_insert ON public.workflow_template_tasks FOR INSERT TO authenticated +WITH CHECK (EXISTS (SELECT 1 FROM public.workflow_templates wt WHERE wt.id = template_id AND (wt.created_by = auth.uid() OR public.is_admin(auth.uid())))); +CREATE POLICY wftt_update ON public.workflow_template_tasks FOR UPDATE TO authenticated +USING (EXISTS (SELECT 1 FROM public.workflow_templates wt WHERE wt.id = template_id AND (wt.created_by = auth.uid() OR public.is_admin(auth.uid())))); +CREATE POLICY wftt_delete ON public.workflow_template_tasks FOR DELETE TO authenticated +USING (EXISTS (SELECT 1 FROM public.workflow_templates wt WHERE wt.id = template_id AND (wt.created_by = auth.uid() OR public.is_admin(auth.uid())))); + +-- Realtime +ALTER PUBLICATION supabase_realtime ADD TABLE public.tasks; +ALTER PUBLICATION supabase_realtime ADD TABLE public.task_comments; +ALTER PUBLICATION supabase_realtime ADD TABLE public.task_assignees; +ALTER PUBLICATION supabase_realtime ADD TABLE public.task_history; +ALTER PUBLICATION supabase_realtime ADD TABLE public.comments; +ALTER PUBLICATION supabase_realtime ADD TABLE public.notifications; \ No newline at end of file From ca5b7ea01a60d9937e18ada1329399314f083c63 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:54:58 +0000 Subject: [PATCH 2/4] Changes Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com> --- src/components/comments/comment-thread.tsx | 164 +++++ .../notifications/notification-bell.tsx | 160 +++++ src/components/tasks/new-task-dialog.tsx | 260 ++++++++ src/components/tasks/task-detail-panel.tsx | 608 ++++++++++++++++++ src/lib/notifications.ts | 36 ++ src/lib/use-bubble-counts.ts | 79 +++ src/routes/tasks.index.tsx | 352 ++++++++++ 7 files changed, 1659 insertions(+) create mode 100644 src/components/comments/comment-thread.tsx create mode 100644 src/components/notifications/notification-bell.tsx create mode 100644 src/components/tasks/new-task-dialog.tsx create mode 100644 src/components/tasks/task-detail-panel.tsx create mode 100644 src/lib/notifications.ts create mode 100644 src/lib/use-bubble-counts.ts create mode 100644 src/routes/tasks.index.tsx diff --git a/src/components/comments/comment-thread.tsx b/src/components/comments/comment-thread.tsx new file mode 100644 index 0000000..67c3a73 --- /dev/null +++ b/src/components/comments/comment-thread.tsx @@ -0,0 +1,164 @@ +import { useEffect, useState, useCallback } from "react"; +import { supabase } from "@/integrations/supabase/client"; +import { useAuth } from "@/lib/auth"; +import { Button } from "@/components/ui/button"; +import { Textarea } from "@/components/ui/textarea"; +import { Avatar, AvatarFallback } from "@/components/ui/avatar"; +import { toast } from "sonner"; +import { Loader2, Trash2 } from "lucide-react"; +import type { Database } from "@/integrations/supabase/types"; + +type EntityType = Database["public"]["Enums"]["comment_entity"]; + +interface Profile { + id: string; + full_name: string; + email: string; +} + +interface CommentRow { + id: string; + entity_type: EntityType; + entity_id: string; + case_id: string | null; + author_id: string; + body: string; + edited_at: string | null; + created_at: string; +} + +function initials(name: string) { + return name.split(" ").map((p) => p[0]).filter(Boolean).slice(0, 2).join("").toUpperCase(); +} + +export function CommentThread({ + entityType, + entityId, + caseId, +}: { + entityType: EntityType; + entityId: string; + caseId?: string | null; +}) { + const { user } = useAuth(); + const [comments, setComments] = useState([]); + const [profiles, setProfiles] = useState>({}); + const [body, setBody] = useState(""); + const [submitting, setSubmitting] = useState(false); + const [loading, setLoading] = useState(true); + + const load = useCallback(async () => { + const { data } = await supabase + .from("comments") + .select("*") + .eq("entity_type", entityType) + .eq("entity_id", entityId) + .order("created_at", { ascending: true }); + const rows = (data ?? []) as CommentRow[]; + setComments(rows); + const ids = Array.from(new Set(rows.map((r) => r.author_id))); + if (ids.length) { + const { data: pr } = await supabase + .from("profiles") + .select("id, full_name, email") + .in("id", ids); + const map: Record = {}; + (pr ?? []).forEach((p) => (map[p.id] = p as Profile)); + setProfiles(map); + } + setLoading(false); + }, [entityType, entityId]); + + useEffect(() => { + load(); + const ch = supabase + .channel(`comments-${entityType}-${entityId}`) + .on( + "postgres_changes", + { event: "*", schema: "public", table: "comments", filter: `entity_id=eq.${entityId}` }, + () => load(), + ) + .subscribe(); + return () => { + supabase.removeChannel(ch); + }; + }, [entityType, entityId, load]); + + const submit = async () => { + if (!body.trim() || !user) return; + setSubmitting(true); + const { error } = await supabase.from("comments").insert({ + entity_type: entityType, + entity_id: entityId, + case_id: caseId ?? null, + author_id: user.id, + body: body.trim(), + }); + setSubmitting(false); + if (error) { + toast.error(error.message); + return; + } + setBody(""); + }; + + const remove = async (id: string) => { + const { error } = await supabase.from("comments").delete().eq("id", id); + if (error) toast.error(error.message); + }; + + return ( +
+
+ {loading &&
Loading…
} + {!loading && comments.length === 0 && ( +
No comments yet.
+ )} + {comments.map((c) => { + const p = profiles[c.author_id]; + const name = p?.full_name || p?.email || "Unknown"; + return ( +
+ + {initials(name)} + +
+
+
{name}
+
+ {new Date(c.created_at).toLocaleString()} +
+ {c.author_id === user?.id && ( + + )} +
+
{c.body}
+
+
+ ); + })} +
+ +
+