myCRM/docs/PROJECT_TASKS_MODULE.md
olli 8a132d2fb9 feat: Implement ProjectTask module with full CRUD functionality
- Added ProjectTask entity with fields for name, description, budget, hour contingent, hourly rate, and total price.
- Created ProjectTaskRepository with methods for querying tasks by project and user access.
- Implemented ProjectTaskVoter for fine-grained access control based on user roles and project membership.
- Developed ProjectTaskSecurityListener to enforce permission checks during task creation.
- Introduced custom ProjectTaskProjectFilter for filtering tasks based on project existence.
- Integrated ProjectTask management in the frontend with Vue.js components, including CRUD operations and filtering capabilities.
- Added API endpoints for ProjectTask with appropriate security measures.
- Created migration for project_tasks table in the database.
- Updated documentation to reflect new module features and usage.
2025-11-14 17:12:40 +01:00

652 lines
16 KiB
Markdown

# 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**