prepare('SELECT * FROM ticket_csat WHERE ticket_id = ?'); $s->execute([$ticketId]); return $s->fetch() ?: null; } function csat_find_by_token(PDO $pdo, string $token): ?array { if (!preg_match('/^[a-f0-9]{40}$/', $token)) return null; $s = $pdo->prepare('SELECT c.*, t.ticket_number, t.subject FROM ticket_csat c JOIN tickets t ON t.id = c.ticket_id WHERE c.token = ?'); $s->execute([$token]); return $s->fetch() ?: null; } /** * Speichert die Bewertung. Jede Abfrage lässt sich nur einmal beantworten. * @return array{ok:bool,error?:string} */ function csat_record(PDO $pdo, string $token, int $rating, string $comment): array { $row = csat_find_by_token($pdo, $token); if (!$row) return ['ok' => false, 'error' => 'Dieser Link ist ungültig oder abgelaufen.']; if ($row['answered_at'] !== null) return ['ok' => false, 'error' => 'Vielen Dank, Sie haben dieses Ticket bereits bewertet.']; if ($rating < 1 || $rating > 5) return ['ok' => false, 'error' => 'Bitte wählen Sie 1 bis 5 Sterne.']; $comment = trim(mb_substr($comment, 0, 2000)); $pdo->prepare('UPDATE ticket_csat SET rating = ?, comment = ?, answered_at = ? WHERE id = ? AND answered_at IS NULL') ->execute([$rating, $comment !== '' ? $comment : null, db_now(), $row['id']]); try { log_activity($pdo, (int) $row['ticket_id'], null, 'Zufriedenheit bewertet: ' . $rating . ' von 5 Sternen'); } catch (Throwable $e) { } return ['ok' => true]; } /** Text der Mail an den Kunden. */ function csat_mail_html(array $ticket, string $token): string { $name = trim((string) ($ticket['requester_name'] ?? '')); $stars = ''; for ($i = 1; $i <= 5; $i++) { $stars .= '★'; } return '
' . ($name !== '' ? 'Hallo ' . e($name) . ', ' : '') . 'Ihr Ticket [#' . e((string) $ticket['ticket_number']) . '] ' . e((string) $ticket['subject']) . ' ist abgeschlossen. ' . 'Eine kurze Bewertung hilft uns, besser zu werden. Es dauert nur einen Klick.
' . '' . $stars . '
' . '1 Stern = unzufrieden · 5 Sterne = sehr zufrieden
'; } /** * Verschickt die fälligen Abfragen. Fällig: Ticket geschlossen oder gelöst (seit mindestens der eingestellten Wartezeit, höchstens 14 Tage), * kein Spam, kein Live-Chat, gültige Kundenadresse, noch keine Abfrage, und geschlossen nach dem Einschalten der Funktion. */ function csat_send_due(PDO $pdo, int $limit = 50): int { if (!csat_enabled()) return 0; $now = time(); $until = date('Y-m-d H:i:s', $now - csat_delay_minutes() * 60); $oldest = date('Y-m-d H:i:s', $now - 14 * 86400); $since = (string) get_setting('csat_since', ''); if ($since !== '' && $since > $oldest) $oldest = $since; $stmt = $pdo->prepare( "SELECT t.* FROM tickets t WHERE t.status IN ('closed','resolved') AND t.is_spam = 0 AND t.source <> 'chat' AND COALESCE(t.closed_at, t.resolved_at, t.updated_at) <= ? AND COALESCE(t.closed_at, t.resolved_at, t.updated_at) >= ? AND t.requester_email IS NOT NULL AND t.requester_email <> '' AND NOT EXISTS (SELECT 1 FROM ticket_csat c WHERE c.ticket_id = t.id) ORDER BY t.id LIMIT " . max(1, $limit) ); $stmt->execute([$until, $oldest]); $sent = 0; foreach ($stmt->fetchAll() as $t) { if (!filter_var($t['requester_email'], FILTER_VALIDATE_EMAIL)) continue; $token = bin2hex(random_bytes(20)); try { $pdo->prepare('INSERT INTO ticket_csat (ticket_id, token, staff_id, department_id, sent_at) VALUES (?,?,?,?,?)') ->execute([$t['id'], $token, $t['assigned_staff_id'] ?: null, $t['department_id'] ?: null, db_now()]); } catch (Throwable $e) { continue; // parallel schon angelegt } $from = resolve_from_address($t['department_id'] ? (int) $t['department_id'] : null, $t['priority_id'] ? (int) $t['priority_id'] : null); email_send_and_log((string) $t['requester_email'], 'Wie zufrieden waren Sie? [#' . $t['ticket_number'] . ']', email_wrap(csat_mail_html($t, $token)), $from, 'Zufriedenheitsabfrage', (string) $t['ticket_number']); $sent++; } return $sent; } /** * Auswertung für einen Zeitraum. * @param array|null $deptIds null = alle Abteilungen * @return array{sent:int,answered:int,avg:?float,rate:?float,dist:array