Treat Foodsharing's zero lastFetch value as no pickup, store memberSince, and show compact, visible mobile status information without hover tooltips.
225 lines
9.3 KiB
JavaScript
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;
|