Design P6: E-Mail-Speicher als Tabelle, Erledigt als Textlink mit data-storage-done

Kundenspalte mit Mindestbreite (col-name) und Erledigt/Bestellt ohne Umbruch, damit am Handy und bei 1440 px keine Woerter mitten im Wort brechen.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Sonnet 5.5 committed 2026-10-06 14:36:45 +00:00
1 parent 568911967d
commit 6eae3c4aae
2 files changed
+66 -46

No files matched your search

+63 -46
View File
@@ -1,11 +1,9 @@
import { useCallback, useEffect, useState } from 'react'
import { Link } from 'react-router-dom'
import { FaSpinner, FaRotateRight } from 'react-icons/fa6'
import { PageHeader, Card, Badge, Button } from '../components/ui'
import { PageHeader, Card, Badge, Button, EmptyState } from '../components/ui'
import { listStorageTasks, markStorageDone } from '../lib/teamApi'
import { fmtDate, fmtDateTime } from '../lib/format'
import { changeText, failedText, isOverdue, orderedKind, partialText, scopeText, summaryText, taskText } from '../lib/storageFormat'
import '../styles/admin-areas.css'
import { fmtDateTime } from '../lib/format'
import { changeText, failedText, orderedKind, partialText, scopeText, storageDue, summaryText, taskText } from '../lib/storageFormat'
// E-Mail-Speicher (Kunden -> E-Mail-Speicher): Zusatzspeicher-Aufgaben in konsoleH wie im frueheren Admin-Panel
export default function StorageTasksPage() {
@@ -39,57 +37,76 @@ export default function StorageTasksPage() {
<div className="page">
<PageHeader title="E-Mail-Speicher" subtitle="Zusatzspeicher der Abos in konsoleH einrichten und senken" />
{error && (
<div className="area-error area-inline">
{error} <Button size="sm" variant="ghost" onClick={load}><FaRotateRight /> Erneut versuchen</Button>
<div className="flex items-center gap-2 wrap mb-2">
<span className="msg err" role="alert">{error}</span>
<Button onClick={load}>Erneut versuchen</Button>
</div>
)}
{!data && !error && <FaSpinner className="spinner" />}
{!data && !error && <div className="loading">Wird geladen …</div>}
{data && (
<>
<div className={`area-summary ${summary.warn ? 'warn' : ''}`}>{summary.text}</div>
<Card pad={false}>
<div className="area-rows">
<div className="area-row storage-row head"><span>Fällig</span><span>Kunde</span><span>Aufgabe in konsoleH</span><span>Bestellt</span><span /></div>
{data.tasks.length === 0 && <div className="area-row"><span className="area-muted">Keine offenen Speicher-Aufgaben.</span></div>}
{data.tasks.map((t) => (
<div key={t.id} className="area-row storage-row">
<span>
{t.kind === 'einrichten'
? <><Badge tone="warn">jetzt einrichten</Badge><br /><span className="area-muted">bestellt {fmtDateTime(t.createdAt)}</span></>
: <><Badge tone={isOverdue(t) ? 'warn' : 'muted'}>zum Periodenende</Badge><br />{fmtDate(t.dueAt)}</>}
</span>
<span>
<Link to={`/customers/${t.customerId}`}><strong>{t.customerName}</strong></Link>
{t.customerCode && <><br /><span className="area-muted">Kd.-Nr. {t.customerCode}</span></>}
{t.email && <><br /><span className="area-muted">{t.email}</span></>}
<br /><span className="area-muted">{t.productLabel}</span>
</span>
<span>
<strong>{taskText(t)}</strong>
<br /><span className="area-muted">{scopeText(t)}</span>
<br /><span className="area-muted">{changeText(t)}</span>
{t.partial && <><br /><span className="area-danger">{partialText(t)}</span></>}
</span>
<span>{fmtDateTime(t.createdAt)}<br /><span className="area-muted">{orderedKind(t)}</span></span>
<span>
<Button size="sm" variant="primary" data-storage-done={t.id} onClick={() => done(t)} disabled={busyId === t.id}>
{busyId === t.id ? <FaSpinner className="spinner" /> : 'Erledigt'}
</Button>
</span>
</div>
))}
</div>
</Card>
<p className={summary.warn ? 'msg warn mb-2' : 'muted mb-2'}>{summary.text}</p>
{data.tasks.length === 0 ? (
<EmptyState title="Keine offenen Speicher-Aufgaben." />
) : (
<table className="table table-wrap">
<thead>
<tr>
<th className="hide-sm">Fällig</th>
<th className="col-name">Kunde</th>
<th>Aufgabe in konsoleH</th>
<th className="hide-md">Bestellt</th>
<th />
</tr>
</thead>
<tbody>
{data.tasks.map((t) => {
const due = storageDue(t)
return (
<tr key={t.id}>
<td className="hide-sm">
<Badge tone={due.tone}>{due.label}</Badge>
<div className={due.subMuted ? 'cell-sub' : undefined}>{due.sub}</div>
</td>
<td>
<div className="cell-title"><Link className="link" to={`/customers/${t.customerId}`}>{t.customerName}</Link></div>
{t.customerCode && <div className="cell-sub">Kd.-Nr. {t.customerCode}</div>}
{t.email && <div className="cell-sub">{t.email}</div>}
<div className="cell-sub">{t.productLabel}</div>
</td>
<td>
<div className="cell-title">{taskText(t)}</div>
<div className="cell-sub">{scopeText(t)}</div>
<div className="cell-sub">{changeText(t)}</div>
{t.partial && <div className="cell-sub"><span className="msg err">{partialText(t)}</span></div>}
</td>
<td className="hide-md nowrap">
{fmtDateTime(t.createdAt)}
<div className="cell-sub">{orderedKind(t)}</div>
</td>
<td className="nowrap">
<button type="button" className="link" data-storage-done={t.id} onClick={() => done(t)} disabled={busyId === t.id}>
{busyId === t.id ? 'Speichert …' : 'Erledigt'}
</button>
</td>
</tr>
)
})}
</tbody>
</table>
)}
{data.failed.length > 0 && (
<Card title="Gescheiterte Änderungen (letzte 30 Tage)">
<Card className="mt-3" title="Gescheiterte Änderungen (letzte 30 Tage)">
{data.failed.map((f) => (
<div key={f.id} style={{ marginBottom: 6 }}>
{fmtDateTime(f.createdAt)} · <strong>{f.customerName}</strong> · <span className="area-muted">{failedText(f)}</span>
<div key={f.id} className="list-row">
<div className="list-row-text">
{fmtDateTime(f.createdAt)} · <strong>{f.customerName}</strong> · <span className="muted">{failedText(f)}</span>
</div>
</div>
))}
</Card>
)}
<p className="area-muted" style={{ marginTop: 12 }}>
<p className="table-foot">
Jedes Abo bringt 5 GB E-Mail-Speicher mit, jedes Zusatzpaket 5 GB dazu. „Einrichten“: Postfachgrenze in konsoleH jetzt auf den Zielwert setzen. „Zum Periodenende“: erst am genannten Tag auf den Zielwert senken. Die Grenzen gelten für den Gesamtspeicher des Kunden (5 GB + 5 GB je Paket); er verteilt ihn nach seiner Vorgabe auf seine Postfächer (§ 3 Abs. 4). Angezeigt wird die Domain der Kontakt-Adresse, eine eigene Mail-Domain ist im Portal nicht gespeichert. „Vergeben“ zählt, was konsoleH gerade hält: eine Verringerung zählt erst nach „Erledigt“. Danach „Erledigt“ drücken – der Kunde hat seine Bestätigung schon bekommen.
</p>
</>
+3
View File
@@ -1220,6 +1220,9 @@ div.cell-actions { display: flex; flex-wrap: wrap; gap: 4px 16px; margin-top: 6p
/* 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; } }
/* E-Mail-Speicher: die Kundenspalte wird nicht schmaler als ein ganzer Name ("Physiotherapie"); die Aufgabespalte ist lang und */
/* nimmt sonst fast die ganze Breite, und td bricht ueberall: am Handy stuende "Physiothe-rapie" in 65 px. */
.table-wrap th.col-name { min-width: 7rem; }
/* ===== ENDE Kunden-Unterseiten (P6) ===== */
/* ===== KOMPAT (bis P10) ===== */