Datei Upload und Download fix
Dieser Commit ist enthalten in:
committet von
Server Deploy
Ursprung
5b1f8b1cfe
Commit
671aaadc26
@ -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
|
||||
================================================================================
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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());
|
||||
|
||||
@ -167,7 +167,9 @@ class NotificationManager {
|
||||
<div class="notification-item ${notification.isRead ? '' : 'unread'} ${notification.isPersistent ? 'persistent' : ''}"
|
||||
data-id="${notification.id}"
|
||||
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}">
|
||||
${icon}
|
||||
</div>
|
||||
@ -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 '<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
|
||||
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) {
|
||||
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)
|
||||
}}));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren