false, 'error' => 'Color must be a hex color like #4ea1ff.' ], 400); } return strtolower($color); } function requireState(int $stateId, string $label = 'State'): void { if ($stateId <= 0) { jsonResponse([ 'success' => false, 'error' => "{$label} must be valid." ], 400); } $stmt = db()->prepare('SELECT id FROM task_states WHERE id = ? LIMIT 1'); $stmt->execute([$stateId]); if (!$stmt->fetch()) { jsonResponse([ 'success' => false, 'error' => "{$label} not found." ], 400); } } function requireTaskType(int $taskTypeId): void { if ($taskTypeId <= 0) { jsonResponse([ 'success' => false, 'error' => 'Task type must be valid.' ], 400); } $stmt = db()->prepare('SELECT id FROM task_types WHERE id = ? LIMIT 1'); $stmt->execute([$taskTypeId]); if (!$stmt->fetch()) { jsonResponse([ 'success' => false, 'error' => 'Task type not found.' ], 400); } } function optionResponse(array $option): array { return [ 'id' => (int)$option['id'], 'name' => $option['name'], 'default_state' => isset($option['default_state']) && $option['default_state'] !== null ? (int)$option['default_state'] : null, 'icon' => $option['logo'] !== null ? 'data:image/svg+xml;base64,' . base64_encode($option['logo']) : null ]; } function workflowData(): array { $states = array_map(static function (array $state): array { return [ 'id' => (int)$state['id'], 'name' => $state['name'], 'color' => $state['color'] ]; }, db()->query( 'SELECT id, name, color FROM task_states ORDER BY id ASC' )->fetchAll()); $transitions = array_map(static function (array $transition): array { return [ 'id' => (int)$transition['id'], 'from_id' => (int)$transition['from_id'], 'to_id' => (int)$transition['to_id'], 'action_name' => $transition['action_name'] ]; }, db()->query( 'SELECT id, from_id, to_id, action_name FROM task_state_transitions ORDER BY id ASC' )->fetchAll()); $taskTypes = array_map('optionResponse', db()->query( 'SELECT id, name, logo, default_state FROM task_types ORDER BY id ASC' )->fetchAll()); $assignments = array_map(static function (array $assignment): array { return [ 'id' => (int)$assignment['id'], 'task_type' => (int)$assignment['task_type'], 'state' => (int)$assignment['state'] ]; }, db()->query( 'SELECT id, task_type, state FROM assigned_task_states ORDER BY task_type ASC, state ASC, id ASC' )->fetchAll()); return [ 'states' => $states, 'transitions' => $transitions, 'task_types' => $taskTypes, 'assignments' => $assignments ]; } $user = requireApiAuth(); requireUserRight((int)$user['id'], 'Admin'); $api = $_GET['api'] ?? ''; switch ($api) { case 'WorkflowData': { jsonResponse([ 'success' => true, 'workflow' => workflowData() ]); } case 'CreateState': { if ($_SERVER['REQUEST_METHOD'] !== 'POST') { jsonResponse(['success' => false, 'error' => 'CreateState requires POST.'], 405); } $data = getInputData(); $name = trim((string)($data['name'] ?? '')); $color = normalizeColor($data['color'] ?? ''); if ($name === '') { jsonResponse(['success' => false, 'error' => 'State name is required.'], 400); } if (strlen($name) > 128) { jsonResponse(['success' => false, 'error' => 'State name is too long.'], 400); } $stmt = db()->prepare('INSERT INTO task_states (name, color) VALUES (?, ?)'); $stmt->execute([$name, $color]); jsonResponse([ 'success' => true, 'state_id' => (int)db()->lastInsertId() ], 201); } case 'DeleteState': { if ($_SERVER['REQUEST_METHOD'] !== 'POST') { jsonResponse(['success' => false, 'error' => 'DeleteState requires POST.'], 405); } $stateId = (int)($_GET['state_id'] ?? 0); requireState($stateId); try { db()->beginTransaction(); db()->prepare('DELETE FROM assigned_task_states WHERE state = ?')->execute([$stateId]); db()->prepare('DELETE FROM task_state_transitions WHERE from_id = ? OR to_id = ?')->execute([$stateId, $stateId]); db()->prepare('UPDATE task_types SET default_state = NULL WHERE default_state = ?')->execute([$stateId]); db()->prepare('DELETE FROM task_states WHERE id = ?')->execute([$stateId]); db()->commit(); } catch (Throwable $e) { db()->rollBack(); jsonResponse(['success' => false, 'error' => 'Could not delete state.'], 500); } jsonResponse(['success' => true, 'state_id' => $stateId]); } case 'CreateTransition': { if ($_SERVER['REQUEST_METHOD'] !== 'POST') { jsonResponse(['success' => false, 'error' => 'CreateTransition requires POST.'], 405); } $data = getInputData(); $fromId = (int)($data['from_id'] ?? 0); $toId = (int)($data['to_id'] ?? 0); $actionName = trim((string)($data['action_name'] ?? '')); requireState($fromId, 'From state'); requireState($toId, 'To state'); if ($actionName === '') { jsonResponse(['success' => false, 'error' => 'Action name is required.'], 400); } if (strlen($actionName) > 128) { jsonResponse(['success' => false, 'error' => 'Action name is too long.'], 400); } $stmt = db()->prepare( 'INSERT INTO task_state_transitions (from_id, to_id, action_name) VALUES (?, ?, ?)' ); $stmt->execute([$fromId, $toId, $actionName]); jsonResponse([ 'success' => true, 'transition_id' => (int)db()->lastInsertId() ], 201); } case 'DeleteTransition': { if ($_SERVER['REQUEST_METHOD'] !== 'POST') { jsonResponse(['success' => false, 'error' => 'DeleteTransition requires POST.'], 405); } $transitionId = (int)($_GET['transition_id'] ?? 0); if ($transitionId <= 0) { jsonResponse(['success' => false, 'error' => 'Transition must be valid.'], 400); } $stmt = db()->prepare('DELETE FROM task_state_transitions WHERE id = ?'); $stmt->execute([$transitionId]); if ($stmt->rowCount() === 0) { jsonResponse(['success' => false, 'error' => 'Transition not found.'], 404); } jsonResponse(['success' => true, 'transition_id' => $transitionId]); } case 'AssignState': { if ($_SERVER['REQUEST_METHOD'] !== 'POST') { jsonResponse(['success' => false, 'error' => 'AssignState requires POST.'], 405); } $data = getInputData(); $taskTypeId = (int)($data['task_type'] ?? 0); $stateId = (int)($data['state'] ?? 0); requireTaskType($taskTypeId); requireState($stateId); $existingStmt = db()->prepare( 'SELECT id FROM assigned_task_states WHERE task_type = ? AND state = ? LIMIT 1' ); $existingStmt->execute([$taskTypeId, $stateId]); $assignmentId = $existingStmt->fetchColumn(); if (!$assignmentId) { $stmt = db()->prepare( 'INSERT INTO assigned_task_states (task_type, state) VALUES (?, ?)' ); $stmt->execute([$taskTypeId, $stateId]); $assignmentId = db()->lastInsertId(); } jsonResponse([ 'success' => true, 'assignment_id' => (int)$assignmentId ], 201); } case 'SetDefaultState': { if ($_SERVER['REQUEST_METHOD'] !== 'POST') { jsonResponse(['success' => false, 'error' => 'SetDefaultState requires POST.'], 405); } $data = getInputData(); $taskTypeId = (int)($data['task_type'] ?? 0); $stateId = isset($data['state']) && $data['state'] !== '' ? (int)$data['state'] : null; requireTaskType($taskTypeId); if ($stateId !== null) { requireState($stateId); $assignedStmt = db()->prepare( 'SELECT id FROM assigned_task_states WHERE task_type = ? AND state = ? LIMIT 1' ); $assignedStmt->execute([$taskTypeId, $stateId]); if (!$assignedStmt->fetch()) { jsonResponse([ 'success' => false, 'error' => 'Default state must be assigned to this task type.' ], 400); } } $stmt = db()->prepare( 'UPDATE task_types SET default_state = ? WHERE id = ?' ); $stmt->execute([$stateId, $taskTypeId]); jsonResponse([ 'success' => true, 'task_type' => $taskTypeId, 'default_state' => $stateId ]); } case 'RemoveAssignedState': { if ($_SERVER['REQUEST_METHOD'] !== 'POST') { jsonResponse(['success' => false, 'error' => 'RemoveAssignedState requires POST.'], 405); } $assignmentId = (int)($_GET['assignment_id'] ?? 0); if ($assignmentId <= 0) { jsonResponse(['success' => false, 'error' => 'Assignment must be valid.'], 400); } $stmt = db()->prepare('DELETE FROM assigned_task_states WHERE id = ?'); $lookupStmt = db()->prepare( 'SELECT task_type, state FROM assigned_task_states WHERE id = ? LIMIT 1' ); $lookupStmt->execute([$assignmentId]); $assignment = $lookupStmt->fetch(); if (!$assignment) { jsonResponse(['success' => false, 'error' => 'Assignment not found.'], 404); } $stmt->execute([$assignmentId]); if ($stmt->rowCount() === 0) { jsonResponse(['success' => false, 'error' => 'Assignment not found.'], 404); } db()->prepare( 'UPDATE task_types SET default_state = NULL WHERE id = ? AND default_state = ?' )->execute([ (int)$assignment['task_type'], (int)$assignment['state'] ]); jsonResponse(['success' => true, 'assignment_id' => $assignmentId]); } default: { jsonResponse([ 'success' => false, 'error' => 'Unknown API action.' ], 404); } }