feat: improve form response experience
This commit is contained in:
+115
-16
@@ -8,10 +8,17 @@ import { getServerSession } from "next-auth";
|
||||
import { authOptions } from "@/app/api/auth/[...nextauth]/route";
|
||||
import { requireAdmin, requireDiscordId, requireFormAccess } from "@/lib/auth";
|
||||
import { sse } from "@/lib/sse";
|
||||
import {
|
||||
decodeCheckboxAnswer,
|
||||
encodeAnswerValue,
|
||||
formatStoredAnswer,
|
||||
type FormAnswerValue,
|
||||
} from "@/lib/form-answer-values";
|
||||
import { stripHtml } from "@/lib/utils";
|
||||
|
||||
export async function submitForm(
|
||||
formId: string,
|
||||
answersList: { questionId: string; value: string }[]
|
||||
answersList: { questionId: string; value: FormAnswerValue }[]
|
||||
) {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session?.user) throw new Error("Not authenticated");
|
||||
@@ -21,6 +28,96 @@ export async function submitForm(
|
||||
const form = await requireFormAccess(formId, discordId);
|
||||
if (!form.isOpen) throw new Error("This form is disabled for now");
|
||||
|
||||
const questionsList = await db.query.questions.findMany({
|
||||
where: (question, { eq }) => eq(question.formId, formId),
|
||||
orderBy: (question, { asc }) => asc(question.displayOrder),
|
||||
});
|
||||
if (questionsList.length === 0) {
|
||||
throw new Error("แบบฟอร์มนี้ยังไม่มีคำถาม");
|
||||
}
|
||||
|
||||
const questionsById = new Map(questionsList.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);
|
||||
}
|
||||
|
||||
const submittedAnswers = new Map(
|
||||
answersList.map((answer) => [answer.questionId, answer.value])
|
||||
);
|
||||
const normalizedAnswers = questionsList.map((question) => {
|
||||
const rawValue = submittedAnswers.get(question.id);
|
||||
|
||||
if (question.type === "checkbox") {
|
||||
if (
|
||||
rawValue !== undefined &&
|
||||
typeof rawValue !== "string" &&
|
||||
(!Array.isArray(rawValue) ||
|
||||
rawValue.some((value) => typeof value !== "string"))
|
||||
) {
|
||||
throw new Error(`รูปแบบคำตอบของ “${stripHtml(question.label)}” ไม่ถูกต้อง`);
|
||||
}
|
||||
|
||||
const selectedValues = [...new Set(decodeCheckboxAnswer(rawValue))];
|
||||
if (
|
||||
selectedValues.some(
|
||||
(value) => !value.trim() || value === "__OTHER__"
|
||||
)
|
||||
) {
|
||||
throw new Error(`โปรดระบุคำตอบสำหรับ “${stripHtml(question.label)}”`);
|
||||
}
|
||||
if (question.required && selectedValues.length === 0) {
|
||||
throw new Error(`จำเป็นต้องตอบ “${stripHtml(question.label)}”`);
|
||||
}
|
||||
|
||||
const standardOptions = question.options ?? [];
|
||||
const customValues = selectedValues.filter(
|
||||
(value) => !standardOptions.includes(value)
|
||||
);
|
||||
if (
|
||||
(!question.allowOther && customValues.length > 0) ||
|
||||
customValues.length > 1
|
||||
) {
|
||||
throw new Error(`ตัวเลือกของ “${stripHtml(question.label)}” ไม่ถูกต้อง`);
|
||||
}
|
||||
|
||||
return {
|
||||
questionId: question.id,
|
||||
value: encodeAnswerValue(selectedValues),
|
||||
displayValue: selectedValues.join(", "),
|
||||
};
|
||||
}
|
||||
|
||||
if (rawValue !== undefined && typeof rawValue !== "string") {
|
||||
throw new Error(`รูปแบบคำตอบของ “${stripHtml(question.label)}” ไม่ถูกต้อง`);
|
||||
}
|
||||
|
||||
const value = rawValue ?? "";
|
||||
if (value === "__OTHER__") {
|
||||
throw new Error(`โปรดระบุคำตอบสำหรับ “${stripHtml(question.label)}”`);
|
||||
}
|
||||
if (question.required && !value.trim()) {
|
||||
throw new Error(`จำเป็นต้องตอบ “${stripHtml(question.label)}”`);
|
||||
}
|
||||
if (
|
||||
question.type === "radio" &&
|
||||
value &&
|
||||
!(question.options ?? []).includes(value) &&
|
||||
!question.allowOther
|
||||
) {
|
||||
throw new Error(`ตัวเลือกของ “${stripHtml(question.label)}” ไม่ถูกต้อง`);
|
||||
}
|
||||
|
||||
return {
|
||||
questionId: question.id,
|
||||
value,
|
||||
displayValue: value,
|
||||
};
|
||||
});
|
||||
|
||||
const result = await db.transaction(async (tx) => {
|
||||
const existingSubmission = await tx.query.submissions.findFirst({
|
||||
where: (submission, { eq, and }) =>
|
||||
@@ -69,9 +166,9 @@ export async function submitForm(
|
||||
submissionId = inserted.id;
|
||||
}
|
||||
|
||||
if (answersList.length > 0) {
|
||||
if (normalizedAnswers.length > 0) {
|
||||
await tx.insert(answers).values(
|
||||
answersList.map((answer) => ({
|
||||
normalizedAnswers.map((answer) => ({
|
||||
submissionId,
|
||||
questionId: answer.questionId,
|
||||
value: answer.value,
|
||||
@@ -89,19 +186,15 @@ export async function submitForm(
|
||||
|
||||
if (form.discordWebhookUrl && templateToUse) {
|
||||
try {
|
||||
const questionsList = await db.query.questions.findMany({
|
||||
where: (q, { eq }) => eq(q.formId, formId),
|
||||
orderBy: (q, { asc }) => asc(q.displayOrder)
|
||||
});
|
||||
const questionMap = questionsList.reduce((acc, q, index) => {
|
||||
const questionOrderMap = questionsList.reduce((acc, q, index) => {
|
||||
acc[q.id] = String(index + 1);
|
||||
return acc;
|
||||
}, {} as Record<string, string>);
|
||||
|
||||
const answersDict = answersList.reduce((acc, a) => {
|
||||
const idx = questionMap[a.questionId];
|
||||
const answersDict = normalizedAnswers.reduce((acc, answer) => {
|
||||
const idx = questionOrderMap[answer.questionId];
|
||||
if (idx) {
|
||||
acc[idx] = a.value;
|
||||
acc[idx] = answer.displayValue;
|
||||
}
|
||||
return acc;
|
||||
}, {} as Record<string, string>);
|
||||
@@ -109,11 +202,17 @@ export async function submitForm(
|
||||
let updatesText = "";
|
||||
if (result.isUpdate) {
|
||||
const changed: string[] = [];
|
||||
for (const ans of answersList) {
|
||||
const oldVal = result.oldAnswersDict[ans.questionId];
|
||||
if (oldVal !== ans.value) {
|
||||
const qTitle = questionsList.find(q => q.id === ans.questionId)?.label || `Question`;
|
||||
changed.push(`**${qTitle}**: \`${oldVal || "Empty"}\` ➡️ \`${ans.value || "Empty"}\``);
|
||||
for (const answer of normalizedAnswers) {
|
||||
const question = questionsById.get(answer.questionId);
|
||||
const oldValue = result.oldAnswersDict[answer.questionId];
|
||||
const oldDisplayValue = question
|
||||
? formatStoredAnswer(oldValue, question.type)
|
||||
: oldValue;
|
||||
if (oldDisplayValue !== answer.displayValue) {
|
||||
const questionTitle = stripHtml(question?.label || "Question");
|
||||
changed.push(
|
||||
`**${questionTitle}**: \`${oldDisplayValue || "Empty"}\` ➡️ \`${answer.displayValue || "Empty"}\``
|
||||
);
|
||||
}
|
||||
}
|
||||
updatesText = changed.length > 0 ? changed.join("\n") : "No changes made.";
|
||||
|
||||
Reference in New Issue
Block a user