feat: Admin-Bereich im Kundenportal (Ticketsystem-Login, Projektsuche, Kundenverwaltung)

- Login: Appwrite-User mit Label 'admin' (Ticketsystem-Mitarbeiter) bekommen
  eine Admin-Session und landen auf /admin.html
- Admin-Dashboard: Statistik-Uebersicht, Projektsuche ueber alle Projekte
  (inkl. Preview-/Gitea-Links, Vorschau ohne Kunden-Login), Kundenliste mit
  Portal-Status, letztem Login und Projektanzahl
- Kundennummer (code) direkt im Admin-Bereich pflegbar
- 'Als Kunde ansehen': Admin sieht das Portal exakt wie der Kunde (Banner
  mit Rueckweg zur Admin-Uebersicht)
- Kunden-Dashboard: neue Bereiche 'Meine Rechnungen' (InvoiceNinja inkl.
  Link ins Rechnungsportal) und 'Was wurde gemacht' (Tickets + Worksheets
  aus dem Ticketsystem, ohne interne Akquise-Tickets); Kundennummer im Header
- Admin-API /api/portal-admin/* (Session-basiert), Kunden-API /api/invoices
  und /api/activity
- appwriteClient: uebrig gebliebene Debug-Instrumentierung entfernt

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-11 17:09:39 +00:00
parent 0320042045
commit 177ee6a13d
12 changed files with 1085 additions and 54 deletions

View File

@@ -0,0 +1,44 @@
import { Router } from 'express'
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'
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' })
}
})
/** 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' })
}
})
export default router