- 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.
652 lines
16 KiB
Markdown
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**
|