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}
Schließen
- if (!d || !form) return
+ if (error) return {error}
Schließen
+ 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}
-
-
Schließen
-
- {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 && In Stripe öffnen }
-
- {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
-
-
+
+
+
+
+
+ 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 && In Stripe öffnen }
+
+
+ {d.otherRequests.length > 0 && (
+
+ {d.otherRequests.map((o) => {
+ const ost = creditStatus(o.status)
+ return (
+
+
+ onOpen?.(o.id)}>{o.ref} · {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) => (
-
- setForm({ ...form, checks: { ...form.checks, [k.key]: e.target.checked } })} /> {k.label}
-
- ))}
-
-
Berechnung
-
- Rechnungsbetrag (€) setFigure('invoiceAmount', e.target.value)} />
- Rechnung gilt für
- setFigure('billingMonths', Number(e.target.value))}>
- {BILLING_MONTHS.map((m) => {m} Monat{m === 1 ? '' : 'e'} )}
-
-
- Bezahlt bis setFigure('paidUntil', e.target.value)} />
- Übernahme ab setFigure('takeoverFrom', e.target.value)} />
-
-
{calcText}
-
Werte weichen von der Rechnung ab? Hier korrigieren – die Korrektur wird beim Genehmigen im Verlauf vermerkt. „Übernahme ab“ ist der Leistungsbeginn des Abos.
-
Gutschrift (€)
- { amountTouched.current = true; setForm({ ...form, amount: e.target.value }) }} />
-
-
Nachricht an den Kunden (bei Rückfrage und Ablehnung Pflicht, beim Genehmigen optional)
-
-
- decide('approve')} disabled={!canApprove}>Genehmigen & gutschreiben
- decide('question')} disabled={busy}>Rückfrage senden
- decide('reject')} disabled={busy}>Ablehnen
- {busy && }
-
-
- Genehmigen geht, sobald alle Punkte abgehakt sind. Der Kunde bekommt jede Entscheidung per E-Mail.
- {!s?.active && Achtung: Der Kunde hat kein laufendes Abo. }
-
+
+
+ {d.requiredChecks.map((k) => (
+
+ setForm({ ...form, checks: { ...form.checks, [k.key]: e.target.checked } })} /> {k.label}
+
+ ))}
+
+
+
+
+
+
+ setFigure('invoiceAmount', e.target.value)} />
+
+
+ setFigure('billingMonths', Number(e.target.value))}>
+ {BILLING_MONTHS.map((m) => {m} Monat{m === 1 ? '' : 'e'} )}
+
+
+
+
+
+ setFigure('paidUntil', e.target.value)} />
+
+
+ setFigure('takeoverFrom', e.target.value)} />
+
+
+
+
{calcText}
+
Werte weichen von der Rechnung ab? Hier korrigieren – die Korrektur wird beim Genehmigen im Verlauf vermerkt. „Übernahme ab“ ist der Leistungsbeginn des Abos.
+
+
+ { amountTouched.current = true; setForm({ ...form, amount: e.target.value }) }} />
+
+
+
+
+ decide('approve')} disabled={!canApprove}>{busyAction === 'approve' ? 'Speichert …' : 'Genehmigen & gutschreiben'}
+ decide('question')} disabled={busy}>{busyAction === 'question' ? 'Sendet …' : 'Rückfrage senden'}
+ decide('reject')} disabled={busy}>{busyAction === 'reject' ? 'Speichert …' : 'Ablehnen'}
+
+
+ Genehmigen geht, sobald alle Punkte abgehakt sind. Der Kunde bekommt jede Entscheidung per E-Mail.
+ {!s?.active && Achtung: Der Kunde hat kein laufendes Abo. }
+
+ {errorLine}
+
+
>
) : (
- <>
-
Entscheidung
+
{r.status === 'genehmigt' ? (
<>
-
+
Genehmigt {fmtDateTime(r.decidedAt)} · {r.decidedBy}
Gutschrift {formatCents(r.amountCents)}
Berechnung {r.calculation}
Geprüft {r.checks ? `alle Punkte abgehakt (${r.checks.by || ''})` : '–'}
- {r.decisionNote && <>Nachricht {r.decisionNote} >}
+ {r.decisionNote && <>Nachricht {r.decisionNote} >}
Verrechnung
- {r.appliedAt ? <>Stripe-Guthaben gebucht am {fmtDateTime(r.appliedAt)} ({r.stripeBalanceTransactionId}) > : noch nicht in Stripe gebucht }
+ {r.appliedAt ? <>Stripe-Guthaben gebucht am {fmtDateTime(r.appliedAt)} ({r.stripeBalanceTransactionId}) > : noch nicht in Stripe gebucht }
- {!r.appliedAt && Jetzt in Stripe gutschreiben
}
+ {!r.appliedAt && {busyAction === 'apply' ? 'Speichert …' : 'Jetzt in Stripe gutschreiben'}
}
>
) : (
-
+
Abgelehnt {fmtDateTime(r.decidedAt)} · {r.decidedBy}
- Begründung {r.decisionNote}
+ Begründung {r.decisionNote}
)}
- >
+ {errorLine}
+
)}
- {actionError &&
{actionError}
}
diff --git a/src/styles/global.css b/src/styles/global.css
index b13e0fd..ed41cf9 100644
--- a/src/styles/global.css
+++ b/src/styles/global.css
@@ -1212,6 +1212,10 @@ div.cell-actions { display: flex; flex-wrap: wrap; gap: 4px 16px; margin-top: 6p
.dialog-foot .btn { white-space: normal; height: auto; min-height: 32px; padding-top: 6px; padding-bottom: 6px; text-align: center; }
/* deckt auch verschachtelte Felder, P7 prueft die Regel */
.autocomplete .input { width: 100%; }
+/* Formularbloecke in einer Karte (Gutschrift: Berechnung) stehen mit 16 px Abstand untereinander, wie im Dialog (.dialog-body). */
+/* Der Abstand kommt vom Container, nicht vom Altabstand .page .field (KOMPAT, faellt in P10); die zweite Regel nimmt ihn dort weg. */
+.form-stack { display: flex; flex-direction: column; gap: 16px; }
+.page .form-stack > .field { margin-bottom: 0; }
/* ===== ENDE Kunden-Unterseiten (P6) ===== */
/* ===== KOMPAT (bis P10) ===== */