From f0aca266422d1e9f62d6f7920fef13f86f3846e5 Mon Sep 17 00:00:00 2001 From: znetsixe Date: Wed, 1 Apr 2026 16:02:38 +0200 Subject: [PATCH] Sprint 2: Live data, CRUD modals, commitments, document upload Frontend: - Connect MetroMap to live Inertia props (replace hardcoded demo data) - Drill-down navigation via router.visit for project-level maps - Reactive breadcrumb based on map level - Empty state when no projects exist - Reusable Modal component with retro styling - ProjectForm and CommitmentForm with Inertia useForm - FormInput reusable component (text, date, textarea, select) - FloatingActions FAB button for creating projects/themes Backend: - CommitmentService + CommitmentController (CRUD, mark complete, overdue) - DocumentService + DocumentController (upload, download, delete) - MapController now passes users and speerpunten to frontend - 7 new routes (4 commitment, 3 document) Co-Authored-By: Claude Opus 4.6 (1M context) --- app/Http/Controllers/CommitmentController.php | 61 +++++ app/Http/Controllers/DocumentController.php | 51 +++++ app/Http/Controllers/MapController.php | 8 +- app/Services/CommitmentService.php | 103 +++++++++ app/Services/DocumentService.php | 77 +++++++ .../js/Components/Forms/CommitmentForm.vue | 186 +++++++++++++++ resources/js/Components/Forms/FormInput.vue | 160 +++++++++++++ resources/js/Components/Forms/ProjectForm.vue | 216 ++++++++++++++++++ .../Components/MetroMap/FloatingActions.vue | 161 +++++++++++++ resources/js/Components/Modal.vue | 114 +++++++++ resources/js/Pages/Map/MetroMap.vue | 158 ++++++++----- routes/web.php | 13 ++ 12 files changed, 1247 insertions(+), 61 deletions(-) create mode 100644 app/Http/Controllers/CommitmentController.php create mode 100644 app/Http/Controllers/DocumentController.php create mode 100644 app/Services/CommitmentService.php create mode 100644 app/Services/DocumentService.php create mode 100644 resources/js/Components/Forms/CommitmentForm.vue create mode 100644 resources/js/Components/Forms/FormInput.vue create mode 100644 resources/js/Components/Forms/ProjectForm.vue create mode 100644 resources/js/Components/MetroMap/FloatingActions.vue create mode 100644 resources/js/Components/Modal.vue diff --git a/app/Http/Controllers/CommitmentController.php b/app/Http/Controllers/CommitmentController.php new file mode 100644 index 0000000..bd3bf36 --- /dev/null +++ b/app/Http/Controllers/CommitmentController.php @@ -0,0 +1,61 @@ +validate([ + 'beschrijving' => 'required|string', + 'eigenaar_id' => 'required|exists:users,id', + 'deadline' => 'required|date', + 'project_id' => 'required|exists:projects,id', + 'prioriteit' => 'nullable|string', + 'bron' => 'nullable|string', + 'besluit_id' => 'nullable|exists:besluiten,id', + ]); + + $this->commitmentService->create($validated); + + return back()->with('success', 'Commitment aangemaakt.'); + } + + public function update(Request $request, Commitment $commitment) + { + $validated = $request->validate([ + 'beschrijving' => 'sometimes|required|string', + 'eigenaar_id' => 'sometimes|required|exists:users,id', + 'deadline' => 'sometimes|required|date', + 'prioriteit' => 'nullable|string', + 'bron' => 'nullable|string', + 'status' => 'nullable|string', + ]); + + $this->commitmentService->update($commitment, $validated); + + return back()->with('success', 'Commitment bijgewerkt.'); + } + + public function complete(Commitment $commitment) + { + $this->commitmentService->markComplete($commitment); + + return back()->with('success', 'Commitment afgerond.'); + } + + public function destroy(Commitment $commitment) + { + $commitment->delete(); + + return back()->with('success', 'Commitment verwijderd.'); + } +} diff --git a/app/Http/Controllers/DocumentController.php b/app/Http/Controllers/DocumentController.php new file mode 100644 index 0000000..5a17116 --- /dev/null +++ b/app/Http/Controllers/DocumentController.php @@ -0,0 +1,51 @@ +validate([ + 'titel' => 'required|string|max:255', + 'bestand' => 'required|file|max:20480', + 'project_id' => 'required|exists:projects,id', + 'fase_id' => 'nullable|exists:fases,id', + 'type' => 'nullable|string|max:50', + ]); + + $this->documentService->upload($validated, $request->file('bestand')); + + return back()->with('success', 'Document geupload.'); + } + + public function download(Document $document) + { + abort_unless( + Storage::disk('local')->exists($document->bestandspad), + 404, + 'Bestand niet gevonden.' + ); + + return Storage::disk('local')->download( + $document->bestandspad, + $document->titel + ); + } + + public function destroy(Document $document) + { + $this->documentService->delete($document); + + return back()->with('success', 'Document verwijderd.'); + } +} diff --git a/app/Http/Controllers/MapController.php b/app/Http/Controllers/MapController.php index cdd484f..93f1f6d 100644 --- a/app/Http/Controllers/MapController.php +++ b/app/Http/Controllers/MapController.php @@ -17,7 +17,9 @@ class MapController extends Controller $mapData = $this->mapDataService->getStrategyMap(); return Inertia::render('Map/MetroMap', [ - 'mapData' => $mapData, + 'mapData' => $mapData, + 'users' => \App\Models\User::select('id', 'name')->get(), + 'speerpunten' => \App\Models\Speerpunt::select('id', 'naam', 'thema_id')->with('thema:id,naam')->get(), ]); } @@ -26,7 +28,9 @@ class MapController extends Controller $mapData = $this->mapDataService->getProjectMap($projectId); return Inertia::render('Map/MetroMap', [ - 'mapData' => $mapData, + 'mapData' => $mapData, + 'users' => \App\Models\User::select('id', 'name')->get(), + 'speerpunten' => \App\Models\Speerpunt::select('id', 'naam', 'thema_id')->with('thema:id,naam')->get(), ]); } diff --git a/app/Services/CommitmentService.php b/app/Services/CommitmentService.php new file mode 100644 index 0000000..39630c2 --- /dev/null +++ b/app/Services/CommitmentService.php @@ -0,0 +1,103 @@ +where('project_id', $projectId) + ->orderBy('deadline') + ->get(); + } + + /** + * Create a new commitment with audit log. + */ + public function create(array $data): Commitment + { + return DB::transaction(function () use ($data) { + $commitment = Commitment::create([ + 'project_id' => $data['project_id'], + 'beschrijving' => $data['beschrijving'], + 'eigenaar_id' => $data['eigenaar_id'], + 'deadline' => $data['deadline'], + 'status' => CommitmentStatus::Open, + 'bron' => $data['bron'] ?? null, + 'besluit_id' => $data['besluit_id'] ?? null, + ]); + + $this->audit('created', $commitment); + + return $commitment; + }); + } + + /** + * Update a commitment with audit log. + */ + public function update(Commitment $commitment, array $data): Commitment + { + return DB::transaction(function () use ($commitment, $data) { + $commitment->update(array_filter([ + 'beschrijving' => $data['beschrijving'] ?? null, + 'eigenaar_id' => $data['eigenaar_id'] ?? null, + 'deadline' => $data['deadline'] ?? null, + 'status' => $data['status'] ?? null, + 'bron' => $data['bron'] ?? null, + ], fn ($v) => $v !== null)); + + $this->audit('updated', $commitment); + + return $commitment->fresh(); + }); + } + + /** + * Mark a commitment as afgerond with audit log. + */ + public function markComplete(Commitment $commitment): Commitment + { + return DB::transaction(function () use ($commitment) { + $commitment->update(['status' => CommitmentStatus::Afgerond]); + + $this->audit('completed', $commitment); + + return $commitment->fresh(); + }); + } + + /** + * Get all commitments past their deadline that are not afgerond. + */ + public function getOverdue(): Collection + { + return Commitment::with(['eigenaar', 'project']) + ->where('deadline', '<', now()->toDateString()) + ->where('status', '!=', CommitmentStatus::Afgerond) + ->orderBy('deadline') + ->get(); + } + + private function audit(string $action, Commitment $commitment, ?array $extra = null): void + { + AuditLog::create([ + 'user_id' => Auth::id(), + 'action' => "commitment.{$action}", + 'entity_type' => 'commitment', + 'entity_id' => $commitment->id, + 'payload' => $extra, + ]); + } +} diff --git a/app/Services/DocumentService.php b/app/Services/DocumentService.php new file mode 100644 index 0000000..aaf6e69 --- /dev/null +++ b/app/Services/DocumentService.php @@ -0,0 +1,77 @@ +where('project_id', $projectId) + ->latest() + ->get(); + } + + /** + * Store a file and create the Document record. + */ + public function upload(array $data, UploadedFile $file): Document + { + return DB::transaction(function () use ($data, $file) { + $projectId = $data['project_id']; + $path = $file->store("documents/{$projectId}", 'local'); + + $document = Document::create([ + 'project_id' => $projectId, + 'fase_id' => $data['fase_id'] ?? null, + 'titel' => $data['titel'], + 'type' => $data['type'] ?? $file->getClientOriginalExtension(), + 'bestandspad' => $path, + 'versie' => 1, + 'auteur_id' => Auth::id(), + ]); + + $this->audit('uploaded', $document); + + return $document; + }); + } + + /** + * Delete the file from storage and soft-delete the Document record. + */ + public function delete(Document $document): void + { + DB::transaction(function () use ($document) { + if ($document->bestandspad && Storage::disk('local')->exists($document->bestandspad)) { + Storage::disk('local')->delete($document->bestandspad); + } + + $this->audit('deleted', $document); + + $document->delete(); + }); + } + + private function audit(string $action, Document $document, ?array $extra = null): void + { + AuditLog::create([ + 'user_id' => Auth::id(), + 'action' => "document.{$action}", + 'entity_type' => 'document', + 'entity_id' => $document->id, + 'payload' => $extra, + ]); + } +} diff --git a/resources/js/Components/Forms/CommitmentForm.vue b/resources/js/Components/Forms/CommitmentForm.vue new file mode 100644 index 0000000..26d6de6 --- /dev/null +++ b/resources/js/Components/Forms/CommitmentForm.vue @@ -0,0 +1,186 @@ + + + + + diff --git a/resources/js/Components/Forms/FormInput.vue b/resources/js/Components/Forms/FormInput.vue new file mode 100644 index 0000000..08316cc --- /dev/null +++ b/resources/js/Components/Forms/FormInput.vue @@ -0,0 +1,160 @@ + + +