From e3933ce0c886b7a03da7361e5212375b90feb843 Mon Sep 17 00:00:00 2001 From: Mike Date: Mon, 23 Mar 2026 19:00:04 +0000 Subject: [PATCH] Add team task tracker API endpoints MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New endpoints at api/tasks/team/ for centralized bot task tracking: - POST create.php — register a new task - POST update.php — change status, add notes (auto-sets PausedOn/CompletedOn) - GET active.php — list active/paused tasks, optional BotName filter - GET list.php — full listing with filters (BotName, Status, Channel, AssignedBy, Since) and pagination Includes schema.sql for the TeamTasks table. Co-Authored-By: Claude Opus 4.6 (1M context) --- api/tasks/team/active.php | 33 +++++++++++++++ api/tasks/team/create.php | 54 ++++++++++++++++++++++++ api/tasks/team/list.php | 89 +++++++++++++++++++++++++++++++++++++++ api/tasks/team/schema.sql | 17 ++++++++ api/tasks/team/update.php | 80 +++++++++++++++++++++++++++++++++++ 5 files changed, 273 insertions(+) create mode 100644 api/tasks/team/active.php create mode 100644 api/tasks/team/create.php create mode 100644 api/tasks/team/list.php create mode 100644 api/tasks/team/schema.sql create mode 100644 api/tasks/team/update.php diff --git a/api/tasks/team/active.php b/api/tasks/team/active.php new file mode 100644 index 0000000..f040819 --- /dev/null +++ b/api/tasks/team/active.php @@ -0,0 +1,33 @@ + true, 'Tasks' => $tasks]); +} catch (Throwable $e) { + http_response_code(500); + apiAbort(['OK' => false, 'ERROR' => 'Failed to fetch tasks: ' . $e->getMessage()]); +} diff --git a/api/tasks/team/create.php b/api/tasks/team/create.php new file mode 100644 index 0000000..52624ac --- /dev/null +++ b/api/tasks/team/create.php @@ -0,0 +1,54 @@ + } + */ + +$data = readJsonBody(); + +$botName = trim($data['BotName'] ?? ''); +$title = trim($data['Title'] ?? ''); +$channel = trim($data['Channel'] ?? ''); +$notes = trim($data['Notes'] ?? ''); +$assignedBy = trim($data['AssignedBy'] ?? ''); + +// Validate required fields +if ($botName === '' || $title === '') { + http_response_code(400); + apiAbort(['OK' => false, 'ERROR' => 'BotName and Title are required']); +} + +// Length guards +if (strlen($botName) > 50) { + http_response_code(400); + apiAbort(['OK' => false, 'ERROR' => 'BotName max 50 characters']); +} +if (strlen($title) > 255) { + http_response_code(400); + apiAbort(['OK' => false, 'ERROR' => 'Title max 255 characters']); +} + +try { + queryTimed(" + INSERT INTO TeamTasks (BotName, Title, Status, Channel, StartedOn, Notes, AssignedBy) + VALUES (?, ?, 'active', ?, NOW(), ?, ?) + ", [ + $botName, + $title, + $channel ?: null, + $notes ?: null, + $assignedBy ?: null, + ]); + + $id = (int) lastInsertId(); + + jsonResponse(['OK' => true, 'ID' => $id]); +} catch (Throwable $e) { + http_response_code(500); + apiAbort(['OK' => false, 'ERROR' => 'Failed to create task: ' . $e->getMessage()]); +} diff --git a/api/tasks/team/list.php b/api/tasks/team/list.php new file mode 100644 index 0000000..f41605d --- /dev/null +++ b/api/tasks/team/list.php @@ -0,0 +1,89 @@ + } + */ + +$botName = trim($_GET['BotName'] ?? ''); +$status = trim($_GET['Status'] ?? ''); +$channel = trim($_GET['Channel'] ?? ''); +$assignedBy = trim($_GET['AssignedBy'] ?? ''); +$since = trim($_GET['Since'] ?? ''); +$limit = min(max((int) ($_GET['Limit'] ?? 100), 1), 500); +$offset = max((int) ($_GET['Offset'] ?? 0), 0); + +$validStatuses = ['active', 'paused', 'done', 'cancelled']; + +try { + $where = []; + $params = []; + + if ($botName !== '') { + $where[] = "BotName = ?"; + $params[] = $botName; + } + + if ($status !== '') { + if (!in_array($status, $validStatuses, true)) { + http_response_code(400); + apiAbort(['OK' => false, 'ERROR' => 'Invalid status filter']); + } + $where[] = "Status = ?"; + $params[] = $status; + } + + if ($channel !== '') { + $where[] = "Channel = ?"; + $params[] = $channel; + } + + if ($assignedBy !== '') { + $where[] = "AssignedBy = ?"; + $params[] = $assignedBy; + } + + if ($since !== '') { + // Validate date format + if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $since)) { + http_response_code(400); + apiAbort(['OK' => false, 'ERROR' => 'Since must be YYYY-MM-DD']); + } + $where[] = "StartedOn >= ?"; + $params[] = $since . ' 00:00:00'; + } + + $whereClause = empty($where) ? '' : 'WHERE ' . implode(' AND ', $where); + + // Get total count + $countRow = queryOne("SELECT COUNT(*) AS Total FROM TeamTasks $whereClause", $params); + $total = (int) ($countRow['Total'] ?? 0); + + // Get paginated results + $params[] = $limit; + $params[] = $offset; + $tasks = queryTimed(" + SELECT ID, BotName, Title, Status, Channel, StartedOn, PausedOn, CompletedOn, Notes, AssignedBy + FROM TeamTasks + $whereClause + ORDER BY StartedOn DESC + LIMIT ? OFFSET ? + ", $params); + + jsonResponse(['OK' => true, 'Tasks' => $tasks, 'Total' => $total]); +} catch (Throwable $e) { + http_response_code(500); + apiAbort(['OK' => false, 'ERROR' => 'Failed to fetch tasks: ' . $e->getMessage()]); +} diff --git a/api/tasks/team/schema.sql b/api/tasks/team/schema.sql new file mode 100644 index 0000000..7d8679b --- /dev/null +++ b/api/tasks/team/schema.sql @@ -0,0 +1,17 @@ +-- TeamTasks: Centralized task tracker for all bots +-- Run against payfrit_dev (and later payfrit prod) + +CREATE TABLE IF NOT EXISTS TeamTasks ( + ID INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + BotName VARCHAR(50) NOT NULL, + Title VARCHAR(255) NOT NULL, + Status ENUM('active','paused','done','cancelled') NOT NULL DEFAULT 'active', + Channel VARCHAR(100) DEFAULT NULL, + StartedOn DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + PausedOn DATETIME DEFAULT NULL, + CompletedOn DATETIME DEFAULT NULL, + Notes TEXT DEFAULT NULL, + AssignedBy VARCHAR(50) DEFAULT NULL, + INDEX idx_status (Status), + INDEX idx_botname (BotName) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; diff --git a/api/tasks/team/update.php b/api/tasks/team/update.php new file mode 100644 index 0000000..01347aa --- /dev/null +++ b/api/tasks/team/update.php @@ -0,0 +1,80 @@ + false, 'ERROR' => 'ID is required']); +} + +// Validate status if provided +$validStatuses = ['active', 'paused', 'done', 'cancelled']; +if ($status !== '' && !in_array($status, $validStatuses, true)) { + http_response_code(400); + apiAbort(['OK' => false, 'ERROR' => 'Status must be one of: ' . implode(', ', $validStatuses)]); +} + +try { + // Verify task exists + $task = queryOne("SELECT ID, Status FROM TeamTasks WHERE ID = ?", [$id]); + if (!$task) { + http_response_code(404); + apiAbort(['OK' => false, 'ERROR' => 'Task not found']); + } + + // Build dynamic update + $sets = []; + $params = []; + + if ($status !== '') { + $sets[] = "Status = ?"; + $params[] = $status; + + // Auto-set timestamp columns based on status transitions + if ($status === 'paused') { + $sets[] = "PausedOn = NOW()"; + } elseif ($status === 'done' || $status === 'cancelled') { + $sets[] = "CompletedOn = NOW()"; + } elseif ($status === 'active' && $task['Status'] === 'paused') { + // Resuming from pause — clear PausedOn + $sets[] = "PausedOn = NULL"; + } + } + + if ($notes !== null) { + $sets[] = "Notes = ?"; + $params[] = trim($notes); + } + + if ($channel !== null) { + $sets[] = "Channel = ?"; + $params[] = trim($channel); + } + + if (empty($sets)) { + http_response_code(400); + apiAbort(['OK' => false, 'ERROR' => 'Nothing to update — provide Status, Notes, or Channel']); + } + + $params[] = $id; + queryTimed("UPDATE TeamTasks SET " . implode(', ', $sets) . " WHERE ID = ?", $params); + + jsonResponse(['OK' => true]); +} catch (Throwable $e) { + http_response_code(500); + apiAbort(['OK' => false, 'ERROR' => 'Failed to update task: ' . $e->getMessage()]); +}