1
0
Fork 0
polaris-task-force/src/lib/surveys/evaluate.ts
Z8MB1E 20b65d08fc feat(surveys): add survey system with audience targeting on plugin-form-builder
- 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
2026-09-23 18:17:29 -04:00

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()));
}