Upload via GUI (74 Dateien)

This commit is contained in:
2026-11-05 21:22:50 +00:00
parent c0b4092498
commit dee5c631b9
10 changed files with 299 additions and 282 deletions
@@ -5,7 +5,7 @@
},
"projects": {
"C:\\Users\\Viper\\Desktop\\deskhub\\desktop\\installer\\DeskHubSetup.csproj": {
"version": "2.12.2",
"version": "0.0.0",
"restore": {
"projectUniqueName": "C:\\Users\\Viper\\Desktop\\deskhub\\desktop\\installer\\DeskHubSetup.csproj",
"projectName": "DeskHub-Setup",
+1 -1
View File
@@ -418,7 +418,7 @@
"C:\\Users\\Viper\\.nuget\\packages\\": {}
},
"project": {
"version": "2.12.2",
"version": "0.0.0",
"restore": {
"projectUniqueName": "C:\\Users\\Viper\\Desktop\\deskhub\\desktop\\installer\\DeskHubSetup.csproj",
"projectName": "DeskHub-Setup",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"version": 2,
"dgSpecHash": "Xvw5MCNB0oU=",
"dgSpecHash": "+dcsa7CcM5g=",
"success": true,
"projectFilePath": "C:\\Users\\Viper\\Desktop\\deskhub\\desktop\\installer\\DeskHubSetup.csproj",
"expectedPackageFiles": [
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "deskhub",
"productName": "DeskHub",
"version": "2.12.2",
"version": "2.12.3",
"description": "DeskHub Desktop-Client für Mitarbeiter",
"main": "src/main.js",
"author": "M_Viper",
+4 -2
View File
@@ -55,10 +55,12 @@ async function adminRoles(body, addBtn) {
function edit(x, r) {
const name = h('input', { value: x?.name || '' }), notes = h('textarea', { value: x?.notes || '' });
const active = h('input', { type: 'checkbox', checked: x ? x.isActive : true });
const perms = (x?.permissions) || r.defaultAgentPermissions || {};
const perms = (x?.effectivePermissions) || (x?.permissions) || r.defaultAgentPermissions || {};
const boxes = r.permissionKeys.map((k) => ({ k, cb: h('input', { type: 'checkbox', checked: !!perms[k] }) }));
const byKey = Object.fromEntries(boxes.map((b) => [b.k, b]));
const groups = (r.permissionGroups && r.permissionGroups.length ? r.permissionGroups : [{ title: 'Rechte', keys: r.permissionKeys }]);
modal(x ? 'Rolle bearbeiten' : 'Neue Rolle', h('div', {}, h('label', {}, 'Name'), name, h('label', { class: 'check' }, active, 'Aktiv (deaktivierte Rolle wird ignoriert)'), h('label', {}, 'Administratornotizen (intern)'), notes, h('label', {}, 'Rechte'),
h('div', { class: 'checks' }, ...boxes.map(({ k, cb }) => h('label', { class: 'check' }, cb, PERM_LABELS[k] || k)))),
...groups.map((g) => h('div', {}, h('h4', {}, g.title), h('div', { class: 'checks' }, ...g.keys.filter((k) => byKey[k]).map((k) => h('label', { class: 'check' }, byKey[k].cb, r.permissionLabels?.[k] || PERM_LABELS[k] || k)))))),
[{ label: 'Speichern', fn: async (close) => { if (await run(() => post('roles.php', { action: 'save', id: x?.id ?? null, name: name.value, isActive: active.checked, notes: notes.value || null, permissions: Object.fromEntries(boxes.map(({ k, cb }) => [k, cb.checked])) }), 'Gespeichert')) { close(); load(); } } }]);
}
load();
+7 -7
View File
@@ -224,7 +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')),
can('fetch_mail') ? 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')) : null,
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(),
@@ -409,7 +409,7 @@ async function vTickets(main, preset) {
};
const saveView = () => {
const name = h('input', { placeholder: 'z. B. Meine überfälligen' }), shared = h('input', { type: 'checkbox' });
modal('Ansicht speichern', h('div', {}, h('p', { class: 'muted' }, 'Speichert die aktuellen Filter (Reiter, Suche, Abteilung, Priorität).'), h('label', {}, 'Name'), name, h('label', { class: 'check' }, shared, 'Für alle Mitarbeiter sichtbar')),
modal('Ansicht speichern', h('div', {}, h('p', { class: 'muted' }, 'Speichert die aktuellen Filter (Reiter, Suche, Abteilung, Priorität).'), h('label', {}, 'Name'), name, can('share_views') ? h('label', { class: 'check' }, shared, 'Für alle Mitarbeiter sichtbar') : null),
[{ label: 'Speichern', fn: async (close) => {
const { page, ...filters } = filt();
if (await run(() => post('queues.php', { action: 'save', name: name.value, isShared: shared.checked, filters }), 'Ansicht gespeichert')) { close(); paintViews(); }
@@ -843,7 +843,7 @@ async function vReports(main) {
h('div', { class: 'checks wide' }, ...metrics),
h('div', { class: 'row top' }, h('button', { class: 'btn', onclick: () => run_() }, 'Bericht erstellen'),
h('button', { class: 'btn ghost', onclick: () => { const name = h('input', { placeholder: 'z. B. Monatsbericht Support' }), shared = h('input', { type: 'checkbox' });
modal('Bericht speichern', h('div', {}, h('label', {}, 'Name'), name, h('label', { class: 'check' }, shared, 'Für alle Mitarbeiter sichtbar')), [{ label: 'Speichern', fn: async (close) => { if (await run(() => post('report_builder.php', { action: 'save', name: name.value, isShared: shared.checked, config: { ...cfg } }), 'Bericht gespeichert')) { close(); run_(); } } }]); } }, 'Bericht speichern')));
modal('Bericht speichern', h('div', {}, h('label', {}, 'Name'), name, can('share_views') ? h('label', { class: 'check' }, shared, 'Für alle Mitarbeiter sichtbar') : null), [{ label: 'Speichern', fn: async (close) => { if (await run(() => post('report_builder.php', { action: 'save', name: name.value, isShared: shared.checked, config: { ...cfg } }), 'Bericht gespeichert')) { close(); run_(); } } }]); } }, 'Bericht speichern')));
};
// Kundenzufriedenheit (Bewertungen der letzten 30 Tage), nur wenn die Abfrage eingeschaltet ist oder es schon Antworten gibt
const csatBox = h('div');
@@ -871,10 +871,10 @@ async function vAvailability(main) {
const note = h('input', { value: r.myNote, placeholder: 'Notiz (optional)' });
main.append(h('h2', {}, 'Verfügbarkeit'), h('div', { class: 'panel' }, h('label', {}, 'Mein Status'), sel, h('label', {}, 'Notiz'), note,
h('div', { class: 'row' }, h('button', { class: 'btn', onclick: () => run(() => post('availability.php', { status: sel.value, note: note.value }), 'Gespeichert') }, 'Speichern'))),
h('h3', {}, isAdmin() ? 'Team (als Administrator kannst du den Status ändern)' : 'Team'),
h('h3', {}, can('manage_availability') ? 'Team (du kannst den Status ändern)' : 'Team'),
h('div', { class: 'panel' }, h('table', {}, ...r.team.map((m) => {
const cells = [h('td', {}, m.name), h('td', {}, m.isOnVacation ? '🏖 Urlaub' : (labels[m.status] || m.status) + (m.autoOffline ? ' (automatisch, inaktiv)' : '')), h('td', { class: 'muted' }, m.note)];
if (isAdmin()) {
if (can('manage_availability')) {
const ms = h('select', {}, ...Object.entries(labels).map(([v, l]) => h('option', { value: v, selected: v === m.status }, l)));
const mn = h('input', { value: m.note || '', placeholder: 'Notiz', maxlength: '120' });
cells.push(h('td', {}, ms), h('td', {}, mn), h('td', {}, h('button', { class: 'btn sm', onclick: async () => { const ok = await run(() => post('availability.php', { status: ms.value, note: mn.value, staffId: m.id }), 'Status von ' + m.name + ' geändert'); if (ok) go('availability'); } }, 'Setzen')));
@@ -913,7 +913,7 @@ const ADMIN = [
['_h2', 'Zugang', H], ['departmentId', 'Hauptabteilung', 'departments?'], ['extraDepartments', 'Erweiterter Zugriff', XD],
['_h3', 'Berechtigungen', H], ['role', 'Kontotyp', [['agent', 'Mitarbeiter'], ['admin', 'Administrator (alle Rechte)']]], ['roleId', 'Rolle (für Mitarbeiter)', 'roles?'],
['_h4', 'Teams', H], ['teamIds', 'Mitglied in Teams', 'multi:teams']] },
{ key: 'banlist', label: 'Sperrliste', admin: true, file: 'banlist.php', cols: [['Typ', 'matchType'], ['Wert', 'value'], ['Grund', 'reason'], ['Von', 'addedByName']], saveAction: 'create', noEdit: true,
{ key: 'banlist', label: 'Sperrliste', perm: 'manage_banlist', file: 'banlist.php', cols: [['Typ', 'matchType'], ['Wert', 'value'], ['Grund', 'reason'], ['Von', 'addedByName']], saveAction: 'create', noEdit: true,
fields: [['matchType', 'Typ', [['email', 'E-Mail-Adresse'], ['domain', 'Domain']]], ['value', 'Wert', T], ['reason', 'Grund', T]] },
{ key: 'audit', label: 'Admin-Protokoll', perm: 'view_audit_log', file: 'audit_log.php', readonly: true, paged: true, cols: [['Zeit', (x) => fmtDate(x.createdAt)], ['Mitarbeiter', 'staffName'], ['Aktion', 'action']] },
{ key: 'time', label: 'Zeiterfassung', perm: 'view_time_entries', file: 'time_entries.php', readonly: true, list: 'entries', cols: [['Datum', (x) => fmtDate(x.loggedAt)], ['Mitarbeiter', 'staffName'], ['Ticket', 'ticketNumber'], ['Minuten', 'minutes'], ['Notiz', 'note']] },
@@ -937,7 +937,7 @@ async function vAdmin(main, sectionKey) {
if (!sectionKey) {
// Bereiche zu Gruppen ordnen (gleiche Gruppen wie im Adminportal und in der Android-App)
const entries = new Map(sections.map((s) => [s.key, { label: s.label, open: () => go('admin', s.key) }]));
if (isAdmin()) entries.set('status', { label: 'Status der Mitarbeiter', open: () => go('availability') });
if (can('manage_availability')) entries.set('status', { label: 'Status der Mitarbeiter', open: () => go('availability') });
const used = new Set();
const groupsView = ADMIN_GROUPS.map(([title, keys]) => {
const tiles = keys.filter((k) => entries.has(k)).map((k) => { used.add(k); const e = entries.get(k);
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -4,7 +4,7 @@
<body><div id="app"></div><div id="toast" class="toast hidden"></div>
<script>
// Test-Attrappe für die Electron-Brücke (window.deskhub) – nur für lokale UI-Tests ohne Server
const staff = { id: 1, name: 'Max Muster', email: 'max@example.de', role: 'agent', permissions: { view_reports: true, reply_ticket: true, close_ticket: true, post_note: true, assign_ticket: true, create_ticket: true, manage_teams: true, manage_roles: true, manage_business_hours: true } };
const staff = { id: 1, name: 'Max Muster', email: 'max@example.de', role: 'agent', permissions: { view_reports: true, reply_ticket: true, close_ticket: true, post_note: true, assign_ticket: true, create_ticket: true, manage_teams: true, manage_roles: true, manage_business_hours: true, fetch_mail: true, share_views: true, manage_banlist: true } };
const prio = [{ id: 1, name: 'Hoch', color: '#ef4444' }, { id: 2, name: 'Normal', color: '#3b82f6' }];
const statuses = [{ value: 'open', label: 'Offen', color: '#3b82f6' }, { value: 'answered', label: 'Beantwortet', color: '#22c55e' }, { value: 'closed', label: 'Geschlossen', color: '#64748b' }, { value: 'resolved', label: 'Gelöst', color: '#16a34a' }];
const tk = (n, s, st, over) => ({ id: n, number: 'T-' + (1000 + n), subject: s, requesterName: 'Kunde ' + n, requesterEmail: `k${n}@mail.de`, status: st, priority: prio[n % 2], department: { id: 1, name: 'Technik' }, assignedStaff: n % 3 ? { id: 1, name: 'Max Muster' } : null, isOverdue: !!over, dueLabel: over ? 'vor 2 Std (überfällig)' : 'in 5 Std', isSpam: false, createdAt: '2026-10-12 08:00:00', updatedAt: '2026-10-12 09:1' + n + ':00' });
@@ -57,7 +57,7 @@ const routes = {
'canned_responses.php': () => ({ items: [{ id: 1, title: 'Gruß', body: 'Hallo,<br>danke für Ihre Nachricht.', departmentId: null }, { id: 2, title: 'Nur Technik', body: 'Technik-Text', departmentId: 1 }, { id: 3, title: 'Nur Vertrieb', body: 'Vertrieb-Text', departmentId: 2 }] }),
'customer.php': () => ({ customer: { name: 'Kunde 1', email: 'k1@mail.de' }, tickets }),
'teams.php': () => ({ staffOptions: [{ id: 1, name: 'Max Muster' }, { id: 2, name: 'Eva Beispiel' }], items: [{ id: 1, name: 'Support', leadId: 1, leadName: 'Max Muster', notes: '', isActive: true, memberCount: 2, memberIds: [1, 2] }] }),
'roles.php': () => ({ permissionKeys: ['reply_ticket', 'close_ticket', 'manage_teams'], defaultAgentPermissions: { reply_ticket: true }, items: [{ id: 1, name: 'Teamleitung', permissions: { reply_ticket: true }, staffCount: 2 }] }),
'roles.php': () => ({ permissionKeys: ['reply_ticket', 'close_ticket', 'manage_teams', 'merge_split_ticket'], permissionLabels: { reply_ticket: 'Tickets beantworten', close_ticket: 'Tickets schließen/lösen', manage_teams: 'Teams verwalten', merge_split_ticket: 'Tickets zusammenführen und teilen' }, permissionGroups: [{ title: 'Tickets bearbeiten', keys: ['reply_ticket', 'close_ticket', 'merge_split_ticket'] }, { title: 'Team und Sicherheit', keys: ['manage_teams'] }], defaultAgentPermissions: { reply_ticket: true, merge_split_ticket: true }, items: [{ id: 1, name: 'Teamleitung', permissions: { reply_ticket: true }, effectivePermissions: { reply_ticket: true, close_ticket: false, manage_teams: false, merge_split_ticket: true }, staffCount: 2 }] }),
'business_hours.php': () => ({ enabled: true, hours: [1, 2, 3, 4, 5, 6, 0].map((w) => ({ weekday: w, isOpen: w > 0 && w < 6, openTime: '09:00', closeTime: '17:00' })), holidays: [{ id: 1, date: '2026-12-25', name: 'Weihnachten' }] }),
};
window.__calls = [];
+15
View File
@@ -55,6 +55,21 @@ app.whenReady().then(async () => {
await run(`[...document.querySelectorAll('.topbar button')].find(b => b.textContent.includes('E-Mails abrufen')).click()`);
check('Abruf von Hand sendet die Anfrage und zeigt das Ergebnis', await waitFor(`(window.__mailFetchPosts || []).length === 1 && document.body.innerText.includes('Abruf fertig: 2 E-Mail(s) verarbeitet.')`));
check('Knopf ist nach dem Abruf wieder bedienbar', await waitFor(`(() => { const b = [...document.querySelectorAll('.topbar button')].find(b => b.textContent.includes('E-Mails abrufen')); return !!b && !b.disabled; })()`));
// Neue Rechte (Server 1.0.85): Knöpfe und Bereiche erscheinen nur mit dem Recht
const withPerm = async (perm, on, fn) => { await run(`state.staff.permissions['${perm}'] = ${on}; renderShell(); go('tickets')`); await wait(400); return fn(); };
check('Ohne das Recht „E-Mails jetzt abrufen“ fehlt der Knopf', await withPerm('fetch_mail', false, () => run(`![...document.querySelectorAll('.topbar button')].find(b => b.textContent.includes('E-Mails abrufen'))`)));
check('Mit dem Recht ist der Knopf wieder da', await withPerm('fetch_mail', true, () => run(`!![...document.querySelectorAll('.topbar button')].find(b => b.textContent.includes('E-Mails abrufen'))`)));
await run(`state.staff.permissions.manage_banlist = false; state.staff.permissions.manage_teams = true; go('admin')`);
check('Sperrliste ist ohne Recht nicht in der Verwaltung', await waitFor(`document.getElementById('main').innerText.includes('Teams') && !document.getElementById('main').innerText.includes('Sperrliste')`));
await run(`state.staff.permissions.manage_banlist = true; go('admin')`);
check('Sperrliste erscheint mit dem Recht „Sperrliste“ (ohne Administrator zu sein)', await waitFor(`document.getElementById('main').innerText.includes('Sperrliste')`));
// Rollen-Editor: Gruppen und Beschriftungen vom Server, wirksame Rechte (Standardwerte) angekreuzt
await run(`go('admin', 'roles')`);
check('Rollen: Tabelle zeigt die Rolle', await waitFor(`document.getElementById('main').innerText.includes('Teamleitung')`));
await run(`[...document.querySelectorAll('#main tr.click')][0].click()`);
check('Rollen-Editor zeigt Gruppen und neue Rechte mit Beschriftung vom Server', await waitFor(`(() => { const t = document.body.innerText.toLowerCase(); return t.includes('tickets bearbeiten') && t.includes('team und sicherheit') && t.includes('tickets zusammenführen und teilen'); })()`));
check('Rechte, die für die Rolle als Standard gelten, sind angekreuzt (nicht „aus“)', await run(`(() => { const l = [...document.querySelectorAll('.modal label.check, .modal-box label.check, label.check')].find(x => x.textContent.includes('zusammenführen und teilen')); return !!l && l.querySelector('input').checked; })()`));
await run(`document.querySelector('.modal button.ghost, .modal .btn.ghost, .modal-box .btn.ghost')?.click()`);
await run(`window.__source = 'email'; go('ticket', 1)`);
check('E-Mail-Ticket: Knopf „Anhänge laden“ ist da', await waitFor(`!![...document.querySelectorAll('#main button')].find(b => b.textContent.includes('Anhänge laden'))`));
await run(`[...document.querySelectorAll('#main button')].find(b => b.textContent.includes('Anhänge laden')).click()`);