From b369a0edf9ab78cbbc7a77ebb0e60e00d301756f Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 6 Oct 2026 11:56:24 +0000 Subject: [PATCH] Design P6: Gutschrift-Detail zweispaltig mit Abschnitten und kv-Listen, Verhalten unveraendert MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Neuer Zustand busyAction (Speichert … / Sendet …) neben busy; Formularbloecke der Berechnung stehen in .form-stack (16 px) statt auf dem Altabstand .page .field. Co-Authored-By: Claude Sonnet 5.5 --- src/components/credits/CreditDetail.jsx | 296 +++++++++++++----------- src/styles/global.css | 4 + 2 files changed, 161 insertions(+), 139 deletions(-) diff --git a/src/components/credits/CreditDetail.jsx b/src/components/credits/CreditDetail.jsx index ef31d20..a83acc5 100644 --- a/src/components/credits/CreditDetail.jsx +++ b/src/components/credits/CreditDetail.jsx @@ -1,14 +1,11 @@ import { useCallback, useEffect, useRef, useState } from 'react' -import { FaSpinner, FaArrowUpRightFromSquare } from 'react-icons/fa6' -import { Card, Badge, Button } from '../ui' +import { Card, Badge, Button, Field } from '../ui' import KundenansichtButton from '../customer/KundenansichtButton' import { applyCredit, calcCredit, decideCredit, getCredit } from '../../lib/teamApi' -import { applyMessage, BILLING_MONTHS, calcLine, creditStatus, decisionConfirm, decisionError, decisionMessage, fmtBytes, historyLabel } from '../../lib/creditFormat' +import { applyMessage, BILLING_MONTHS, calcLine, checkGlyph, checkTone, creditStatus, decisionConfirm, decisionError, decisionMessage, fmtBytes, historyLabel } from '../../lib/creditFormat' import { fmtDate, fmtDateTime, formatCents, productLabelOf } from '../../lib/format' import { storageLabel } from '../../lib/storageFormat' -import '../../styles/admin-areas.css' -const CHECK_TONE = { ok: 'ok', warn: 'warn', info: 'info' } const staffOf = (by) => (by && by !== 'customer' && by !== 'system' ? ` · ${by}` : '') const toCents = (value) => Math.round(Number(value) * 100) @@ -24,6 +21,7 @@ export default function CreditDetail({ id, onClose, onChanged, onOpen }) { const [calcText, setCalcText] = useState('') const [actionError, setActionError] = useState('') const [busy, setBusy] = useState(false) + const [busyAction, setBusyAction] = useState('') const busyRef = useRef(false) const amountTouched = useRef(false) const calcTimer = useRef(null) @@ -85,6 +83,7 @@ export default function CreditDetail({ id, onClose, onChanged, onOpen }) { if (busyRef.current) return busyRef.current = true setBusy(true) + setBusyAction(action) try { const result = await decideCredit(id, { action, note: form.note.trim(), amountCents, checks: form.checks, ...figures(form) }) onChanged?.() @@ -94,6 +93,7 @@ export default function CreditDetail({ id, onClose, onChanged, onOpen }) { } finally { busyRef.current = false setBusy(false) + setBusyAction('') } } @@ -101,6 +101,7 @@ export default function CreditDetail({ id, onClose, onChanged, onOpen }) { if (busyRef.current) return busyRef.current = true setBusy(true) + setBusyAction('apply') try { const result = await applyCredit(id) onChanged?.() @@ -110,11 +111,12 @@ export default function CreditDetail({ id, onClose, onChanged, onOpen }) { } finally { busyRef.current = false setBusy(false) + setBusyAction('') } } - if (error) return
{error}
- if (!d || !form) return + if (error) return

{error}

+ if (!d || !form) return
Wird geladen …
const r = d.request const c = d.customer @@ -124,160 +126,176 @@ export default function CreditDetail({ id, onClose, onChanged, onOpen }) { const fileNames = new Map(r.files.map((f) => [f.id, f.name])) const allChecked = Object.values(form.checks).every(Boolean) const canApprove = allChecked && Number(form.amount) > 0 && !busy + const errorLine = actionError ?

{actionError}

: null return ( - -
+ Schließen}> +

eingegangen {fmtDateTime(r.createdAt)} {st.label}

+ {message &&

{message}

} +
-

Antrag {r.ref} · {c.name || c.companyName}

- eingegangen {fmtDateTime(r.createdAt)} {st.label} -
- -
- {message &&
{message}
} -
-
-

Antrag

-
-
Leistung
{r.services}
-
Bisher bei
{r.provider}
-
Domain
{r.domain ? {r.domain} : '–'}
-
Rechnung
{formatCents(r.invoiceAmountCents)} für {r.billingMonths} Monat{r.billingMonths === 1 ? '' : 'e'}
-
Bezahlt bis
{fmtDate(r.paidUntil)}
-
Antwort an
{r.email}
-
Bestätigt
Angaben und Bedingungen (Stand {r.conditionsVersion || '–'}) am {fmtDateTime(r.consentAt)}
-
-

Grund / Nachricht

-

{r.message}

-

Nachweise

- {r.files.length ? ( -
    - {r.files.map((f) => ( -
  • - {f.name} ({fmtBytes(f.size)}, {fmtDate(f.uploadedAt)}) -
  • - ))} -
- ) :

Keine Nachweise (mehr) vorhanden.

} -

Verlauf

- {r.history.map((h, i) => ( -
- {historyLabel(h.type)} {fmtDateTime(h.at)}{staffOf(h.by)} - {h.text &&
{h.text}
} - {(h.files || []).length > 0 &&
Dateien: {h.files.map((fid) => fileNames.get(fid) || 'gelöscht').join(', ')}
} -
- ))} -
-
-

Kunde & Vertrag

-
-
Kunde
{c.name || c.companyName}{c.code ? ` · Kd.-Nr. ${c.code}` : ''}
-
E-Mail
{c.email || '–'}
-
Abo
-
- {s ? <>{productLabelOf(s.productKey)} {s.status}
Zusatzspeicher: {storageLabel(s.storageUnits)} : kein Abo} -
- {s && ( - <> -
Leistungsbeginn
{fmtDate(s.serviceStart)}
-
Nächste Abbuchung
{s.active ? fmtDate(s.currentPeriodEnd) : '–'}
- {s.cancelAt && <>
Gekündigt zum
{fmtDate(s.cancelAt)}
} -
Stripe-Guthaben
{s.creditBalanceCents == null ? '–' : formatCents(s.creditBalanceCents)}
- - )} -
-
- - {s?.stripeUrl && } -
- {d.otherRequests.length > 0 && ( - <> -

Weitere Anträge des Kunden

-
    - {d.otherRequests.map((o) => { - const ost = creditStatus(o.status) - return ( -
  • - { e.preventDefault(); onOpen?.(o.id) }}>{o.ref} · {o.services} · {fmtDate(o.createdAt)} {ost.label}{o.amountCents ? ` ${formatCents(o.amountCents)}` : ''} -
  • - ) - })} -
- - )} -

Automatische Prüfung

-
    - {d.autoChecks.map((k) => ( -
  • {k.status === 'ok' ? '✓' : k.status === 'warn' ? '!' : 'i'} {k.label}{k.detail && <>
    {k.detail}}
  • + +
    +
    Leistung
    {r.services}
    +
    Bisher bei
    {r.provider}
    +
    Domain
    {r.domain ? {r.domain} : '–'}
    +
    Rechnung
    {formatCents(r.invoiceAmountCents)} für {r.billingMonths} Monat{r.billingMonths === 1 ? '' : 'e'}
    +
    Bezahlt bis
    {fmtDate(r.paidUntil)}
    +
    Antwort an
    {r.email}
    +
    Bestätigt
    Angaben und Bedingungen (Stand {r.conditionsVersion || '–'}) am {fmtDateTime(r.consentAt)}
    +
    +
    + +

    {r.message}

    +
    + + {r.files.length ? ( + r.files.map((f) => ( +
    +
    + {f.name} ({fmtBytes(f.size)}, {fmtDate(f.uploadedAt)}) +
    +
    + )) + ) :

    Keine Nachweise (mehr) vorhanden.

    } +
    + + {r.history.map((h, i) => ( +
    +
    + {historyLabel(h.type)} {fmtDateTime(h.at)}{staffOf(h.by)} + {h.text &&

    {h.text}

    } + {(h.files || []).length > 0 &&
    Dateien: {h.files.map((fid) => fileNames.get(fid) || 'gelöscht').join(', ')}
    } +
    +
    ))} -
- + +
+
+ +
+
Kunde
{c.name || c.companyName}{c.code ? ` · Kd.-Nr. ${c.code}` : ''}
+
E-Mail
{c.email || '–'}
+
Abo
+
+ {s ? <>{productLabelOf(s.productKey)} {s.status}
Zusatzspeicher: {storageLabel(s.storageUnits)}
: kein Abo} +
+ {s && ( + <> +
Leistungsbeginn
{fmtDate(s.serviceStart)}
+
Nächste Abbuchung
{s.active ? fmtDate(s.currentPeriodEnd) : '–'}
+ {s.cancelAt && <>
Gekündigt zum
{fmtDate(s.cancelAt)}
} +
Stripe-Guthaben
{s.creditBalanceCents == null ? '–' : formatCents(s.creditBalanceCents)}
+ + )} +
+
+ + {s?.stripeUrl && } +
+
+ {d.otherRequests.length > 0 && ( + + {d.otherRequests.map((o) => { + const ost = creditStatus(o.status) + return ( +
+
+ · {o.services} · {fmtDate(o.createdAt)} {ost.label}{o.amountCents ? ` ${formatCents(o.amountCents)}` : ''} +
+
+ ) + })} +
+ )} + + {d.autoChecks.map((k) => ( +
+
+ {checkGlyph(k.status)} {k.label}{k.detail &&
{k.detail}
} +
+
+ ))} +
{pending ? ( <> -

Prüfung durch dich

-
- {d.requiredChecks.map((k) => ( - - ))} -
-

Berechnung

-
- - - - -
-
{calcText}
-

Werte weichen von der Rechnung ab? Hier korrigieren – die Korrektur wird beim Genehmigen im Verlauf vermerkt. „Übernahme ab“ ist der Leistungsbeginn des Abos.

- -