import { db } from "@/db"; import { submissions } from "@/db/schema"; import { and, asc, count, desc, eq, gte, ilike, lt, or, sql, type SQL, } from "drizzle-orm"; export const RESULTS_PAGE_SIZE = 25; export type ResultFilters = { q: string; page: number; sort: "submittedAt" | "respondent"; direction: "asc" | "desc"; edited: "all" | "edited" | "unedited"; dateFrom: string; dateTo: string; }; export function parseResultFilters( params: Record ): ResultFilters { const value = (key: string) => { const entry = params[key]; return Array.isArray(entry) ? entry[0] ?? "" : entry ?? ""; }; const page = Number.parseInt(value("page"), 10); const sort = value("sort"); const direction = value("direction"); const edited = value("edited"); return { q: value("q").trim().slice(0, 100), page: Number.isFinite(page) && page > 0 ? page : 1, sort: sort === "respondent" ? "respondent" : "submittedAt", direction: direction === "asc" ? "asc" : "desc", edited: edited === "edited" || edited === "unedited" ? edited : "all", dateFrom: /^\d{4}-\d{2}-\d{2}$/.test(value("dateFrom")) ? value("dateFrom") : "", dateTo: /^\d{4}-\d{2}-\d{2}$/.test(value("dateTo")) ? value("dateTo") : "", }; } function bangkokBoundary(date: string, end: boolean) { if (!date) return null; const value = new Date(`${date}T00:00:00+07:00`); if (end) value.setUTCDate(value.getUTCDate() + 1); return value; } function buildWhere(formId: string, filters: ResultFilters) { const conditions: SQL[] = [eq(submissions.formId, formId)]; if (filters.q) { const query = `%${filters.q}%`; conditions.push( or( ilike(submissions.userName, query), ilike(submissions.userDiscordId, query) )! ); } if (filters.edited === "edited") { conditions.push(sql`jsonb_array_length(coalesce(${submissions.editHistory}, '[]'::jsonb)) > 0`); } if (filters.edited === "unedited") { conditions.push(sql`jsonb_array_length(coalesce(${submissions.editHistory}, '[]'::jsonb)) = 0`); } const from = bangkokBoundary(filters.dateFrom, false); const to = bangkokBoundary(filters.dateTo, true); if (from) conditions.push(gte(submissions.submittedAt, from)); if (to) conditions.push(lt(submissions.submittedAt, to)); return and(...conditions)!; } export async function getFilteredSubmissions( formId: string, filters: ResultFilters, options: { paginate?: boolean } = {} ) { const where = buildWhere(formId, filters); const orderDirection = filters.direction === "asc" ? asc : desc; const order = filters.sort === "respondent" ? orderDirection(submissions.userName) : orderDirection(submissions.submittedAt); const [{ total }] = await db .select({ total: count() }) .from(submissions) .where(where); const baseQuery = db .select() .from(submissions) .where(where) .orderBy(order, orderDirection(submissions.id)); const rows = options.paginate === false ? await baseQuery : await baseQuery .limit(RESULTS_PAGE_SIZE) .offset((filters.page - 1) * RESULTS_PAGE_SIZE); return { rows, total }; } export function resultFiltersToSearchParams( filters: ResultFilters, overrides: Partial = {} ) { const next = { ...filters, ...overrides }; const params = new URLSearchParams(); if (next.q) params.set("q", next.q); if (next.page > 1) params.set("page", String(next.page)); if (next.sort !== "submittedAt") params.set("sort", next.sort); if (next.direction !== "desc") params.set("direction", next.direction); if (next.edited !== "all") params.set("edited", next.edited); if (next.dateFrom) params.set("dateFrom", next.dateFrom); if (next.dateTo) params.set("dateTo", next.dateTo); return params.toString(); }