- forms collection: targeting group (all/users/roles/qualifications), activeFrom/activeUntil window, radio/date field blocks enabled - form-submissions: server-side user attribution, expiry and audience gates, one attempt per user - forms/submissions permissions in the Surveys registry group + admin role seed grants - frontend /surveys list and /surveys/[id] take page with client block renderer - register the pending role permission enum values migration
64 lines
2 KiB
TypeScript
64 lines
2 KiB
TypeScript
/**
|
|
* Pure survey eligibility helpers (client-safe: type-only payload-types import).
|
|
* The list/detail pages use them server-side to mirror the enforced gates in
|
|
* formBuilderConfig.ts, so users never see surveys they cannot submit.
|
|
*/
|
|
import type { Form } from "@/payload-types";
|
|
|
|
export interface SurveyAudienceContext {
|
|
userId: number;
|
|
roleIds: number[];
|
|
qualificationNames: string[];
|
|
}
|
|
|
|
export function isSurveyOpen(
|
|
form: Pick<Form, "activeFrom" | "activeUntil">,
|
|
now: number,
|
|
): boolean {
|
|
if (form.activeFrom && new Date(form.activeFrom).getTime() > now) return false;
|
|
if (form.activeUntil && new Date(form.activeUntil).getTime() <= now) return false;
|
|
return true;
|
|
}
|
|
|
|
function relationId(value: unknown): number | undefined {
|
|
if (typeof value === "number") return value;
|
|
if (typeof value === "object" && value !== null && "id" in value) {
|
|
return (value as { id: number }).id;
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
function relationName(value: unknown): string | null {
|
|
if (typeof value === "object" && value !== null && "name" in value) {
|
|
return (value as { name: string }).name;
|
|
}
|
|
if (typeof value === "string") return value;
|
|
return null;
|
|
}
|
|
|
|
export function surveyAudienceMatches(
|
|
form: Pick<Form, "targeting">,
|
|
ctx: SurveyAudienceContext,
|
|
): boolean {
|
|
const targeting = form.targeting;
|
|
const audience = targeting?.audience ?? "all";
|
|
|
|
if (audience === "all") return true;
|
|
|
|
if (audience === "users") {
|
|
const ids = (targeting?.users ?? []).map(relationId).filter((v): v is number => v !== undefined);
|
|
return ids.includes(ctx.userId);
|
|
}
|
|
|
|
if (audience === "roles") {
|
|
const ids = (targeting?.roles ?? []).map(relationId).filter((v): v is number => v !== undefined);
|
|
return ids.some((id) => ctx.roleIds.includes(id));
|
|
}
|
|
|
|
const names = (targeting?.qualifications ?? [])
|
|
.map(relationName)
|
|
.filter((n): n is string => n !== null)
|
|
.map((n) => n.toLowerCase());
|
|
if (names.length === 0) return false;
|
|
return ctx.qualificationNames.some((qn) => names.includes(qn.toLowerCase()));
|
|
}
|