Abo-Status und Protokoll ohne Karten und Icons, neuer Grundstil fuer admin-areas.css
Protokoll als Tabelle (Zeit, Mitarbeiter, Aktion) statt Liste mit festen Mindestbreiten, damit bei 390 px nichts mitten im Wort bricht. Abo-Status als dl.kv, Fehler als .msg.err. admin-areas.css: Haarlinien statt Kaesten, Kopfzeilen ohne Grossbuchstaben, Links ohne eigene Regel, Responsive-Bloecke am Dateiende. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
8c34e932c1
commit
b24121e34f
3 files changed
+52
-51
No files matched your search
@@ -1,10 +1,8 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { FaSpinner } from 'react-icons/fa6'
|
||||
import { Card, Badge } from '../ui'
|
||||
import { getCustomerAboStatus } from '../../lib/teamApi'
|
||||
import { fmtDate, productLabelOf } from '../../lib/format'
|
||||
import { storageLabel } from '../../lib/storageFormat'
|
||||
import '../../styles/admin-areas.css'
|
||||
|
||||
// Abo-Status eines Kunden (Reiter "Abo & Angebote"): Appwrite-Spiegel des Stripe-Abos, fuer alle Mitarbeiter
|
||||
export default function AboStatusCard({ customerId }) {
|
||||
@@ -23,13 +21,13 @@ export default function AboStatusCard({ customerId }) {
|
||||
|
||||
return (
|
||||
<Card title="Abo">
|
||||
{error && <div className="area-error">{error}</div>}
|
||||
{!abo && !error && <FaSpinner className="spinner" />}
|
||||
{abo && !abo.status && <span className="area-muted">kein Abo</span>}
|
||||
{error && <div className="msg err" role="alert">{error}</div>}
|
||||
{!abo && !error && <div className="loading">Wird geladen …</div>}
|
||||
{abo && !abo.status && <p className="muted">kein Abo</p>}
|
||||
{abo && abo.status && (
|
||||
<dl className="area-kv">
|
||||
<dl className="kv">
|
||||
<dt>Abo</dt>
|
||||
<dd className="area-inline">
|
||||
<dd className="flex items-center gap-2 wrap">
|
||||
{productLabelOf(abo.productKey)}
|
||||
<Badge tone={abo.active ? 'ok' : 'danger'}>{abo.active ? 'aktiv' : abo.status}</Badge>
|
||||
</dd>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { FaSpinner, FaRotateRight } from 'react-icons/fa6'
|
||||
import { Card, Button, EmptyState } from '../ui'
|
||||
import { Button, EmptyState } from '../ui'
|
||||
import { getCustomerViewLog } from '../../lib/teamApi'
|
||||
import { formatViewLogTime, viewLogLabel } from '../../lib/viewLog'
|
||||
|
||||
@@ -24,33 +23,38 @@ export default function CustomerViewLog({ customerId }) {
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<Card>
|
||||
<div className="flex items-center gap-2 wrap">
|
||||
<span className="muted">{error}</span>
|
||||
<Button variant="ghost" onClick={load}><FaRotateRight /> Erneut versuchen</Button>
|
||||
</div>
|
||||
</Card>
|
||||
<div className="flex items-center gap-2 wrap">
|
||||
<div className="msg err" role="alert">{error}</div>
|
||||
<Button onClick={load}>Erneut versuchen</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
if (!entries) return <Card><FaSpinner className="spinner" /></Card>
|
||||
if (!entries) return <div className="loading">Wird geladen …</div>
|
||||
if (entries.length === 0) {
|
||||
return <Card><EmptyState title="Noch keine Einträge" hint="Hier steht, wer die Kundenansicht geöffnet oder darin etwas geändert hat." /></Card>
|
||||
return <EmptyState title="Noch keine Einträge" hint="Hier steht, wer die Kundenansicht geöffnet oder darin etwas geändert hat." />
|
||||
}
|
||||
|
||||
return (
|
||||
<Card pad={false}>
|
||||
<div className="list">
|
||||
<table className="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Zeit</th>
|
||||
<th>Mitarbeiter</th>
|
||||
<th>Aktion</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{entries.map((e) => (
|
||||
<div key={e.id} className="list-row">
|
||||
<span className="mono muted list-row-text" style={{ minWidth: 130 }}>{formatViewLogTime(e.at)}</span>
|
||||
<span className="list-row-text" style={{ minWidth: 120, fontWeight: 600 }}>{e.staffName || '–'}</span>
|
||||
<div className="grow list-row-text">
|
||||
<tr key={e.id}>
|
||||
<td className="nowrap num">{formatViewLogTime(e.at)}</td>
|
||||
<td>{e.staffName || '–'}</td>
|
||||
<td>
|
||||
<div>{viewLogLabel(e.action)}</div>
|
||||
{e.detail && <div className="muted">{e.detail}</div>}
|
||||
</div>
|
||||
</div>
|
||||
{e.detail && <div className="cell-sub">{e.detail}</div>}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
</tbody>
|
||||
</table>
|
||||
)
|
||||
}
|
||||
+22
-23
@@ -1,34 +1,27 @@
|
||||
/* Bereiche aus dem frueheren Admin-Panel (Gutschriften, Abo-Angebote, E-Mail-Speicher, Abo-Status).
|
||||
Zeilen als Raster; unter 720 px untereinander, damit nichts waagerecht scrollt. */
|
||||
/* Bereiche aus dem frueheren Admin-Panel (Gutschriften, Abo-Angebote, E-Mail-Speicher).
|
||||
Zeilen als Raster mit Haarlinien; unter 860 px untereinander, damit nichts waagerecht scrollt. P6 ersetzt sie durch Tabellen. */
|
||||
.area-rows { display: flex; flex-direction: column; container-type: inline-size; }
|
||||
.area-row {
|
||||
display: grid; gap: var(--sp-3); align-items: start; padding: 12px 16px;
|
||||
border-bottom: 1px solid var(--border); overflow-wrap: anywhere;
|
||||
display: grid; gap: var(--sp-3); align-items: start; padding: 12px 0;
|
||||
border-bottom: 1px solid var(--line); overflow-wrap: anywhere;
|
||||
}
|
||||
.area-row:last-child { border-bottom: none; }
|
||||
.area-row.head { color: var(--text-muted); font-size: 12px; text-transform: uppercase; letter-spacing: 0.5px; }
|
||||
.area-row.active { background: var(--accent-soft); }
|
||||
.area-muted { color: var(--text-muted); font-size: 13px; }
|
||||
.area-row.head { color: var(--text-2); font-size: 12px; font-weight: 500; }
|
||||
.area-row.active { background: var(--bg-subtle); }
|
||||
.area-muted { color: var(--text-2); font-size: 13px; }
|
||||
.area-danger { color: var(--red-fg); font-size: 13px; }
|
||||
.area-warn { color: var(--orange-fg); font-size: 13px; }
|
||||
.area-summary { padding: 12px 16px; border-radius: var(--r-sm); background: var(--surface-2); margin-bottom: var(--sp-3); }
|
||||
.area-summary.warn { background: var(--orange-bg); color: var(--orange-fg); }
|
||||
.area-message { padding: 10px 14px; border-radius: var(--r-sm); background: var(--accent-soft); margin-bottom: var(--sp-3); }
|
||||
.area-error { padding: 10px 14px; border-radius: var(--r-sm); background: var(--red-bg); color: var(--red-fg); margin: var(--sp-2) 0; }
|
||||
.area-summary { margin-bottom: 12px; }
|
||||
.area-summary.warn { color: var(--orange-fg); }
|
||||
.area-message, .area-error { font-size: 13px; margin: 8px 0; }
|
||||
.area-message { color: var(--green-fg); }
|
||||
.area-error { color: var(--red-fg); }
|
||||
.area-kv { display: grid; grid-template-columns: max-content minmax(0, 1fr); gap: 6px 16px; margin: 0; }
|
||||
.area-kv dt { color: var(--text-muted); }
|
||||
.area-kv dt { color: var(--text-2); }
|
||||
.area-kv dd { margin: 0; overflow-wrap: anywhere; }
|
||||
.area-filter { display: inline-flex; gap: 4px; margin-bottom: var(--sp-3); }
|
||||
.area-inline { display: flex; flex-wrap: wrap; gap: var(--sp-2); align-items: center; }
|
||||
.storage-row { grid-template-columns: 150px 220px minmax(0, 1fr) 150px 110px; }
|
||||
@media (max-width: 720px) {
|
||||
.area-rows .area-row { grid-template-columns: minmax(0, 1fr); }
|
||||
.area-row.head { display: none; }
|
||||
.area-kv { grid-template-columns: minmax(0, 1fr); }
|
||||
.area-kv dt { margin-top: 6px; }
|
||||
}
|
||||
.area-rows a { color: var(--text); text-decoration: none; }
|
||||
.area-rows a:hover { color: var(--accent); text-decoration: underline; }
|
||||
.credit-row { grid-template-columns: 130px minmax(0, 1fr) minmax(0, 1fr) 110px 100px 150px 100px; }
|
||||
.credit-cols { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: var(--sp-5); }
|
||||
.credit-cols h4 { margin: var(--sp-4) 0 var(--sp-2); font-size: 14px; }
|
||||
@@ -37,11 +30,17 @@
|
||||
.credit-history { margin-bottom: 8px; }
|
||||
.credit-manual { display: flex; flex-direction: column; gap: 6px; }
|
||||
.credit-calc { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--sp-2) var(--sp-3); margin-bottom: var(--sp-2); }
|
||||
.credit-calc label, .credit-field { display: flex; flex-direction: column; gap: 4px; font-size: 13px; color: var(--text-muted); margin-bottom: var(--sp-2); }
|
||||
.credit-calc label, .credit-field { display: flex; flex-direction: column; gap: 4px; font-size: 13px; color: var(--text-2); margin-bottom: var(--sp-2); }
|
||||
.offer-row { grid-template-columns: 130px 200px minmax(0, 1fr) 110px 210px; }
|
||||
@media (max-width: 900px) { .credit-cols { grid-template-columns: minmax(0, 1fr); } }
|
||||
@media (max-width: 720px) { .credit-calc { grid-template-columns: minmax(0, 1fr); } }
|
||||
.credit-cols a { color: var(--accent); }
|
||||
.offer-row { grid-template-columns: 130px 200px minmax(0, 1fr) 110px 210px; }
|
||||
/* Responsive: hinter allen Spaltenregeln, damit bei gleicher Spezifitaet die spaetere Regel gewinnt (ohne Vorrangmarker) */
|
||||
@media (max-width: 860px) {
|
||||
.area-rows .area-row { grid-template-columns: minmax(0, 1fr); }
|
||||
.area-row.head { display: none; }
|
||||
.area-kv { grid-template-columns: minmax(0, 1fr); }
|
||||
.area-kv dt { margin-top: 6px; }
|
||||
}
|
||||
/* Nach verfuegbarer Breite stapeln (nicht nach Fensterbreite): mit ausgeklappter Sidebar sind Tablet quer oder ein
|
||||
halbes Laptop-Fenster sonst zu schmal fuer die Spalten */
|
||||
@container (max-width: 960px) {
|
||||
|
||||
Reference in new issue
Block a user