fix: restrict delete button to admins
This commit is contained in:
@@ -1,81 +1,23 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { DateRange } from 'react-date-range';
|
import { startOfDay } from 'date-fns';
|
||||||
import { format, parseISO, isValid, startOfDay } from 'date-fns';
|
import PickupConfigTable from './components/PickupConfigTable';
|
||||||
import { de } from 'date-fns/locale';
|
import RangePickerModal from './components/RangePickerModal';
|
||||||
import 'react-date-range/dist/styles.css';
|
import usePickupConfig from './hooks/usePickupConfig';
|
||||||
import 'react-date-range/dist/theme/default.css';
|
import { formatDateValue } from './utils/dateUtils';
|
||||||
|
|
||||||
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 PickupConfigEditor = () => {
|
||||||
const [config, setConfig] = useState([]);
|
const {
|
||||||
const [loading, setLoading] = useState(true);
|
config,
|
||||||
const [status, setStatus] = useState('');
|
setConfig,
|
||||||
const [error, setError] = useState('');
|
loading,
|
||||||
|
status,
|
||||||
|
error,
|
||||||
|
fetchConfig,
|
||||||
|
saveConfig
|
||||||
|
} = usePickupConfig();
|
||||||
const [activeRangePicker, setActiveRangePicker] = useState(null);
|
const [activeRangePicker, setActiveRangePicker] = useState(null);
|
||||||
const minSelectableDate = startOfDay(new Date());
|
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(() => {
|
useEffect(() => {
|
||||||
if (!activeRangePicker) {
|
if (!activeRangePicker) {
|
||||||
return;
|
return;
|
||||||
@@ -86,68 +28,6 @@ const PickupConfigEditor = () => {
|
|||||||
}
|
}
|
||||||
}, [activeRangePicker, config]);
|
}, [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 handleToggleActive = (index) => {
|
||||||
const newConfig = [...config];
|
const newConfig = [...config];
|
||||||
newConfig[index].active = !newConfig[index].active;
|
newConfig[index].active = !newConfig[index].active;
|
||||||
@@ -233,96 +113,15 @@ const PickupConfigEditor = () => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="overflow-x-auto">
|
<PickupConfigTable
|
||||||
<table className="min-w-full bg-white border border-gray-200">
|
config={config}
|
||||||
<thead>
|
weekdays={weekdays}
|
||||||
<tr className="bg-gray-100">
|
onToggleActive={handleToggleActive}
|
||||||
<th className="px-4 py-2">Aktiv</th>
|
onToggleProfileCheck={handleToggleProfileCheck}
|
||||||
<th className="px-4 py-2">Geschäft</th>
|
onToggleOnlyNotify={handleToggleOnlyNotify}
|
||||||
<th className="px-4 py-2">Profil prüfen</th>
|
onWeekdayChange={handleWeekdayChange}
|
||||||
<th className="px-4 py-2">Nur benachrichtigen</th>
|
onRangePickerRequest={setActiveRangePicker}
|
||||||
<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">
|
<div className="mt-6 flex justify-between">
|
||||||
<button
|
<button
|
||||||
@@ -346,73 +145,19 @@ const PickupConfigEditor = () => {
|
|||||||
</pre>
|
</pre>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{activeRangeEntry && !activeRangeEntry.desiredWeekday && (
|
{activeRangeEntry && (
|
||||||
<div
|
<RangePickerModal
|
||||||
className="fixed inset-0 z-40 flex items-center justify-center bg-black bg-opacity-40 px-4"
|
entry={activeRangeEntry}
|
||||||
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}
|
minDate={minSelectableDate}
|
||||||
/>
|
onSelectRange={(startDate, endDate) =>
|
||||||
</div>
|
handleDateRangeSelection(activeRangeEntry.id, startDate, endDate)
|
||||||
<div className="flex items-center justify-between px-5 py-3 border-t bg-gray-50 rounded-b-2xl">
|
}
|
||||||
<button
|
onResetRange={() => {
|
||||||
type="button"
|
|
||||||
className="text-sm text-gray-600 hover:text-gray-900"
|
|
||||||
onClick={() => {
|
|
||||||
handleDateRangeSelection(activeRangeEntry.id, null, null);
|
handleDateRangeSelection(activeRangeEntry.id, null, null);
|
||||||
setActiveRangePicker(null);
|
setActiveRangePicker(null);
|
||||||
}}
|
}}
|
||||||
>
|
onClose={() => 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>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
106
src/components/PickupConfigTable.js
Normal file
106
src/components/PickupConfigTable.js
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
import { formatRangeLabel } from '../utils/dateUtils';
|
||||||
|
|
||||||
|
const PickupConfigTable = ({
|
||||||
|
config,
|
||||||
|
weekdays,
|
||||||
|
onToggleActive,
|
||||||
|
onToggleProfileCheck,
|
||||||
|
onToggleOnlyNotify,
|
||||||
|
onWeekdayChange,
|
||||||
|
onRangePickerRequest
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<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={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(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={() => onToggleProfileCheck(index)}
|
||||||
|
className="h-5 w-5"
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-2 text-center">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={item.onlyNotify}
|
||||||
|
onChange={() => onToggleOnlyNotify(index)}
|
||||||
|
className="h-5 w-5"
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-2">
|
||||||
|
<select
|
||||||
|
value={item.desiredWeekday || ''}
|
||||||
|
onChange={(e) => onWeekdayChange(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;
|
||||||
|
}
|
||||||
|
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>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PickupConfigTable;
|
||||||
66
src/components/RangePickerModal.js
Normal file
66
src/components/RangePickerModal.js
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import { DateRange } from 'react-date-range';
|
||||||
|
import { de } from 'date-fns/locale';
|
||||||
|
import { buildSelectionRange } from '../utils/dateUtils';
|
||||||
|
import 'react-date-range/dist/styles.css';
|
||||||
|
import 'react-date-range/dist/theme/default.css';
|
||||||
|
|
||||||
|
const RangePickerModal = ({ entry, minDate, onSelectRange, onResetRange, onClose }) => {
|
||||||
|
if (!entry || entry.desiredWeekday) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="fixed inset-0 z-40 flex items-center justify-center bg-black bg-opacity-40 px-4" onClick={onClose}>
|
||||||
|
<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">{entry.label || `Store ${entry.id}`}</p>
|
||||||
|
</div>
|
||||||
|
<div className="px-2 py-4">
|
||||||
|
<DateRange
|
||||||
|
onChange={(ranges) => {
|
||||||
|
const { startDate, endDate } = ranges.selection;
|
||||||
|
onSelectRange(startDate, endDate);
|
||||||
|
}}
|
||||||
|
moveRangeOnFirstSelection={false}
|
||||||
|
ranges={[
|
||||||
|
buildSelectionRange(
|
||||||
|
entry.desiredDateRange?.start,
|
||||||
|
entry.desiredDateRange?.end,
|
||||||
|
minDate
|
||||||
|
)
|
||||||
|
]}
|
||||||
|
rangeColors={['#2563EB']}
|
||||||
|
months={1}
|
||||||
|
direction="horizontal"
|
||||||
|
showDateDisplay={false}
|
||||||
|
locale={de}
|
||||||
|
minDate={minDate}
|
||||||
|
/>
|
||||||
|
</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={onResetRange}>
|
||||||
|
Zurücksetzen
|
||||||
|
</button>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<button type="button" className="text-sm text-gray-600 hover:text-gray-900" onClick={onClose}>
|
||||||
|
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={onClose}
|
||||||
|
>
|
||||||
|
Fertig
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RangePickerModal;
|
||||||
78
src/hooks/usePickupConfig.js
Normal file
78
src/hooks/usePickupConfig.js
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
|
import { sortEntriesByLabel } from '../utils/configUtils';
|
||||||
|
|
||||||
|
const API_URL = '/api/iobroker/pickup-config';
|
||||||
|
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
|
|
||||||
|
const STATIC_CONFIG = [
|
||||||
|
{ 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' }
|
||||||
|
];
|
||||||
|
|
||||||
|
const usePickupConfig = () => {
|
||||||
|
const [config, setConfig] = useState([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [status, setStatus] = useState('');
|
||||||
|
const [error, setError] = useState('');
|
||||||
|
|
||||||
|
const fetchConfig = useCallback(async () => {
|
||||||
|
setLoading(true);
|
||||||
|
setError('');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// In einer echten Implementierung würde hier die API aufgerufen:
|
||||||
|
// const response = await fetch(API_URL);
|
||||||
|
// const data = await response.json();
|
||||||
|
await delay(500);
|
||||||
|
setConfig(sortEntriesByLabel(STATIC_CONFIG));
|
||||||
|
} catch (err) {
|
||||||
|
setError('Fehler beim Laden der Konfiguration: ' + err.message);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const saveConfig = useCallback(async () => {
|
||||||
|
setStatus('Speichere...');
|
||||||
|
setError('');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// API-Aufruf zum Speichern der Konfiguration:
|
||||||
|
// await fetch(API_URL, { method: 'POST', body: JSON.stringify(config) });
|
||||||
|
await delay(1000);
|
||||||
|
setStatus('Konfiguration erfolgreich gespeichert!');
|
||||||
|
setTimeout(() => setStatus(''), 3000);
|
||||||
|
} catch (err) {
|
||||||
|
setError('Fehler beim Speichern: ' + err.message);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchConfig();
|
||||||
|
}, [fetchConfig]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
API_URL,
|
||||||
|
config,
|
||||||
|
setConfig,
|
||||||
|
loading,
|
||||||
|
status,
|
||||||
|
error,
|
||||||
|
fetchConfig,
|
||||||
|
saveConfig
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default usePickupConfig;
|
||||||
5
src/utils/configUtils.js
Normal file
5
src/utils/configUtils.js
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export const sortEntriesByLabel = (entries = []) => {
|
||||||
|
return [...entries].sort((a, b) =>
|
||||||
|
(a?.label || '').localeCompare(b?.label || '', 'de', { sensitivity: 'base' })
|
||||||
|
);
|
||||||
|
};
|
||||||
51
src/utils/dateUtils.js
Normal file
51
src/utils/dateUtils.js
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import { format, parseISO, isValid, startOfDay } from 'date-fns';
|
||||||
|
import { de } from 'date-fns/locale';
|
||||||
|
|
||||||
|
export const parseDateValue = (value) => {
|
||||||
|
if (!value) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const parsed = parseISO(value);
|
||||||
|
return isValid(parsed) ? parsed : null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const formatDateValue = (date) => {
|
||||||
|
if (!(date instanceof Date) || !isValid(date)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return format(date, 'yyyy-MM-dd');
|
||||||
|
};
|
||||||
|
|
||||||
|
export 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';
|
||||||
|
};
|
||||||
|
|
||||||
|
export 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'
|
||||||
|
};
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user