diff --git a/src/components/messaging/NewTopicThreadDialog.tsx b/src/components/messaging/NewTopicThreadDialog.tsx new file mode 100644 index 0000000..310c11a --- /dev/null +++ b/src/components/messaging/NewTopicThreadDialog.tsx @@ -0,0 +1,183 @@ +import { useEffect, useState } from "react"; +import { useAuth } from "@/contexts/AuthContext"; +import { supabase } from "@/integrations/supabase/client"; +import { useToast } from "@/hooks/use-toast"; +import { MESSAGE_TOPICS } from "@/lib/messageTopics"; +import { + Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger, DialogFooter, +} from "@/components/ui/dialog"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Textarea } from "@/components/ui/textarea"; +import { Checkbox } from "@/components/ui/checkbox"; +import { Label } from "@/components/ui/label"; +import { Plus, Loader2 } from "lucide-react"; + +type Recipient = { user_id: string; name: string; category: "Management" | "Board"; context?: string }; + +export default function NewTopicThreadDialog({ onCreated }: { onCreated: (threadId: string) => void }) { + const { user, isStaff } = useAuth(); + const { toast } = useToast(); + const [open, setOpen] = useState(false); + const [recipients, setRecipients] = useState([]); + const [loadingRecipients, setLoadingRecipients] = useState(false); + const [topic, setTopic] = useState(""); + const [subject, setSubject] = useState(""); + const [body, setBody] = useState(""); + const [selected, setSelected] = useState>(new Set()); + const [submitting, setSubmitting] = useState(false); + + useEffect(() => { + if (!open || !user) return; + setLoadingRecipients(true); + (async () => { + // Management = admins/managers. + const { data: roles } = await supabase + .from("user_roles").select("user_id, role").in("role", ["admin", "manager"]); + const mgmtIds = new Set((roles ?? []).map((r: any) => r.user_id).filter((id: string) => id && id !== user.id)); + + // Board peers = board members of the caller's own association(s); staff see all. + let boardQuery = (supabase as any) + .from("board_members") + .select("user_id, role, association_id, associations(name)") + .not("user_id", "is", null); + if (!isStaff) { + const { data: mine } = await (supabase as any) + .from("board_members").select("association_id").eq("user_id", user.id); + const assoc = Array.from(new Set((mine ?? []).map((m: any) => m.association_id).filter(Boolean))); + if (assoc.length === 0) { setRecipients([]); setLoadingRecipients(false); return; } + boardQuery = boardQuery.in("association_id", assoc); + } + const { data: board } = await boardQuery; + + const list: Recipient[] = []; + const seen = new Set(); + for (const b of (board ?? []) as any[]) { + if (!b.user_id || b.user_id === user.id || seen.has(b.user_id)) continue; + seen.add(b.user_id); + list.push({ + user_id: b.user_id, name: "", category: "Board", + context: `${b.role || "Board Member"}${b.associations?.name ? ` · ${b.associations.name}` : ""}`, + }); + } + for (const id of mgmtIds) { + if (seen.has(id)) continue; + seen.add(id); + list.push({ user_id: id, name: "", category: "Management", context: "Management" }); + } + + // Hydrate names from profiles. + const ids = list.map((r) => r.user_id); + if (ids.length) { + const { data: profiles } = await supabase.from("profiles").select("user_id, full_name").in("user_id", ids); + const pmap = new Map((profiles ?? []).map((p: any) => [p.user_id, p.full_name])); + for (const r of list) r.name = pmap.get(r.user_id) || "Member"; + } + list.sort((a, b) => a.category.localeCompare(b.category) || a.name.localeCompare(b.name)); + setRecipients(list); + setLoadingRecipients(false); + })(); + }, [open, user, isStaff]); + + const reset = () => { setTopic(""); setSubject(""); setBody(""); setSelected(new Set()); }; + + const toggle = (id: string) => { + setSelected((prev) => { + const next = new Set(prev); + next.has(id) ? next.delete(id) : next.add(id); + return next; + }); + }; + + const submit = async () => { + if (!topic) return toast({ title: "Pick a topic", variant: "destructive" }); + if (!subject.trim()) return toast({ title: "Add a subject", variant: "destructive" }); + if (selected.size === 0) return toast({ title: "Select at least one recipient", variant: "destructive" }); + if (!body.trim()) return toast({ title: "Write a message", variant: "destructive" }); + setSubmitting(true); + try { + const recipientIds = Array.from(selected); + const { data: threadId, error } = await (supabase as any).rpc("create_topic_thread", { + _topic: topic, _subject: subject.trim(), _body: body.trim(), _recipient_ids: recipientIds, + }); + if (error) throw error; + + // Notify recipients (mirrors notifyMessageRecipients in useDirectMessages). + const senderName = (await supabase.from("profiles").select("full_name").eq("user_id", user!.id).maybeSingle()) + .data?.full_name || "A board member"; + await Promise.allSettled(recipientIds.map((rid) => + (supabase as any).rpc("insert_notification", { + p_user_id: rid, p_type: "info", + p_title: `New topic from ${senderName}: ${topic}`, + p_message: subject.trim(), + p_related_item_type: "message_thread", + p_link: `/dashboard/messages?thread=${threadId}`, + }) + )); + + toast({ title: "Topic started" }); + reset(); + setOpen(false); + onCreated(threadId as string); + } catch (e: any) { + toast({ title: "Could not start topic", description: e.message, variant: "destructive" }); + } finally { + setSubmitting(false); + } + }; + + return ( + { setOpen(o); if (!o) reset(); }}> + + + + + New Topic +
+
+ + +
+
+ + setSubject(e.target.value)} placeholder="Short summary" maxLength={150} /> +
+
+ +
+ {loadingRecipients ? ( +
+ ) : recipients.length === 0 ? ( +

No contacts available.

+ ) : recipients.map((r) => ( + + ))} +
+
+
+ +