fix: restrict delete button to admins

This commit is contained in:
2025-11-10 09:27:55 +01:00
parent 104a8c2da6
commit c747d40d3d
6 changed files with 343 additions and 292 deletions

View File

@@ -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>
); );

View 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;

View 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;

View 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
View 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
View 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'
};
};