- Kopfzeile mit Firmenname, Kundennr., Initialen-Avatar und Abmelden - Seitenleiste: Navigation Projekte/Fortschritt/Rechnungen/Chat mit Zaehlern (offene Rechnungen, ungelesene Nachrichten), "Dein Ansprechpartner" (Mitarbeiter des juengsten zugewiesenen Tickets, sonst WEBklar-Team) und Wartungs-Abo-Karte (Angebot bzw. Status, Stunden, naechste Abbuchung, Abo verwalten, Uebernahmegutschrift, Widerruf, Mindestlaufzeit, Kuendigen) - Projektkarte: Vorschau mit "Website oeffnen", Status, Vorschau-Adresse, Live-Domain, letzte Aenderung, Schnellzugriffe, Projektdetails - Abo-Bestellung und Stundenkonto/Verlauf sowie Uebernahmegutschrift als eigene Ansichten; Direktlinks #abo/#gutschrift (auch ueber den Login) - Pflichtlinks (Kuendigungsbutton, Widerruf, Bedingungen, Datenschutz, Impressum) als Fusszeile; Systemschriften statt externer Fonts - Mobil: Navigation im 2x2-Raster, kein horizontales Scrollen - API: /api/contact, updatedAt in /api/projects; Mail-Links auf Ansichten Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
103 lines
4.0 KiB
JavaScript
103 lines
4.0 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 { 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'],
|
|
})
|
|
return res.json({ tickets })
|
|
} 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
|