Files
Pickup-Config/src/components/DashboardView.js
2025-11-10 11:34:54 +01:00

353 lines
15 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useEffect } from 'react';
import NotificationPanel from './NotificationPanel';
const DashboardView = ({
session,
onRefresh,
onLogout,
notificationPanelOpen,
onToggleNotificationPanel,
notificationProps,
stores,
availableCollapsed,
onToggleStores,
onStoreSelect,
configMap,
error,
onDismissError,
status,
visibleConfig,
config,
onToggleActive,
onToggleProfileCheck,
onToggleOnlyNotify,
onWeekdayChange,
weekdays,
onRangePickerRequest,
formatRangeLabel,
onSaveConfig,
onResetConfig,
onHideEntry,
onDeleteEntry,
canDelete,
focusedStoreId,
onClearFocus
}) => {
useEffect(() => {
if (!focusedStoreId) {
return;
}
const row = document.querySelector(`[data-store-row="${focusedStoreId}"]`);
if (!row) {
onClearFocus();
return;
}
row.scrollIntoView({ behavior: 'smooth', block: 'center' });
row.classList.add('ring-2', 'ring-blue-400', 'bg-yellow-100');
const timeout = setTimeout(() => {
row.classList.remove('ring-2', 'ring-blue-400', 'bg-yellow-100');
onClearFocus();
}, 3000);
return () => {
clearTimeout(timeout);
row.classList.remove('ring-2', 'ring-blue-400', 'bg-yellow-100');
};
}, [focusedStoreId, onClearFocus]);
const {
error: notificationError,
message: notificationMessage,
settings: notificationSettings,
capabilities: notificationCapabilities,
loading: notificationLoading,
dirty: notificationDirty,
saving: notificationSaving,
onReset: onNotificationReset,
onSave: onNotificationSave,
onFieldChange: onNotificationFieldChange,
onSendTest: onNotificationTest,
onCopyLink: onNotificationCopy,
copyFeedback,
ntfyPreviewUrl
} = notificationProps;
return (
<div className="p-4 max-w-6xl mx-auto bg-white shadow-lg rounded-lg mt-4">
<h1 className="text-2xl font-bold mb-4 text-center text-gray-800">Foodsharing Pickup Manager</h1>
<div className="bg-blue-50 border border-blue-100 rounded-lg p-4 mb-4">
<div className="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
<div>
<p className="text-sm uppercase text-blue-600 font-semibold">Angemeldet</p>
<p className="text-lg font-medium text-gray-800">{session.profile.name}</p>
<p className="text-gray-500 text-sm">Profil-ID: {session.profile.id}</p>
</div>
<div className="flex flex-wrap gap-2 items-center">
<button
onClick={() => onRefresh({ block: false })}
className="bg-blue-500 hover:bg-blue-600 text-white py-2 px-4 rounded focus:outline-none focus:ring-2 focus:ring-blue-500 transition-colors"
>
Betriebe aktualisieren
</button>
<button
onClick={onLogout}
className="bg-gray-200 hover:bg-gray-300 text-gray-800 py-2 px-4 rounded focus:outline-none focus:ring-2 focus:ring-gray-400 transition-colors"
>
Logout
</button>
<button
type="button"
onClick={onToggleNotificationPanel}
className={`flex items-center justify-center w-11 h-11 rounded-full border ${
notificationPanelOpen ? 'border-blue-500 text-blue-600' : 'border-gray-300 text-gray-600'
} hover:text-blue-700 hover:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-400 transition`}
title="Benachrichtigungen konfigurieren"
>
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 0 0 2.573 1.066c1.543-.89 3.31.877 2.42 2.42a1.724 1.724 0 0 0 1.065 2.572c1.757.426 1.757 2.924 0 3.35a1.724 1.724 0 0 0-1.066 2.573c.89 1.543-.877 3.31-2.42 2.42a1.724 1.724 0 0 0-2.572 1.065c-.426 1.757-2.924 1.757-3.35 0a1.724 1.724 0 0 0-2.573-1.066c-1.543.89-3.31-.877-2.42-2.42a1.724 1.724 0 0 0-1.065-2.572c-1.757-.426-1.757-2.924 0-3.35a1.724 1.724 0 0 0 1.066-2.573c-.89-1.543.877-3.31 2.42-2.42.996.575 2.273.155 2.573-1.065z"
/>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0z" />
</svg>
</button>
{notificationLoading && <span className="text-sm text-gray-500 ml-1">Lade</span>}
</div>
</div>
</div>
{notificationPanelOpen && (
<NotificationPanel
error={notificationError}
message={notificationMessage}
settings={notificationSettings}
capabilities={notificationCapabilities}
loading={notificationLoading}
dirty={notificationDirty}
saving={notificationSaving}
onReset={onNotificationReset}
onSave={onNotificationSave}
onFieldChange={onNotificationFieldChange}
onSendTest={onNotificationTest}
onCopyLink={onNotificationCopy}
copyFeedback={copyFeedback}
ntfyPreviewUrl={ntfyPreviewUrl}
/>
)}
<div className="mb-6 border border-gray-200 rounded-lg overflow-hidden">
<button
onClick={onToggleStores}
className="w-full flex items-center justify-between px-4 py-3 bg-gray-50 hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-300"
>
<span className="font-semibold text-gray-800">Verfügbare Betriebe ({stores.length})</span>
<svg
xmlns="http://www.w3.org/2000/svg"
className={`h-5 w-5 text-gray-600 transition-transform ${availableCollapsed ? '' : 'rotate-180'}`}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</button>
{!availableCollapsed && (
<div className="max-h-72 overflow-y-auto divide-y divide-gray-100">
{stores.map((store) => {
const storeId = String(store.id);
const entry = configMap.get(storeId);
const isVisible = entry && !entry.hidden;
const blockedByNoPickups = store.hasPickupSlots === false;
let statusLabel = 'Hinzufügen';
let statusClass = 'text-blue-600';
if (isVisible) {
statusLabel = 'Bereits in Konfiguration';
statusClass = 'text-gray-500';
} else if (entry) {
statusLabel = 'Ausgeblendet erneut hinzufügen';
statusClass = 'text-amber-600';
} else if (blockedByNoPickups) {
statusLabel = 'Keine Pickups automatisch verborgen';
statusClass = 'text-red-600';
}
return (
<div
key={storeId}
className="flex items-center justify-between px-4 py-3 hover:bg-gray-50 transition cursor-default"
>
<div>
<p className="font-semibold text-gray-800">{store.name}</p>
<p className="text-sm text-gray-500">
{store.zip} {store.city}
</p>
<p className={`text-xs mt-2 ${statusClass}`}>{statusLabel}</p>
</div>
<button
onClick={() => onStoreSelect(store)}
title={blockedByNoPickups ? 'Keine Pickups automatisch verborgen' : undefined}
className="bg-blue-50 hover:bg-blue-100 text-blue-600 border border-blue-200 px-3 py-1.5 rounded text-sm focus:outline-none focus:ring-2 focus:ring-blue-300"
>
{isVisible ? 'Zur Liste springen' : 'Hinzufügen'}
</button>
</div>
);
})}
</div>
)}
</div>
{error && (
<div className="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-4 relative">
<span className="block sm:inline">{error}</span>
<button className="absolute top-0 bottom-0 right-0 px-4 py-3" onClick={onDismissError}>
<span className="text-xl">&times;</span>
</button>
</div>
)}
{status && (
<div className="bg-green-100 border border-green-400 text-green-700 px-4 py-3 rounded mb-4 relative">
<span className="block sm:inline">{status}</span>
</div>
)}
<div className="overflow-x-auto">
<table className="min-w-full bg-white border border-gray-200">
<thead>
<tr className="bg-gray-100">
<th className="px-4 py-2">Aktiv</th>
<th className="px-4 py-2">Geschäft</th>
<th className="px-4 py-2">Profil prüfen</th>
<th className="px-4 py-2">Nur benachrichtigen</th>
<th className="px-4 py-2">Wochentag</th>
<th className="px-4 py-2">Datum / Zeitraum</th>
<th className="px-4 py-2 text-right">Aktionen</th>
</tr>
</thead>
<tbody>
{visibleConfig.map((item, index) => {
const normalizedRange = item.desiredDateRange
? { ...item.desiredDateRange }
: item.desiredDate
? { start: item.desiredDate, end: item.desiredDate }
: null;
const rangeStart = normalizedRange?.start || '';
const rangeEnd = normalizedRange?.end || '';
const hasDateRange = Boolean(rangeStart || rangeEnd);
return (
<tr
key={item.id || index}
data-store-row={item.id}
className={index % 2 === 0 ? 'bg-gray-50' : 'bg-white'}
>
<td className="px-4 py-2 text-center">
<input
type="checkbox"
checked={item.active}
onChange={() => onToggleActive(item.id)}
className="h-5 w-5"
/>
</td>
<td className="px-4 py-2">
<span className="font-medium">{item.label}</span>
{item.hidden && <span className="ml-2 text-xs text-gray-400">(ausgeblendet)</span>}
</td>
<td className="px-4 py-2 text-center">
<input
type="checkbox"
checked={item.checkProfileId}
onChange={() => onToggleProfileCheck(item.id)}
className="h-5 w-5"
/>
</td>
<td className="px-4 py-2 text-center">
<input
type="checkbox"
checked={item.onlyNotify}
onChange={() => onToggleOnlyNotify(item.id)}
className="h-5 w-5"
/>
</td>
<td className="px-4 py-2">
<select
value={item.desiredWeekday || ''}
onChange={(event) => onWeekdayChange(item.id, event.target.value)}
className="border rounded p-1 w-full"
disabled={hasDateRange}
>
<option value="">Kein Wochentag</option>
{weekdays.map((day) => (
<option key={day} value={day}>
{day}
</option>
))}
</select>
</td>
<td className="px-4 py-2">
<button
type="button"
onClick={() => {
if (item.desiredWeekday) {
return;
}
onRangePickerRequest(item.id);
}}
disabled={Boolean(item.desiredWeekday)}
className={`w-full border rounded p-2 text-left transition focus:outline-none focus:ring-2 focus:ring-blue-500 ${
item.desiredWeekday
? 'bg-gray-100 text-gray-400 cursor-not-allowed'
: 'bg-white hover:border-blue-400'
}`}
>
<span className="block text-sm text-gray-700">{formatRangeLabel(rangeStart, rangeEnd)}</span>
<span className="block text-xs text-gray-500">Klicke zum Auswählen</span>
</button>
</td>
<td className="px-4 py-2 text-right space-x-2">
<button
type="button"
onClick={() => onHideEntry(item.id)}
className="text-gray-600 hover:text-gray-900"
title="Ausblenden"
>
<svg xmlns="http://www.w3.org/2000/svg" className="inline h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 19c7 0 9-7 9-7s-2-7-9-7-9 7-9 7 2 7 9 7z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 15a3 3 0 100-6 3 3 0 000 6z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 3l18 18" />
</svg>
</button>
{canDelete && (
<button
type="button"
onClick={() => onDeleteEntry(item.id)}
className="ml-2 text-red-600 hover:text-red-800"
title="Löschen"
>
<svg xmlns="http://www.w3.org/2000/svg" className="inline h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
</button>
)}
</td>
</tr>
);
})}
</tbody>
</table>
</div>
<div className="mt-6 flex justify-between">
<button onClick={onResetConfig} className="bg-gray-500 hover:bg-gray-600 text-white py-2 px-4 rounded">
Zurücksetzen
</button>
<button onClick={onSaveConfig} className="bg-blue-500 hover:bg-blue-600 text-white py-2 px-4 rounded">
Konfiguration speichern
</button>
</div>
</div>
);
};
export default DashboardView;