Files
tickte-system/src/lib/integrationsApi.js
Webklar Deploy a1cb90372f feat: CRM-Stufen (Lead/Kunde) + InvoiceNinja-Sync (eine Pflege)
- Zwei Kundenstufen: Lead (potenziell) und fester Kunde; Lead braucht nur
  Unternehmen + E-Mail und erzeugt automatisch ein Akquise-Ticket
- Akquise-Worksheet-Ergebnis (In Kontakt/Zugesagt/Abgesagt) sichtbar in WSIDs;
  "Zugesagt" macht den Lead automatisch zum festen Kunden, "Abgesagt" -> verloren
- Kunde wird nur in Appwrite gepflegt und automatisch als InvoiceNinja-Client
  angelegt/aktualisiert (beim Upgrade bzw. bei Datenpflege)
- Rechnungen nur fuer feste Kunden mit vollstaendigen Daten (Strasse/PLZ/Stadt)
- Kundenliste mit Stufen-Filter, neue Rechnungs-/Adressfelder

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-17 11:18:28 +00:00

100 lines
3.5 KiB
JavaScript

import { account } from './appwrite'
/**
* Client fuer das Integrations-Backend (InvoiceNinja + Paperless).
* Same-Origin unter /api/integrations, authentifiziert per Appwrite-JWT.
*/
const BASE = '/api/integrations'
async function authHeaders(extra = {}) {
const { jwt } = await account.createJWT()
return { 'X-Appwrite-JWT': jwt, ...extra }
}
async function call(path, { method = 'GET', body } = {}) {
const headers = await authHeaders(body ? { 'Content-Type': 'application/json' } : {})
const res = await fetch(`${BASE}${path}`, {
method,
headers,
body: body ? JSON.stringify(body) : undefined,
})
const data = await res.json().catch(() => ({}))
if (!res.ok) {
throw new Error(data.error || `Integrations-Fehler ${res.status}`)
}
return data
}
async function callBlob(path) {
const headers = await authHeaders()
const res = await fetch(`${BASE}${path}`, { headers })
if (!res.ok) throw new Error(`Download fehlgeschlagen (${res.status})`)
return res.blob()
}
function qs(params = {}) {
const sp = new URLSearchParams()
Object.entries(params).forEach(([k, v]) => {
if (v !== undefined && v !== null && v !== '') sp.set(k, v)
})
const s = sp.toString()
return s ? `?${s}` : ''
}
async function openBlobInNewTab(blob) {
const url = URL.createObjectURL(blob)
window.open(url, '_blank', 'noopener')
setTimeout(() => URL.revokeObjectURL(url), 60_000)
}
export const integrationsApi = {
// ---- health ----
health: () => call('/health'),
// ---- InvoiceNinja: clients ----
listClients: (search = '') => call(`/clients${qs({ search })}`),
createClient: (payload) => call('/clients', { method: 'POST', body: payload }),
updateClient: (id, payload) => call(`/clients/${encodeURIComponent(id)}`, { method: 'PUT', body: payload }),
// ---- InvoiceNinja: invoices ----
listInvoices: (clientId, perPage) => call(`/invoices${qs({ clientId, per_page: perPage })}`),
invoicesSummary: () => call('/invoices/summary'),
createInvoice: (payload) => call('/invoices', { method: 'POST', body: payload }),
async openInvoicePdf(id) {
const blob = await callBlob(`/invoices/${encodeURIComponent(id)}/pdf`)
await openBlobInNewTab(blob)
},
// ---- Paperless ----
listDocuments: (params = {}) => call(`/paperless/documents${qs(params)}`),
ensureCorrespondent: (name) => call('/paperless/ensure-correspondent', { method: 'POST', body: { name } }),
ensureTag: (name) => call('/paperless/ensure-tag', { method: 'POST', body: { name } }),
uploadToPaperless: (payload) => call('/paperless/upload', { method: 'POST', body: payload }),
async openDocument(id, kind = 'preview') {
const blob = await callBlob(`/paperless/documents/${encodeURIComponent(id)}/${kind}`)
await openBlobInNewTab(blob)
},
}
// Oeffentliche InvoiceNinja-URL (fuer Deeplinks ins Portal)
export const INVOICE_NINJA_PUBLIC_URL = 'https://invoice.webklar.com'
// InvoiceNinja status_id -> deutsche Bezeichnung + Farbe
export const INVOICE_STATUS = {
1: { label: 'Entwurf', color: '#a0aec0' },
2: { label: 'Versendet', color: '#3b82f6' },
3: { label: 'Teilzahlung', color: '#f59e0b' },
4: { label: 'Bezahlt', color: '#10b981' },
5: { label: 'Storniert', color: '#ef4444' },
'-1': { label: 'Storniert', color: '#ef4444' },
}
export function formatMoney(amount, currency = 'EUR') {
const n = Number(amount || 0)
try {
return new Intl.NumberFormat('de-DE', { style: 'currency', currency }).format(n)
} catch {
return `${n.toFixed(2)} ${currency}`
}
}