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

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 filterField korrekt konfiguriert ist
  • Filter müssen in CrudDataTable fü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_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