feat: support pickup booking date ranges
This commit is contained in:
@@ -29,6 +29,7 @@
|
|||||||
|
|
||||||
## Commit & Pull Request Guidelines
|
## Commit & Pull Request Guidelines
|
||||||
- Follow conventional commits (e.g., `feat: add mqtt auth fields`, `fix: debounce config saves`) and keep subjects ≤72 characters.
|
- Follow conventional commits (e.g., `feat: add mqtt auth fields`, `fix: debounce config saves`) and keep subjects ≤72 characters.
|
||||||
|
- After every change, refresh `.commitmessage` with the final commit text and ensure it is staged (e.g., `git add -f .commitmessage`) so tooling can reuse it automatically.
|
||||||
- Reference issues or MQTT topics impacted inside the body, and describe user-visible changes plus verification steps.
|
- Reference issues or MQTT topics impacted inside the body, and describe user-visible changes plus verification steps.
|
||||||
- PRs must include: summary of API/UI changes, screenshots or JSON samples when modifying config shape, notes on new env vars, and confirmation that `npm test` and `npm run build` succeed.
|
- PRs must include: summary of API/UI changes, screenshots or JSON samples when modifying config shape, notes on new env vars, and confirmation that `npm test` and `npm run build` succeed.
|
||||||
|
|
||||||
|
|||||||
@@ -78,17 +78,41 @@ async function ensureSession(session) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function matchesDesiredDate(pickupDate, desiredDate) {
|
function toDateValue(input) {
|
||||||
if (!desiredDate) {
|
if (!input) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const date = input instanceof Date ? new Date(input.getTime()) : new Date(input);
|
||||||
|
if (Number.isNaN(date.getTime())) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return new Date(date.getFullYear(), date.getMonth(), date.getDate()).getTime();
|
||||||
|
}
|
||||||
|
|
||||||
|
function matchesDesiredDate(pickupDate, desiredDate, desiredDateRange) {
|
||||||
|
const pickupValue = toDateValue(pickupDate);
|
||||||
|
if (pickupValue === null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasRange = Boolean(desiredDateRange && (desiredDateRange.start || desiredDateRange.end));
|
||||||
|
if (hasRange) {
|
||||||
|
const startValue = toDateValue(desiredDateRange.start);
|
||||||
|
const endValue = toDateValue(desiredDateRange.end);
|
||||||
|
if (startValue !== null && pickupValue < startValue) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (endValue !== null && pickupValue > endValue) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const desired = new Date(desiredDate);
|
const desiredValue = toDateValue(desiredDate);
|
||||||
return (
|
if (desiredValue === null) {
|
||||||
pickupDate.getFullYear() === desired.getFullYear() &&
|
return true;
|
||||||
pickupDate.getMonth() === desired.getMonth() &&
|
}
|
||||||
pickupDate.getDate() === desired.getDate()
|
return pickupValue === desiredValue;
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function matchesDesiredWeekday(pickupDate, desiredWeekday) {
|
function matchesDesiredWeekday(pickupDate, desiredWeekday) {
|
||||||
@@ -158,7 +182,7 @@ async function checkEntry(sessionId, entry, settings) {
|
|||||||
|
|
||||||
pickups.forEach((pickup) => {
|
pickups.forEach((pickup) => {
|
||||||
const pickupDate = new Date(pickup.date);
|
const pickupDate = new Date(pickup.date);
|
||||||
if (!matchesDesiredDate(pickupDate, entry.desiredDate)) {
|
if (!matchesDesiredDate(pickupDate, entry.desiredDate, entry.desiredDateRange)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!matchesDesiredWeekday(pickupDate, desiredWeekday)) {
|
if (!matchesDesiredWeekday(pickupDate, desiredWeekday)) {
|
||||||
|
|||||||
87
src/App.js
87
src/App.js
@@ -748,6 +748,9 @@ function App() {
|
|||||||
if (value && updated.desiredDate) {
|
if (value && updated.desiredDate) {
|
||||||
delete updated.desiredDate;
|
delete updated.desiredDate;
|
||||||
}
|
}
|
||||||
|
if (value && updated.desiredDateRange) {
|
||||||
|
delete updated.desiredDateRange;
|
||||||
|
}
|
||||||
return updated;
|
return updated;
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@@ -764,6 +767,49 @@ function App() {
|
|||||||
if (value && updated.desiredWeekday) {
|
if (value && updated.desiredWeekday) {
|
||||||
delete updated.desiredWeekday;
|
delete updated.desiredWeekday;
|
||||||
}
|
}
|
||||||
|
if (value && updated.desiredDateRange) {
|
||||||
|
delete updated.desiredDateRange;
|
||||||
|
}
|
||||||
|
return updated;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDateRangeChange = (entryId, field, value) => {
|
||||||
|
setIsDirty(true);
|
||||||
|
setConfig((prev) =>
|
||||||
|
prev.map((item) => {
|
||||||
|
if (item.id !== entryId) {
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
const nextRange = {
|
||||||
|
start: item.desiredDateRange?.start || null,
|
||||||
|
end: item.desiredDateRange?.end || null
|
||||||
|
};
|
||||||
|
if (field === 'start') {
|
||||||
|
nextRange.start = value || null;
|
||||||
|
if (nextRange.end && value && value > nextRange.end) {
|
||||||
|
nextRange.end = value;
|
||||||
|
}
|
||||||
|
} else if (field === 'end') {
|
||||||
|
nextRange.end = value || null;
|
||||||
|
if (nextRange.start && value && value < nextRange.start) {
|
||||||
|
nextRange.start = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const hasRange = Boolean(nextRange.start || nextRange.end);
|
||||||
|
const updated = { ...item };
|
||||||
|
if (hasRange) {
|
||||||
|
updated.desiredDateRange = nextRange;
|
||||||
|
if (updated.desiredWeekday) {
|
||||||
|
delete updated.desiredWeekday;
|
||||||
|
}
|
||||||
|
if (updated.desiredDate) {
|
||||||
|
delete updated.desiredDate;
|
||||||
|
}
|
||||||
|
} else if (updated.desiredDateRange) {
|
||||||
|
delete updated.desiredDateRange;
|
||||||
|
}
|
||||||
return updated;
|
return updated;
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@@ -1165,19 +1211,22 @@ function App() {
|
|||||||
<th className="px-4 py-2 border-b">Nur benachrichtigen</th>
|
<th className="px-4 py-2 border-b">Nur benachrichtigen</th>
|
||||||
<th className="px-4 py-2 border-b">Wochentag</th>
|
<th className="px-4 py-2 border-b">Wochentag</th>
|
||||||
<th className="px-4 py-2 border-b">Spezifisches Datum</th>
|
<th className="px-4 py-2 border-b">Spezifisches Datum</th>
|
||||||
|
<th className="px-4 py-2 border-b">Datumsbereich</th>
|
||||||
<th className="px-4 py-2 border-b">Aktionen</th>
|
<th className="px-4 py-2 border-b">Aktionen</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{visibleConfig.length === 0 && (
|
{visibleConfig.length === 0 && (
|
||||||
<tr>
|
<tr>
|
||||||
<td colSpan="7" className="px-4 py-6 text-center text-sm text-gray-500">
|
<td colSpan="8" className="px-4 py-6 text-center text-sm text-gray-500">
|
||||||
Keine sichtbaren Einträge. Nutze „Verfügbare Betriebe“, um Betriebe hinzuzufügen oder ausgeblendete Einträge zurückzuholen.
|
Keine sichtbaren Einträge. Nutze „Verfügbare Betriebe“, um Betriebe hinzuzufügen oder ausgeblendete Einträge zurückzuholen.
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
)}
|
)}
|
||||||
{visibleConfig.map((item, index) => (
|
{visibleConfig.map((item, index) => {
|
||||||
<tr key={`${item.id}-${index}`} className={index % 2 === 0 ? 'bg-gray-50' : 'bg-white'}>
|
const hasDateRange = Boolean(item.desiredDateRange?.start || item.desiredDateRange?.end);
|
||||||
|
return (
|
||||||
|
<tr key={`${item.id}-${index}`} className={index % 2 === 0 ? 'bg-gray-50' : 'bg-white'}>
|
||||||
<td className="px-4 py-2 border-b text-center">
|
<td className="px-4 py-2 border-b text-center">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
@@ -1214,7 +1263,7 @@ function App() {
|
|||||||
value={item.desiredWeekday || ''}
|
value={item.desiredWeekday || ''}
|
||||||
onChange={(e) => handleWeekdayChange(item.id, e.target.value)}
|
onChange={(e) => handleWeekdayChange(item.id, e.target.value)}
|
||||||
className="border rounded p-2 w-full bg-white focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
className="border rounded p-2 w-full bg-white focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||||
disabled={item.desiredDate}
|
disabled={item.desiredDate || hasDateRange}
|
||||||
>
|
>
|
||||||
<option value="">Kein Wochentag</option>
|
<option value="">Kein Wochentag</option>
|
||||||
{weekdays.map((day) => (
|
{weekdays.map((day) => (
|
||||||
@@ -1230,9 +1279,34 @@ function App() {
|
|||||||
value={item.desiredDate || ''}
|
value={item.desiredDate || ''}
|
||||||
onChange={(e) => handleDateChange(item.id, e.target.value)}
|
onChange={(e) => handleDateChange(item.id, e.target.value)}
|
||||||
className="border rounded p-2 w-full focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
className="border rounded p-2 w-full focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||||
disabled={item.desiredWeekday}
|
disabled={item.desiredWeekday || hasDateRange}
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
|
<td className="px-4 py-2 border-b">
|
||||||
|
<div className="grid grid-cols-1 gap-2">
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs text-gray-500 mb-1">Von</label>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
value={item.desiredDateRange?.start || ''}
|
||||||
|
onChange={(e) => handleDateRangeChange(item.id, 'start', e.target.value)}
|
||||||
|
className="border rounded p-2 w-full focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||||
|
disabled={item.desiredWeekday || item.desiredDate}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs text-gray-500 mb-1">Bis</label>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
value={item.desiredDateRange?.end || ''}
|
||||||
|
onChange={(e) => handleDateRangeChange(item.id, 'end', e.target.value)}
|
||||||
|
className="border rounded p-2 w-full focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||||
|
disabled={item.desiredWeekday || item.desiredDate}
|
||||||
|
min={item.desiredDateRange?.start || undefined}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
<td className="px-4 py-2 border-b">
|
<td className="px-4 py-2 border-b">
|
||||||
<div className="flex items-center justify-center gap-2">
|
<div className="flex items-center justify-center gap-2">
|
||||||
<button
|
<button
|
||||||
@@ -1256,7 +1330,8 @@ function App() {
|
|||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -94,6 +94,9 @@ const PickupConfigEditor = () => {
|
|||||||
if (newConfig[index].desiredDate) {
|
if (newConfig[index].desiredDate) {
|
||||||
delete newConfig[index].desiredDate;
|
delete newConfig[index].desiredDate;
|
||||||
}
|
}
|
||||||
|
if (newConfig[index].desiredDateRange) {
|
||||||
|
delete newConfig[index].desiredDateRange;
|
||||||
|
}
|
||||||
setConfig(newConfig);
|
setConfig(newConfig);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -104,6 +107,41 @@ const PickupConfigEditor = () => {
|
|||||||
if (newConfig[index].desiredWeekday) {
|
if (newConfig[index].desiredWeekday) {
|
||||||
delete newConfig[index].desiredWeekday;
|
delete newConfig[index].desiredWeekday;
|
||||||
}
|
}
|
||||||
|
if (newConfig[index].desiredDateRange) {
|
||||||
|
delete newConfig[index].desiredDateRange;
|
||||||
|
}
|
||||||
|
setConfig(newConfig);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDateRangeChange = (index, field, value) => {
|
||||||
|
const newConfig = [...config];
|
||||||
|
const nextRange = {
|
||||||
|
start: newConfig[index].desiredDateRange?.start || null,
|
||||||
|
end: newConfig[index].desiredDateRange?.end || null
|
||||||
|
};
|
||||||
|
if (field === 'start') {
|
||||||
|
nextRange.start = value || null;
|
||||||
|
if (nextRange.end && value && value > nextRange.end) {
|
||||||
|
nextRange.end = value;
|
||||||
|
}
|
||||||
|
} else if (field === 'end') {
|
||||||
|
nextRange.end = value || null;
|
||||||
|
if (nextRange.start && value && value < nextRange.start) {
|
||||||
|
nextRange.start = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const hasRange = Boolean(nextRange.start || nextRange.end);
|
||||||
|
if (hasRange) {
|
||||||
|
newConfig[index].desiredDateRange = nextRange;
|
||||||
|
if (newConfig[index].desiredWeekday) {
|
||||||
|
delete newConfig[index].desiredWeekday;
|
||||||
|
}
|
||||||
|
if (newConfig[index].desiredDate) {
|
||||||
|
delete newConfig[index].desiredDate;
|
||||||
|
}
|
||||||
|
} else if (newConfig[index].desiredDateRange) {
|
||||||
|
delete newConfig[index].desiredDateRange;
|
||||||
|
}
|
||||||
setConfig(newConfig);
|
setConfig(newConfig);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -139,10 +177,13 @@ const PickupConfigEditor = () => {
|
|||||||
<th className="px-4 py-2">Nur benachrichtigen</th>
|
<th className="px-4 py-2">Nur benachrichtigen</th>
|
||||||
<th className="px-4 py-2">Wochentag</th>
|
<th className="px-4 py-2">Wochentag</th>
|
||||||
<th className="px-4 py-2">Spezifisches Datum</th>
|
<th className="px-4 py-2">Spezifisches Datum</th>
|
||||||
|
<th className="px-4 py-2">Datumsbereich</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{config.map((item, index) => (
|
{config.map((item, index) => {
|
||||||
|
const hasDateRange = Boolean(item.desiredDateRange?.start || item.desiredDateRange?.end);
|
||||||
|
return (
|
||||||
<tr key={index} className={index % 2 === 0 ? 'bg-gray-50' : 'bg-white'}>
|
<tr key={index} 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
|
||||||
@@ -178,7 +219,7 @@ const PickupConfigEditor = () => {
|
|||||||
value={item.desiredWeekday || ''}
|
value={item.desiredWeekday || ''}
|
||||||
onChange={(e) => handleWeekdayChange(index, e.target.value)}
|
onChange={(e) => handleWeekdayChange(index, e.target.value)}
|
||||||
className="border rounded p-1 w-full"
|
className="border rounded p-1 w-full"
|
||||||
disabled={item.desiredDate}
|
disabled={item.desiredDate || hasDateRange}
|
||||||
>
|
>
|
||||||
<option value="">Kein Wochentag</option>
|
<option value="">Kein Wochentag</option>
|
||||||
{weekdays.map((day) => (
|
{weekdays.map((day) => (
|
||||||
@@ -192,11 +233,37 @@ const PickupConfigEditor = () => {
|
|||||||
value={item.desiredDate || ''}
|
value={item.desiredDate || ''}
|
||||||
onChange={(e) => handleDateChange(index, e.target.value)}
|
onChange={(e) => handleDateChange(index, e.target.value)}
|
||||||
className="border rounded p-1 w-full"
|
className="border rounded p-1 w-full"
|
||||||
disabled={item.desiredWeekday}
|
disabled={item.desiredWeekday || hasDateRange}
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
|
<td className="px-4 py-2">
|
||||||
|
<div className="grid grid-cols-1 gap-2">
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs text-gray-500 mb-1">Von</label>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
value={item.desiredDateRange?.start || ''}
|
||||||
|
onChange={(e) => handleDateRangeChange(index, 'start', e.target.value)}
|
||||||
|
className="border rounded p-1 w-full"
|
||||||
|
disabled={item.desiredWeekday || item.desiredDate}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs text-gray-500 mb-1">Bis</label>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
value={item.desiredDateRange?.end || ''}
|
||||||
|
onChange={(e) => handleDateRangeChange(index, 'end', e.target.value)}
|
||||||
|
className="border rounded p-1 w-full"
|
||||||
|
disabled={item.desiredWeekday || item.desiredDate}
|
||||||
|
min={item.desiredDateRange?.start || undefined}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user