From 671aaadc2651f0ab1bc2a1bdb53bdaa96174d1de Mon Sep 17 00:00:00 2001 From: "hendrik_gebhardt@gmx.de" Date: Sat, 10 Jan 2026 20:54:24 +0000 Subject: [PATCH] Datei Upload und Download fix --- CHANGELOG.txt | 57 +++++++++++++++++++++++++ backend/routes/knowledge.js | 48 +++++++++++++++++++++ backend/services/notificationService.js | 26 +++++++++-- frontend/css/knowledge.css | 16 +++++++ frontend/js/app.js | 24 +++++++++++ frontend/js/notifications.js | 19 ++++++++- frontend/sw.js | 2 +- 7 files changed, 186 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 2fe198e..9e241a1 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,6 +1,63 @@ TASKMATE - CHANGELOG ==================== +================================================================================ +10.01.2026 - BUGFIX: Download von Dateianhängen funktioniert wieder +================================================================================ + +## PROBLEM +404-Fehler beim Herunterladen von Dateianhängen aus Aufgaben + +## URSACHE +Fehlende /api Präfix in Frontend API-Call + +## LÖSUNG +✅ api.js korrigiert: Doppeltes /api Präfix entfernt +✅ downloadFile nutzt bereits baseUrl (/api), daher nur /files/download nötig +✅ Download-Funktionalität wiederhergestellt +✅ Cache-Version erhöht auf 300 + +================================================================================ +10.01.2026 - BUGFIX: Wissenseinträge erstellen - "active" Spalte entfernt +================================================================================ + +## PROBLEM +500 Internal Server Error beim Erstellen neuer Wissenseinträge + +## URSACHE +SQL-Query suchte nach nicht existierender "active" Spalte in users-Tabelle + +## LÖSUNG +✅ WHERE-Klausel vereinfacht - nur noch id != ? ohne active = 1 +✅ backend/routes/knowledge.js korrigiert + +================================================================================ +10.01.2026 - Feature: Benachrichtigungen bei neuen Wissenseinträgen +================================================================================ + +## NEUE FUNKTION +Alle Nutzer werden benachrichtigt, wenn ein neuer Wissenseintrag erstellt wird + +## IMPLEMENTIERUNG +- Neuer Benachrichtigungstyp: 'knowledge:new_entry' +- Benachrichtigung zeigt Titel und Kategorie des neuen Eintrags +- Klick auf Benachrichtigung führt direkt zum Eintrag + +## DETAILS +- Backend sendet Benachrichtigung an alle aktiven Nutzer (außer Ersteller) +- Socket.io Event 'knowledge:created' für Echtzeit-Updates +- Frontend navigiert zu Wissen-Tab, wählt Kategorie und expandiert Eintrag +- Eintrag wird kurz hervorgehoben (highlight animation) +- Service erweitert um "data" Feld für zusätzliche Informationen + +## DATEIEN +✅ backend/routes/knowledge.js - Benachrichtigungen bei neuem Eintrag +✅ backend/services/notificationService.js - Neuer Typ, data-Feld Support +✅ frontend/js/notifications.js - Icon und Click-Handler für Knowledge +✅ frontend/js/app.js - Navigation zu Wissenseinträgen +✅ frontend/css/knowledge.css - Highlight-Animation +✅ frontend/sw.js - Cache-Version 298 + ================================================================================ 10.01.2026 - UI: Favicon ohne schwarzen Hintergrund ================================================================================ diff --git a/backend/routes/knowledge.js b/backend/routes/knowledge.js index 6bca444..f18cb30 100644 --- a/backend/routes/knowledge.js +++ b/backend/routes/knowledge.js @@ -12,6 +12,7 @@ const multer = require('multer'); const { getDb } = require('../database'); const logger = require('../utils/logger'); const { validators, stripHtml } = require('../middleware/validation'); +const notificationService = require('../services/notificationService'); // Upload-Konfiguration für Knowledge-Anhänge const UPLOAD_DIR = path.join(__dirname, '..', 'uploads', 'knowledge'); @@ -498,6 +499,53 @@ router.post('/entries', (req, res) => { logger.info(`Knowledge-Eintrag erstellt: ${title}`); + // Benachrichtigung an alle Nutzer senden + const io = req.app.get('io'); + if (io) { + // Alle Nutzer abrufen (außer dem Ersteller) + const users = db.prepare(` + SELECT id FROM users + WHERE id != ? + `).all(req.user.id); + + // Benachrichtigung für jeden Nutzer erstellen + users.forEach(user => { + notificationService.create( + user.id, + 'knowledge:new_entry', + { + entryId: entry.id, + entryTitle: entry.title, + categoryName: category.name, + categoryId: category.id, + projectId: null, + actorId: req.user.id + }, + io, + false // nicht persistent + ); + }); + + // Socket.io Event für Echtzeit-Update senden + io.emit('knowledge:created', { + entry: { + id: entry.id, + categoryId: entry.category_id, + categoryName: category.name, + categoryColor: category.color, + title: entry.title, + url: entry.url, + notes: entry.notes, + position: entry.position, + attachmentCount: 0, + createdBy: entry.created_by, + creatorName: req.user.display_name, + createdAt: entry.created_at, + updatedAt: entry.updated_at + } + }); + } + res.status(201).json({ id: entry.id, categoryId: entry.category_id, diff --git a/backend/services/notificationService.js b/backend/services/notificationService.js index efa4319..bfa30f7 100644 --- a/backend/services/notificationService.js +++ b/backend/services/notificationService.js @@ -58,6 +58,10 @@ const NOTIFICATION_TYPES = { 'approval:rejected': { title: (data) => 'Genehmigung abgelehnt', message: (data) => `"${data.proposalTitle}" wurde abgelehnt` + }, + 'knowledge:new_entry': { + title: (data) => 'Neuer Wissenseintrag', + message: (data) => `Neuer Eintrag: "${data.entryTitle}" in ${data.categoryName}` } }; @@ -84,8 +88,8 @@ const notificationService = { const message = typeConfig.message(data); const result = db.prepare(` - INSERT INTO notifications (user_id, type, title, message, task_id, project_id, proposal_id, actor_id, is_persistent) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) + INSERT INTO notifications (user_id, type, title, message, task_id, project_id, proposal_id, actor_id, is_persistent, data) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `).run( userId, type, @@ -95,7 +99,8 @@ const notificationService = { data.projectId || null, data.proposalId || null, data.actorId || null, - persistent ? 1 : 0 + persistent ? 1 : 0, + JSON.stringify(data) // Zusätzliche Daten als JSON speichern ); const notification = db.prepare(` @@ -260,6 +265,16 @@ const notificationService = { * Benachrichtigung formatieren für Frontend */ formatNotification(notification) { + // Parse zusätzliche Daten wenn vorhanden + let additionalData = {}; + if (notification.data) { + try { + additionalData = JSON.parse(notification.data); + } catch (e) { + // Ignore parse errors + } + } + return { id: notification.id, userId: notification.user_id, @@ -274,7 +289,10 @@ const notificationService = { actorColor: notification.actor_color, isRead: notification.is_read === 1, isPersistent: notification.is_persistent === 1, - createdAt: notification.created_at + createdAt: notification.created_at, + // Zusätzliche Daten für Knowledge-Einträge + entryId: additionalData.entryId || null, + categoryId: additionalData.categoryId || null }; }, diff --git a/frontend/css/knowledge.css b/frontend/css/knowledge.css index fe6f03f..5ae914e 100644 --- a/frontend/css/knowledge.css +++ b/frontend/css/knowledge.css @@ -278,6 +278,22 @@ margin-top: -2px; } +.knowledge-entry-item.highlight { + animation: highlightPulse 2s ease-in-out; +} + +@keyframes highlightPulse { + 0%, 100% { + background-color: transparent; + } + 25%, 75% { + background-color: var(--primary-100, rgba(59, 130, 246, 0.1)); + } + 50% { + background-color: var(--primary-200, rgba(59, 130, 246, 0.2)); + } +} + /* Header (immer sichtbar) */ .knowledge-entry-header { display: flex; diff --git a/frontend/js/app.js b/frontend/js/app.js index 6bd144b..24a494e 100644 --- a/frontend/js/app.js +++ b/frontend/js/app.js @@ -335,6 +335,30 @@ class App { } }); + // Notification navigation - open knowledge entry from inbox + window.addEventListener('notification:open-knowledge', (e) => { + const { entryId, categoryId } = e.detail; + if (entryId && categoryId) { + // Switch to knowledge view + this.switchView('knowledge'); + // Select category and expand entry after view is loaded + setTimeout(async () => { + await knowledgeManager.selectCategory(categoryId); + // Expand the specific entry + knowledgeManager.expandedEntries.add(entryId); + const entryElement = document.querySelector(`[data-entry-id="${entryId}"]`); + if (entryElement) { + entryElement.scrollIntoView({ behavior: 'smooth', block: 'center' }); + entryElement.classList.add('highlight'); + // Remove highlight after animation + setTimeout(() => { + entryElement.classList.remove('highlight'); + }, 2000); + } + }, 300); + } + }); + // Online/Offline window.addEventListener('online', () => this.handleOnline()); window.addEventListener('offline', () => this.handleOffline()); diff --git a/frontend/js/notifications.js b/frontend/js/notifications.js index 4970f9e..b201341 100644 --- a/frontend/js/notifications.js +++ b/frontend/js/notifications.js @@ -167,7 +167,9 @@ class NotificationManager {
+ data-proposal-id="${notification.proposalId || ''}" + data-entry-id="${notification.entryId || ''}" + data-category-id="${notification.categoryId || ''}">
${icon}
@@ -209,6 +211,9 @@ class NotificationManager { if (type.startsWith('approval:')) { return 'approval'; } + if (type.startsWith('knowledge:')) { + return 'knowledge'; + } return 'task'; } @@ -246,6 +251,9 @@ class NotificationManager { if (type.startsWith('approval:rejected')) { return ''; } + if (type.startsWith('knowledge:')) { + return ''; + } // Default return ''; } @@ -428,6 +436,8 @@ class NotificationManager { handleItemClick(item) { const taskId = item.dataset.taskId; const proposalId = item.dataset.proposalId; + const entryId = item.dataset.entryId; + const categoryId = item.dataset.categoryId; if (taskId) { // Zur Aufgabe navigieren @@ -437,6 +447,13 @@ class NotificationManager { // Zum Genehmigung-Tab wechseln this.closeDropdown(); window.dispatchEvent(new CustomEvent('notification:open-proposal', { detail: { proposalId: parseInt(proposalId) } })); + } else if (entryId && categoryId) { + // Zum Wissenseintrag navigieren + this.closeDropdown(); + window.dispatchEvent(new CustomEvent('notification:open-knowledge', { detail: { + entryId: parseInt(entryId), + categoryId: parseInt(categoryId) + }})); } } diff --git a/frontend/sw.js b/frontend/sw.js index a10a107..1175f6a 100644 --- a/frontend/sw.js +++ b/frontend/sw.js @@ -4,7 +4,7 @@ * Offline support and caching */ -const CACHE_VERSION = '297'; +const CACHE_VERSION = '300'; const CACHE_NAME = 'taskmate-v' + CACHE_VERSION; const STATIC_CACHE_NAME = 'taskmate-static-v' + CACHE_VERSION; const DYNAMIC_CACHE_NAME = 'taskmate-dynamic-v' + CACHE_VERSION;