feat: Kundenportal-Ausbau - Tabs, Projekt-Einblicke, Git-Zeiterfassung, Rechnungen, Chat
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>
This commit is contained in:
@@ -3,7 +3,7 @@ import { config } from '../config.js'
|
||||
import { getSessionCustomerId, requireSession } from '../middleware/session.js'
|
||||
import { getDocument } from '../services/appwriteAdmin.js'
|
||||
import { getCustomerActivity } from '../services/customerActivity.js'
|
||||
import { listInvoicesForClient } from '../services/invoiceNinja.js'
|
||||
import { fetchInvoicePdf, getInvoiceRaw, listInvoicesForClient } from '../services/invoiceNinja.js'
|
||||
|
||||
const router = Router()
|
||||
|
||||
@@ -27,6 +27,40 @@ router.get('/invoices', requireSession, async (req, res) => {
|
||||
}
|
||||
})
|
||||
|
||||
/** Rechnungs-PDF des eingeloggten Kunden (Download ueber Server-Proxy). */
|
||||
router.get('/invoices/:invoiceId/pdf', requireSession, async (req, res) => {
|
||||
try {
|
||||
const customer = await getDocument(
|
||||
config.collections.customers,
|
||||
getSessionCustomerId(req)
|
||||
).catch(() => null)
|
||||
if (!customer?.invoiceNinjaClientId) {
|
||||
return res.status(404).json({ error: 'Rechnung nicht gefunden' })
|
||||
}
|
||||
|
||||
const invoice = await getInvoiceRaw(req.params.invoiceId).catch(() => null)
|
||||
// Ownership-Pruefung: Rechnung muss zum InvoiceNinja-Client des Kunden gehoeren
|
||||
if (!invoice || invoice.client_id !== customer.invoiceNinjaClientId) {
|
||||
return res.status(404).json({ error: 'Rechnung nicht gefunden' })
|
||||
}
|
||||
|
||||
const invitationKey = invoice.invitations?.[0]?.key
|
||||
if (!invitationKey) {
|
||||
return res.status(404).json({ error: 'Kein PDF verfuegbar' })
|
||||
}
|
||||
|
||||
const pdf = await fetchInvoicePdf(invitationKey)
|
||||
res.writeHead(200, {
|
||||
'Content-Type': 'application/pdf',
|
||||
'Content-Disposition': `attachment; filename="Rechnung-${invoice.number || invoice.id}.pdf"`,
|
||||
'Cache-Control': 'no-store',
|
||||
})
|
||||
return res.end(pdf)
|
||||
} catch (err) {
|
||||
return res.status(err.status || 500).json({ error: err.message || 'PDF konnte nicht geladen werden' })
|
||||
}
|
||||
})
|
||||
|
||||
/** Ticket-/Arbeitshistorie des eingeloggten Kunden ("was wurde gemacht"). */
|
||||
router.get('/activity', requireSession, async (req, res) => {
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user