Files
project-manager/src/Http/Controllers/CardStatusController.php
T
aneurinandClaude Sonnet 5 c47c800d01 Add per-project card statuses and a kanban board
Statuses
- Migration 006: card_statuses table (project-scoped) and cards.status_id, a
  nullable FK with ON DELETE SET NULL. Every new project is seeded with
  "To do" / "Doing" / "Done"; GET /api/projects/{id}/statuses lists them.
- New cards have no status -- they sit in an "inbox" until moved.

Project view
- Full-width and tabbed: "All tasks" (a flat list, sorted by name
  case-insensitively) and "Kanban" (Inbox plus one column per status).
- Drag a card within or between columns to reorder / restatus; the Inbox
  column has its own name + Add form.

Ordering
- Migration 007: `position` is now a dense 0..n-1 rank within a
  (project_id, status_id) column, not a project-wide order. New composite
  index idx_cards_project_status_position; existing rows re-ranked.
- PUT /api/projects/{id}/cards/order takes { status_id, card_ids } and sets one
  column's contents and order, re-parenting moved-in cards and re-packing their
  source column in a single transaction. PATCH status_id appends the card to the
  end of the destination column.

58 phpunit tests pass; the frontend type-checks and builds.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-04 13:37:17 +01:00

65 lines
1.8 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Http\Controllers;
use App\Exception\ApiException;
use App\Repository\CardStatusRepository;
use App\Repository\ProjectRepository;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
/**
* Read-only listing of a project's card statuses. Statuses are seeded when the
* project is created; there is no create/update/delete yet.
*/
final class CardStatusController extends Controller
{
public function __construct(
private readonly ProjectRepository $projects,
private readonly CardStatusRepository $statuses,
) {
}
/**
* GET /api/projects/{projectId}/statuses
*/
public function index(Request $request, Response $response, array $args): Response
{
$projectId = $this->requireOwnedProjectId($request, $args);
return $this->json($response, [
'statuses' => array_map($this->present(...), $this->statuses->allForProject($projectId)),
]);
}
/**
* @param array<string, string> $args
*/
private function requireOwnedProjectId(Request $request, array $args): int
{
$project = $this->projects->findOwnedBy((int) $args['projectId'], $this->user($request)['id']);
if ($project === null) {
throw new ApiException('Project not found.', 404);
}
return $project['id'];
}
/**
* @param array{id: int, project_id: int, name: string, position: int, created_at: string, updated_at: string} $status
* @return array<string, mixed>
*/
private function present(array $status): array
{
return [
'id' => $status['id'],
'project_id' => $status['project_id'],
'name' => $status['name'],
'position' => $status['position'],
];
}
}