feat(form): show response totals and queue numbers
This commit is contained in:
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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} />
|
||||
|
||||
Reference in New Issue
Block a user