Files
Webklar-Kundenbereich/server/routes/customerData.js
T
ClaudeandClaude Opus 5.5 ba9916cfe6 Projektdetails und Fortschritt kundenverständlich
Der Server bereitet Commits, Tickets und Arbeitseinträge für Kunden auf
(server/services/customerProjectView.js), die Rohdaten bleiben intern:
- keine Co-Authored-By-/Signed-off-by-Zeilen und KI-Hinweise, keine
  Commit-Kürzel, Kontonamen, Git-Links oder Branch
- technische Commits (Preview-Config, Merges) ausgeblendet, bekannte
  Automatik-Texte übersetzt, "fix"/"upload" -> "Website aktualisiert"
- Ticketstatus und Arbeitsarten auf Deutsch (Open -> Eingegangen,
  Remote -> Online erledigt, ...), Fortschritt in 4 Schritten
- Akquise-Tickets auch in den Projektdetails verborgen
- /api/projects ohne Gitea-URL, Repo- und Vorlagennamen

Darstellung: Auftragskarte mit Status und Fortschrittsleiste, Zeitleiste
"Letzte Änderungen" nach Tagen, Arbeiten mit Farbe/Symbol je Art und
Arbeitszeit; der Tab "Fortschritt" nutzt dieselben Bausteine. In der
App-Ansicht öffnen sich die Projektdetails als eigene Seite mit Zurück
zu "Projekte".

Tests: npm test (node --test, 22 Fälle).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 18:46:22 +00:00

109 lines
4.4 KiB
JavaScript

import { Router } from 'express'
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)
}
})
export default router