user()->can('viewAny', Project::class), 403); $query = Project::query()->with('creator')->withCount(['revisions', 'listingLinks']); if ($request->filled('stage') && in_array($request->string('stage')->toString(), Project::STAGES, true)) { $query->where('stage', $request->string('stage')->toString()); } if ($request->filled('search')) { $term = '%'.$request->string('search')->toString().'%'; $query->where(fn ($q) => $q->where('title', 'like', $term)->orWhere('reference', 'like', $term)); } return view('projects.index', [ 'projects' => $query->orderByDesc('created_at')->paginate(20)->withQueryString(), 'stages' => Project::STAGES, ]); } public function create(Request $request): View { abort_unless($request->user()->can('create', Project::class), 403); return view('projects.create'); } public function store(Request $request): RedirectResponse { abort_unless($request->user()->can('create', Project::class), 403); $data = $request->validate([ 'reference' => ['required', 'string', 'max:64', 'unique:projects,reference'], 'title' => ['required', 'string', 'max:255'], 'notes' => ['nullable', 'string'], 'currency' => ['required', 'string', 'size:3', Rule::in(ProjectCosting::ALLOWED_CURRENCIES)], ]); $project = Project::create($data + ['created_by' => $request->user()->id]); ProjectEvent::record($project->id, 'created', $request->user()->id); return redirect()->route('projects.show', $project)->with('status', 'Project created.'); } public function show(Request $request, Project $project): View { abort_unless($request->user()->can('view', $project), 403); $project->load([ 'revisions' => fn ($q) => $q->orderByDesc('revision_number'), 'revisions.files', 'revisions.creator', 'revisions.approver', 'listingLinks' => fn ($q) => $q->orderByDesc('created_at'), 'listingLinks.liveRelease.revision', 'listingLinks.liveRelease.costSnapshot', 'costSnapshots' => fn ($q) => $q->orderByDesc('recorded_at'), 'costSnapshots.lines', 'costSnapshots.revision', 'events.actor', ]); return view('projects.show', ['project' => $project]); } public function edit(Request $request, Project $project): View { abort_unless($request->user()->can('update', $project), 403); return view('projects.edit', ['project' => $project]); } public function update(Request $request, Project $project): RedirectResponse { abort_unless($request->user()->can('update', $project), 403); $data = $request->validate([ 'title' => ['sometimes', 'string', 'max:255'], 'notes' => ['sometimes', 'nullable', 'string'], 'stage' => ['sometimes', Rule::in(Project::STAGES)], ]); if (($data['stage'] ?? null) === 'live' && ! ProjectLifecycle::canMoveToLive($project)) { return back()->withErrors(['stage' => 'A project can only move to Live once a listing mapping has an active release.']); } $fromStage = $project->stage; $project->update($data); if (isset($data['stage']) && $data['stage'] !== $fromStage) { ProjectEvent::record($project->id, 'stage_changed', $request->user()->id, metadata: ['from' => $fromStage, 'to' => $data['stage']]); } return redirect()->route('projects.show', $project)->with('status', 'Project updated.'); } public function archive(Request $request, Project $project): RedirectResponse { abort_unless($request->user()->can('archive', $project), 403); $project->update(['archived_at' => now()]); ProjectEvent::record($project->id, 'archived', $request->user()->id); return redirect()->route('projects.show', $project)->with('status', 'Project archived.'); } }