Upload via GUI (74 Dateien)
This commit is contained in:
+49
-7
@@ -40,6 +40,8 @@ async function api(path, opts = {}) {
|
||||
e.code = r.error.code; e.status = r.error.status; e.offline = down; throw e;
|
||||
}
|
||||
if (state.offline) setOffline(false);
|
||||
// Änderungen an Tickets (antworten, Status, Zuweisung): Zähler bald neu holen, damit er sofort stimmt
|
||||
if (/^(ticket_message|ticket_update|ticket_spam|ticket_collaborators)\.php/.test(path) && opts.method === 'POST' && typeof pollSoon === 'function') pollSoon();
|
||||
return r.data;
|
||||
}
|
||||
|
||||
@@ -96,6 +98,16 @@ async function installUpdate() {
|
||||
if (r.error) { state.update = { ...state.update, busy: false }; paintBanners(); toast(r.error.message, true); }
|
||||
}
|
||||
window.deskhub.onUpdateProgress((pct) => { if (state.update?.busy) { state.update.pct = pct; paintBanners(); } });
|
||||
/** E-Mail-Abruf von Hand (wie der Knopf in der Ticketliste der Weboberfläche). */
|
||||
async function fetchMailNow(btn) {
|
||||
const old = btn.textContent;
|
||||
btn.disabled = true; btn.textContent = tr('⏳ Abruf läuft …');
|
||||
try {
|
||||
const r = await post('mail_fetch.php', {});
|
||||
toast(r.message, r.level === 'error');
|
||||
} catch (e) { toast(e.message, true); }
|
||||
finally { btn.disabled = false; btn.textContent = old; }
|
||||
}
|
||||
async function run(fn, okMsg) {
|
||||
try { const r = await fn(); if (okMsg) toast(okMsg); return r; }
|
||||
catch (e) { toast(e.message, true); return undefined; }
|
||||
@@ -172,7 +184,7 @@ const metaKey = (url) => 'meta:' + url;
|
||||
async function startApp(staff, cachedMeta, serverUrl) {
|
||||
state.staff = staff;
|
||||
checkUpdate(); clearInterval(state.updateTimer); state.updateTimer = setInterval(checkUpdate, 6 * 3600 * 1000);
|
||||
state.since = ''; state.badge = 0;
|
||||
state.since = ''; state.badge = 0; state.knownIds = null;
|
||||
if (!state.prefs) state.prefs = await window.deskhub.settings();
|
||||
applyTheme();
|
||||
const fresh = (async () => {
|
||||
@@ -212,6 +224,7 @@ function renderShell() {
|
||||
const top = h('header', { class: 'topbar' },
|
||||
h('div', { class: 'brand' }, h('span', { class: 'logo' }, 'D'), 'DeskHub'),
|
||||
can('create_ticket') ? h('button', { class: 'btn ok', onclick: () => go('newticket') }, 'Neues Ticket') : null,
|
||||
h('button', { class: 'btn ghost sm', title: tr('Ruft neue E-Mails aus den Postfächern ab'), onclick: (e) => fetchMailNow(e.currentTarget) }, tr('📥 E-Mails abrufen')),
|
||||
h('button', { class: 'searchbtn', onclick: openPalette }, h('span', { class: 'nl' }, icon('search'), 'Ticket, Kunde oder Bereich suchen…'), h('kbd', {}, 'Strg K')),
|
||||
h('div', { class: 'spacer' }),
|
||||
window.I18N.switcher(),
|
||||
@@ -244,15 +257,16 @@ function go(page, arg) {
|
||||
main.scrollTop = 0;
|
||||
const view = VIEWS_BY_PAGE()[page];
|
||||
if (!view) return;
|
||||
Promise.resolve(view(main, arg)).catch((e) => main.append(h('div', { class: 'warnbox' }, e.message)));
|
||||
Promise.resolve(view(main, arg)).then(() => { if (page === 'ticket') pollSoon(); }).catch((e) => main.append(h('div', { class: 'warnbox' }, e.message)));
|
||||
}
|
||||
window.addEventListener('focus', () => { if (state.staff) pollActivity(); }); // beim Zurückkehren zum Fenster sofort den aktuellen Stand holen
|
||||
function every(ms, fn) { const id = setInterval(fn, ms); state.timers.push({ id, page: true }); }
|
||||
|
||||
async function doLogout() {
|
||||
clearTimeout(state.pollTimer);
|
||||
await window.deskhub.logout();
|
||||
state.timers.forEach((t) => clearInterval(t.id ?? t)); state.timers = []; state.staff = null;
|
||||
window.deskhub.badge(0, null);
|
||||
window.deskhub.badge(0, null); state.knownIds = null;
|
||||
showLogin();
|
||||
}
|
||||
|
||||
@@ -270,7 +284,8 @@ function paintBadge() {
|
||||
if (b) { b.textContent = state.badge; b.classList.toggle('hidden', !state.badge); }
|
||||
window.deskhub.badge(state.badge, state.badge ? badgeImage(state.badge) : null);
|
||||
}
|
||||
function resetBadge() { state.badge = 0; paintBadge(); }
|
||||
// Der Zähler folgt dem Stand auf dem Server (siehe pollActivity) und wird nicht mehr beim Öffnen der Ticketliste geleert; nach einer Aktion fragen wir bald neu ab
|
||||
function pollSoon() { if (state.staff) schedulePoll(1200); }
|
||||
|
||||
function schedulePoll(delay) {
|
||||
clearTimeout(state.pollTimer);
|
||||
@@ -278,10 +293,28 @@ function schedulePoll(delay) {
|
||||
state.pollTimer = setTimeout(async () => { await pollActivity(); if (state.staff) schedulePoll(seconds * 1000); }, delay ?? seconds * 1000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Der Zähler zeigt, wie viele Tickets jetzt Aufmerksamkeit brauchen (offen, dir zugewiesen oder noch niemandem, seit der letzten
|
||||
* Kundennachricht nicht geöffnet): Er sinkt von selbst, wenn ein Ticket geöffnet, beantwortet oder übernommen wird, auch anderswo
|
||||
* (Weboberfläche, Chrome-Erweiterung). Benachrichtigt wird nur für Tickets, die neu dazukommen. Ältere Server ohne „attention“ liefern nur
|
||||
* „seit dem letzten Mal“: Dafür bleibt die frühere, aufsummierende Zählung.
|
||||
*/
|
||||
async function pollActivity() {
|
||||
try {
|
||||
const a = await api('activity.php', { query: { since: state.since } });
|
||||
if (state.since && (a.newReplies || a.newUnassigned)) {
|
||||
if (a.attention && Array.isArray(a.attention.unassigned) && Array.isArray(a.attention.mine)) {
|
||||
const known = state.knownIds;
|
||||
const freshU = known ? a.attention.unassigned.filter((id) => !known.has(id)).length : 0;
|
||||
const freshM = known ? a.attention.mine.filter((id) => !known.has(id)).length : 0;
|
||||
state.knownIds = new Set([...a.attention.unassigned, ...a.attention.mine]);
|
||||
if (state.badge !== state.knownIds.size) { state.badge = state.knownIds.size; paintBadge(); }
|
||||
if ((freshU || freshM) && state.prefs?.notifications !== false) {
|
||||
const parts = [];
|
||||
if (freshM) parts.push(`${freshM} Aktualisierung(en) an deinen Tickets`);
|
||||
if (freshU) parts.push(`${freshU} neue(s) nicht zugewiesene(s) Ticket(s)`);
|
||||
window.deskhub.notify('DeskHub', parts.map((p) => tr(p)).join(', '));
|
||||
}
|
||||
} else if (state.since && (a.newReplies || a.newUnassigned)) {
|
||||
state.badge += a.newReplies + a.newUnassigned;
|
||||
if (state.prefs?.notifications !== false) {
|
||||
const parts = [];
|
||||
@@ -346,7 +379,7 @@ const VIEWS = {
|
||||
overdue: ['Überfällig', { status: 'open,pending,answered,on_hold', assigned: '', sla: 'overdue' }],
|
||||
atrisk: ['Bald fällig', { status: 'open,pending,answered,on_hold', assigned: '', sla: 'at_risk' }],
|
||||
paused: ['Pausiert', { status: 'on_hold', assigned: '', sla: '' }],
|
||||
snoozed: ['💤 Wiedervorlage', { status: 'open,pending,answered,on_hold,resolved,closed', assigned: '', sla: '', snoozed: '1' }],
|
||||
snoozed: ['Wiedervorlage', { status: 'open,pending,answered,on_hold,resolved,closed', assigned: '', sla: '', snoozed: '1' }],
|
||||
following: ['👁 Beobachtet', { status: 'open,pending,answered,on_hold', assigned: '', sla: '', following: '1' }],
|
||||
closed: ['Erledigt', { status: 'resolved,closed', assigned: '', sla: '' }],
|
||||
all: ['Alle', { status: 'open,pending,answered,on_hold,resolved,closed', assigned: '', sla: '' }],
|
||||
@@ -430,7 +463,7 @@ async function vTickets(main, preset) {
|
||||
tabs, viewsBar, h('div', { class: 'toolbar' }, search, h('button', { class: 'btn', onclick: () => { f.q = search.value; f.page = 1; load(); } }, 'Suchen'),
|
||||
sel('departmentId', [['', 'Alle Abteilungen'], ...state.meta.departments.map((d) => [d.id, d.name])]), sel('priorityId', [['', 'Alle Prioritäten'], ...state.meta.priorities.map((p) => [p.id, p.name])])),
|
||||
bulk, list);
|
||||
paintTabs(); paintViews(); resetBadge();
|
||||
paintTabs(); paintViews();
|
||||
await load();
|
||||
every(30000, () => { if (!selected.size && !document.querySelector('.modal-bg')) load(true); });
|
||||
}
|
||||
@@ -569,6 +602,14 @@ async function vTicket(main, number) {
|
||||
}));
|
||||
|
||||
const spamBtn = can('close_ticket') ? h('button', { class: 'btn ghost sm', onclick: () => run(async () => { await post('ticket_spam.php', { number, action: t.isSpam ? 'unspam' : 'spam' }); reload(); }, t.isSpam ? 'Kein Spam' : 'Als Spam markiert') }, t.isSpam ? 'Kein Spam' : 'Spam') : null;
|
||||
// Anhänge, Bilder und Formatierung der Original-E-Mail aus dem Postfach nachladen (nur bei E-Mail-Tickets)
|
||||
const reloadMailBtn = t.source === 'email' && can('reply_ticket') ? h('button', { class: 'btn ghost sm', title: tr('Holt Anhänge, Bilder und die Formatierung der E-Mail aus dem Postfach, soweit sie beim Abruf nicht übernommen wurden'), onclick: () => run(async () => {
|
||||
const r = await post('ticket_mail_reload.php', { number });
|
||||
const n = (r.added || 0) + (r.inlineImages || 0);
|
||||
toast(n > 0 || r.formatting ? tr('Anhänge und Bilder aus der E-Mail nachgeladen.') : tr('Es gab nichts Neues zu laden.'));
|
||||
if (r.failed?.length) toast(r.failed.join(' | '), true);
|
||||
reload();
|
||||
}) }, tr('📎 Anhänge laden')) : null;
|
||||
const others = h('div', { class: 'muted' }, 'Lade…');
|
||||
if (t.requesterEmail) api('customer.php', { query: { email: t.requesterEmail } }).then((r) => {
|
||||
const rest = r.tickets.filter((x) => x.number !== number).slice(0, 6);
|
||||
@@ -586,6 +627,7 @@ async function vTicket(main, number) {
|
||||
h('button', { class: 'btn ghost sm', onclick: () => update({ following: !t.following }, t.following ? 'Du beobachtest das Ticket nicht mehr' : 'Du beobachtest das Ticket jetzt') }, t.following ? 'Nicht mehr beobachten' : '👁 Beobachten'),
|
||||
h('button', { class: 'btn ghost sm', title: 'Im Browser öffnen: zusammenführen, aufteilen, drucken', onclick: () => openWebPage('/staff/ticket_view.php?n=' + encodeURIComponent(t.number)) }, '🌐 Im Browser'),
|
||||
spamBtn,
|
||||
reloadMailBtn,
|
||||
h('button', { class: 'btn ghost sm', title: 'Ticketnummer kopieren', onclick: () => { navigator.clipboard?.writeText(t.number); toast('Nummer kopiert'); } }, 'Nummer kopieren'),
|
||||
h('button', { class: 'btn ghost sm', onclick: reload }, 'Aktualisieren'));
|
||||
const st = statusInfo(t.status);
|
||||
|
||||
Reference in New Issue
Block a user