feat: schedule automatic form closure
This commit is contained in:
+51
-16
@@ -106,6 +106,7 @@ interface FormFillerClientProps {
|
||||
title: string;
|
||||
description: string | null;
|
||||
isOpen: boolean;
|
||||
closesAt: string | null;
|
||||
allowMultipleSubmissions: boolean;
|
||||
};
|
||||
questions: Question[];
|
||||
@@ -276,6 +277,40 @@ export function FormFillerClient({
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
const [totalAnswers, setTotalAnswers] = useState(initialTotalAnswers);
|
||||
const [queueNumber, setQueueNumber] = useState(initialQueueNumber);
|
||||
const closesAtTime = form.closesAt
|
||||
? new Date(form.closesAt).getTime()
|
||||
: null;
|
||||
const [reachedDeadline, setReachedDeadline] = useState<number | null>(null);
|
||||
const deadlineReached =
|
||||
closesAtTime !== null && reachedDeadline === closesAtTime;
|
||||
const isAcceptingSubmissions =
|
||||
preview || (form.isOpen && !deadlineReached);
|
||||
|
||||
useEffect(() => {
|
||||
if (closesAtTime === null) return;
|
||||
|
||||
let timeoutId: number | undefined;
|
||||
const checkDeadline = () => {
|
||||
const remaining = closesAtTime - Date.now();
|
||||
if (remaining <= 0) {
|
||||
setReachedDeadline(closesAtTime);
|
||||
return;
|
||||
}
|
||||
timeoutId = window.setTimeout(
|
||||
checkDeadline,
|
||||
Math.min(remaining, 2_147_483_647)
|
||||
);
|
||||
};
|
||||
|
||||
const remaining = closesAtTime - Date.now();
|
||||
timeoutId = window.setTimeout(
|
||||
checkDeadline,
|
||||
Math.max(0, Math.min(remaining, 2_147_483_647))
|
||||
);
|
||||
return () => {
|
||||
if (timeoutId !== undefined) window.clearTimeout(timeoutId);
|
||||
};
|
||||
}, [closesAtTime]);
|
||||
|
||||
const methods = useForm<z.infer<typeof formSchema>>({
|
||||
resolver: zodResolver(formSchema),
|
||||
@@ -332,7 +367,7 @@ export function FormFillerClient({
|
||||
}, [form.id, isUpdating, preview, questions, reset]);
|
||||
|
||||
useEffect(() => {
|
||||
if (preview || !draftReady || !form.isOpen) {
|
||||
if (preview || !draftReady || !isAcceptingSubmissions) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -348,7 +383,7 @@ export function FormFillerClient({
|
||||
} catch {
|
||||
queueMicrotask(() => setDraftSaved(false));
|
||||
}
|
||||
}, [draftReady, form.id, form.isOpen, getValues, imageAnswers, otherValues, preview, watchedValues]);
|
||||
}, [draftReady, form.id, getValues, imageAnswers, isAcceptingSubmissions, otherValues, preview, watchedValues]);
|
||||
|
||||
const switchToSubmission = (submissionId: string | null) => {
|
||||
const submission = submissionId
|
||||
@@ -432,7 +467,7 @@ export function FormFillerClient({
|
||||
|
||||
const onSubmit = async (values: z.infer<typeof formSchema>) => {
|
||||
if (preview) return;
|
||||
if (!form.isOpen) return;
|
||||
if (!isAcceptingSubmissions) return;
|
||||
|
||||
for (const question of questions) {
|
||||
const value = values[question.id] as FormAnswerValue | undefined;
|
||||
@@ -519,7 +554,7 @@ export function FormFillerClient({
|
||||
}
|
||||
};
|
||||
|
||||
if (!form.isOpen && !isUpdating) {
|
||||
if (!isAcceptingSubmissions && !isUpdating) {
|
||||
return (
|
||||
<Card className="mx-auto max-w-md">
|
||||
<CardHeader>
|
||||
@@ -567,7 +602,7 @@ export function FormFillerClient({
|
||||
ช่องที่มีป้าย “จำเป็น” ต้องตอบให้ครบ
|
||||
</span>
|
||||
)}
|
||||
{!form.isOpen && (
|
||||
{!isAcceptingSubmissions && (
|
||||
<span className="text-sm text-muted-foreground">
|
||||
คุณยังดูหรือลบคำตอบเดิมได้ แต่บันทึกการแก้ไขไม่ได้
|
||||
</span>
|
||||
@@ -699,7 +734,7 @@ export function FormFillerClient({
|
||||
value={(field.value as string) || ""}
|
||||
aria-labelledby={questionLabelId}
|
||||
aria-required={q.required}
|
||||
disabled={!form.isOpen || isSubmitting}
|
||||
disabled={!isAcceptingSubmissions || isSubmitting}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage id={questionErrorId} />
|
||||
@@ -720,7 +755,7 @@ export function FormFillerClient({
|
||||
aria-labelledby={questionLabelId}
|
||||
aria-required={q.required}
|
||||
className="min-h-32 resize-y"
|
||||
disabled={!form.isOpen || isSubmitting}
|
||||
disabled={!isAcceptingSubmissions || isSubmitting}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage id={questionErrorId} />
|
||||
@@ -743,7 +778,7 @@ export function FormFillerClient({
|
||||
aria-labelledby={questionLabelId}
|
||||
aria-required={q.required}
|
||||
className="flex flex-col gap-3"
|
||||
disabled={!form.isOpen || isSubmitting}
|
||||
disabled={!isAcceptingSubmissions || isSubmitting}
|
||||
>
|
||||
{q.options.map((option, optionIndex) => (
|
||||
<Field
|
||||
@@ -797,7 +832,7 @@ export function FormFillerClient({
|
||||
}
|
||||
className="w-full max-w-xs sm:ml-6"
|
||||
placeholder="โปรดระบุ"
|
||||
disabled={!form.isOpen || isSubmitting}
|
||||
disabled={!isAcceptingSubmissions || isSubmitting}
|
||||
/>
|
||||
)}
|
||||
</FieldGroup>
|
||||
@@ -835,7 +870,7 @@ export function FormFillerClient({
|
||||
<Checkbox
|
||||
id={`${q.id}-${optionIndex}`}
|
||||
checked={isChecked}
|
||||
disabled={!form.isOpen || isSubmitting}
|
||||
disabled={!isAcceptingSubmissions || isSubmitting}
|
||||
onCheckedChange={(checked) => {
|
||||
const current = Array.isArray(field.value)
|
||||
? field.value
|
||||
@@ -867,7 +902,7 @@ export function FormFillerClient({
|
||||
Array.isArray(field.value) &&
|
||||
field.value.includes("__OTHER__")
|
||||
}
|
||||
disabled={!form.isOpen || isSubmitting}
|
||||
disabled={!isAcceptingSubmissions || isSubmitting}
|
||||
onCheckedChange={(checked) => {
|
||||
const current = Array.isArray(field.value)
|
||||
? field.value
|
||||
@@ -910,7 +945,7 @@ export function FormFillerClient({
|
||||
}
|
||||
className="w-full max-w-xs sm:ml-6"
|
||||
placeholder="โปรดระบุ"
|
||||
disabled={!form.isOpen || isSubmitting}
|
||||
disabled={!isAcceptingSubmissions || isSubmitting}
|
||||
/>
|
||||
)}
|
||||
</FieldGroup>
|
||||
@@ -939,7 +974,7 @@ export function FormFillerClient({
|
||||
}}
|
||||
type="file"
|
||||
accept="image/png,image/jpeg,image/webp,image/gif"
|
||||
disabled={!form.isOpen || isSubmitting || uploadingAnswerId !== null}
|
||||
disabled={!isAcceptingSubmissions || isSubmitting || uploadingAnswerId !== null}
|
||||
onChange={(event) => handleAnswerImageUpload(q.id, event)}
|
||||
className="max-w-sm"
|
||||
aria-invalid={Boolean(imageErrors[q.id])}
|
||||
@@ -983,7 +1018,7 @@ export function FormFillerClient({
|
||||
type="button"
|
||||
variant="ghost"
|
||||
onClick={() => clearAnswerImage(q.id)}
|
||||
disabled={!form.isOpen || isSubmitting}
|
||||
disabled={!isAcceptingSubmissions || isSubmitting}
|
||||
>
|
||||
ลบรูป
|
||||
</Button>
|
||||
@@ -1014,7 +1049,7 @@ export function FormFillerClient({
|
||||
<p className="text-sm text-muted-foreground" aria-live="polite">
|
||||
{preview
|
||||
? "ตัวอย่างสำหรับผู้ดูแลระบบ — ไม่มีการส่งคำตอบ"
|
||||
: !form.isOpen
|
||||
: !isAcceptingSubmissions
|
||||
? "ปิดรับคำตอบแล้ว"
|
||||
: draftSaved
|
||||
? "บันทึกฉบับร่างไว้ในอุปกรณ์นี้แล้ว"
|
||||
@@ -1061,7 +1096,7 @@ export function FormFillerClient({
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
)}
|
||||
{form.isOpen && (
|
||||
{isAcceptingSubmissions && (
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={isSubmitting || questions.length === 0}
|
||||
|
||||
Reference in New Issue
Block a user