Commit Graph

11 Commits

Author SHA1 Message Date
mAi
a4e2f3526d feat(checklists): t-paliad-225 Slice A backend — user-authored templates
m/paliad#61 Slice A. Introduces paliad.checklists (mig 114) as the
DB-backed companion to the static Go catalog. ChecklistCatalogService
unifies both sources at read time; ChecklistTemplateService handles
authoring CRUD + visibility toggle (private↔firm; Slice B opens
'shared' and 'global').

Schema (mig 114, idempotent):
- paliad.checklists (uuid, slug UNIQUE, owner_id FK, title/description
  /regime/court/reference/deadline/lang, body jsonb, visibility CHECK
  ('private','shared','firm','global'), promoted_at/_by, timestamps)
- paliad.can_see_checklist(uuid, uuid) STABLE SECURITY DEFINER —
  owner OR firm/global. Slice B extends with the explicit-share branch.
- RLS: select via can_see_checklist; insert owner=self; update/delete
  owner OR global_admin
- ALTER paliad.checklist_instances ADD COLUMN template_snapshot jsonb
  (snapshot semantics so per-Akte instances stay decoupled from
  subsequent template edits)

Services:
- ChecklistCatalogService — ListVisible, Find, SnapshotBody, IsStaticSlug.
  Reapplies visibility application-side (service-role bypasses RLS, per
  visibility.go pattern). Static-slug map computed once at boot for
  collision detection.
- ChecklistTemplateService — Create (auto-generates u-<slug>-<hex> with
  retry), Update (changed_fields[] in audit), SetVisibility, Delete,
  ListOwnedBy, GetBySlug. Owner-or-global_admin gate.
- SystemAuditLogService.WriteChecklistEvent — thin helper writing into
  paliad.system_audit_log with scope='org'.
- ChecklistInstanceService.Create now captures template_snapshot via
  the catalog; GetByID returns it inline so the frontend can render
  the captured body even after the upstream template is mutated.

Endpoints (all owner-gated where mutating):
- GET    /api/checklists                 — merged catalog (static + DB visible)
- GET    /api/checklists/{slug}          — single template; static-first lookup
- GET    /api/checklists/templates/mine  — caller's authored templates
- POST   /api/checklists/templates       — create
- PATCH  /api/checklists/templates/{slug}            — edit
- PATCH  /api/checklists/templates/{slug}/visibility — private↔firm
- DELETE /api/checklists/templates/{slug}            — delete
- GET    /checklists/new, /checklists/{slug}/edit    — author wizard pages

Tests: pure-helper unit tests cover slugifyTitle (umlaut → ae/oe/ue/ss
normalisation + clamp), regime/lang/visibility validation, body-shape
enforcement, static-slug detection, predicate shape, clamp.
2026-05-20 15:24:06 +02:00
m
df321acb63 feat(t-paliad-097): clickable checklist references + Vorhandene Instanzen tab
Two related checklist UX gaps:

1. Checklist events in a project's Verlauf tab were unclickable — and
   nothing in the project_events row carried the originating instance ID.
   Add an `insertProjectEventWithMeta` helper, write
   {"checklist_instance_id": <uuid>} as project_events.metadata for
   checklist_created / _renamed / _linked / _unlinked / _reset (skipped
   for _deleted — instance is gone). Surface metadata on
   /api/projects/{id}/events and on dashboard recent_activity. The
   Verlauf renderer wraps the title in <a href="/checklists/instances/{id}">
   when metadata.checklist_instance_id is present, and the dashboard's
   activity feed deep-links the project ref to the instance directly for
   checklist_* events. Existing rows (metadata `{}`) stay non-clickable —
   no migration backfill needed.

2. /checklists previously demanded a template pick before any existing
   instance was reachable. Add a tab nav (Vorlagen / Vorhandene Instanzen)
   using the existing entity-tab pattern. New endpoint
   GET /api/checklist-instances and ChecklistInstanceService.ListAllVisible
   return every visible instance across templates + projects, joined with
   project ref/title and sorted by created_at DESC. Rows show template,
   instance name (linked), project link (or "Persönlich"), progress bar,
   and created date. URL state (?tab=instances) keeps the active tab
   shareable. EN + DE i18n covered for tab labels and column headers.

Also adds event.title.checklist_* localizations for the Verlauf header
that translateEvent looks up.
2026-05-01 09:48:25 +02:00
m
460736ad1e refactor(t-paliad-092): rename Go module path patholo → paliad
F-6 from t-paliad-074 architecture audit. The Gitea repo was renamed
m/patholo → mAi/paliad → m/paliad, but go.mod still declared
`mgit.msbls.de/m/patholo` and every internal import echoed the
pre-rebrand name.

Sweep:
- go.mod: module path → mgit.msbls.de/m/paliad
- All *.go files: imports rewritten via sed
- README.md, docs/design-kanzlai-integration.md: mAi/paliad → m/paliad
- Frontend issue-reference comments (mAi/paliad#N → m/paliad#N) in
  i18n.ts, theme.ts, sidebar.ts, app.ts, Sidebar.tsx, PWAHead.tsx,
  global.css

Verified: go build/vet/test ./... clean, bun run build clean,
no remaining mgit.msbls.de/m/patholo or mAi/paliad references
outside docs that intentionally describe the rename history.
2026-04-30 16:46:31 +02:00
m
ce3227c1c0 refactor(t-paliad-080): service-layer naming sweep — Notiz/Termin/Frist/Projekt/Partei → Note/Appointment/Deadline/Project/Party
Mechanical rename across 8 service files plus their handler call sites and
two related helpers. The English types existed already; what changed are the
input-struct names, helper functions, list/create method suffixes, and
parameter names so they no longer mix English types with German parameter
names.

Renames cover:
- CreateNotizInput/UpdateNotizInput → CreateNoteInput/UpdateNoteInput,
  notizColumns/notizSelect → noteColumns/noteSelect, ListForProjekt/Frist/
  Termin → ListForProject/Deadline/Appointment, CreateForProjekt/Frist/
  Termin → CreateForProject/Deadline/Appointment, fristProjectID →
  deadlineProjectID
- CreateTerminInput/UpdateTerminInput → CreateAppointmentInput/
  UpdateAppointmentInput, terminColumns → appointmentColumns, ListForProjekt
  → ListForProject; parameter renames terminID → appointmentID, projektID
  → projectID
- CreateFristInput/UpdateFristInput → CreateDeadlineInput/
  UpdateDeadlineInput, fristColumns → deadlineColumns, ListForProjekt →
  ListForProject, isValidFristStatus → isValidDeadlineStatus; parameter
  renames fristID → deadlineID, projektID → projectID
- CreateProjektInput/UpdateProjektInput → CreateProjectInput/
  UpdateProjectInput, projektColumns → projectColumns,
  validateProjektStatus → validateProjectStatus, ProjektRole comment →
  ProjectRole
- CreateParteiInput → CreatePartyInput, parteiColumns → partyColumns,
  ListForProjekt → ListForProject; parameter renames parteiID → partyID
- OnTerminCreated/Updated/Deleted → OnAppointmentCreated/Updated/Deleted on
  the AppointmentCalDAVPusher interface and its CalDAVService impl
- formatTermin → formatAppointment in caldav_ical
- ListForProjekt → ListForProject, listWithProjekt → listWithProject,
  checklistInstanceWithProjektSelect → checklistInstanceWithProjectSelect,
  ClearProjekt → ClearProject (JSON tag clear_projekt unchanged — wire
  format)
- insertProjectEvent helper parameter projektID → projectID, error message
  "insert projekt_event" → "insert project_event"
- TeamService AddMember/RemoveMember/ListDirectMembers/ListEffectiveMembers
  parameter projektID → projectID; matching handler renames
- Frontend doc-comments referencing CreateProjektInput/UpdateProjektInput
  updated to CreateProjectInput/UpdateProjectInput

JSON wire tags (clear_projekt, etc.) and German user-facing strings
(glossary entries, search.go labels, email templates, changelog,
Terminsgebuehr, Fristenrechner product name) are intentionally untouched.

API contract unchanged. go build/vet/test ./... clean. Frontend bun build
clean.
2026-04-30 04:39:23 +02:00
m
abd99980fc fix(t-paliad-058): honor global_admin in visibilityPredicate
Mirror paliad.can_see_project's global-admin shortcut at the application
layer. The in-Go predicate previously relied on callers passing
user.GlobalRole as a separate :role / $roleArg parameter — the positional
variant compared against the literal 'admin' instead of 'global_admin',
so any global_admin without team membership got 404 from
/api/projects/{id} (and the other positional callsites: ListAncestors,
BuildTree, GetTree, deadline counts).

Fold the gate into a Go helper that resolves global_admin via EXISTS on
paliad.users, keyed only by userID. Callers no longer pass role, which
removes the foot-gun entirely. Drops the unused
visibilityPredicatePlaceholder dead helper.

Adds a regression test (visibility_test.go) covering global_admin +
standard user against GetByID and BuildTree without project_teams rows.
2026-04-27 16:35:55 +02:00
m
b34500ad31 feat(t-paliad-051): split paliad.users.role into job_title + global_role
Conflation: paliad.users.role was simultaneously job title (display only)
and global permission ('role=admin' checks across Go/SQL/JS). m wanted
to set his real job title ('Counsel Knowledge Lawyer') without losing
admin access — the t-paliad-050 admin-team UI even rejected role='admin'
on edit, so any UI-driven update silently demoted m.

Per m's three-axis principle ("firm roles are not project roles are not
tool roles"), this lands TWO orthogonal columns:

* paliad.users.job_title — free text, NULL allowed, display only.
  NEVER gates anything in code or SQL.
* paliad.users.global_role — CHECK ('standard'|'global_admin'),
  default 'standard'. The only thing that gates ops.

Migration 023:
* Drops NOT NULL + 'associate' default off the legacy role column
* Promotes role='admin' rows to global_role='global_admin'; clears
  their role text; sets m's job_title='Counsel Knowledge Lawyer'
* Renames role -> job_title with CHECK (job_title IS NULL OR <> '')
* Replaces can_see_project body with global_role='global_admin'
* CASCADE-rebuilds every RLS policy under canonical English names —
  with the historic u.role IN ('partner','admin') gates simplified
  to u.global_role='global_admin' only (job_title NEVER gates)

Code surface:
* internal/models/models.go: User.Role -> User.JobTitle (*string) +
  User.GlobalRole (string)
* internal/services/user_service.go: bootstrap (first row promoted to
  global_admin via pg_advisory_xact_lock(7346298141), unchanged constant);
  UpdateProfile drops role, accepts job_title only; AdminUpdateUser adds
  global_role with last-admin demotion guard (ErrLastGlobalAdmin);
  IsAdmin reads global_role
* Other services (dashboard/agenda/appointment/project/deadline/
  department/party/note/checklist_instance): pass user.GlobalRole into
  visibility predicates; partner-or-admin gates simplified to
  global_admin only
* Handlers: drop now-impossible ErrAdminBootstrapOnly cases;
  admin_users handles ErrLastGlobalAdmin -> 409
* department_service: SQL u.role -> u.job_title, DepartmentMember.Role
  -> JobTitle (*string)

Frontend:
* /api/me + Me interfaces ship {job_title, global_role}
* Onboarding form: 'Berufsbezeichnung / Job title' (job_title)
* Settings + admin-team forms: same renames + i18n updates
* Admin-team: new 'Berechtigung / Permission' column with
  'Standard'|'Global Admin' badge + dropdown editor; last-admin
  demotion guard at the UI layer
* Sidebar admin-section reveal: me.global_role==='global_admin'
* deadlines/deadlines-detail/projects-detail/notes: partner-as-permission
  gates dropped, only global_admin grants those operations

Tests:
* user_service_test: bootstrap promotes first user to global_admin,
  subsequent default to standard; AdminUpdateUser refuses to demote
  the last global_admin; IsAdmin reads global_role

Migration applied to ydb 2026-04-27. Live state verified:
* m: job_title='Counsel Knowledge Lawyer', global_role='global_admin'
* tester: job_title=NULL, global_role='global_admin'
* 29 stub colleagues: job_title='associate', global_role='standard'
2026-04-27 14:59:03 +02:00
m
70c3f08668 fix(projects-detail, services): empty-list endpoints returned JSON null → tab content blank
m reported /projects/{id} loaded the chrome and tabs but every panel was
empty even with deadlines/appointments/team rows that should render.
Console error: "Cannot read properties of null (reading 'length')" at
projects-detail.js — the Project Detail page expects every list endpoint
to return [] but at least two were returning literal JSON null.

Reproduced via the in-page fetch console:
  /api/projects/{id}/parties   → 200, body: "null"
  /api/projects/{id}/children  → 200, body: "null"
  /api/projects/{id}/deadlines → 200, body: "[…]"   (had data, fine)
  /api/projects/{id}/team      → 200, body: "[…]"   (had data, fine)

Root cause: every list service in internal/services declared its result
as `var rows []models.X` and returned that to the handler, which
encoding/json marshals as `null` when the SELECT returns zero rows
(nil slice, not empty slice). Most endpoints happen to have data so
the bug stayed dormant until t-paliad-038 hit /projects/{id} where
parties + children are commonly empty.

Fix at the source — every list service that JSON-marshals to a client
now initialises `rows := []models.X{}` so the encoder produces `[]`:

  party_service        ListForProjekt
  project_service      List, ListAncestors, BuildTree, GetTree
                       (ListChildren goes through List)
  deadline_service     List + ListForProjekt
  appointment_service  List + ListForProjekt
  note_service         ListForProjekt
  checklist_instance_service  ListForProjekt
  team_service         List
  department_service   List + ListMembers + ListWithMembers

caldav_service was deliberately left alone — its lists are admin-only
debug surfaces, not user-facing tab fillers, and changing them would
mix scopes.

Belt-and-braces on the client too — projects-detail.ts now coerces every
`await resp.json()` for an array endpoint with `?? []` so a future
service regression can't crash the page.

Verified: go build/vet/test clean, bun run build clean.
2026-04-26 01:44:09 +02:00
m
3111c7440a fix(polish): i18n leaks, untranslated labels, /api/departments 500, 404 chrome (t-paliad-037)
Four bugs from tests/smoke-auth-2026-04-25.md.

Bug 4 — Dashboard activity log leaked raw i18n keys. Root cause was a mix
of three issues:
  - Go services wrote German event_types (frist_created, termin_*,
    projekt_*, notiz_created, checkliste_*) — no matching i18n key.
  - i18n.ts only had keys for legacy `akte_*` types, none for what was
    actually being written.
  - The dashboard renderer always rendered `e.title` (a static label like
    "Project angelegt") as a trailing detail, duplicating the action verb.
    Old `akte_created` rows had English titles ("Akte created") that
    bled into German output.

Switched all event_type writes to English (deadline_*, appointment_*,
project_*, note_created, checklist_*, deadlines_imported). Moved dynamic
text out of `title` into `description` for status_changed and
deadlines_imported so the static label/description split is consistent.
Added i18n keys for both new English types AND legacy German types so
historical project_events rows render cleanly. Dashboard now prefers
description over title; falls back to title only for events with no
i18n match (defensive for any unknown legacy kinds).

Bug 5 — /deadlines and /appointments matter-filter dropdowns showed raw
keys `fristen.filter.project.all` / `termine.filter.project.all`. The
client TS referenced English-prefix keys that didn't exist; the existing
keys use `fristen.filter.akte.*` / `termine.filter.akte.*`. Updated the
client refs to match the existing keys (kept i18n key namespace stable
to avoid touching every other reference).

Bug 6 — /api/departments?include=members returned 500. Reproduced via
curl: ListWithMembers (and ListMembers) used `LEFT JOIN paliad.users` on
a member.user_id that FKs auth.users — pre-onboarding members produced
NULL u.email/display_name/office/role, which sqlx can't scan into the
non-pointer string fields. Switched both to INNER JOIN; unonboarded
members are skipped (correct UX — without a profile there's nothing to
render anyway).

Bug 9 — Bare `404 page not found` on unknown auth-gated paths
(/whatsnew, /search, /settings/notifications, etc). Added a chromed 404
page (frontend/src/notfound.tsx) with sidebar + friendly card + "back
to dashboard" CTA, plus a catch-all handler on the protected mux that
serves it with HTTP 404 (and JSON 404 for /api/* misses). Anonymous
visitors keep being redirected to /login by the auth middleware before
the catch-all runs, so no separate marketing-shell variant needed.

Verification:
- go build ./... + go vet ./... + go test ./... clean
- bun run build clean (notfound.html + notfound.js produced)
- Visual checks pending after deploy
2026-04-26 00:36:33 +02:00
m
3faec6c526 refactor(rename): German→English for backend (tables, types, services, handler files)
t-paliad-025 — Phase 1: backend rename.

Migrations 018+019 rewritten from scratch with English table/column
names throughout. Since v2 schema (018/019) has never been applied to
youpc prod DB, this is a clean replacement — not an ALTER RENAME chain.
Pre-existing German tables (parteien, fristen, termine, dokumente,
akten_events, notizen) are renamed inline in 018 via ALTER TABLE … RENAME
TO alongside the akte_id → project_id column rewrite.

Renames applied:
  projekte            → projects
  projekt_teams       → project_teams
  projekt_events      → project_events (via akten_events → project_events)
  fristen             → deadlines
  termine             → appointments
  parteien            → parties
  notizen             → notes
  dezernate           → departments
  dezernat_mitglieder → department_members
  dokumente           → documents
  can_see_projekt     → can_see_project
  notiz_is_visible    → note_is_visible
  akte_id  / frist_id / termin_id / akten_event_id → project_id /
    deadline_id / appointment_id / project_event_id
  termin_type → appointment_type

Go types + services renamed:
  Projekt / ProjektService / ProjektEvent / ProjektTeamMember
  Frist / FristService / FristWithProjekt
  Termin / TerminService / TerminWithProjekt / TerminType
  Notiz / NotizService / ChecklistInstanceWithProjekt
  Dezernat / DezernatService / DezernatMitglied
  Partei / Parteien / ParteienService

Files renamed (git mv):
  internal/services/{projekt,frist,termin,notiz,dezernat,parteien}_service.go
    → {project,deadline,appointment,note,department,party}_service.go
  internal/handlers/{projekte,fristen,fristen_pages,termine,termine_pages,
    notizen,dezernate,akten_pages,gerichte,glossar,checklisten}.go
    → {projects,deadlines,deadlines_pages,appointments,appointments_pages,
       notes,departments,projects_pages,courts,glossary,checklists}.go
  internal/checklisten/ → internal/checklists/
  internal/db/migrations/018_projekte_v2.* → 018_projects_v2.*
  internal/db/migrations/019_seed_dezernate_from_user_text.*
    → 019_seed_departments_from_user_text.*

User-facing i18n strings (DE/EN labels) stay untouched. Product names
Fristenrechner / Kostenrechner / Gebührentabellen stay German.

Build + vet + tests clean.
2026-04-20 17:35:38 +02:00
m
9aa8037193 refactor: services — Projekt, Team, Dezernat services (WIP Phase 2)
Models: Akte → Projekt (tree type + parent_id + path + client/matter numbers
+ netDocuments URL + type-specific client/patent/case columns). AkteEvent →
ProjektEvent. FristWithAkte → FristWithProjekt. TerminWithAkte → TerminWithProjekt.
Notiz.AkteID → ProjektID. ChecklistInstance.AkteID → ProjektID. Partei.AkteID →
ProjektID. User adds AdditionalOffices pq.StringArray.

Services:
- NEW projekt_service.go replaces akte_service.go. Adds tree ops: List/GetByID/
  ListChildren/ListAncestors/GetTree. Create auto-adds creator to projekt_teams
  role=lead in same tx. ResolveClientNumber walks path for inheritance.
  Visibility helpers (visibilityPredicate / Positional / Placeholder) centralise
  team-based access check: admin OR any ancestor/direct projekt_teams row.
- NEW team_service.go — AddMember/RemoveMember/ListDirectMembers/
  ListEffectiveMembers (unions direct + inherited via path, dedup by user;
  direct wins)/IsEffectiveMember. Inherited=true set at read time only.
- NEW dezernat_service.go — admin-gated CRUD + member add/remove + user
  membership lookup for settings page.
- frist_service.go → projekt_id everywhere, uses visibilityPredicate. ListFilter.
  AkteID → ProjektID.
- termin_service.go → projekt_id everywhere. CalDAV log reads projekt_events.
- notiz_service.go → projekt_id polymorphic branch; eventProjektID() looks at
  projekt_events; akten_event_id column kept (FK now resolves to projekt_events).
- parteien_service.go → projekt_id.
- checklist_instance_service.go → projekt_id with ClearProjekt flag.
- dashboard_service.go → rewrites all four queries against projekte +
  projekt_events + projekt_teams. Matter/Upcoming/Activity surfaces use
  ProjektID/ProjektTitle/ProjektRef.
- reminder_service.go → joins paliad.projekte, aliases a.reference AS
  akte_aktenzeichen for template compat.

Handlers/tests still reference old API — Phase 2 completion requires handler
rewrite (next commit). Build currently broken in internal/handlers.
2026-04-20 14:46:59 +02:00
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