mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 18:41:08 +02:00
feat(forms): M10a wiederkehrende Forms — cohort-tagging + UI
Plan: docs/plans/forms-module.md M10. Erste Hälfte (Datenshape +
Submit-Stamping + ResponsesView-Filter); cron-basierter
Broadcast-Versand bleibt M10b.
- types.ts:
- FormSettings.recurrence?: { frequency: 'weekly'|'monthly',
startedAt?, sendVia? }. Im Settings-Blob mitverschlüsselt.
- LocalFormResponse.cohort?: string. Plaintext (sortier-/filter-
Feld, kein PII). FormResponse.cohort: string|null.
- queries.toFormResponse maps null durch.
- lib/cohort.ts (pure):
- computeCohort(iso, frequency) → "YYYY-WNN" (ISO-Woche, padded für
lex-sort) | "YYYY-MM". Behandelt Year-Boundary korrekt
(2027-01-01 → 2026-W53).
- cohortLabel(cohort, frequency, now=new Date()) → "Diese Woche" /
"Letzte Woche" / "KW NN / YYYY" für weekly; "Dieser Monat" /
"Letzter Monat" / "Monatsname YYYY" für monthly. Falsche Keys
fallen auf Roh-Wert zurück (inkl. Bounds-Check 1-12 für month).
- sortCohortsDesc(): immutable, lex-sort newest-first.
- 15/15 Vitest-Cases (compute, label-current/prev/older, malformed,
sort, no-mutation).
- buildFormBlob (data/unlisted/resolvers.ts) zieht recurrence.frequency
in den Public-Snapshot. Frequenz ist nicht-sensitive Metadaten —
kann öffentlich diskoverabel sein, der Server braucht es zum
cohort-stamping.
- apps/api/src/modules/forms/public-routes.ts:
- Inline computeCohort (Mirror der pure-Funktion — apps/api kann
apps/mana nicht importieren).
- Bei jeder Submission: wenn snapshot.recurrence.frequency gesetzt,
stamp data.cohort = computeCohort(submittedAt, freq). Cohort
landet im sync_changes-Insert und reist zum Owner-Client.
- errorResponse-Aufrufe: { code, field } → { code, details: { field } }
weil der Hono-helper die details-Struktur erzwingt (M3b
type-safety-fix mitgenommen).
- SettingsPanel: Recurrence-Block mit Dropdown (Einmalig / Wöchentlich
/ Monatlich) + Hint-Erklärung. Setzt startedAt automatisch beim
Aktivieren.
- ResponsesView: cohort-chip-bar oberhalb der Status-Tabs. Zeigt
"Alle Wellen" + ein Chip pro distincter cohort (newest-first), je
mit Anzahl. Klick filtert die responses-Liste — alle bestehenden
Status-Counts berechnen sich auf den cohort-gefilterten Set, also
CSV-Export + Detail-Modal-Status-Pills bleiben konsistent.
- 8 neue i18n-Keys × 5 Locales (forms.builder.recurrence.* +
forms.responses.cohort.all). i18n-parity 6483 keys aligned.
Tests: 41/41 forms-tests grün (5 csv + 11 branching + 10 auto-sync +
15 cohort). svelte-check 0 errors. apps/api buildet.
M10b open: cron-worker in mana-ai oder mana-notify, der
recurrence-Forms scant + Share-Link via broadcasts an die
Recipients-Liste sendet. Schemata stehen, Pipeline fehlt noch.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
cb9d79d2c9
commit
38e0ae2ff8
15 changed files with 470 additions and 11 deletions
|
|
@ -88,6 +88,30 @@ interface FormSnapshotBlob {
|
|||
}>;
|
||||
branching?: unknown[];
|
||||
settings?: { submitButtonLabel?: string; successMessage?: string };
|
||||
recurrence?: { frequency?: 'weekly' | 'monthly' };
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute the cohort label for a response based on the form's
|
||||
* recurrence frequency. Mirror of `lib/cohort.ts` in the webapp —
|
||||
* duplicated here because the apps/api surface can't import from
|
||||
* apps/mana/. ISO 8601 week math.
|
||||
*/
|
||||
function computeCohort(submittedAtIso: string, frequency: 'weekly' | 'monthly'): string {
|
||||
const date = new Date(submittedAtIso);
|
||||
if (Number.isNaN(date.getTime())) return '';
|
||||
if (frequency === 'monthly') {
|
||||
const year = date.getUTCFullYear();
|
||||
const month = date.getUTCMonth() + 1;
|
||||
return `${year}-${String(month).padStart(2, '0')}`;
|
||||
}
|
||||
const utc = new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate()));
|
||||
const dayOfWeek = utc.getUTCDay() || 7;
|
||||
utc.setUTCDate(utc.getUTCDate() + 4 - dayOfWeek);
|
||||
const year = utc.getUTCFullYear();
|
||||
const yearStart = Date.UTC(year, 0, 1);
|
||||
const week = Math.ceil(((utc.getTime() - yearStart) / 86_400_000 + 1) / 7);
|
||||
return `${year}-W${String(week).padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
interface SubmitBody {
|
||||
|
|
@ -168,7 +192,7 @@ routes.post('/:token/submit', async (c) => {
|
|||
) {
|
||||
return errorResponse(c, `Feld "${field.label ?? field.id}" ist erforderlich`, 400, {
|
||||
code: 'REQUIRED_MISSING',
|
||||
field: field.id,
|
||||
details: { field: field.id },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -177,7 +201,7 @@ routes.post('/:token/submit', async (c) => {
|
|||
if (cleanAnswers[field.id] !== true) {
|
||||
return errorResponse(c, `Einwilligung "${field.label ?? field.id}" ist erforderlich`, 400, {
|
||||
code: 'CONSENT_REQUIRED',
|
||||
field: field.id,
|
||||
details: { field: field.id },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -214,6 +238,10 @@ routes.post('/:token/submit', async (c) => {
|
|||
};
|
||||
if (submitterEmail) data.submitterEmail = submitterEmail;
|
||||
if (submitterName) data.submitterName = submitterName;
|
||||
if (blob.recurrence?.frequency) {
|
||||
const cohort = computeCohort(submittedAt, blob.recurrence.frequency);
|
||||
if (cohort) data.cohort = cohort;
|
||||
}
|
||||
if (ipHash || userAgent || referrer) {
|
||||
data.submitterMeta = {
|
||||
...(ipHash ? { ipHash } : {}),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue