/** * @file app/(app)/tasks/useTaskForm.ts * @description Custom hook managing state, validation, and submission logic for task creation and editing. */ import { useState, useTransition } from "react"; import { useRouter } from "next/navigation"; import { TaskPriority, TaskStatus, DbTask } from "@/lib/types/task"; import { taskSchema } from "@/lib/schemas/task.schema"; /** * Custom React hook that encapsulates form state management, field updates, assignee toggling, * validation rules, and network submission logic for both task creation and editing workflows. * * @param {DbTask & { assignees?: { id: string }[] }} [initialData] - Initial task data loaded for editing workflows. * @param {string} [mode] - The current operating mode (e.g., "edit"). * @param {TaskStatus} [defaultStatus] - The fallback status applied when creating new tasks. * @returns {Object} An object containing form state values, error states, and event handling functions. */ export function useTaskForm( initialData?: DbTask & { assignees?: { id: string }[] }, mode?: string, defaultStatus?: TaskStatus, ) { const router = useRouter(); const [isPending, startTransition] = useTransition(); const isEditMode = mode === "edit" && initialData; const todayString = new Date().toISOString().split("T")[0]; const dueDateObj = initialData?.dueDate ? new Date(initialData.dueDate) : null; const [form, setForm] = useState({ title: initialData?.title ?? "", description: initialData?.description ?? "", priority: (initialData?.priority ?? "medium") as TaskPriority, status: (initialData?.status ?? defaultStatus ?? "todo") as TaskStatus, assignees: initialData?.assignees?.map((a) => a.id) ?? [], dueDate: dueDateObj ? dueDateObj.toISOString().split("T")[0] : todayString, dueTime: dueDateObj ? dueDateObj.toTimeString().slice(0, 5) : "23:59", }); const [error, setError] = useState(null); /** * Updates a single property within the form state object. * * @param {string} field - The target field name to update. * @param {any} value - The new value for the field. */ const updateField = (field: string, value: any) => { setForm((prev) => ({ ...prev, [field]: value })); }; /** * Toggles the inclusion of a user ID within the assignees selection list. * * @param {string} userId - The unique identifier of the user to toggle. */ const handleAssigneeToggle = (userId: string) => { const assignees = form.assignees.includes(userId) ? form.assignees.filter((id) => id !== userId) : [...form.assignees, userId]; updateField("assignees", assignees); }; /** * Validates form input against Zod schemas and dispatches POST or PATCH network requests to persist task changes. * * @param {React.SubmitEvent} e - The form submission event. */ const handleSubmit = (e: React.SubmitEvent) => { e.preventDefault(); setError(null); const finalDate = form.dueDate || todayString; const combinedDateTime = new Date(`${finalDate}T${form.dueTime}`); if (!isEditMode && combinedDateTime <= new Date()) { setError("The due date and time must be in the future."); return; } const validationResult = taskSchema.safeParse({ ...form, dueDate: combinedDateTime.toISOString(), }); if (!validationResult.success) { setError(validationResult.error.issues[0].message); return; } startTransition(async () => { try { const response = await fetch("/api/tasks", { method: isEditMode ? "PATCH" : "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ ...validationResult.data, id: initialData?.id, }), }); if (!response.ok) { const errorText = await response.text(); throw new Error(errorText || "Failed to save task."); } router.push("/dashboard"); router.refresh(); } catch (err: any) { console.error("Error submitting task:", err); setError(err.message || "Something went wrong. Please try again."); } }); }; return { form, error, isEditMode, isSubmitting: isPending, todayString, updateField, handleAssigneeToggle, handleSubmit, }; }