false, 'error' => "Invalid {$fieldName}. Use YYYY-MM-DD." ], 400); } return $value; } function requireProjectAccess(string $projectId, int $userId): void { requireProjectAccessForUser($userId, $projectId); } $user = requireApiAuth(); $api = $_GET['api'] ?? ''; switch ($api) { case 'ListVersions': { $projectId = strtoupper(trim($_GET['project_id'] ?? '')); if ($projectId === '') { jsonResponse([ 'success' => false, 'error' => 'Missing project_id.' ], 400); } requireProjectAccess($projectId, (int)$user['id']); $stmt = db()->prepare( 'SELECT id FROM versions WHERE project = ? ORDER BY created_date DESC, id DESC' ); $stmt->execute([$projectId]); jsonResponse([ 'success' => true, 'versions' => array_map('intval', $stmt->fetchAll(PDO::FETCH_COLUMN)) ]); } case 'VersionInfo': { $versionId = (int)($_GET['version_id'] ?? 0); if ($versionId <= 0) { jsonResponse([ 'success' => false, 'error' => 'Missing or invalid version_id.' ], 400); } $stmt = db()->prepare( 'SELECT versions.id, versions.name, versions.description, versions.created_date, versions.due_date, versions.released_date, versions.project FROM versions INNER JOIN projects ON projects.id = versions.project WHERE versions.id = ? LIMIT 1' ); $stmt->execute([$versionId]); $version = $stmt->fetch(); if (!$version) { jsonResponse([ 'success' => false, 'error' => 'Version not found.' ], 404); } requireProjectAccessForUser((int)$user['id'], (string)$version['project']); jsonResponse([ 'success' => true, 'version' => $version ]); } case 'Create': { if ($_SERVER['REQUEST_METHOD'] !== 'POST') { jsonResponse([ 'success' => false, 'error' => 'Create requires POST.' ], 405); } $data = getInputData(); $projectId = strtoupper(trim($data['project_id'] ?? $data['project'] ?? '')); $name = trim($data['name'] ?? ''); $description = array_key_exists('description', $data) ? trim((string)$data['description']) : null; $dueDate = array_key_exists('due_date', $data) ? normalizeDateValue($data['due_date'], 'due_date') : null; $releasedDate = array_key_exists('released_date', $data) ? normalizeDateValue($data['released_date'], 'released_date') : null; if ($projectId === '') { jsonResponse([ 'success' => false, 'error' => 'Project id is required.' ], 400); } requireProjectRight((int)$user['id'], $projectId, 'Create Versions'); if ($name === '') { jsonResponse([ 'success' => false, 'error' => 'Version name is required.' ], 400); } if (strlen($name) > 128) { jsonResponse([ 'success' => false, 'error' => 'Version name is too long.' ], 400); } $stmt = db()->prepare( 'INSERT INTO versions (name, description, created_date, due_date, released_date, project) VALUES (?, ?, CURDATE(), ?, ?, ?)' ); $stmt->execute([ $name, $description, $dueDate, $releasedDate, $projectId ]); jsonResponse([ 'success' => true, 'version_id' => (int)db()->lastInsertId() ], 201); } case 'Edit': { if ($_SERVER['REQUEST_METHOD'] !== 'POST') { jsonResponse([ 'success' => false, 'error' => 'Edit requires POST.' ], 405); } $versionId = (int)($_GET['version_id'] ?? 0); if ($versionId <= 0) { jsonResponse([ 'success' => false, 'error' => 'Missing or invalid version_id.' ], 400); } $data = getInputData(); $projectStmt = db()->prepare( 'SELECT project FROM versions WHERE id = ? LIMIT 1' ); $projectStmt->execute([$versionId]); $versionProject = $projectStmt->fetchColumn(); if (!$versionProject) { jsonResponse([ 'success' => false, 'error' => 'Version not found.' ], 404); } requireProjectRight((int)$user['id'], (string)$versionProject, 'Edit Versions'); $fields = []; $values = []; if (array_key_exists('name', $data)) { $name = trim((string)$data['name']); if ($name === '') { jsonResponse([ 'success' => false, 'error' => 'Version name cannot be empty.' ], 400); } if (strlen($name) > 128) { jsonResponse([ 'success' => false, 'error' => 'Version name is too long.' ], 400); } $fields[] = 'versions.name = ?'; $values[] = $name; } if (array_key_exists('description', $data)) { $description = trim((string)$data['description']); $fields[] = 'versions.description = ?'; $values[] = $description !== '' ? $description : null; } if (array_key_exists('due_date', $data)) { $fields[] = 'versions.due_date = ?'; $values[] = normalizeDateValue($data['due_date'], 'due_date'); } if (array_key_exists('released_date', $data)) { $fields[] = 'versions.released_date = ?'; $values[] = normalizeDateValue($data['released_date'], 'released_date'); } if (empty($fields)) { jsonResponse([ 'success' => false, 'error' => 'No editable fields provided.' ], 400); } $values[] = $versionId; $stmt = db()->prepare( 'UPDATE versions INNER JOIN projects ON projects.id = versions.project SET ' . implode(', ', $fields) . ' WHERE versions.id = ?' ); $stmt->execute($values); if ($stmt->rowCount() === 0) { $existsStmt = db()->prepare( 'SELECT versions.id FROM versions INNER JOIN projects ON projects.id = versions.project WHERE versions.id = ? LIMIT 1' ); $existsStmt->execute([$versionId]); if ($existsStmt->fetch()) { jsonResponse([ 'success' => true, 'version_id' => $versionId ]); } jsonResponse([ 'success' => false, 'error' => 'Version not found.' ], 404); } jsonResponse([ 'success' => true, 'version_id' => $versionId ]); } default: { jsonResponse([ 'success' => false, 'error' => 'Unknown API action.' ], 404); } }