Finanzen: Einstellungen, Rechnungen am Kunden, InvoiceNinja aus der Oberflaeche entfernt

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Sonnet 5.5 committed 2026-10-01 15:12:18 +00:00
1 parent 24a3cc41ef
commit 2aacdb2d56
24 files changed
+1410 -499

No files matched your search

+14 -30
View File
@@ -4,13 +4,13 @@ import { FaSpinner, FaStar, FaArrowLeft, FaFloppyDisk, FaArrowUp, FaKey, FaArrow
import { databases, DATABASE_ID, COLLECTIONS, Query } from '../lib/appwrite'
import { PageHeader, Card, Button, Badge, Tabs, EmptyState, Field } from '../components/ui'
import { StatusPill, UrgentPill } from '../components/ui/StatusPill'
import InvoicePanel from '../components/InvoicePanel'
import CustomerInvoices from '../components/finance/CustomerInvoices'
import AssignedProjectCard from '../components/AssignedProjectCard'
import CopyableCredential, { getCustomerPortalPassword } from '../components/CopyableCredential'
import { updateCustomerWithPortalAccess, deleteCustomerWithPortalAccess } from '../lib/customerAdminApi'
import { useAuth } from '../context/AuthContext'
import { useWebsiteProjects } from '../hooks/useWebsiteProjects'
import { customerStage, isArchived, isInvoiceReady, setCustomerArchived, upgradeToCustomer, syncCustomerToNinja } from '../lib/leads'
import { customerStage, isArchived, isInvoiceReady, setCustomerArchived, upgradeToCustomer } from '../lib/leads'
const PORTAL_URL = 'https://project.webklar.com'
@@ -49,7 +49,6 @@ export default function CustomerDetailPage() {
const stage = customerStage(customer)
const archived = isArchived(customer)
const syntheticTicket = { customerId: customer.$id, customerName: customer.name, woid: '' }
const toggleArchived = async () => {
await setCustomerArchived(customer, !archived)
@@ -96,15 +95,7 @@ export default function CustomerDetailPage() {
{tab === 'overview' && <OverviewTab customer={customer} stage={stage} onSaved={load} />}
{tab === 'tickets' && <CustomerTickets customerId={customer.$id} />}
{tab === 'invoices' && (
stage === 'lead' ? (
<Card><EmptyState title="Noch ein Lead" hint="Erst zum festen Kunden machen (vollstaendige Daten noetig), dann sind Rechnungen moeglich." action={<Button variant="primary" onClick={() => setTab('overview')}>Daten erfassen</Button>} /></Card>
) : !isInvoiceReady(customer) ? (
<Card><EmptyState title="Daten unvollstaendig" hint="Fuer Rechnungen werden Unternehmen, E-Mail, Strasse, Stadt und PLZ benoetigt." action={<Button variant="primary" onClick={() => setTab('overview')}>Daten vervollstaendigen</Button>} /></Card>
) : (
<InvoicePanel ticket={syntheticTicket} />
)
)}
{tab === 'invoices' && <CustomerInvoices customer={customer} onShowOverview={() => setTab('overview')} />}
{tab === 'projects' && <CustomerProjects customerId={customer.$id} />}
</div>
)
@@ -135,13 +126,8 @@ function OverviewTab({ customer, stage, onSaved }) {
const save = async () => {
setBusy(true); setMsg(null)
try {
const updated = await databases.updateDocument(DATABASE_ID, COLLECTIONS.CUSTOMERS, customer.$id, form)
let note = 'Gespeichert'
// Wenn fester Kunde: Aenderungen nach InvoiceNinja syncen (sofern Daten reichen oder bereits verknuepft)
if (stage === 'customer' && (updated.invoiceNinjaClientId || isInvoiceReady(updated))) {
try { await syncCustomerToNinja(updated) } catch (e) { note = 'Gespeichert, InvoiceNinja-Sync fehlgeschlagen: ' + e.message }
}
setMsg(note)
await databases.updateDocument(DATABASE_ID, COLLECTIONS.CUSTOMERS, customer.$id, form)
setMsg('Gespeichert')
onSaved()
} catch (err) {
setMsg('Fehler: ' + err.message)
@@ -151,11 +137,13 @@ function OverviewTab({ customer, stage, onSaved }) {
const upgrade = async () => {
if (!ready) { setMsg('Bitte zuerst alle Pflichtfelder (Unternehmen, E-Mail, Strasse, PLZ, Stadt) ausfuellen.'); return }
setBusy(true); setMsg(null)
const r = await upgradeToCustomer(customer, form)
if (r.clientId) setMsg('Zum festen Kunden gemacht und mit InvoiceNinja verknuepft.')
else setMsg('Status gesetzt, aber InvoiceNinja-Verknuepfung fehlgeschlagen: ' + (r.syncError || 'unbekannt'))
setBusy(false)
onSaved()
try {
await upgradeToCustomer(customer, form)
setMsg('Zum festen Kunden gemacht.')
onSaved()
} catch (err) {
setMsg('Fehler: ' + err.message)
} finally { setBusy(false) }
}
return (
@@ -183,8 +171,8 @@ function OverviewTab({ customer, stage, onSaved }) {
</div>
</Card>
<Card title="Rechnungsdaten (fuer InvoiceNinja)">
{!ready && <div className="badge badge-warn" style={{ marginBottom: 12 }}>Unvollstaendig - fuer Rechnungen erforderlich</div>}
<Card title="Rechnungsdaten">
{!ready && <div className="badge badge-warn" style={{ marginBottom: 12 }}>Unvollständig – für Rechnungen erforderlich</div>}
<Field label="Strasse + Nr."><input className="form-control" value={form.street} onChange={(e) => setForm({ ...form, street: e.target.value })} /></Field>
<div className="flex gap-3 wrap">
<Field label="PLZ"><input className="form-control" value={form.postalCode} onChange={(e) => setForm({ ...form, postalCode: e.target.value })} /></Field>
@@ -194,10 +182,6 @@ function OverviewTab({ customer, stage, onSaved }) {
<Field label="Land"><input className="form-control" value={form.country} onChange={(e) => setForm({ ...form, country: e.target.value })} /></Field>
<Field label="USt-IdNr."><input className="form-control" value={form.vatNumber} onChange={(e) => setForm({ ...form, vatNumber: e.target.value })} /></Field>
</div>
<div className="flex items-center justify-between" style={{ marginTop: 8 }}>
<span className="muted">InvoiceNinja</span>
{customer.invoiceNinjaClientId ? <Badge tone="ok" dot>verknuepft</Badge> : <Badge tone="muted">nicht verknuepft</Badge>}
</div>
</Card>
<PortalAccessCard customer={customer} onSaved={onSaved} />
+2 -3
View File
@@ -1,6 +1,6 @@
import { useState, useMemo } from 'react'
import { Link } from 'react-router-dom'
import { FaSpinner, FaPlus, FaStar, FaFileInvoiceDollar } from 'react-icons/fa6'
import { FaSpinner, FaPlus, FaStar } from 'react-icons/fa6'
import { useCustomers } from '../hooks/useCustomers'
import { useAuth } from '../context/AuthContext'
import { createLead, customerStage, isArchived } from '../lib/leads'
@@ -37,7 +37,7 @@ export default function CustomersPage() {
<div className="page">
<PageHeader
title="Kunden"
subtitle="Potenzielle Kunden (Leads) und feste Kunden - eine Pflege, Sync mit InvoiceNinja"
subtitle="Potenzielle Kunden (Leads) und feste Kunden – eine Pflege"
actions={
<>
{isAdmin && (
@@ -116,7 +116,6 @@ export default function CustomersPage() {
{st === 'lead' && <Badge tone="warn" dot>Lead</Badge>}
{st === 'customer' && <Badge tone="ok" dot>Fester Kunde</Badge>}
{st === 'lost' && <Badge tone="muted">Abgesagt</Badge>}
{c.invoiceNinjaClientId && <Badge tone="info"><FaFileInvoiceDollar /> Ninja</Badge>}
</div>
</Link>
)
+11 -3
View File
@@ -1,6 +1,6 @@
import { useCallback, useEffect, useRef, useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { FaSpinner } from 'react-icons/fa6'
import { Link, useNavigate } from 'react-router-dom'
import { FaGear, FaSpinner } from 'react-icons/fa6'
import { PageHeader, Card, Button } from '../components/ui'
import StatusLine from '../components/finance/StatusLine'
import LiquidityHead from '../components/finance/LiquidityHead'
@@ -191,7 +191,15 @@ export default function FinancePage() {
return (
<div className="page">
<PageHeader title="Finanzen" subtitle="Kontostand, Aufgaben und Zahlungen im Blick" />
<PageHeader
title="Finanzen"
subtitle="Kontostand, Aufgaben und Zahlungen im Blick"
actions={
<Button as={Link} variant="ghost" to="/finance/einstellungen">
<FaGear aria-hidden="true" /> Einstellungen
</Button>
}
/>
{assignNotice && (
<p className="fin-hint fin-hint-warn fin-page-notice" role="alert">
{assignNotice}{' '}
+31
View File
@@ -0,0 +1,31 @@
import { Link } from 'react-router-dom'
import { FaArrowLeft } from 'react-icons/fa6'
import { PageHeader, Button } from '../components/ui'
import SettingsCard from '../components/finance/SettingsCard'
import BankAccessCard from '../components/finance/BankAccessCard'
import FixedCostsCard from '../components/finance/FixedCostsCard'
/**
* Finanzen / Einstellungen (/finance/einstellungen, nur Admins): Rücklagen und Bankverbindungstext, Bankzugang
* (Status, Abruf, neu verbinden) und die Fixkostenliste. Jede Karte lädt und speichert für sich.
*/
export default function FinanceSettingsPage() {
return (
<div className="page">
<PageHeader
title="Finanzen: Einstellungen"
subtitle="Rücklagen, Bankzugang und Fixkosten"
actions={
<Button as={Link} variant="ghost" to="/finance">
<FaArrowLeft aria-hidden="true" /> Zurück zu den Finanzen
</Button>
}
/>
<div className="fin-stack">
<SettingsCard />
<BankAccessCard />
<FixedCostsCard />
</div>
</div>
)
}