"use client"; import { useRef, useState } from "react"; import { useFocusTrap } from "~/hooks/useFocusTrap.js"; import { trpc } from "~/lib/trpc/client.js"; import { DateInput } from "~/components/ui/DateInput.js"; type Mode = "single" | "group"; const TARGET_TYPES = [ { value: "all", label: "All Users" }, { value: "role", label: "By Role" }, { value: "project", label: "By Project" }, { value: "orgUnit", label: "By Org Unit" }, ] as const; const ROLES = ["ADMIN", "MANAGER", "CONTROLLER", "USER", "VIEWER"] as const; const PRIORITY_OPTIONS = [ { value: "LOW", label: "Low" }, { value: "NORMAL", label: "Normal" }, { value: "HIGH", label: "High" }, { value: "URGENT", label: "Urgent" }, ] as const; const CHANNEL_OPTIONS = [ { value: "in_app", label: "In-App" }, { value: "email", label: "Email" }, { value: "both", label: "Both" }, ] as const; interface CreateTaskModalProps { onClose: () => void; onSuccess: () => void; } export function CreateTaskModal({ onClose, onSuccess }: CreateTaskModalProps) { const [mode, setMode] = useState("single"); const [userId, setUserId] = useState(""); const [userSearch, setUserSearch] = useState(""); const [title, setTitle] = useState(""); const [body, setBody] = useState(""); const [priority, setPriority] = useState("NORMAL"); const [dueDate, setDueDate] = useState(""); const [dueTime, setDueTime] = useState("09:00"); const [channel, setChannel] = useState("in_app"); const [link, setLink] = useState(""); const [targetType, setTargetType] = useState("all"); const [targetValue, setTargetValue] = useState(""); const [serverError, setServerError] = useState(null); const [result, setResult] = useState<{ recipientCount?: number; taskId?: string } | null>(null); const panelRef = useRef(null); useFocusTrap(panelRef, true); const utils = trpc.useUtils(); const { data: users = [] } = trpc.user.listAssignable.useQuery(undefined, { staleTime: 60_000, }); const filteredUsers = userSearch.trim() ? users.filter( (u) => (u.name ?? "").toLowerCase().includes(userSearch.toLowerCase()) || u.email.toLowerCase().includes(userSearch.toLowerCase()), ) : users; const createTaskMutation = trpc.notification.createTask.useMutation({ onSuccess: async (data) => { await utils.notification.listTasks.invalidate(); await utils.notification.list.invalidate(); await utils.notification.taskCounts.invalidate(); await utils.notification.unreadCount.invalidate(); const id = (data as { id?: string }).id; setResult(id !== undefined ? { taskId: id } : {}); }, onError: (err) => setServerError(err.message), }); const createBroadcastMutation = trpc.notification.createBroadcast.useMutation({ onSuccess: async (data) => { await utils.notification.listBroadcasts.invalidate(); await utils.notification.list.invalidate(); await utils.notification.taskCounts.invalidate(); await utils.notification.unreadCount.invalidate(); const count = (data as { recipientCount?: number }).recipientCount ?? 0; setResult({ recipientCount: count }); }, onError: (err) => setServerError(err.message), }); const isPending = createTaskMutation.isPending || createBroadcastMutation.isPending; function buildDueDate(): Date | undefined { if (!dueDate) return undefined; const [hours, minutes] = dueTime.split(":").map(Number); const d = new Date(dueDate + "T00:00:00"); d.setHours(hours ?? 9, minutes ?? 0, 0, 0); return d; } function handleSubmit(e: React.FormEvent) { e.preventDefault(); setServerError(null); if (!title.trim()) { setServerError("Title is required."); return; } if (mode === "single") { if (!userId) { setServerError("Please select a recipient."); return; } const due = buildDueDate(); createTaskMutation.mutate({ userId, title: title.trim(), ...(body.trim() ? { body: body.trim() } : {}), priority: priority as "LOW" | "NORMAL" | "HIGH" | "URGENT", ...(due !== undefined ? { dueDate: due } : {}), channel: channel as "in_app" | "email" | "both", ...(link.trim() ? { link: link.trim() } : {}), }); } else { createBroadcastMutation.mutate({ title: title.trim(), ...(body.trim() ? { body: body.trim() } : {}), targetType: targetType as "all" | "role" | "project" | "orgUnit", ...(targetType !== "all" && targetValue.trim() ? { targetValue: targetValue.trim() } : {}), priority: priority as "LOW" | "NORMAL" | "HIGH" | "URGENT", channel: channel as "in_app" | "email" | "both", ...(link.trim() ? { link: link.trim() } : {}), category: "TASK", }); } } function handleCloseResult() { onSuccess(); onClose(); } const inputClass = "w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-brand-500 text-sm dark:bg-gray-900 dark:text-gray-100"; const labelClass = "block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1"; // After successful send, show result if (result) { const isGroup = mode === "group"; return (
{ if (e.target === e.currentTarget) handleCloseResult(); }} >
{ if (e.key === "Escape") handleCloseResult(); }} >

Task Created

{isGroup ? `Task sent to ${result.recipientCount ?? 0} recipient${(result.recipientCount ?? 0) !== 1 ? "s" : ""}` : "Task has been assigned successfully"}

); } return (
{ if (e.target === e.currentTarget) onClose(); }} >
{ if (e.key === "Escape") onClose(); }} > {/* Header */}

Create Task

{/* Mode toggle */}
{/* Recipient (single mode) */} {mode === "single" && (
setUserSearch(e.target.value)} className={inputClass} placeholder="Search by name or email..." /> {userId && (
Selected: {users.find((u) => u.id === userId)?.name ?? users.find((u) => u.id === userId)?.email ?? userId}
)} {userSearch.trim() && filteredUsers.length > 0 && (
{filteredUsers.slice(0, 20).map((u) => ( ))}
)} {userSearch.trim() && filteredUsers.length === 0 && (

No users found.

)}
)} {/* Target (group mode) */} {mode === "group" && ( <>
{targetType === "role" && (
)} {targetType === "project" && (
setTargetValue(e.target.value)} className={inputClass} placeholder="Project ID..." />
)} {targetType === "orgUnit" && (
setTargetValue(e.target.value)} className={inputClass} placeholder="Org Unit ID..." />
)} )} {/* Title */}
setTitle(e.target.value)} maxLength={200} className={inputClass} required placeholder="Task title..." />
{/* Body */}