Commit Graph

9 Commits

Author SHA1 Message Date
m
efaa7787af Merge remote-tracking branch 'origin/main' into mai/kepler/inventor-profession-vs 2026-05-07 22:00:26 +02:00
m
e6937d232e feat(t-paliad-148) commit 3/6: TeamService + UserService + Models + Handlers — write profession + responsibility
Models:
- ProjectTeamMember.Responsibility (new) + .Role (kept as deprecated shadow). JSON exposes both during the deprecation window.
- ProjectTeamMemberWithUser.UserProfession — populated by reads so the team-tab UI can render the firm-tier badge.
- User.Profession (*string) — structured firm-tier driving the approval ladder. Distinct from JobTitle (display) and GlobalRole (tool admin).

TeamService:
- AddMember signature kept as (callerID, projectID, userID, responsibility) — third arg renamed conceptually. Accepts the new responsibility enum and writes both legacy `role` (via legacyRoleFromResponsibility helper) and `responsibility` to keep the deprecated shadow consistent.
- ListDirectMembers + ListEffectiveMembers SELECT both `pt.role`, `pt.responsibility`, and `u.profession`. ORDER BY switches from pt.role to pt.responsibility.
- legacy isValidRole removed (unused after switch to IsValidResponsibility).

UserService:
- CreateUserInput + AdminCreateInput + AdminUpdateInput accept Profession. Self-service onboarding defaults to 'associate' when empty. AdminCreate likewise. AdminUpdate empty-string clears to NULL (external collaborator). Invalid values rejected with ErrInvalidInput.
- INSERT statements write the new column on both Create paths.

ProjectService.Create:
- Auto-add-creator INSERT writes responsibility='lead' alongside legacy role='lead'.

Handlers:
- POST /api/projects/{id}/team accepts `responsibility` (preferred) and falls back to legacy `role` for one release while frontend migrates.

Build + vet clean. Pure-Go tests pass.
2026-05-07 21:48:38 +02:00
m
52ee319fd8 feat(t-paliad-147): bulk team email — send to filtered selection from /team page
Implements issue #7. Adds an "E-Mail an Auswahl" button on /team that sends
personalised emails to a filter-narrowed subset of the team. Each recipient
gets their own envelope (per-recipient privacy, no shared To: list); From
stays on the SMTP infrastructure address with Reply-To set to the human
sender so replies route correctly without forging DKIM/SPF.

Backend
- Migration 057: paliad.email_broadcasts (subject, body, sender_id,
  template_key, recipient_filter jsonb, recipient_user_ids uuid[],
  send_report jsonb, sent_at). RLS: senders read own rows, global_admin
  reads all; inserts must self-attribute. No CHECK-constraint extension to
  partner_unit_events — broadcasts get their own table per the lock.
- BroadcastService (internal/services/broadcast_service.go): validates
  subject/body/recipient cap (100), enforces project_lead-OR-global_admin,
  persists audit row, dispatches via 5-deep goroutine pool with 15s
  per-send timeout. Send report (sent/failed counts + per-recipient errors)
  is captured back into email_broadcasts.send_report.
- markdown.go: minimal Markdown→safe HTML renderer (paragraphs, **bold**,
  *italic*, `code`, [text](url), bullet lists). Inputs are HTML-escaped
  first; only whitelisted tags re-emitted. Script tags and javascript:
  URLs can't slip through.
- Placeholder substitution: {{name}}, {{first_name}},
  {{role_on_project}} (whitespace tolerated). Unknown {{...}} tokens pass
  through unchanged.
- mail_service.go: buildMIMEWithReplyTo helper layers a Reply-To header
  on top of the existing multipart/alternative envelope.
- TeamService.ListMembershipsIndex: visibility-gated user→project_ids
  index. Powers the /team project multi-select filter without N round
  trips per project.
- Handlers: POST /api/team/broadcast (gateOnboarded; service enforces
  authority), GET /api/team/memberships, GET /api/admin/broadcasts (list),
  GET /api/admin/broadcasts/{id} (detail), GET /admin/broadcasts (page).
  /admin/broadcasts is gateOnboarded (not adminGate) so leads can see
  their own sends; the service applies the per-row visibility filter.

Frontend
- /team gains a project multi-select chip dropdown (visible projects
  loaded from /api/projects, intersected against the memberships index)
  alongside the existing office and role filters.
- "E-Mail an Auswahl (N)" button appears only when canBroadcast() is
  true (global_admin always; non-admin needs lead-ship on selected
  projects, or at least one project when no filter is set). Server still
  re-checks per send.
- Compose modal (broadcast.ts): subject + body textarea + optional
  template dropdown (loads existing email templates and strips Go-template
  directives) + recipient preview (first 5 + expand) + send. Hard-blocks
  empty subject/body and N=0. Shows per-send report on success.
- /admin/broadcasts viewer: read-only list with click-row-to-expand
  detail (subject, body, recipient list, send_report counts).

Tests
- broadcast_service_test.go: placeholder substitution table-driven,
  Markdown safe-render incl. XSS guards (<script>, javascript: URLs),
  validation cases (empty subject/body, recipient cap, invalid email),
  signature rendering DE/EN.
- broadcast_service_live_test.go: end-to-end Send + List + Get + visibility
  rules (lead can send on own project, member cannot, admin sees all,
  member can't read lead's row). Skips when TEST_DATABASE_URL is unset.

i18n: 60 new keys × 2 langs (broadcast modal labels, error messages,
recipient summary, /admin/broadcasts viewer, common.close/loading/forbidden/
load_error).
2026-05-07 20:58:57 +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
31db66e3b7 refactor(t-paliad-076): consolidate visibility predicate — 6 dashboard/agenda sites use helper
F-2 from t-paliad-074 audit. The inlined visibility predicate had drifted
back into 6 hot-path SQL sites despite the central helper extracted in
t-paliad-058. Consolidating now so future visibility changes (e.g.
Chinese-wall in design v2 §8) only need one edit.

**Sites converted (6):**
- dashboard_service.go:158, 214, 244, 274
- agenda_service.go:138, 204

All six replace `$N = 'global_admin' OR EXISTS (path-walk)` with the
existing `visibilityPredicatePositional("p", 1)` helper. The helper
resolves global_admin via EXISTS on paliad.users — the role string no
longer flows through positional args, removing one foot-gun (typo'd
literal mismatched against bound role) entirely. Equivalence verified
on the live youpc DB:

    tester@hlc.de (global_admin, 1 team membership):
      old predicate count = 11   new predicate count = 11
    standard user (no team):
      old predicate count =  0   new predicate count =  0

**No new helper variant added.** The audit suggested
`visibilityPredicateLateral`, but the existing positional helper drops
into the dashboard/agenda WHERE clauses unchanged — adding a redundant
variant would be technical debt. dashboard/agenda do not use LATERAL
JOIN; they use plain WHERE EXISTS in (sub-)SELECT context, which is
already what visibilityPredicatePositional emits.

**Other 4 sites flagged by audit — left intentionally:**

- reminder_service.go:312, 325 are role-restricted (`pt.role = 'lead'`)
  membership checks, NOT visibility predicates. Adding a global_admin
  shortcut to the lead branch would over-include rows: every global
  admin would receive every project's lead-targeted reminder, even with
  the `own.escalation_contact_id` override that exists precisely to
  avoid that. global_admin already has its own dedicated branch in the
  query (`$3 = TRUE AND own.escalation_contact_id IS NULL` at line 328).

- deadline_service.go:422 (`assertCanAdminProject`) is role-restricted
  (`pt.role IN ('admin', 'lead')`) and already short-circuits global_admin
  at the Go level before the SQL runs (line 413). Both halves correct;
  no change needed.

- team_service.go:162 (`IsEffectiveMember`) was dead code with no callers
  in the entire repo. "Is this user a structural team member?" and
  "can this user see this project?" are different questions; adding a
  global_admin shortcut would have conflated them. Deleted instead.

**Test:** new TestVisibilityPredicate_DashboardAgendaForGlobalAdmin in
visibility_test.go seeds a project + deadline + appointment + activity
event with project_teams empty, then asserts a global_admin sees all
four on /dashboard and /agenda while a standard user sees none. Skips
when TEST_DATABASE_URL is unset (matching the existing live-DB tests).

**Pre-existing finding (separate concern):** the live-DB test gate is
currently blocked locally by a stale `public.paliad_schema_migrations`
(version=2, dirty=t) left over from before the schema-pinned tracker
landed. Authoritative `paliad.paliad_schema_migrations` is at version
27, dirty=f. Out of scope for this task; should be filed as cleanup.
2026-04-30 03:48:49 +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
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