Site Protocols
Construction-site protocols (Baustellenprotokolle) documented per building: walk-throughs, acceptance protocols, kick-off and construction meetings, defect inspections. Lives as the “Protokolle” tab on the building dashboard (I#1963, PR#1969), with PDF export added in I#1988 / PR#1994.
Data Model
site_protocols (backend/src/db/schema.ts): buildingId (FK → buildings, cascade), type (enum site_protocol_type), title, protocolDate (YYYY-MM-DD), body (JSONB), createdBy / finalizedBy (FK → contacts.id, per Human FK Rule), visibleToParticipants, finalizedAt. Indexed on building, type, date, and both contact FKs.
Protocol types: site_protocol, walk_through, group_appointment, acceptance_protocol, kick_off_meeting, defect_walk_through, construction_meeting, free_form — displayed via the protocolTypes.* keys in the dashboard i18n namespace.
The body JSONB (Zod-validated in the route layer) holds:
location,notes— free textattendees[]— name, company, role, optionalcontactId,signedflagtopics[]— title, description, decisionopenPoints[]— title, responsible, dueDate, status (open/done)photos[]—fileId(CAS reference, see Files) + caption
site_protocol_participant_visibility: per-contact portal whitelist (protocolId + contactId, unique pair, cascade both ways).
API
Mounted under /api/buildings, backed by SiteProtocolServiceImpl (backend/src/services/site-protocol-service.ts); building access checked via canAccessBuilding() with a leak-proof 404 surface; all actions audit-logged.
| Method | Path | Purpose |
|---|---|---|
| GET | /:buildingId/site-protocols | List protocols for a building |
| GET | /:buildingId/site-protocols/:id | Single protocol + visibility contact IDs |
| POST / PUT | /:buildingId/site-protocols(/:id) | Create / update |
| POST | /:buildingId/site-protocols/:id/finalize | Lock content (immutable afterwards) |
| GET | /:buildingId/site-protocols/:id/pdf | PDF export (attachment download) |
| DELETE | /:buildingId/site-protocols/:id | Delete (rejected once finalized) |
| PUT | /:buildingId/site-protocols/:id/visibility | Set per-contact portal visibility |
| GET | /:buildingId/portal-audience | Customer + partner roster for the visibility picker |
PDF Export
renderSiteProtocolPdf() in backend/src/services/site-protocol-pdf.ts renders an A4 document with pdf-lib: RENEWA letterhead, building address, type/date/title block, attendees/topics/open-points tables, embedded photos (capped height, never split across page breaks), legal footer. German default, English supported via a message-map shim (backend i18next not wired yet — same pattern as the participants export). Photos are fetched defensively through FileService.download(); missing blobs become placeholder notes instead of failing the export. The route streams the buffer as an attachment named from the protocol date.
Frontend
Tab protocols registered in DASHBOARD_TABS (frontend/src/pages/dashboard/dashboardService.ts), URL-synced via useTabState(); rendered from BuildingDetail.tsx.
ProtocolsSection.tsx(pages/dashboard/construction/) — thumbnail grid, type picker with color-coded icons, empty state, delete confirmation.ProtocolEditor.tsx+useProtocolEditorState()— composed ofProtocolSectionHeader,ProtocolAttendeesSection,ProtocolTopicsSection,ProtocolOpenPointsSection,ProtocolPhotosSection; finalize button, PDF download, portal-visibility dialog.protocolEditorService.ts— pure helpers (type ordering/colors,downloadProtocolPdf(), date formatting), following the standard component decomposition pattern.
Query hooks in frontend/src/lib/queries/siteProtocolQueries.ts: useProtocols, useProtocol, useCreateProtocolMutation, useUpdateProtocolMutation, useFinalizeProtocolMutation, useDeleteProtocolMutation, useSetProtocolVisibilityMutation, usePortalAudience. API layer: frontend/src/lib/api/site-protocols.ts.
Lifecycle and Visibility
Drafts are fully editable; finalizing sets finalizedAt and locks content (updates and deletes rejected) while visibility sharing stays editable. Two visibility mechanisms: the coarse visibleToParticipants flag and the per-contact whitelist feeding the customer/partner Portal. The portal audience aggregates building-wide customers (customer/owner/main-contact roles) and partners (contractor + referrer contacts from quotes), reusing DashboardService.getParticipants() for consistency with the Baubeteiligte tab. No mock data is seeded for site protocols.