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) <noreply@anthropic.com>
187 lines
5.7 KiB
PHP
187 lines
5.7 KiB
PHP
<?php
|
|
|
|
namespace App\Services;
|
|
|
|
use App\Models\Project;
|
|
use App\Models\AuditLog;
|
|
use App\Enums\ProjectStatus;
|
|
use App\Enums\FaseType;
|
|
use App\Enums\FaseStatus;
|
|
use Illuminate\Database\Eloquent\Collection;
|
|
use Illuminate\Support\Facades\Auth;
|
|
use Illuminate\Support\Facades\DB;
|
|
|
|
class ProjectService
|
|
{
|
|
/**
|
|
* Get all projects with their relationships for the metro map.
|
|
*/
|
|
public function getAllForMap(): Collection
|
|
{
|
|
return Project::with(['eigenaar', 'speerpunt.thema', 'fases', 'risicos', 'commitments'])
|
|
->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,
|
|
]);
|
|
}
|
|
}
|