Finanzen: Einstellungen, Bankzugang, Fixkosten und Bankrueckkehr ohne Karten
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
b5d27d5801
commit
d1a1046969
7 files changed
+91
-126
No files matched your search
@@ -1,7 +1,7 @@
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { FaRotate, FaSpinner } from 'react-icons/fa6'
|
||||
import { Fragment, useEffect, useRef, useState } from 'react'
|
||||
import { Badge, Button, Card } from '../ui'
|
||||
import { useFinanceResource } from '../../hooks/useFinanceResource'
|
||||
import { noticeProps } from '../../lib/financeUi'
|
||||
import { getBank, refreshBank, startBankAuth } from '../../lib/financeApi'
|
||||
import { bankCardModel, refreshResultMessage } from '../../lib/financeSettings'
|
||||
import { safeUrl } from '../../lib/financeView'
|
||||
@@ -67,14 +67,11 @@ export default function BankAccessCard() {
|
||||
return (
|
||||
<Card title="Bankzugang">
|
||||
{res.loading ? (
|
||||
<div className="empty" role="status">
|
||||
<FaSpinner className="spinner" aria-hidden="true" />
|
||||
<p className="muted">Bankzugang wird geladen …</p>
|
||||
</div>
|
||||
<div className="loading" role="status">Bankzugang wird geladen …</div>
|
||||
) : (
|
||||
<div className="fin-load-error" style={{ padding: 0 }}>
|
||||
<p className="fin-hint fin-hint-warn" role="alert">{res.error || LOAD_ERROR}</p>
|
||||
<Button size="sm" onClick={res.reload}>Erneut versuchen</Button>
|
||||
<div className="fin-load-error">
|
||||
<p {...noticeProps('warn')}>{res.error || LOAD_ERROR}</p>
|
||||
<Button onClick={res.reload}>Erneut versuchen</Button>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
@@ -85,36 +82,28 @@ export default function BankAccessCard() {
|
||||
return (
|
||||
<Card title="Bankzugang" actions={<Badge tone={model.tone}>{model.statusLabel}</Badge>}>
|
||||
{res.error && (
|
||||
<p className="fin-hint fin-hint-warn fin-form-notice" role="alert">
|
||||
Der Stand konnte nicht neu geladen werden: {res.error}
|
||||
</p>
|
||||
<p {...noticeProps('warn', 'fin-notice')}>Der Stand konnte nicht neu geladen werden: {res.error}</p>
|
||||
)}
|
||||
{model.rows.length > 0 && (
|
||||
<dl className="fin-bank-facts">
|
||||
<dl className="kv fin-kv">
|
||||
{model.rows.map((row) => (
|
||||
<div key={row.key}>
|
||||
<Fragment key={row.key}>
|
||||
<dt>{row.label}</dt>
|
||||
<dd>{row.text}</dd>
|
||||
</div>
|
||||
<dd className="num">{row.text}</dd>
|
||||
</Fragment>
|
||||
))}
|
||||
</dl>
|
||||
)}
|
||||
{model.error && (
|
||||
<p className="fin-hint fin-hint-warn fin-form-notice" role="alert">Fehler: {model.error}</p>
|
||||
)}
|
||||
{model.hint && <p className="fin-hint fin-form-notice">{model.hint}</p>}
|
||||
{message && (
|
||||
<p className={`fin-hint fin-form-notice${message.tone === 'warn' ? ' fin-hint-warn' : ''}`} role={message.tone === 'warn' ? 'alert' : 'status'}>
|
||||
{message.text}
|
||||
</p>
|
||||
)}
|
||||
{model.error && <p {...noticeProps('warn', 'fin-notice')}>Fehler: {model.error}</p>}
|
||||
{model.hint && <p {...noticeProps('info', 'fin-notice')}>{model.hint}</p>}
|
||||
{message && <p {...noticeProps(message.tone, 'fin-notice')}>{message.text}</p>}
|
||||
<div className="fin-form-actions">
|
||||
{model.canRefresh && (
|
||||
<Button variant="primary" onClick={refresh} disabled={busy !== null}>
|
||||
<FaRotate className={busy === 'refresh' ? 'fin-spin' : undefined} aria-hidden="true" /> {busy === 'refresh' ? 'Rufe ab …' : 'Jetzt abrufen'}
|
||||
{busy === 'refresh' ? 'Rufe ab …' : 'Jetzt abrufen'}
|
||||
</Button>
|
||||
)}
|
||||
<Button variant={model.canRefresh ? 'ghost' : 'primary'} onClick={connect} disabled={busy !== null}>
|
||||
<Button variant={model.canRefresh ? 'default' : 'primary'} onClick={connect} disabled={busy !== null}>
|
||||
{busy === 'connect' ? 'Leite weiter …' : model.connectLabel}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { FaPlus, FaSpinner } from 'react-icons/fa6'
|
||||
import { Badge, Button, Card, EmptyState } from '../ui'
|
||||
import FormField from './FormField'
|
||||
import { useFinanceResource } from '../../hooks/useFinanceResource'
|
||||
import { noticeProps } from '../../lib/financeUi'
|
||||
import { createFixedCost, deleteFixedCost, getFixedCosts, updateFixedCost } from '../../lib/financeApi'
|
||||
import {
|
||||
FIXED_COST_DELETE_CONFIRM,
|
||||
@@ -146,45 +146,44 @@ export default function FixedCostsCard() {
|
||||
let body
|
||||
if (!res.data) {
|
||||
body = res.loading ? (
|
||||
<div className="empty" role="status">
|
||||
<FaSpinner className="spinner" aria-hidden="true" />
|
||||
<p className="muted">Fixkosten werden geladen …</p>
|
||||
</div>
|
||||
<div className="loading" role="status">Fixkosten werden geladen …</div>
|
||||
) : (
|
||||
<div className="fin-load-error" style={{ padding: 0 }}>
|
||||
<p className="fin-hint fin-hint-warn" role="alert">{res.error || LOAD_ERROR}</p>
|
||||
<Button size="sm" onClick={res.reload}>Erneut versuchen</Button>
|
||||
<div className="fin-load-error">
|
||||
<p {...noticeProps('warn')}>{res.error || LOAD_ERROR}</p>
|
||||
<Button onClick={res.reload}>Erneut versuchen</Button>
|
||||
</div>
|
||||
)
|
||||
} else {
|
||||
body = (
|
||||
<>
|
||||
{res.error && (
|
||||
<p className="fin-hint fin-hint-warn" role="alert">
|
||||
<p {...noticeProps('warn', 'fin-notice')}>
|
||||
Die Liste konnte nicht neu geladen werden: {res.error}{' '}
|
||||
<Button size="sm" variant="ghost" onClick={res.reload} disabled={res.loading}>Erneut versuchen</Button>
|
||||
<button type="button" className="link" onClick={res.reload} disabled={res.loading}>Erneut versuchen</button>
|
||||
</p>
|
||||
)}
|
||||
{editing && (
|
||||
<form className="fin-fixed-form" onSubmit={submit} noValidate aria-labelledby="fin-fix-form-title">
|
||||
<h4 id="fin-fix-form-title" className="fin-fixed-form-title">{editing.id ? 'Position bearbeiten' : 'Neue Position'}</h4>
|
||||
<h3 id="fin-fix-form-title" className="fin-form-title">{editing.id ? 'Position bearbeiten' : 'Neue Position'}</h3>
|
||||
<fieldset className="fin-fieldset" disabled={busy === 'save'}>
|
||||
<div className="fin-form-grid">
|
||||
<div className="field-row">
|
||||
<FormField id={fieldId('name')} label="Name" error={errors.name}>
|
||||
<input className="form-control" autoComplete="off" value={form.name} onChange={change('name')} placeholder="z. B. Hetzner" />
|
||||
<input className="input" autoComplete="off" value={form.name} onChange={change('name')} placeholder="z. B. Hetzner" />
|
||||
</FormField>
|
||||
<FormField id={fieldId('amount')} label="Betrag (€)" error={errors.amount}>
|
||||
<input className="form-control fin-input-amount" inputMode="decimal" autoComplete="off" value={form.amount} onChange={change('amount')} placeholder="z. B. 49,90" />
|
||||
<input className="input fin-input-amount" inputMode="decimal" autoComplete="off" value={form.amount} onChange={change('amount')} placeholder="z. B. 49,90" />
|
||||
</FormField>
|
||||
</div>
|
||||
<div className="field-row">
|
||||
<FormField id={fieldId('interval')} label="Rhythmus" error={errors.interval}>
|
||||
<select className="form-control" value={form.interval} onChange={change('interval')}>
|
||||
<select className="select" value={form.interval} onChange={change('interval')}>
|
||||
{INTERVAL_OPTIONS.map((o) => (
|
||||
<option key={o.value} value={o.value}>{o.label}</option>
|
||||
))}
|
||||
</select>
|
||||
</FormField>
|
||||
<FormField id={fieldId('nextDate')} label="Nächster Termin" error={errors.nextDate}>
|
||||
<input className="form-control" type="date" value={form.nextDate} onChange={change('nextDate')} />
|
||||
<input className="input" type="date" value={form.nextDate} onChange={change('nextDate')} />
|
||||
</FormField>
|
||||
</div>
|
||||
<FormField
|
||||
@@ -193,22 +192,18 @@ export default function FixedCostsCard() {
|
||||
hint={`Teil des Namens der Gegenpartei in der Bankbuchung, z. B. hetzner (höchstens ${SETTINGS_LIMITS.maxMatch} Zeichen). Damit schlägt die Seite diese Abbuchung nicht noch einmal als neue Fixkosten vor.`}
|
||||
error={errors.counterpartyMatch}
|
||||
>
|
||||
<input className="form-control" autoComplete="off" value={form.counterpartyMatch} onChange={change('counterpartyMatch')} />
|
||||
<input className="input" autoComplete="off" value={form.counterpartyMatch} onChange={change('counterpartyMatch')} />
|
||||
</FormField>
|
||||
</fieldset>
|
||||
<div className="fin-form-actions">
|
||||
<Button type="submit" variant="primary" disabled={busy !== null}>
|
||||
{busy === 'save' ? 'Speichere …' : 'Speichern'}
|
||||
</Button>
|
||||
<Button type="button" variant="ghost" onClick={closeForm} disabled={busy !== null}>Abbrechen</Button>
|
||||
<Button type="button" onClick={closeForm} disabled={busy !== null}>Abbrechen</Button>
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
{message && (
|
||||
<p className={`fin-hint fin-form-notice${message.tone === 'warn' ? ' fin-hint-warn' : ''}`} role={message.tone === 'warn' ? 'alert' : 'status'}>
|
||||
{message.text}
|
||||
</p>
|
||||
)}
|
||||
{message && <p {...noticeProps(message.tone, 'fin-notice')}>{message.text}</p>}
|
||||
{rows.length === 0 ? (
|
||||
<EmptyState title="Noch keine Fixkosten." hint="Lege regelmäßige Ausgaben an, damit Vorschau und „Verfügbar“ sie berücksichtigen." />
|
||||
) : (
|
||||
@@ -222,13 +217,13 @@ export default function FixedCostsCard() {
|
||||
<div className="fin-amount fin-fixed-amount">{row.amountText}</div>
|
||||
<div className="fin-fixed-meta muted">{row.metaText}</div>
|
||||
<div className="fin-actions fin-fixed-actions">
|
||||
<Button size="sm" variant="ghost" onClick={() => openEdit(row)} disabled={busy !== null} aria-label={`Bearbeiten: ${row.name}`}>
|
||||
<button type="button" className="link" onClick={() => openEdit(row)} disabled={busy !== null} aria-label={`Bearbeiten: ${row.name}`}>
|
||||
Bearbeiten
|
||||
</Button>
|
||||
<Button size="sm" variant="ghost" onClick={() => toggle(row)} disabled={busy !== null} aria-label={`${row.toggleLabel}: ${row.name}`}>
|
||||
</button>
|
||||
<button type="button" className="link" onClick={() => toggle(row)} disabled={busy !== null} aria-label={`${row.toggleLabel}: ${row.name}`}>
|
||||
{busy === `${row.id}:toggle` ? 'Speichere …' : row.toggleLabel}
|
||||
</Button>
|
||||
<Button size="sm" variant="danger" onClick={() => remove(row)} disabled={busy !== null} aria-label={`Löschen: ${row.name}`}>
|
||||
</button>
|
||||
<Button type="button" variant="danger" onClick={() => remove(row)} disabled={busy !== null} aria-label={`Löschen: ${row.name}`}>
|
||||
{busy === `${row.id}:delete` ? 'Lösche …' : 'Löschen'}
|
||||
</Button>
|
||||
</div>
|
||||
@@ -245,8 +240,8 @@ export default function FixedCostsCard() {
|
||||
title="Fixkosten"
|
||||
actions={
|
||||
res.data && (
|
||||
<Button size="sm" variant="primary" onClick={openNew} disabled={busy !== null || editing?.id === null}>
|
||||
<FaPlus aria-hidden="true" /> Neue Position
|
||||
<Button onClick={openNew} disabled={busy !== null || editing?.id === null}>
|
||||
Neue Position
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { cloneElement } from 'react'
|
||||
|
||||
/**
|
||||
* Formularfeld mit Beschriftung, Hinweis und Fehlermeldung. `children` ist genau ein Eingabeelement; es bekommt
|
||||
* `id`, `aria-invalid` und `aria-describedby` (Hinweis und Fehler). Die Fehlermeldung wird vorgelesen (role="alert").
|
||||
* Formularfeld mit Beschriftung, Hinweis und Fehlermeldung (Klassen des Bausteins `Field`: `.field`, `.hint`, `.msg.err`).
|
||||
* `children` ist genau ein Eingabeelement; es bekommt `id`, `aria-invalid` und `aria-describedby` (Hinweis und Fehler).
|
||||
* Den roten Ring am Feld liefert allein `aria-invalid`. Die Fehlermeldung wird vorgelesen (role="alert").
|
||||
*/
|
||||
export default function FormField({ id, label, hint, error, children }) {
|
||||
const hintId = hint ? `${id}-hint` : undefined
|
||||
@@ -13,12 +14,12 @@ export default function FormField({ id, label, hint, error, children }) {
|
||||
<label htmlFor={id}>{label}</label>
|
||||
{cloneElement(children, { id, 'aria-invalid': error ? 'true' : undefined, 'aria-describedby': describedBy })}
|
||||
{hint && (
|
||||
<div id={hintId} className="faint fin-form-hint">
|
||||
<div id={hintId} className="hint">
|
||||
{hint}
|
||||
</div>
|
||||
)}
|
||||
{error && (
|
||||
<div id={errorId} className="fin-field-error" role="alert">
|
||||
<div id={errorId} className="msg err" role="alert">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { FaFloppyDisk, FaSpinner } from 'react-icons/fa6'
|
||||
import { Button, Card } from '../ui'
|
||||
import FormField from './FormField'
|
||||
import { useFinanceResource } from '../../hooks/useFinanceResource'
|
||||
import { noticeProps } from '../../lib/financeUi'
|
||||
import { getSettings, patchSettings } from '../../lib/financeApi'
|
||||
import {
|
||||
BANK_DETAILS_EXAMPLE,
|
||||
@@ -29,14 +29,11 @@ export default function SettingsCard() {
|
||||
return (
|
||||
<Card title="Einstellungen">
|
||||
{res.loading ? (
|
||||
<div className="empty" role="status">
|
||||
<FaSpinner className="spinner" aria-hidden="true" />
|
||||
<p className="muted">Einstellungen werden geladen …</p>
|
||||
</div>
|
||||
<div className="loading" role="status">Einstellungen werden geladen …</div>
|
||||
) : (
|
||||
<div className="fin-load-error" style={{ padding: 0 }}>
|
||||
<p className="fin-hint fin-hint-warn" role="alert">{res.error || LOAD_ERROR}</p>
|
||||
<Button size="sm" onClick={res.reload}>Erneut versuchen</Button>
|
||||
<div className="fin-load-error">
|
||||
<p {...noticeProps('warn')}>{res.error || LOAD_ERROR}</p>
|
||||
<Button onClick={res.reload}>Erneut versuchen</Button>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
@@ -103,14 +100,14 @@ function SettingsForm({ initial }) {
|
||||
<form onSubmit={submit} noValidate>
|
||||
{/* Während des Speicherns gesperrt, damit die Antwort keine neuen Eingaben überschreibt */}
|
||||
<fieldset className="fin-fieldset" disabled={busy}>
|
||||
<div className="fin-form-grid">
|
||||
<div className="field-row">
|
||||
<FormField
|
||||
id={fieldId('minBuffer')}
|
||||
label="Mindestpuffer (€)"
|
||||
hint="Dieser Betrag bleibt bei „Verfügbar“ immer auf dem Konto. 0 ist möglich."
|
||||
error={errors.minBuffer}
|
||||
>
|
||||
<input className="form-control fin-input-amount" inputMode="decimal" autoComplete="off" value={form.minBuffer} onChange={change('minBuffer')} placeholder="z. B. 500,00" />
|
||||
<input className="input fin-input-amount" inputMode="decimal" autoComplete="off" value={form.minBuffer} onChange={change('minBuffer')} placeholder="z. B. 500,00" />
|
||||
</FormField>
|
||||
<FormField
|
||||
id={fieldId('taxReservePercent')}
|
||||
@@ -118,15 +115,17 @@ function SettingsForm({ initial }) {
|
||||
hint={`Anteil des Gewinns, der als Rücklage gilt (0 bis ${SETTINGS_LIMITS.maxPercent}).`}
|
||||
error={errors.taxReservePercent}
|
||||
>
|
||||
<input className="form-control fin-input-amount" inputMode="numeric" autoComplete="off" value={form.taxReservePercent} onChange={change('taxReservePercent')} placeholder="30" />
|
||||
<input className="input fin-input-amount" inputMode="numeric" autoComplete="off" value={form.taxReservePercent} onChange={change('taxReservePercent')} placeholder="30" />
|
||||
</FormField>
|
||||
</div>
|
||||
<div className="field-row">
|
||||
<FormField
|
||||
id={fieldId('previousYearRevenue')}
|
||||
label="Vorjahresumsatz (€)"
|
||||
hint="Für die Kleinunternehmer-Grenze. Leer lassen, wenn unbekannt."
|
||||
error={errors.previousYearRevenue}
|
||||
>
|
||||
<input className="form-control fin-input-amount" inputMode="decimal" autoComplete="off" value={form.previousYearRevenue} onChange={change('previousYearRevenue')} placeholder="leer = unbekannt" />
|
||||
<input className="input fin-input-amount" inputMode="decimal" autoComplete="off" value={form.previousYearRevenue} onChange={change('previousYearRevenue')} placeholder="leer = unbekannt" />
|
||||
</FormField>
|
||||
<FormField
|
||||
id={fieldId('estPrepayment')}
|
||||
@@ -134,7 +133,7 @@ function SettingsForm({ initial }) {
|
||||
hint="Betrag je Termin (10.03., 10.06., 10.09., 10.12.). 0 = keine Vorauszahlung."
|
||||
error={errors.estPrepayment}
|
||||
>
|
||||
<input className="form-control fin-input-amount" inputMode="decimal" autoComplete="off" value={form.estPrepayment} onChange={change('estPrepayment')} placeholder="0,00" />
|
||||
<input className="input fin-input-amount" inputMode="decimal" autoComplete="off" value={form.estPrepayment} onChange={change('estPrepayment')} placeholder="0,00" />
|
||||
</FormField>
|
||||
</div>
|
||||
|
||||
@@ -144,21 +143,17 @@ function SettingsForm({ initial }) {
|
||||
hint={`Steht im Fuß jeder Rechnung, z. B. „${BANK_DETAILS_EXAMPLE}“.`}
|
||||
error={errors.bankDetailsText}
|
||||
>
|
||||
<textarea className="form-control" rows={3} value={form.bankDetailsText} onChange={change('bankDetailsText')} />
|
||||
<textarea className="textarea" rows={3} value={form.bankDetailsText} onChange={change('bankDetailsText')} />
|
||||
</FormField>
|
||||
<div className={`fin-counter${tooLong ? ' fin-counter-over' : ''}`}>
|
||||
{length} / {SETTINGS_LIMITS.maxBankDetails} Zeichen
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
{notice && (
|
||||
<p className={`fin-hint fin-form-notice${notice.tone === 'warn' ? ' fin-hint-warn' : ''}`} role={notice.tone === 'warn' ? 'alert' : 'status'}>
|
||||
{notice.text}
|
||||
</p>
|
||||
)}
|
||||
{notice && <p {...noticeProps(notice.tone, 'fin-notice')}>{notice.text}</p>}
|
||||
<div className="fin-form-actions">
|
||||
<Button type="submit" variant="primary" disabled={busy}>
|
||||
{busy ? <FaSpinner className="spinner" aria-hidden="true" /> : <FaFloppyDisk aria-hidden="true" />} {busy ? 'Speichere …' : 'Speichern'}
|
||||
{busy ? 'Speichere …' : 'Speichern'}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { Link, useLocation, useNavigate } from 'react-router-dom'
|
||||
import { PageHeader, Card, Button } from '../components/ui'
|
||||
import { PageHeader, Button } from '../components/ui'
|
||||
import { completeBankAuth } from '../lib/financeApi'
|
||||
import { dateDe } from '../lib/financeFormat'
|
||||
import { parseCallback, successResult, failureResult, isResult } from '../lib/bankCallback'
|
||||
@@ -38,28 +38,25 @@ export default function BankCallbackPage() {
|
||||
if (result) {
|
||||
body = (
|
||||
<>
|
||||
<p role={result.ok ? 'status' : 'alert'} style={{ fontWeight: 600 }}>
|
||||
<p className={`msg ${result.ok ? 'ok' : 'err'} fin-result`} role={result.ok ? 'status' : 'alert'}>
|
||||
{result.ok
|
||||
? `Bankzugang erneuert, gültig bis ${dateDe(result.validUntil)}`
|
||||
: result.message}
|
||||
</p>
|
||||
<div style={{ marginTop: 16 }}>
|
||||
<div className="fin-form-actions">
|
||||
<Button as={Link} variant="primary" to="/finance">Zu den Finanzen</Button>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
} else if (params) {
|
||||
body = (
|
||||
<div className="text-center" role="status">
|
||||
<div className="spinner" />
|
||||
<p className="muted">Bankzugang wird eingerichtet …</p>
|
||||
</div>
|
||||
<div className="loading" role="status">Bankzugang wird eingerichtet …</div>
|
||||
)
|
||||
} else {
|
||||
body = (
|
||||
<>
|
||||
<p className="muted">Zu dieser Seite liegt keine Rückmeldung der Bank vor.</p>
|
||||
<div style={{ marginTop: 16 }}>
|
||||
<div className="fin-form-actions">
|
||||
<Button as={Link} variant="primary" to="/finance">Zu den Finanzen</Button>
|
||||
</div>
|
||||
</>
|
||||
@@ -69,7 +66,7 @@ export default function BankCallbackPage() {
|
||||
return (
|
||||
<div className="page">
|
||||
<PageHeader title="Bankzugang" />
|
||||
<Card>{body}</Card>
|
||||
{body}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
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'
|
||||
@@ -7,7 +6,7 @@ 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.
|
||||
* (Status, Abruf, neu verbinden) und die Fixkostenliste. Jeder Abschnitt lädt und speichert für sich.
|
||||
*/
|
||||
export default function FinanceSettingsPage() {
|
||||
return (
|
||||
@@ -16,8 +15,8 @@ export default function FinanceSettingsPage() {
|
||||
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 as={Link} to="/finance">
|
||||
← Zurück zu den Finanzen
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
|
||||
+21
-32
@@ -663,9 +663,7 @@ 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) */
|
||||
.fin-spin { animation: spin 1s linear infinite; }
|
||||
.fin-hint { margin: 0; font-size: 13px; color: var(--text-muted); }
|
||||
/* Uebergang: diese Regel faellt, sobald keine Datei die Klasse mehr benutzt (Hinweis im Zuordnen-Dialog) */
|
||||
.fin-hint-warn { color: var(--warn); }
|
||||
|
||||
/* Block 0: Statuszeile (ohne Kasten) */
|
||||
@@ -892,38 +890,29 @@ button.link-danger { background: none; border: 0; padding: 0; font: inherit; fon
|
||||
}
|
||||
|
||||
/* Finanzen: Einstellungen (Formular, Bankzugang, Fixkosten) */
|
||||
.fin-fieldset { border: 0; padding: 0; margin: 0; min-width: 0; }
|
||||
.fin-form-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0 var(--sp-4); }
|
||||
.fin-fieldset { display: grid; gap: 16px; border: 0; padding: 0; margin: 0; min-width: 0; }
|
||||
.fin-fieldset .field-row { gap: 16px; }
|
||||
.fin-form-field { min-width: 0; }
|
||||
.fin-form-hint { font-size: 12px; overflow-wrap: anywhere; }
|
||||
.fin-field-error { font-size: 13px; font-weight: 600; color: var(--danger); overflow-wrap: anywhere; }
|
||||
/* KOMPAT setzt .page .field auf 12 px Abstand unten; im Raster bestimmt allein der gap den Abstand */
|
||||
.page .fin-fieldset .field, .page .fin-fixed-form .field { margin-bottom: 0; }
|
||||
.fin-input-amount { text-align: right; font-variant-numeric: tabular-nums; }
|
||||
.fin-counter { margin: -6px 0 var(--sp-3); text-align: right; font-size: 12px; color: var(--text-muted); font-variant-numeric: tabular-nums; }
|
||||
.fin-counter-over { color: var(--danger); font-weight: 700; }
|
||||
.fin-form-notice { margin: 0 0 var(--sp-3); }
|
||||
.fin-form-actions { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-3); margin-top: var(--sp-3); }
|
||||
.fin-bank-facts { display: grid; gap: 6px; margin: 0 0 var(--sp-3); }
|
||||
.fin-bank-facts > div { display: grid; grid-template-columns: 170px minmax(0, 1fr); gap: var(--sp-3); align-items: baseline; }
|
||||
.fin-bank-facts dt { color: var(--text-muted); font-size: 13px; }
|
||||
.fin-bank-facts dd { margin: 0; overflow-wrap: anywhere; font-variant-numeric: tabular-nums; }
|
||||
.fin-fixed-form { margin-bottom: var(--sp-4); padding: var(--sp-4); border: 1px solid var(--border-strong); border-radius: var(--r-sm); }
|
||||
.fin-fixed-form-title { margin: 0 0 var(--sp-3); font-size: 15px; font-weight: 700; }
|
||||
.fin-fixed-list { list-style: none; margin: 0; padding: 0; }
|
||||
.fin-fixed {
|
||||
display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 4px var(--sp-4); align-items: baseline;
|
||||
padding: var(--sp-3) 0; border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.fin-fixed:last-child { border-bottom: none; }
|
||||
.fin-fixed-off .fin-fixed-name, .fin-fixed-off .fin-fixed-amount { color: var(--text-muted); }
|
||||
.fin-fixed-main { display: flex; flex-wrap: wrap; align-items: center; gap: 6px var(--sp-2); min-width: 0; }
|
||||
.fin-fixed-name { font-weight: 700; overflow-wrap: anywhere; }
|
||||
.fin-fixed-amount { font-weight: 700; }
|
||||
.fin-fixed-meta { grid-column: 1 / -1; display: flex; flex-wrap: wrap; gap: 2px var(--sp-4); font-size: 13px; overflow-wrap: anywhere; }
|
||||
.fin-counter { margin-top: -8px; text-align: right; font-size: 12px; color: var(--text-2); font-variant-numeric: tabular-nums; }
|
||||
.fin-counter-over { color: var(--danger); font-weight: 600; }
|
||||
.fin-form-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 12px; margin-top: 16px; }
|
||||
.fin-kv { margin: 0 0 16px; }
|
||||
.fin-result { font-size: 16px; line-height: 24px; font-weight: 600; }
|
||||
.fin-fixed-form { padding-bottom: 24px; margin-bottom: 8px; border-bottom: 1px solid var(--line); }
|
||||
.fin-form-title { margin: 0 0 16px; font-size: 14px; line-height: 20px; font-weight: 600; }
|
||||
.fin-fixed-list { list-style: none; margin: 0; padding: 0; border-top: 1px solid var(--line); }
|
||||
.fin-fixed-form ~ .fin-fixed-list { border-top: 0; } /* das offene Formular schliesst mit seiner eigenen Linie ab */
|
||||
.fin-fixed { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 4px 16px; align-items: baseline; padding: 12px 0; border-bottom: 1px solid var(--line); }
|
||||
.fin-fixed-off .fin-fixed-name, .fin-fixed-off .fin-fixed-amount { color: var(--text-2); }
|
||||
.fin-fixed-main { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 8px; min-width: 0; }
|
||||
.fin-fixed-name { font-weight: 500; overflow-wrap: anywhere; }
|
||||
.fin-fixed-amount { font-weight: 600; }
|
||||
.fin-fixed-meta { grid-column: 1 / -1; display: flex; flex-wrap: wrap; gap: 2px 16px; font-size: 13px; overflow-wrap: anywhere; }
|
||||
.fin-fixed-actions { grid-column: 1 / -1; padding-top: 4px; }
|
||||
@media (max-width: 640px) {
|
||||
.fin-form-grid { grid-template-columns: minmax(0, 1fr); }
|
||||
.fin-bank-facts > div { grid-template-columns: minmax(0, 1fr); gap: 2px; }
|
||||
}
|
||||
.fin-fixed-actions button:disabled, .fin-notice button:disabled { cursor: not-allowed; }
|
||||
|
||||
/* ===== ENDE FINANZEN ===== */
|
||||
|
||||
|
||||
Reference in new issue
Block a user