Files
GerbilManager/gerbil-manager-web/src/pages/AnfragenPage.tsx
Gulum 6eb82da90c UX-MOBILE-1: FilterPanel — einklappbare Filter auf Mobil (390px)
Neues FilterPanel-Muster (src/components/FilterPanel.tsx + filterPanel.css):
- Mobil (<768px): Suchfeld immer sichtbar; alle weiteren Filter hinter einem
  'Filter (N)'-Button eingeklappt (Badge = Anzahl aktiver Nicht-Default-Filter).
  Tippen öffnet/schließt den Drawer. 'Filter zurücksetzen' im Drawer.
- Desktop (>=768px): Toggle versteckt, Drawer als display:contents → alle Controls
  inline in der bestehenden .filters-Flexzeile, wie bisher.
Auf GerbilsPage, WuerfeListPage und AnfragenPage ausgerollt.
BeckenPage/KontaktePage nur ein Suchfeld → kein Panel nötig.

e2e: filter-panel.spec.ts (8 Tests, phone+desktop); bestand/tiere/anfragen-Specs
erhalten openFilterPanel-Aufruf vor Drawer-Controls; fixtures.ts +openFilterPanel().
Vitest 78 / e2e 102 grün.

Mobil-Pass (390px, Durchsicht): Hauptproblem behoben. Beobachtete Restpunkte
für god (nicht selbst gefixed):
- .genotype-table in Genetik/Probeverpaarung: Genotyp-Strings können eng werden
- Stammbaum (react-d3-tree SVG): kein explizites mobile Viewport-Handling
- BreedingResultView Genotyp-Detail-Tabelle: ggf. horizontal scrollbar-los

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
2026-06-06 15:28:13 +02:00

155 lines
5.0 KiB
TypeScript

/**
* INBOX-1: Anfragen-Posteingang — Liste mit Status-Filter (Gridify) und
* manuellem Gmail-Abruf („Anfragen abrufen“, POST /api/requests/sync).
* Solange Gmail unkonfiguriert ist, antwortet der Sync mit MailNotConfigured
* → freundlicher deutscher Hinweis statt Fehler.
*/
import { useState } from 'react'
import { Link } from 'react-router-dom'
import { de } from '../strings/de'
import {
REQUEST_STATUSES,
listRequests,
syncRequests,
type RequestStatus,
} from '../api/requests'
import { useApi, useMutation } from '../hooks/useApi'
import { formatDateTime } from '../format/labels'
import { FilterPanel } from '../components/FilterPanel'
import './anfragen.css'
const PAGE_SIZE = 20
export function StatusBadge({ status }: { status: RequestStatus }) {
return (
<span className={`badge anfrage-badge anfrage-badge--${status.toLowerCase()}`}>
{de.pages.anfragen.statusLabels[status]}
</span>
)
}
export default function AnfragenPage() {
const t = de.pages.anfragen
const [status, setStatus] = useState<RequestStatus | ''>('')
const [page, setPage] = useState(1)
const [notice, setNotice] = useState<string | null>(null)
const requests = useApi(
() =>
listRequests({
page,
pageSize: PAGE_SIZE,
orderBy: 'receivedAt desc',
filter: status === '' ? undefined : `status==${status}`,
}),
[page, status],
)
const sync = useMutation(() => syncRequests())
async function onSync() {
setNotice(null)
const result = await sync.run()
if (!result.ok) return // sync.error treibt den Alert
if (result.value.error === 'MailNotConfigured') setNotice(t.mailNotConfigured)
else if (result.value.error === 'MailAuthFailed') setNotice(t.mailAuthFailed)
else {
setNotice(t.syncImported(result.value.imported))
if (result.value.imported > 0) requests.reload()
}
}
return (
<section className="page">
<header className="page-head">
<div>
<h2>{t.title}</h2>
{requests.data && <p className="muted">{t.countText(requests.data.totalCount)}</p>}
</div>
<div className="head-actions">
<button type="button" className="btn btn--primary" onClick={onSync} disabled={sync.pending}>
{sync.pending ? t.syncing : t.sync}
</button>
</div>
</header>
{notice && <div className="alert">{notice}</div>}
{sync.error && <div className="alert alert--error">{sync.error}</div>}
{/* Status-Filter */}
<FilterPanel
activeCount={status !== '' ? 1 : 0}
onReset={() => { setStatus(''); setPage(1) }}
>
<label className="field">
<span>{t.detail.statusLabel}</span>
<select
value={status}
onChange={(e) => {
setStatus(e.target.value as RequestStatus | '')
setPage(1)
}}
>
<option value="">{t.filterAll}</option>
{REQUEST_STATUSES.map((s) => (
<option key={s} value={s}>
{t.statusLabels[s]}
</option>
))}
</select>
</label>
</FilterPanel>
{requests.loading && <p className="muted">{de.common.loading}</p>}
{requests.error && (
<div className="alert alert--error">
<span>{requests.error}</span>
<button type="button" className="btn" onClick={requests.reload}>
{de.common.retry}
</button>
</div>
)}
{requests.data && requests.data.items.length === 0 && (
<p className="muted">{status === '' ? t.empty : t.emptyFiltered}</p>
)}
{requests.data && requests.data.items.length > 0 && (
<ul className="card-list">
{requests.data.items.map((r) => (
<li key={r.id}>
<Link to={`/anfragen/${r.id}`} className="gerbil-card anfrage-card">
<span className="gerbil-card__name">{r.fromName ?? r.fromAddress}</span>
<StatusBadge status={r.status} />
<span className="anfrage-card__subject">
{r.subject ?? de.pages.anfragen.detail.noSubject}
</span>
<span className="gerbil-card__meta">{formatDateTime(r.receivedAt)}</span>
</Link>
</li>
))}
</ul>
)}
{requests.data && requests.data.totalCount > PAGE_SIZE && (
<nav className="pager" aria-label="Seitennavigation">
<button type="button" className="btn" disabled={page <= 1} onClick={() => setPage(page - 1)}>
{de.common.previous}
</button>
<span>
{de.common.page} {page} {de.common.of}{' '}
{Math.max(1, Math.ceil(requests.data.totalCount / PAGE_SIZE))}
</span>
<button
type="button"
className="btn"
disabled={page >= Math.ceil(requests.data.totalCount / PAGE_SIZE)}
onClick={() => setPage(page + 1)}
>
{de.common.next}
</button>
</nav>
)}
</section>
)
}