design(t-paliad-088): resolve open questions per m's calls
m greenlit all 7 open questions on 2026-04-30 12:23. Notable changes from the initial draft: - Submissions are explicitly the primary Event-Type use case, not a secondary discriminator. m: "those are the event types I mean, mainly". Deferring a separate paliad.submissions table stands. - /deadlines + /agenda Typ filter is MULTI-SELECT (UNION across selected types, AND-intersected with Status/Projekt). New EventTypeMultiSelect component spec'd in §4: trigger button styled like the existing <select>s, popover with search + grouped checkbox list. Status/Projekt stay single-select. - Firm-wide Event-Type creation OPEN to any authenticated user. RLS insert policy simplified to created_by=self. Admins moderate via archive. Mitigation: duplicate-warning in the add modal. Follow-up t-paliad-089 flagged for admin moderation panel. - Broader-scope seeds confirmed (UPC + EPO + DPMA + DE + contract). - §12 rewritten as a resolution table.
This commit is contained in:
@@ -3,20 +3,21 @@
|
||||
**Task:** t-paliad-088
|
||||
**Author:** cronus (inventor)
|
||||
**Date:** 2026-04-30
|
||||
**Status:** AWAITING m's go/no-go on §1, §2, §4 before any code.
|
||||
**Status:** RESOLVED 2026-04-30 12:23 — m greenlit all 7 questions. See §12 for the resolution table. Awaiting head's coder assignment.
|
||||
|
||||
m's directive (2026-04-30 11:56):
|
||||
|
||||
> "let's add an inventor for 'Event Types', in particular deadlines and submissions — I want to be able to select from existing Event Types when creating a deadline but also add a new custom one if it does not exist. This also needs to be filterable in the overview"
|
||||
|
||||
## TL;DR — recommended decisions
|
||||
## TL;DR — resolved decisions
|
||||
|
||||
1. **Concept:** "Event Type" is the **categorization tag** on a deadline — *what kind of work this is* (a Reply, a Defence, a court decision, a fee payment). It is **distinct from** `paliad.trigger_events`, which is calc-engine state — *what just happened, used as anchor for "what comes after?"*. They overlap by ~70 % of rows but live separate lives. Don't conflate.
|
||||
2. **Schema:** new table `paliad.event_types` + nullable FK `paliad.deadlines.event_type_id`. Seed firm-wide types from a curated subset of `trigger_events` (~30 most-used UPC submissions + ~10 EPO/DPMA/national/contract additions). Allow user-private types and global-admin-created firm-wide types.
|
||||
3. **Submissions:** **NOT** a separate entity. A submission deadline is a `paliad.deadlines` row whose Event Type has `category='submission'`. A future Schriftsatz-Verwaltung surface can derive a submissions tab via that filter. No new table today.
|
||||
4. **Picker:** typeahead `<select>`-flavoured combobox with grouping by category, plus inline "+ Neuen Typ hinzufügen…" → small modal. Reuse the `/tools/fristenrechner` trigger-picker visual style for consistency.
|
||||
5. **Filter:** new `Typ` `<select>` on `/deadlines` (matching the existing Status/Projekt selects, **not** pills — the pill row exists only on `/agenda` for the Type-of-item chip). Optionally extend to `/agenda` later (see §4).
|
||||
6. **Backfill:** existing 10 deadlines get `event_type_id=NULL`, render as "Ohne Typ". No automatic title-parsing backfill (too noisy).
|
||||
1. **Concept:** "Event Type" is the **categorization tag** on a deadline. **Event Types lead**, with an optional bridge FK to `paliad.trigger_events` for the seeded UPC rows (m's call: "event_types should lead and later we can connect things to it"). Submissions are explicitly the primary use case — m's words: *"those are the event types I mean, mainly"*. trigger_events stays as separate calc-engine state (UPC-only verbatim youpc imports).
|
||||
2. **Schema:** new table `paliad.event_types` + nullable FK `paliad.deadlines.event_type_id`. The bridge `event_types.trigger_event_id bigint NULL REFERENCES paliad.trigger_events(id)` populates only for seeded UPC rows; user customs and non-UPC entries leave it NULL. Broader scope from day one (UPC + EPO + DPMA + DE-national + contract).
|
||||
3. **Submissions live as Event Types.** No separate `paliad.submissions` table. `event_types.category='submission'` carries the discrimination. A future Schriftsatz-Verwaltung surface pivots on that filter.
|
||||
4. **Picker:** typeahead `<select>`-flavoured combobox with grouping by category, plus inline "+ Neuen Typ hinzufügen…" → small modal. Reuses the `/tools/fristenrechner` trigger-picker style.
|
||||
5. **Filter on `/deadlines` is MULTI-SELECT.** Trigger button styled like an `<select>`; click opens a listbox panel with search + "Alle" toggle + checkbox list grouped by category. Backend: `?event_type=uuid1,uuid2,…` (UNION within Event Types, AND-intersected with Status/Projekt). Special value `none` for "Ohne Typ"; combinable with selected types. **Same multi-select filter on `/agenda`**.
|
||||
6. **Permissions:** any authenticated user can create both private AND firm-wide types. Admins moderate firm-wide via archive after the fact (m's call: lighter-weight onboarding > admin gating).
|
||||
7. **Backfill:** existing 10 deadlines get `event_type_id=NULL`, render as "Ohne Typ". Migration 030 ships ~40 curated firm-wide seeds (~25 UPC submissions + ~10 UPC decisions/orders + ~10 non-UPC EPO/DPMA/DE-national/contract). Spreadsheet attached to the implementation PR.
|
||||
|
||||
## 1 · Concept clarification
|
||||
|
||||
@@ -124,32 +125,20 @@ CREATE POLICY event_types_select ON paliad.event_types
|
||||
AND (is_firm_wide = true OR created_by = auth.uid())
|
||||
);
|
||||
|
||||
-- Insert: any authenticated user can insert is_firm_wide=false rows (created_by=self).
|
||||
-- is_firm_wide=true gated to global_admin.
|
||||
CREATE POLICY event_types_insert_private ON paliad.event_types
|
||||
-- Insert: any authenticated user can insert any row, as long as created_by = self.
|
||||
-- Firm-wide types are open to all users; admins moderate via archive after the fact.
|
||||
CREATE POLICY event_types_insert ON paliad.event_types
|
||||
FOR INSERT TO authenticated
|
||||
WITH CHECK (
|
||||
is_firm_wide = false
|
||||
AND created_by = auth.uid()
|
||||
);
|
||||
WITH CHECK (created_by = auth.uid());
|
||||
|
||||
CREATE POLICY event_types_insert_firmwide ON paliad.event_types
|
||||
FOR INSERT TO authenticated
|
||||
WITH CHECK (
|
||||
is_firm_wide = true
|
||||
AND EXISTS (
|
||||
SELECT 1 FROM paliad.users u
|
||||
WHERE u.id = auth.uid() AND u.global_role = 'global_admin'
|
||||
)
|
||||
);
|
||||
|
||||
-- Update: author owns private rows; global_admin owns firm-wide rows.
|
||||
CREATE POLICY event_types_update_private ON paliad.event_types
|
||||
-- Update: author owns their own rows (private or firm-wide they created).
|
||||
-- global_admin can update / archive any firm-wide row regardless of authorship.
|
||||
CREATE POLICY event_types_update_owner ON paliad.event_types
|
||||
FOR UPDATE TO authenticated
|
||||
USING (is_firm_wide = false AND created_by = auth.uid())
|
||||
WITH CHECK (is_firm_wide = false AND created_by = auth.uid());
|
||||
USING (created_by = auth.uid())
|
||||
WITH CHECK (created_by = auth.uid());
|
||||
|
||||
CREATE POLICY event_types_update_firmwide ON paliad.event_types
|
||||
CREATE POLICY event_types_update_admin ON paliad.event_types
|
||||
FOR UPDATE TO authenticated
|
||||
USING (
|
||||
is_firm_wide = true
|
||||
@@ -268,43 +257,58 @@ Two reasons:
|
||||
|
||||
## 4 · Filter UX on `/deadlines` (and `/agenda`)
|
||||
|
||||
### `/deadlines` (primary scope)
|
||||
### `/deadlines` (primary scope) — multi-select
|
||||
|
||||
The brief mentions "filter pill row, parallel to the existing Status / Akte / Regel filters". **The actual existing pattern on `/deadlines` is `<select>` dropdowns**, not pills (verified in `frontend/src/deadlines.tsx:70-83`). Pills exist only on `/agenda` for the Type-of-item chip (deadlines/appointments). I'll match the **existing select pattern** for consistency — a third row, same shape:
|
||||
m's call (Q4): match the existing `<select>`-row pattern visually, but make Event Types **multi-select**. Status/Projekt stay single-select. New `EventTypeMultiSelect` component:
|
||||
|
||||
```html
|
||||
<div class="akten-filter-row">
|
||||
<label for="deadline-filter-event-type" data-i18n="deadlines.filter.event_type">Typ</label>
|
||||
<select id="deadline-filter-event-type" class="akten-select">
|
||||
<option value="all" data-i18n="deadlines.filter.event_type.all">Alle Typen</option>
|
||||
<option value="none" data-i18n="deadlines.filter.event_type.none">— Ohne Typ —</option>
|
||||
<optgroup label="Eingaben (UPC)" data-i18n-label="event_types.cat.submission">
|
||||
<option value="<uuid>">Statement of Defence</option>
|
||||
…
|
||||
</optgroup>
|
||||
<optgroup label="Entscheidungen" …>…</optgroup>
|
||||
…
|
||||
<optgroup label="Eigene" …>…</optgroup>
|
||||
</select>
|
||||
</div>
|
||||
```
|
||||
.akten-filter-row
|
||||
├─ <label>Typ</label>
|
||||
└─ <button class="akten-select akten-multi-trigger" aria-haspopup="listbox">
|
||||
<span class="akten-multi-label">Alle</span> ← / "3 Typen" / single label
|
||||
<span class="akten-multi-chevron">▾</span>
|
||||
</button>
|
||||
|
||||
opens (popover, anchored to the trigger, click-outside dismisses):
|
||||
┌──────────────────────────────────────────┐
|
||||
│ 🔍 [Suche…] │
|
||||
│ ☐ Alle / ☐ — Ohne Typ — │
|
||||
│ ─────────────────────────────────────────│
|
||||
│ Eingaben (UPC) │
|
||||
│ ☐ Statement of Defence │
|
||||
│ ☐ Reply to the Defence │
|
||||
│ ☐ Counterclaim for Revocation … │
|
||||
│ Entscheidungen │
|
||||
│ ☐ Decision on the merits … │
|
||||
│ Anordnungen │
|
||||
│ Gebühren │
|
||||
│ Eigene │
|
||||
│ ☐ Mein Mahnschriftsatz-Template │
|
||||
│ ─────────────────────────────────────────│
|
||||
│ [ Zurücksetzen ] [ Anwenden ] │
|
||||
└──────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
- **Single-select**, intersects with Status + Projekt (existing pattern).
|
||||
- Default `all`. `none` = `event_type_id IS NULL` filter.
|
||||
- State round-trips through `?event_type=` query param (matches existing `?status=` + `?project=` pattern in `client/deadlines.ts`).
|
||||
- Mobile: the existing `.akten-filter-row` already wraps responsively; no new CSS.
|
||||
**Behaviour:**
|
||||
- Default state: "Alle" toggle on, list disabled. Toggling any list item turns "Alle" off and ticks the row.
|
||||
- "— Ohne Typ —" is a special row separate from "Alle"; ticking it adds `event_type_id IS NULL` rows alongside whatever specific types are ticked.
|
||||
- Trigger label shows "Alle", "Ohne Typ", "Statement of Defence", or "3 Typen" depending on count.
|
||||
- Search box filters the list across `label_de` + `label_en`.
|
||||
- "Anwenden" (or click-outside) commits; "Zurücksetzen" returns to "Alle".
|
||||
- Mobile: popover becomes a full-width sheet from the bottom (reuses the existing `.modal-mobile-bottom` class if it exists, else a one-CSS-rule bottom sheet).
|
||||
|
||||
**Backend / query param:**
|
||||
- `?event_type=<uuid>,<uuid>,none` — comma-separated UUIDs and/or the literal `none` keyword. Empty / absent = "Alle".
|
||||
- Service layer parses to `(event_type_id IN (uuid1,uuid2,…) OR event_type_id IS NULL [if 'none' present])`, AND-intersected with the existing status / project predicates.
|
||||
- State persists in URL — bookmark + back-button preserve the filter.
|
||||
|
||||
### Add a `Typ` column to the deadlines table?
|
||||
|
||||
Yes — small column showing `label_de` (or `label_en` per current language). Column hides via `.akten-table--hide-status`-style toggle (t-paliad-073 pattern: hide when every visible row shares the same value). This costs ~10 lines.
|
||||
Yes — small column showing `label_de` (or `label_en` per current language). Column hides via `.akten-table--hide-status`-style toggle (t-paliad-073 pattern: hide when every visible row shares the same value).
|
||||
|
||||
### `/agenda`
|
||||
### `/agenda` — same multi-select
|
||||
|
||||
`/agenda` already has a Type chip row (deadlines vs. appointments). Adding an Event-Type filter there:
|
||||
|
||||
- **Yes**, but as a **second pill row below the existing one** — preserves the chip pattern. Each pill = one Event Type with `count > 0` in the current view; "Alle" default.
|
||||
- Implementation requires `AgendaService.List` to accept and enforce `event_type_id` filter on the deadlines side (appointments don't have event_types — they're orthogonal).
|
||||
- **Scope decision:** `/agenda` filter ships in the same task to keep parity. Without it the user can filter on `/deadlines` but not on the unified agenda — confusing.
|
||||
m's call (Q5): ship `/agenda` filter in the same task. Same `EventTypeMultiSelect` component, mounted as a second filter row below the existing Type chip row (deadlines/appointments). `AgendaService.List` accepts the same `?event_type=` param; appointments are unaffected (they have no event_type) — they're returned regardless when "appointments" is in the type-of-item filter.
|
||||
|
||||
## 5 · Permission model
|
||||
|
||||
@@ -312,22 +316,28 @@ Yes — small column showing `label_de` (or `label_en` per current language). Co
|
||||
|---|---|
|
||||
| Read firm-wide types | any authenticated user |
|
||||
| Read own private types | only the author |
|
||||
| Create private type (`is_firm_wide=false`, `created_by=self`) | any authenticated user |
|
||||
| Create firm-wide type (`is_firm_wide=true`) | `global_role='global_admin'` |
|
||||
| Edit own private type | only the author |
|
||||
| Edit any firm-wide type | global_admin |
|
||||
| Create private type (`is_firm_wide=false`) | any authenticated user (`created_by=self`) |
|
||||
| Create firm-wide type (`is_firm_wide=true`) | **any authenticated user** (m's Q6: lighter-weight onboarding, admin moderates after the fact) |
|
||||
| Edit own type (private or firm-wide) | the author |
|
||||
| Edit / archive any firm-wide type | `global_role='global_admin'` (moderation lever) |
|
||||
| Archive a type | same as edit |
|
||||
| Delete a type | never (set `archived_at` instead) |
|
||||
| "Promote" private type → firm-wide | global_admin can flip `is_firm_wide=true` on any private row (out-of-scope-of-this-task admin endpoint; flag as follow-up) |
|
||||
|
||||
Enforced at two layers:
|
||||
|
||||
- **RLS policies** (above) — the safety net.
|
||||
- **Service layer** — `EventTypeService.Create` rejects `is_firm_wide=true` for non-admins with 403, before the DB ever sees it. Mirrors existing `internal/services` pattern.
|
||||
- **RLS policies** (§2 above) — the safety net.
|
||||
- **Service layer** — `EventTypeService.Create` validates the slug shape and uniqueness before insert, rejects `created_by` mismatches with 400.
|
||||
|
||||
### Why this looser firm-wide-create policy
|
||||
|
||||
m's call. Two consequences worth naming:
|
||||
|
||||
1. **Drift risk:** users will create overlapping firm-wide types ("Klage", "Klageeinreichung", "Klage erheben"). Mitigation: search-prevent-duplicate in the add modal — if a fuzzy match (`%label_de%`) already exists firm-wide, surface "Existiert vermutlich schon: …" with a "Trotzdem anlegen" override. Reduces but doesn't eliminate.
|
||||
2. **Moderation backlog:** admins need a lightweight surface to scan + archive. Not in scope for this task; flag as follow-up *t-paliad-089: admin Event-Type moderation panel*.
|
||||
|
||||
### Why allow user-private types at all
|
||||
|
||||
Real usage: each lawyer has a couple of personal categories that nobody else needs ("Reminder for myself when X", "My standing template for Y"). Forcing every type through admin approval kills the feature's appeal. Private types stay out of others' pickers and don't pollute firm-wide search.
|
||||
Each lawyer has a couple of personal categories that nobody else needs ("Reminder for myself when X", "My standing template for Y"). Private types stay out of others' pickers; the user's own picker shows them under "Eigene".
|
||||
|
||||
## 6 · Backfill strategy
|
||||
|
||||
@@ -424,21 +434,23 @@ Tests live alongside each layer. Verify via `bun run build` + `go test ./...` +
|
||||
| Reuse `trigger_events` directly with a `created_by` column | Breaks the verbatim-import-for-diffability invariant; mixes calc state with user state; bigint vs uuid id space friction. |
|
||||
| Free-text `deadlines.event_type` column with self-distinct lookup | Typo-driven duplicates kill the filter UX; no metadata (category/jurisdiction); no privacy boundary. |
|
||||
| `paliad.submissions` as a sibling entity to deadlines | Forces migration of existing rows; duplicates fields (due_date, project_id, created_by); a submission *is* a deadline-bearing item. Defer until real submission-specific fields (file uploads, recipient party) are needed. |
|
||||
| Multi-select filter (intersect across multiple Event Types) | Not how Status/Projekt work today — would be the only multi-select on `/deadlines`. Keep parity; revisit if users ask. |
|
||||
| Multi-select filter (UNION across multiple Event Types) | **PICKED.** m's Q4 call — Event Types specifically benefits from multi-select (a user often wants "show me all my Replies, Rejoinders, and Defences"). Status/Projekt stay single-select; the asymmetry is intentional. |
|
||||
| Auto-create on free-text in picker | Generates noise; can't ask the user category/jurisdiction; wrong default permission. Keep the explicit "+ Neuen Typ" affordance. |
|
||||
| Hierarchical Event Types (parent type → sub-type) | Over-engineered for 40 seeds + handful of customs. Use `category` for the one level of grouping users care about. |
|
||||
|
||||
## 12 · Open questions for m before code starts
|
||||
## 12 · Open questions — RESOLVED 2026-04-30 12:23
|
||||
|
||||
These are the gate-blocking calls. Schema decisions in §1, §2, §4 should be greenlit before cronus or another coder picks this up.
|
||||
| # | Question | m's call |
|
||||
|---|---|---|
|
||||
| 1 | Concept boundary — broader (UPC + EPO + DPMA + DE + contract) or UPC-only first cut? | **A — broader from day one** |
|
||||
| 2 | Schema — new `paliad.event_types` table + FK? | **A — yes**, with the bridge `event_types.trigger_event_id → paliad.trigger_events(id)` populated only for seeded UPC rows. *m: "event_types should lead and later we can connect things to it"* |
|
||||
| 3 | Submissions — defer separate table? | **A — yes, defer**. *m: "those are the event types I mean, mainly"* |
|
||||
| 4 | Filter style — `<select>` matching existing pattern? | **A, but multi-select.** Custom listbox-panel multi-select component (see §4 above). Status/Projekt stay single-select. |
|
||||
| 5 | `/agenda` filter — same task? | **A — same task** |
|
||||
| 6 | Firm-wide type permission floor — global_admin only? | **B — any authenticated user can create firm-wide; admins moderate via archive after the fact.** Mitigation: duplicate-warning in the add modal. Follow-up: *t-paliad-089: admin moderation panel*. |
|
||||
| 7 | Seed list — ~40 curated rows in migration 030? | **A — yes**, spreadsheet on the implementation PR for review. |
|
||||
|
||||
1. **Concept boundary (§1):** Are you OK with Event Types being a **separate, broader taxonomy** that overlaps with but is not equal to `trigger_events`? Specifically: should non-UPC categories (EPO, DPMA, DE national, contract) be in scope from day one, or do you want a UPC-only first cut?
|
||||
2. **Schema (§2):** New table `paliad.event_types` with FK on `paliad.deadlines` — confirm this over the alternatives in §11.
|
||||
3. **Submissions (§3):** OK to defer a separate `paliad.submissions` table? Use `event_types.category='submission'` as the discriminator until a true Schriftsatz-Verwaltung surface is built?
|
||||
4. **Filter on `/deadlines`:** Existing pattern is `<select>` dropdown, not pills (the brief said pills). Confirm I should match the existing select pattern, NOT introduce pills here.
|
||||
5. **`/agenda` filter:** Ship in same task or split off? Parity argues for same task; conservatism (smaller PRs) argues for split.
|
||||
6. **Permission floor for firm-wide types:** Global-admin only? Or any authenticated user (and let admins moderate after the fact via archive)? The first feels safer; the second feels lighter-weight. Default in the design above is global-admin.
|
||||
7. **Initial seed list:** OK to ship ~40 curated rows (~25 UPC submissions + ~10 UPC decisions/orders + ~10 non-UPC) in migration 030, with the exact spreadsheet attached to the implementation PR for review?
|
||||
**Status:** all gate-blocking calls answered. Awaiting head's coder assignment. Inventor stays parked.
|
||||
|
||||
## 13 · Inventor recommendation on implementer
|
||||
|
||||
|
||||
Reference in New Issue
Block a user