import type { ImageAnswerMode, QuestionType } from "@/db/schema/form"; import { decodeCheckboxAnswer, encodeAnswerValue, formatStoredAnswer, type FormAnswerValue, } from "@/lib/form/form-answer-values"; import { stripHtml } from "@/lib/utils"; export type SubmissionQuestion = { id: string; type: QuestionType; label: string; required: boolean; options?: string[] | null; allowOther: boolean; imageAnswerMode?: ImageAnswerMode; }; export type SubmittedFormAnswer = { questionId: string; value: FormAnswerValue; imageCdnId?: string | null; }; export type NormalizedSubmissionAnswer = { questionId: string; value: string; imageCdnId: string | null; displayValue: string; }; export type StoredAnswerSnapshot = { value: string; imageCdnId: string | null; }; export type OldAnswersByQuestionId = Record< string, string | StoredAnswerSnapshot | undefined >; function questionTitle(question: Pick) { return stripHtml(question.label); } function ensureSubmittedQuestionIdsBelongToForm( questions: SubmissionQuestion[], answersList: SubmittedFormAnswer[], ) { const questionsById = new Map(questions.map((question) => [question.id, question])); const submittedQuestionIds = new Set(); for (const answer of answersList) { if (!questionsById.has(answer.questionId) || submittedQuestionIds.has(answer.questionId)) { throw new Error("ข้อมูลคำตอบไม่ตรงกับแบบฟอร์ม"); } submittedQuestionIds.add(answer.questionId); } return questionsById; } function normalizeCheckboxAnswer( question: SubmissionQuestion, rawValue: FormAnswerValue | undefined, imageCdnId: string | null, ): NormalizedSubmissionAnswer { if ( rawValue !== undefined && typeof rawValue !== "string" && (!Array.isArray(rawValue) || rawValue.some((value) => typeof value !== "string")) ) { throw new Error(`รูปแบบคำตอบของ “${questionTitle(question)}” ไม่ถูกต้อง`); } const selectedValues = [...new Set(decodeCheckboxAnswer(rawValue))]; if (selectedValues.some((value) => !value.trim() || value === "__OTHER__")) { throw new Error(`โปรดระบุคำตอบสำหรับ “${questionTitle(question)}”`); } if (question.required && selectedValues.length === 0) { throw new Error(`จำเป็นต้องตอบ “${questionTitle(question)}”`); } const standardOptions = question.options ?? []; const customValues = selectedValues.filter((value) => !standardOptions.includes(value)); if ((!question.allowOther && customValues.length > 0) || customValues.length > 1) { throw new Error(`ตัวเลือกของ “${questionTitle(question)}” ไม่ถูกต้อง`); } return { questionId: question.id, value: encodeAnswerValue(selectedValues), imageCdnId, displayValue: selectedValues.join(", "), }; } function normalizeScalarAnswer( question: SubmissionQuestion, rawValue: FormAnswerValue | undefined, imageCdnId: string | null, ): NormalizedSubmissionAnswer { if (rawValue !== undefined && typeof rawValue !== "string") { throw new Error(`รูปแบบคำตอบของ “${questionTitle(question)}” ไม่ถูกต้อง`); } const value = rawValue ?? ""; if (value === "__OTHER__") { throw new Error(`โปรดระบุคำตอบสำหรับ “${questionTitle(question)}”`); } if (question.required && !value.trim()) { throw new Error(`จำเป็นต้องตอบ “${questionTitle(question)}”`); } if ( question.type === "radio" && value && !(question.options ?? []).includes(value) && !question.allowOther ) { throw new Error(`ตัวเลือกของ “${questionTitle(question)}” ไม่ถูกต้อง`); } return { questionId: question.id, value, imageCdnId, displayValue: value, }; } function normalizeImageAnswer( question: SubmissionQuestion, rawImageCdnId: string | null | undefined, ) { const imageAnswerMode = question.imageAnswerMode ?? "none"; const imageCdnId = typeof rawImageCdnId === "string" && rawImageCdnId.trim() ? rawImageCdnId.trim() : null; if (imageAnswerMode === "none") { return null; } if (imageAnswerMode === "required" && !imageCdnId) { throw new Error(`จำเป็นต้องอัปโหลดรูปสำหรับ “${questionTitle(question)}”`); } return imageCdnId; } export function normalizeSubmissionAnswers( questions: SubmissionQuestion[], answersList: SubmittedFormAnswer[], ) { ensureSubmittedQuestionIdsBelongToForm(questions, answersList); const submittedAnswers = new Map( answersList.map((answer) => [answer.questionId, answer]), ); return questions.map((question) => { const submittedAnswer = submittedAnswers.get(question.id); const rawValue = submittedAnswer?.value; const imageCdnId = normalizeImageAnswer(question, submittedAnswer?.imageCdnId); return question.type === "checkbox" ? normalizeCheckboxAnswer(question, rawValue, imageCdnId) : normalizeScalarAnswer(question, rawValue, imageCdnId); }); } export function displayAnswerValue(answer: Pick) { return answer.displayValue || (answer.imageCdnId ? `/cdn/${answer.imageCdnId}` : ""); } export function answersByTemplateIndex( questions: Pick[], normalizedAnswers: NormalizedSubmissionAnswer[], ) { const questionOrderMap = questions.reduce>((acc, question, index) => { acc[question.id] = String(index + 1); return acc; }, {}); return normalizedAnswers.reduce>((acc, answer) => { const index = questionOrderMap[answer.questionId]; if (index) acc[index] = displayAnswerValue(answer); return acc; }, {}); } export function normalizeStoredAnswerSnapshot( snapshot: string | StoredAnswerSnapshot | undefined, ): StoredAnswerSnapshot { if (!snapshot) { return { value: "", imageCdnId: null }; } if (typeof snapshot === "string") { return { value: snapshot, imageCdnId: null }; } return { value: snapshot.value ?? "", imageCdnId: snapshot.imageCdnId ?? null, }; } export function submissionUpdateText( questions: SubmissionQuestion[], normalizedAnswers: NormalizedSubmissionAnswer[], oldAnswersByQuestionId: OldAnswersByQuestionId, ) { const questionsById = new Map(questions.map((question) => [question.id, question])); const changed: string[] = []; for (const answer of normalizedAnswers) { const question = questionsById.get(answer.questionId); const oldSnapshot = normalizeStoredAnswerSnapshot(oldAnswersByQuestionId[answer.questionId]); const oldDisplayValue = question ? formatStoredAnswer(oldSnapshot.value, question.type) : oldSnapshot.value; const oldValue = oldDisplayValue || (oldSnapshot.imageCdnId ? `/cdn/${oldSnapshot.imageCdnId}` : ""); const newValue = displayAnswerValue(answer); if (oldValue !== newValue) { const title = question ? questionTitle(question) : "Question"; changed.push( `**${title}**: \`${oldValue || "Empty"}\` ➡️ \`${newValue || "Empty"}\``, ); } } return changed.length > 0 ? changed.join("\n") : "No changes made."; }