diff --git a/src/components/finance/AddressWarning.jsx b/src/components/finance/AddressWarning.jsx index ba64a33..a2c3b03 100644 --- a/src/components/finance/AddressWarning.jsx +++ b/src/components/finance/AddressWarning.jsx @@ -5,19 +5,20 @@ import { ADDRESS_INCOMPLETE_TEXT, addressComplete } from '../../lib/financeView' * Hinweis „Adresse des Kunden unvollständig (Straße, PLZ, Ort). Für den Versand bitte zuerst ergänzen.“ mit Weg zur * Übersicht des Kunden (dort stehen die Stammdaten). Rendert nichts, wenn die Anschrift vollständig ist. Mit * `onShowOverview` (Reiter am Kunden) wechselt ein Knopf zum Reiter Übersicht, sonst führt ein Link zur Kundenseite. - * `variant`: 'dialog' (im Rechnungsdialog) oder 'page' (über der Rechnungsliste des Kunden). + * `variant`: 'dialog' (im Rechnungsdialog) oder 'page' (über der Rechnungsliste des Kunden). Der Knopf trägt immer + * type="button", denn die Variante 'dialog' steht in einem Formular. */ export default function AddressWarning({ customer, onShowOverview, variant = 'page' }) { if (!customer || addressComplete(customer)) return null return ( -
+
{ADDRESS_INCOMPLETE_TEXT} {onShowOverview ? ( - ) : customer.$id ? ( - + Zur Übersicht ) : null} diff --git a/src/components/finance/CustomerInvoices.jsx b/src/components/finance/CustomerInvoices.jsx index 2c93100..0cb8e33 100644 --- a/src/components/finance/CustomerInvoices.jsx +++ b/src/components/finance/CustomerInvoices.jsx @@ -1,4 +1,4 @@ -import { Button, Card, EmptyState } from '../ui' +import { Button, EmptyState } from '../ui' import AddressWarning from './AddressWarning' import InvoiceTable from './InvoiceTable' import { customerInvoicesGate } from '../../lib/financeView' @@ -13,13 +13,11 @@ export default function CustomerInvoices({ customer, onShowOverview }) { const gate = customerInvoicesGate(customer) if (!gate.ok) { return ( - - Daten erfassen : null} - /> - + Daten erfassen : null} + /> ) } return ( diff --git a/src/components/finance/InvoiceTable.jsx b/src/components/finance/InvoiceTable.jsx index 7797be3..3d84c7f 100644 --- a/src/components/finance/InvoiceTable.jsx +++ b/src/components/finance/InvoiceTable.jsx @@ -1,10 +1,10 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react' -import { FaPlus, FaSpinner } from 'react-icons/fa6' -import { Badge, Button, Card, EmptyState } from '../ui' +import { Badge, Button, Card, EmptyState, Stat } from '../ui' import InvoiceCreateModal from './InvoiceCreateModal' import Pager from './Pager' import { deleteInvoice, getInvoices, resendInvoice, sendInvoice, voidInvoice } from '../../lib/financeApi' import { pageOf } from '../../lib/financeFormat' +import { noticeProps } from '../../lib/financeUi' import { ADDRESS_INCOMPLETE_TEXT, DEFAULT_INVOICE_FILTER, @@ -33,8 +33,8 @@ const CALLS = { send: sendInvoice, resend: resendInvoice, void: voidInvoice, del * eine Anfrage läuft, sind alle Aktionen gesperrt. „Rechnung schreiben“ öffnet den Dialog. * * customerId filtert auf einen Kunden; `customer` (Appwrite-Dokument) legt zusätzlich den Kunden im Dialog fest - * (Kundenansicht). onChanged() meldet jede Änderung (Übersicht neu laden). Schmal (Karte bis 700 px) wird jede - * Zeile ein Raster: Nummer und Status / Kunde / Datum, Fällig / Betrag, Offen / Aktionen. „Senden“ eines Entwurfs + * (Kundenansicht). onChanged() meldet jede Änderung (Übersicht neu laden). Schmal (Wrapper .fin-body bis 860 px) + * wird jede Zeile ein Raster: Nummer und Status / Kunde / Datum, Fällig / Betrag, Offen / Aktionen. „Senden“ eines Entwurfs * fragt vorher nach. In der Kundenansicht ist „Senden“ gesperrt, solange dem Kunden die Anschrift fehlt und der * Entwurf über 250 € liegt (§ 14 Abs. 4 Nr. 1 UStG); `onShowOverview` führt dann im Dialog zur Übersicht. * @@ -147,137 +147,147 @@ export default function InvoiceTable({ altersgruppen = null, customerId = null, changed() } + return (
setCreateOpen(true)}> - - Seite {page} von {pages} {typeof total === 'number' ? ` · ${total} ${noun}` : ''} - diff --git a/src/components/finance/TransactionTable.jsx b/src/components/finance/TransactionTable.jsx index 7496b0c..2aab145 100644 --- a/src/components/finance/TransactionTable.jsx +++ b/src/components/finance/TransactionTable.jsx @@ -1,8 +1,8 @@ import { useMemo, useState } from 'react' -import { FaSpinner, FaTriangleExclamation } from 'react-icons/fa6' import { Badge, Button, Card, EmptyState } from '../ui' import Pager from './Pager' import { pageOf } from '../../lib/financeFormat' +import { isNegText, noticeProps } from '../../lib/financeUi' import { transactionRows } from '../../lib/financeView' const PAGE_SIZE = 50 @@ -13,8 +13,8 @@ export const TRANSACTIONS_ANCHOR = 'finanzen-buchungen' * Verwendungszweck (zweizeilig), Betrag mit Vorzeichen, Zuordnung (bei offenen Buchungen der stärkste Vorschlag als * Text) und der Knopf „Zuordnen“ bzw. „Ändern“. Vorgemerkte Buchungen (PDNG) tragen „vorgemerkt“. Die Daten kommen * vom Aufrufer (useFinanceTransactions), damit Seite und Zuordnen-Dialog dieselbe Liste benutzen. - * onAssign(buchung) öffnet den Dialog. Schmal (Karte bis 640 px) wird jede Zeile ein Raster: Datum und Betrag / - * Gegenpartei und Verwendungszweck / Zuordnung und Knopf. + * onAssign(buchung) öffnet den Dialog. Schmal (Wrapper .fin-body bis 640 px) wird jede Zeile ein Raster: Datum und + * Betrag / Gegenpartei und Verwendungszweck / Zuordnung und Knopf. Beträge mit „−“ stehen in Rot (fin-neg). */ export default function TransactionTable({ transactions, stripeFehler = '', loaded = false, loading = false, error = '', onReload, onAssign }) { const [page, setPage] = useState(1) @@ -24,16 +24,13 @@ export default function TransactionTable({ transactions, stripeFehler = '', load let body if (!loaded && loading) { body = ( -
-
+
Buchungen werden geladen …
) } else if (!loaded) { body = (
-

{error || 'Die Buchungen konnten nicht geladen werden.'}

- +

{error || 'Die Buchungen konnten nicht geladen werden.'}

+
) } else if (rows.length === 0) { @@ -41,7 +38,7 @@ export default function TransactionTable({ transactions, stripeFehler = '', load } else { body = (
- +
@@ -62,7 +59,12 @@ export default function TransactionTable({ transactions, stripeFehler = '', load
{row.remittance}
)} - + @@ -93,20 +95,22 @@ export default function TransactionTable({ transactions, stripeFehler = '', load return (
- - {loaded && error && ( -

- Die Buchungen konnten nicht neu geladen werden: {error}{' '} - -

- )} - {stripeFehler && ( -

-

- )} - {body} - + +
+ {loaded && error && ( +

+ Die Buchungen konnten nicht neu geladen werden: {error}{' '} + +

+ )} + {stripeFehler && ( +

+ {stripeFehler} +

+ )} + {body} + +
) diff --git a/src/styles/global.css b/src/styles/global.css index e78dbd7..0b800d9 100644 --- a/src/styles/global.css +++ b/src/styles/global.css @@ -663,16 +663,10 @@ button.link-danger { background: none; border: 0; padding: 0; font: inherit; fon .fin-block { scroll-margin-top: 68px; } /* unter der 52 px hohen Mobil-Leiste 16 px Luft */ } -/* Uebergang: diese Regeln fallen, sobald keine Datei mehr die Klassen benutzt (Hinweis, Drehknopf, Textknopf der Tabellen und Formulare) */ +/* Uebergang: diese Regeln fallen, sobald keine Datei mehr die Klassen benutzt (Hinweis, Drehknopf) */ .fin-spin { animation: spin 1s linear infinite; } .fin-hint { margin: 0; font-size: 13px; color: var(--text-muted); } .fin-hint-warn { color: var(--warn); } -.fin-link { - background: none; border: 0; padding: 0; font: inherit; cursor: pointer; - color: var(--accent); display: inline-flex; align-items: center; gap: 6px; - text-decoration: underline; text-underline-offset: 2px; -} -.fin-link:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 4px; } /* Block 0: Statuszeile (ohne Kasten) */ .fin-status-wrap > .fin-notice { margin: 8px 0 0; } @@ -759,54 +753,44 @@ button.link-danger { background: none; border: 0; padding: 0; font: inherit; fon .fin-abo-customer { font-weight: 600; overflow-wrap: anywhere; } .fin-abo-meta { grid-column: 1 / -1; display: flex; flex-wrap: wrap; align-items: center; gap: 4px 12px; font-size: 13px; } -/* Block 6 und 7: Rechnungen und Buchungen (Tabellen; in einer schmalen Karte wird jede Zeile ein Raster) */ -.fin-invoices-card, .fin-tx-card { container-type: inline-size; overflow: hidden; } -.fin-toolbar { padding: var(--sp-3) var(--sp-4); } -.fin-toolbar .seg { flex-wrap: wrap; max-width: 100%; } -.fin-ages { - list-style: none; margin: 0; padding: var(--sp-3) var(--sp-4); border-bottom: 1px solid var(--border); - display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: var(--sp-3); -} -.fin-age { display: grid; gap: 2px; min-width: 0; } -.fin-age-label { color: var(--text-muted); font-size: 12px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.4px; } -.fin-age-count { font-size: 13px; color: var(--text-muted); } -.fin-age-sum { text-align: left; font-size: 18px; font-weight: 800; } -.fin-pager { display: flex; align-items: center; justify-content: center; flex-wrap: wrap; gap: var(--sp-3); padding: var(--sp-3) var(--sp-4); border-top: 1px solid var(--border); } -.fin-pager-info { font-size: 13px; color: var(--text-muted); font-variant-numeric: tabular-nums; } +/* Block 6 und 7: Rechnungen und Buchungen. Beide Tabellen liegen in .fin-body (Container); schmal wird jede Zeile ein Raster. */ +.fin-block .seg { flex-wrap: wrap; max-width: 100%; } +.stats.fin-ages { list-style: none; padding: 0; display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 16px 32px; margin: 0 0 16px; } +.fin-ages .stat .v { font-size: 18px; line-height: 26px; } +.fin-pager { justify-content: center; flex-wrap: wrap; gap: 16px; } +.fin-pager-info { font-variant-numeric: tabular-nums; } +.fin-block button.link:disabled, .fin-block button.link-danger:disabled { cursor: not-allowed; } -.fin-invoices th, .fin-invoices td, .fin-tx th, .fin-tx td { padding: 9px 10px; vertical-align: top; } .fin-invoices th.fin-amount, .fin-tx th.fin-amount { text-align: right; } -.fin-invoices th:first-child, .fin-invoices td:first-child, .fin-tx th:first-child, .fin-tx td:first-child { padding-left: var(--sp-4); } -.fin-invoices th:last-child, .fin-invoices td:last-child, .fin-tx th:last-child, .fin-tx td:last-child { padding-right: var(--sp-4); } -.fin-invoices tbody tr:last-child td, .fin-tx tbody tr:last-child td { border-bottom: none; } -.fin-inv-number { font-weight: 700; white-space: nowrap; } -.fin-inv-customer { overflow-wrap: anywhere; } +.fin-inv-number { font-weight: 500; white-space: nowrap; } +.fin-inv-customer, .fin-tx-counterparty { overflow-wrap: anywhere; } +.fin-tx-counterparty { font-weight: 500; } .fin-inv-created, .fin-inv-due, .fin-tx-date { white-space: nowrap; font-variant-numeric: tabular-nums; } .fin-inv-statusbox { display: grid; gap: 3px; justify-items: start; } .fin-inv-note { font-size: 12px; } -.fin-actions { display: flex; flex-wrap: wrap; gap: 6px; } -.fin-tx-counterparty { font-weight: 600; overflow-wrap: anywhere; } +.fin-actions { display: flex; flex-wrap: wrap; gap: 4px 14px; } .fin-tx-remittance { font-size: 13px; overflow-wrap: anywhere; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } -.fin-tx-amount { font-weight: 700; } +.fin-tx-amount { font-weight: 600; } .fin-tx-badges { display: flex; flex-wrap: wrap; gap: 4px; } -.fin-tx-assign .badge { white-space: normal; } +.fin-tx-assign .badge { white-space: normal; height: auto; padding-block: 2px; } .fin-tx-suggestion, .fin-tx-note { font-size: 12px; margin-top: 4px; overflow-wrap: anywhere; } .fin-tx-action { white-space: nowrap; text-align: right; } +@media (max-width: 860px) { + .fin-block .seg button { padding: 0 9px; } +} @container (max-width: 860px) { - .fin-ages { grid-template-columns: repeat(2, minmax(0, 1fr)); } - .fin-invoices thead, .fin-tx thead { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; } - .fin-invoices, .fin-invoices tbody, .fin-tx, .fin-tx tbody { display: block; } + .stats.fin-ages { grid-template-columns: repeat(2, minmax(0, 1fr)); } + .fin-invoices thead { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; } + .fin-invoices, .fin-invoices tbody { display: block; } .fin-invoices tr { - display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 4px var(--sp-4); align-items: baseline; + display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 4px 16px; align-items: baseline; grid-template-areas: "number status" "customer customer" "created due" "total remaining" "actions actions"; - padding: 10px var(--sp-4); border-bottom: 1px solid var(--border); + padding: 10px 0; border-bottom: 1px solid var(--line); } - .fin-invoices tbody tr:last-child { border-bottom: none; } - .fin-invoices td, .fin-invoices td:first-child, .fin-invoices td:last-child, .fin-tx td, .fin-tx td:first-child, .fin-tx td:last-child { display: block; padding: 0; border: 0; min-width: 0; } - .fin-invoices tbody tr:hover td, .fin-tx tbody tr:hover td { background: transparent; } + .fin-invoices td, .fin-invoices td:first-child, .fin-invoices td:last-child { display: block; padding: 0; border: 0; min-width: 0; } .fin-inv-number { grid-area: number; } .fin-inv-status { grid-area: status; } .fin-inv-statusbox { justify-items: end; text-align: right; } @@ -816,16 +800,18 @@ button.link-danger { background: none; border: 0; padding: 0; font: inherit; fon .fin-inv-total { grid-area: total; } .fin-inv-remaining { grid-area: remaining; } .fin-inv-actions { grid-area: actions; padding-top: 4px; } - .fin-invoices td[data-label] { display: flex; flex-wrap: wrap; justify-content: space-between; gap: 0 var(--sp-2); font-size: 13px; } - .fin-invoices td[data-label]::before { content: attr(data-label); color: var(--text-muted); font-weight: 500; } + .fin-invoices td[data-label] { display: flex; flex-wrap: wrap; justify-content: space-between; gap: 0 8px; font-size: 13px; } + .fin-invoices td[data-label]::before { content: attr(data-label); color: var(--text-2); font-weight: 500; } } @container (max-width: 640px) { + .fin-tx thead { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; } + .fin-tx, .fin-tx tbody { display: block; } .fin-tx tr { - display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 4px var(--sp-4); align-items: baseline; + display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 4px 16px; align-items: baseline; grid-template-areas: "date amount" "what what" "assign action"; - padding: 10px var(--sp-4); border-bottom: 1px solid var(--border); + padding: 10px 0; border-bottom: 1px solid var(--line); } - .fin-tx tbody tr:last-child { border-bottom: none; } + .fin-tx td, .fin-tx td:first-child, .fin-tx td:last-child { display: block; padding: 0; border: 0; min-width: 0; } .fin-tx-date { grid-area: date; } .fin-tx-amount { grid-area: amount; } .fin-tx-what { grid-area: what; } @@ -846,10 +832,9 @@ button.link-danger { background: none; border: 0; padding: 0; font: inherit; fon .fin-dialog-warn { margin: 6px 0 0; font-size: 13px; color: var(--orange-fg); overflow-wrap: anywhere; } .fin-dialog-sub { margin: 6px 0 0; font-size: 13px; color: var(--text-muted); overflow-wrap: anywhere; } .fin-fixed-customer { margin: 0; overflow-wrap: anywhere; } -.fin-address-warn { display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px var(--sp-3); font-size: 13px; overflow-wrap: anywhere; } -.fin-address-warn-dialog { margin: 6px 0 0; color: var(--orange-fg); } -.fin-address-warn-page { margin: 0 0 var(--sp-4); padding: var(--sp-3) var(--sp-4); border: 1px solid var(--warn); border-radius: var(--r-sm); color: var(--warn); } -.fin-address-warn .fin-link { font-size: 13px; } +.fin-address-warn { display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 12px; overflow-wrap: anywhere; } +.fin-address-warn-page { margin: 0 0 16px; } +.fin-address-warn-dialog { margin: 0; } .fin-items { border: 0; padding: 0; margin: 0 0 var(--sp-4); min-width: 0; } .fin-items legend { padding: 0; } .fin-item { @@ -904,7 +889,6 @@ button.link-danger { background: none; border: 0; padding: 0; font: inherit; fon .fin-dialog-row { grid-template-columns: minmax(0, 1fr); } .fin-tx-facts > div { grid-template-columns: minmax(0, 1fr); gap: 2px; } .fin-dialog-actions .ui-btn { flex: 1 1 auto; } - .fin-toolbar .seg button { padding: 6px 9px; } } /* Finanzen: Einstellungen (Formular, Bankzugang, Fixkosten) */
Buchungen, neueste zuerst
{row.amountText} + {row.amountText} +
{row.assignmentText} @@ -73,14 +75,14 @@ export default function TransactionTable({ transactions, stripeFehler = '', load
{row.action && ( - + )}