Files
Pickup-Config/src/components/MobileConfigList.js
Meik 61c2488a1e fix: show membership date for stores without pickups
Treat Foodsharing's zero lastFetch value as no pickup, store memberSince, and
show compact, visible mobile status information without hover tooltips.
2026-08-05 20:22:18 +02:00

225 lines
9.3 KiB
JavaScript

import { useMemo, useState } from 'react';
import { formatDistanceToNowStrict } from 'date-fns';
import { de } from 'date-fns/locale';
import './MobileConfigList.css';
const FILTER_OPTIONS = [
{ value: 'all', label: 'Alle' },
{ value: 'active', label: 'Aktiv' },
{ value: 'inactive', label: 'Inaktiv' }
];
const formatDate = (date) => date.toLocaleDateString('de-DE', {
day: '2-digit',
month: '2-digit',
year: 'numeric'
});
const formatLastPickup = (lastPickupAt, memberSinceAt) => {
const date = new Date(lastPickupAt);
if (!lastPickupAt || Number.isNaN(date.getTime()) || date.getTime() <= 0) {
const memberSince = new Date(memberSinceAt);
return {
state: 'never',
memberSince: memberSinceAt && !Number.isNaN(memberSince.getTime())
? formatDate(memberSince)
: null
};
}
const ageDays = (Date.now() - date.getTime()) / (1000 * 60 * 60 * 24);
return {
state: ageDays >= 90 ? 'stale' : 'current',
relative: formatDistanceToNowStrict(date, { addSuffix: true, locale: de }),
exact: formatDate(date)
};
};
const MobileConfigList = ({
entries,
regularPickupMap,
weekdays,
formatRangeLabel,
formatRegularPickup,
onToggleActive,
onToggleAutoDeactivate,
onToggleProfileCheck,
onToggleOnlyNotify,
onToggleDormantSkip,
onWeekdayChange,
onRangePickerRequest,
onHideEntry,
onDeleteEntry,
canDelete,
isDirty,
onSaveConfig,
onResetConfig
}) => {
const [search, setSearch] = useState('');
const [activityFilter, setActivityFilter] = useState('all');
const filteredEntries = useMemo(() => {
const normalizedSearch = search.trim().toLocaleLowerCase('de-DE');
return (Array.isArray(entries) ? entries : []).filter((entry) => {
if (activityFilter === 'active' && !entry.active) {
return false;
}
if (activityFilter === 'inactive' && entry.active) {
return false;
}
if (!normalizedSearch) {
return true;
}
return `${entry.label || ''} ${entry.id || ''}`.toLocaleLowerCase('de-DE').includes(normalizedSearch);
});
}, [activityFilter, entries, search]);
return (
<section className="mobile-config-list" aria-label="Slot-Konfiguration">
<div className="mobile-config-list__heading">
<div>
<h2>Slot-Konfiguration</h2>
<p>{entries.length} Betriebe</p>
</div>
</div>
<div className="mobile-config-list__filters">
<label className="mobile-config-list__search">
<span className="sr-only">Betrieb suchen</span>
<input
type="search"
value={search}
onChange={(event) => setSearch(event.target.value)}
placeholder="Betrieb oder ID suchen"
/>
</label>
<div className="mobile-config-list__filter-tabs" aria-label="Aktivitätsfilter">
{FILTER_OPTIONS.map((option) => (
<button
key={option.value}
type="button"
className={activityFilter === option.value ? 'is-selected' : ''}
aria-pressed={activityFilter === option.value}
onClick={() => setActivityFilter(option.value)}
>
{option.label}
</button>
))}
</div>
</div>
<div className="mobile-config-list__cards">
{filteredEntries.map((entry) => {
const range = entry.desiredDateRange || (entry.desiredDate ? { start: entry.desiredDate, end: entry.desiredDate } : null);
const slots = formatRegularPickup(regularPickupMap?.[entry.id]);
const lastPickup = formatLastPickup(entry.lastPickupAt, entry.memberSinceAt);
return (
<article
key={entry.id}
className={`mobile-config-card ${entry.active ? 'mobile-config-card--active' : 'mobile-config-card--inactive'}`}
data-store-row={entry.id}
>
<header className="mobile-config-card__header">
<div>
<h3 className="mobile-config-card__title">{entry.label || `Store ${entry.id}`}</h3>
<p className="mobile-config-card__id">#{entry.id}</p>
{slots && <p className="mobile-config-card__slots">Slots: {slots}</p>}
<p className={`mobile-config-card__last-pickup mobile-config-card__last-pickup--${lastPickup.state}`}>
{lastPickup.state === 'never' ? (
<>
Noch nicht abgeholt{lastPickup.memberSince && <> · Beitritt: <strong>{lastPickup.memberSince}</strong></>}
</>
) : (
<>
Letzte Abholung: <strong>{lastPickup.relative}</strong> · {lastPickup.exact}
</>
)}
</p>
</div>
<a
href={`https://foodsharing.de/store/${entry.id}`}
target="_blank"
rel="noopener noreferrer"
className="mobile-config-card__store-link"
aria-label={`${entry.label || `Store ${entry.id}`} bei Foodsharing öffnen`}
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path d="M12.293 2.293a1 1 0 011.414 0L18 6.586V7a1 1 0 01-1 1h-.586l-3.707-3.707a1 1 0 010-1.414z" />
<path d="M3 5a2 2 0 012-2h5a1 1 0 010 2H5v10h10v-5a1 1 0 112 0v5a2 2 0 01-2 2H5a2 2 0 01-2-2V5z" />
<path d="M11 3a1 1 0 100 2h2v2a1 1 0 102 0V4a1 1 0 00-1-1h-3z" />
</svg>
</a>
</header>
<label className="mobile-config-card__switch-row">
<span>
<strong className="mobile-config-card__booking-label">Automatisch buchen</strong>
<small className="mobile-config-card__booking-status">{entry.active ? 'Aktiv' : 'Inaktiv'}</small>
</span>
<input
type="checkbox"
checked={!!entry.active}
onChange={() => onToggleActive(entry.id)}
aria-label={`${entry.label || entry.id} automatisch buchen`}
/>
</label>
<div className="mobile-config-card__core-fields">
<label className="mobile-config-card__field">
<span>Wochentag</span>
<select className="mobile-config-card__field-value" value={entry.desiredWeekday || ''} onChange={(event) => onWeekdayChange(entry.id, event.target.value)}>
<option value="">Kein Wochentag</option>
{weekdays.map((day) => <option key={day} value={day}>{day}</option>)}
</select>
</label>
<button type="button" className="mobile-config-card__range" onClick={() => onRangePickerRequest(entry.id)}>
<span>Datum / Zeitraum</span>
<strong>{formatRangeLabel(range?.start || '', range?.end || '')}</strong>
</button>
</div>
<label className="mobile-config-card__check-row">
<input type="checkbox" checked={!!entry.onlyNotify} onChange={() => onToggleOnlyNotify(entry.id)} />
<span>
<strong className="mobile-config-card__notify-label">Nur benachrichtigen</strong>
<small>Keinen Slot automatisch buchen</small>
</span>
</label>
<details className="mobile-config-card__details">
<summary>Weitere Optionen</summary>
<label className="mobile-config-card__check-row">
<input type="checkbox" checked={entry.checkProfileId !== false} onChange={() => onToggleProfileCheck(entry.id)} />
<span>Profil prüfen</span>
</label>
<label className="mobile-config-card__check-row">
<input type="checkbox" checked={entry.autoDeactivate !== false} onChange={() => onToggleAutoDeactivate(entry.id)} />
<span>Nach erfolgreicher Aktion deaktivieren</span>
</label>
<label className="mobile-config-card__check-row">
<input type="checkbox" checked={!entry.skipDormantCheck} onChange={() => onToggleDormantSkip(entry.id)} />
<span>Inaktivitätswarnung</span>
</label>
<div className="mobile-config-card__actions">
<button type="button" onClick={() => onHideEntry(entry.id)}>Ausblenden</button>
{canDelete && <button type="button" className="is-danger" onClick={() => onDeleteEntry(entry.id)}>Löschen</button>}
</div>
</details>
</article>
);
})}
</div>
{filteredEntries.length === 0 && <p className="mobile-config-list__empty">Keine passenden Betriebe gefunden.</p>}
{isDirty && (
<div className="mobile-config-list__save-bar">
<button type="button" className="mobile-config-list__reset" onClick={onResetConfig}>Zurücksetzen</button>
<button type="button" className="mobile-config-list__save" onClick={onSaveConfig}>Speichern</button>
</div>
)}
</section>
);
};
export default MobileConfigList;