Upload via GUI (267 Dateien)
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
</main>
|
||||
</div>
|
||||
<script src="/assets/js/richtext.js"></script>
|
||||
<script src="/assets/js/richtext.js?v=<?= @filemtime(__DIR__ . '/../assets/js/richtext.js') ?: time() ?>"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -24,15 +24,10 @@ if (file_exists(__DIR__ . '/../includes/push.php')) {
|
||||
<meta name="push-key" content="<?= e($pushKey) ?>">
|
||||
<?php endif; ?>
|
||||
<link rel="stylesheet" href="/assets/css/style.css?v=<?= @filemtime(__DIR__ . "/../assets/css/style.css") ?: time() ?>">
|
||||
<link rel="icon" href="/favicon.ico" sizes="any">
|
||||
<link rel="icon" href="/assets/icons/icon-192.png" type="image/png">
|
||||
<?= site_icon_links() ?>
|
||||
<link rel="manifest" href="/manifest-staff.php">
|
||||
<meta name="theme-color" content="#0f6f6a">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<link rel="apple-touch-icon" href="/assets/icons/icon-192.png">
|
||||
<?php $faviconLogo = get_setting('logo_path'); if ($faviconLogo): ?>
|
||||
<link rel="icon" href="<?= e($faviconLogo) ?>">
|
||||
<?php endif; ?>
|
||||
<script>
|
||||
if ('serviceWorker' in navigator) {
|
||||
navigator.serviceWorker.register('/staff/sw-staff.js', {scope: '/staff/'}).catch(function(){});
|
||||
@@ -122,7 +117,7 @@ if ('serviceWorker' in navigator) {
|
||||
<?php if (has_permission($staff, 'manage_roles')): ?><a href="/staff/roles.php" class="<?= $activePage==='roles'?'active':'' ?>">Rollen</a><?php endif; ?>
|
||||
|
||||
<span class="nav-group-label">System</span>
|
||||
<?php if ($staff['role'] === 'admin'): ?><a href="/staff/security_check.php" class="<?= $activePage==='security_check'?'active':'' ?>">Sicherheitsprüfung</a><?php endif; ?>
|
||||
<?php if ($staff['role'] === 'admin'): ?><a href="/staff/security_check.php" class="<?= $activePage==='security_check'?'active':'' ?>">Sicherheitsprüfung</a><?php endif; ?>
|
||||
<?php if (has_permission($staff, 'view_audit_log')): ?><a href="/staff/audit_log.php" class="<?= $activePage==='audit_log'?'active':'' ?>">Admin-Protokoll</a><?php endif; ?>
|
||||
<?php if ($staff['role'] === 'admin'): ?><a href="/staff/ticket_rules.php" class="<?= $activePage==='ticket_rules'?'active':'' ?>">Ticket-Regeln</a><?php endif; ?>
|
||||
<?php if (has_permission($staff, 'manage_escalations')): ?><a href="/staff/escalation_rules.php" class="<?= $activePage==='escalation_rules'?'active':'' ?>">Eskalationsregeln</a><?php endif; ?>
|
||||
|
||||
@@ -87,6 +87,7 @@ $siteName = get_setting('display_name', config()['site']['name'] ?? 'DeskHub');
|
||||
@page { margin: 15mm; }
|
||||
}
|
||||
</style>
|
||||
<?= site_icon_links() ?>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@ $actionLabels = [
|
||||
'assign_staff' => 'Mitarbeiter zuweisen',
|
||||
'add_tag' => 'Tag hinzufügen',
|
||||
'auto_reply' => 'Auto-Antwort senden (Textbaustein)',
|
||||
'close' => 'Ticket schließen (wird angelegt, aber sofort geschlossen)',
|
||||
'reject' => 'Ticket verwerfen (z.B. Spam)',
|
||||
];
|
||||
|
||||
@@ -80,10 +81,10 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
'assign_staff' => $actStaff[$i] ?? '',
|
||||
'add_tag' => trim($actTag[$i] ?? ''),
|
||||
'auto_reply' => $actCanned[$i] ?? '',
|
||||
'reject' => '1',
|
||||
'reject', 'close' => '1',
|
||||
default => '',
|
||||
};
|
||||
if ($t !== 'reject' && $value === '') continue;
|
||||
if (!in_array($t, ['reject', 'close'], true) && $value === '') continue;
|
||||
$actionsToSave[] = ['action_type' => $t, 'action_value' => $value];
|
||||
}
|
||||
if (!$actionsToSave) $errors[] = 'Bitte mindestens eine Aktion festlegen.';
|
||||
@@ -213,6 +214,7 @@ function render_action_value_selects(array $departments, array $priorities, arra
|
||||
<?php else: ?>
|
||||
<span class="rule-val rule-val-auto_reply muted" style="<?= $sel('auto_reply') === '' ? '' : 'display:none' ?>; align-self:center; font-size:.85rem">Noch keine Textbausteine angelegt – <a href="/staff/canned_responses.php">jetzt erstellen</a>.</span>
|
||||
<?php endif; ?>
|
||||
<span class="rule-val rule-val-close muted" style="<?= $sel('close') === '' ? '' : 'display:none' ?>; align-self:center; font-size:.85rem">Das Ticket wird angelegt und gleich geschlossen. Es gibt keine Benachrichtigung an Mitarbeiter oder Kunden.</span>
|
||||
<span class="rule-val rule-val-reject muted" style="<?= $sel('reject') === '' ? '' : 'display:none' ?>; align-self:center; font-size:.85rem">Keine weitere Einstellung nötig.</span>
|
||||
<?php
|
||||
}
|
||||
@@ -258,7 +260,7 @@ function render_action_value_selects(array $departments, array $priorities, arra
|
||||
</td>
|
||||
<td class="muted" style="font-size:.82rem">
|
||||
<?php foreach ($r['actions'] as $a): ?>
|
||||
<div><?= e($actionLabels[$a['action_type']] ?? $a['action_type']) ?><?= $a['action_type']!=='reject' ? ': ' . e($a['action_value']) : '' ?></div>
|
||||
<div><?= e($actionLabels[$a['action_type']] ?? $a['action_type']) ?><?= !in_array($a['action_type'], ['reject', 'close'], true) ? ': ' . e($a['action_value']) : '' ?></div>
|
||||
<?php endforeach; ?>
|
||||
</td>
|
||||
<td>
|
||||
|
||||
@@ -84,7 +84,10 @@ $departments = departments_for_select($pdo, [(int) $ticket['department_id']]);
|
||||
$priorities = $pdo->query('SELECT * FROM priorities ORDER BY sort_order')->fetchAll();
|
||||
$agents = $pdo->query('SELECT id, name FROM staff WHERE is_active = 1 ORDER BY name')->fetchAll();
|
||||
$teams = $pdo->query('SELECT id, name FROM teams WHERE is_active = 1 ORDER BY name')->fetchAll();
|
||||
$cannedResponses = $pdo->query('SELECT id, title, body FROM canned_responses ORDER BY title')->fetchAll();
|
||||
// Nur Vorlagen dieser Abteilung und solche, die für alle Abteilungen gelten (keine Abteilung zugeordnet)
|
||||
$cannedStmt = $pdo->prepare('SELECT id, title, body, department_id FROM canned_responses WHERE department_id IS NULL OR department_id = ? ORDER BY title');
|
||||
$cannedStmt->execute([(int) $ticket['department_id']]);
|
||||
$cannedResponses = $cannedStmt->fetchAll();
|
||||
|
||||
$helpTopicName = null;
|
||||
if (!empty($ticket['help_topic_id'])) {
|
||||
@@ -252,12 +255,8 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
log_activity($pdo, $ticket['id'], $staff['id'], 'Automatisch zugewiesen (erste Antwort)');
|
||||
}
|
||||
|
||||
$signature = trim($staff['signature'] ?? '');
|
||||
if ($signature === '') {
|
||||
$deptSigStmt = $pdo->prepare('SELECT signature FROM departments WHERE id = ?');
|
||||
$deptSigStmt->execute([$ticket['department_id']]);
|
||||
$signature = (string) $deptSigStmt->fetchColumn();
|
||||
}
|
||||
// Signatur laut Auswahl im Antwortformular (eigene, Abteilung oder keine); ohne Angabe wie bisher
|
||||
$signature = resolve_reply_signature($pdo, $staff, (int) $ticket['department_id'], isset($_POST['signature_choice']) ? (string) $_POST['signature_choice'] : null);
|
||||
$ccEmails = array_column($collaborators, 'email');
|
||||
notify_customer_new_reply($ticket, $message, $signature, $ccEmails);
|
||||
save_mentions($pdo, $ticket['id'], $threadId, $staff['id'], $message, $ticket);
|
||||
@@ -996,6 +995,14 @@ $sourceLabel = $sourceLabels[$ticket['source']] ?? $ticket['source'];
|
||||
|
||||
<div class="card">
|
||||
<h3>Antworten / Notiz</h3>
|
||||
<?php
|
||||
$sigOwn = trim((string) ($staff['signature'] ?? ''));
|
||||
$sigDeptStmt = $pdo->prepare('SELECT signature FROM departments WHERE id = ?');
|
||||
$sigDeptStmt->execute([$ticket['department_id']]);
|
||||
$sigDept = trim((string) $sigDeptStmt->fetchColumn());
|
||||
$sigDefault = $sigOwn !== '' ? 'own' : ($sigDept !== '' ? 'dept' : 'none');
|
||||
$sigHtml = ['own' => $sigOwn !== '' ? sanitize_staff_html($sigOwn) : '', 'dept' => $sigDept !== '' ? sanitize_staff_html($sigDept) : '', 'none' => ''];
|
||||
?>
|
||||
<form method="post" enctype="multipart/form-data">
|
||||
<?= csrf_field() ?>
|
||||
<input type="hidden" name="action" value="reply" id="form-action">
|
||||
@@ -1021,8 +1028,16 @@ $sourceLabel = $sourceLabels[$ticket['source']] ?? $ticket['source'];
|
||||
<div style="display:flex;gap:8px;align-items:center;margin-bottom:6px">
|
||||
<select id="canned_picker" style="flex:1" onchange="applyCannedResponse(this)">
|
||||
<option value="">– Vorlage wählen –</option>
|
||||
<?php foreach ($cannedResponses as $cr): ?>
|
||||
<option value="<?= $cr['id'] ?>" data-body="<?= e($cr['body']) ?>"><?= e($cr['title']) ?></option>
|
||||
<?php
|
||||
$cannedOwn = array_filter($cannedResponses, fn($cr) => !empty($cr['department_id']));
|
||||
$cannedAll = array_filter($cannedResponses, fn($cr) => empty($cr['department_id']));
|
||||
foreach ([['Abteilung „' . ($ticket['department_name'] ?? '') . '“', $cannedOwn], ['Für alle Abteilungen', $cannedAll]] as [$groupLabel, $groupItems]):
|
||||
if (!$groupItems) continue; ?>
|
||||
<optgroup label="<?= e($groupLabel) ?>">
|
||||
<?php foreach ($groupItems as $cr): ?>
|
||||
<option value="<?= $cr['id'] ?>" data-body="<?= e($cr['body']) ?>"><?= e($cr['title']) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</optgroup>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<button type="button" id="canned-preview-btn" style="display:none;font-size:.78rem;padding:5px 10px" class="btn secondary" onclick="toggleCannedPreview()">Vorschau</button>
|
||||
@@ -1030,6 +1045,35 @@ $sourceLabel = $sourceLabels[$ticket['source']] ?? $ticket['source'];
|
||||
<div id="canned-preview" style="display:none;background:var(--bg-soft);border:1px solid var(--border);border-radius:6px;padding:12px;font-size:.84rem;line-height:1.7;margin-bottom:8px;max-height:200px;overflow-y:auto"></div>
|
||||
<?php endif; ?>
|
||||
<textarea name="message" id="message-field" data-richtext placeholder="Nachricht eingeben..." required></textarea>
|
||||
|
||||
<div id="reply-signature" style="margin:14px 0">
|
||||
<div style="display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin-bottom:6px">
|
||||
<label for="signature_choice" style="margin:0">Signatur</label>
|
||||
<select name="signature_choice" id="signature_choice" style="width:auto;min-width:260px">
|
||||
<option value="own" <?= $sigDefault === 'own' ? 'selected' : '' ?> <?= $sigOwn === '' ? 'disabled' : '' ?>>Meine Signatur<?= $sigOwn === '' ? ' (nicht hinterlegt)' : '' ?></option>
|
||||
<option value="dept" <?= $sigDefault === 'dept' ? 'selected' : '' ?> <?= $sigDept === '' ? 'disabled' : '' ?>>Abteilung „<?= e($ticket['department_name']) ?>“<?= $sigDept === '' ? ' (nicht hinterlegt)' : '' ?></option>
|
||||
<option value="none" <?= $sigDefault === 'none' ? 'selected' : '' ?>>Keine Signatur</option>
|
||||
</select>
|
||||
<a href="/staff/profile.php?tab=signature" class="muted" style="font-size:.78rem">Meine Signatur bearbeiten</a>
|
||||
</div>
|
||||
<div id="signature-preview" style="border:1px dashed var(--border);border-radius:8px;padding:12px 14px;font-size:.88rem;line-height:1.6;color:var(--muted);background:var(--bg-soft,transparent);min-height:20px"></div>
|
||||
<div class="muted" style="font-size:.76rem;margin-top:4px">Wird nur an öffentliche Antworten angehängt, nicht an interne Notizen.</div>
|
||||
</div>
|
||||
<script>
|
||||
(function () {
|
||||
var sigs = <?= json_encode($sigHtml, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP) ?>;
|
||||
var sel = document.getElementById('signature_choice');
|
||||
var box = document.getElementById('signature-preview');
|
||||
function show() {
|
||||
var html = sigs[sel.value] || '';
|
||||
if (html) { box.innerHTML = html; box.style.opacity = '1'; }
|
||||
else { box.textContent = sel.value === 'none' ? 'Es wird keine Signatur angehängt.' : 'Keine Signatur hinterlegt.'; box.style.opacity = '.7'; }
|
||||
}
|
||||
sel.addEventListener('change', show);
|
||||
show();
|
||||
})();
|
||||
</script>
|
||||
|
||||
<div class="field-row">
|
||||
<div>
|
||||
<label for="attachments">Anhänge (optional, mehrere möglich)</label>
|
||||
|
||||
@@ -133,6 +133,25 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
}
|
||||
$action = $_POST['action'] ?? '';
|
||||
|
||||
// Zugangstoken für den Update-Server speichern oder entfernen (nur Admins erreichen diese Seite)
|
||||
if ($action === 'save_update_token') {
|
||||
$newToken = trim((string) ($_POST['update_token'] ?? ''));
|
||||
if ($newToken === '' || !preg_match('/^[A-Za-z0-9_\-.]{20,200}$/', $newToken)) {
|
||||
$errors[] = 'Das Token sieht ungültig aus (erwartet: 20 bis 200 Zeichen aus Buchstaben, Ziffern, _ - .). Bitte genau so einfügen, wie Gitea es anzeigt.';
|
||||
} else {
|
||||
set_setting('update_token', encrypt_secret($newToken));
|
||||
audit_log($staff['id'], 'Zugangstoken für den Update-Server gespeichert');
|
||||
flash('success', 'Zugangstoken gespeichert. Die Release-Liste wird jetzt damit geladen.');
|
||||
redirect('/staff/update.php');
|
||||
}
|
||||
}
|
||||
if ($action === 'remove_update_token') {
|
||||
set_setting('update_token', '');
|
||||
audit_log($staff['id'], 'Zugangstoken für den Update-Server entfernt');
|
||||
flash('success', 'Zugangstoken entfernt.');
|
||||
redirect('/staff/update.php');
|
||||
}
|
||||
|
||||
// Manueller Upload
|
||||
if ($action === 'upload_update') {
|
||||
$f = $_FILES['update_zip'] ?? [];
|
||||
@@ -259,7 +278,9 @@ if ($code === 200) {
|
||||
usort($all, fn($a, $b) => version_compare($b['tag_name'], $a['tag_name']));
|
||||
$releases = $all;
|
||||
} else {
|
||||
$relErr = "API HTTP $code" . (update_http_headers() === [] ? ' (Ist das Repository privat? Dann in config/config.php unter update → token ein Lese-Token eintragen.)' : '');
|
||||
$relErr = "API HTTP $code" . (update_http_headers() === []
|
||||
? ($code === 404 || $code === 401 || $code === 403 ? ' – das Repository ist vermutlich privat. Unten unter „Zugang zum Update-Server“ ein Lese-Token eintragen.' : '')
|
||||
: ($code === 404 || $code === 401 || $code === 403 ? ' – das eingetragene Token wird nicht akzeptiert (falsch, abgelaufen oder ohne Leserecht).' : ''));
|
||||
}
|
||||
$signedOnly = update_signature_required();
|
||||
// Version frisch aus Datei lesen
|
||||
@@ -284,6 +305,10 @@ require __DIR__ . '/_header.php';
|
||||
<div class="flash error"><?= htmlspecialchars($e) ?></div>
|
||||
<?php endforeach; ?>
|
||||
|
||||
<?php if ($relErr): ?>
|
||||
<div class="flash warning" style="margin-bottom:16px">Die Liste der Versionen konnte nicht geladen werden: <?= htmlspecialchars($relErr) ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($hasUpdate): ?>
|
||||
<div style="background:#e0f2f1;border:1px solid #0f6f6a;border-radius:10px;padding:16px 20px;margin-bottom:20px;display:flex;align-items:center;justify-content:space-between;gap:16px;flex-wrap:wrap">
|
||||
<div>
|
||||
@@ -365,12 +390,47 @@ require __DIR__ . '/_header.php';
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php [$__tokenValue, $__tokenSource] = update_token_info(); ?>
|
||||
<div class="card" style="margin-bottom:20px">
|
||||
<h3>Zugang zum Update-Server</h3>
|
||||
<p style="color:var(--muted);font-size:.86rem;margin-bottom:12px">
|
||||
Nur nötig, wenn das Repository mit den Releases <strong>privat</strong> ist. Gitea meldet dann beim Laden der Versionen „404“. Ein Token mit <strong>Leserecht</strong> genügt
|
||||
(in Gitea: Einstellungen → Anwendungen → Zugriffstoken erzeugen, Berechtigung „repository: Lesen“). Es wird verschlüsselt gespeichert.
|
||||
</p>
|
||||
<?php if ($__tokenSource === 'config'): ?>
|
||||
<p style="margin-bottom:10px"><strong style="color:#15803d">✓ Token aus config/config.php wird verwendet.</strong> Es hat Vorrang vor einem hier gespeicherten.</p>
|
||||
<?php elseif ($__tokenSource === 'einstellungen'): ?>
|
||||
<p style="margin-bottom:10px"><strong style="color:#15803d">✓ Token gespeichert.</strong> Zum Ersetzen unten ein neues eintragen.</p>
|
||||
<?php else: ?>
|
||||
<p style="margin-bottom:10px;color:var(--muted)">Kein Token eingetragen (öffentlicher Zugriff).</p>
|
||||
<?php endif; ?>
|
||||
<form method="post" style="display:flex;gap:10px;flex-wrap:wrap;align-items:flex-end">
|
||||
<?= csrf_field() ?>
|
||||
<input type="hidden" name="action" value="save_update_token">
|
||||
<div style="flex:1;min-width:260px">
|
||||
<label for="update_token" style="margin-top:0">Zugriffstoken</label>
|
||||
<input type="password" id="update_token" name="update_token" autocomplete="off" placeholder="<?= $__tokenSource === 'einstellungen' ? '•••••••• (gespeichert)' : 'Token aus Gitea einfügen' ?>" required>
|
||||
</div>
|
||||
<button type="submit" class="btn">Speichern</button>
|
||||
</form>
|
||||
<?php if ($__tokenSource === 'einstellungen'): ?>
|
||||
<form method="post" style="margin-top:10px" onsubmit="return confirm('Gespeichertes Token entfernen?')">
|
||||
<?= csrf_field() ?><input type="hidden" name="action" value="remove_update_token">
|
||||
<button type="submit" class="btn secondary">Token entfernen</button>
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
<details style="margin-top:12px;font-size:.82rem;color:var(--muted)"><summary>Alternative: in config/config.php eintragen</summary>
|
||||
<p style="margin:8px 0 4px">In der Datei <code>config/config.php</code> (Hauptordner von DeskHub, im Ordner <code>config</code>) als eigener Eintrag im großen <code>return [ … ];</code>, zum Beispiel direkt unter <code>'app_key'</code>:</p>
|
||||
<pre style="background:var(--bg-soft,#f4f6f9);padding:10px;border-radius:6px;overflow:auto">'update' => ['token' => 'HIER_DAS_TOKEN'],</pre>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>Hinweise</h3>
|
||||
<ul style="padding-left:20px;line-height:2;font-size:.88rem;color:var(--muted)">
|
||||
<li><strong style="color:var(--ink)">config/config.php</strong> und <strong style="color:var(--ink)">database/deskhub.sqlite</strong> werden niemals überschrieben</li>
|
||||
<?php if ($signedOnly): ?><li><strong style="color:var(--ink)">Nur signierte Releases:</strong> Updates werden nur installiert, wenn sie mit dem Schlüssel des Herausgebers signiert sind. Ein manipuliertes Paket wird abgelehnt. (Ein manuell hochgeladenes ZIP wird nicht geprüft: Laden Sie nur Pakete aus vertrauenswürdiger Quelle hoch.)</li><?php endif; ?>
|
||||
<?php if ($relErr): ?><li style="color:#b45309">Release-Liste konnte nicht geladen werden: <?= htmlspecialchars($relErr) ?></li><?php endif; ?>
|
||||
<?php if ($signedOnly): ?><li><strong style="color:var(--ink)">Nur signierte Releases:</strong> Updates werden nur installiert, wenn sie mit dem Schlüssel des Herausgebers signiert sind. Ein manipuliertes Paket wird abgelehnt. (Ein manuell hochgeladenes ZIP wird nicht geprüft: Laden Sie nur Pakete aus vertrauenswürdiger Quelle hoch.)</li><?php endif; ?>
|
||||
|
||||
<li>Nach dem Update Browser-Cache leeren <kbd>Strg+F5</kbd></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user