From d03fe155423ace113747d425423b70061bb5c0f6 Mon Sep 17 00:00:00 2001 From: znetsixe Date: Wed, 1 Apr 2026 13:52:35 +0200 Subject: [PATCH] Sprint 1: Auth, metro map canvas, services, and retro UI Authentication: - Laravel Fortify + Sanctum with Inertia views - RBAC middleware (admin, project_owner, team_member, viewer) - Retro terminal-styled login/register/forgot-password pages Metro Map (core UI): - D3.js zoomable SVG canvas with metro line rendering - Station nodes with glow-on-hover, status coloring, tooltips - Breadcrumb navigation for multi-level drill-down - Node preview panel with zoom-in action - C64-style CLI bar with blinking cursor at bottom Backend services: - ProjectService (CRUD, phase transitions, park/stop, audit logging) - ThemaService (CRUD with audit) - MapDataService (strategy map L1, project map L2) - Thin controllers: MapController, ProjectController, ThemaController - 32 routes total (auth + app + API) Style foundation: - Retro-futurism theme: VT323, Press Start 2P, IBM Plex Mono fonts - Dark palette with cyan/orange/green/purple neon accents - Comprehensive seed data (4 themes, 12 projects, commitments, deps) Co-Authored-By: Claude Opus 4.6 (1M context) --- app/Actions/Fortify/CreateNewUser.php | 50 ++ .../Fortify/PasswordValidationRules.php | 19 + app/Actions/Fortify/ResetUserPassword.php | 32 ++ app/Actions/Fortify/UpdateUserPassword.php | 31 + .../Fortify/UpdateUserProfileInformation.php | 42 ++ app/Http/Controllers/MapController.php | 42 ++ app/Http/Controllers/ProjectController.php | 91 +++ app/Http/Controllers/ThemaController.php | 52 ++ app/Http/Middleware/CheckRole.php | 19 + app/Http/Middleware/HandleInertiaRequests.php | 16 +- app/Models/User.php | 3 +- app/Providers/FortifyServiceProvider.php | 58 ++ app/Services/MapDataService.php | 165 ++++++ app/Services/ProjectService.php | 186 ++++++ app/Services/ThemaService.php | 60 ++ bootstrap/app.php | 4 + bootstrap/providers.php | 1 + composer.json | 2 + composer.lock | 354 +++++++++++- config/fortify.php | 154 +++++ config/sanctum.php | 84 +++ ..._add_two_factor_columns_to_users_table.php | 42 ++ ...10_create_personal_access_tokens_table.php | 33 ++ database/seeders/DatabaseSeeder.php | 541 +++++++++++++++++- package-lock.json | 489 ++++++++++++++++ package.json | 4 + resources/css/app.css | 28 + resources/js/Components/Cli/CliBar.vue | 156 +++++ .../js/Components/MetroMap/Breadcrumb.vue | 51 ++ .../js/Components/MetroMap/MetroCanvas.vue | 354 ++++++++++++ .../js/Components/MetroMap/NodePreview.vue | 147 +++++ resources/js/Pages/Auth/ForgotPassword.vue | 346 +++++++++++ resources/js/Pages/Auth/Login.vue | 377 ++++++++++++ resources/js/Pages/Auth/Register.vue | 380 ++++++++++++ resources/js/Pages/Auth/ResetPassword.vue | 368 ++++++++++++ resources/js/Pages/Auth/VerifyEmail.vue | 344 +++++++++++ resources/js/Pages/Dashboard.vue | 90 ++- resources/js/Pages/Map/MetroMap.vue | 127 ++++ routes/api.php | 8 + routes/web.php | 39 +- 40 files changed, 5368 insertions(+), 21 deletions(-) create mode 100644 app/Actions/Fortify/CreateNewUser.php create mode 100644 app/Actions/Fortify/PasswordValidationRules.php create mode 100644 app/Actions/Fortify/ResetUserPassword.php create mode 100644 app/Actions/Fortify/UpdateUserPassword.php create mode 100644 app/Actions/Fortify/UpdateUserProfileInformation.php create mode 100644 app/Http/Controllers/MapController.php create mode 100644 app/Http/Controllers/ProjectController.php create mode 100644 app/Http/Controllers/ThemaController.php create mode 100644 app/Http/Middleware/CheckRole.php create mode 100644 app/Providers/FortifyServiceProvider.php create mode 100644 app/Services/MapDataService.php create mode 100644 app/Services/ProjectService.php create mode 100644 app/Services/ThemaService.php create mode 100644 config/fortify.php create mode 100644 config/sanctum.php create mode 100644 database/migrations/2026_04_01_114223_add_two_factor_columns_to_users_table.php create mode 100644 database/migrations/2026_04_01_114310_create_personal_access_tokens_table.php create mode 100644 resources/js/Components/Cli/CliBar.vue create mode 100644 resources/js/Components/MetroMap/Breadcrumb.vue create mode 100644 resources/js/Components/MetroMap/MetroCanvas.vue create mode 100644 resources/js/Components/MetroMap/NodePreview.vue create mode 100644 resources/js/Pages/Auth/ForgotPassword.vue create mode 100644 resources/js/Pages/Auth/Login.vue create mode 100644 resources/js/Pages/Auth/Register.vue create mode 100644 resources/js/Pages/Auth/ResetPassword.vue create mode 100644 resources/js/Pages/Auth/VerifyEmail.vue create mode 100644 resources/js/Pages/Map/MetroMap.vue create mode 100644 routes/api.php diff --git a/app/Actions/Fortify/CreateNewUser.php b/app/Actions/Fortify/CreateNewUser.php new file mode 100644 index 0000000..fd8c6b1 --- /dev/null +++ b/app/Actions/Fortify/CreateNewUser.php @@ -0,0 +1,50 @@ + $input + * + * @throws ValidationException + */ + public function create(array $input): User + { + Validator::make($input, [ + 'name' => ['required', 'string', 'max:255'], + 'email' => [ + 'required', + 'string', + 'email', + 'max:255', + Rule::unique(User::class), + ], + 'password' => ['required', 'string', 'min:8', 'confirmed'], + ])->validate(); + + $user = User::create([ + 'name' => $input['name'], + 'email' => $input['email'], + 'password' => Hash::make($input['password']), + ]); + + // Assign default 'viewer' role + $viewerRole = Role::where('naam', 'viewer')->first(); + if ($viewerRole) { + $user->roles()->attach($viewerRole); + } + + return $user; + } +} diff --git a/app/Actions/Fortify/PasswordValidationRules.php b/app/Actions/Fortify/PasswordValidationRules.php new file mode 100644 index 0000000..3678865 --- /dev/null +++ b/app/Actions/Fortify/PasswordValidationRules.php @@ -0,0 +1,19 @@ +|string> + */ + protected function passwordRules(): array + { + return ['required', 'string', Password::default(), 'confirmed']; + } +} diff --git a/app/Actions/Fortify/ResetUserPassword.php b/app/Actions/Fortify/ResetUserPassword.php new file mode 100644 index 0000000..667651f --- /dev/null +++ b/app/Actions/Fortify/ResetUserPassword.php @@ -0,0 +1,32 @@ + $input + * + * @throws ValidationException + */ + public function reset(User $user, array $input): void + { + Validator::make($input, [ + 'password' => $this->passwordRules(), + ])->validate(); + + $user->forceFill([ + 'password' => Hash::make($input['password']), + ])->save(); + } +} diff --git a/app/Actions/Fortify/UpdateUserPassword.php b/app/Actions/Fortify/UpdateUserPassword.php new file mode 100644 index 0000000..ce24209 --- /dev/null +++ b/app/Actions/Fortify/UpdateUserPassword.php @@ -0,0 +1,31 @@ + $input + * + * @throws ValidationException + */ + public function update(User $user, array $input): void + { + Validator::make($input, [ + 'current_password' => ['required', 'string', 'current_password:web'], + 'password' => ['required', 'string', 'min:8', 'confirmed'], + ])->validate(); + + $user->forceFill([ + 'password' => Hash::make($input['password']), + ])->save(); + } +} diff --git a/app/Actions/Fortify/UpdateUserProfileInformation.php b/app/Actions/Fortify/UpdateUserProfileInformation.php new file mode 100644 index 0000000..b97db02 --- /dev/null +++ b/app/Actions/Fortify/UpdateUserProfileInformation.php @@ -0,0 +1,42 @@ + $input + * + * @throws ValidationException + */ + public function update(User $user, array $input): void + { + Validator::make($input, [ + 'name' => ['required', 'string', 'max:255'], + 'email' => [ + 'required', + 'string', + 'email', + 'max:255', + Rule::unique('users')->ignore($user->id), + ], + 'functie' => ['nullable', 'string', 'max:255'], + 'afdeling' => ['nullable', 'string', 'max:255'], + ])->validate(); + + $user->forceFill([ + 'name' => $input['name'], + 'email' => $input['email'], + 'functie' => $input['functie'] ?? $user->functie, + 'afdeling' => $input['afdeling'] ?? $user->afdeling, + ])->save(); + } +} diff --git a/app/Http/Controllers/MapController.php b/app/Http/Controllers/MapController.php new file mode 100644 index 0000000..cdd484f --- /dev/null +++ b/app/Http/Controllers/MapController.php @@ -0,0 +1,42 @@ +mapDataService->getStrategyMap(); + + return Inertia::render('Map/MetroMap', [ + 'mapData' => $mapData, + ]); + } + + public function project(int $projectId) + { + $mapData = $this->mapDataService->getProjectMap($projectId); + + return Inertia::render('Map/MetroMap', [ + 'mapData' => $mapData, + ]); + } + + public function apiStrategy(Request $request) + { + return response()->json($this->mapDataService->getStrategyMap()); + } + + public function apiProject(int $projectId) + { + return response()->json($this->mapDataService->getProjectMap($projectId)); + } +} diff --git a/app/Http/Controllers/ProjectController.php b/app/Http/Controllers/ProjectController.php new file mode 100644 index 0000000..0bee3da --- /dev/null +++ b/app/Http/Controllers/ProjectController.php @@ -0,0 +1,91 @@ +validate([ + 'naam' => 'required|string|max:255', + 'beschrijving' => 'nullable|string', + 'speerpunt_id' => 'nullable|exists:speerpunten,id', + 'prioriteit' => 'nullable|string', + 'startdatum' => 'nullable|date', + 'streef_einddatum' => 'nullable|date|after:startdatum', + ]); + + $project = $this->projectService->create($validated); + + return back()->with('success', "Project '{$project->naam}' aangemaakt."); + } + + public function show(Project $project) + { + $project = $this->projectService->getWithDetails($project->id); + + return Inertia::render('Project/Show', [ + 'project' => $project, + ]); + } + + public function update(Request $request, Project $project) + { + $validated = $request->validate([ + 'naam' => 'sometimes|string|max:255', + 'beschrijving' => 'nullable|string', + 'prioriteit' => 'nullable|string', + 'speerpunt_id' => 'nullable|exists:speerpunten,id', + 'streef_einddatum' => 'nullable|date', + ]); + + $this->projectService->update($project, $validated); + + return back()->with('success', 'Project bijgewerkt.'); + } + + public function transition(Request $request, Project $project) + { + $validated = $request->validate([ + 'status' => 'required|string', + ]); + + $newStatus = ProjectStatus::from($validated['status']); + $this->projectService->transitionPhase($project, $newStatus); + + return back()->with('success', 'Projectfase bijgewerkt.'); + } + + public function park(Request $request, Project $project) + { + $reason = $request->input('reason', ''); + $this->projectService->park($project, $reason); + + return back()->with('success', 'Project geparkeerd.'); + } + + public function stop(Request $request, Project $project) + { + $reason = $request->input('reason', ''); + $this->projectService->stop($project, $reason); + + return back()->with('success', 'Project gestopt.'); + } + + public function destroy(Project $project) + { + $project->delete(); + + return redirect('/map')->with('success', 'Project verwijderd.'); + } +} diff --git a/app/Http/Controllers/ThemaController.php b/app/Http/Controllers/ThemaController.php new file mode 100644 index 0000000..13f028a --- /dev/null +++ b/app/Http/Controllers/ThemaController.php @@ -0,0 +1,52 @@ + $this->themaService->getAll(), + ]); + } + + public function store(Request $request) + { + $validated = $request->validate([ + 'naam' => 'required|string|max:255', + 'beschrijving' => 'nullable|string', + 'prioriteit' => 'nullable|string', + 'periode_start' => 'nullable|date', + 'periode_eind' => 'nullable|date|after:periode_start', + ]); + + $this->themaService->create($validated); + + return back()->with('success', 'Thema aangemaakt.'); + } + + public function update(Request $request, Thema $thema) + { + $validated = $request->validate([ + 'naam' => 'sometimes|string|max:255', + 'beschrijving' => 'nullable|string', + 'prioriteit' => 'nullable|string', + 'periode_start' => 'nullable|date', + 'periode_eind' => 'nullable|date', + ]); + + $this->themaService->update($thema, $validated); + + return back()->with('success', 'Thema bijgewerkt.'); + } +} diff --git a/app/Http/Middleware/CheckRole.php b/app/Http/Middleware/CheckRole.php new file mode 100644 index 0000000..4357ef4 --- /dev/null +++ b/app/Http/Middleware/CheckRole.php @@ -0,0 +1,19 @@ +user() || ! $request->user()->roles()->whereIn('naam', $roles)->exists()) { + abort(403, 'Je hebt geen toegang tot deze functie.'); + } + + return $next($request); + } +} diff --git a/app/Http/Middleware/HandleInertiaRequests.php b/app/Http/Middleware/HandleInertiaRequests.php index c19ce18..e7a0bc9 100644 --- a/app/Http/Middleware/HandleInertiaRequests.php +++ b/app/Http/Middleware/HandleInertiaRequests.php @@ -37,7 +37,21 @@ class HandleInertiaRequests extends Middleware { return [ ...parent::share($request), - // + 'auth' => [ + 'user' => $request->user() ? [ + 'id' => $request->user()->id, + 'name' => $request->user()->name, + 'email' => $request->user()->email, + 'functie' => $request->user()->functie, + 'afdeling' => $request->user()->afdeling, + 'roles' => $request->user()->roles->pluck('naam'), + ] : null, + ], + 'flash' => [ + 'success' => $request->session()->get('success'), + 'error' => $request->session()->get('error'), + ], + 'locale' => app()->getLocale(), ]; } } diff --git a/app/Models/User.php b/app/Models/User.php index 34b09ff..5501fe8 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -9,11 +9,12 @@ use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; +use Laravel\Sanctum\HasApiTokens; class User extends Authenticatable { /** @use HasFactory */ - use HasFactory, Notifiable; + use HasApiTokens, HasFactory, Notifiable; protected $fillable = [ 'name', diff --git a/app/Providers/FortifyServiceProvider.php b/app/Providers/FortifyServiceProvider.php new file mode 100644 index 0000000..28012b0 --- /dev/null +++ b/app/Providers/FortifyServiceProvider.php @@ -0,0 +1,58 @@ +singleton(\Laravel\Fortify\Contracts\CreatesNewUsers::class, CreateNewUser::class); + app()->singleton(\Laravel\Fortify\Contracts\UpdatesUserProfileInformation::class, UpdateUserProfileInformation::class); + app()->singleton(\Laravel\Fortify\Contracts\UpdatesUserPasswords::class, UpdateUserPassword::class); + } + + /** + * Bootstrap any application services. + */ + public function boot(): void + { + Fortify::createUsersUsing(CreateNewUser::class); + Fortify::updateUserProfileInformationUsing(UpdateUserProfileInformation::class); + Fortify::updateUserPasswordsUsing(UpdateUserPassword::class); + Fortify::resetUserPasswordsUsing(ResetUserPassword::class); + + Fortify::loginView(fn () => Inertia::render('Auth/Login')); + Fortify::registerView(fn () => Inertia::render('Auth/Register')); + Fortify::requestPasswordResetLinkView(fn () => Inertia::render('Auth/ForgotPassword')); + Fortify::resetPasswordView(fn ($request) => Inertia::render('Auth/ResetPassword', [ + 'token' => $request->route('token'), + 'email' => $request->query('email'), + ])); + Fortify::verifyEmailView(fn () => Inertia::render('Auth/VerifyEmail')); + + RateLimiter::for('login', function (Request $request) { + $throttleKey = Str::transliterate(Str::lower($request->input(Fortify::username())).'|'.$request->ip()); + + return Limit::perMinute(5)->by($throttleKey); + }); + + RateLimiter::for('two-factor', function (Request $request) { + return Limit::perMinute(5)->by($request->session()->get('login.id')); + }); + } +} diff --git a/app/Services/MapDataService.php b/app/Services/MapDataService.php new file mode 100644 index 0000000..c0f18d7 --- /dev/null +++ b/app/Services/MapDataService.php @@ -0,0 +1,165 @@ + function ($q) { + $q->with('eigenaar') + ->withCount(['documents', 'commitments', 'risicos', 'fases']); + } + ])->get(); + + $lines = []; + $nodes = []; + $connections = []; + + $lineColors = ['#00d2ff', '#e94560', '#00ff88', '#7b68ee', '#ff6b6b', '#ffd93d', '#6bcb77', '#4d96ff']; + $yOffset = 0; + + foreach ($themas as $index => $thema) { + $color = $lineColors[$index % count($lineColors)]; + $lines[] = [ + 'id' => "thema-{$thema->id}", + 'name' => $thema->naam, + 'color' => $color, + ]; + + $projects = $thema->speerpunten->flatMap->projects; + $xOffset = -200; + + foreach ($projects as $order => $project) { + $nodes[] = [ + 'id' => "project-{$project->id}", + 'entityId' => $project->id, + 'entityType' => 'project', + 'name' => $project->naam, + 'lineId' => "thema-{$thema->id}", + 'x' => $xOffset + ($order * 200), + 'y' => $yOffset, + 'order' => $order + 1, + 'status' => $project->status->value, + 'description' => Str::limit($project->beschrijving, 100), + 'owner' => $project->eigenaar?->name, + 'badge' => ucfirst(str_replace('_', ' ', $project->status->value)), + 'children' => $project->documents_count + $project->commitments_count, + ]; + } + + $yOffset += 130; + } + + // Get dependencies as connections + $dependencies = Afhankelijkheid::all(); + foreach ($dependencies as $dep) { + $connections[] = [ + 'from' => "project-{$dep->project_id}", + 'to' => "project-{$dep->afhankelijk_van_project_id}", + ]; + } + + return [ + 'lines' => $lines, + 'nodes' => $nodes, + 'connections' => $connections, + 'level' => 1, + ]; + } + + /** + * Build Level 2 (Project) metro map data. + * The project's lifecycle phases = a metro line, milestones = stations. + */ + public function getProjectMap(int $projectId): array + { + $project = Project::with([ + 'fases', + 'commitments' => fn ($q) => $q->with('eigenaar'), + 'documents', + 'risicos', + 'besluiten', + ])->findOrFail($projectId); + + $lines = [ + ['id' => 'lifecycle', 'name' => $project->naam, 'color' => '#00d2ff'], + ['id' => 'commitments', 'name' => 'Commitments', 'color' => '#e94560'], + ['id' => 'documents', 'name' => 'Documenten', 'color' => '#7b68ee'], + ]; + + $nodes = []; + $xOffset = -300; + + // Phase nodes on lifecycle line + foreach ($project->fases->sortBy('type') as $order => $fase) { + $nodes[] = [ + 'id' => "fase-{$fase->id}", + 'entityId' => $fase->id, + 'entityType' => 'fase', + 'name' => ucfirst(str_replace('_', ' ', $fase->type->value)), + 'lineId' => 'lifecycle', + 'x' => $xOffset + ($order * 180), + 'y' => -50, + 'order' => $order + 1, + 'status' => $fase->status->value, + 'badge' => ucfirst($fase->status->value), + ]; + } + + // Commitment nodes + foreach ($project->commitments as $order => $commitment) { + $nodes[] = [ + 'id' => "commitment-{$commitment->id}", + 'entityId' => $commitment->id, + 'entityType' => 'commitment', + 'name' => Str::limit($commitment->beschrijving, 40), + 'lineId' => 'commitments', + 'x' => $xOffset + ($order * 180), + 'y' => 80, + 'order' => $order + 1, + 'status' => $commitment->status->value, + 'owner' => $commitment->eigenaar?->name, + 'badge' => $commitment->deadline?->format('d M'), + ]; + } + + // Document nodes + foreach ($project->documents as $order => $doc) { + $nodes[] = [ + 'id' => "document-{$doc->id}", + 'entityId' => $doc->id, + 'entityType' => 'document', + 'name' => $doc->titel, + 'lineId' => 'documents', + 'x' => $xOffset + ($order * 180), + 'y' => 210, + 'order' => $order + 1, + 'status' => 'active', + 'badge' => "v{$doc->versie}", + ]; + } + + return [ + 'lines' => $lines, + 'nodes' => $nodes, + 'connections' => [], + 'level' => 2, + 'project' => [ + 'id' => $project->id, + 'naam' => $project->naam, + 'status' => $project->status->value, + ], + ]; + } +} diff --git a/app/Services/ProjectService.php b/app/Services/ProjectService.php new file mode 100644 index 0000000..973cff3 --- /dev/null +++ b/app/Services/ProjectService.php @@ -0,0 +1,186 @@ +withCount(['documents', 'commitments', 'risicos']) + ->get(); + } + + /** + * Get a single project with full details. + */ + public function getWithDetails(int $id): Project + { + return Project::with([ + 'eigenaar', + 'speerpunt.thema', + 'fases', + 'risicos', + 'commitments.acties', + 'commitments.eigenaar', + 'documents', + 'besluiten', + 'teamleden', + 'afhankelijkheden.afhankelijkVan', + 'overdrachtsplannen.criteria', + ])->findOrFail($id); + } + + /** + * Create a new project with initial phase. + */ + public function create(array $data): Project + { + return DB::transaction(function () use ($data) { + $project = Project::create([ + 'naam' => $data['naam'], + 'beschrijving' => $data['beschrijving'] ?? '', + 'eigenaar_id' => $data['eigenaar_id'] ?? Auth::id(), + 'speerpunt_id' => $data['speerpunt_id'] ?? null, + 'status' => ProjectStatus::Signaal, + 'prioriteit' => $data['prioriteit'] ?? \App\Enums\Prioriteit::Midden, + 'startdatum' => $data['startdatum'] ?? now(), + 'streef_einddatum' => $data['streef_einddatum'] ?? null, + ]); + + // Create initial "signaal" phase + $project->fases()->create([ + 'type' => FaseType::Signaal, + 'status' => FaseStatus::Actief, + 'startdatum' => now(), + ]); + + // Assign creator as project owner + $project->teamleden()->attach(Auth::id(), ['rol' => \App\Enums\ProjectRol::Eigenaar]); + + $this->audit('created', $project); + + return $project; + }); + } + + /** + * Update project details. + */ + public function update(Project $project, array $data): Project + { + $project->update(array_filter([ + 'naam' => $data['naam'] ?? null, + 'beschrijving' => $data['beschrijving'] ?? null, + 'prioriteit' => $data['prioriteit'] ?? null, + 'speerpunt_id' => $data['speerpunt_id'] ?? null, + 'streef_einddatum' => $data['streef_einddatum'] ?? null, + ], fn ($v) => $v !== null)); + + $this->audit('updated', $project); + + return $project->fresh(); + } + + /** + * Transition a project to the next phase. + */ + public function transitionPhase(Project $project, ProjectStatus $newStatus): Project + { + return DB::transaction(function () use ($project, $newStatus) { + $oldStatus = $project->status; + + // Close current active phase + $project->fases() + ->where('status', FaseStatus::Actief) + ->update([ + 'status' => FaseStatus::Afgerond->value, + 'einddatum' => now(), + ]); + + // Create new phase (if it maps to a FaseType) + $faseType = FaseType::tryFrom($newStatus->value); + if ($faseType) { + $project->fases()->create([ + 'type' => $faseType, + 'status' => FaseStatus::Actief, + 'startdatum' => now(), + ]); + } + + // Update project status + $project->update(['status' => $newStatus]); + + $this->audit('phase_transition', $project, [ + 'from' => $oldStatus->value, + 'to' => $newStatus->value, + ]); + + return $project->fresh(['fases']); + }); + } + + /** + * Park a project (temporarily halt). + */ + public function park(Project $project, string $reason = ''): Project + { + return $this->transitionToSpecialStatus($project, ProjectStatus::Geparkeerd, $reason); + } + + /** + * Stop a project permanently. + */ + public function stop(Project $project, string $reason = ''): Project + { + return $this->transitionToSpecialStatus($project, ProjectStatus::Gestopt, $reason); + } + + private function transitionToSpecialStatus(Project $project, ProjectStatus $status, string $reason): Project + { + return DB::transaction(function () use ($project, $status, $reason) { + $oldStatus = $project->status; + + $project->fases() + ->where('status', FaseStatus::Actief) + ->update([ + 'status' => FaseStatus::Afgerond->value, + 'einddatum' => now(), + 'opmerkingen' => $reason, + ]); + + $project->update(['status' => $status]); + + $this->audit('status_change', $project, [ + 'from' => $oldStatus->value, + 'to' => $status->value, + 'reason' => $reason, + ]); + + return $project->fresh(); + }); + } + + private function audit(string $action, Project $project, ?array $extra = null): void + { + AuditLog::create([ + 'user_id' => Auth::id(), + 'action' => "project.{$action}", + 'entity_type' => 'project', + 'entity_id' => $project->id, + 'payload' => $extra, + ]); + } +} diff --git a/app/Services/ThemaService.php b/app/Services/ThemaService.php new file mode 100644 index 0000000..e49066f --- /dev/null +++ b/app/Services/ThemaService.php @@ -0,0 +1,60 @@ +get(); + } + + public function getForMap(): Collection + { + return Thema::with([ + 'speerpunten.projects' => function ($q) { + $q->with('eigenaar') + ->withCount(['documents', 'commitments', 'risicos']); + } + ])->get(); + } + + public function create(array $data): Thema + { + $thema = Thema::create([ + 'naam' => $data['naam'], + 'beschrijving' => $data['beschrijving'] ?? '', + 'prioriteit' => $data['prioriteit'] ?? \App\Enums\Prioriteit::Midden, + 'periode_start' => $data['periode_start'] ?? null, + 'periode_eind' => $data['periode_eind'] ?? null, + ]); + + AuditLog::create([ + 'user_id' => Auth::id(), + 'action' => 'thema.created', + 'entity_type' => 'thema', + 'entity_id' => $thema->id, + ]); + + return $thema; + } + + public function update(Thema $thema, array $data): Thema + { + $thema->update(array_filter($data, fn ($v) => $v !== null)); + + AuditLog::create([ + 'user_id' => Auth::id(), + 'action' => 'thema.updated', + 'entity_type' => 'thema', + 'entity_id' => $thema->id, + ]); + + return $thema->fresh(); + } +} diff --git a/bootstrap/app.php b/bootstrap/app.php index b5ffae3..c6493c2 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -7,6 +7,7 @@ use Illuminate\Foundation\Configuration\Middleware; return Application::configure(basePath: dirname(__DIR__)) ->withRouting( web: __DIR__.'/../routes/web.php', + api: __DIR__.'/../routes/api.php', commands: __DIR__.'/../routes/console.php', health: '/up', ) @@ -14,6 +15,9 @@ return Application::configure(basePath: dirname(__DIR__)) $middleware->web(append: [ \App\Http\Middleware\HandleInertiaRequests::class, ]); + $middleware->alias([ + 'role' => \App\Http\Middleware\CheckRole::class, + ]); }) ->withExceptions(function (Exceptions $exceptions): void { // diff --git a/bootstrap/providers.php b/bootstrap/providers.php index fc94ae6..b437418 100644 --- a/bootstrap/providers.php +++ b/bootstrap/providers.php @@ -4,4 +4,5 @@ use App\Providers\AppServiceProvider; return [ AppServiceProvider::class, + App\Providers\FortifyServiceProvider::class, ]; diff --git a/composer.json b/composer.json index 3b13b7f..fc8a51b 100644 --- a/composer.json +++ b/composer.json @@ -8,7 +8,9 @@ "require": { "php": "^8.3", "inertiajs/inertia-laravel": "^3.0", + "laravel/fortify": "^1.36", "laravel/framework": "^13.0", + "laravel/sanctum": "^4.0", "laravel/tinker": "^3.0" }, "require-dev": { diff --git a/composer.lock b/composer.lock index a07bfc5..1b9ae66 100644 --- a/composer.lock +++ b/composer.lock @@ -4,8 +4,63 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "1c6dc1e0948ef3b8fd1905a824a40c58", + "content-hash": "49877ead17f176f4f560ededac27ed08", "packages": [ + { + "name": "bacon/bacon-qr-code", + "version": "v3.0.4", + "source": { + "type": "git", + "url": "https://github.com/Bacon/BaconQrCode.git", + "reference": "3feed0e212b8412cc5d2612706744789b0615824" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Bacon/BaconQrCode/zipball/3feed0e212b8412cc5d2612706744789b0615824", + "reference": "3feed0e212b8412cc5d2612706744789b0615824", + "shasum": "" + }, + "require": { + "dasprid/enum": "^1.0.3", + "ext-iconv": "*", + "php": "^8.1" + }, + "require-dev": { + "phly/keep-a-changelog": "^2.12", + "phpunit/phpunit": "^10.5.11 || ^11.0.4", + "spatie/phpunit-snapshot-assertions": "^5.1.5", + "spatie/pixelmatch-php": "^1.2.0", + "squizlabs/php_codesniffer": "^3.9" + }, + "suggest": { + "ext-imagick": "to generate QR code images" + }, + "type": "library", + "autoload": { + "psr-4": { + "BaconQrCode\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-2-Clause" + ], + "authors": [ + { + "name": "Ben Scholzen 'DASPRiD'", + "email": "mail@dasprids.de", + "homepage": "https://dasprids.de/", + "role": "Developer" + } + ], + "description": "BaconQrCode is a QR code generator for PHP.", + "homepage": "https://github.com/Bacon/BaconQrCode", + "support": { + "issues": "https://github.com/Bacon/BaconQrCode/issues", + "source": "https://github.com/Bacon/BaconQrCode/tree/v3.0.4" + }, + "time": "2026-03-16T01:01:30+00:00" + }, { "name": "brick/math", "version": "0.14.8", @@ -135,6 +190,56 @@ ], "time": "2024-02-09T16:56:22+00:00" }, + { + "name": "dasprid/enum", + "version": "1.0.7", + "source": { + "type": "git", + "url": "https://github.com/DASPRiD/Enum.git", + "reference": "b5874fa9ed0043116c72162ec7f4fb50e02e7cce" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/DASPRiD/Enum/zipball/b5874fa9ed0043116c72162ec7f4fb50e02e7cce", + "reference": "b5874fa9ed0043116c72162ec7f4fb50e02e7cce", + "shasum": "" + }, + "require": { + "php": ">=7.1 <9.0" + }, + "require-dev": { + "phpunit/phpunit": "^7 || ^8 || ^9 || ^10 || ^11", + "squizlabs/php_codesniffer": "*" + }, + "type": "library", + "autoload": { + "psr-4": { + "DASPRiD\\Enum\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-2-Clause" + ], + "authors": [ + { + "name": "Ben Scholzen 'DASPRiD'", + "email": "mail@dasprids.de", + "homepage": "https://dasprids.de/", + "role": "Developer" + } + ], + "description": "PHP 7.1 enum implementation", + "keywords": [ + "enum", + "map" + ], + "support": { + "issues": "https://github.com/DASPRiD/Enum/issues", + "source": "https://github.com/DASPRiD/Enum/tree/1.0.7" + }, + "time": "2025-09-16T12:23:56+00:00" + }, { "name": "dflydev/dot-access-data", "version": "v3.0.3", @@ -1126,6 +1231,69 @@ }, "time": "2026-03-25T21:07:46+00:00" }, + { + "name": "laravel/fortify", + "version": "v1.36.2", + "source": { + "type": "git", + "url": "https://github.com/laravel/fortify.git", + "reference": "b36e0782e6f5f6cfbab34327895a63b7c4c031f9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/fortify/zipball/b36e0782e6f5f6cfbab34327895a63b7c4c031f9", + "reference": "b36e0782e6f5f6cfbab34327895a63b7c4c031f9", + "shasum": "" + }, + "require": { + "bacon/bacon-qr-code": "^3.0", + "ext-json": "*", + "illuminate/console": "^10.0|^11.0|^12.0|^13.0", + "illuminate/support": "^10.0|^11.0|^12.0|^13.0", + "php": "^8.1", + "pragmarx/google2fa": "^9.0" + }, + "require-dev": { + "orchestra/testbench": "^8.36|^9.15|^10.8|^11.0", + "phpstan/phpstan": "^1.10" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Laravel\\Fortify\\FortifyServiceProvider" + ] + }, + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Laravel\\Fortify\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "Backend controllers and scaffolding for Laravel authentication.", + "keywords": [ + "auth", + "laravel" + ], + "support": { + "issues": "https://github.com/laravel/fortify/issues", + "source": "https://github.com/laravel/fortify" + }, + "time": "2026-03-20T20:13:51+00:00" + }, { "name": "laravel/framework", "version": "v13.2.0", @@ -1406,6 +1574,69 @@ }, "time": "2026-03-23T14:35:33+00:00" }, + { + "name": "laravel/sanctum", + "version": "v4.3.1", + "source": { + "type": "git", + "url": "https://github.com/laravel/sanctum.git", + "reference": "e3b85d6e36ad00e5db2d1dcc27c81ffdf15cbf76" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/sanctum/zipball/e3b85d6e36ad00e5db2d1dcc27c81ffdf15cbf76", + "reference": "e3b85d6e36ad00e5db2d1dcc27c81ffdf15cbf76", + "shasum": "" + }, + "require": { + "ext-json": "*", + "illuminate/console": "^11.0|^12.0|^13.0", + "illuminate/contracts": "^11.0|^12.0|^13.0", + "illuminate/database": "^11.0|^12.0|^13.0", + "illuminate/support": "^11.0|^12.0|^13.0", + "php": "^8.2", + "symfony/console": "^7.0|^8.0" + }, + "require-dev": { + "mockery/mockery": "^1.6", + "orchestra/testbench": "^9.15|^10.8|^11.0", + "phpstan/phpstan": "^1.10" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Laravel\\Sanctum\\SanctumServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Laravel\\Sanctum\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "Laravel Sanctum provides a featherweight authentication system for SPAs and simple APIs.", + "keywords": [ + "auth", + "laravel", + "sanctum" + ], + "support": { + "issues": "https://github.com/laravel/sanctum/issues", + "source": "https://github.com/laravel/sanctum" + }, + "time": "2026-02-07T17:19:31+00:00" + }, { "name": "laravel/serializable-closure", "version": "v2.0.10", @@ -2606,6 +2837,75 @@ ], "time": "2026-02-16T23:10:27+00:00" }, + { + "name": "paragonie/constant_time_encoding", + "version": "v3.1.3", + "source": { + "type": "git", + "url": "https://github.com/paragonie/constant_time_encoding.git", + "reference": "d5b01a39b3415c2cd581d3bd3a3575c1ebbd8e77" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/paragonie/constant_time_encoding/zipball/d5b01a39b3415c2cd581d3bd3a3575c1ebbd8e77", + "reference": "d5b01a39b3415c2cd581d3bd3a3575c1ebbd8e77", + "shasum": "" + }, + "require": { + "php": "^8" + }, + "require-dev": { + "infection/infection": "^0", + "nikic/php-fuzzer": "^0", + "phpunit/phpunit": "^9|^10|^11", + "vimeo/psalm": "^4|^5|^6" + }, + "type": "library", + "autoload": { + "psr-4": { + "ParagonIE\\ConstantTime\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Paragon Initiative Enterprises", + "email": "security@paragonie.com", + "homepage": "https://paragonie.com", + "role": "Maintainer" + }, + { + "name": "Steve 'Sc00bz' Thomas", + "email": "steve@tobtu.com", + "homepage": "https://www.tobtu.com", + "role": "Original Developer" + } + ], + "description": "Constant-time Implementations of RFC 4648 Encoding (Base-64, Base-32, Base-16)", + "keywords": [ + "base16", + "base32", + "base32_decode", + "base32_encode", + "base64", + "base64_decode", + "base64_encode", + "bin2hex", + "encoding", + "hex", + "hex2bin", + "rfc4648" + ], + "support": { + "email": "info@paragonie.com", + "issues": "https://github.com/paragonie/constant_time_encoding/issues", + "source": "https://github.com/paragonie/constant_time_encoding" + }, + "time": "2025-09-24T15:06:41+00:00" + }, { "name": "phpoption/phpoption", "version": "1.9.5", @@ -2681,6 +2981,58 @@ ], "time": "2025-12-27T19:41:33+00:00" }, + { + "name": "pragmarx/google2fa", + "version": "v9.0.0", + "source": { + "type": "git", + "url": "https://github.com/antonioribeiro/google2fa.git", + "reference": "e6bc62dd6ae83acc475f57912e27466019a1f2cf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/antonioribeiro/google2fa/zipball/e6bc62dd6ae83acc475f57912e27466019a1f2cf", + "reference": "e6bc62dd6ae83acc475f57912e27466019a1f2cf", + "shasum": "" + }, + "require": { + "paragonie/constant_time_encoding": "^1.0|^2.0|^3.0", + "php": "^7.1|^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.9", + "phpunit/phpunit": "^7.5.15|^8.5|^9.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "PragmaRX\\Google2FA\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Antonio Carlos Ribeiro", + "email": "acr@antoniocarlosribeiro.com", + "role": "Creator & Designer" + } + ], + "description": "A One Time Password Authentication package, compatible with Google Authenticator.", + "keywords": [ + "2fa", + "Authentication", + "Two Factor Authentication", + "google2fa" + ], + "support": { + "issues": "https://github.com/antonioribeiro/google2fa/issues", + "source": "https://github.com/antonioribeiro/google2fa/tree/v9.0.0" + }, + "time": "2025-09-19T22:51:08+00:00" + }, { "name": "psr/clock", "version": "1.0.0", diff --git a/config/fortify.php b/config/fortify.php new file mode 100644 index 0000000..2dbf195 --- /dev/null +++ b/config/fortify.php @@ -0,0 +1,154 @@ + 'web', + + /* + |-------------------------------------------------------------------------- + | Fortify Password Broker + |-------------------------------------------------------------------------- + | + | Here you may specify which password broker Fortify can use when a user + | is resetting their password. This configured value should match one + | of your password brokers setup in your "auth" configuration file. + | + */ + + 'passwords' => 'users', + + /* + |-------------------------------------------------------------------------- + | Username / Email + |-------------------------------------------------------------------------- + | + | This value defines which model attribute should be considered as your + | application's "username" field. Typically, this might be the email + | address of the users but you are free to change this value here. + | + | Out of the box, Fortify expects forgot password and reset password + | requests to have a field named 'email'. If the application uses + | another name for the field you may define it below as needed. + | + */ + + 'username' => 'email', + + 'email' => 'email', + + /* + |-------------------------------------------------------------------------- + | Lowercase Usernames + |-------------------------------------------------------------------------- + | + | This value defines whether usernames should be lowercased before saving + | them in the database, as some database system string fields are case + | sensitive. You may disable this for your application if necessary. + | + */ + + 'lowercase_usernames' => true, + + /* + |-------------------------------------------------------------------------- + | Home Path + |-------------------------------------------------------------------------- + | + | Here you may configure the path where users will get redirected during + | authentication or password reset when the operations are successful + | and the user is authenticated. You are free to change this value. + | + */ + + 'home' => '/dashboard', + + /* + |-------------------------------------------------------------------------- + | Fortify Routes Prefix / Subdomain + |-------------------------------------------------------------------------- + | + | Here you may specify which prefix Fortify will assign to all the routes + | that it registers with the application. If necessary, you may change + | subdomain under which all of the Fortify routes will be available. + | + */ + + 'prefix' => '', + + 'domain' => null, + + /* + |-------------------------------------------------------------------------- + | Fortify Routes Middleware + |-------------------------------------------------------------------------- + | + | Here you may specify which middleware Fortify will assign to the routes + | that it registers with the application. If necessary, you may change + | these middleware but typically this provided default is preferred. + | + */ + + 'middleware' => ['web'], + + /* + |-------------------------------------------------------------------------- + | Rate Limiting + |-------------------------------------------------------------------------- + | + | By default, Fortify will throttle logins to five requests per minute for + | every email and IP address combination. However, if you would like to + | specify a custom rate limiter to call then you may specify it here. + | + */ + + 'limiters' => [ + 'login' => 'login', + 'two-factor' => 'two-factor', + ], + + /* + |-------------------------------------------------------------------------- + | Register View Routes + |-------------------------------------------------------------------------- + | + | Here you may specify if the routes returning views should be disabled as + | you may not need them when building your own application. This may be + | especially true if you're writing a custom single-page application. + | + */ + + 'views' => false, + + /* + |-------------------------------------------------------------------------- + | Features + |-------------------------------------------------------------------------- + | + | Some of the Fortify features are optional. You may disable the features + | by removing them from this array. You're free to only remove some of + | these features or you can even remove all of these if you need to. + | + */ + + 'features' => [ + Features::registration(), + Features::resetPasswords(), + Features::emailVerification(), + Features::updateProfileInformation(), + Features::updatePasswords(), + ], + +]; diff --git a/config/sanctum.php b/config/sanctum.php new file mode 100644 index 0000000..44527d6 --- /dev/null +++ b/config/sanctum.php @@ -0,0 +1,84 @@ + explode(',', env('SANCTUM_STATEFUL_DOMAINS', sprintf( + '%s%s', + 'localhost,localhost:3000,127.0.0.1,127.0.0.1:8000,::1', + Sanctum::currentApplicationUrlWithPort(), + // Sanctum::currentRequestHost(), + ))), + + /* + |-------------------------------------------------------------------------- + | Sanctum Guards + |-------------------------------------------------------------------------- + | + | This array contains the authentication guards that will be checked when + | Sanctum is trying to authenticate a request. If none of these guards + | are able to authenticate the request, Sanctum will use the bearer + | token that's present on an incoming request for authentication. + | + */ + + 'guard' => ['web'], + + /* + |-------------------------------------------------------------------------- + | Expiration Minutes + |-------------------------------------------------------------------------- + | + | This value controls the number of minutes until an issued token will be + | considered expired. This will override any values set in the token's + | "expires_at" attribute, but first-party sessions are not affected. + | + */ + + 'expiration' => null, + + /* + |-------------------------------------------------------------------------- + | Token Prefix + |-------------------------------------------------------------------------- + | + | Sanctum can prefix new tokens in order to take advantage of numerous + | security scanning initiatives maintained by open source platforms + | that notify developers if they commit tokens into repositories. + | + | See: https://docs.github.com/en/code-security/secret-scanning/about-secret-scanning + | + */ + + 'token_prefix' => env('SANCTUM_TOKEN_PREFIX', ''), + + /* + |-------------------------------------------------------------------------- + | Sanctum Middleware + |-------------------------------------------------------------------------- + | + | When authenticating your first-party SPA with Sanctum you may need to + | customize some of the middleware Sanctum uses while processing the + | request. You may change the middleware listed below as required. + | + */ + + 'middleware' => [ + 'authenticate_session' => Laravel\Sanctum\Http\Middleware\AuthenticateSession::class, + 'encrypt_cookies' => Illuminate\Cookie\Middleware\EncryptCookies::class, + 'validate_csrf_token' => Illuminate\Foundation\Http\Middleware\ValidateCsrfToken::class, + ], + +]; diff --git a/database/migrations/2026_04_01_114223_add_two_factor_columns_to_users_table.php b/database/migrations/2026_04_01_114223_add_two_factor_columns_to_users_table.php new file mode 100644 index 0000000..45739ef --- /dev/null +++ b/database/migrations/2026_04_01_114223_add_two_factor_columns_to_users_table.php @@ -0,0 +1,42 @@ +text('two_factor_secret') + ->after('password') + ->nullable(); + + $table->text('two_factor_recovery_codes') + ->after('two_factor_secret') + ->nullable(); + + $table->timestamp('two_factor_confirmed_at') + ->after('two_factor_recovery_codes') + ->nullable(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('users', function (Blueprint $table) { + $table->dropColumn([ + 'two_factor_secret', + 'two_factor_recovery_codes', + 'two_factor_confirmed_at', + ]); + }); + } +}; diff --git a/database/migrations/2026_04_01_114310_create_personal_access_tokens_table.php b/database/migrations/2026_04_01_114310_create_personal_access_tokens_table.php new file mode 100644 index 0000000..40ff706 --- /dev/null +++ b/database/migrations/2026_04_01_114310_create_personal_access_tokens_table.php @@ -0,0 +1,33 @@ +id(); + $table->morphs('tokenable'); + $table->text('name'); + $table->string('token', 64)->unique(); + $table->text('abilities')->nullable(); + $table->timestamp('last_used_at')->nullable(); + $table->timestamp('expires_at')->nullable()->index(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('personal_access_tokens'); + } +}; diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index 6b901f8..c2c23e7 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -2,24 +2,551 @@ namespace Database\Seeders; +use App\Enums\CommitmentStatus; +use App\Enums\FaseStatus; +use App\Enums\FaseType; +use App\Enums\Prioriteit; +use App\Enums\ProjectRol; +use App\Enums\ProjectStatus; +use App\Enums\SpeerpuntStatus; +use App\Models\Afhankelijkheid; +use App\Models\Commitment; +use App\Models\Document; +use App\Models\Fase; +use App\Models\Project; +use App\Models\Role; +use App\Models\Speerpunt; +use App\Models\Thema; use App\Models\User; -use Illuminate\Database\Console\Seeds\WithoutModelEvents; use Illuminate\Database\Seeder; +use Illuminate\Support\Facades\Hash; class DatabaseSeeder extends Seeder { - use WithoutModelEvents; - /** * Seed the application's database. */ public function run(): void { - // User::factory(10)->create(); + // ────────────────────────────────────────────────────────────── + // 1. System roles + // ────────────────────────────────────────────────────────────── + $roleAdmin = Role::create([ + 'naam' => 'admin', + 'beschrijving' => 'Volledige toegang tot het platform', + 'permissies' => ['*'], + ]); - User::factory()->create([ - 'name' => 'Test User', - 'email' => 'test@example.com', + $roleProjectOwner = Role::create([ + 'naam' => 'project_owner', + 'beschrijving' => 'Kan projecten beheren en bewerken', + 'permissies' => ['projects.manage', 'commitments.manage', 'documents.manage'], + ]); + + $roleTeamMember = Role::create([ + 'naam' => 'team_member', + 'beschrijving' => 'Kan bijdragen aan toegewezen projecten', + 'permissies' => ['projects.view', 'commitments.edit', 'documents.upload'], + ]); + + Role::create([ + 'naam' => 'viewer', + 'beschrijving' => 'Alleen-lezen toegang', + 'permissies' => ['projects.view', 'documents.view'], + ]); + + // ────────────────────────────────────────────────────────────── + // 2. Users + // ────────────────────────────────────────────────────────────── + $adminUser = User::create([ + 'name' => 'Admin Gebruiker', + 'email' => 'admin@innovatieplatform.nl', + 'password' => Hash::make('password'), + 'functie' => 'Platform Beheerder', + 'afdeling' => 'R&D Lab', + 'email_verified_at' => now(), + ]); + $adminUser->roles()->attach($roleAdmin); + + $testUser = User::create([ + 'name' => 'Rene de Ren', + 'email' => 'rene@wbd-rd.nl', + 'password' => Hash::make('password'), + 'functie' => 'R&D Engineer', + 'afdeling' => 'R&D Lab', + 'email_verified_at' => now(), + ]); + $testUser->roles()->attach($roleProjectOwner); + + $analyst = User::create([ + 'name' => 'Lisanne Bakker', + 'email' => 'l.bakker@wbd-rd.nl', + 'password' => Hash::make('password'), + 'functie' => 'Data Analist', + 'afdeling' => 'Watermanagement', + 'email_verified_at' => now(), + ]); + $analyst->roles()->attach($roleTeamMember); + + $engineer = User::create([ + 'name' => 'Joris van Dam', + 'email' => 'j.vandam@wbd-rd.nl', + 'password' => Hash::make('password'), + 'functie' => 'Senior Technisch Adviseur', + 'afdeling' => 'Infrastructuur', + 'email_verified_at' => now(), + ]); + $engineer->roles()->attach($roleTeamMember); + + // ────────────────────────────────────────────────────────────── + // 3. Themas (4 strategic themes) + // ────────────────────────────────────────────────────────────── + $themaWater = Thema::create([ + 'naam' => 'Waterkwaliteit', + 'beschrijving' => 'Verbetering van de kwaliteit van oppervlaktewater en grondwater door innovatieve monitoring- en zuiveringstechnieken.', + 'prioriteit' => Prioriteit::Hoog, + 'periode_start' => '2025-01-01', + 'periode_eind' => '2028-12-31', + ]); + + $themaInfra = Thema::create([ + 'naam' => 'Slimme Infrastructuur', + 'beschrijving' => 'Digitalisering en automatisering van waterkeringen, gemalen en sluizen voor efficiënter beheer en snellere responstijden.', + 'prioriteit' => Prioriteit::Hoog, + 'periode_start' => '2025-01-01', + 'periode_eind' => '2028-12-31', + ]); + + $themaData = Thema::create([ + 'naam' => 'Data-gedreven Beheer', + 'beschrijving' => 'Inzet van data-analyse, AI en digitale tweelingen voor betere besluitvorming in waterbeheer.', + 'prioriteit' => Prioriteit::Midden, + 'periode_start' => '2025-06-01', + 'periode_eind' => '2029-06-30', + ]); + + $themaDuurzaam = Thema::create([ + 'naam' => 'Duurzaamheid & Klimaatadaptatie', + 'beschrijving' => 'Innovaties gericht op energieneutraliteit, circulaire waterketens en klimaatrobuuste inrichting van het beheergebied.', + 'prioriteit' => Prioriteit::Midden, + 'periode_start' => '2025-01-01', + 'periode_eind' => '2030-12-31', + ]); + + // ────────────────────────────────────────────────────────────── + // 4. Speerpunten (2 per thema) + // ────────────────────────────────────────────────────────────── + + // Waterkwaliteit + $spWaterMonitoring = Speerpunt::create([ + 'thema_id' => $themaWater->id, + 'naam' => 'Real-time Watermonitoring', + 'beschrijving' => 'Continue meting van waterkwaliteitsparameters via IoT-sensoren in het beheergebied.', + 'eigenaar_id' => $testUser->id, + 'status' => SpeerpuntStatus::Actief, + ]); + + $spMicropollution = Speerpunt::create([ + 'thema_id' => $themaWater->id, + 'naam' => 'Microverontreinigingen', + 'beschrijving' => 'Detectie en verwijdering van opkomende stoffen zoals medicijnresten en PFAS.', + 'eigenaar_id' => $engineer->id, + 'status' => SpeerpuntStatus::Concept, + ]); + + // Slimme Infrastructuur + $spDigitaalBeheer = Speerpunt::create([ + 'thema_id' => $themaInfra->id, + 'naam' => 'Digitaal Kunstwerkenregister', + 'beschrijving' => 'Volledig digitaal beheer van kunstwerken met BIM-koppeling en conditiebewaking.', + 'eigenaar_id' => $engineer->id, + 'status' => SpeerpuntStatus::Actief, + ]); + + $spSmartGemaal = Speerpunt::create([ + 'thema_id' => $themaInfra->id, + 'naam' => 'Slimme Gemaalbesturing', + 'beschrijving' => 'Predictieve sturing van gemalen op basis van weersvoorspelling en waterstanden.', + 'eigenaar_id' => $testUser->id, + 'status' => SpeerpuntStatus::Actief, + ]); + + // Data-gedreven Beheer + $spDigitaalTwin = Speerpunt::create([ + 'thema_id' => $themaData->id, + 'naam' => 'Digitale Tweeling Watersysteem', + 'beschrijving' => 'Virtueel model van het watersysteem voor scenario-analyse en operationele ondersteuning.', + 'eigenaar_id' => $analyst->id, + 'status' => SpeerpuntStatus::Concept, + ]); + + $spAIVoorspelling = Speerpunt::create([ + 'thema_id' => $themaData->id, + 'naam' => 'AI-gestuurde Waterstandsvoorspelling', + 'beschrijving' => 'Machine learning modellen voor nauwkeurige korte- en middellangetermijnwaterstanden.', + 'eigenaar_id' => $analyst->id, + 'status' => SpeerpuntStatus::Actief, + ]); + + // Duurzaamheid + $spEnergieneutraal = Speerpunt::create([ + 'thema_id' => $themaDuurzaam->id, + 'naam' => 'Energieneutrale Zuivering', + 'beschrijving' => 'Zelfvoorzienende rwzi\'s door terugwinning van energie uit afvalwater.', + 'eigenaar_id' => $engineer->id, + 'status' => SpeerpuntStatus::Actief, + ]); + + $spCirculaireWater = Speerpunt::create([ + 'thema_id' => $themaDuurzaam->id, + 'naam' => 'Circulaire Waterketen', + 'beschrijving' => 'Terugwinning van grondstoffen (fosfaat, cellulose, warmte) uit afvalwater.', + 'eigenaar_id' => $testUser->id, + 'status' => SpeerpuntStatus::Concept, + ]); + + // ────────────────────────────────────────────────────────────── + // 5. Projects (10–12 spread across themes and lifecycle phases) + // ────────────────────────────────────────────────────────────── + + $projects = []; + + // --- Waterkwaliteit projects --- + $p1 = $this->createProject([ + 'speerpunt_id' => $spWaterMonitoring->id, + 'naam' => 'LoRaWAN Sensornetwerk Biesbosch', + 'beschrijving' => 'Uitrol van een draadloos sensornetwerk in het Biesbosch-gebied voor real-time meting van waterkwaliteitsparameters (pH, DO, troebelheid, geleidbaarheid).', + 'eigenaar_id' => $testUser->id, + 'status' => ProjectStatus::Pilot, + 'prioriteit' => Prioriteit::Hoog, + 'startdatum' => '2025-03-01', + 'streef_einddatum' => '2025-12-31', + ], [FaseType::Signaal, FaseType::Verkenning, FaseType::Concept, FaseType::Experiment, FaseType::Pilot]); + + $p2 = $this->createProject([ + 'speerpunt_id' => $spWaterMonitoring->id, + 'naam' => 'Drone-inspectie Waterbodem', + 'beschrijving' => 'Onderzoek naar de inzet van autonome onderwaterdrones voor sedimentkartering en vervuilingdetectie in kanalen en sloten.', + 'eigenaar_id' => $engineer->id, + 'status' => ProjectStatus::Experiment, + 'prioriteit' => Prioriteit::Midden, + 'startdatum' => '2025-06-01', + 'streef_einddatum' => '2026-06-30', + ], [FaseType::Signaal, FaseType::Verkenning, FaseType::Concept, FaseType::Experiment]); + + $p3 = $this->createProject([ + 'speerpunt_id' => $spMicropollution->id, + 'naam' => 'PFAS Detectiemethode Ontwikkeling', + 'beschrijving' => 'Ontwikkeling van een snelle en goedkope veldmethode voor PFAS-detectie in oppervlaktewater, als alternatief voor kostbare laboratoriumanalyse.', + 'eigenaar_id' => $analyst->id, + 'status' => ProjectStatus::Verkenning, + 'prioriteit' => Prioriteit::Hoog, + 'startdatum' => '2025-09-01', + 'streef_einddatum' => '2026-09-30', + ], [FaseType::Signaal, FaseType::Verkenning]); + + // --- Slimme Infrastructuur projects --- + $p4 = $this->createProject([ + 'speerpunt_id' => $spDigitaalBeheer->id, + 'naam' => 'BIM-model Gemaal De Donge', + 'beschrijving' => 'Digitale driedimensionale representatie van gemaal De Donge inclusief alle technische installaties, leidingen en elektrotechnische componenten.', + 'eigenaar_id' => $engineer->id, + 'status' => ProjectStatus::OverdrachtBouwen, + 'prioriteit' => Prioriteit::Midden, + 'startdatum' => '2024-09-01', + 'streef_einddatum' => '2025-06-30', + ], [FaseType::Signaal, FaseType::Verkenning, FaseType::Concept, FaseType::Experiment, FaseType::Pilot, FaseType::Besluitvorming, FaseType::OverdrachtBouwen]); + + $p5 = $this->createProject([ + 'speerpunt_id' => $spSmartGemaal->id, + 'naam' => 'Predictieve Gemaalbesturing Mark-Vliet', + 'beschrijving' => 'Implementatie van een ML-algoritme dat op basis van KNMI-weerdata en historische afvoerpatronen de optimale pompsturing berekent voor het Mark-Vliet systeem.', + 'eigenaar_id' => $testUser->id, + 'status' => ProjectStatus::Besluitvorming, + 'prioriteit' => Prioriteit::Hoog, + 'startdatum' => '2025-01-01', + 'streef_einddatum' => '2025-12-31', + ], [FaseType::Signaal, FaseType::Verkenning, FaseType::Concept, FaseType::Experiment, FaseType::Pilot, FaseType::Besluitvorming]); + + $p6 = $this->createProject([ + 'speerpunt_id' => $spSmartGemaal->id, + 'naam' => 'Remote Monitoring Waterkeringen', + 'beschrijving' => 'Continuemonitoring van primaire waterkeringen met IoT-sensoren voor zakking, piping-detectie en grondwaterstand.', + 'eigenaar_id' => $engineer->id, + 'status' => ProjectStatus::Signaal, + 'prioriteit' => Prioriteit::Laag, + 'startdatum' => '2026-01-01', + 'streef_einddatum' => null, + ], [FaseType::Signaal]); + + // --- Data-gedreven Beheer projects --- + $p7 = $this->createProject([ + 'speerpunt_id' => $spDigitaalTwin->id, + 'naam' => 'Digitale Tweeling Pilot Roosendaalse Vliet', + 'beschrijving' => 'Eerste proof-of-concept van een digitale tweeling voor het deelgebied Roosendaalse Vliet, gekoppeld aan het SOBEK-hydraulisch model.', + 'eigenaar_id' => $analyst->id, + 'status' => ProjectStatus::Concept, + 'prioriteit' => Prioriteit::Hoog, + 'startdatum' => '2025-07-01', + 'streef_einddatum' => '2026-12-31', + ], [FaseType::Signaal, FaseType::Verkenning, FaseType::Concept]); + + $p8 = $this->createProject([ + 'speerpunt_id' => $spAIVoorspelling->id, + 'naam' => 'AI Waterstandsmodel Hollandsch Diep', + 'beschrijving' => 'Training en validatie van een LSTM-neuraal netwerk voor 48-uurs waterstandsvoorspellingen op het Hollandsch Diep, ter vervanging van het huidige regressiemodel.', + 'eigenaar_id' => $analyst->id, + 'status' => ProjectStatus::Experiment, + 'prioriteit' => Prioriteit::Hoog, + 'startdatum' => '2025-04-01', + 'streef_einddatum' => '2026-03-31', + ], [FaseType::Signaal, FaseType::Verkenning, FaseType::Concept, FaseType::Experiment]); + + $p9 = $this->createProject([ + 'speerpunt_id' => $spAIVoorspelling->id, + 'naam' => 'Open Data Platform Waterschap', + 'beschrijving' => 'Ontwikkeling van een publiekstoegankelijk data-portaal voor het ontsluiten van historische en actuele meetdata van het waterschap.', + 'eigenaar_id' => $testUser->id, + 'status' => ProjectStatus::Geparkeerd, + 'prioriteit' => Prioriteit::Laag, + 'startdatum' => '2024-06-01', + 'streef_einddatum' => '2025-12-31', + ], [FaseType::Signaal, FaseType::Verkenning]); + + // --- Duurzaamheid projects --- + $p10 = $this->createProject([ + 'speerpunt_id' => $spEnergieneutraal->id, + 'naam' => 'Biogasopwaardering RWZI Bath', + 'beschrijving' => 'Opwaardering van slibvergistingsgas naar groengas-kwaliteit voor invoeding op het gasnet en verkoop aan een energiemaatschappij.', + 'eigenaar_id' => $engineer->id, + 'status' => ProjectStatus::Evaluatie, + 'prioriteit' => Prioriteit::Midden, + 'startdatum' => '2024-01-01', + 'streef_einddatum' => '2025-06-30', + ], [FaseType::Signaal, FaseType::Verkenning, FaseType::Concept, FaseType::Experiment, FaseType::Pilot, FaseType::Besluitvorming, FaseType::OverdrachtBouwen, FaseType::OverdrachtBeheer, FaseType::Evaluatie]); + + $p11 = $this->createProject([ + 'speerpunt_id' => $spCirculaireWater->id, + 'naam' => 'Fosfaatterugwinning Struviet', + 'beschrijving' => 'Implementatie van struvietkristallisatie-technologie op RWZI Nieuw-Vossemeer voor de terugwinning van fosfaat als meststof.', + 'eigenaar_id' => $testUser->id, + 'status' => ProjectStatus::Pilot, + 'prioriteit' => Prioriteit::Midden, + 'startdatum' => '2025-02-01', + 'streef_einddatum' => '2026-02-28', + ], [FaseType::Signaal, FaseType::Verkenning, FaseType::Concept, FaseType::Experiment, FaseType::Pilot]); + + $p12 = $this->createProject([ + 'speerpunt_id' => $spEnergieneutraal->id, + 'naam' => 'Warmteterugwinning Afvalwater Centrum', + 'beschrijving' => 'Pilotinstallatie voor warmtewisselaars op het rioolstelsel in Breda-centrum om warmte terug te winnen voor stadsverwarming.', + 'eigenaar_id' => $engineer->id, + 'status' => ProjectStatus::Verkenning, + 'prioriteit' => Prioriteit::Midden, + 'startdatum' => '2025-10-01', + 'streef_einddatum' => '2027-03-31', + ], [FaseType::Signaal, FaseType::Verkenning]); + + $projects = [$p1, $p2, $p3, $p4, $p5, $p6, $p7, $p8, $p9, $p10, $p11, $p12]; + + // ────────────────────────────────────────────────────────────── + // 6. Assign team members + // ────────────────────────────────────────────────────────────── + $p1->teamleden()->attach($analyst->id, ['rol' => ProjectRol::Lid->value]); + $p2->teamleden()->attach($testUser->id, ['rol' => ProjectRol::Reviewer->value]); + $p5->teamleden()->attach($analyst->id, ['rol' => ProjectRol::Lid->value]); + $p5->teamleden()->attach($engineer->id, ['rol' => ProjectRol::Reviewer->value]); + $p8->teamleden()->attach($engineer->id, ['rol' => ProjectRol::Lid->value]); + + // ────────────────────────────────────────────────────────────── + // 7. Commitments + // ────────────────────────────────────────────────────────────── + Commitment::create([ + 'project_id' => $p1->id, + 'beschrijving' => 'Installatie van 20 sensorknooppunten in het veld vóór einde Q3 2025', + 'eigenaar_id' => $testUser->id, + 'deadline' => '2025-09-30', + 'status' => CommitmentStatus::InUitvoering, + 'bron' => 'Stuurgroep Waterkwaliteit — 12 maart 2025', + ]); + + Commitment::create([ + 'project_id' => $p1->id, + 'beschrijving' => 'Validatierapport sensornauwkeurigheid opleveren aan dataplatformteam', + 'eigenaar_id' => $analyst->id, + 'deadline' => '2025-11-30', + 'status' => CommitmentStatus::Open, + 'bron' => 'Stuurgroep Waterkwaliteit — 12 maart 2025', + ]); + + Commitment::create([ + 'project_id' => $p5->id, + 'beschrijving' => 'Afstemming met Rijkswaterstaat over databeschikbaarheid afvoermetingen', + 'eigenaar_id' => $engineer->id, + 'deadline' => '2025-07-31', + 'status' => CommitmentStatus::Afgerond, + 'bron' => 'Projectstartup 15 januari 2025', + ]); + + Commitment::create([ + 'project_id' => $p5->id, + 'beschrijving' => 'Businesscase energiebesparing opstellen voor directie', + 'eigenaar_id' => $testUser->id, + 'deadline' => '2025-10-15', + 'status' => CommitmentStatus::Open, + 'bron' => 'Besluitvormingsrapport fase 5', + ]); + + Commitment::create([ + 'project_id' => $p8->id, + 'beschrijving' => 'Trainingsdata leveren: minimaal 5 jaar uurlijkse waterstandsmetingen', + 'eigenaar_id' => $analyst->id, + 'deadline' => '2025-08-01', + 'status' => CommitmentStatus::Afgerond, + 'bron' => 'Projectplan AI Waterstandsmodel v1.0', + ]); + + Commitment::create([ + 'project_id' => $p10->id, + 'beschrijving' => 'Eindrapportage energieopbrengst en milieuprestatie biogasinstallatie', + 'eigenaar_id' => $engineer->id, + 'deadline' => '2025-05-31', + 'status' => CommitmentStatus::Afgerond, + 'bron' => 'Evaluatieprogramma RWZI Bath', + ]); + + Commitment::create([ + 'project_id' => $p11->id, + 'beschrijving' => 'Technische specificaties struvietreactor aanleveren aan leverancier', + 'eigenaar_id' => $testUser->id, + 'deadline' => '2025-05-01', + 'status' => CommitmentStatus::Afgerond, + 'bron' => 'Pilotopzet Fosfaatterugwinning', + ]); + + // ────────────────────────────────────────────────────────────── + // 8. Documents + // ────────────────────────────────────────────────────────────── + Document::create([ + 'project_id' => $p1->id, + 'titel' => 'Technisch Ontwerp LoRaWAN Netwerk', + 'type' => 'technisch_ontwerp', + 'inhoud' => 'Systeemarchitectuur, gatewaylocaties, frequentieplan en databeheerprotocol voor het LoRaWAN sensornetwerk in het Biesboschgebied.', + 'versie' => 2, + 'auteur_id' => $engineer->id, + ]); + + Document::create([ + 'project_id' => $p1->id, + 'titel' => 'Projectplan Fase Pilot', + 'type' => 'projectplan', + 'inhoud' => 'Doelstellingen, activiteiten, planning en risico\'s voor de pilotfase van het LoRaWAN sensornetwerk.', + 'versie' => 1, + 'auteur_id' => $testUser->id, + ]); + + Document::create([ + 'project_id' => $p5->id, + 'titel' => 'Businesscase Predictieve Gemaalbesturing', + 'type' => 'businesscase', + 'inhoud' => 'Kosten-batenanalyse voor de implementatie van ML-gestuurde pompsturing, inclusief energiebesparingspotentieel en investeringskosten.', + 'versie' => 3, + 'auteur_id' => $testUser->id, + ]); + + Document::create([ + 'project_id' => $p8->id, + 'titel' => 'Modelarchitectuur LSTM Waterstandsvoorspelling', + 'type' => 'technisch_rapport', + 'inhoud' => 'Gedetailleerde beschrijving van de LSTM-netwerkarchitectuur, feature engineering, trainingsopzet en validatiemethodiek.', + 'versie' => 1, + 'auteur_id' => $analyst->id, + ]); + + Document::create([ + 'project_id' => $p10->id, + 'titel' => 'Evaluatierapport Biogasopwaardering RWZI Bath', + 'type' => 'evaluatierapport', + 'inhoud' => 'Eindresultaten van de biogasopwaarderingsinstallatie: energieproductie, CH4-gehalte, opbrengst en geleerde lessen.', + 'versie' => 1, + 'auteur_id' => $engineer->id, + ]); + + Document::create([ + 'project_id' => $p4->id, + 'titel' => 'BIM-protocol Kunstwerken v2.0', + 'type' => 'protocol', + 'inhoud' => 'Afspraken voor objectcodering, LOD-niveaus, attribuutvelden en uitwisselformaten (IFC) voor het BIM-model van kunstwerken.', + 'versie' => 2, + 'auteur_id' => $engineer->id, + ]); + + // ────────────────────────────────────────────────────────────── + // 9. Dependencies between projects + // ────────────────────────────────────────────────────────────── + + // AI Waterstandsmodel is afhankelijk van LoRaWAN Sensornetwerk (databron) + Afhankelijkheid::create([ + 'project_id' => $p8->id, + 'afhankelijk_van_project_id' => $p1->id, + 'type' => 'data', + 'beschrijving' => 'Het AI-model heeft real-time sensordata nodig uit het LoRaWAN netwerk als input feature.', + 'status' => 'actief', + ]); + + // Digitale Tweeling is afhankelijk van AI Waterstandsmodel + Afhankelijkheid::create([ + 'project_id' => $p7->id, + 'afhankelijk_van_project_id' => $p8->id, + 'type' => 'technisch', + 'beschrijving' => 'De digitale tweeling integreert de voorspellingsmodule van het AI waterstandsmodel.', + 'status' => 'actief', + ]); + + // Predictieve Gemaalbesturing is afhankelijk van AI Waterstandsmodel + Afhankelijkheid::create([ + 'project_id' => $p5->id, + 'afhankelijk_van_project_id' => $p8->id, + 'type' => 'technisch', + 'beschrijving' => 'De predictieve besturing gebruikt de 48-uurs waterstandsvoorspellingen als stuurinput.', + 'status' => 'actief', + ]); + + // Remote Monitoring Waterkeringen is afhankelijk van LoRaWAN Sensornetwerk + Afhankelijkheid::create([ + 'project_id' => $p6->id, + 'afhankelijk_van_project_id' => $p1->id, + 'type' => 'infrastructuur', + 'beschrijving' => 'Het monitoring-project maakt gebruik van de LoRaWAN-infrastructuur voor datatransport.', + 'status' => 'gepland', ]); } + + /** + * Helper: create a project with its completed and active phases. + * + * @param array $attributes + * @param FaseType[] $faseTypes All phase types in chronological order; the last one is the active phase. + */ + private function createProject(array $attributes, array $faseTypes): Project + { + $project = Project::create($attributes); + + // Attach the project owner as eigenaar in the pivot table + $project->teamleden()->attach($attributes['eigenaar_id'], ['rol' => ProjectRol::Eigenaar->value]); + + foreach ($faseTypes as $index => $faseType) { + $isLast = $index === count($faseTypes) - 1; + + Fase::create([ + 'project_id' => $project->id, + 'type' => $faseType, + 'status' => $isLast ? FaseStatus::Actief : FaseStatus::Afgerond, + 'startdatum' => now()->subMonths(count($faseTypes) - 1 - $index), + 'einddatum' => $isLast ? null : now()->subMonths(count($faseTypes) - 2 - $index), + ]); + } + + return $project; + } } diff --git a/package-lock.json b/package-lock.json index eb39380..d5568c4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5,9 +5,13 @@ "packages": { "": { "dependencies": { + "@fontsource/ibm-plex-mono": "^5.2.7", + "@fontsource/press-start-2p": "^5.2.7", + "@fontsource/vt323": "^5.2.7", "@inertiajs/vue3": "^3.0.1", "@vitejs/plugin-vue": "^6.0.5", "@vueuse/core": "^14.2.1", + "d3": "^7.9.0", "pinia": "^3.0.4", "vue": "^3.5.31" }, @@ -97,6 +101,33 @@ "tslib": "^2.4.0" } }, + "node_modules/@fontsource/ibm-plex-mono": { + "version": "5.2.7", + "resolved": "https://registry.npmjs.org/@fontsource/ibm-plex-mono/-/ibm-plex-mono-5.2.7.tgz", + "integrity": "sha512-MKAb8qV+CaiMQn2B0dIi1OV3565NYzp3WN5b4oT6LTkk+F0jR6j0ZN+5BKJiIhffDC3rtBULsYZE65+0018z9w==", + "license": "OFL-1.1", + "funding": { + "url": "https://github.com/sponsors/ayuhito" + } + }, + "node_modules/@fontsource/press-start-2p": { + "version": "5.2.7", + "resolved": "https://registry.npmjs.org/@fontsource/press-start-2p/-/press-start-2p-5.2.7.tgz", + "integrity": "sha512-ezSxYRchANoteja170ZHh2Tt4oejmVEvX42VFPOVVvERSZ68pcuZFjbhHfrK/sNjYmaGg656y7B3431ojO9PCQ==", + "license": "OFL-1.1", + "funding": { + "url": "https://github.com/sponsors/ayuhito" + } + }, + "node_modules/@fontsource/vt323": { + "version": "5.2.7", + "resolved": "https://registry.npmjs.org/@fontsource/vt323/-/vt323-5.2.7.tgz", + "integrity": "sha512-8JTMM23vMhQxin9Cn/ijty8cNwXW4INrln0VAJ2227Rz0CVfkzM3qr3l/CqudZJ6BXCnbCGUTdf2ym3cTNex8A==", + "license": "OFL-1.1", + "funding": { + "url": "https://github.com/sponsors/ayuhito" + } + }, "node_modules/@inertiajs/core": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/@inertiajs/core/-/core-3.0.1.tgz", @@ -1071,6 +1102,15 @@ "node": ">= 0.8" } }, + "node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, "node_modules/concurrently": { "version": "9.2.1", "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.2.1.tgz", @@ -1117,6 +1157,416 @@ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", "license": "MIT" }, + "node_modules/d3": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/d3/-/d3-7.9.0.tgz", + "integrity": "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==", + "license": "ISC", + "dependencies": { + "d3-array": "3", + "d3-axis": "3", + "d3-brush": "3", + "d3-chord": "3", + "d3-color": "3", + "d3-contour": "4", + "d3-delaunay": "6", + "d3-dispatch": "3", + "d3-drag": "3", + "d3-dsv": "3", + "d3-ease": "3", + "d3-fetch": "3", + "d3-force": "3", + "d3-format": "3", + "d3-geo": "3", + "d3-hierarchy": "3", + "d3-interpolate": "3", + "d3-path": "3", + "d3-polygon": "3", + "d3-quadtree": "3", + "d3-random": "3", + "d3-scale": "4", + "d3-scale-chromatic": "3", + "d3-selection": "3", + "d3-shape": "3", + "d3-time": "3", + "d3-time-format": "4", + "d3-timer": "3", + "d3-transition": "3", + "d3-zoom": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-axis": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz", + "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-brush": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz", + "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "3", + "d3-transition": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-chord": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz", + "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==", + "license": "ISC", + "dependencies": { + "d3-path": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-contour": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.2.tgz", + "integrity": "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==", + "license": "ISC", + "dependencies": { + "d3-array": "^3.2.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==", + "license": "ISC", + "dependencies": { + "delaunator": "5" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz", + "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==", + "license": "ISC", + "dependencies": { + "commander": "7", + "iconv-lite": "0.6", + "rw": "1" + }, + "bin": { + "csv2json": "bin/dsv2json.js", + "csv2tsv": "bin/dsv2dsv.js", + "dsv2dsv": "bin/dsv2dsv.js", + "dsv2json": "bin/dsv2json.js", + "json2csv": "bin/json2dsv.js", + "json2dsv": "bin/json2dsv.js", + "json2tsv": "bin/json2dsv.js", + "tsv2csv": "bin/dsv2dsv.js", + "tsv2json": "bin/dsv2json.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-fetch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz", + "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==", + "license": "ISC", + "dependencies": { + "d3-dsv": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-force": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz", + "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-quadtree": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz", + "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-geo": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.1.tgz", + "integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2.5.0 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-hierarchy": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz", + "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-polygon": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz", + "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-quadtree": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz", + "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-random": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz", + "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-interpolate": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" + } + }, + "node_modules/d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/delaunator": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.1.0.tgz", + "integrity": "sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ==", + "license": "ISC", + "dependencies": { + "robust-predicates": "^3.0.2" + } + }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -1465,6 +1915,27 @@ "integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==", "license": "MIT" }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", @@ -1949,6 +2420,12 @@ "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", "license": "MIT" }, + "node_modules/robust-predicates": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.3.tgz", + "integrity": "sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA==", + "license": "Unlicense" + }, "node_modules/rolldown": { "version": "1.0.0-rc.12", "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.12.tgz", @@ -1988,6 +2465,12 @@ "integrity": "sha512-HHMwmarRKvoFsJorqYlFeFRzXZqCt2ETQlEDOb9aqssrnVBB1/+xgTGtuTrIk5vzLNX1MjMtTf7W9z3tsSbrxw==", "license": "MIT" }, + "node_modules/rw": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", + "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==", + "license": "BSD-3-Clause" + }, "node_modules/rxjs": { "version": "7.8.2", "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", @@ -1998,6 +2481,12 @@ "tslib": "^2.1.0" } }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, "node_modules/shell-quote": { "version": "1.8.3", "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", diff --git a/package.json b/package.json index dbd0f4c..d624c73 100644 --- a/package.json +++ b/package.json @@ -15,9 +15,13 @@ "vite": "^8.0.0" }, "dependencies": { + "@fontsource/ibm-plex-mono": "^5.2.7", + "@fontsource/press-start-2p": "^5.2.7", + "@fontsource/vt323": "^5.2.7", "@inertiajs/vue3": "^3.0.1", "@vitejs/plugin-vue": "^6.0.5", "@vueuse/core": "^14.2.1", + "d3": "^7.9.0", "pinia": "^3.0.4", "vue": "^3.5.31" } diff --git a/resources/css/app.css b/resources/css/app.css index f1d8c73..36ce7f7 100644 --- a/resources/css/app.css +++ b/resources/css/app.css @@ -1 +1,29 @@ @import "tailwindcss"; +@import "@fontsource/vt323"; +@import "@fontsource/press-start-2p"; +@import "@fontsource/ibm-plex-mono"; + +@theme { + --color-bg-deep: #1a1a2e; + --color-bg-surface: #16213e; + --color-primary: #0f3460; + --color-accent-cyan: #00d2ff; + --color-accent-orange: #e94560; + --color-accent-green: #00ff88; + --color-accent-purple: #7b68ee; + --color-text-primary: #e8e8e8; + --color-text-secondary: #8892b0; + + --font-mono: 'IBM Plex Mono', monospace; + --font-retro: 'VT323', monospace; + --font-pixel: 'Press Start 2P', monospace; +} + +body { + background-color: var(--color-bg-deep); + color: var(--color-text-primary); + font-family: var(--font-mono); + margin: 0; + overflow: hidden; + height: 100vh; +} diff --git a/resources/js/Components/Cli/CliBar.vue b/resources/js/Components/Cli/CliBar.vue new file mode 100644 index 0000000..e9ef0b8 --- /dev/null +++ b/resources/js/Components/Cli/CliBar.vue @@ -0,0 +1,156 @@ + + + + + diff --git a/resources/js/Components/MetroMap/Breadcrumb.vue b/resources/js/Components/MetroMap/Breadcrumb.vue new file mode 100644 index 0000000..f72a5a3 --- /dev/null +++ b/resources/js/Components/MetroMap/Breadcrumb.vue @@ -0,0 +1,51 @@ + + + + + diff --git a/resources/js/Components/MetroMap/MetroCanvas.vue b/resources/js/Components/MetroMap/MetroCanvas.vue new file mode 100644 index 0000000..1e382ca --- /dev/null +++ b/resources/js/Components/MetroMap/MetroCanvas.vue @@ -0,0 +1,354 @@ + + + + + diff --git a/resources/js/Components/MetroMap/NodePreview.vue b/resources/js/Components/MetroMap/NodePreview.vue new file mode 100644 index 0000000..7845ae0 --- /dev/null +++ b/resources/js/Components/MetroMap/NodePreview.vue @@ -0,0 +1,147 @@ + + + + + diff --git a/resources/js/Pages/Auth/ForgotPassword.vue b/resources/js/Pages/Auth/ForgotPassword.vue new file mode 100644 index 0000000..3b6717b --- /dev/null +++ b/resources/js/Pages/Auth/ForgotPassword.vue @@ -0,0 +1,346 @@ + + + + + diff --git a/resources/js/Pages/Auth/Login.vue b/resources/js/Pages/Auth/Login.vue new file mode 100644 index 0000000..788dd48 --- /dev/null +++ b/resources/js/Pages/Auth/Login.vue @@ -0,0 +1,377 @@ + + + + + diff --git a/resources/js/Pages/Auth/Register.vue b/resources/js/Pages/Auth/Register.vue new file mode 100644 index 0000000..5bbde87 --- /dev/null +++ b/resources/js/Pages/Auth/Register.vue @@ -0,0 +1,380 @@ + + + + + diff --git a/resources/js/Pages/Auth/ResetPassword.vue b/resources/js/Pages/Auth/ResetPassword.vue new file mode 100644 index 0000000..d1d5737 --- /dev/null +++ b/resources/js/Pages/Auth/ResetPassword.vue @@ -0,0 +1,368 @@ + + + + + diff --git a/resources/js/Pages/Auth/VerifyEmail.vue b/resources/js/Pages/Auth/VerifyEmail.vue new file mode 100644 index 0000000..5d05a11 --- /dev/null +++ b/resources/js/Pages/Auth/VerifyEmail.vue @@ -0,0 +1,344 @@ + + + + + diff --git a/resources/js/Pages/Dashboard.vue b/resources/js/Pages/Dashboard.vue index 8e69796..f6c5268 100644 --- a/resources/js/Pages/Dashboard.vue +++ b/resources/js/Pages/Dashboard.vue @@ -1,10 +1,90 @@ + + diff --git a/resources/js/Pages/Map/MetroMap.vue b/resources/js/Pages/Map/MetroMap.vue new file mode 100644 index 0000000..e4b1494 --- /dev/null +++ b/resources/js/Pages/Map/MetroMap.vue @@ -0,0 +1,127 @@ + + + + + diff --git a/routes/api.php b/routes/api.php new file mode 100644 index 0000000..ccc387f --- /dev/null +++ b/routes/api.php @@ -0,0 +1,8 @@ +user(); +})->middleware('auth:sanctum'); diff --git a/routes/web.php b/routes/web.php index 94d28e5..8be54df 100644 --- a/routes/web.php +++ b/routes/web.php @@ -1,12 +1,39 @@ redirect('/map')); -Route::get('/dashboard', function () { - return Inertia::render('Dashboard'); +// Authenticated routes +Route::middleware(['auth', 'verified'])->group(function () { + // Metro Map + Route::get('/map', [MapController::class, 'index'])->name('map'); + Route::get('/map/project/{project}', [MapController::class, 'project'])->name('map.project'); + + // API endpoints for dynamic map data + Route::prefix('api')->group(function () { + Route::get('/map/strategy', [MapController::class, 'apiStrategy'])->name('api.map.strategy'); + Route::get('/map/project/{project}', [MapController::class, 'apiProject'])->name('api.map.project'); + }); + + // Projects + Route::post('/projects', [ProjectController::class, 'store'])->name('projects.store'); + Route::get('/projects/{project}', [ProjectController::class, 'show'])->name('projects.show'); + Route::put('/projects/{project}', [ProjectController::class, 'update'])->name('projects.update'); + Route::post('/projects/{project}/transition', [ProjectController::class, 'transition'])->name('projects.transition'); + Route::post('/projects/{project}/park', [ProjectController::class, 'park'])->name('projects.park'); + Route::post('/projects/{project}/stop', [ProjectController::class, 'stop'])->name('projects.stop'); + Route::delete('/projects/{project}', [ProjectController::class, 'destroy'])->name('projects.destroy'); + + // Themas + Route::get('/themas', [ThemaController::class, 'index'])->name('themas.index'); + Route::post('/themas', [ThemaController::class, 'store'])->name('themas.store'); + Route::put('/themas/{thema}', [ThemaController::class, 'update'])->name('themas.update'); + + // Dashboard (redirects to map) + Route::get('/dashboard', fn () => redirect('/map'))->name('dashboard'); });