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}
+
+
+ ); + })} +
+ +
+