diff --git a/src/components/offers/OfferList.jsx b/src/components/offers/OfferList.jsx index 9154d57..4b6ea5d 100644 --- a/src/components/offers/OfferList.jsx +++ b/src/components/offers/OfferList.jsx @@ -1,12 +1,10 @@ import { useEffect, useState } from 'react' -import { FaSpinner } from 'react-icons/fa6' -import { Card, Badge, Button } from '../ui' +import { Badge, Button, EmptyState, Tabs } from '../ui' import { listOffers, notifyOffer, withdrawOffer } from '../../lib/teamApi' -import { discountLine, offerStatus } from '../../lib/offerFormat' +import { discountLine, filterOffers, offerMailLine, offerRowActions, offerStatus } from '../../lib/offerFormat' import { fmtDate, fmtDateTime, formatCents, productLabelOf } from '../../lib/format' -import '../../styles/admin-areas.css' -// Liste der Abo-Angebote (Filter "Offen & aktiv"/"Alle") wie im frueheren Admin-Panel; optional nur ein Kunde +// Liste der Abo-Angebote (Reiter "Offen & aktiv"/"Alle") als Tabelle; optional nur ein Kunde export default function OfferList({ customerId, reloadKey, onMessage, onChanged }) { const [filter, setFilter] = useState('open') const [offers, setOffers] = useState(null) @@ -51,69 +49,75 @@ export default function OfferList({ customerId, reloadKey, onMessage, onChanged } } - const shown = (offers || []).filter((o) => filter === 'all' || ['offen', 'aktiv'].includes(o.status)) + const shown = filterOffers(offers, filter) return (
-
- - -
- - {error &&
{error}
} - {!offers && !error &&
} - {offers && ( -
-
AngelegtKundeAngebotGültig bisStatus
- {shown.length === 0 &&
{filter === 'open' ? 'Keine offenen oder aktiven Angebote.' : 'Noch keine Angebote.'}
} + + {error &&
{error}
} + {!offers && !error &&
Wird geladen …
} + {offers && shown.length === 0 && ( + + )} + {offers && shown.length > 0 && ( + + + + + + + + + + + {shown.map((o) => { const st = offerStatus(o.status) const discount = discountLine(o) const sub = o.status === 'aktiv' && o.appliedAt ? `seit ${fmtDate(o.appliedAt)}` : o.status === 'offen' ? 'wartet auf Abschluss' : '' + const mail = offerMailLine(o) + const { sendLabel, canWithdraw } = offerRowActions(o) return ( -
- {fmtDateTime(o.createdAt)}
{o.ref}{o.createdBy ? ` · ${o.createdBy.replace(/\s*<.*>$/, '')}` : ''}
- - {o.customerName} - {o.customerEmail && <>
{o.customerEmail}} - {o.customerCode && <>
Kd.-Nr. {o.customerCode}} -
- - {productLabelOf(o.productKey)}
{o.text} -
Ersparnis {formatCents(o.savingsCents)}{o.note ? ` · „${o.note}“` : ''} - {discount && <>
{discount}} - {o.internalNote && <>
intern: {o.internalNote}} -
- {o.scope === 'laufend' ? laufendes Abo : fmtDate(o.validUntil)} - +
+ + + + + + ) })} - - )} - + +
AngelegtKundeAngebotGültig bisStatus
+ {fmtDateTime(o.createdAt)} +
{o.ref}{o.createdBy ? ` · ${o.createdBy.replace(/\s*<.*>$/, '')}` : ''}
+
+
{o.customerName}
+ {o.customerEmail &&
{o.customerEmail}
} + {o.customerCode &&
Kd.-Nr. {o.customerCode}
} +
+
{productLabelOf(o.productKey)}
+ {o.text} +
Ersparnis {formatCents(o.savingsCents)}{o.note ? ` · „${o.note}“` : ''}
+ {discount &&
{discount}
} + {o.internalNote &&
intern: {o.internalNote}
} +
{o.scope === 'laufend' ? laufendes Abo : fmtDate(o.validUntil)} {st.label} - {sub && <>
{sub}} -
Stripe:{' '} - {o.stripeCouponUrl ? Coupon : '–'} - {o.stripeSubscriptionUrl && <> · Abo} - {o.notifiedAt - ? <>
E-Mail gesendet {fmtDateTime(o.notifiedAt)} - : o.mailSendable - ? <>
E-Mail nicht verschickt - : o.status === 'offen' && !o.customerEmail ? <>
Keine E-Mail-Adresse hinterlegt : null} - {(['offen', 'abgelaufen'].includes(o.status) || o.mailSendable) && ( - - {o.mailSendable && ( - + {sub &&
{sub}
} +
+ Stripe: {o.stripeCouponUrl ? Coupon : '–'} + {o.stripeSubscriptionUrl && <> · Abo} +
+ {mail &&
{mail.tone === 'warn' ? {mail.text} : mail.text}
} + {(sendLabel || canWithdraw) && ( +
+ {sendLabel && ( + )} - {['offen', 'abgelaufen'].includes(o.status) && ( - - )} - + {canWithdraw && } +
)} -
- +
+ )}
) } diff --git a/src/components/offers/OffersView.jsx b/src/components/offers/OffersView.jsx index 18252cb..82fb12b 100644 --- a/src/components/offers/OffersView.jsx +++ b/src/components/offers/OffersView.jsx @@ -1,5 +1,4 @@ import { useState } from 'react' -import { FaPlus } from 'react-icons/fa6' import { Button } from '../ui' import OfferList from './OfferList' import OfferForm from './OfferForm' @@ -13,23 +12,19 @@ export default function OffersView({ customerId, customerName }) { return (
- {message &&
{message}
} - {!showForm && ( -
- -
- )} - {showForm && ( -
- { setMessage(text); setShowForm(false); reload() }} - onCancel={() => setShowForm(false)} - /> -
- )} + {message &&

{message}

} +
+ +
+ {showForm && ( + { setMessage(text); setShowForm(false); reload() }} + onCancel={() => setShowForm(false)} + /> + )}
) } diff --git a/src/pages/OffersPage.jsx b/src/pages/OffersPage.jsx index 1ccf6f8..03345b8 100644 --- a/src/pages/OffersPage.jsx +++ b/src/pages/OffersPage.jsx @@ -7,7 +7,7 @@ export default function OffersPage() {
-

Ein Angebot legt sofort einen einmal einlösbaren Coupon in Stripe an. Ohne laufendes Abo wird er beim Abschluss angewendet (und schaltet die Bestellung für diesen Kunden frei), bei laufendem Abo gilt er ab der nächsten Abbuchung. Nach den Angebotsmonaten gilt automatisch wieder der reguläre Preis.

+

Ein Angebot legt sofort einen einmal einlösbaren Coupon in Stripe an. Ohne laufendes Abo wird er beim Abschluss angewendet (und schaltet die Bestellung für diesen Kunden frei), bei laufendem Abo gilt er ab der nächsten Abbuchung. Nach den Angebotsmonaten gilt automatisch wieder der reguläre Preis.

) } diff --git a/src/styles/global.css b/src/styles/global.css index ed41cf9..e9cd89e 100644 --- a/src/styles/global.css +++ b/src/styles/global.css @@ -1216,6 +1216,10 @@ div.cell-actions { display: flex; flex-wrap: wrap; gap: 4px 16px; margin-top: 6p /* 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; } +/* Angebote: die Statusspalte (Badge, Stripe-Links, Mail-Zeile, Aktionen) wird nicht schmaler als ganze Woerter; ohne Mindestbreite */ +/* gibt ihr die Tabelle (td bricht ueberall) fast nichts ab und "Abschluss" oder "Zurueckziehen" brechen mitten im Wort. */ +.table-wrap th.col-status { min-width: 7rem; } +@media (min-width: 861px) { .table-wrap th.col-status { min-width: 12rem; } } /* ===== ENDE Kunden-Unterseiten (P6) ===== */ /* ===== KOMPAT (bis P10) ===== */