false, 'error' => 'unauthorized'], 401); } try { Database::requireTicketsTable(); } catch (RuntimeException $e) { json_out(['ok' => false, 'error' => 'tickets_table_missing', 'hint' => $e->getMessage()], 503); } if (strtoupper($_SERVER['REQUEST_METHOD'] ?? 'GET') !== 'PUT' && strtoupper($_SERVER['REQUEST_METHOD'] ?? '') !== 'POST') { json_out(['ok' => false, 'error' => 'method not allowed'], 405); } $raw = file_get_contents('php://input'); $data = is_string($raw) && $raw !== '' ? json_decode($raw, true) : $_POST; if (!is_array($data)) { json_out(['ok' => false, 'error' => 'invalid json'], 400); } $id = (int) ($data['id'] ?? 0); if ($id <= 0) { json_out(['ok' => false, 'error' => 'invalid id'], 400); } $ticket = TicketRepository::getById($id); if (!$ticket) { json_out(['ok' => false, 'error' => 'not found'], 404); } if (empty($ticket['can_edit'])) { json_out(['ok' => false, 'error' => 'forbidden'], 403); } $fields = []; foreach (['title', 'brief', 'body', 'rating', 'owner_username', 'verified_by_username', 'workflow_state'] as $key) { if (array_key_exists($key, $data)) { $fields[$key] = $data[$key]; } } if (array_key_exists('assignees', $data)) { $fields['assignees'] = $data['assignees']; } try { TicketRepository::update($id, $fields); $updated = TicketRepository::getById($id); json_out(['ok' => true, 'ticket' => $updated]); } catch (InvalidArgumentException $e) { json_out(['ok' => false, 'error' => $e->getMessage()], 400); } catch (Throwable $e) { error_log('ticket_update: ' . $e->getMessage()); json_out(['ok' => false, 'error' => 'save failed'], 500); }