422 lines
15 KiB
JavaScript
422 lines
15 KiB
JavaScript
import { useState, useEffect } from 'react';
|
||
import { DateRange } from 'react-date-range';
|
||
import { format, parseISO, isValid, startOfDay } from 'date-fns';
|
||
import { de } from 'date-fns/locale';
|
||
import 'react-date-range/dist/styles.css';
|
||
import 'react-date-range/dist/theme/default.css';
|
||
|
||
const parseDateValue = (value) => {
|
||
if (!value) {
|
||
return null;
|
||
}
|
||
const parsed = parseISO(value);
|
||
return isValid(parsed) ? parsed : null;
|
||
};
|
||
|
||
const formatDateValue = (date) => {
|
||
if (!(date instanceof Date) || !isValid(date)) {
|
||
return null;
|
||
}
|
||
return format(date, 'yyyy-MM-dd');
|
||
};
|
||
|
||
const formatRangeLabel = (start, end) => {
|
||
const startDate = parseDateValue(start);
|
||
const endDate = parseDateValue(end);
|
||
if (startDate && endDate) {
|
||
const startLabel = format(startDate, 'dd.MM.yyyy', { locale: de });
|
||
const endLabel = format(endDate, 'dd.MM.yyyy', { locale: de });
|
||
if (startLabel === endLabel) {
|
||
return startLabel;
|
||
}
|
||
return `${startLabel} – ${endLabel}`;
|
||
}
|
||
if (startDate) {
|
||
return format(startDate, 'dd.MM.yyyy', { locale: de });
|
||
}
|
||
return 'Zeitraum auswählen';
|
||
};
|
||
|
||
const buildSelectionRange = (start, end, minDate) => {
|
||
const minimum = minDate || startOfDay(new Date());
|
||
let startDate = parseDateValue(start) || parseDateValue(end) || minimum;
|
||
let endDate = parseDateValue(end) || parseDateValue(start) || startDate;
|
||
if (startDate < minimum) {
|
||
startDate = minimum;
|
||
}
|
||
if (endDate < minimum) {
|
||
endDate = startDate;
|
||
}
|
||
return {
|
||
startDate,
|
||
endDate,
|
||
key: 'selection'
|
||
};
|
||
};
|
||
|
||
const sortEntriesByLabel = (entries = []) => {
|
||
return [...entries].sort((a, b) =>
|
||
(a.label || '').localeCompare(b.label || '', 'de', { sensitivity: 'base' })
|
||
);
|
||
};
|
||
|
||
const PickupConfigEditor = () => {
|
||
const [config, setConfig] = useState([]);
|
||
const [loading, setLoading] = useState(true);
|
||
const [status, setStatus] = useState('');
|
||
const [error, setError] = useState('');
|
||
const [activeRangePicker, setActiveRangePicker] = useState(null);
|
||
const minSelectableDate = startOfDay(new Date());
|
||
|
||
// Simulierte API-Endpunkte - diese müssen in Ihrer tatsächlichen Implementierung angepasst werden
|
||
const API_URL = '/api/iobroker/pickup-config';
|
||
|
||
useEffect(() => {
|
||
// Beim Laden der Komponente die aktuelle Konfiguration abrufen
|
||
fetchConfig();
|
||
}, []);
|
||
|
||
useEffect(() => {
|
||
if (!activeRangePicker) {
|
||
return;
|
||
}
|
||
const entry = config.find((item) => item.id === activeRangePicker);
|
||
if (!entry || entry.desiredWeekday) {
|
||
setActiveRangePicker(null);
|
||
}
|
||
}, [activeRangePicker, config]);
|
||
|
||
const fetchConfig = async () => {
|
||
setLoading(true);
|
||
setError('');
|
||
|
||
try {
|
||
// In einer echten Implementierung würden Sie Ihre API aufrufen
|
||
// Hier wird die statische Konfiguration verwendet
|
||
// const response = await fetch(API_URL);
|
||
// const data = await response.json();
|
||
|
||
// Simulierte Verzögerung und Antwort mit der statischen Konfiguration
|
||
setTimeout(() => {
|
||
const staticConfig = [
|
||
{ id: "63448", active: false, checkProfileId: true, onlyNotify: true, label: "Penny Baden-Oos" },
|
||
{ id: "44975", active: false, checkProfileId: true, onlyNotify: false, label: "Aldi Kuppenheim", desiredWeekday: "Samstag" },
|
||
{ id: "44972", active: false, checkProfileId: true, onlyNotify: false, label: "Aldi Biblisweg", desiredWeekday: "Dienstag" },
|
||
{
|
||
id: "44975",
|
||
active: false,
|
||
checkProfileId: true,
|
||
onlyNotify: false,
|
||
label: "Aldi Kuppenheim",
|
||
desiredDateRange: { start: "2025-05-18", end: "2025-05-18" }
|
||
},
|
||
{ id: "33875", active: false, checkProfileId: true, onlyNotify: false, label: "Cap Markt", desiredWeekday: "Donnerstag" },
|
||
{ id: "42322", active: false, checkProfileId: false, onlyNotify: false, label: "Edeka Haueneberstein" },
|
||
{ id: "51450", active: false, checkProfileId: true, onlyNotify: false, label: "Hornbach Grünwinkel" }
|
||
];
|
||
setConfig(sortEntriesByLabel(staticConfig));
|
||
setLoading(false);
|
||
}, 500);
|
||
} catch (err) {
|
||
setError('Fehler beim Laden der Konfiguration: ' + err.message);
|
||
setLoading(false);
|
||
}
|
||
};
|
||
|
||
const saveConfig = async () => {
|
||
setStatus('Speichere...');
|
||
setError('');
|
||
|
||
try {
|
||
// API-Aufruf zum Speichern der Konfiguration in ioBroker
|
||
// In einer echten Implementierung würden Sie Ihre API aufrufen
|
||
// const response = await fetch(API_URL, {
|
||
// method: 'POST',
|
||
// headers: {
|
||
// 'Content-Type': 'application/json',
|
||
// },
|
||
// body: JSON.stringify(config),
|
||
// });
|
||
|
||
// Simulierte Verzögerung zum Darstellen des Speichervorgangs
|
||
setTimeout(() => {
|
||
setStatus('Konfiguration erfolgreich gespeichert!');
|
||
setTimeout(() => setStatus(''), 3000);
|
||
}, 1000);
|
||
} catch (err) {
|
||
setError('Fehler beim Speichern: ' + err.message);
|
||
}
|
||
};
|
||
|
||
const handleToggleActive = (index) => {
|
||
const newConfig = [...config];
|
||
newConfig[index].active = !newConfig[index].active;
|
||
setConfig(newConfig);
|
||
};
|
||
|
||
const handleToggleProfileCheck = (index) => {
|
||
const newConfig = [...config];
|
||
newConfig[index].checkProfileId = !newConfig[index].checkProfileId;
|
||
setConfig(newConfig);
|
||
};
|
||
|
||
const handleToggleOnlyNotify = (index) => {
|
||
const newConfig = [...config];
|
||
newConfig[index].onlyNotify = !newConfig[index].onlyNotify;
|
||
setConfig(newConfig);
|
||
};
|
||
|
||
const handleWeekdayChange = (index, value) => {
|
||
const newConfig = [...config];
|
||
const entryId = newConfig[index]?.id;
|
||
newConfig[index].desiredWeekday = value;
|
||
if (newConfig[index].desiredDateRange) {
|
||
delete newConfig[index].desiredDateRange;
|
||
}
|
||
setConfig(newConfig);
|
||
if (value && entryId) {
|
||
setActiveRangePicker((prev) => (prev === entryId ? null : prev));
|
||
}
|
||
};
|
||
|
||
const handleDateRangeSelection = (entryId, startDate, endDate) => {
|
||
const startValue = formatDateValue(startDate);
|
||
const endValue = formatDateValue(endDate);
|
||
setConfig((prev) =>
|
||
prev.map((item) => {
|
||
if (item.id !== entryId) {
|
||
return item;
|
||
}
|
||
const updated = { ...item };
|
||
if (startValue || endValue) {
|
||
updated.desiredDateRange = {
|
||
start: startValue || endValue,
|
||
end: endValue || startValue
|
||
};
|
||
if (updated.desiredWeekday) {
|
||
delete updated.desiredWeekday;
|
||
}
|
||
} else if (updated.desiredDateRange) {
|
||
delete updated.desiredDateRange;
|
||
}
|
||
if (updated.desiredDate) {
|
||
delete updated.desiredDate;
|
||
}
|
||
return updated;
|
||
})
|
||
);
|
||
};
|
||
|
||
const activeRangeEntry = activeRangePicker
|
||
? config.find((item) => item.id === activeRangePicker) || null
|
||
: null;
|
||
|
||
if (loading) {
|
||
return <div className="text-center p-8">Lade Konfiguration...</div>;
|
||
}
|
||
|
||
const weekdays = ['Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'Samstag', 'Sonntag'];
|
||
|
||
return (
|
||
<div className="p-4 max-w-4xl mx-auto">
|
||
<h1 className="text-2xl font-bold mb-6">ioBroker Abholung-Konfiguration</h1>
|
||
|
||
{error && (
|
||
<div className="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-4">
|
||
{error}
|
||
</div>
|
||
)}
|
||
|
||
{status && (
|
||
<div className="bg-green-100 border border-green-400 text-green-700 px-4 py-3 rounded mb-4">
|
||
{status}
|
||
</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>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{config.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={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={() => handleToggleActive(index)}
|
||
className="h-5 w-5"
|
||
/>
|
||
</td>
|
||
<td className="px-4 py-2">
|
||
<span className="font-medium">{item.label}</span>
|
||
</td>
|
||
<td className="px-4 py-2 text-center">
|
||
<input
|
||
type="checkbox"
|
||
checked={item.checkProfileId}
|
||
onChange={() => handleToggleProfileCheck(index)}
|
||
className="h-5 w-5"
|
||
/>
|
||
</td>
|
||
<td className="px-4 py-2 text-center">
|
||
<input
|
||
type="checkbox"
|
||
checked={item.onlyNotify}
|
||
onChange={() => handleToggleOnlyNotify(index)}
|
||
className="h-5 w-5"
|
||
/>
|
||
</td>
|
||
<td className="px-4 py-2">
|
||
<select
|
||
value={item.desiredWeekday || ''}
|
||
onChange={(e) => handleWeekdayChange(index, e.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;
|
||
}
|
||
setActiveRangePicker(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>
|
||
</tr>
|
||
);
|
||
})}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<div className="mt-6 flex justify-between">
|
||
<button
|
||
onClick={fetchConfig}
|
||
className="bg-gray-500 hover:bg-gray-600 text-white py-2 px-4 rounded"
|
||
>
|
||
Zurücksetzen
|
||
</button>
|
||
<button
|
||
onClick={saveConfig}
|
||
className="bg-blue-500 hover:bg-blue-600 text-white py-2 px-4 rounded"
|
||
>
|
||
In ioBroker speichern
|
||
</button>
|
||
</div>
|
||
|
||
<div className="mt-8 p-4 border rounded bg-gray-50">
|
||
<h2 className="text-lg font-bold mb-2">Aktuelle JSON-Konfiguration:</h2>
|
||
<pre className="bg-gray-100 p-4 rounded overflow-x-auto">
|
||
{JSON.stringify(config, null, 2)}
|
||
</pre>
|
||
</div>
|
||
|
||
{activeRangeEntry && !activeRangeEntry.desiredWeekday && (
|
||
<div
|
||
className="fixed inset-0 z-40 flex items-center justify-center bg-black bg-opacity-40 px-4"
|
||
onClick={() => setActiveRangePicker(null)}
|
||
>
|
||
<div
|
||
className="bg-white rounded-2xl shadow-2xl w-full max-w-lg"
|
||
onClick={(event) => event.stopPropagation()}
|
||
>
|
||
<div className="px-5 pt-5 pb-3 border-b">
|
||
<p className="text-xs uppercase tracking-wide text-gray-500">Zeitraum auswählen für</p>
|
||
<p className="text-lg font-semibold text-gray-900">
|
||
{activeRangeEntry.label || `Store ${activeRangeEntry.id}`}
|
||
</p>
|
||
</div>
|
||
<div className="px-2 py-4">
|
||
<DateRange
|
||
onChange={(ranges) => {
|
||
const { startDate, endDate } = ranges.selection;
|
||
handleDateRangeSelection(activeRangeEntry.id, startDate, endDate);
|
||
}}
|
||
moveRangeOnFirstSelection={false}
|
||
ranges={[
|
||
buildSelectionRange(
|
||
activeRangeEntry.desiredDateRange?.start,
|
||
activeRangeEntry.desiredDateRange?.end,
|
||
minSelectableDate
|
||
)
|
||
]}
|
||
rangeColors={['#2563EB']}
|
||
months={1}
|
||
direction="horizontal"
|
||
showDateDisplay={false}
|
||
locale={de}
|
||
minDate={minSelectableDate}
|
||
/>
|
||
</div>
|
||
<div className="flex items-center justify-between px-5 py-3 border-t bg-gray-50 rounded-b-2xl">
|
||
<button
|
||
type="button"
|
||
className="text-sm text-gray-600 hover:text-gray-900"
|
||
onClick={() => {
|
||
handleDateRangeSelection(activeRangeEntry.id, null, null);
|
||
setActiveRangePicker(null);
|
||
}}
|
||
>
|
||
Zurücksetzen
|
||
</button>
|
||
<div className="flex items-center gap-3">
|
||
<button
|
||
type="button"
|
||
className="text-sm text-gray-600 hover:text-gray-900"
|
||
onClick={() => setActiveRangePicker(null)}
|
||
>
|
||
Abbrechen
|
||
</button>
|
||
<button
|
||
type="button"
|
||
className="text-sm font-semibold text-white bg-blue-600 hover:bg-blue-700 px-4 py-2 rounded-md"
|
||
onClick={() => setActiveRangePicker(null)}
|
||
>
|
||
Fertig
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
};
|
||
|
||
export default PickupConfigEditor;
|