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:
@@ -68,6 +68,53 @@ export function sanitizeInvoice(inv) {
|
||||
}
|
||||
}
|
||||
|
||||
/** Rohe Rechnung inkl. Invitations (fuer PDF-Download + Ownership-Pruefung). */
|
||||
export async function getInvoiceRaw(invoiceId) {
|
||||
const token = config.invoiceNinja.apiToken
|
||||
if (!token) {
|
||||
const error = new Error('INVOICE_NINJA_API_TOKEN ist nicht konfiguriert')
|
||||
error.status = 503
|
||||
throw error
|
||||
}
|
||||
const response = await fetch(
|
||||
`${config.invoiceNinja.url}/api/v1/invoices/${encodeURIComponent(invoiceId)}?include=invitations`,
|
||||
{
|
||||
headers: {
|
||||
'X-API-TOKEN': token,
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
Accept: 'application/json',
|
||||
},
|
||||
}
|
||||
)
|
||||
const data = await response.json().catch(() => ({}))
|
||||
if (!response.ok) {
|
||||
const error = new Error(data.message || 'Rechnung nicht gefunden')
|
||||
error.status = response.status === 401 ? 502 : response.status
|
||||
throw error
|
||||
}
|
||||
return data.data
|
||||
}
|
||||
|
||||
/** PDF einer Rechnung ueber den Client-Portal-Invitation-Link streamen. */
|
||||
export async function fetchInvoicePdf(invitationKey) {
|
||||
const token = config.invoiceNinja.apiToken
|
||||
const response = await fetch(
|
||||
`${config.invoiceNinja.url}/client/invoice/${encodeURIComponent(invitationKey)}/download_pdf`,
|
||||
{
|
||||
headers: {
|
||||
'X-Api-Token': token,
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
},
|
||||
}
|
||||
)
|
||||
if (!response.ok) {
|
||||
const error = new Error('PDF-Download fehlgeschlagen')
|
||||
error.status = response.status
|
||||
throw error
|
||||
}
|
||||
return Buffer.from(await response.arrayBuffer())
|
||||
}
|
||||
|
||||
/** Rechnungen eines InvoiceNinja-Clients (fuer Kundenportal + Admin-Ansicht). */
|
||||
export async function listInvoicesForClient(clientId, { includeDrafts = false } = {}) {
|
||||
const token = config.invoiceNinja.apiToken
|
||||
|
||||
Reference in New Issue
Block a user