diff --git a/src/PickupConfigEditor.js b/src/PickupConfigEditor.js index 2affb55..47e75eb 100644 --- a/src/PickupConfigEditor.js +++ b/src/PickupConfigEditor.js @@ -1,81 +1,23 @@ -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' }) - ); -}; +import { useEffect, useState } from 'react'; +import { startOfDay } from 'date-fns'; +import PickupConfigTable from './components/PickupConfigTable'; +import RangePickerModal from './components/RangePickerModal'; +import usePickupConfig from './hooks/usePickupConfig'; +import { formatDateValue } from './utils/dateUtils'; const PickupConfigEditor = () => { - const [config, setConfig] = useState([]); - const [loading, setLoading] = useState(true); - const [status, setStatus] = useState(''); - const [error, setError] = useState(''); + const { + config, + setConfig, + loading, + status, + error, + fetchConfig, + saveConfig + } = usePickupConfig(); 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; @@ -86,68 +28,6 @@ const PickupConfigEditor = () => { } }, [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; @@ -233,96 +113,15 @@ const PickupConfigEditor = () => { )} -
- - - - - - - - - - - - - {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 ( - - - - - - - - - ); - })} - -
AktivGeschäftProfil prüfenNur benachrichtigenWochentagDatum / Zeitraum
- handleToggleActive(index)} - className="h-5 w-5" - /> - - {item.label} - - handleToggleProfileCheck(index)} - className="h-5 w-5" - /> - - handleToggleOnlyNotify(index)} - className="h-5 w-5" - /> - - - - -
-
+
- {activeRangeEntry && !activeRangeEntry.desiredWeekday && ( -
setActiveRangePicker(null)} - > -
event.stopPropagation()} - > -
-

Zeitraum auswählen für

-

- {activeRangeEntry.label || `Store ${activeRangeEntry.id}`} -

-
-
- { - 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} - /> -
-
- -
- - -
-
-
-
+ {activeRangeEntry && ( + + handleDateRangeSelection(activeRangeEntry.id, startDate, endDate) + } + onResetRange={() => { + handleDateRangeSelection(activeRangeEntry.id, null, null); + setActiveRangePicker(null); + }} + onClose={() => setActiveRangePicker(null)} + /> )} ); diff --git a/src/components/PickupConfigTable.js b/src/components/PickupConfigTable.js new file mode 100644 index 0000000..0400784 --- /dev/null +++ b/src/components/PickupConfigTable.js @@ -0,0 +1,106 @@ +import { formatRangeLabel } from '../utils/dateUtils'; + +const PickupConfigTable = ({ + config, + weekdays, + onToggleActive, + onToggleProfileCheck, + onToggleOnlyNotify, + onWeekdayChange, + onRangePickerRequest +}) => { + return ( +
+ + + + + + + + + + + + + {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 ( + + + + + + + + + ); + })} + +
AktivGeschäftProfil prüfenNur benachrichtigenWochentagDatum / Zeitraum
+ onToggleActive(index)} + className="h-5 w-5" + /> + + {item.label} + + onToggleProfileCheck(index)} + className="h-5 w-5" + /> + + onToggleOnlyNotify(index)} + className="h-5 w-5" + /> + + + + +
+
+ ); +}; + +export default PickupConfigTable; diff --git a/src/components/RangePickerModal.js b/src/components/RangePickerModal.js new file mode 100644 index 0000000..b45cc04 --- /dev/null +++ b/src/components/RangePickerModal.js @@ -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 ( +
+
event.stopPropagation()} + > +
+

Zeitraum auswählen für

+

{entry.label || `Store ${entry.id}`}

+
+
+ { + 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} + /> +
+
+ +
+ + +
+
+
+
+ ); +}; + +export default RangePickerModal; diff --git a/src/hooks/usePickupConfig.js b/src/hooks/usePickupConfig.js new file mode 100644 index 0000000..2d9b687 --- /dev/null +++ b/src/hooks/usePickupConfig.js @@ -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; diff --git a/src/utils/configUtils.js b/src/utils/configUtils.js new file mode 100644 index 0000000..1f3d3ce --- /dev/null +++ b/src/utils/configUtils.js @@ -0,0 +1,5 @@ +export const sortEntriesByLabel = (entries = []) => { + return [...entries].sort((a, b) => + (a?.label || '').localeCompare(b?.label || '', 'de', { sensitivity: 'base' }) + ); +}; diff --git a/src/utils/dateUtils.js b/src/utils/dateUtils.js new file mode 100644 index 0000000..9da40f3 --- /dev/null +++ b/src/utils/dateUtils.js @@ -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' + }; +};