Implement project board status workflow

This commit is contained in:
2026-03-25 08:04:34 +01:00
parent f7a0543ea1
commit 7e767b0c6c
22 changed files with 1076 additions and 7 deletions
@@ -27,6 +27,8 @@ class ProjectBacklogController extends Controller
);
return Inertia::render('projects/Backlog', [
'boardColumns' => $project->boardColumns()->get(),
'canEditTickets' => $this->projectPermissionService->userHasPermission($request->user(), $project, 'edit'),
'canCreateTickets' => $this->projectPermissionService->userHasPermission($request->user(), $project, 'create'),
'project' => $project,
'status' => session('status'),
@@ -0,0 +1,88 @@
<?php
namespace App\Http\Controllers;
use App\Actions\ProjectPermissions\ProjectPermissionService;
use App\Actions\Tickets\CreateTicketWorklogEntry;
use App\Http\Requests\MoveProjectTicketRequest;
use App\Models\Project;
use App\Models\Ticket;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Inertia\Inertia;
use Inertia\Response;
class ProjectBoardController extends Controller
{
public function __construct(
private readonly CreateTicketWorklogEntry $createTicketWorklogEntry,
private readonly ProjectPermissionService $projectPermissionService,
) {}
public function index(Request $request, Project $project): Response
{
abort_unless(
$this->projectPermissionService->userCanViewProject($request->user(), $project),
403,
);
return Inertia::render('projects/Board', [
'boardColumns' => $project->boardColumns()
->with([
'tickets' => fn ($query) => $query
->with(['ticketPriority', 'ticketType']),
])
->get(),
'canEditTickets' => $this->projectPermissionService->userHasPermission($request->user(), $project, 'edit'),
'project' => $project,
'status' => session('status'),
]);
}
public function move(
MoveProjectTicketRequest $request,
Project $project,
Ticket $ticket,
): RedirectResponse {
abort_unless(
$this->projectPermissionService->userHasPermission($request->user(), $project, 'edit'),
403,
);
$destinationColumn = $project->boardColumns()
->find($request->boardColumnId());
$originBoardColumnId = $ticket->board_column_id;
$originLabel = $ticket->boardColumn?->name ?? 'Backlog';
$destinationLabel = $destinationColumn?->name ?? 'Backlog';
if ($ticket->board_column_id === $request->boardColumnId()) {
return to_route($request->redirectRouteName(), $project)
->with('status', "{$ticket->ticket_key} already belongs to {$destinationLabel}.");
}
$ticket->update([
'board_column_id' => $destinationColumn?->id,
]);
$this->createTicketWorklogEntry->handle(
$ticket->fresh(),
$request->user(),
'status_changed',
"Moved from {$originLabel} to {$destinationLabel}.",
[
'from' => [
'board_column_id' => $originBoardColumnId,
'label' => $originLabel,
],
'to' => [
'board_column_id' => $destinationColumn?->id,
'label' => $destinationLabel,
],
],
);
return to_route($request->redirectRouteName(), $project)
->with('status', "{$ticket->ticket_key} moved to {$destinationLabel}.");
}
}
@@ -0,0 +1,57 @@
<?php
namespace App\Http\Requests;
use App\Models\Project;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class MoveProjectTicketRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return $this->user() !== null;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
/** @var Project $project */
$project = $this->route('project');
return [
'board_column_id' => [
'nullable',
'integer',
Rule::exists('board_columns', 'id')->where('project_id', $project->id),
],
'redirect_to' => ['required', 'string', Rule::in(['backlog', 'board'])],
];
}
public function boardColumnId(): ?int
{
$boardColumnId = $this->validated('board_column_id');
if ($boardColumnId === null || $boardColumnId === '') {
return null;
}
return (int) $boardColumnId;
}
public function redirectRouteName(): string
{
return $this->validated('redirect_to') === 'backlog'
? 'projects.backlog.index'
: 'projects.board.index';
}
}