Art => [Bezeichnung vom Quell-Ticket aus, Bezeichnung vom Ziel-Ticket aus] */ function ticket_link_types(): array { return [ 'related' => ['Verwandt mit', 'Verwandt mit'], 'duplicate' => ['Doppelt zu', 'Hat Duplikat'], 'blocks' => ['Blockiert', 'Blockiert durch'], ]; } /** * Verknüpft zwei Tickets. Gibt eine Fehlermeldung zurück oder null bei Erfolg. * Der Mitarbeiter muss beide Tickets sehen dürfen ($deptScope: Abteilungs-IDs des Mitarbeiters, null = alle). */ function ticket_link_add(PDO $pdo, int $ticketId, string $targetNumber, string $type, int $staffId, ?array $deptScope = null): ?string { if (!isset(ticket_link_types()[$type])) return 'Ungültige Art der Verknüpfung.'; $targetNumber = ltrim(trim($targetNumber), '#'); if ($targetNumber === '') return 'Bitte eine Ticketnummer angeben.'; $stmt = $pdo->prepare('SELECT id, department_id FROM tickets WHERE ticket_number = ?'); $stmt->execute([$targetNumber]); $target = $stmt->fetch(); if (!$target) return 'Ticket #' . $targetNumber . ' wurde nicht gefunden.'; $targetId = (int) $target['id']; if ($targetId === $ticketId) return 'Ein Ticket lässt sich nicht mit sich selbst verknüpfen.'; if ($deptScope !== null && !in_array((int) $target['department_id'], array_map('intval', $deptScope), true)) { return 'Ticket #' . $targetNumber . ' gehört zu einer Abteilung, auf die Sie keinen Zugriff haben.'; } $exists = $pdo->prepare('SELECT 1 FROM ticket_links WHERE (ticket_id = ? AND linked_ticket_id = ?) OR (ticket_id = ? AND linked_ticket_id = ?)'); $exists->execute([$ticketId, $targetId, $targetId, $ticketId]); if ($exists->fetchColumn()) return 'Die beiden Tickets sind bereits verknüpft.'; $pdo->prepare('INSERT INTO ticket_links (ticket_id, linked_ticket_id, link_type, created_by, created_at) VALUES (?,?,?,?,?)') ->execute([$ticketId, $targetId, $type, $staffId > 0 ? $staffId : null, db_now()]); return null; } /** Entfernt eine Verknüpfung, sofern sie zu diesem Ticket gehört. */ function ticket_link_remove(PDO $pdo, int $linkId, int $ticketId): bool { $stmt = $pdo->prepare('DELETE FROM ticket_links WHERE id = ? AND (ticket_id = ? OR linked_ticket_id = ?)'); $stmt->execute([$linkId, $ticketId, $ticketId]); return $stmt->rowCount() > 0; } /** * Alle Verknüpfungen eines Tickets aus dessen Sicht. Tickets aus Abteilungen, die der Mitarbeiter nicht sehen darf, bleiben weg. * @return list */ function ticket_links_for(PDO $pdo, int $ticketId, ?array $deptScope = null): array { $stmt = $pdo->prepare( 'SELECT l.id, l.ticket_id, l.linked_ticket_id, l.link_type, t.ticket_number, t.subject, t.status, t.department_id FROM ticket_links l JOIN tickets t ON t.id = CASE WHEN l.ticket_id = ? THEN l.linked_ticket_id ELSE l.ticket_id END WHERE l.ticket_id = ? OR l.linked_ticket_id = ? ORDER BY l.id' ); $stmt->execute([$ticketId, $ticketId, $ticketId]); $types = ticket_link_types(); $scope = $deptScope === null ? null : array_map('intval', $deptScope); $out = []; foreach ($stmt->fetchAll() as $row) { if ($scope !== null && !in_array((int) $row['department_id'], $scope, true)) continue; $outgoing = (int) $row['ticket_id'] === $ticketId; $labels = $types[$row['link_type']] ?? ['Verknüpft mit', 'Verknüpft mit']; $out[] = [ 'id' => (int) $row['id'], 'type' => (string) $row['link_type'], 'label' => $outgoing ? $labels[0] : $labels[1], 'number' => (string) $row['ticket_number'], 'subject' => (string) $row['subject'], 'status' => (string) $row['status'], ]; } return $out; } /** Offene Tickets, die dieses Ticket blockieren (Hinweis vor dem Schließen). */ function ticket_open_blockers(PDO $pdo, int $ticketId): array { return array_values(array_filter(ticket_links_for($pdo, $ticketId), fn($l) => $l['type'] === 'blocks' && $l['label'] === 'Blockiert durch' && !in_array($l['status'], ['resolved', 'closed'], true))); } /** Beim Löschen eines Tickets die Verknüpfungen mit entfernen. */ function ticket_links_purge(PDO $pdo, int $ticketId): void { $pdo->prepare('DELETE FROM ticket_links WHERE ticket_id = ? OR linked_ticket_id = ?')->execute([$ticketId, $ticketId]); }