Darstellung
04 – Projekte
Reifegrad: 🟢 produktiv · Schicht: projects + project_personen + project_team, @work7/domain (Project-Funktionen), src/app/projects/*, src/app/api/projects/*
Zweck
Bauvorhaben/Aufträge als zentrale Klammer: verbindet Kunde (Organisation/Person), internes Team, Termine, Zeiterfassung, Rapporte, Angebote und Rechnungen.
Datenmodell
projects:name,description,status(Defaultactive),type(DefaultNeubau),phase(DefaultPlanung),customer_id(→kontakte),project_manager_id(→users),project_number(UNIQUE), Termine (start_date,end_date,deadline), Kalkulation (estimated_hours,actual_hours,budget,actual_costs,completion_percentage0–100), Standort (address,postal_code,city,geo_coordinates),notes(JSONB).actual_hoursundcompletion_percentagesind abgeleitet (siehe unten) und in der UI schreibgeschützt.project_personen(Migration 036): beteiligte externe Personen/Kontakte,role∈ {lead,member},UNIQUE (project_id, person_id).project_team(Migration 037): internes Team aus eigenen Usern,role∈ {lead,member},UNIQUE (project_id, user_id).
Wichtig: Es gibt zwei Beteiligten-Dimensionen – externe Kontaktpersonen (Auftraggeber- Seite) und internes Personal. Sie sind bewusst getrennt modelliert.
Domain-Funktionen (@work7/domain)
listProjects, getProject, createProject, updateProject sowie Team-/Personen-Verknüpfung: linkUserToProject, listTeamForProject, unlinkUserFromProject, linkPersonToProject, listPersonsForProject, unlinkPersonFromProject. Helper ensureProjectExists schützt referenzierende Operationen.
API-Endpunkte
| Endpoint | Funktion |
|---|---|
GET/POST /api/projects | Liste / anlegen |
GET/PUT/DELETE /api/projects/[id] | Detail / ändern / löschen |
GET/POST/DELETE /api/projects/[id]/team | Internes Team (User) verwalten |
GET/POST/DELETE /api/projects/[id]/personen | Externe Kontaktpersonen verwalten |
Routenauflösung per Slug/ID über src/lib/load-project-by-route.ts.
UI
/projects(Liste),/projects/new(Anlage),/projects/[id](Detail mit Team, Personen, Terminen, Zeiten, Rapporten),/projects/[id]/edit.- Komponenten:
src/components/projects/*. AI-Unterstützung: Projektbeschreibung kann über/api/ai/summarize(Taskproject_description) generiert/zusammengefasst werden (siehe 12-ai-speech-realtime).
Verknüpfungen zu anderen Features
- Zeiterfassung: Projektzeiten in
time_logs(entry_type='project',project_id) – siehe 08-zeiterfassung. - Rapporte/Termine/Angebote/Rechnungen referenzieren
project_id(alleON DELETE SET NULL). - WhatsApp-Agent nutzt das „sticky active project" pro Session, um Rapport-/Foto-Eingaben einem Projekt zuzuordnen (siehe 11-whatsapp-agent).
Fortschritt (abgeleitet)
packages/domain/src/project-progress.ts ist der einzige Schreibpfad für actual_hours und completion_percentage. syncProjectProgress(companyId, projectId) rechnet beide neu und läuft bei jeder Zeitbuchung/-änderung (Web-API, Agent, Domain-Tool time_entries.project_add_hours) sowie nach Änderungen an estimated_hours, phase oder sub_phase in updateProject.
Regel für den Fertigstellungsgrad:
- Mit geschätzten Stunden:
actual_hours ÷ estimated_hours, gedeckelt auf 100 %. - Ohne Schätzung: Position der Projektphase in der Phasenliste des Betriebs (Trade-Config). Phase i von n belegt die Spanne
[i/n, (i+1)/n]; angezeigt wird deren Mitte — bei den 5 Standardphasen also Planung 10 %, Ausschreibung 30 %, Ausführung 50 %, Abnahme 70 %, Gewährleistung 90 %. Eine gesetzte Unterphase verfeinert innerhalb der Spanne (z. B. Ausführung 40–60 % → 43/50/57 % bei drei Unterphasen). - Weder Schätzung noch bekannte Phase: 0 %.
completion_percentage steht deshalb nicht mehr in der Allow-List von updateProject und ist im Web (FortschrittSectionFields, ProjectForm) sowie im WhatsApp-Agenten (project.preview_update) nicht editierbar. Wer den Wert bewegen will, pflegt geschätzte Stunden oder die Phase.
Einmaliger Backfill für Altprojekte (bleiben sonst auf dem DB-Default 0 %): npx tsx --env-file=.env.local scripts/backfill-project-progress.ts [--dry-run] [companyId]
Bekannte Befunde / offene Punkte
actual_costswird weiterhin manuell gepflegt — es gibt keine Kostenerfassung, die den Wert fortschreibt.- Geo-Koordinaten als
VARCHAR(100)(kein PostGIS).