feat(form): show response totals and queue numbers

This commit is contained in:
2026-07-26 15:54:20 +07:00 Unverified
parent 1b58d2c363
commit 73bd6ef5d2
6 changed files with 148 additions and 10 deletions
+36 -3
View File
@@ -2,7 +2,7 @@
import { db } from "@/db";
import { submissions, answers } from "@/db/schema";
import { eq } from "drizzle-orm";
import { and, count, eq, lt, lte, or } from "drizzle-orm";
import { revalidatePath } from "next/cache";
import { getServerSession } from "next-auth";
import { authOptions } from "@/lib/auth-options";
@@ -44,11 +44,13 @@ export async function submitForm(
});
let submissionId = "";
let submittedAt = new Date();
const isUpdate = Boolean(existingSubmission);
let oldAnswersDict: Record<string, string> = {};
if (existingSubmission) {
submissionId = existingSubmission.id;
submittedAt = existingSubmission.submittedAt;
const oldAnswersList = await tx.query.answers.findMany({
where: (answer, { eq }) => eq(answer.submissionId, submissionId),
@@ -83,6 +85,7 @@ export async function submitForm(
})
.returning();
submissionId = inserted.id;
submittedAt = inserted.submittedAt;
}
if (normalizedAnswers.length > 0) {
@@ -95,7 +98,33 @@ export async function submitForm(
);
}
return { submissionId, isUpdate, oldAnswersDict };
const [submissionStats] = await tx
.select({ totalAnswers: count() })
.from(submissions)
.where(eq(submissions.formId, formId));
const [queueStats] = await tx
.select({ queueNumber: count() })
.from(submissions)
.where(
and(
eq(submissions.formId, formId),
or(
lt(submissions.submittedAt, submittedAt),
and(
eq(submissions.submittedAt, submittedAt),
lte(submissions.id, submissionId)
)
)
)
);
return {
submissionId,
isUpdate,
oldAnswersDict,
totalAnswers: submissionStats.totalAnswers,
queueNumber: queueStats.queueNumber,
};
});
// Send Discord Webhook
@@ -146,7 +175,11 @@ export async function submitForm(
action: result.isUpdate ? "updated" : "created",
});
revalidatePath(`/admin/form/${formId}/result`);
return { ok: true };
return {
ok: true,
totalAnswers: result.totalAnswers,
queueNumber: result.queueNumber,
};
}
export async function deleteSubmission(id: string) {
+12 -1
View File
@@ -15,6 +15,7 @@ import { DeleteFormButton } from "@/components/delete-buttons";
import { AdminShellServer } from "@/components/admin-shell-server";
import { createDefaultForm } from "./actions";
import { RealtimeRefresh } from "@/components/realtime-refresh";
import { getFormSubmissionStats } from "@/lib/form-submission-stats";
export const dynamic = "force-dynamic";
@@ -22,6 +23,10 @@ export default async function AdminFormsPage() {
const formsList = await db.query.forms.findMany({
orderBy: (f, { desc }) => [desc(f.createdAt)],
});
const submissionStats = await getFormSubmissionStats(
formsList.map((form) => form.id),
null
);
return (
<AdminShellServer>
@@ -48,6 +53,7 @@ export default async function AdminFormsPage() {
<TableRow>
<TableHead>Title</TableHead>
<TableHead>Status</TableHead>
<TableHead>Responses</TableHead>
<TableHead>Created</TableHead>
<TableHead className="text-right">Actions</TableHead>
</TableRow>
@@ -55,7 +61,7 @@ export default async function AdminFormsPage() {
<TableBody>
{formsList.length === 0 ? (
<TableRow>
<TableCell colSpan={4} className="h-24 text-center text-muted-foreground">
<TableCell colSpan={5} className="h-24 text-center text-muted-foreground">
No forms found. Create one to get started.
</TableCell>
</TableRow>
@@ -76,6 +82,11 @@ export default async function AdminFormsPage() {
)}
</Link>
</TableCell>
<TableCell className="p-0 align-middle">
<Link href={`/admin/form/${form.id}/result`} className="flex items-center p-3 h-full w-full">
{submissionStats.get(form.id)?.totalAnswers ?? 0}
</Link>
</TableCell>
<TableCell className="p-0 align-middle">
<Link href={`/admin/form/${form.id}`} className="flex items-center p-3 h-full w-full">
{form.createdAt ? new Date(form.createdAt).toLocaleDateString() : 'N/A'}
+14 -4
View File
@@ -10,6 +10,7 @@ import {
decodeCheckboxAnswer,
type FormAnswerValue,
} from "@/lib/form-answer-values";
import { getFormSubmissionStats } from "@/lib/form-submission-stats";
export const dynamic = "force-dynamic";
@@ -43,10 +44,17 @@ export default async function FormsSubmitPage(
orderBy: (q, { asc }) => [asc(q.displayOrder)],
});
const existingSubmission = await db.query.submissions.findFirst({
where: (s, { eq, and }) =>
and(eq(s.formId, form.id), eq(s.userDiscordId, discordId)),
});
const [existingSubmission, submissionStats] = await Promise.all([
db.query.submissions.findFirst({
where: (s, { eq, and }) =>
and(eq(s.formId, form.id), eq(s.userDiscordId, discordId)),
}),
getFormSubmissionStats([form.id], discordId),
]);
const stats = submissionStats.get(form.id) ?? {
totalAnswers: 0,
queueNumber: null,
};
const initialAnswers: Record<string, FormAnswerValue> = {};
if (existingSubmission) {
@@ -71,6 +79,8 @@ export default async function FormsSubmitPage(
questions={questionsList}
initialAnswers={initialAnswers}
hasSubmission={Boolean(existingSubmission)}
totalAnswers={stats.totalAnswers}
queueNumber={stats.queueNumber}
/>
</div>
</>
+14 -1
View File
@@ -104,6 +104,8 @@ interface FormFillerClientProps {
questions: Question[];
initialAnswers?: Record<string, FormAnswerValue>;
hasSubmission?: boolean;
totalAnswers: number;
queueNumber: number | null;
}
const LS_KEY = (formId: string) => `form-draft-${formId}`;
@@ -212,6 +214,8 @@ export function FormFillerClient({
questions,
initialAnswers = {},
hasSubmission = false,
totalAnswers: initialTotalAnswers,
queueNumber: initialQueueNumber,
}: FormFillerClientProps) {
const router = useRouter();
const isUpdating = hasSubmission;
@@ -230,6 +234,8 @@ export function FormFillerClient({
const [showSuccessDialog, setShowSuccessDialog] = useState(false);
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
const [isDeleting, setIsDeleting] = useState(false);
const [totalAnswers, setTotalAnswers] = useState(initialTotalAnswers);
const [queueNumber, setQueueNumber] = useState(initialQueueNumber);
const methods = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
@@ -337,6 +343,8 @@ export function FormFillerClient({
const res = await submitForm(form.id, formattedAnswers);
if (res.ok) {
setTotalAnswers(res.totalAnswers);
setQueueNumber(res.queueNumber);
if (isUpdating) {
toast.success("บันทึกการแก้ไขแล้ว");
} else {
@@ -398,6 +406,10 @@ export function FormFillerClient({
: "ปิดรับคำตอบแล้ว"}
</Badge>
<span>{questions.length} คำถาม</span>
<span>{totalAnswers} คำตอบทั้งหมด</span>
{queueNumber !== null && (
<Badge variant="outline">คิวของคุณ #{queueNumber}</Badge>
)}
</CardDescription>
</CardHeader>
{form.description && (
@@ -833,7 +845,8 @@ export function FormFillerClient({
ส่งคำตอบแล้ว
</AlertDialogTitle>
<AlertDialogDescription>
บันทึกคำตอบของคุณเรียบร้อย คุณกลับมาแก้ไขได้ตราบใดที่แบบฟอร์มยังเปิดอยู่
บันทึกคำตอบของคุณเรียบร้อย คุณอยู่ในคิวที่ #{queueNumber} จากทั้งหมด{" "}
{totalAnswers} คำตอบ และกลับมาแก้ไขได้ตราบใดที่แบบฟอร์มยังเปิดอยู่
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
+19 -1
View File
@@ -10,6 +10,8 @@ import { getServerSession } from "next-auth";
import { authOptions } from "@/lib/auth-options";
import { canAccessForm, getSessionDiscordId } from "@/lib/auth";
import { RealtimeRefresh } from "@/components/realtime-refresh";
import { Badge } from "@/components/ui/badge";
import { getFormSubmissionStats } from "@/lib/form-submission-stats";
export const dynamic = "force-dynamic";
@@ -27,6 +29,10 @@ export default async function FormsListingPage() {
visibleForms.push(form);
}
}
const submissionStats = await getFormSubmissionStats(
visibleForms.map((form) => form.id),
discordId
);
return (
<>
@@ -41,7 +47,19 @@ export default async function FormsListingPage() {
<Link key={form.id} href={`/form/${form.id}`}>
<Card className="hover:bg-muted/50 transition-colors h-full flex flex-col">
<CardHeader>
<CardTitle>{form.title}</CardTitle>
<div className="flex flex-wrap items-start justify-between gap-3">
<CardTitle>{form.title}</CardTitle>
<div className="flex flex-wrap gap-2">
<Badge variant="secondary">
{submissionStats.get(form.id)?.totalAnswers ?? 0} คำตอบ
</Badge>
{submissionStats.get(form.id)?.queueNumber != null && (
<Badge variant="outline">
คิวของคุณ #{submissionStats.get(form.id)?.queueNumber}
</Badge>
)}
</div>
</div>
{form.description && (
<div className="line-clamp-3 text-sm text-muted-foreground mt-1.5">
<HtmlDisplay html={form.description} />