From 7e30cbf7fa5a42602940df7cf5bf72ae3c236c50 Mon Sep 17 00:00:00 2001 From: M_Viper Date: Thu, 22 Oct 2026 19:00:54 +0000 Subject: [PATCH] Upload via GUI (69 Dateien) --- desktop/package.json | 2 +- desktop/src/app.js | 3 +++ desktop/test/mock.html | 3 ++- desktop/test/run.js | 6 ++++++ 4 files changed, 12 insertions(+), 2 deletions(-) diff --git a/desktop/package.json b/desktop/package.json index 8325b0a8..998f6d72 100644 --- a/desktop/package.json +++ b/desktop/package.json @@ -1,7 +1,7 @@ { "name": "deskhub", "productName": "DeskHub", - "version": "2.3.0", + "version": "2.3.1", "description": "DeskHub Desktop-Client für Mitarbeiter", "main": "src/main.js", "author": "M_Viper", diff --git a/desktop/src/app.js b/desktop/src/app.js index cc92218b..8ef32a8c 100644 --- a/desktop/src/app.js +++ b/desktop/src/app.js @@ -522,6 +522,9 @@ async function vTicket(main, number) { h('h2', {}, t.subject), h('div', { class: 'sub' }, `${t.requesterName} · ${t.department.name} · erstellt ${fmtDate(t.createdAt)}`), actions), + // Administratoren dürfen ein von anderen bearbeitetes Ticket lesen und bearbeiten und die Sperre aufheben + t.lockedBy ? h('div', { class: 'warnbox row' }, h('span', { class: 'grow' }, `🔒 Dieses Ticket wird gerade von „${t.lockedBy}“ bearbeitet. Als Administrator kannst du es trotzdem lesen und bearbeiten.`), + t.canUnlock ? h('button', { class: 'btn sm', onclick: () => run(async () => { await post('ticket_unlock.php', { number }); reload(); }, 'Sperre aufgehoben') }, 'Sperre aufheben und übernehmen') : null) : null, t.lockedByOtherName ? h('div', { class: 'warnbox' }, `Dieser Chat wird gerade von „${t.lockedByOtherName}“ bearbeitet – nur Ansicht.`) : null, h('div', { class: 'split' }, h('div', {}, threadsEl, (can('reply_ticket') || can('post_note')) && !t.lockedByOtherName ? h('div', { class: 'panel composer' }, tabs, ta, sigBox, h('div', { class: 'row' }, canned, sendStatus, h('span', { class: 'grow' }), sendBtn)) : null), diff --git a/desktop/test/mock.html b/desktop/test/mock.html index 73fcaa4c..4fc5f7d9 100644 --- a/desktop/test/mock.html +++ b/desktop/test/mock.html @@ -14,10 +14,11 @@ const routes = { 'activity.php': () => ({ newReplies: 0, newUnassigned: 0, serverTime: '2026-10-12 10:00:00', health: window.__health || [] }), 'dashboard.php': () => ({ myOpen: 3, overdue: 2, unassigned: 1, unanswered: 4, resolvedToday: 5, slaPercent: 92.4, recent: tickets, departments: [{ name: 'Technik', open: 8, overdue: 2 }, { name: 'Vertrieb', open: 3, overdue: 0 }] }), 'tickets.php': () => ({ items: tickets, page: 1, perPage: 30, total: 4, hasMore: false }), - 'ticket.php': () => ({ ticket: { ...tickets[0], source: 'web', firstResponseDueAt: null, resolutionDueAt: '2026-10-12 12:00:00', lockedByOtherName: null, customFields: [{ label: 'Titel der Geschichte', type: 'text', value: 'Der kleine Bär' }, { label: 'Nutzerbedingungen', type: 'checkbox', value: 'Ja' }], replySignatures: { default: 'own', own: { available: true, text: 'Viele Grüße\nMax persönlich', html: '

Viele Grüße

Max persönlich
', hasImage: false }, department: { available: true, name: 'Technik', text: 'Technik-Team Support', html: '

Technik-Team Support

Linkalert(2)', hasImage: true } }, threads: [ + 'ticket.php': () => ({ ticket: { ...tickets[0], source: 'web', firstResponseDueAt: null, resolutionDueAt: '2026-10-12 12:00:00', lockedByOtherName: null, lockedBy: window.__lockedBy || null, canUnlock: !!window.__lockedBy, customFields: [{ label: 'Titel der Geschichte', type: 'text', value: 'Der kleine Bär' }, { label: 'Nutzerbedingungen', type: 'checkbox', value: 'Ja' }], replySignatures: { default: 'own', own: { available: true, text: 'Viele Grüße\nMax persönlich', html: '

Viele Grüße

Max persönlich
', hasImage: false }, department: { available: true, name: 'Technik', text: 'Technik-Team Support', html: '

Technik-Team Support

Linkalert(2)', hasImage: true } }, threads: [ { id: 1, authorType: 'user', authorName: 'Kunde 1', message: 'Mein Drucker druckt nichts mehr.\nBitte helft.', isInternalNote: false, createdAt: '2026-10-12 08:00:00', attachments: [{ id: 1, name: 'foto.png', size: 20480 }], comments: [] }, { id: 2, authorType: 'staff', authorName: 'Max Muster', message: 'Wir schauen uns das an.', isInternalNote: false, createdAt: '2026-10-12 08:30:00', attachments: [], comments: [] }, { id: 3, authorType: 'staff', authorName: 'Max Muster', message: 'Treiber prüfen', isInternalNote: true, createdAt: '2026-10-12 08:35:00', attachments: [], comments: [] }] } }), + 'ticket_unlock.php': () => { window.__lockedBy = null; return { ok: true, unlocked: true }; }, 'queues.php': () => ({ items: [{ id: 1, name: 'Meine überfälligen', isShared: false, mine: true, ownerName: 'Max Muster', filters: { status: 'open,pending', assigned: 'me', sla: 'overdue' } }, { id: 2, name: 'Team-Überblick', isShared: true, mine: false, ownerName: 'Eva Beispiel', filters: { status: 'open,pending,answered' } }] }), 'report_builder.php': () => ({ config: { dimension: 'department', metrics: ['total', 'open', 'resolved', 'sla_resolve'], from: '2026-09-15', to: '2026-10-15', sort: 'label', desc: false, filters: {} }, columns: [{ key: 'label', label: 'Abteilung', unit: '' }, { key: 'total', label: 'Tickets gesamt', unit: '' }, { key: 'open', label: 'Offen', unit: '' }, { key: 'resolved', label: 'Gelöst/Geschlossen', unit: '' }, { key: 'sla_resolve', label: 'SLA Lösung', unit: '%' }], diff --git a/desktop/test/run.js b/desktop/test/run.js index fb8e431a..cb323071 100644 --- a/desktop/test/run.js +++ b/desktop/test/run.js @@ -59,6 +59,12 @@ app.whenReady().then(async () => { check('„Keine Signatur“ wird in der Vorschau erklärt', await run(`document.querySelector('.composer .sigpreview').textContent.includes('keine Signatur')`)); check('Signatur-Auswahl erscheint nur bei Antworten, nicht bei internen Notizen', await run(`(() => { const tab = [...document.querySelectorAll('.composer .tab')].find((x) => x.textContent.includes('Notiz')); if (!tab) return true; tab.click(); const hidden = document.querySelector('.composer .sigbox').classList.contains('hidden'); [...document.querySelectorAll('.composer .tab')].find((x) => x.textContent.includes('Antwort')).click(); return hidden; })()`)); + // Administrator öffnet ein von anderen bearbeitetes Ticket: Hinweis mit Knopf zum Aufheben der Sperre + await run(`window.__lockedBy = 'Florian'; go('ticket', 1)`); + check('Gesperrtes Ticket: Admin sieht den Inhalt und den Hinweis mit dem Namen', await waitFor(`document.getElementById('main').innerText.includes('Drucker druckt nicht') && document.getElementById('main').innerText.includes('„Florian“ bearbeitet')`)); + check('Gesperrtes Ticket: Knopf „Sperre aufheben und übernehmen“ vorhanden', await run(`!![...document.querySelectorAll('#main button')].find((b) => b.innerText.includes('Sperre aufheben'))`)); + await run(`[...document.querySelectorAll('#main button')].find((b) => b.innerText.includes('Sperre aufheben')).click()`); + check('Sperre aufheben ruft den Server auf und der Hinweis verschwindet', await waitFor(`window.__calls.includes('ticket_unlock.php') && !document.getElementById('main').innerText.includes('„Florian“ bearbeitet')`)); await run(`go('reports')`); check('Berichte laden Tabelle mit Summen', await waitFor(`['Technik', 'Vertrieb', '59'].every(x => document.getElementById('main').innerText.includes(x))`));