Files
paliad/internal/db/migrations/014_checklist_instances.up.sql
m 4c0babb2f3 feat(checklisten): instanceable checklists — DB-backed state, Akte linkage
Checklisten move from one-per-slug localStorage state to a template/instance
model. A user creates multiple named instances of each template (UPC SoC,
EPA Einspruch, …), each with its own checkbox state in paliad.checklist_instances
and an optional akte_id for office-wide visibility.

- Migration 014: paliad.checklist_instances + RLS mirroring the Termine
  pattern (akte_id nullable → creator-only; akte_id set → can_see_akte gate).
- Static template data moves out of internal/handlers into internal/checklisten
  so both handlers and the new ChecklistInstanceService can reference it
  without an import cycle.
- ChecklistInstanceService: CRUD + state merge via `state || $n::jsonb`
  so concurrent checkbox toggles don't clobber each other. Reset clears
  state to {}. Akte-linked mutations append akten_events audit rows.
- Handlers: GET/POST /api/checklisten/{slug}/instances, GET/PATCH/DELETE
  /api/checklisten/instances/{id}, POST .../reset, GET /api/akten/{id}/checklisten.
- /checklisten/{slug} redesigned to show template metadata + instance
  table + "Neue Instanz" modal (with optional Akte dropdown). The
  interactive checkboxes move to /checklisten/instances/{id} where the
  state is DB-backed and Reset posts to the server. Fixes the original
  Reset button regression — it now operates on real server state rather
  than silently failing client-side.
- Akten detail grows a Checklisten tab listing linked instances with
  progress bars; only loads on tab activation.
- localStorage-based progress removed from the overview grid (state no
  longer lives there).
- DE + EN i18n keys added.

Verified: bun run build clean; go build ./...; go vet ./...; go test ./...
all green.
2026-04-17 13:54:32 +02:00

67 lines
2.9 KiB
SQL

-- Phase K: paliad.checklist_instances — per-user (and optionally per-Akte)
-- instantiations of the static Checklisten templates defined in Go.
--
-- Templates stay in handlers.checklists (static Go data). A row here is one
-- user's personal checklist for one situation: a name, a link to an Akte
-- (optional), and the current checkbox state as a jsonb map
-- ({item_key: true}). State lives in the DB (not localStorage) so checkboxes
-- survive browser changes, and office-mates can share them via an Akte link.
--
-- Visibility mirrors paliad.termine (akte_id nullable):
-- - akte_id NULL → creator-only (personal instance)
-- - akte_id NOT NULL → parent Akte's office-scoped gate
--
-- template_slug is a FK concept only — the slug's validity is enforced in
-- the service layer against the static Go list.
CREATE TABLE paliad.checklist_instances (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
template_slug text NOT NULL,
name text NOT NULL,
akte_id uuid REFERENCES paliad.akten(id) ON DELETE SET NULL,
state jsonb NOT NULL DEFAULT '{}'::jsonb,
created_by uuid NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX checklist_instances_template_idx ON paliad.checklist_instances (template_slug);
CREATE INDEX checklist_instances_akte_idx ON paliad.checklist_instances (akte_id) WHERE akte_id IS NOT NULL;
CREATE INDEX checklist_instances_created_by_idx ON paliad.checklist_instances (created_by);
ALTER TABLE paliad.checklist_instances ENABLE ROW LEVEL SECURITY;
-- RLS — mirrors the termine policy so personal instances are creator-only
-- while Akte-linked instances follow the office-scoped can_see_akte gate.
CREATE POLICY checklist_instances_select ON paliad.checklist_instances
FOR SELECT TO authenticated
USING (
(akte_id IS NULL AND created_by = auth.uid())
OR (akte_id IS NOT NULL AND paliad.can_see_akte(akte_id))
);
CREATE POLICY checklist_instances_insert ON paliad.checklist_instances
FOR INSERT TO authenticated
WITH CHECK (
created_by = auth.uid()
AND (
akte_id IS NULL
OR paliad.can_see_akte(akte_id)
)
);
CREATE POLICY checklist_instances_update ON paliad.checklist_instances
FOR UPDATE TO authenticated
USING (
(akte_id IS NULL AND created_by = auth.uid())
OR (akte_id IS NOT NULL AND paliad.can_see_akte(akte_id))
)
WITH CHECK (
(akte_id IS NULL AND created_by = auth.uid())
OR (akte_id IS NOT NULL AND paliad.can_see_akte(akte_id))
);
CREATE POLICY checklist_instances_delete ON paliad.checklist_instances
FOR DELETE TO authenticated
USING (
(akte_id IS NULL AND created_by = auth.uid())
OR (akte_id IS NOT NULL AND paliad.can_see_akte(akte_id))
);