feat: add calendar date-range picker for slots

This commit is contained in:
2025-11-09 19:31:48 +01:00
parent 4cebada1ed
commit bb65ab8bed
4 changed files with 293 additions and 109 deletions

View File

@@ -1,10 +1,58 @@
import { useState, useEffect } from 'react';
import { DateRange } from 'react-date-range';
import { format, parseISO, isValid } 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) => {
const startDate = parseDateValue(start) || parseDateValue(end) || new Date();
const endDate = parseDateValue(end) || parseDateValue(start) || startDate;
return {
startDate,
endDate,
key: 'selection'
};
};
const PickupConfigEditor = () => {
const [config, setConfig] = useState([]);
const [loading, setLoading] = useState(true);
const [status, setStatus] = useState('');
const [error, setError] = useState('');
const [activeRangePicker, setActiveRangePicker] = useState(null);
// Simulierte API-Endpunkte - diese müssen in Ihrer tatsächlichen Implementierung angepasst werden
const API_URL = '/api/iobroker/pickup-config';
@@ -96,44 +144,26 @@ const PickupConfigEditor = () => {
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 handleDateRangeChange = (index, field, value) => {
const handleDateRangeSelection = (index, startDate, endDate) => {
const newConfig = [...config];
const currentRange = newConfig[index].desiredDateRange
? { ...newConfig[index].desiredDateRange }
: newConfig[index].desiredDate
? { start: newConfig[index].desiredDate, end: newConfig[index].desiredDate }
: { start: null, end: null };
const nextRange = {
start: currentRange.start || null,
end: currentRange.end || null
};
if (field === 'start') {
nextRange.start = value || null;
if (!nextRange.start) {
nextRange.end = null;
} else if (!nextRange.end || nextRange.end < nextRange.start) {
nextRange.end = nextRange.start;
}
} else if (field === 'end') {
nextRange.end = value || null;
if (!nextRange.end && nextRange.start) {
nextRange.end = nextRange.start;
} else if (!nextRange.start && nextRange.end) {
nextRange.start = nextRange.end;
} else if (nextRange.start && nextRange.end < nextRange.start) {
nextRange.start = nextRange.end;
}
}
const hasRange = Boolean(nextRange.start || nextRange.end);
if (hasRange) {
newConfig[index].desiredDateRange = nextRange;
const startValue = formatDateValue(startDate);
const endValue = formatDateValue(endDate);
if (startValue || endValue) {
newConfig[index].desiredDateRange = {
start: startValue || endValue,
end: endValue || startValue
};
if (newConfig[index].desiredWeekday) {
delete newConfig[index].desiredWeekday;
}
@@ -235,32 +265,66 @@ const PickupConfigEditor = () => {
</select>
</td>
<td className="px-4 py-2">
<div className="border rounded p-2 bg-gray-50">
<label className="block text-xs text-gray-500 mb-1">Datum / Zeitraum</label>
<div className="flex items-center gap-2">
<input
type="date"
value={rangeStart}
onChange={(e) => handleDateRangeChange(index, 'start', e.target.value)}
className="border rounded p-1 w-full"
disabled={Boolean(item.desiredWeekday)}
/>
<span className="text-xs text-gray-500">bis</span>
<input
type="date"
value={rangeEnd}
onChange={(e) => handleDateRangeChange(index, 'end', e.target.value)}
className="border rounded p-1 w-full"
disabled={Boolean(item.desiredWeekday)}
min={rangeStart || undefined}
/>
</div>
<p className="text-xs text-gray-500 mt-1">Ein einzelner Tag: Start = Ende.</p>
<div className="relative">
<button
type="button"
onClick={() => {
if (item.desiredWeekday) {
return;
}
setActiveRangePicker((prev) => (prev === item.id ? null : 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>
{activeRangePicker === item.id && !item.desiredWeekday && (
<div className="absolute z-20 mt-2 bg-white border border-gray-200 rounded-lg shadow-2xl">
<DateRange
onChange={(ranges) => {
const { startDate, endDate } = ranges.selection;
handleDateRangeSelection(index, startDate, endDate);
}}
moveRangeOnFirstSelection={false}
ranges={[buildSelectionRange(rangeStart, rangeEnd)]}
rangeColors={['#2563EB']}
months={1}
direction="horizontal"
showDateDisplay={false}
locale={de}
/>
<div className="flex items-center justify-between px-4 py-2 border-t text-sm bg-gray-50 rounded-b-lg">
<button
type="button"
className="text-gray-600 hover:text-gray-900"
onClick={() => {
handleDateRangeSelection(index, null, null);
setActiveRangePicker(null);
}}
>
Zurücksetzen
</button>
<button
type="button"
className="text-blue-600 font-semibold hover:text-blue-800"
onClick={() => setActiveRangePicker(null)}
>
Fertig
</button>
</div>
</div>
)}
</div>
</td>
</tr>
);
})}
})}
</tbody>
</table>
</div>