(int) $p['id']], ); Response::json(['data' => array_map([self::class, 'present'], $rows)]); } public function store(Request $req, array $params): void { $p = ProjectsController::findOr404($params['slug']); $body = $req->json(); $v = new Validator($body); $v->required('title')->string('title', 200); $v->string('description', 5000); $v->date('step_date'); $v->boolean('is_done'); $v->integer('sort_order', 0); $v->check(); $sortOrder = isset($body['sort_order']) ? (int) $body['sort_order'] : self::nextSortOrder((int) $p['id']); $id = Database::insert('roadmap_steps', [ 'project_id' => (int) $p['id'], 'title' => (string) $body['title'], 'description' => $body['description'] ?? null, 'step_date' => $body['step_date'] ?? null, 'is_done' => self::toBool($body['is_done'] ?? false) ? 1 : 0, 'sort_order' => $sortOrder, ]); ActivityLogger::log( 'added_step', sprintf('a ajouté l\'étape « %s » au projet « %s »', (string) $body['title'], $p['title']), (int) $p['id'], ); Response::json(self::present(self::findStepOr404($id, (int) $p['id'])), 201); } public function update(Request $req, array $params): void { $p = ProjectsController::findOr404($params['slug']); $step = self::findStepOr404((int) $params['id'], (int) $p['id']); $body = $req->json(); $v = new Validator($body); $v->string('title', 200); $v->string('description', 5000); $v->date('step_date'); $v->boolean('is_done'); $v->integer('sort_order', 0); $v->check(); $update = []; foreach (['title', 'description', 'step_date', 'sort_order'] as $f) { if (array_key_exists($f, $body)) { $update[$f] = $body[$f]; } } if (isset($update['sort_order'])) { $update['sort_order'] = (int) $update['sort_order']; } $becameDone = false; if (array_key_exists('is_done', $body)) { $newDone = self::toBool($body['is_done']); $oldDone = (bool) $step['is_done']; $update['is_done'] = $newDone ? 1 : 0; if ($newDone && !$oldDone) { $becameDone = true; } } if ($update !== []) { Database::update('roadmap_steps', $update, 'id = :id', [':id' => (int) $step['id']]); } if ($becameDone) { ActivityLogger::log( 'completed_step', sprintf('a terminé l\'étape « %s » du projet « %s »', $step['title'], $p['title']), (int) $p['id'], ); } Response::json(self::present(self::findStepOr404((int) $step['id'], (int) $p['id']))); } public function destroy(Request $req, array $params): void { $p = ProjectsController::findOr404($params['slug']); $step = self::findStepOr404((int) $params['id'], (int) $p['id']); Database::delete('roadmap_steps', 'id = :id', [':id' => (int) $step['id']]); Response::noContent(); } private static function findStepOr404(int $id, int $projectId): array { $row = Database::fetchOne( 'SELECT id, project_id, title, description, step_date, is_done, sort_order, created_at, updated_at FROM roadmap_steps WHERE id = :id AND project_id = :pid', [':id' => $id, ':pid' => $projectId], ); if ($row === null) { throw new \ApiException(404, ['error' => 'not_found']); } return $row; } /** Sort order par défaut : MAX + 10 par projet (laisse de l'espace pour insertion ultérieure). */ private static function nextSortOrder(int $projectId): int { return (int) Database::fetchValue( 'SELECT COALESCE(MAX(sort_order), 0) + 10 FROM roadmap_steps WHERE project_id = :pid', [':pid' => $projectId], ); } private static function toBool(mixed $v): bool { if (is_bool($v)) return $v; if (is_int($v)) return $v !== 0; if (is_string($v)) return in_array(strtolower($v), ['true', '1', 'yes', 'on'], true); return (bool) $v; } private static function present(array $row): array { return [ 'id' => (int) $row['id'], 'project_id' => (int) $row['project_id'], 'title' => $row['title'], 'description' => $row['description'], 'step_date' => $row['step_date'], 'is_done' => (bool) $row['is_done'], 'sort_order' => (int) $row['sort_order'], 'created_at' => $row['created_at'], 'updated_at' => $row['updated_at'], ]; } }