refactoring
This commit is contained in:
@@ -3,21 +3,33 @@ import { startOfDay } from 'date-fns';
|
|||||||
import PickupConfigTable from './components/PickupConfigTable';
|
import PickupConfigTable from './components/PickupConfigTable';
|
||||||
import RangePickerModal from './components/RangePickerModal';
|
import RangePickerModal from './components/RangePickerModal';
|
||||||
import usePickupConfig from './hooks/usePickupConfig';
|
import usePickupConfig from './hooks/usePickupConfig';
|
||||||
import { formatDateValue } from './utils/dateUtils';
|
import 'react-date-range/dist/styles.css';
|
||||||
|
import 'react-date-range/dist/theme/default.css';
|
||||||
|
|
||||||
const PickupConfigEditor = () => {
|
const PickupConfigEditor = () => {
|
||||||
const {
|
const {
|
||||||
config,
|
config,
|
||||||
setConfig,
|
|
||||||
loading,
|
loading,
|
||||||
status,
|
status,
|
||||||
error,
|
error,
|
||||||
fetchConfig,
|
fetchConfig,
|
||||||
saveConfig
|
saveConfig,
|
||||||
|
toggleActive,
|
||||||
|
toggleProfileCheck,
|
||||||
|
toggleOnlyNotify,
|
||||||
|
changeWeekday,
|
||||||
|
updateDateRange
|
||||||
} = usePickupConfig();
|
} = usePickupConfig();
|
||||||
const [activeRangePicker, setActiveRangePicker] = useState(null);
|
const [activeRangePicker, setActiveRangePicker] = useState(null);
|
||||||
const minSelectableDate = startOfDay(new Date());
|
const minSelectableDate = startOfDay(new Date());
|
||||||
|
|
||||||
|
const handleWeekdayChange = (index, value, entryId) => {
|
||||||
|
changeWeekday(index, value);
|
||||||
|
if (value && entryId) {
|
||||||
|
setActiveRangePicker((prev) => (prev === entryId ? null : prev));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!activeRangePicker) {
|
if (!activeRangePicker) {
|
||||||
return;
|
return;
|
||||||
@@ -28,65 +40,6 @@ const PickupConfigEditor = () => {
|
|||||||
}
|
}
|
||||||
}, [activeRangePicker, config]);
|
}, [activeRangePicker, config]);
|
||||||
|
|
||||||
const handleToggleActive = (index) => {
|
|
||||||
const newConfig = [...config];
|
|
||||||
newConfig[index].active = !newConfig[index].active;
|
|
||||||
setConfig(newConfig);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleToggleProfileCheck = (index) => {
|
|
||||||
const newConfig = [...config];
|
|
||||||
newConfig[index].checkProfileId = !newConfig[index].checkProfileId;
|
|
||||||
setConfig(newConfig);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleToggleOnlyNotify = (index) => {
|
|
||||||
const newConfig = [...config];
|
|
||||||
newConfig[index].onlyNotify = !newConfig[index].onlyNotify;
|
|
||||||
setConfig(newConfig);
|
|
||||||
};
|
|
||||||
|
|
||||||
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 handleDateRangeSelection = (entryId, startDate, endDate) => {
|
|
||||||
const startValue = formatDateValue(startDate);
|
|
||||||
const endValue = formatDateValue(endDate);
|
|
||||||
setConfig((prev) =>
|
|
||||||
prev.map((item) => {
|
|
||||||
if (item.id !== entryId) {
|
|
||||||
return item;
|
|
||||||
}
|
|
||||||
const updated = { ...item };
|
|
||||||
if (startValue || endValue) {
|
|
||||||
updated.desiredDateRange = {
|
|
||||||
start: startValue || endValue,
|
|
||||||
end: endValue || startValue
|
|
||||||
};
|
|
||||||
if (updated.desiredWeekday) {
|
|
||||||
delete updated.desiredWeekday;
|
|
||||||
}
|
|
||||||
} else if (updated.desiredDateRange) {
|
|
||||||
delete updated.desiredDateRange;
|
|
||||||
}
|
|
||||||
if (updated.desiredDate) {
|
|
||||||
delete updated.desiredDate;
|
|
||||||
}
|
|
||||||
return updated;
|
|
||||||
})
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const activeRangeEntry = activeRangePicker
|
const activeRangeEntry = activeRangePicker
|
||||||
? config.find((item) => item.id === activeRangePicker) || null
|
? config.find((item) => item.id === activeRangePicker) || null
|
||||||
: null;
|
: null;
|
||||||
@@ -116,9 +69,9 @@ const PickupConfigEditor = () => {
|
|||||||
<PickupConfigTable
|
<PickupConfigTable
|
||||||
config={config}
|
config={config}
|
||||||
weekdays={weekdays}
|
weekdays={weekdays}
|
||||||
onToggleActive={handleToggleActive}
|
onToggleActive={toggleActive}
|
||||||
onToggleProfileCheck={handleToggleProfileCheck}
|
onToggleProfileCheck={toggleProfileCheck}
|
||||||
onToggleOnlyNotify={handleToggleOnlyNotify}
|
onToggleOnlyNotify={toggleOnlyNotify}
|
||||||
onWeekdayChange={handleWeekdayChange}
|
onWeekdayChange={handleWeekdayChange}
|
||||||
onRangePickerRequest={setActiveRangePicker}
|
onRangePickerRequest={setActiveRangePicker}
|
||||||
/>
|
/>
|
||||||
@@ -150,10 +103,10 @@ const PickupConfigEditor = () => {
|
|||||||
entry={activeRangeEntry}
|
entry={activeRangeEntry}
|
||||||
minDate={minSelectableDate}
|
minDate={minSelectableDate}
|
||||||
onSelectRange={(startDate, endDate) =>
|
onSelectRange={(startDate, endDate) =>
|
||||||
handleDateRangeSelection(activeRangeEntry.id, startDate, endDate)
|
updateDateRange(activeRangeEntry.id, startDate, endDate)
|
||||||
}
|
}
|
||||||
onResetRange={() => {
|
onResetRange={() => {
|
||||||
handleDateRangeSelection(activeRangeEntry.id, null, null);
|
updateDateRange(activeRangeEntry.id, null, null);
|
||||||
setActiveRangePicker(null);
|
setActiveRangePicker(null);
|
||||||
}}
|
}}
|
||||||
onClose={() => setActiveRangePicker(null)}
|
onClose={() => setActiveRangePicker(null)}
|
||||||
|
|||||||
@@ -32,8 +32,9 @@ const PickupConfigTable = ({
|
|||||||
const rangeStart = normalizedRange?.start || '';
|
const rangeStart = normalizedRange?.start || '';
|
||||||
const rangeEnd = normalizedRange?.end || '';
|
const rangeEnd = normalizedRange?.end || '';
|
||||||
const hasDateRange = Boolean(rangeStart || rangeEnd);
|
const hasDateRange = Boolean(rangeStart || rangeEnd);
|
||||||
|
const rowKey = item.id ? `${item.id}-${index}` : `row-${index}`;
|
||||||
return (
|
return (
|
||||||
<tr key={item.id || index} className={index % 2 === 0 ? 'bg-gray-50' : 'bg-white'}>
|
<tr key={rowKey} className={index % 2 === 0 ? 'bg-gray-50' : 'bg-white'}>
|
||||||
<td className="px-4 py-2 text-center">
|
<td className="px-4 py-2 text-center">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
@@ -64,7 +65,7 @@ const PickupConfigTable = ({
|
|||||||
<td className="px-4 py-2">
|
<td className="px-4 py-2">
|
||||||
<select
|
<select
|
||||||
value={item.desiredWeekday || ''}
|
value={item.desiredWeekday || ''}
|
||||||
onChange={(e) => onWeekdayChange(index, e.target.value)}
|
onChange={(e) => onWeekdayChange(index, e.target.value, item.id)}
|
||||||
className="border rounded p-1 w-full"
|
className="border rounded p-1 w-full"
|
||||||
disabled={hasDateRange}
|
disabled={hasDateRange}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
import { DateRange } from 'react-date-range';
|
import { DateRange } from 'react-date-range';
|
||||||
import { de } from 'date-fns/locale';
|
import { de } from 'date-fns/locale';
|
||||||
import { buildSelectionRange } from '../utils/dateUtils';
|
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 }) => {
|
const RangePickerModal = ({ entry, minDate, onSelectRange, onResetRange, onClose }) => {
|
||||||
if (!entry || entry.desiredWeekday) {
|
if (!entry || entry.desiredWeekday) {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useCallback, useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
import { sortEntriesByLabel } from '../utils/configUtils';
|
import { sortEntriesByLabel } from '../utils/configUtils';
|
||||||
|
import { formatDateValue } from '../utils/dateUtils';
|
||||||
|
|
||||||
const API_URL = '/api/iobroker/pickup-config';
|
const API_URL = '/api/iobroker/pickup-config';
|
||||||
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
@@ -59,6 +60,73 @@ const usePickupConfig = () => {
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const updateEntryByIndex = useCallback((index, updater) => {
|
||||||
|
setConfig((prev) => {
|
||||||
|
if (!prev[index]) {
|
||||||
|
return prev;
|
||||||
|
}
|
||||||
|
const next = [...prev];
|
||||||
|
next[index] = updater(next[index]);
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const updateEntryById = useCallback((entryId, updater) => {
|
||||||
|
setConfig((prev) =>
|
||||||
|
prev.map((item) => {
|
||||||
|
if (item.id !== entryId) {
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
return updater(item);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const toggleActive = useCallback((index) => {
|
||||||
|
updateEntryByIndex(index, (entry) => ({ ...entry, active: !entry.active }));
|
||||||
|
}, [updateEntryByIndex]);
|
||||||
|
|
||||||
|
const toggleProfileCheck = useCallback((index) => {
|
||||||
|
updateEntryByIndex(index, (entry) => ({ ...entry, checkProfileId: !entry.checkProfileId }));
|
||||||
|
}, [updateEntryByIndex]);
|
||||||
|
|
||||||
|
const toggleOnlyNotify = useCallback((index) => {
|
||||||
|
updateEntryByIndex(index, (entry) => ({ ...entry, onlyNotify: !entry.onlyNotify }));
|
||||||
|
}, [updateEntryByIndex]);
|
||||||
|
|
||||||
|
const changeWeekday = useCallback((index, value) => {
|
||||||
|
updateEntryByIndex(index, (entry) => {
|
||||||
|
const next = { ...entry, desiredWeekday: value };
|
||||||
|
if (next.desiredDateRange) {
|
||||||
|
delete next.desiredDateRange;
|
||||||
|
}
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
}, [updateEntryByIndex]);
|
||||||
|
|
||||||
|
const updateDateRange = useCallback((entryId, startDate, endDate) => {
|
||||||
|
const startValue = formatDateValue(startDate);
|
||||||
|
const endValue = formatDateValue(endDate);
|
||||||
|
updateEntryById(entryId, (entry) => {
|
||||||
|
const next = { ...entry };
|
||||||
|
if (startValue || endValue) {
|
||||||
|
next.desiredDateRange = {
|
||||||
|
start: startValue || endValue,
|
||||||
|
end: endValue || startValue
|
||||||
|
};
|
||||||
|
if (next.desiredWeekday) {
|
||||||
|
delete next.desiredWeekday;
|
||||||
|
}
|
||||||
|
} else if (next.desiredDateRange) {
|
||||||
|
delete next.desiredDateRange;
|
||||||
|
}
|
||||||
|
if (next.desiredDate) {
|
||||||
|
delete next.desiredDate;
|
||||||
|
}
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
}, [updateEntryById]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchConfig();
|
fetchConfig();
|
||||||
}, [fetchConfig]);
|
}, [fetchConfig]);
|
||||||
@@ -66,12 +134,16 @@ const usePickupConfig = () => {
|
|||||||
return {
|
return {
|
||||||
API_URL,
|
API_URL,
|
||||||
config,
|
config,
|
||||||
setConfig,
|
|
||||||
loading,
|
loading,
|
||||||
status,
|
status,
|
||||||
error,
|
error,
|
||||||
fetchConfig,
|
fetchConfig,
|
||||||
saveConfig
|
saveConfig,
|
||||||
|
toggleActive,
|
||||||
|
toggleProfileCheck,
|
||||||
|
toggleOnlyNotify,
|
||||||
|
changeWeekday,
|
||||||
|
updateDateRange
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
72
src/hooks/usePickupConfig.test.js
Normal file
72
src/hooks/usePickupConfig.test.js
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { act, render } from '@testing-library/react';
|
||||||
|
import usePickupConfig from './usePickupConfig';
|
||||||
|
|
||||||
|
describe('usePickupConfig', () => {
|
||||||
|
const advanceTimers = async (ms = 0) => {
|
||||||
|
await act(async () => {
|
||||||
|
jest.advanceTimersByTime(ms);
|
||||||
|
await Promise.resolve();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderHook = () => {
|
||||||
|
let latest;
|
||||||
|
const TestHarness = () => {
|
||||||
|
latest = usePickupConfig();
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
render(<TestHarness />);
|
||||||
|
return () => latest;
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.useFakeTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.useRealTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('fetchConfig loads sorted configuration', async () => {
|
||||||
|
const getHook = renderHook();
|
||||||
|
await advanceTimers(600);
|
||||||
|
const hook = getHook();
|
||||||
|
expect(hook.loading).toBe(false);
|
||||||
|
expect(hook.config[0].label).toBe('Aldi Biblisweg');
|
||||||
|
expect(hook.config[hook.config.length - 1].label).toBe('Penny Baden-Oos');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('mutators update entries as expected', async () => {
|
||||||
|
const getHook = renderHook();
|
||||||
|
await advanceTimers(600);
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
getHook().toggleActive(0);
|
||||||
|
getHook().toggleProfileCheck(0);
|
||||||
|
getHook().toggleOnlyNotify(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
let updated = getHook().config[0];
|
||||||
|
expect(updated.active).toBe(true);
|
||||||
|
expect(updated.checkProfileId).toBe(false);
|
||||||
|
expect(updated.onlyNotify).toBe(true);
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
getHook().changeWeekday(0, 'Montag');
|
||||||
|
});
|
||||||
|
updated = getHook().config[0];
|
||||||
|
expect(updated.desiredWeekday).toBe('Montag');
|
||||||
|
expect(updated.desiredDateRange).toBeUndefined();
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
getHook().updateDateRange(updated.id, new Date('2025-01-01'), new Date('2025-01-03'));
|
||||||
|
});
|
||||||
|
updated = getHook().config[0];
|
||||||
|
expect(updated.desiredDateRange).toEqual({
|
||||||
|
start: '2025-01-01',
|
||||||
|
end: '2025-01-03'
|
||||||
|
});
|
||||||
|
expect(updated.desiredWeekday).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,5 +1,16 @@
|
|||||||
export const sortEntriesByLabel = (entries = []) => {
|
export const sortEntriesByLabel = (entries = []) => {
|
||||||
return [...entries].sort((a, b) =>
|
return [...entries].sort((a, b) => {
|
||||||
(a?.label || '').localeCompare(b?.label || '', 'de', { sensitivity: 'base' })
|
const labelA = (a?.label || '').trim();
|
||||||
);
|
const labelB = (b?.label || '').trim();
|
||||||
|
if (!labelA && !labelB) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (!labelA) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (!labelB) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return labelA.localeCompare(labelB, 'de', { sensitivity: 'base' });
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
21
src/utils/configUtils.test.js
Normal file
21
src/utils/configUtils.test.js
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { sortEntriesByLabel } from './configUtils';
|
||||||
|
|
||||||
|
describe('configUtils', () => {
|
||||||
|
test('sortEntriesByLabel orders entries alphabetically', () => {
|
||||||
|
const entries = [
|
||||||
|
{ label: 'Zeta Markt' },
|
||||||
|
{ label: 'alpha Shop' },
|
||||||
|
{ label: 'Beta Store' }
|
||||||
|
];
|
||||||
|
const sorted = sortEntriesByLabel(entries);
|
||||||
|
expect(sorted.map((item) => item.label)).toEqual(['alpha Shop', 'Beta Store', 'Zeta Markt']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('sortEntriesByLabel handles missing labels', () => {
|
||||||
|
const entries = [{}, { label: 'B' }, { label: 'A' }];
|
||||||
|
const sorted = sortEntriesByLabel(entries);
|
||||||
|
expect(sorted[0].label).toBe('A');
|
||||||
|
expect(sorted[1].label).toBe('B');
|
||||||
|
expect(sorted[2].label).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
27
src/utils/dateUtils.test.js
Normal file
27
src/utils/dateUtils.test.js
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { buildSelectionRange, formatDateValue, formatRangeLabel, parseDateValue } from './dateUtils';
|
||||||
|
|
||||||
|
describe('dateUtils', () => {
|
||||||
|
test('parseDateValue returns null for invalid input', () => {
|
||||||
|
expect(parseDateValue('invalid')).toBeNull();
|
||||||
|
expect(parseDateValue('')).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('formatDateValue formats valid dates', () => {
|
||||||
|
const date = new Date('2024-02-10T00:00:00.000Z');
|
||||||
|
expect(formatDateValue(date)).toBe('2024-02-10');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('formatRangeLabel handles ranges and single dates', () => {
|
||||||
|
expect(formatRangeLabel('2024-02-10', '2024-02-10')).toBe('10.02.2024');
|
||||||
|
expect(formatRangeLabel('2024-02-10', '2024-02-12')).toBe('10.02.2024 – 12.02.2024');
|
||||||
|
expect(formatRangeLabel(null, null)).toBe('Zeitraum auswählen');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('buildSelectionRange enforces minimum date', () => {
|
||||||
|
const minDate = new Date('2024-05-01T00:00:00.000Z');
|
||||||
|
const selection = buildSelectionRange('2024-04-01', '2024-04-05', minDate);
|
||||||
|
expect(selection.startDate).toEqual(minDate);
|
||||||
|
expect(selection.endDate).toEqual(minDate);
|
||||||
|
expect(selection.key).toBe('selection');
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user