Files
erika/lib/submission-intake.ts
T

174 lines
5.5 KiB
TypeScript

import type { QuestionType } from "@/db/schema/form";
import {
decodeCheckboxAnswer,
encodeAnswerValue,
formatStoredAnswer,
type FormAnswerValue,
} from "@/lib/form-answer-values";
import { stripHtml } from "@/lib/utils";
export type SubmissionQuestion = {
id: string;
type: QuestionType;
label: string;
required: boolean;
options?: string[] | null;
allowOther: boolean;
};
export type SubmittedFormAnswer = {
questionId: string;
value: FormAnswerValue;
};
export type NormalizedSubmissionAnswer = {
questionId: string;
value: string;
displayValue: string;
};
function questionTitle(question: Pick<SubmissionQuestion, "label">) {
return stripHtml(question.label);
}
function ensureSubmittedQuestionIdsBelongToForm(
questions: SubmissionQuestion[],
answersList: SubmittedFormAnswer[],
) {
const questionsById = new Map(questions.map((question) => [question.id, question]));
const submittedQuestionIds = new Set<string>();
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,
): 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),
displayValue: selectedValues.join(", "),
};
}
function normalizeScalarAnswer(
question: SubmissionQuestion,
rawValue: FormAnswerValue | undefined,
): 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,
displayValue: value,
};
}
export function normalizeSubmissionAnswers(
questions: SubmissionQuestion[],
answersList: SubmittedFormAnswer[],
) {
ensureSubmittedQuestionIdsBelongToForm(questions, answersList);
const submittedAnswers = new Map(
answersList.map((answer) => [answer.questionId, answer.value]),
);
return questions.map((question) => {
const rawValue = submittedAnswers.get(question.id);
return question.type === "checkbox"
? normalizeCheckboxAnswer(question, rawValue)
: normalizeScalarAnswer(question, rawValue);
});
}
export function answersByTemplateIndex(
questions: Pick<SubmissionQuestion, "id">[],
normalizedAnswers: NormalizedSubmissionAnswer[],
) {
const questionOrderMap = questions.reduce<Record<string, string>>((acc, question, index) => {
acc[question.id] = String(index + 1);
return acc;
}, {});
return normalizedAnswers.reduce<Record<string, string>>((acc, answer) => {
const index = questionOrderMap[answer.questionId];
if (index) acc[index] = answer.displayValue;
return acc;
}, {});
}
export function submissionUpdateText(
questions: SubmissionQuestion[],
normalizedAnswers: NormalizedSubmissionAnswer[],
oldAnswersByQuestionId: Record<string, string>,
) {
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 oldValue = oldAnswersByQuestionId[answer.questionId];
const oldDisplayValue = question
? formatStoredAnswer(oldValue, question.type)
: oldValue;
if (oldDisplayValue !== answer.displayValue) {
const title = question ? questionTitle(question) : "Question";
changed.push(
`**${title}**: \`${oldDisplayValue || "Empty"}\` ➡️ \`${answer.displayValue || "Empty"}\``,
);
}
}
return changed.length > 0 ? changed.join("\n") : "No changes made.";
}