diff --git a/package-lock.json b/package-lock.json index 957db8c..076de37 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,9 +15,11 @@ "axios": "^1.7.7", "body-parser": "^1.20.2", "cors": "^2.8.5", + "date-fns": "^4.1.0", "express": "^4.18.2", "node-cron": "^3.0.3", "react": "^19.1.0", + "react-date-range": "^2.0.1", "react-dom": "^19.1.0", "react-router-dom": "^7.9.5", "uuid": "^11.0.3", @@ -6241,6 +6243,12 @@ "dev": true, "license": "MIT" }, + "node_modules/classnames": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", + "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==", + "license": "MIT" + }, "node_modules/clean-css": { "version": "5.3.3", "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.3.tgz", @@ -7142,6 +7150,17 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/date-fns": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", + "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==", + "license": "MIT", + "peer": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, "node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -12324,7 +12343,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "dev": true, "license": "MIT", "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" @@ -14901,7 +14919,6 @@ "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", - "dev": true, "license": "MIT", "dependencies": { "loose-envify": "^1.4.0", @@ -14913,7 +14930,6 @@ "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "dev": true, "license": "MIT" }, "node_modules/proxy-addr": { @@ -15085,6 +15101,22 @@ "node": ">=14" } }, + "node_modules/react-date-range": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/react-date-range/-/react-date-range-2.0.1.tgz", + "integrity": "sha512-jwKYc9zcjYMg2hWbPht+6BF2wjGG5DkRVNJLRXn2Y0B/QCOOnvQX6YXziZVujVADWmgsBaoQnILdmzYw+Bwh0g==", + "license": "MIT", + "dependencies": { + "classnames": "^2.2.6", + "prop-types": "^15.7.2", + "react-list": "^0.8.13", + "shallow-equal": "^1.2.1" + }, + "peerDependencies": { + "date-fns": "3.0.6 || >=3.0.0", + "react": "^0.14 || ^15.0.0-rc || >=15.0" + } + }, "node_modules/react-dev-utils": { "version": "12.0.1", "resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-12.0.1.tgz", @@ -15222,6 +15254,15 @@ "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", "license": "MIT" }, + "node_modules/react-list": { + "version": "0.8.18", + "resolved": "https://registry.npmjs.org/react-list/-/react-list-0.8.18.tgz", + "integrity": "sha512-1OSdDvzuKuwDJvQNuhXxxL+jTmmdtKg1i6KtYgxI9XR98kbOql1FcSGP+Lcvo91fk3cYng+Z6YkC6X9HRJwxfw==", + "license": "MIT", + "peerDependencies": { + "react": "0.14 || 15 - 19" + } + }, "node_modules/react-refresh": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz", @@ -16268,6 +16309,12 @@ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", "license": "ISC" }, + "node_modules/shallow-equal": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/shallow-equal/-/shallow-equal-1.2.1.tgz", + "integrity": "sha512-S4vJDjHHMBaiZuT9NPb616CSmLf618jawtv3sufLl6ivK8WocjAo58cXwbRV1cgqxH0Qbv+iUt6m05eqEa2IRA==", + "license": "MIT" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", diff --git a/package.json b/package.json index a1a85a6..97bdf4e 100644 --- a/package.json +++ b/package.json @@ -10,9 +10,11 @@ "axios": "^1.7.7", "body-parser": "^1.20.2", "cors": "^2.8.5", + "date-fns": "^4.1.0", "express": "^4.18.2", "node-cron": "^3.0.3", "react": "^19.1.0", + "react-date-range": "^2.0.1", "react-dom": "^19.1.0", "react-router-dom": "^7.9.5", "uuid": "^11.0.3", diff --git a/src/App.js b/src/App.js index a084d0c..729e3f2 100644 --- a/src/App.js +++ b/src/App.js @@ -1,8 +1,54 @@ import React, { useState, useEffect, useCallback, useMemo } from 'react'; import { BrowserRouter as Router, Routes, Route, Navigate, Link, useLocation, useNavigate } from 'react-router-dom'; +import { DateRange } from 'react-date-range'; +import { format, parseISO, isValid } from 'date-fns'; +import { de } from 'date-fns/locale'; import './App.css'; +import 'react-date-range/dist/styles.css'; +import 'react-date-range/dist/theme/default.css'; const TOKEN_STORAGE_KEY = 'pickupConfigToken'; +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' + }; +}; function App() { const [session, setSession] = useState(null); @@ -29,6 +75,7 @@ function App() { const [pendingNavigation, setPendingNavigation] = useState(null); const [dirtyDialogSaving, setDirtyDialogSaving] = useState(false); const [confirmDialog, setConfirmDialog] = useState({ open: false, resolve: null }); + const [activeRangePicker, setActiveRangePicker] = useState(null); const weekdays = ['Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'Samstag', 'Sonntag']; @@ -787,46 +834,26 @@ function App() { return updated; }) ); + if (value) { + setActiveRangePicker((prev) => (prev === entryId ? null : prev)); + } }; - const handleDateRangeChange = (entryId, field, value) => { + const handleDateRangeSelection = useCallback((entryId, startDate, endDate) => { setIsDirty(true); setConfig((prev) => prev.map((item) => { if (item.id !== entryId) { return item; } - const sanitizedValue = value || null; - const currentRange = item.desiredDateRange - ? { ...item.desiredDateRange } - : item.desiredDate - ? { start: item.desiredDate, end: item.desiredDate } - : { start: null, end: null }; - const nextRange = { - start: currentRange.start || null, - end: currentRange.end || null - }; - if (field === 'start') { - nextRange.start = sanitizedValue; - if (!nextRange.start) { - nextRange.end = null; - } else if (!nextRange.end || nextRange.end < nextRange.start) { - nextRange.end = nextRange.start; - } - } else if (field === 'end') { - nextRange.end = sanitizedValue; - 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); const updated = { ...item }; - if (hasRange) { - updated.desiredDateRange = nextRange; + const startValue = formatDateValue(startDate); + const endValue = formatDateValue(endDate); + if (startValue || endValue) { + updated.desiredDateRange = { + start: startValue || endValue, + end: endValue || startValue + }; if (updated.desiredWeekday) { delete updated.desiredWeekday; } @@ -839,7 +866,7 @@ function App() { return updated; }) ); - }; + }, [setConfig, setIsDirty]); const configMap = useMemo(() => { const map = new Map(); @@ -853,6 +880,16 @@ function App() { const visibleConfig = useMemo(() => config.filter((item) => !item.hidden), [config]); + useEffect(() => { + if (!activeRangePicker) { + return; + } + const stillExists = config.some((item) => item.id === activeRangePicker); + if (!stillExists) { + setActiveRangePicker(null); + } + }, [activeRangePicker, config]); + const handleStoreSelection = async (store) => { const storeId = String(store.id); const existing = configMap.get(storeId); @@ -1305,30 +1342,64 @@ function App() { ))} -
Ein einzelner Tag: Start und Ende identisch wählen.
-Ein einzelner Tag: Start = Ende.
+