'" . db_now() . "'" : '1=0') : ticket_not_snoozed_sql('t'); // Beobachtet: nur Tickets, die der angemeldete Mitarbeiter beobachtet (?following=1) if (!empty($_GET['following'])) { $where[] = 't.id IN (SELECT ticket_id FROM ticket_followers WHERE staff_id = ?)'; $params[] = (int) $staff['id']; } $statuses = array_values(array_filter( array_map('trim', explode(',', (string) ($_GET['status'] ?? 'open,pending,answered,on_hold'))), fn(string $status) => in_array($status, MOBILE_API_STATUSES, true) )); if ($statuses) { $where[] = 't.status IN (' . implode(',', array_fill(0, count($statuses), '?')) . ')'; array_push($params, ...$statuses); } $department = filter_var($_GET['departmentId'] ?? null, FILTER_VALIDATE_INT); if ($department !== false && $department !== null) { if ($departmentIds !== null && !in_array((int) $department, $departmentIds, true)) { api_error(403, 'DEPARTMENT_FORBIDDEN', 'Auf diese Abteilung besteht kein Zugriff.'); } $where[] = 't.department_id = ?'; $params[] = (int) $department; } $assigned = trim((string) ($_GET['assigned'] ?? '')); if ($assigned === 'me') { $where[] = 't.assigned_staff_id = ?'; $params[] = $staff['id']; } elseif ($assigned === 'unassigned') { $where[] = 't.assigned_staff_id IS NULL'; } elseif ($assigned !== '' && ctype_digit($assigned)) { $where[] = 't.assigned_staff_id = ?'; $params[] = (int) $assigned; } $priority = filter_var($_GET['priorityId'] ?? null, FILTER_VALIDATE_INT); if ($priority !== false && $priority !== null) { $where[] = 't.priority_id = ?'; $params[] = (int) $priority; } $tag = filter_var($_GET['tagId'] ?? null, FILTER_VALIDATE_INT); if ($tag !== false && $tag !== null) { $where[] = 't.id IN (SELECT ticket_id FROM ticket_tags WHERE tag_id = ?)'; $params[] = (int) $tag; } // SLA-Status: siehe staff/tickets.php für dieselbe Logik (Web und App teilen die // 4-Stunden-Risiko-Schwelle, damit beide Oberflächen dasselbe "bald fällig" meinen). $sla = trim((string) ($_GET['sla'] ?? '')); if ($sla !== '') { $now = db_now(); $riskThreshold = date('Y-m-d H:i:s', time() + 4 * 3600); if ($sla === 'overdue') { $where[] = "t.resolution_due_at IS NOT NULL AND t.resolution_due_at < ? AND t.status NOT IN ('resolved','closed')"; $params[] = $now; } elseif ($sla === 'at_risk') { $where[] = "t.resolution_due_at IS NOT NULL AND t.resolution_due_at >= ? AND t.resolution_due_at < ? AND t.status NOT IN ('resolved','closed')"; array_push($params, $now, $riskThreshold); } elseif ($sla === 'on_time') { $where[] = "(t.resolution_due_at IS NULL OR t.resolution_due_at >= ?) AND t.status NOT IN ('resolved','closed')"; $params[] = $riskThreshold; } } $search = mb_substr(trim((string) ($_GET['q'] ?? '')), 0, 100); if ($search !== '') { $like = '%' . $search . '%'; $where[] = '(t.subject LIKE ? OR t.ticket_number LIKE ? OR t.requester_email LIKE ? OR t.requester_name LIKE ? OR t.id IN (SELECT ticket_id FROM ticket_threads WHERE message LIKE ?))'; array_push($params, $like, $like, $like, $like, $like); } $page = max(1, (int) ($_GET['page'] ?? 1)); $perPage = max(10, min(100, (int) ($_GET['perPage'] ?? 30))); $offset = ($page - 1) * $perPage; $from = ' FROM tickets t JOIN priorities p ON p.id = t.priority_id JOIN departments d ON d.id = t.department_id LEFT JOIN staff s ON s.id = t.assigned_staff_id'; $whereSql = $where ? ' WHERE ' . implode(' AND ', $where) : ''; $countStmt = $pdo->prepare('SELECT COUNT(*)' . $from . $whereSql); $countStmt->execute($params); $total = (int) $countStmt->fetchColumn(); $stmt = $pdo->prepare( 'SELECT t.*, p.name AS priority_name, p.color AS priority_color, d.name AS department_name, s.name AS staff_name' . $from . $whereSql . ' ORDER BY t.updated_at DESC, t.id DESC LIMIT ' . $perPage . ' OFFSET ' . $offset ); $stmt->execute($params); api_json([ 'items' => array_map('api_ticket_summary', $stmt->fetchAll()), 'page' => $page, 'perPage' => $perPage, 'total' => $total, 'hasMore' => ($offset + $perPage) < $total, ]);