"use client"; import React, { useState, useEffect, useMemo } from "react"; import { useRouter } from "next/navigation"; import { Card, CardContent, CardDescription, CardHeader, CardTitle, CardFooter } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Textarea } from "@/components/ui/textarea"; import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; import { Checkbox } from "@/components/ui/checkbox"; import { Label } from "@/components/ui/label"; import { cn, stripHtml } from "@/lib/utils"; import { HtmlDisplay } from "@/components/html-display"; import { FormattedText } from "@/components/formatted-text"; import { Dialog, DialogContent, DialogTrigger, DialogTitle } from "@/components/ui/dialog"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger } from "@/components/ui/alert-dialog"; import { toast } from "sonner"; import { submitForm, deleteOwnSubmission } from "@/app/actions/submissions"; import { Loader2, LogOut, ChevronRight, ChevronLeft } from "lucide-react"; import { signOut } from "next-auth/react"; import Image from "next/image"; import { z } from "zod"; import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage, } from "@/components/ui/form"; export function FormLayoutClient({ userName, userImage, }: { userName: string; userImage: string; }) { return (
{userImage && ( {userName} )} {userName}
); } interface Question { id: string; formId: string; type: "text" | "textarea" | "radio" | "checkbox"; label: string; imageUrl: string | null; displayOrder: number; required: boolean; options: string[] | null; allowOther: boolean; } interface FormFillerClientProps { form: { id: string; title: string; description: string | null; isOpen: boolean; }; questions: Question[]; initialAnswers?: Record; } const LS_KEY = (formId: string) => `form-draft-${formId}`; const QUESTIONS_PER_STEP = 3; const buildSchema = (questions: Question[]) => { const shape: Record = {}; questions.forEach(q => { const plainLabel = stripHtml(q.label); if (q.type === "checkbox") { shape[q.id] = q.required ? z.array(z.string()).min(1, `จำเป็นต้องตอบ "${plainLabel}"`) : z.array(z.string()).optional(); } else { shape[q.id] = q.required ? z.string().min(1, `จำเป็นต้องตอบ "${plainLabel}"`) : z.string().optional(); } }); return z.object(shape); }; export function FormFillerClient({ form, questions, initialAnswers = {} }: FormFillerClientProps) { const router = useRouter(); const isUpdating = Object.keys(initialAnswers).length > 0; const formSchema = useMemo(() => buildSchema(questions), [questions]); const { defaultValues, initialOtherValues } = useMemo(() => { const serverAnswers = { ...initialAnswers }; let draft: Record = {}; try { const raw = localStorage.getItem(LS_KEY(form.id)); if (raw) draft = JSON.parse(raw); } catch {} const merged = { ...draft, ...serverAnswers }; const vals: Record = {}; const others: Record = {}; questions.forEach(q => { const ans = merged[q.id]; if (q.type === "checkbox") { let arr = ans ? (Array.isArray(ans) ? ans : ans.split(", ").filter(Boolean)) : []; if (q.allowOther && q.options) { const standardOptions = q.options; const customAnswers = arr.filter((x: string) => !standardOptions.includes(x) && x !== "__OTHER__"); if (customAnswers.length > 0) { others[q.id] = customAnswers.join(", "); } // If the draft had __OTHER__ or if there's a custom answer from server, ensure __OTHER__ is selected if (arr.includes("__OTHER__") || customAnswers.length > 0) { arr = arr.filter((x: string) => standardOptions.includes(x)); arr.push("__OTHER__"); } } vals[q.id] = arr; } else if (q.type === "radio") { let val = ans || ""; if (q.allowOther && q.options && val !== "" && !q.options.includes(val)) { if (val !== "__OTHER__") { others[q.id] = val; } val = "__OTHER__"; } vals[q.id] = val; } else { vals[q.id] = ans || ""; } }); return { defaultValues: vals, initialOtherValues: others }; }, [form.id, initialAnswers, questions]); const [otherValues, setOtherValues] = useState>(initialOtherValues); const methods = useForm>({ resolver: zodResolver(formSchema), defaultValues, mode: "onTouched", }); const { handleSubmit, control, watch, formState: { isSubmitting }, trigger, reset } = methods; useEffect(() => { const subscription = watch((value) => { try { localStorage.setItem(LS_KEY(form.id), JSON.stringify(value)); } catch {} }); return () => subscription.unsubscribe(); }, [watch, form.id]); const [currentStep, setCurrentStep] = useState(0); const totalSteps = Math.ceil(questions.length / QUESTIONS_PER_STEP); const currentQuestions = questions.slice( currentStep * QUESTIONS_PER_STEP, (currentStep + 1) * QUESTIONS_PER_STEP ); const nextStep = async () => { const fieldsToValidate = currentQuestions.map(q => q.id); const isStepValid = await trigger(fieldsToValidate); if (isStepValid) { setCurrentStep(prev => Math.min(prev + 1, totalSteps - 1)); window.scrollTo(0, 0); } }; const prevStep = () => { setCurrentStep(prev => Math.max(prev - 1, 0)); window.scrollTo(0, 0); }; const onSubmit = async (values: z.infer) => { if (!form.isOpen) return; try { const formattedAnswers = questions.map((q) => { let val = values[q.id] as string | string[] | undefined; if (q.allowOther) { const customText = otherValues[q.id] || ""; if (Array.isArray(val)) { val = val.map(v => v === "__OTHER__" ? customText : v); } else if (val === "__OTHER__") { val = customText; } } const finalString = Array.isArray(val) ? val.join(", ") : (val || ""); if (q.required && !finalString.trim()) { throw new Error(`จำเป็นต้องตอบ "${stripHtml(q.label)}" (สำหรับตัวเลือก 'อื่นๆ')`); } return { questionId: q.id, value: finalString, }; }); const res = await submitForm(form.id, formattedAnswers); if (res.ok) { toast.success(isUpdating ? "อัปเดตฟอร์มสำเร็จ!" : "ส่งฟอร์มสำเร็จ!"); } } catch (err: any) { console.error(err); toast.error(err.message || "ไม่สามารถส่งฟอร์มได้"); } }; const handleDelete = async () => { try { const res = await deleteOwnSubmission(form.id); if (res.ok) { toast.success("ลบข้อมูลสำเร็จ!"); localStorage.removeItem(LS_KEY(form.id)); reset({}); router.refresh(); } } catch (err: any) { console.error(err); toast.error(err.message || "ไม่สามารถลบข้อมูลได้"); } }; return (
{!form.isOpen && (
ฟอร์มนี้ถูกปิดใช้งานชั่วคราว
)} {form.title} {form.description && ( {form.description} )}
{currentQuestions.map((q) => (
{q.imageUrl && (
{q.label}
Enlarged image for {q.label}
{q.label}
)} {q.required && *}
( {q.type === "text" && ( )} {q.type === "textarea" && (