Upload via GUI (335 Dateien)
This commit is contained in:
@@ -1058,3 +1058,79 @@ body:has(.auth-wrap) .page-content > .auth-wrap {
|
|||||||
details > summary { outline: none; }
|
details > summary { outline: none; }
|
||||||
details > summary::-webkit-details-marker { display: none; }
|
details > summary::-webkit-details-marker { display: none; }
|
||||||
details[open] > summary span:last-child { transform: rotate(180deg); display:inline-block; }
|
details[open] > summary span:last-child { transform: rotate(180deg); display:inline-block; }
|
||||||
|
|
||||||
|
/* Anmeldeseite der Mitarbeiter: Kopf- und Fußzeile, Hintergrund. Themes setzen --staff-login-* oder überschreiben die Klassen. */
|
||||||
|
body.staff-login-page {
|
||||||
|
margin: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 100vh;
|
||||||
|
background-color: var(--staff-login-bg, #16202a);
|
||||||
|
}
|
||||||
|
.staff-login-header {
|
||||||
|
display: flex; align-items: center; justify-content: space-between; flex-shrink: 0;
|
||||||
|
height: 56px; padding: 0 28px;
|
||||||
|
background: var(--staff-login-chrome, rgba(14,26,34,.95));
|
||||||
|
border-bottom: var(--staff-login-line, 1px solid rgba(255,255,255,.08));
|
||||||
|
}
|
||||||
|
.staff-login-brand { display: flex; align-items: center; gap: 12px; }
|
||||||
|
.staff-login-brand img { height: 32px; width: auto; }
|
||||||
|
.staff-login-brand-name { color: var(--staff-login-chrome-ink, #fff); font-weight: 700; font-size: .95rem; }
|
||||||
|
.staff-login-back { color: var(--staff-login-chrome-muted, rgba(255,255,255,.5)); font-size: .8rem; text-decoration: none; }
|
||||||
|
.staff-login-back:hover { color: var(--staff-login-chrome-ink, #fff); }
|
||||||
|
.staff-login-footer {
|
||||||
|
flex-shrink: 0; padding: 12px 28px; text-align: center;
|
||||||
|
background: var(--staff-login-chrome, rgba(14,26,34,.95));
|
||||||
|
border-top: var(--staff-login-line, 1px solid rgba(255,255,255,.08));
|
||||||
|
}
|
||||||
|
.staff-login-footer span { color: var(--staff-login-chrome-faint, rgba(255,255,255,.3)); font-size: .75rem; }
|
||||||
|
.staff-login-title { font-size: 1.4rem; font-weight: 700; margin: 0 0 4px; color: #0f1923; }
|
||||||
|
|
||||||
|
/* ── Seitenleiste ein-/ausklappen (eingeklappt bleibt eine Iconleiste) ─────────── */
|
||||||
|
.sidebar-toggle {
|
||||||
|
border: 0; cursor: pointer; font: inherit; line-height: 1;
|
||||||
|
color: #a8b8c8; background: rgba(255,255,255,.07);
|
||||||
|
width: 30px; height: 30px; border-radius: 8px; font-size: 1.1rem;
|
||||||
|
transition: background .12s, color .12s;
|
||||||
|
}
|
||||||
|
.sidebar-toggle:hover { background: rgba(255,255,255,.16); color: #fff; }
|
||||||
|
.sidebar-toggle-close { position: absolute; top: 16px; right: 12px; z-index: 2; }
|
||||||
|
.sidebar-toggle-open { display: none; }
|
||||||
|
.sidebar .nav-ico { display: none; }
|
||||||
|
.sidebar .nav-ico svg { display: block; }
|
||||||
|
|
||||||
|
@media (min-width: 821px) {
|
||||||
|
html.sidebar-closed .sidebar { width: 64px; overflow-x: hidden; }
|
||||||
|
html.sidebar-closed .sidebar .brand { height: 62px; padding: 0; font-size: 0; overflow: hidden; }
|
||||||
|
html.sidebar-closed .sidebar .brand > * { display: none; }
|
||||||
|
html.sidebar-closed .sidebar .sidebar-toggle-close { right: 17px; }
|
||||||
|
html.sidebar-closed .sidebar nav a { justify-content: center; padding: 11px 0; gap: 0; }
|
||||||
|
html.sidebar-closed .sidebar .nav-ico { display: flex; }
|
||||||
|
html.sidebar-closed .sidebar .nav-label { display: none; }
|
||||||
|
html.sidebar-closed .sidebar .nav-section-label { display: none; }
|
||||||
|
html.sidebar-closed .sidebar .nav-admin-block { display: block !important; border-top: 1px solid #243040; margin-top: 6px; padding-top: 6px; }
|
||||||
|
html.sidebar-closed .sidebar .nav-group-label { display: block; height: 1px; padding: 0; margin: 8px 14px; background: #243040; font-size: 0; overflow: hidden; }
|
||||||
|
html.sidebar-closed .sidebar .sidebar-footer { padding: 10px 0; }
|
||||||
|
html.sidebar-closed .sidebar .sidebar-footer > div { flex-direction: column; justify-content: center; align-items: center; gap: 12px !important; margin-bottom: 10px !important; }
|
||||||
|
html.sidebar-closed .sidebar .sidebar-footer > div:first-child > div:last-child { display: none; }
|
||||||
|
html.sidebar-closed .sidebar .sidebar-footer a { display: flex; }
|
||||||
|
html.sidebar-closed .sidebar .sidebar-footer .nav-ico { display: flex; }
|
||||||
|
html.sidebar-closed .sidebar .sidebar-footer .nav-label { display: none; }
|
||||||
|
}
|
||||||
|
@media (max-width: 820px) {
|
||||||
|
.sidebar { position: relative; }
|
||||||
|
.sidebar-toggle-close { top: 14px; }
|
||||||
|
.sidebar-toggle-open { position: fixed; top: 12px; left: 12px; z-index: 9998; background: var(--ink); color: #fff; box-shadow: 0 2px 8px rgba(0,0,0,.25); }
|
||||||
|
html.sidebar-closed .sidebar { display: none; }
|
||||||
|
html.sidebar-closed .sidebar-toggle-open { display: block; }
|
||||||
|
html.sidebar-closed .main { padding-top: 56px; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Das Logo darf nicht unter den Einklappen-Knopf rutschen */
|
||||||
|
.sidebar .brand { padding-right: 52px; }
|
||||||
|
.sidebar .brand img { max-width: 100% !important; height: auto; }
|
||||||
|
|
||||||
|
/* Sprachwechsler: in der schmalen Iconleiste nicht anzeigen */
|
||||||
|
@media (min-width: 821px) {
|
||||||
|
html.sidebar-closed .sidebar .lang-row { display: none; }
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,103 @@
|
|||||||
|
/**
|
||||||
|
* Live-Anzeige im Mitarbeiterbereich: neue Tickets erscheinen sofort als Hinweis unten rechts, die Zahl steht im Browser-Tab und die
|
||||||
|
* Kennzahlen auf der Übersicht (Elemente mit data-live-count) aktualisieren sich von selbst. Fragt /staff/live_tickets.php ab.
|
||||||
|
*/
|
||||||
|
(function () {
|
||||||
|
'use strict';
|
||||||
|
if (window.__deskhubLive) return;
|
||||||
|
window.__deskhubLive = true;
|
||||||
|
|
||||||
|
var KEY = 'deskhub_live_last_ticket';
|
||||||
|
var since = 0;
|
||||||
|
try { since = parseInt(sessionStorage.getItem(KEY) || '0', 10) || 0; } catch (e) {}
|
||||||
|
var unseen = 0;
|
||||||
|
var baseTitle = document.title;
|
||||||
|
var stopped = false;
|
||||||
|
var box = null;
|
||||||
|
|
||||||
|
function container() {
|
||||||
|
if (box) return box;
|
||||||
|
box = document.createElement('div');
|
||||||
|
box.setAttribute('aria-live', 'polite');
|
||||||
|
box.style.cssText = 'position:fixed;right:18px;bottom:18px;z-index:2147483000;display:flex;flex-direction:column;gap:10px;max-width:360px;font:14px/1.4 "Segoe UI",system-ui,sans-serif';
|
||||||
|
document.body.appendChild(box);
|
||||||
|
return box;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toast(t) {
|
||||||
|
var a = document.createElement('a');
|
||||||
|
a.href = '/staff/ticket_view.php?n=' + encodeURIComponent(t.number);
|
||||||
|
a.style.cssText = 'display:block;background:#0f172a;color:#fff;text-decoration:none;border-radius:12px;padding:12px 16px;box-shadow:0 10px 30px rgba(0,0,0,.35);border-left:4px solid #2dd4bf;transition:opacity .4s,transform .4s;opacity:0;transform:translateY(10px)';
|
||||||
|
var h = document.createElement('div');
|
||||||
|
h.style.cssText = 'font-size:.74rem;letter-spacing:.04em;text-transform:uppercase;color:#5eead4;font-weight:700';
|
||||||
|
h.textContent = 'Neues Ticket #' + t.number + (t.department ? ' · ' + t.department : '');
|
||||||
|
var s = document.createElement('div');
|
||||||
|
s.style.cssText = 'font-weight:600;margin-top:2px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap';
|
||||||
|
s.textContent = t.subject;
|
||||||
|
var n = document.createElement('div');
|
||||||
|
n.style.cssText = 'color:#94a3b8;font-size:.82rem';
|
||||||
|
n.textContent = t.name;
|
||||||
|
a.appendChild(h); a.appendChild(s); a.appendChild(n);
|
||||||
|
container().appendChild(a);
|
||||||
|
requestAnimationFrame(function () { a.style.opacity = '1'; a.style.transform = 'none'; });
|
||||||
|
setTimeout(function () { a.style.opacity = '0'; setTimeout(function () { a.remove(); }, 450); }, 14000);
|
||||||
|
a.addEventListener('click', function () { a.remove(); });
|
||||||
|
}
|
||||||
|
|
||||||
|
function info(text) {
|
||||||
|
var d = document.createElement('div');
|
||||||
|
d.style.cssText = 'background:#334155;color:#e2e8f0;border-radius:10px;padding:8px 14px;font-size:.84rem;box-shadow:0 6px 18px rgba(0,0,0,.3)';
|
||||||
|
d.textContent = text;
|
||||||
|
container().appendChild(d);
|
||||||
|
setTimeout(function () { d.remove(); }, 14000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setCounts(c) {
|
||||||
|
document.querySelectorAll('[data-live-count]').forEach(function (el) {
|
||||||
|
var v = c[el.getAttribute('data-live-count')];
|
||||||
|
if (v === undefined || el.textContent.trim() === String(v)) return;
|
||||||
|
el.textContent = v;
|
||||||
|
el.style.transition = 'transform .3s';
|
||||||
|
el.style.transform = 'scale(1.25)';
|
||||||
|
setTimeout(function () { el.style.transform = 'none'; }, 350);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateTitle() {
|
||||||
|
document.title = unseen > 0 ? '(' + unseen + ') ' + baseTitle : baseTitle;
|
||||||
|
}
|
||||||
|
function seen() {
|
||||||
|
if (unseen === 0) return;
|
||||||
|
unseen = 0;
|
||||||
|
updateTitle();
|
||||||
|
}
|
||||||
|
document.addEventListener('visibilitychange', function () { if (!document.hidden) seen(); });
|
||||||
|
window.addEventListener('focus', seen);
|
||||||
|
|
||||||
|
function poll() {
|
||||||
|
if (stopped) return;
|
||||||
|
fetch('/staff/live_tickets.php?since=' + since, { credentials: 'same-origin', cache: 'no-store' })
|
||||||
|
.then(function (r) {
|
||||||
|
if (r.status === 401 || r.status === 403) { stopped = true; return null; }
|
||||||
|
return r.ok ? r.json() : null;
|
||||||
|
})
|
||||||
|
.then(function (d) {
|
||||||
|
if (!d) return;
|
||||||
|
setCounts(d.counts || {});
|
||||||
|
var list = d.tickets || [];
|
||||||
|
list.forEach(toast);
|
||||||
|
if (d.more > 0) info('… und ' + d.more + ' weitere neue Tickets');
|
||||||
|
if (list.length) {
|
||||||
|
unseen += list.length + (d.more || 0);
|
||||||
|
if (document.hidden || !document.hasFocus()) updateTitle(); else unseen = 0;
|
||||||
|
}
|
||||||
|
if (d.latest > since) {
|
||||||
|
since = d.latest;
|
||||||
|
try { sessionStorage.setItem(KEY, String(since)); } catch (e) {}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(function () {})
|
||||||
|
.then(function () { if (!stopped) setTimeout(poll, document.hidden ? 30000 : 10000); });
|
||||||
|
}
|
||||||
|
setTimeout(poll, 1500);
|
||||||
|
})();
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
/**
|
||||||
|
* Seitenleiste im Mitarbeiterbereich: ein-/ausklappen. Eingeklappt bleibt eine schmale Iconleiste sichtbar.
|
||||||
|
* Die Icons werden hier anhand der Seitenadresse zugeordnet, auch für Menüpunkte von Plugins (sonst ein Puzzle-Teil).
|
||||||
|
*/
|
||||||
|
(function () {
|
||||||
|
var root = document.documentElement, KEY = 'deskhub_sidebar';
|
||||||
|
var side = document.getElementById('sidebar');
|
||||||
|
if (!side) return;
|
||||||
|
|
||||||
|
var P = {
|
||||||
|
grid: '<rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/>',
|
||||||
|
chart: '<line x1="18" y1="20" x2="18" y2="10"/><line x1="12" y1="20" x2="12" y2="4"/><line x1="6" y1="20" x2="6" y2="14"/>',
|
||||||
|
inbox: '<polyline points="22 12 16 12 14 15 10 15 8 12 2 12"/><path d="M5.45 5.11L2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z"/>',
|
||||||
|
check: '<polyline points="9 11 12 14 22 4"/><path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"/>',
|
||||||
|
ban: '<circle cx="12" cy="12" r="10"/><line x1="4.9" y1="4.9" x2="19.1" y2="19.1"/>',
|
||||||
|
users: '<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/>',
|
||||||
|
user: '<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/>',
|
||||||
|
phone: '<rect x="5" y="2" width="14" height="20" rx="2"/><line x1="12" y1="18" x2="12.01" y2="18"/>',
|
||||||
|
chat: '<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>',
|
||||||
|
puzzle: '<path d="M20.5 11H19V7a2 2 0 0 0-2-2h-4V3.5a2.5 2.5 0 0 0-5 0V5H4a2 2 0 0 0-2 2v3.8h1.5a2.7 2.7 0 0 1 0 5.4H2V20a2 2 0 0 0 2 2h3.8v-1.5a2.7 2.7 0 0 1 5.4 0V22H17a2 2 0 0 0 2-2v-4h1.5a2.5 2.5 0 0 0 0-5z"/>',
|
||||||
|
building: '<rect x="4" y="2" width="16" height="20" rx="2"/><line x1="9" y1="22" x2="9" y2="18"/><line x1="15" y1="22" x2="15" y2="18"/><line x1="8" y1="6" x2="8.01" y2="6"/><line x1="12" y1="6" x2="12.01" y2="6"/><line x1="16" y1="6" x2="16.01" y2="6"/><line x1="8" y1="10" x2="8.01" y2="10"/><line x1="12" y1="10" x2="12.01" y2="10"/><line x1="16" y1="10" x2="16.01" y2="10"/>',
|
||||||
|
flag: '<path d="M4 15s1-1 4-1 5 2 8 2 4-1 4-1V3s-1 1-4 1-5-2-8-2-4 1-4 1z"/><line x1="4" y1="22" x2="4" y2="15"/>',
|
||||||
|
clock: '<circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/>',
|
||||||
|
help: '<circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><line x1="12" y1="17" x2="12.01" y2="17"/>',
|
||||||
|
sliders: '<line x1="4" y1="21" x2="4" y2="14"/><line x1="4" y1="10" x2="4" y2="3"/><line x1="12" y1="21" x2="12" y2="12"/><line x1="12" y1="8" x2="12" y2="3"/><line x1="20" y1="21" x2="20" y2="16"/><line x1="20" y1="12" x2="20" y2="3"/><line x1="1" y1="14" x2="7" y2="14"/><line x1="9" y1="8" x2="15" y2="8"/><line x1="17" y1="16" x2="23" y2="16"/>',
|
||||||
|
key: '<path d="M21 2l-2 2m-7.61 7.61a5.5 5.5 0 1 1-7.78 7.78 5.5 5.5 0 0 1 7.78-7.78zm0 0L15.5 7.5m0 0l3 3L22 7l-3-3m-3.5 3.5L19 4"/>',
|
||||||
|
link: '<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/>',
|
||||||
|
list: '<line x1="8" y1="6" x2="21" y2="6"/><line x1="8" y1="12" x2="21" y2="12"/><line x1="8" y1="18" x2="21" y2="18"/><line x1="3" y1="6" x2="3.01" y2="6"/><line x1="3" y1="12" x2="3.01" y2="12"/><line x1="3" y1="18" x2="3.01" y2="18"/>',
|
||||||
|
file: '<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/>',
|
||||||
|
mail: '<path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"/><polyline points="22,6 12,13 2,6"/>',
|
||||||
|
book: '<path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/>',
|
||||||
|
bot: '<rect x="3" y="8" width="18" height="12" rx="2"/><path d="M12 8V4"/><circle cx="12" cy="3" r="1"/><line x1="8" y1="14" x2="8.01" y2="14"/><line x1="16" y1="14" x2="16.01" y2="14"/>',
|
||||||
|
briefcase: '<rect x="2" y="7" width="20" height="14" rx="2"/><path d="M16 21V5a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v16"/>',
|
||||||
|
shield: '<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/>',
|
||||||
|
shieldCheck: '<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/><polyline points="9 12 11 14 15 10"/>',
|
||||||
|
calendar: '<rect x="3" y="4" width="18" height="18" rx="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/>',
|
||||||
|
clipboard: '<path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"/><rect x="8" y="2" width="8" height="4" rx="1"/>',
|
||||||
|
branch: '<line x1="6" y1="3" x2="6" y2="15"/><circle cx="18" cy="6" r="3"/><circle cx="6" cy="18" r="3"/><path d="M18 9a9 9 0 0 1-9 9"/>',
|
||||||
|
trend: '<polyline points="23 6 13.5 15.5 8.5 10.5 1 18"/><polyline points="17 6 23 6 23 12"/>',
|
||||||
|
repeat: '<polyline points="17 1 21 5 17 9"/><path d="M3 11V9a4 4 0 0 1 4-4h14"/><polyline points="7 23 3 19 7 15"/><path d="M21 13v2a4 4 0 0 1-4 4H3"/>',
|
||||||
|
gear: '<circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/>',
|
||||||
|
db: '<ellipse cx="12" cy="5" rx="9" ry="3"/><path d="M21 12c0 1.66-4 3-9 3s-9-1.34-9-3"/><path d="M3 5v14c0 1.66 4 3 9 3s9-1.34 9-3V5"/>',
|
||||||
|
download: '<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/>',
|
||||||
|
lock: '<rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/>',
|
||||||
|
activity: '<polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/>',
|
||||||
|
megaphone: '<path d="M3 11l18-8v18L3 13z"/><path d="M11.6 16.8a3 3 0 1 1-5.8-1.6"/>',
|
||||||
|
sun: '<circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/>',
|
||||||
|
userx: '<path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="8.5" cy="7" r="4"/><line x1="18" y1="8" x2="23" y2="13"/><line x1="23" y1="8" x2="18" y2="13"/>',
|
||||||
|
logout: '<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><polyline points="16 17 21 12 16 7"/><line x1="21" y1="12" x2="9" y2="12"/>',
|
||||||
|
globe: '<circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/>',
|
||||||
|
filter: '<polygon points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3"/>'
|
||||||
|
};
|
||||||
|
var PAGES = {
|
||||||
|
reports: 'grid', report_builder: 'chart', tickets: 'inbox', tasks: 'check', banlist: 'ban', agent_directory: 'users', apps: 'phone', live_chat: 'chat',
|
||||||
|
departments: 'building', priorities: 'flag', sla_plans: 'clock', help_topics: 'help', custom_fields: 'sliders', api_keys: 'key', webhooks: 'link',
|
||||||
|
custom_lists: 'list', canned_responses: 'file', site_pages: 'file', email_addresses: 'mail', email_templates: 'mail', kb_manage: 'book', assistant: 'bot',
|
||||||
|
plugins: 'puzzle', customers: 'user', organizations: 'briefcase', staff_manage: 'users', teams: 'users', roles: 'shield', availability: 'calendar',
|
||||||
|
security_check: 'shieldCheck', audit_log: 'clipboard', ticket_rules: 'branch', escalation_rules: 'trend', recurring_tickets: 'repeat', time_entries: 'clock',
|
||||||
|
business_hours: 'clock', settings: 'gear', backup: 'db', update: 'download', profile: 'user', logout: 'logout', ticket_templates: 'file', status_page: 'activity', languages: 'globe'
|
||||||
|
};
|
||||||
|
var PLUGINS = {
|
||||||
|
schichtuebergabe: 'sun', spamfilter: 'filter', vertretung: 'userx', virenscan: 'shieldCheck', datenschutz: 'lock', ueberwachung: 'activity', rundmail: 'megaphone',
|
||||||
|
'community-bot': 'bot', 'nas-backup': 'db', tagesbericht: 'clipboard'
|
||||||
|
};
|
||||||
|
|
||||||
|
function svg(name) {
|
||||||
|
return '<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">' + (P[name] || P.puzzle) + '</svg>';
|
||||||
|
}
|
||||||
|
function iconFor(a) {
|
||||||
|
var u;
|
||||||
|
try { u = new URL(a.getAttribute('href'), location.href); } catch (e) { return 'puzzle'; }
|
||||||
|
if (/\/staff\/plugin\.php$/.test(u.pathname)) return PLUGINS[u.searchParams.get('p')] || 'puzzle';
|
||||||
|
var m = u.pathname.match(/\/([a-z_]+)\.php$/);
|
||||||
|
return (m && PAGES[m[1]]) || 'puzzle';
|
||||||
|
}
|
||||||
|
|
||||||
|
var links = side.querySelectorAll('nav a[href], .sidebar-footer a[href$="profile.php"], .sidebar-footer a[href$="logout.php"]');
|
||||||
|
Array.prototype.forEach.call(links, function (a) {
|
||||||
|
if (a.querySelector('.nav-ico')) return;
|
||||||
|
var text = (a.textContent || '').replace(/\s+/g, ' ').trim();
|
||||||
|
if (!text) return;
|
||||||
|
var ico = document.createElement('span');
|
||||||
|
ico.className = 'nav-ico';
|
||||||
|
ico.innerHTML = svg(iconFor(a));
|
||||||
|
var label = document.createElement('span');
|
||||||
|
label.className = 'nav-label';
|
||||||
|
while (a.firstChild) label.appendChild(a.firstChild);
|
||||||
|
a.appendChild(ico);
|
||||||
|
a.appendChild(label);
|
||||||
|
a.setAttribute('title', text.replace(/\s*\d+\s*$/, ''));
|
||||||
|
});
|
||||||
|
|
||||||
|
var btn = document.getElementById('sidebar-toggle');
|
||||||
|
function paint() {
|
||||||
|
var closed = root.classList.contains('sidebar-closed');
|
||||||
|
btn.innerHTML = closed ? '»' : '«';
|
||||||
|
var t = closed ? 'Seitenleiste ausklappen' : 'Seitenleiste einklappen';
|
||||||
|
btn.setAttribute('title', t);
|
||||||
|
btn.setAttribute('aria-label', t);
|
||||||
|
}
|
||||||
|
btn.addEventListener('click', function () {
|
||||||
|
var closed = !root.classList.contains('sidebar-closed');
|
||||||
|
root.classList.toggle('sidebar-closed', closed);
|
||||||
|
try { localStorage.setItem(KEY, closed ? 'closed' : 'open'); } catch (e) {}
|
||||||
|
paint();
|
||||||
|
});
|
||||||
|
paint();
|
||||||
|
})();
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
/**
|
||||||
|
* Verkleinert große Fotos, bevor sie hochgeladen werden (Handy-Fotos haben oft 5–15 MB).
|
||||||
|
* Das macht den Upload deutlich schneller. Ist etwas nicht möglich, wird die Originaldatei unverändert hochgeladen.
|
||||||
|
* Gilt für alle <input type="file"> auf der Seite; nur JPG/PNG über 1 MB, längste Seite höchstens 2400 px.
|
||||||
|
*/
|
||||||
|
(function () {
|
||||||
|
var MAX_SIDE = 2400, MIN_BYTES = 1024 * 1024, QUALITY = 0.82;
|
||||||
|
if (!window.DataTransfer || !window.HTMLCanvasElement || !window.URL) return;
|
||||||
|
|
||||||
|
function note(input, text) {
|
||||||
|
var el = input.parentNode && input.parentNode.querySelector('.upload-note');
|
||||||
|
if (!el) {
|
||||||
|
el = document.createElement('div');
|
||||||
|
el.className = 'upload-note';
|
||||||
|
el.style.cssText = 'font-size:.8rem;color:#64748b;margin-top:4px';
|
||||||
|
input.insertAdjacentElement('afterend', el);
|
||||||
|
}
|
||||||
|
el.textContent = text;
|
||||||
|
}
|
||||||
|
|
||||||
|
function shrink(file) {
|
||||||
|
return new Promise(function (resolve) {
|
||||||
|
if (!/^image\/(jpeg|png)$/.test(file.type) || file.size < MIN_BYTES) return resolve(file);
|
||||||
|
var url = URL.createObjectURL(file), img = new Image();
|
||||||
|
img.onerror = function () { URL.revokeObjectURL(url); resolve(file); };
|
||||||
|
img.onload = function () {
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
var w = img.naturalWidth, h = img.naturalHeight, k = Math.min(1, MAX_SIDE / Math.max(w, h));
|
||||||
|
var c = document.createElement('canvas');
|
||||||
|
c.width = Math.round(w * k); c.height = Math.round(h * k);
|
||||||
|
var ctx = c.getContext('2d');
|
||||||
|
if (!ctx) return resolve(file);
|
||||||
|
ctx.fillStyle = '#fff';
|
||||||
|
ctx.fillRect(0, 0, c.width, c.height);
|
||||||
|
ctx.drawImage(img, 0, 0, c.width, c.height);
|
||||||
|
c.toBlob(function (blob) {
|
||||||
|
if (!blob || blob.size >= file.size) return resolve(file);
|
||||||
|
var name = file.name.replace(/\.(png|jpe?g)$/i, '') + '.jpg';
|
||||||
|
resolve(new File([blob], name, { type: 'image/jpeg', lastModified: file.lastModified }));
|
||||||
|
}, 'image/jpeg', QUALITY);
|
||||||
|
};
|
||||||
|
img.src = url;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('change', function (e) {
|
||||||
|
var input = e.target;
|
||||||
|
if (!input || input.type !== 'file' || input.dataset.shrunk === '1' || !input.files || !input.files.length) return;
|
||||||
|
var files = Array.prototype.slice.call(input.files);
|
||||||
|
if (!files.some(function (f) { return /^image\/(jpeg|png)$/.test(f.type) && f.size >= MIN_BYTES; })) return;
|
||||||
|
note(input, 'Bilder werden für einen schnelleren Upload verkleinert …');
|
||||||
|
Promise.all(files.map(shrink)).then(function (out) {
|
||||||
|
try {
|
||||||
|
var dt = new DataTransfer();
|
||||||
|
out.forEach(function (f) { dt.items.add(f); });
|
||||||
|
input.dataset.shrunk = '1';
|
||||||
|
input.files = dt.files;
|
||||||
|
input.dispatchEvent(new Event('change', { bubbles: true }));
|
||||||
|
input.dataset.shrunk = '';
|
||||||
|
var before = files.reduce(function (s, f) { return s + f.size; }, 0), after = out.reduce(function (s, f) { return s + f.size; }, 0);
|
||||||
|
note(input, after < before ? 'Bilder verkleinert: ' + (before / 1048576).toFixed(1) + ' MB → ' + (after / 1048576).toFixed(1) + ' MB' : '');
|
||||||
|
} catch (err) { note(input, ''); }
|
||||||
|
});
|
||||||
|
}, true);
|
||||||
|
|
||||||
|
// Beim Absenden: Knopf sperren, damit nicht doppelt hochgeladen wird, und Fortschritt anzeigen
|
||||||
|
document.addEventListener('submit', function (e) {
|
||||||
|
var form = e.target;
|
||||||
|
if (!form || !form.querySelector || !form.querySelector('input[type=file]')) return;
|
||||||
|
var has = Array.prototype.some.call(form.querySelectorAll('input[type=file]'), function (i) { return i.files && i.files.length; });
|
||||||
|
if (!has) return;
|
||||||
|
setTimeout(function () {
|
||||||
|
if (e.defaultPrevented) return;
|
||||||
|
var b = form.querySelector('button[type=submit]');
|
||||||
|
if (b) { b.disabled = true; b.textContent = 'Wird hochgeladen …'; }
|
||||||
|
}, 0);
|
||||||
|
});
|
||||||
|
})();
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
<?php
|
||||||
|
// Direkter Web-Zugriff wird geblockt (nur CLI oder Einbindung aus dem Backend/Pseudo-Cron).
|
||||||
|
if (php_sapi_name() !== 'cli' && !defined('DESKHUB_CRON_RUNNER')) { http_response_code(403); exit('Kein Web-Zugriff.'); }
|
||||||
|
/**
|
||||||
|
* cron/run_plugins.php
|
||||||
|
*
|
||||||
|
* Löst die Hintergrundaufgaben der Plugins aus (Hook "cron.run"): Überwachung, Tagesbericht, NAS-Backup, Rundmail-Versand usw.
|
||||||
|
* Läuft jede Minute, z. B. per Cronjob (wird mit „Neu installieren / aktualisieren“ unter Einstellungen → Cron-Jobs eingetragen):
|
||||||
|
* * * * * * /usr/bin/php /pfad/zu/deskhub/cron/run_plugins.php
|
||||||
|
* Das Ereignis „cron.minute“ kommt bei jedem Lauf, „cron.run“ höchstens alle 5 Minuten.
|
||||||
|
*/
|
||||||
|
require_once __DIR__ . '/../includes/bootstrap.php';
|
||||||
|
|
||||||
|
/** @return array{count:int, log:string[]} */
|
||||||
|
function run_plugins_cron(PDO $pdo, bool $verbose = false): array
|
||||||
|
{
|
||||||
|
$n = count(plugin_state()['loaded'] ?? []);
|
||||||
|
// „cron.minute“: bei jedem Lauf (etwa jede Minute), für Aufgaben mit kurzem Prüfabstand (zum Beispiel die Überwachung)
|
||||||
|
plugin_hook('cron.minute', ['time' => time()]);
|
||||||
|
// „cron.run“: wie bisher höchstens alle 5 Minuten
|
||||||
|
if (!function_exists('pseudo_cron_try_claim') || pseudo_cron_try_claim($pdo, 'cron_plugins_run_last', 290)) plugin_hook('cron.run', ['time' => time()]);
|
||||||
|
// Plugin- und Theme-Katalog aktuell halten (höchstens alle 5 Minuten)
|
||||||
|
if (function_exists('catalog_refresh_background') && (!function_exists('pseudo_cron_try_claim') || pseudo_cron_try_claim($pdo, 'catalog_refresh_last_run', 280))) catalog_refresh_background();
|
||||||
|
set_setting('cron_run_plugins_last_run', db_now());
|
||||||
|
// Woher kam der Lauf? Der echte Server-Cronjob (Kommandozeile) oder der eingebaute Hintergrund-Betrieb bei einem Seitenaufruf
|
||||||
|
set_setting('cron_run_plugins_source', php_sapi_name() === 'cli' ? 'cron' : 'web');
|
||||||
|
if (php_sapi_name() === 'cli') set_setting('cron_run_plugins_last_cli', db_now());
|
||||||
|
return ['count' => $n, 'log' => ['[' . date('Y-m-d H:i:s') . "] Hintergrundaufgaben von {$n} aktiven Plugin(s) ausgelöst."]];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (php_sapi_name() === 'cli') {
|
||||||
|
// Läuft zeitgleich der eingebaute Hintergrund-Betrieb, nicht doppelt ausführen (gleiche Sperre wie dort)
|
||||||
|
if (!function_exists('pseudo_cron_try_claim') || pseudo_cron_try_claim(get_db(), 'cron_run_plugins_last_run', 50)) {
|
||||||
|
foreach (run_plugins_cron(get_db(), true)['log'] as $line) echo $line . "\n";
|
||||||
|
} else {
|
||||||
|
echo "[" . date('Y-m-d H:i:s') . "] Übersprungen: wurde gerade erst ausgeführt.\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
<?php
|
||||||
|
// Direkter Web-Zugriff wird geblockt (nur CLI oder Einbindung aus dem Backend/Pseudo-Cron).
|
||||||
|
if (php_sapi_name() !== 'cli' && !defined('DESKHUB_CRON_RUNNER')) { http_response_code(403); exit('Kein Web-Zugriff.'); }
|
||||||
|
/**
|
||||||
|
* cron/send_csat.php
|
||||||
|
*
|
||||||
|
* Verschickt die Zufriedenheitsabfrage an Kunden, deren Ticket geschlossen oder gelöst wurde (Einstellungen → Allgemein →
|
||||||
|
* „Zufriedenheitsabfrage“). Pro Ticket gibt es höchstens eine Mail.
|
||||||
|
*/
|
||||||
|
require_once __DIR__ . '/../includes/bootstrap.php';
|
||||||
|
|
||||||
|
/** @return array{sent:int, log:string[]} */
|
||||||
|
function run_send_csat(PDO $pdo, bool $verbose = false): array
|
||||||
|
{
|
||||||
|
set_setting('cron_send_csat_last_run', db_now());
|
||||||
|
if (!csat_enabled()) {
|
||||||
|
return ['sent' => 0, 'log' => ['[' . date('Y-m-d H:i:s') . '] Zufriedenheitsabfrage ist deaktiviert – nichts zu tun.']];
|
||||||
|
}
|
||||||
|
$sent = csat_send_due($pdo);
|
||||||
|
return ['sent' => $sent, 'log' => ['[' . date('Y-m-d H:i:s') . "] Zufriedenheitsabfrage: {$sent} Mail(s) gesendet."]];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (php_sapi_name() === 'cli') {
|
||||||
|
foreach (run_send_csat(get_db(), true)['log'] as $line) echo $line . "\n";
|
||||||
|
}
|
||||||
@@ -1330,6 +1330,114 @@ function run_migrations(PDO $pdo, bool $verbose = false): array
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Migration 0048: Zufriedenheitsabfrage (CSAT), Statusseite für Kunden und Ticket-Vorlagen.
|
||||||
|
if (!isset($done['php_0048_csat_status_templates'])) {
|
||||||
|
try {
|
||||||
|
$drv = $pdo->getAttribute(PDO::ATTR_DRIVER_NAME);
|
||||||
|
$ai = $drv === 'sqlite' ? 'INTEGER PRIMARY KEY AUTOINCREMENT' : 'INT AUTO_INCREMENT PRIMARY KEY';
|
||||||
|
$eng = $drv === 'sqlite' ? '' : ' ENGINE=InnoDB DEFAULT CHARSET=utf8mb4';
|
||||||
|
$pdo->exec("CREATE TABLE IF NOT EXISTS ticket_csat (
|
||||||
|
id $ai,
|
||||||
|
ticket_id INT NOT NULL,
|
||||||
|
token VARCHAR(64) NOT NULL,
|
||||||
|
staff_id INT NULL,
|
||||||
|
department_id INT NULL,
|
||||||
|
rating TINYINT NULL,
|
||||||
|
comment TEXT NULL,
|
||||||
|
sent_at DATETIME NOT NULL,
|
||||||
|
answered_at DATETIME NULL
|
||||||
|
)$eng");
|
||||||
|
$pdo->exec('CREATE UNIQUE INDEX idx_ticket_csat_ticket ON ticket_csat (ticket_id)');
|
||||||
|
$pdo->exec('CREATE UNIQUE INDEX idx_ticket_csat_token ON ticket_csat (token)');
|
||||||
|
$pdo->exec("CREATE TABLE IF NOT EXISTS status_components (
|
||||||
|
id $ai,
|
||||||
|
name VARCHAR(120) NOT NULL,
|
||||||
|
description VARCHAR(255) NULL,
|
||||||
|
status VARCHAR(20) NOT NULL DEFAULT 'operational',
|
||||||
|
sort_order INT NOT NULL DEFAULT 0,
|
||||||
|
created_at DATETIME NOT NULL
|
||||||
|
)$eng");
|
||||||
|
$pdo->exec("CREATE TABLE IF NOT EXISTS status_incidents (
|
||||||
|
id $ai,
|
||||||
|
title VARCHAR(200) NOT NULL,
|
||||||
|
body TEXT NULL,
|
||||||
|
status VARCHAR(20) NOT NULL DEFAULT 'investigating',
|
||||||
|
impact VARCHAR(20) NOT NULL DEFAULT 'minor',
|
||||||
|
component_id INT NULL,
|
||||||
|
created_by INT NULL,
|
||||||
|
created_at DATETIME NOT NULL,
|
||||||
|
updated_at DATETIME NOT NULL,
|
||||||
|
resolved_at DATETIME NULL
|
||||||
|
)$eng");
|
||||||
|
$pdo->exec('CREATE INDEX idx_status_incidents_created ON status_incidents (created_at)');
|
||||||
|
$pdo->exec("CREATE TABLE IF NOT EXISTS status_incident_updates (
|
||||||
|
id $ai,
|
||||||
|
incident_id INT NOT NULL,
|
||||||
|
status VARCHAR(20) NOT NULL,
|
||||||
|
body TEXT NULL,
|
||||||
|
created_by INT NULL,
|
||||||
|
created_at DATETIME NOT NULL
|
||||||
|
)$eng");
|
||||||
|
$pdo->exec('CREATE INDEX idx_status_incident_updates_incident ON status_incident_updates (incident_id)');
|
||||||
|
$pdo->exec("CREATE TABLE IF NOT EXISTS ticket_templates (
|
||||||
|
id $ai,
|
||||||
|
name VARCHAR(120) NOT NULL,
|
||||||
|
description VARCHAR(255) NULL,
|
||||||
|
subject VARCHAR(200) NOT NULL DEFAULT '',
|
||||||
|
body TEXT NULL,
|
||||||
|
department_id INT NULL,
|
||||||
|
help_topic_id INT NULL,
|
||||||
|
priority_id INT NULL,
|
||||||
|
is_public TINYINT NOT NULL DEFAULT 1,
|
||||||
|
is_active TINYINT NOT NULL DEFAULT 1,
|
||||||
|
sort_order INT NOT NULL DEFAULT 0,
|
||||||
|
created_at DATETIME NOT NULL
|
||||||
|
)$eng");
|
||||||
|
$defaults = ['csat_enabled' => '0', 'csat_delay_minutes' => '30', 'status_page_enabled' => '0', 'status_page_title' => 'Systemstatus'];
|
||||||
|
$chk = $pdo->prepare('SELECT COUNT(*) FROM settings WHERE setting_key = ?');
|
||||||
|
$ins = $pdo->prepare('INSERT INTO settings (setting_key, setting_value) VALUES (?,?)');
|
||||||
|
foreach ($defaults as $k => $v) {
|
||||||
|
$chk->execute([$k]);
|
||||||
|
if ((int) $chk->fetchColumn() === 0) $ins->execute([$k, $v]);
|
||||||
|
}
|
||||||
|
$pdo->prepare('INSERT INTO schema_migrations (migration) VALUES (?)')->execute(['php_0048_csat_status_templates']);
|
||||||
|
$result['applied']++;
|
||||||
|
if ($verbose) $result['log'][] = ' [OK] php_0048_csat_status_templates';
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
$result['errors'][] = 'php_0048_csat_status_templates: ' . $e->getMessage();
|
||||||
|
if ($verbose) $result['log'][] = ' [FAIL] php_0048_csat_status_templates - ' . $e->getMessage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Migration 0049: gesammelte, noch nicht übersetzte Texte (Sammelmodus der Mehrsprachigkeit).
|
||||||
|
if (!isset($done['php_0049_i18n_missing'])) {
|
||||||
|
try {
|
||||||
|
$drv = $pdo->getAttribute(PDO::ATTR_DRIVER_NAME);
|
||||||
|
$ai = $drv === 'sqlite' ? 'INTEGER PRIMARY KEY AUTOINCREMENT' : 'INT AUTO_INCREMENT PRIMARY KEY';
|
||||||
|
$eng = $drv === 'sqlite' ? '' : ' ENGINE=InnoDB DEFAULT CHARSET=utf8mb4';
|
||||||
|
$pdo->exec("CREATE TABLE IF NOT EXISTS i18n_missing (
|
||||||
|
id $ai,
|
||||||
|
text_hash CHAR(32) NOT NULL,
|
||||||
|
text VARCHAR(400) NOT NULL,
|
||||||
|
hits INT NOT NULL DEFAULT 1,
|
||||||
|
last_seen DATETIME NOT NULL
|
||||||
|
)$eng");
|
||||||
|
$pdo->exec('CREATE UNIQUE INDEX idx_i18n_missing_hash ON i18n_missing (text_hash)');
|
||||||
|
$chk = $pdo->prepare('SELECT COUNT(*) FROM settings WHERE setting_key = ?');
|
||||||
|
$ins = $pdo->prepare('INSERT INTO settings (setting_key, setting_value) VALUES (?,?)');
|
||||||
|
foreach (['default_language' => 'de', 'i18n_collect' => '0'] as $k => $v) {
|
||||||
|
$chk->execute([$k]);
|
||||||
|
if ((int) $chk->fetchColumn() === 0) $ins->execute([$k, $v]);
|
||||||
|
}
|
||||||
|
$pdo->prepare('INSERT INTO schema_migrations (migration) VALUES (?)')->execute(['php_0049_i18n_missing']);
|
||||||
|
$result['applied']++;
|
||||||
|
if ($verbose) $result['log'][] = ' [OK] php_0049_i18n_missing';
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
$result['errors'][] = 'php_0049_i18n_missing: ' . $e->getMessage();
|
||||||
|
if ($verbose) $result['log'][] = ' [FAIL] php_0049_i18n_missing - ' . $e->getMessage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return $result;
|
return $result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 3.8 MiB After Width: | Height: | Size: 1.9 MiB |
Reference in New Issue
Block a user