- 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.
16 KiB
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
#[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:
// 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:
// 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
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
// 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
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
{
path: '/project-tasks',
name: 'project-tasks',
component: ProjectTaskManagement
}
5. Menü-Integration
Pfad: assets/js/layout/AppMenu.vue
{
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)
{
"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)
{
"@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
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
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
#[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
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
filterFieldkorrekt konfiguriert ist - Filter müssen in
CrudDataTablefür verschachtelte Felder initialisiert werden
// 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:
#[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:
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)
# 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)
# 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_idfü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
-
Zeiterfassung
- Tatsächlich gebuchte Stunden tracken
- Vergleich: Geplant vs. Tatsächlich
- Zeiterfassungs-Widget
-
Rechnungsstellung
- Tasks zu Rechnungen zuordnen
- Automatische Rechnungsgenerierung aus Tasks
- Status: Abgerechnet/Offen
-
Dashboard-Widget
- Übersicht über laufende Tätigkeiten
- Budget-Status (Verbrauch)
- Warnung bei Überschreitung
-
Berechtigungen verfeinern
- Modul-spezifische Rollen
- Feinere Kontrolle über Task-Berechtigungen
- Team-basierte Zugriffskontrolle
-
Reporting
- Budget-Reports pro Projekt
- Stundenübersichten
- Export als PDF/Excel
-
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