326 lines
14 KiB
JavaScript
326 lines
14 KiB
JavaScript
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
|
||
}) => {
|
||
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)}
|
||
disabled={blockedByNoPickups}
|
||
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 disabled:opacity-60"
|
||
>
|
||
{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">×</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} 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">
|
||
In ioBroker speichern
|
||
</button>
|
||
</div>
|
||
|
||
</div>
|
||
);
|
||
};
|
||
|
||
export default DashboardView;
|