Files
Webklar-Kundenbereich/server/routes/customerData.js
T
ClaudeandClaude Opus 5.5 8c9d28c30f Profilbild: zufälliger DiceBear-Avatar (Big Smile) pro Kunde
Der Server erzeugt das SVG lokal unter /api/avatar.svg (Seed = Kunden-ID),
es geht also keine Anfrage an dicebear.com. Die Initialen bleiben als
Fallback, bis das Bild geladen ist.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 16:36:49 +00:00

123 lines
5.0 KiB
JavaScript

import { Router } from 'express'
import { createAvatar } from '@dicebear/core'
import * as bigSmile from '@dicebear/big-smile'
import { config } from '../config.js'
import { getSessionCustomerId, requireSession } from '../middleware/session.js'
import { getDocument, listDocuments, Query } from '../services/appwriteAdmin.js'
import { getCustomerActivity } from '../services/customerActivity.js'
import { customerOrder, customerWorkEntries } from '../services/customerProjectView.js'
import { fetchInvoicePdf, getInvoiceRaw, listInvoicesForClient } from '../services/invoiceNinja.js'
const router = Router()
/** Rechnungen des eingeloggten Kunden (InvoiceNinja, sofern verknuepft). */
router.get('/invoices', requireSession, async (req, res) => {
try {
const customer = await getDocument(
config.collections.customers,
getSessionCustomerId(req)
).catch(() => null)
if (!customer) {
return res.status(404).json({ error: 'Kunde nicht gefunden' })
}
if (!customer.invoiceNinjaClientId) {
return res.json({ linked: false, invoices: [] })
}
const invoices = await listInvoicesForClient(customer.invoiceNinjaClientId)
return res.json({ linked: true, invoices })
} catch (err) {
return res.status(500).json({ error: err.message || 'Rechnungen konnten nicht geladen werden' })
}
})
/** 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 {
const tickets = await getCustomerActivity(getSessionCustomerId(req), {
ticketLimit: 25,
// Interne Akquise-Tickets gehoeren nicht in die Kundenansicht
excludeTypes: ['Akquise'],
})
// Status auf Deutsch, Arbeiten ohne Git-Technik (customerProjectView.js)
const orders = tickets
.map((t) => ({ order: customerOrder(t), createdAt: t.createdAt, entries: customerWorkEntries(t.worksheets) }))
.filter((t) => t.order)
.map(({ order, createdAt, entries }) => ({ ...order, createdAt, entries }))
return res.json({ tickets: orders })
} catch (err) {
return res.status(500).json({ error: err.message || 'Aktivität konnte nicht geladen werden' })
}
})
/**
* "Dein Ansprechpartner" in der Seitenleiste: der Mitarbeiter, dem das juengste zugewiesene
* Ticket des Kunden gehoert (workorders.assignedTo -> employees). Sonst das Team.
*/
router.get('/contact', requireSession, async (req, res) => {
const fallback = { name: 'WEBklar-Team', responseTime: 'Antwort meist < 1 Werktag' }
try {
const [tickets, employees] = await Promise.all([
listDocuments(config.collections.workorders, [
Query.equal('customerId', getSessionCustomerId(req)),
Query.orderDesc('$createdAt'),
Query.limit(50),
]),
listDocuments(config.collections.employees, [Query.limit(100)]),
])
const nameOf = new Map(employees.flatMap((e) => [[e.userId, e.displayName], [e.$id, e.displayName]]))
const assignee = (t) => nameOf.get(String(t.assignedTo || '')) || t.assignedName || ''
const ticket = tickets.find((t) => assignee(t))
return res.json({ ...fallback, name: ticket ? assignee(ticket) : fallback.name })
} catch {
return res.json(fallback)
}
})
/** Profilbild: DiceBear "Big Smile", lokal erzeugt (keine Anfrage an dicebear.com).
Die Kunden-ID ist der Seed - zufaellig pro Kunde, aber bei jedem Login dasselbe Bild. */
router.get('/avatar.svg', requireSession, (req, res) => {
const svg = createAvatar(bigSmile, {
seed: String(getSessionCustomerId(req) || 'webklar'),
backgroundColor: ['b6e3f4', 'c0aede', 'd1d4f9', 'ffd5dc', 'ffdfbf'],
}).toString()
res.setHeader('Content-Type', 'image/svg+xml; charset=utf-8')
res.setHeader('Cache-Control', 'private, max-age=86400')
return res.send(svg)
})
export default router