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:
1 parent
24a3cc41ef
commit
2aacdb2d56
24 files changed
+1410
-499
No files matched your search
@@ -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} />
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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}{' '}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
Reference in new issue
Block a user