Kunden-Dashboard: - Tab-Navigation: Projekte | Was wurde gemacht | Rechnungen | Chat - Projekt-Details pro Projekt: letzte Git-Commits (Titel+Beschreibung), Projektgroesse, Datei-Uebersicht (Top-Level aggregiert), Ticket-Arbeiten - Rechnungen: gestylte Liste mit Status-Pillen, Ansehen/Zahlen-Link, PDF-Download ueber Server-Proxy (IDOR-geschuetzt) - Chat mit dem Webklar-Team (Polling, Ungelesen-Badge, viewAs blockiert) Admin-Dashboard: - Chat-Tab: Konversationsliste + Thread + Antwortfeld, Ungelesen-Badge Backend: - giteaAdmin: getRepoInfo, listRepoCommits, getRepoTreeSummary - projectInsights: 5-min-Cache, Invalidierung per Gitea-Webhook - /api/projects/:id/git|files|work mit Ownership-Check (404) - /api/chat/* (Kunde) + /api/portal-admin/chats/* (Admin), portalMessages-Collection - mailer: E-Mail-Benachrichtigung an Admins bei Kundennachricht (15-min-Throttle) - gitWorksheet: dd.mm.yyyy, voller Commit-Body, startTime leer (Zeit-Nachtrag), Auto-Webpage-Ticket bei Push auf Projekt ohne Ticket (ensureProjectTicket) - customerActivity: Git-Push-Eintraege fuer Kunden sichtbar Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
124 lines
3.8 KiB
JavaScript
124 lines
3.8 KiB
JavaScript
import { Router } from 'express'
|
|
import { config } from '../config.js'
|
|
import { getSessionCustomerId, requireSession } from '../middleware/session.js'
|
|
import {
|
|
createDocument,
|
|
getDocument,
|
|
listDocuments,
|
|
updateDocument,
|
|
Query,
|
|
ID,
|
|
} from '../services/appwriteAdmin.js'
|
|
import { sendChatNotification } from '../services/mailer.js'
|
|
|
|
const router = Router()
|
|
|
|
export function sanitizeMessage(doc) {
|
|
return {
|
|
id: doc.$id,
|
|
sender: doc.sender || '',
|
|
senderName: doc.senderName || '',
|
|
text: doc.text || '',
|
|
createdAt: doc.createdAt || doc.$createdAt || '',
|
|
}
|
|
}
|
|
|
|
async function countUnreadForCustomer(customerId) {
|
|
const docs = await listDocuments(config.collections.portalMessages, [
|
|
Query.equal('customerId', customerId),
|
|
Query.equal('readByCustomer', false),
|
|
Query.equal('sender', 'admin'),
|
|
Query.limit(100),
|
|
]).catch(() => [])
|
|
return docs.length
|
|
}
|
|
|
|
/** Nachrichten des eingeloggten Kunden (optional nur neue seit ?since=ISO). */
|
|
router.get('/messages', requireSession, async (req, res) => {
|
|
const customerId = getSessionCustomerId(req)
|
|
try {
|
|
const queries = [
|
|
Query.equal('customerId', customerId),
|
|
Query.orderAsc('createdAt'),
|
|
Query.limit(200),
|
|
]
|
|
const since = String(req.query.since || '').trim()
|
|
if (since) queries.push(Query.greaterThan('createdAt', since))
|
|
|
|
const docs = await listDocuments(config.collections.portalMessages, queries)
|
|
const unread = await countUnreadForCustomer(customerId)
|
|
return res.json({ messages: docs.map(sanitizeMessage), unread })
|
|
} catch (err) {
|
|
return res.status(500).json({ error: err.message || 'Nachrichten konnten nicht geladen werden' })
|
|
}
|
|
})
|
|
|
|
/** Nachricht des Kunden an die Administratoren. */
|
|
router.post('/messages', requireSession, async (req, res) => {
|
|
if (req.session.viewAs) {
|
|
return res.status(403).json({ error: 'Im Ansichtsmodus kann nicht als Kunde geschrieben werden.' })
|
|
}
|
|
|
|
const text = String(req.body?.text || '').trim()
|
|
if (!text || text.length > 2000) {
|
|
return res.status(400).json({ error: 'Nachricht muss zwischen 1 und 2000 Zeichen lang sein.' })
|
|
}
|
|
|
|
const customerId = getSessionCustomerId(req)
|
|
try {
|
|
const doc = await createDocument(
|
|
config.collections.portalMessages,
|
|
{
|
|
customerId,
|
|
sender: 'customer',
|
|
senderName: req.session.name || '',
|
|
text,
|
|
readByAdmin: false,
|
|
readByCustomer: true,
|
|
createdAt: new Date().toISOString(),
|
|
},
|
|
ID.unique()
|
|
)
|
|
|
|
// Admins benachrichtigen (gedrosselt, Fehler nicht durchreichen)
|
|
getDocument(config.collections.customers, customerId)
|
|
.then((customer) => sendChatNotification(customer, text))
|
|
.catch(() => {})
|
|
|
|
return res.status(201).json({ message: sanitizeMessage(doc) })
|
|
} catch (err) {
|
|
return res.status(500).json({ error: err.message || 'Nachricht konnte nicht gesendet werden' })
|
|
}
|
|
})
|
|
|
|
/** Admin-Nachrichten als gelesen markieren. */
|
|
router.post('/read', requireSession, async (req, res) => {
|
|
const customerId = getSessionCustomerId(req)
|
|
try {
|
|
const docs = await listDocuments(config.collections.portalMessages, [
|
|
Query.equal('customerId', customerId),
|
|
Query.equal('readByCustomer', false),
|
|
Query.equal('sender', 'admin'),
|
|
Query.limit(100),
|
|
])
|
|
for (const doc of docs) {
|
|
await updateDocument(config.collections.portalMessages, doc.$id, { readByCustomer: true })
|
|
}
|
|
return res.json({ updated: docs.length })
|
|
} catch (err) {
|
|
return res.status(500).json({ error: err.message || 'Fehler beim Markieren' })
|
|
}
|
|
})
|
|
|
|
/** Billiger Badge-Poll. */
|
|
router.get('/unread-count', requireSession, async (req, res) => {
|
|
try {
|
|
const unread = await countUnreadForCustomer(getSessionCustomerId(req))
|
|
return res.json({ unread })
|
|
} catch {
|
|
return res.json({ unread: 0 })
|
|
}
|
|
})
|
|
|
|
export default router
|