Upload via GUI (275 Dateien)
This commit is contained in:
@@ -86,11 +86,38 @@ function department_save(PDO $pdo, int $id, array $in): int
|
||||
$pdo->prepare('UPDATE departments SET name=?, email=?, is_default=?, is_active=?, manager_id=?, sla_plan_id=?, notes=?, signature=?, updated_at=? WHERE id=?')
|
||||
->execute([$name, $email, $isDefault, $isActive, $managerId, $slaPlanId, $notes ?: null, $signature ?: null, $now, $id]);
|
||||
}
|
||||
// Automatische Zuweisung neuer Tickets (nur wenn mitgeschickt: ältere Apps lassen die Einstellung unverändert)
|
||||
if (array_key_exists('auto_assign_mode', $in)) {
|
||||
try {
|
||||
department_save_auto_assign($pdo, $id, $in);
|
||||
} catch (PDOException $e) {
|
||||
// Spalten fehlen: die Datenbank ist noch nicht auf dem neuesten Stand
|
||||
throw new AdminModelException('Die Datenbank ist noch nicht auf dem neuesten Stand. Bitte unter System-Update auf "Datenbank aktualisieren" klicken und es dann erneut versuchen.', 'DB_OUTDATED', 503);
|
||||
}
|
||||
}
|
||||
// Eingehende Mails an die Abteilungs-E-Mail landen ohne weiteren Schritt in dieser Abteilung
|
||||
if ($email !== '') sync_department_email_route($pdo, $id, $email);
|
||||
return $id;
|
||||
}
|
||||
|
||||
/** Speichert die automatische Zuweisung einer Abteilung (Art und Mitarbeiter). */
|
||||
function department_save_auto_assign(PDO $pdo, int $id, array $in): void
|
||||
{
|
||||
$mode = (string) $in['auto_assign_mode'];
|
||||
if (!array_key_exists($mode, department_auto_assign_modes())) throw new AdminModelException('Die Art der automatischen Zuweisung ist ungültig.', 'INVALID_AUTO_ASSIGN');
|
||||
$assignStaff = am_int_or_null($in, 'auto_assign_staff_id');
|
||||
if ($mode === 'staff') {
|
||||
$chk = $pdo->prepare('SELECT COUNT(*) FROM staff WHERE id = ? AND is_active = 1');
|
||||
$chk->execute([(int) $assignStaff]);
|
||||
if (!$assignStaff || !(int) $chk->fetchColumn()) throw new AdminModelException('Bitte einen aktiven Mitarbeiter für die automatische Zuweisung wählen.', 'INVALID_AUTO_ASSIGN_STAFF');
|
||||
} else {
|
||||
$assignStaff = null;
|
||||
}
|
||||
$prevMode = (string) am_current($pdo, 'departments', $id, 'auto_assign_mode', 'none');
|
||||
$pdo->prepare('UPDATE departments SET auto_assign_mode = ?, auto_assign_staff_id = ? WHERE id = ?')->execute([$mode, $assignStaff, $id]);
|
||||
if ($prevMode !== $mode) $pdo->prepare('UPDATE departments SET auto_assign_last_id = NULL WHERE id = ?')->execute([$id]);
|
||||
}
|
||||
|
||||
/** Löscht eine Abteilung (nur ohne zugeordnete Tickets). Gibt den Namen zurück. */
|
||||
/**
|
||||
* Anzahl der Datensätze in $table, die über $column auf $id verweisen (0, wenn Tabelle oder Spalte in dieser Datenbank fehlen).
|
||||
|
||||
@@ -90,15 +90,27 @@ function require_staff_login(): array
|
||||
// Inaktivitäts-Timeout
|
||||
$timeout = (int)(get_setting('staff_session_timeout', '30') ?: 30); // Minuten
|
||||
$last = $_SESSION['staff_last_activity'] ?? 0;
|
||||
$userNavigation = staff_is_user_navigation();
|
||||
if ($last && (time() - $last) > ($timeout * 60)) {
|
||||
staff_logout();
|
||||
session_start();
|
||||
if (session_status() !== PHP_SESSION_ACTIVE) session_start();
|
||||
if (!$userNavigation) {
|
||||
// Hintergrund-Abruf (Aktualisierung der Liste, Chat-Abfrage …): kurze Antwort statt Weiterleitung auf die Anmeldeseite
|
||||
http_response_code(401);
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
echo json_encode(['error' => 'Session abgelaufen', 'sessionExpired' => true]);
|
||||
exit;
|
||||
}
|
||||
$_SESSION['flash'] = [['type'=>'error','message'=>'Sie wurden wegen Inaktivität abgemeldet.']];
|
||||
redirect('/staff/login.php');
|
||||
}
|
||||
// Nur echte Bedienung hält die Sitzung am Leben: ein vom Nutzer ausgelöster Seitenaufruf oder der Ping der Seite bei Klicks und
|
||||
// Tastatureingaben (staff/session_ping.php). Hintergrund-Abfragen der offenen Seiten (Listen, Chat, Anwesenheit alle paar
|
||||
// Sekunden) zählen nicht, sonst würde eine offene Seite die Sitzung ewig verlängern und die Abmeldung nie greifen.
|
||||
if ($userNavigation) {
|
||||
$_SESSION['staff_last_activity'] = time();
|
||||
// Echte Seitenaufrufe zählen als Aktivität (Hintergrund-Abrufe per fetch nicht)
|
||||
if (staff_is_user_navigation()) staff_touch_activity(get_db(), (int) $staff['id']);
|
||||
staff_touch_activity(get_db(), (int) $staff['id']);
|
||||
}
|
||||
|
||||
// "Eingebauter" Ersatz für echte Server-Cronjobs (siehe includes/pseudo_cron.php):
|
||||
// bei jeder authentifizierten Backend-Anfrage werden fällige Jobs im Hintergrund geprüft.
|
||||
|
||||
@@ -32,6 +32,9 @@ date_default_timezone_set(get_setting('timezone', config()['site']['timezone'] ?
|
||||
|
||||
// Sichere Session-Cookie-Einstellungen (vor session_start)
|
||||
if (session_status() === PHP_SESSION_NONE) {
|
||||
// Die Sitzungsdaten müssen mindestens so lange liegen bleiben wie die Inaktivitätszeit (PHP-Standard: 24 Minuten, sonst würde man
|
||||
// vor Ablauf der eingestellten Zeit abgemeldet)
|
||||
@ini_set('session.gc_maxlifetime', (string) max(1440, ((int) (get_setting('staff_session_timeout', '30') ?: 30)) * 60 + 600));
|
||||
session_set_cookie_params([
|
||||
'lifetime' => 0,
|
||||
'path' => '/',
|
||||
|
||||
@@ -54,8 +54,8 @@ function verify_csrf(): void
|
||||
http_response_code(403);
|
||||
die('Ungültige Anfrage (CSRF-Token fehlt oder ist ungültig). Bitte gehen Sie zurück und versuchen Sie es erneut.');
|
||||
}
|
||||
// Token nach Verbrauch rotieren
|
||||
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
|
||||
// Der Token gilt für die ganze Sitzung und wird NICHT nach jeder Benutzung erneuert: Sonst würden alle anderen offenen Tabs desselben
|
||||
// Browsers beim nächsten Absenden "CSRF-Token ungültig" melden (typisch bei mehreren Tickets in Tabs). Er wird beim Anmelden neu erzeugt.
|
||||
}
|
||||
|
||||
function flash(string $type, string $message): void
|
||||
@@ -423,6 +423,90 @@ function staff_department_ids(array $staff): ?array
|
||||
return array_values(array_unique($ids));
|
||||
}
|
||||
|
||||
/**
|
||||
* Beim Öffnen eines noch niemandem zugewiesenen Tickets wird es dem öffnenden Mitarbeiter zugewiesen (Einstellung "Beim Öffnen
|
||||
* automatisch zuweisen", Standard an). Nicht bei geschlossenen, gelösten, zusammengeführten oder als Spam markierten Tickets und nicht,
|
||||
* wenn der Mitarbeiter in dieser Abteilung nicht antworten darf. Aktualisiert $ticket (assigned_staff_id, staff_name) und gibt zurück,
|
||||
* ob zugewiesen wurde.
|
||||
*/
|
||||
function ticket_assign_on_open(PDO $pdo, array &$ticket, array $staff): bool
|
||||
{
|
||||
if (get_setting('auto_assign_on_open', '1') !== '1') return false;
|
||||
if (!empty($ticket['assigned_staff_id']) || !empty($ticket['merged_into']) || !empty($ticket['is_spam'])) return false;
|
||||
if (in_array((string) ($ticket['status'] ?? ''), ['closed', 'resolved'], true)) return false;
|
||||
if (!has_permission($staff, 'reply_ticket', (int) ($ticket['department_id'] ?? 0))) return false;
|
||||
$stmt = $pdo->prepare('UPDATE tickets SET assigned_staff_id = ? WHERE id = ? AND assigned_staff_id IS NULL');
|
||||
$stmt->execute([(int) $staff['id'], (int) $ticket['id']]);
|
||||
if ($stmt->rowCount() !== 1) return false;
|
||||
$ticket['assigned_staff_id'] = (int) $staff['id'];
|
||||
$ticket['staff_name'] = $staff['name'] ?? null;
|
||||
log_activity($pdo, (int) $ticket['id'], (int) $staff['id'], 'Beim Öffnen automatisch zugewiesen');
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Arten der automatischen Zuweisung neuer Tickets einer Abteilung. */
|
||||
function department_auto_assign_modes(): array
|
||||
{
|
||||
return ['none' => 'Nicht automatisch zuweisen', 'staff' => 'Immer an einen bestimmten Mitarbeiter', 'round_robin' => 'Reihum an die Mitarbeiter der Abteilung'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Wem ein neues Ticket dieser Abteilung laut Einstellung der Abteilung automatisch zugewiesen wird (Mitarbeiter-ID) oder null.
|
||||
* - "staff": immer derselbe Mitarbeiter; ist er im Urlaub, geht es an seine Vertretung, ohne Vertretung bleibt das Ticket unzugewiesen
|
||||
* - "round_robin": der Reihe nach an die Mitarbeiter der Abteilung (Hauptabteilung oder zusätzlich zugewiesen), die nicht im Urlaub und
|
||||
* gerade erreichbar sind (nicht offline, in der Schicht); ist niemand erreichbar, bekommt der nächste aus der Reihe es trotzdem
|
||||
* Fehlende Spalten (Datenbank noch nicht aktualisiert) und unbekannte Abteilungen ergeben null.
|
||||
*/
|
||||
function department_auto_assignee(PDO $pdo, int $departmentId): ?int
|
||||
{
|
||||
try {
|
||||
$s = $pdo->prepare('SELECT auto_assign_mode, auto_assign_staff_id, auto_assign_last_id FROM departments WHERE id = ?');
|
||||
$s->execute([$departmentId]);
|
||||
$d = $s->fetch();
|
||||
} catch (Throwable $e) {
|
||||
return null;
|
||||
}
|
||||
if (!$d) return null;
|
||||
$mode = (string) ($d['auto_assign_mode'] ?? 'none');
|
||||
|
||||
if ($mode === 'staff') {
|
||||
$id = (int) ($d['auto_assign_staff_id'] ?? 0);
|
||||
if ($id <= 0) return null;
|
||||
$st = $pdo->prepare('SELECT id, is_active, is_on_vacation, vacation_substitute_id FROM staff WHERE id = ?');
|
||||
$st->execute([$id]);
|
||||
$row = $st->fetch();
|
||||
if (!$row || !(int) $row['is_active']) return null;
|
||||
if (!(int) $row['is_on_vacation']) return $id;
|
||||
$sub = (int) ($row['vacation_substitute_id'] ?? 0);
|
||||
if ($sub > 0) {
|
||||
$st->execute([$sub]);
|
||||
$subRow = $st->fetch();
|
||||
if ($subRow && (int) $subRow['is_active'] && !(int) $subRow['is_on_vacation']) return $sub;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($mode === 'round_robin') {
|
||||
$c = $pdo->prepare(
|
||||
'SELECT id FROM staff WHERE is_active = 1 AND is_on_vacation = 0
|
||||
AND (department_id = ? OR id IN (SELECT staff_id FROM staff_department WHERE department_id = ?)) ORDER BY id'
|
||||
);
|
||||
$c->execute([$departmentId, $departmentId]);
|
||||
$candidates = array_map('intval', $c->fetchAll(PDO::FETCH_COLUMN));
|
||||
if (!$candidates) return null;
|
||||
$available = array_values(array_filter($candidates, fn(int $id) => is_staff_available($pdo, $id)));
|
||||
$pool = $available ?: $candidates;
|
||||
$last = (int) ($d['auto_assign_last_id'] ?? 0);
|
||||
$next = null;
|
||||
foreach ($pool as $id) { if ($id > $last) { $next = $id; break; } }
|
||||
$next ??= $pool[0];
|
||||
$pdo->prepare('UPDATE departments SET auto_assign_last_id = ? WHERE id = ?')->execute([$next, $departmentId]);
|
||||
return $next;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Löscht ein Ticket dauerhaft, protokolliert es vorher für Berichte */
|
||||
function delete_ticket(PDO $pdo, array $ticket, ?int $deletedByStaffId): void
|
||||
{
|
||||
|
||||
@@ -112,8 +112,14 @@ function public_ticket_create(PDO $pdo, array $in, string $clientIp): array
|
||||
->execute([$ticketId, 'customer', $message, $now]);
|
||||
log_activity($pdo, $ticketId, null, 'Ticket über die API erstellt');
|
||||
|
||||
if ($ruleResult['assign_staff'] !== null) {
|
||||
$pdo->prepare('UPDATE tickets SET assigned_staff_id = ? WHERE id = ?')->execute([$ruleResult['assign_staff'], $ticketId]);
|
||||
$assignTo = $ruleResult['assign_staff'];
|
||||
if ($assignTo === null) {
|
||||
// Einstellung der Abteilung ("Neue Tickets automatisch zuweisen")
|
||||
$assignTo = department_auto_assignee($pdo, (int) $deptId);
|
||||
if ($assignTo !== null) log_activity($pdo, $ticketId, null, 'Automatisch zugewiesen (Einstellung der Abteilung)');
|
||||
}
|
||||
if ($assignTo !== null) {
|
||||
$pdo->prepare('UPDATE tickets SET assigned_staff_id = ? WHERE id = ?')->execute([$assignTo, $ticketId]);
|
||||
}
|
||||
foreach ($ruleResult['add_tags'] as $tagName) apply_tag_to_ticket($pdo, $ticketId, $tagName);
|
||||
apply_auto_reply($pdo, $ticketId, $ruleResult['auto_reply']);
|
||||
|
||||
@@ -89,7 +89,9 @@ function staff_is_user_navigation(): bool
|
||||
{
|
||||
if (($_SERVER['REQUEST_METHOD'] ?? 'GET') !== 'GET' && ($_SERVER['REQUEST_METHOD'] ?? '') !== 'POST') return false;
|
||||
$mode = $_SERVER['HTTP_SEC_FETCH_MODE'] ?? '';
|
||||
if ($mode !== '') return $mode === 'navigate';
|
||||
// Sec-Fetch-User (?1) sendet der Browser nur bei Aufrufen, die der Nutzer selbst ausgelöst hat (Klick, Adresszeile, F5, Formular),
|
||||
// nicht bei Neuladen oder Weiterleitung per Skript (z. B. "neue Antwort, Seite aktualisieren")
|
||||
if ($mode !== '') return $mode === 'navigate' && ($_SERVER['HTTP_SEC_FETCH_USER'] ?? '') === '?1';
|
||||
// Ältere Browser ohne Sec-Fetch-Header: Seitenaufrufe erkennt man daran, dass kein X-Requested-With gesetzt ist und HTML erwartet wird
|
||||
return empty($_SERVER['HTTP_X_REQUESTED_WITH']) && str_contains((string) ($_SERVER['HTTP_ACCEPT'] ?? ''), 'text/html');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user