152 lines
4.1 KiB
TypeScript
152 lines
4.1 KiB
TypeScript
import { z } from "zod";
|
|
import type { FormAnswerValue } from "@/lib/form/form-answer-values";
|
|
import { decodeCheckboxAnswer } from "@/lib/form/form-answer-values";
|
|
|
|
export type Question = {
|
|
id: string;
|
|
formId: string;
|
|
type: "text" | "textarea" | "radio" | "checkbox";
|
|
label: string;
|
|
imageUrl: string | null;
|
|
displayOrder: number;
|
|
required: boolean;
|
|
options: string[] | null;
|
|
allowOther: boolean;
|
|
imageAnswerMode: "none" | "optional" | "required";
|
|
};
|
|
|
|
export type AnswerDraft = {
|
|
value: FormAnswerValue;
|
|
imageCdnId: string | null;
|
|
};
|
|
|
|
export type FormFillerClientProps = {
|
|
form: {
|
|
id: string;
|
|
title: string;
|
|
description: string | null;
|
|
isOpen: boolean;
|
|
closesAt: string | null;
|
|
allowMultipleSubmissions: boolean;
|
|
};
|
|
questions: Question[];
|
|
initialAnswers?: Record<string, AnswerDraft>;
|
|
hasSubmission?: boolean;
|
|
selectedSubmissionId?: string | null;
|
|
ownSubmissions?: {
|
|
id: string;
|
|
submittedAt: string;
|
|
answers: Record<string, AnswerDraft>;
|
|
}[];
|
|
totalAnswers: number;
|
|
queueNumber: number | null;
|
|
preview?: boolean;
|
|
};
|
|
|
|
export type FormValues = Record<string, string | string[] | undefined>;
|
|
|
|
export function formatQueuePosition(queueNumber: number) {
|
|
return `#${queueNumber}`;
|
|
}
|
|
|
|
export type StoredDraft = {
|
|
version: 1;
|
|
answers: FormValues;
|
|
otherValues: Record<string, string>;
|
|
imageAnswers?: Record<string, string | null>;
|
|
};
|
|
|
|
export function buildFormSchema(questions: Question[]) {
|
|
const shape: Record<string, z.ZodTypeAny> = {};
|
|
questions.forEach((question) => {
|
|
if (question.type === "checkbox") {
|
|
shape[question.id] = question.required
|
|
? z.array(z.string()).min(1, "จำเป็นต้องตอบข้อนี้")
|
|
: z.array(z.string()).optional();
|
|
} else {
|
|
shape[question.id] = question.required
|
|
? z.string().trim().min(1, "จำเป็นต้องตอบข้อนี้")
|
|
: z.string().optional();
|
|
}
|
|
});
|
|
return z.object(shape);
|
|
}
|
|
|
|
export function prepareInitialState(
|
|
questions: Question[],
|
|
answers: Record<string, AnswerDraft | undefined>,
|
|
savedOtherValues: Record<string, string> = {}
|
|
) {
|
|
const values: FormValues = {};
|
|
const imageAnswers: Record<string, string | null> = {};
|
|
const otherValues = { ...savedOtherValues };
|
|
|
|
questions.forEach((question) => {
|
|
const answer = answers[question.id]?.value;
|
|
imageAnswers[question.id] = answers[question.id]?.imageCdnId ?? null;
|
|
|
|
if (question.type === "checkbox") {
|
|
let selected = decodeCheckboxAnswer(answer);
|
|
if (question.allowOther && question.options) {
|
|
const customAnswers = selected.filter(
|
|
(value) => !question.options?.includes(value) && value !== "__OTHER__"
|
|
);
|
|
if (customAnswers.length > 0) {
|
|
otherValues[question.id] = customAnswers.join(", ");
|
|
}
|
|
if (selected.includes("__OTHER__") || customAnswers.length > 0) {
|
|
selected = selected.filter((value) => question.options?.includes(value));
|
|
selected.push("__OTHER__");
|
|
}
|
|
}
|
|
values[question.id] = selected;
|
|
return;
|
|
}
|
|
|
|
if (question.type === "radio") {
|
|
let selected = typeof answer === "string" ? answer : "";
|
|
if (
|
|
question.allowOther &&
|
|
question.options &&
|
|
selected &&
|
|
!question.options.includes(selected)
|
|
) {
|
|
if (selected !== "__OTHER__") {
|
|
otherValues[question.id] = selected;
|
|
}
|
|
selected = "__OTHER__";
|
|
}
|
|
values[question.id] = selected;
|
|
return;
|
|
}
|
|
|
|
values[question.id] = typeof answer === "string" ? answer : "";
|
|
});
|
|
|
|
return { values, otherValues, imageAnswers };
|
|
}
|
|
|
|
export function parseStoredDraft(raw: string) {
|
|
const parsed: unknown = JSON.parse(raw);
|
|
if (
|
|
parsed &&
|
|
typeof parsed === "object" &&
|
|
"version" in parsed &&
|
|
parsed.version === 1 &&
|
|
"answers" in parsed
|
|
) {
|
|
const draft = parsed as StoredDraft;
|
|
return {
|
|
answers: draft.answers ?? {},
|
|
otherValues: draft.otherValues ?? {},
|
|
imageAnswers: draft.imageAnswers ?? {},
|
|
};
|
|
}
|
|
|
|
return {
|
|
answers: (parsed as FormValues) ?? {},
|
|
otherValues: {},
|
|
imageAnswers: {},
|
|
};
|
|
}
|