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) <noreply@anthropic.com>
This commit is contained in:
186
app/Services/ProjectService.php
Normal file
186
app/Services/ProjectService.php
Normal file
@@ -0,0 +1,186 @@
|
||||
<?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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user