# ProjectTask Modul - Dokumentation **Erstellt am:** 14. November 2025 **Status:** Produktionsbereit ✅ ## Überblick Das ProjectTask-Modul ermöglicht die Verwaltung von Tätigkeiten (Arbeitspakete) im myCRM-System. Tätigkeiten können projektbezogen oder projektunabhängig sein und verfügen über Budget-Tracking, Stundenkontingente und flexible Preismodelle. --- ## Features ### ✅ Kerngfunktionen - **Projektbezogene & projektunabhängige Tätigkeiten** - Tätigkeiten können einem Projekt zugeordnet werden (optional) - Projektunabhängige Tätigkeiten nur für Admins - **Budget & Stundenkontingent** - Budget-Tracking pro Tätigkeit - Stundenkontingent definieren - Aggregierte Übersichten auf Projekt-Ebene - **Flexibles Preismodell** - Stundensatz (€/h) - Gesamtpreis (Festpreis) - Beide Varianten kombinierbar - Automatische Berechnung im Frontend - **Berechtigungssystem** - Granulare Zugriffskontrolle via Symfony Security Voters - Admin- und Team-basierte Berechtigungen - Projektteammitglieder können Tätigkeiten verwalten - **Frontend-Integration** - Moderne Vue.js-Komponente mit PrimeVue - CrudDataTable mit Filter, Export und Spaltenkonfiguration - Integration im Dashboard und Projekt-Detailansicht - Responsive Design --- ## Backend-Architektur ### 1. Entity: `ProjectTask` **Pfad:** `src/Entity/ProjectTask.php` #### Felder | Feld | Typ | Beschreibung | |------|-----|--------------| | `id` | int | Primärschlüssel | | `name` | string(255) | Name der Tätigkeit (Pflichtfeld) | | `description` | text | Beschreibung (optional) | | `project` | ManyToOne | Verknüpfung zu Project (optional, CASCADE delete) | | `budget` | decimal(10,2) | Budget in Euro | | `hourContingent` | decimal(8,2) | Verfügbare Stunden | | `hourlyRate` | decimal(8,2) | Stundensatz in Euro | | `totalPrice` | decimal(10,2) | Gesamtpreis in Euro | | `createdAt` | DateTimeImmutable | Erstellungsdatum | | `updatedAt` | DateTimeImmutable | Letzte Änderung | #### API Platform Konfiguration ```php #[ApiResource( operations: [ new GetCollection(security: "is_granted('VIEW', 'project_tasks')"), new Get(security: "is_granted('VIEW', object)"), new Post(security: "is_granted('CREATE', 'project_tasks')"), new Put(security: "is_granted('EDIT', object)"), new Delete(security: "is_granted('DELETE', object)") ], paginationClientItemsPerPage: true, paginationItemsPerPage: 30, normalizationContext: ['groups' => ['project_task:read']], denormalizationContext: ['groups' => ['project_task:write']], order: ['createdAt' => 'DESC'] )] ``` #### API-Filter - **SearchFilter:** `name`, `project.name` (partial search) - **DateFilter:** `createdAt`, `updatedAt` - **ProjectTaskProjectFilter:** Custom Filter für Projekt-Existenz (`hasProject=true/false`) --- ### 2. Repository: `ProjectTaskRepository` **Pfad:** `src/Repository/ProjectTaskRepository.php` #### Methoden | Methode | Beschreibung | |---------|--------------| | `findByProject(Project $project)` | Tasks eines bestimmten Projekts | | `findWithoutProject()` | Projektunabhängige Tasks | | `findUserTasks(User $user)` | Tasks mit Benutzerzugriff | | `getTotalBudgetByProject(Project $project)` | Gesamtbudget-Berechnung | | `getTotalHourContingentByProject(Project $project)` | Gesamtstunden-Berechnung | **Beispiel:** ```php // Alle Tasks eines Projekts laden $tasks = $projectTaskRepository->findByProject($project); // Gesamtbudget berechnen $totalBudget = $projectTaskRepository->getTotalBudgetByProject($project); ``` --- ### 3. Security: `ProjectTaskVoter` **Pfad:** `src/Security/Voter/ProjectTaskVoter.php` #### Berechtigungslogik | Aktion | Berechtigung | |--------|--------------| | **VIEW** | Admin ODER Projektteammitglied | | **EDIT** | Admin ODER Projektteammitglied | | **DELETE** | Admin ODER Projektbesitzer | | **CREATE** | Alle authentifizierten User (mit Prüfung) | #### Besonderheiten - **Projektunabhängige Tasks:** Nur Admins haben Zugriff - **Projektbezogene Tasks:** Zugriff über Projektmitgliedschaft - Prüfung erfolgt über `Project::hasAccess(User $user)` Methode **Beispiel:** ```php // Im Controller $this->denyAccessUnlessGranted('VIEW', $projectTask); $this->denyAccessUnlessGranted('EDIT', $projectTask); $this->denyAccessUnlessGranted('DELETE', $projectTask); ``` --- ### 4. Event Listener: `ProjectTaskSecurityListener` **Pfad:** `src/EventListener/ProjectTaskSecurityListener.php` #### Funktion Validiert Berechtigungen beim **Erstellen** neuer Tasks (Doctrine `prePersist` Event): - **Ohne Projekt:** Nur Admins dürfen erstellen - **Mit Projekt:** Admin oder Projektteammitglied **Beispiel-Exception:** ``` AccessDeniedException: "Nur Administratoren können projektunabhängige Tätigkeiten erstellen." ``` --- ### 5. Custom Filter: `ProjectTaskProjectFilter` **Pfad:** `src/Filter/ProjectTaskProjectFilter.php` #### Beschreibung API Platform Filter zur Filterung nach Projekt-Existenz. #### Query Parameter ``` GET /api/project_tasks?hasProject=true → Nur Tasks mit Projekt GET /api/project_tasks?hasProject=false → Nur Tasks ohne Projekt ``` #### Implementation ```php if ($hasProject) { $queryBuilder->andWhere('task.project IS NOT NULL'); } else { $queryBuilder->andWhere('task.project IS NULL'); } ``` --- ## Frontend-Architektur ### 1. Hauptkomponente: `ProjectTaskManagement.vue` **Pfad:** `assets/js/views/ProjectTaskManagement.vue` #### Features - **CrudDataTable** für Übersicht - **Filterfunktionen:** - Alle / Mit Projekt / Ohne Projekt - Spaltenfilter (Name, Projekt, etc.) - Globale Suche - **CRUD-Operationen:** - Create-Dialog mit Formular - Edit-Dialog (vorausgefülltes Formular) - View-Dialog (Readonly-Ansicht) - Delete-Bestätigung - **Spalten:** - Name - Projekt (mit verschachteltem Feld-Support) - Budget - Stundenkontingent - Stundensatz - Gesamtpreis - Abrechnungsart (Tag) #### Key Functions ```javascript // Filter nach Projekt-Existenz function filterByProject(type, loadData) { let filters = {} if (type === 'with-project') { filters['hasProject'] = 'true' } else if (type === 'without-project') { filters['hasProject'] = 'false' } loadData(filters) } // Preisberechnung function updateTotalPriceFromHourly() { if (editingTask.value.hourlyRate && editingTask.value.hourContingent) { const calculated = editingTask.value.hourlyRate * editingTask.value.hourContingent if (!editingTask.value.totalPrice || editingTask.value.totalPrice === 0) { editingTask.value.totalPrice = calculated } } } ``` --- ### 2. Dashboard-Integration **Pfad:** `assets/js/views/Dashboard.vue` #### Features - **KPI-Card:** Zeigt Gesamtzahl der Tätigkeiten - **Widget "Neueste Tätigkeiten":** - Top 5 neueste Tasks - Projekt-Tag oder "Projektunabhängig" - Stundensatz & Gesamtpreis - Progress-Bar für Stundenkontingent - **Budget-Übersicht:** - Progress Bar für Task-Budgets - Anteil der Tasks mit Budget --- ### 3. Projekt-Detailansicht Integration **Pfad:** `assets/js/views/ProjectManagement.vue` #### Features - **Neuer Tab "Tätigkeiten"** im Projekt-View-Dialog - **Zusammenfassung:** - Gesamt-Budget aller Tasks - Gesamt-Stunden - Gesamt-Preis - **DataTable** mit allen Tasks des Projekts - **Filterung:** Tasks werden automatisch nach Projekt gefiltert ```javascript async function loadProjectTasks(projectId) { const response = await fetch(`/api/project_tasks?project=${projectId}`) const data = await response.json() projectTasks.value = data['hydra:member'] || [] } ``` --- ### 4. Router-Konfiguration **Pfad:** `assets/js/router.js` ```javascript { path: '/project-tasks', name: 'project-tasks', component: ProjectTaskManagement } ``` --- ### 5. Menü-Integration **Pfad:** `assets/js/layout/AppMenu.vue` ```javascript { label: 'CRM', items: [ { label: 'Kontakte', icon: 'pi pi-fw pi-users', to: '/contacts' }, { label: 'Projekte', icon: 'pi pi-fw pi-briefcase', to: '/projects' }, { label: 'Tätigkeiten', icon: 'pi pi-fw pi-list-check', to: '/project-tasks' } ] } ``` --- ## API Endpoints ### REST-Endpoints | Methode | Endpoint | Beschreibung | |---------|----------|--------------| | `GET` | `/api/project_tasks` | Liste aller Tasks (mit Paginierung) | | `GET` | `/api/project_tasks/{id}` | Einzelne Task abrufen | | `POST` | `/api/project_tasks` | Neue Task erstellen | | `PUT` | `/api/project_tasks/{id}` | Task aktualisieren | | `DELETE` | `/api/project_tasks/{id}` | Task löschen | ### Filter-Parameter ``` GET /api/project_tasks?hasProject=true GET /api/project_tasks?hasProject=false GET /api/project_tasks?project=5 GET /api/project_tasks?name=entwicklung GET /api/project_tasks?project.name=myproject ``` ### Request Body (POST/PUT) ```json { "name": "Frontend-Entwicklung", "description": "Implementierung der Vue.js Komponenten", "project": "/api/projects/5", "budget": "5000.00", "hourContingent": "50.00", "hourlyRate": "100.00", "totalPrice": "5000.00" } ``` ### Response (GET) ```json { "@context": "/api/contexts/ProjectTask", "@id": "/api/project_tasks/1", "@type": "ProjectTask", "id": 1, "name": "Frontend-Entwicklung", "description": "Implementierung der Vue.js Komponenten", "project": { "@id": "/api/projects/5", "id": 5, "name": "myCRM Projekt" }, "budget": "5000.00", "hourContingent": "50.00", "hourlyRate": "100.00", "totalPrice": "5000.00", "createdAt": "2025-11-14T14:32:27+00:00", "updatedAt": "2025-11-14T15:45:12+00:00" } ``` --- ## Datenbank-Schema ### Tabelle: `project_tasks` ```sql CREATE TABLE project_tasks ( id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(255) NOT NULL, description LONGTEXT DEFAULT NULL, project_id INT DEFAULT NULL, budget NUMERIC(10, 2) DEFAULT NULL, hour_contingent NUMERIC(8, 2) DEFAULT NULL, hourly_rate NUMERIC(8, 2) DEFAULT NULL, total_price NUMERIC(10, 2) DEFAULT NULL, created_at DATETIME NOT NULL COMMENT '(DC2Type:datetime_immutable)', updated_at DATETIME DEFAULT NULL COMMENT '(DC2Type:datetime_immutable)', INDEX IDX_project_id (project_id), CONSTRAINT FK_project_tasks_project FOREIGN KEY (project_id) REFERENCES projects (id) ON DELETE CASCADE ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB; ``` ### Migration **Datei:** `migrations/Version20251114143227.php` ```bash php bin/console doctrine:migrations:migrate ``` --- ## Berechtigungen & Sicherheit ### Berechtigungs-Matrix | Rolle | VIEW | CREATE | EDIT | DELETE | Projektunabh. erstellen | |-------|------|--------|------|--------|-------------------------| | **Admin** | ✅ Alle | ✅ Alle | ✅ Alle | ✅ Alle | ✅ | | **Projektbesitzer** | ✅ Projekt-Tasks | ✅ Projekt-Tasks | ✅ Projekt-Tasks | ✅ Projekt-Tasks | ❌ | | **Teammitglied** | ✅ Projekt-Tasks | ✅ Projekt-Tasks | ✅ Projekt-Tasks | ❌ | ❌ | | **Anderer User** | ❌ | ❌ | ❌ | ❌ | ❌ | ### Validierung #### Backend-Validierung ```php #[Assert\NotBlank(message: 'Der Name der Tätigkeit darf nicht leer sein')] #[Assert\Length(max: 255)] private ?string $name = null; #[Assert\PositiveOrZero(message: 'Das Budget muss positiv sein')] private ?string $budget = null; #[Assert\PositiveOrZero(message: 'Das Stundenkontingent muss positiv sein')] private ?string $hourContingent = null; #[Assert\PositiveOrZero(message: 'Der Stundensatz muss positiv sein')] private ?string $hourlyRate = null; #[Assert\PositiveOrZero(message: 'Der Gesamtpreis muss positiv sein')] private ?string $totalPrice = null; ``` #### Frontend-Validierung ```javascript async function saveTask() { submitted.value = true if (!editingTask.value.name) { return // Name ist Pflichtfeld } // ... Speichern } ``` --- ## Troubleshooting ### Problem: Filter für Projekt-Spalte funktioniert nicht **Symptom:** Spaltenfilter in DataTable öffnet sich nicht oder zeigt Fehler. **Lösung:** - Sicherstellen, dass `filterField` korrekt konfiguriert ist - Filter müssen in `CrudDataTable` für verschachtelte Felder initialisiert werden ```javascript // In ProjectTaskManagement.vue { key: 'project', label: 'Projekt', field: 'project.name', // Verschachteltes Feld dataType: 'text', showFilterMatchModes: true } // In CrudDataTable.vue (onMounted) const filterKey = col.filterField || col.field || col.key internalFilters.value[filterKey] = { operator: FilterOperator.AND, constraints: [{ value: null, matchMode: FilterMatchMode.STARTS_WITH }] } ``` ### Problem: Projekt wird nicht in Übersicht angezeigt **Symptom:** Projekt-Spalte zeigt immer "Projektunabhängig". **Lösung:** - Serialization Groups in Project Entity anpassen: ```php #[Groups(['project:read', 'project_task:read'])] // project_task:read hinzufügen! private ?int $id = null; #[Groups(['project:read', 'project:write', 'project_task:read'])] private ?string $name = null; ``` ### Problem: Projektauswahl wird nicht gespeichert **Symptom:** Beim Bearbeiten wird das Projekt nicht im Select angezeigt. **Lösung:** - Projekt-Objekt aus IRI extrahieren: ```javascript function editTask(task) { let projectObject = null if (task.project) { if (typeof task.project === 'object' && task.project.id) { projectObject = projects.value.find(p => p.id === task.project.id) || task.project } else if (typeof task.project === 'string') { const projectId = parseInt(task.project.split('/').pop()) projectObject = projects.value.find(p => p.id === projectId) } } editingTask.value = { ...task, project: projectObject // Objekt statt IRI } } ``` --- ## Testing ### Unit Tests (PHPUnit) ```bash # Entity Tests php bin/phpunit tests/Entity/ProjectTaskTest.php # Repository Tests php bin/phpunit tests/Repository/ProjectTaskRepositoryTest.php # Voter Tests php bin/phpunit tests/Security/Voter/ProjectTaskVoterTest.php ``` ### Frontend Tests (Vitest) ```bash # Component Tests npm run test -- ProjectTaskManagement.spec.js # E2E Tests npm run test:e2e -- project-tasks.spec.js ``` --- ## Performance-Optimierung ### Backend - **Eager Loading:** Projekte werden mit Tasks geladen, um N+1 Queries zu vermeiden - **Indexierung:** Index auf `project_id` für schnelle Filterung - **Pagination:** Client-side Pagination für große Datenmengen ### Frontend - **Lazy Loading:** Vue.js Route-based Code Splitting - **Computed Properties:** Für reaktive Berechnungen (Budget-Summen) - **Debouncing:** Für Suche/Filter-Eingaben --- ## Zukünftige Erweiterungen ### 🔮 Geplante Features 1. **Zeiterfassung** - Tatsächlich gebuchte Stunden tracken - Vergleich: Geplant vs. Tatsächlich - Zeiterfassungs-Widget 2. **Rechnungsstellung** - Tasks zu Rechnungen zuordnen - Automatische Rechnungsgenerierung aus Tasks - Status: Abgerechnet/Offen 3. **Dashboard-Widget** - Übersicht über laufende Tätigkeiten - Budget-Status (Verbrauch) - Warnung bei Überschreitung 4. **Berechtigungen verfeinern** - Modul-spezifische Rollen - Feinere Kontrolle über Task-Berechtigungen - Team-basierte Zugriffskontrolle 5. **Reporting** - Budget-Reports pro Projekt - Stundenübersichten - Export als PDF/Excel 6. **Abhängigkeiten** - Task-Abhängigkeiten definieren - Gantt-Chart-Ansicht - Kritischer Pfad --- ## Changelog ### Version 1.0.0 (14.11.2025) **Neu:** - ✅ ProjectTask Entity mit allen Feldern - ✅ ProjectTaskRepository mit Aggregations-Methoden - ✅ ProjectTaskVoter für Berechtigungen - ✅ ProjectTaskSecurityListener für Validierung - ✅ Custom ProjectTaskProjectFilter - ✅ Frontend: ProjectTaskManagement.vue - ✅ Dashboard-Integration - ✅ Projekt-Detailansicht Integration - ✅ CrudDataTable mit verschachtelten Feld-Support - ✅ Router & Menü-Konfiguration - ✅ Doctrine Migration - ✅ API Platform Konfiguration **Fixes:** - ✅ Artikel "Neue Tätigkeit" (statt "Neuer") - ✅ Projekt-Anzeige in Übersicht (Serialization Groups) - ✅ Projektauswahl beim Bearbeiten - ✅ Filter "Mit Projekt" / "Ohne Projekt" - ✅ Spaltenfilter für verschachtelte Felder --- ## Kontakt & Support Bei Fragen oder Problemen: - **Dokumentation:** `docs/INSTRUCTIONS.md` - **GitHub Issues:** [GitHub Repository] - **Code Review:** Siehe Code-Kommentare in den jeweiligen Dateien --- **Ende der Dokumentation**