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>
115 lines
4.2 KiB
JavaScript
115 lines
4.2 KiB
JavaScript
import { Router } from 'express'
|
|
import { config } from '../config.js'
|
|
import { getDocument, listDocuments, Query } from '../services/appwriteAdmin.js'
|
|
import { getSessionCustomerId, requireSession } from '../middleware/session.js'
|
|
import { getProjectGitInsights, getProjectFileOverview } from '../services/projectInsights.js'
|
|
|
|
const router = Router()
|
|
|
|
/**
|
|
* Laedt das Projekt und stellt sicher, dass es dem eingeloggten Kunden gehoert.
|
|
* Fremde/unbekannte IDs antworten einheitlich mit 404 (kein Enumeration-Leak).
|
|
*/
|
|
async function loadOwnProject(req, res, next) {
|
|
const customerId = getSessionCustomerId(req)
|
|
const project = await getDocument(config.collections.websiteProjects, req.params.id).catch(() => null)
|
|
if (!project || !customerId || project.customerId !== customerId) {
|
|
return res.status(404).json({ error: 'Projekt nicht gefunden' })
|
|
}
|
|
req.project = project
|
|
return next()
|
|
}
|
|
|
|
router.get('/', requireSession, async (req, res) => {
|
|
const customerId = getSessionCustomerId(req)
|
|
if (!customerId) {
|
|
return res.status(401).json({ error: 'Nicht angemeldet' })
|
|
}
|
|
|
|
try {
|
|
const projects = await listDocuments(config.collections.websiteProjects, [
|
|
Query.equal('customerId', customerId),
|
|
Query.orderDesc('$createdAt'),
|
|
])
|
|
|
|
const sanitized = projects.map((p) => ({
|
|
id: p.$id,
|
|
projectName: p.projectName || '',
|
|
subdomain: p.subdomain || '',
|
|
previewUrl: p.previewUrl || '',
|
|
liveDomain: p.liveDomain || '',
|
|
status: p.status || '',
|
|
provisioningStatus: p.provisioningStatus || '',
|
|
templateName: p.templateName || '',
|
|
giteaRepoUrl: p.giteaRepoUrl || '',
|
|
repoFullName: p.repoFullName || '',
|
|
}))
|
|
|
|
return res.json({ projects: sanitized })
|
|
} catch (err) {
|
|
return res.status(500).json({ error: err.message || 'Projekte konnten nicht geladen werden' })
|
|
}
|
|
})
|
|
|
|
/** Letzte Git-Pushes + Projektgroesse. */
|
|
router.get('/:id/git', requireSession, loadOwnProject, async (req, res) => {
|
|
try {
|
|
const { repo, commits } = await getProjectGitInsights(req.project)
|
|
return res.json({ repo, commits })
|
|
} catch (err) {
|
|
return res.status(500).json({ error: err.message || 'Git-Daten konnten nicht geladen werden' })
|
|
}
|
|
})
|
|
|
|
/** Datei-Uebersicht (Top-Level, Groessensummen). */
|
|
router.get('/:id/files', requireSession, loadOwnProject, async (req, res) => {
|
|
try {
|
|
const summary = await getProjectFileOverview(req.project)
|
|
if (!summary) return res.json({ totalFiles: 0, totalSizeBytes: 0, truncated: false, topLevel: [] })
|
|
return res.json(summary)
|
|
} catch (err) {
|
|
return res.status(500).json({ error: err.message || 'Datei-Uebersicht konnte nicht geladen werden' })
|
|
}
|
|
})
|
|
|
|
/** Ticket-Arbeiten am Projekt (inkl. Git-Push-Worksheets, "Zeit offen" markiert). */
|
|
router.get('/:id/work', requireSession, loadOwnProject, async (req, res) => {
|
|
try {
|
|
const project = req.project
|
|
if (!project.ticketId) {
|
|
return res.json({ ticket: null, worksheets: [] })
|
|
}
|
|
|
|
const ticket = await getDocument(config.collections.workorders, project.ticketId).catch(() => null)
|
|
const worksheets = await listDocuments(config.collections.worksheets, [
|
|
Query.equal('workorderId', project.ticketId),
|
|
Query.orderDesc('$createdAt'),
|
|
Query.limit(50),
|
|
]).catch(() => [])
|
|
|
|
const sanitized = worksheets
|
|
// GIT-Eintraege einschliessen, sonstige reine Kommentare ausblenden
|
|
.filter((ws) => ws.serviceType === 'GIT' || !ws.isComment)
|
|
.map((ws) => ({
|
|
wsid: ws.wsid || '',
|
|
date: ws.endDate || ws.startDate || '',
|
|
time: ws.endTime || ws.startTime || '',
|
|
employeeName: ws.employeeName || ws.employeeShort || '',
|
|
serviceType: ws.serviceType || '',
|
|
details: ws.details || '',
|
|
totalTime: Number(ws.totalTime || 0),
|
|
isGit: ws.serviceType === 'GIT',
|
|
pending: ws.serviceType === 'GIT' && !ws.startTime,
|
|
}))
|
|
|
|
return res.json({
|
|
ticket: ticket ? { woid: ticket.woid || '', topic: ticket.topic || '', status: ticket.status || '' } : null,
|
|
worksheets: sanitized,
|
|
})
|
|
} catch (err) {
|
|
return res.status(500).json({ error: err.message || 'Arbeiten konnten nicht geladen werden' })
|
|
}
|
|
})
|
|
|
|
export default router
|