Datei Upload und Download fix
Dieser Commit ist enthalten in:
committet von
Server Deploy
Ursprung
5b1f8b1cfe
Commit
671aaadc26
@ -1,6 +1,63 @@
|
|||||||
TASKMATE - CHANGELOG
|
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
|
10.01.2026 - UI: Favicon ohne schwarzen Hintergrund
|
||||||
================================================================================
|
================================================================================
|
||||||
|
|||||||
@ -12,6 +12,7 @@ const multer = require('multer');
|
|||||||
const { getDb } = require('../database');
|
const { getDb } = require('../database');
|
||||||
const logger = require('../utils/logger');
|
const logger = require('../utils/logger');
|
||||||
const { validators, stripHtml } = require('../middleware/validation');
|
const { validators, stripHtml } = require('../middleware/validation');
|
||||||
|
const notificationService = require('../services/notificationService');
|
||||||
|
|
||||||
// Upload-Konfiguration für Knowledge-Anhänge
|
// Upload-Konfiguration für Knowledge-Anhänge
|
||||||
const UPLOAD_DIR = path.join(__dirname, '..', 'uploads', 'knowledge');
|
const UPLOAD_DIR = path.join(__dirname, '..', 'uploads', 'knowledge');
|
||||||
@ -498,6 +499,53 @@ router.post('/entries', (req, res) => {
|
|||||||
|
|
||||||
logger.info(`Knowledge-Eintrag erstellt: ${title}`);
|
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({
|
res.status(201).json({
|
||||||
id: entry.id,
|
id: entry.id,
|
||||||
categoryId: entry.category_id,
|
categoryId: entry.category_id,
|
||||||
|
|||||||
@ -58,6 +58,10 @@ const NOTIFICATION_TYPES = {
|
|||||||
'approval:rejected': {
|
'approval:rejected': {
|
||||||
title: (data) => 'Genehmigung abgelehnt',
|
title: (data) => 'Genehmigung abgelehnt',
|
||||||
message: (data) => `"${data.proposalTitle}" wurde 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 message = typeConfig.message(data);
|
||||||
|
|
||||||
const result = db.prepare(`
|
const result = db.prepare(`
|
||||||
INSERT INTO notifications (user_id, type, title, message, task_id, project_id, proposal_id, actor_id, is_persistent)
|
INSERT INTO notifications (user_id, type, title, message, task_id, project_id, proposal_id, actor_id, is_persistent, data)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
`).run(
|
`).run(
|
||||||
userId,
|
userId,
|
||||||
type,
|
type,
|
||||||
@ -95,7 +99,8 @@ const notificationService = {
|
|||||||
data.projectId || null,
|
data.projectId || null,
|
||||||
data.proposalId || null,
|
data.proposalId || null,
|
||||||
data.actorId || null,
|
data.actorId || null,
|
||||||
persistent ? 1 : 0
|
persistent ? 1 : 0,
|
||||||
|
JSON.stringify(data) // Zusätzliche Daten als JSON speichern
|
||||||
);
|
);
|
||||||
|
|
||||||
const notification = db.prepare(`
|
const notification = db.prepare(`
|
||||||
@ -260,6 +265,16 @@ const notificationService = {
|
|||||||
* Benachrichtigung formatieren für Frontend
|
* Benachrichtigung formatieren für Frontend
|
||||||
*/
|
*/
|
||||||
formatNotification(notification) {
|
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 {
|
return {
|
||||||
id: notification.id,
|
id: notification.id,
|
||||||
userId: notification.user_id,
|
userId: notification.user_id,
|
||||||
@ -274,7 +289,10 @@ const notificationService = {
|
|||||||
actorColor: notification.actor_color,
|
actorColor: notification.actor_color,
|
||||||
isRead: notification.is_read === 1,
|
isRead: notification.is_read === 1,
|
||||||
isPersistent: notification.is_persistent === 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
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@ -278,6 +278,22 @@
|
|||||||
margin-top: -2px;
|
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) */
|
/* Header (immer sichtbar) */
|
||||||
.knowledge-entry-header {
|
.knowledge-entry-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@ -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
|
// Online/Offline
|
||||||
window.addEventListener('online', () => this.handleOnline());
|
window.addEventListener('online', () => this.handleOnline());
|
||||||
window.addEventListener('offline', () => this.handleOffline());
|
window.addEventListener('offline', () => this.handleOffline());
|
||||||
|
|||||||
@ -167,7 +167,9 @@ class NotificationManager {
|
|||||||
<div class="notification-item ${notification.isRead ? '' : 'unread'} ${notification.isPersistent ? 'persistent' : ''}"
|
<div class="notification-item ${notification.isRead ? '' : 'unread'} ${notification.isPersistent ? 'persistent' : ''}"
|
||||||
data-id="${notification.id}"
|
data-id="${notification.id}"
|
||||||
data-task-id="${notification.taskId || ''}"
|
data-task-id="${notification.taskId || ''}"
|
||||||
data-proposal-id="${notification.proposalId || ''}">
|
data-proposal-id="${notification.proposalId || ''}"
|
||||||
|
data-entry-id="${notification.entryId || ''}"
|
||||||
|
data-category-id="${notification.categoryId || ''}">
|
||||||
<div class="notification-type-icon ${iconClass}">
|
<div class="notification-type-icon ${iconClass}">
|
||||||
${icon}
|
${icon}
|
||||||
</div>
|
</div>
|
||||||
@ -209,6 +211,9 @@ class NotificationManager {
|
|||||||
if (type.startsWith('approval:')) {
|
if (type.startsWith('approval:')) {
|
||||||
return 'approval';
|
return 'approval';
|
||||||
}
|
}
|
||||||
|
if (type.startsWith('knowledge:')) {
|
||||||
|
return 'knowledge';
|
||||||
|
}
|
||||||
return 'task';
|
return 'task';
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -246,6 +251,9 @@ class NotificationManager {
|
|||||||
if (type.startsWith('approval:rejected')) {
|
if (type.startsWith('approval:rejected')) {
|
||||||
return '<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2" fill="none"/><path d="M15 9l-6 6M9 9l6 6" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>';
|
return '<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2" fill="none"/><path d="M15 9l-6 6M9 9l6 6" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>';
|
||||||
}
|
}
|
||||||
|
if (type.startsWith('knowledge:')) {
|
||||||
|
return '<svg viewBox="0 0 24 24"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z" stroke="currentColor" stroke-width="2" fill="none" stroke-linejoin="round"/></svg>';
|
||||||
|
}
|
||||||
// Default
|
// Default
|
||||||
return '<svg viewBox="0 0 24 24"><path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9" stroke="currentColor" stroke-width="2" fill="none"/></svg>';
|
return '<svg viewBox="0 0 24 24"><path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9" stroke="currentColor" stroke-width="2" fill="none"/></svg>';
|
||||||
}
|
}
|
||||||
@ -428,6 +436,8 @@ class NotificationManager {
|
|||||||
handleItemClick(item) {
|
handleItemClick(item) {
|
||||||
const taskId = item.dataset.taskId;
|
const taskId = item.dataset.taskId;
|
||||||
const proposalId = item.dataset.proposalId;
|
const proposalId = item.dataset.proposalId;
|
||||||
|
const entryId = item.dataset.entryId;
|
||||||
|
const categoryId = item.dataset.categoryId;
|
||||||
|
|
||||||
if (taskId) {
|
if (taskId) {
|
||||||
// Zur Aufgabe navigieren
|
// Zur Aufgabe navigieren
|
||||||
@ -437,6 +447,13 @@ class NotificationManager {
|
|||||||
// Zum Genehmigung-Tab wechseln
|
// Zum Genehmigung-Tab wechseln
|
||||||
this.closeDropdown();
|
this.closeDropdown();
|
||||||
window.dispatchEvent(new CustomEvent('notification:open-proposal', { detail: { proposalId: parseInt(proposalId) } }));
|
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)
|
||||||
|
}}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -4,7 +4,7 @@
|
|||||||
* Offline support and caching
|
* Offline support and caching
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const CACHE_VERSION = '297';
|
const CACHE_VERSION = '300';
|
||||||
const CACHE_NAME = 'taskmate-v' + CACHE_VERSION;
|
const CACHE_NAME = 'taskmate-v' + CACHE_VERSION;
|
||||||
const STATIC_CACHE_NAME = 'taskmate-static-v' + CACHE_VERSION;
|
const STATIC_CACHE_NAME = 'taskmate-static-v' + CACHE_VERSION;
|
||||||
const DYNAMIC_CACHE_NAME = 'taskmate-dynamic-v' + CACHE_VERSION;
|
const DYNAMIC_CACHE_NAME = 'taskmate-dynamic-v' + CACHE_VERSION;
|
||||||
|
|||||||
In neuem Issue referenzieren
Einen Benutzer sperren