Einheit für Erinnerung hinzugefügt
This commit is contained in:
13
server.js
13
server.js
@@ -237,12 +237,23 @@ function getCachedStoreStatus(storeId) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function normalizeJournalReminder(reminder = {}) {
|
function normalizeJournalReminder(reminder = {}) {
|
||||||
|
const unit = ['days', 'weeks', 'months'].includes(reminder.beforeUnit) ? reminder.beforeUnit : 'days';
|
||||||
|
const parsedBeforeValue = Number(reminder.beforeValue);
|
||||||
|
const parsedDaysBefore = Number(reminder.daysBefore);
|
||||||
|
const beforeValue = Number.isFinite(parsedBeforeValue)
|
||||||
|
? Math.max(0, parsedBeforeValue)
|
||||||
|
: Number.isFinite(parsedDaysBefore)
|
||||||
|
? Math.max(0, parsedDaysBefore)
|
||||||
|
: 42;
|
||||||
|
const daysBefore = unit === 'weeks' ? beforeValue * 7 : unit === 'months' ? beforeValue * 30 : beforeValue;
|
||||||
return {
|
return {
|
||||||
enabled: !!reminder.enabled,
|
enabled: !!reminder.enabled,
|
||||||
interval: ['monthly', 'quarterly', 'yearly'].includes(reminder.interval)
|
interval: ['monthly', 'quarterly', 'yearly'].includes(reminder.interval)
|
||||||
? reminder.interval
|
? reminder.interval
|
||||||
: 'yearly',
|
: 'yearly',
|
||||||
daysBefore: Number.isFinite(reminder.daysBefore) ? Math.max(0, reminder.daysBefore) : 42
|
beforeUnit: unit,
|
||||||
|
beforeValue,
|
||||||
|
daysBefore
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,16 @@ const intervalLabels = {
|
|||||||
quarterly: 'Vierteljährlich',
|
quarterly: 'Vierteljährlich',
|
||||||
yearly: 'Jährlich'
|
yearly: 'Jährlich'
|
||||||
};
|
};
|
||||||
|
const reminderUnitLabels = {
|
||||||
|
days: 'Tage',
|
||||||
|
weeks: 'Wochen',
|
||||||
|
months: 'Monate'
|
||||||
|
};
|
||||||
|
const reminderUnitSingular = {
|
||||||
|
days: 'Tag',
|
||||||
|
weeks: 'Woche',
|
||||||
|
months: 'Monat'
|
||||||
|
};
|
||||||
|
|
||||||
const JournalPage = ({ authorizedFetch, stores }) => {
|
const JournalPage = ({ authorizedFetch, stores }) => {
|
||||||
const [entries, setEntries] = useState([]);
|
const [entries, setEntries] = useState([]);
|
||||||
@@ -42,7 +52,8 @@ const JournalPage = ({ authorizedFetch, stores }) => {
|
|||||||
note: '',
|
note: '',
|
||||||
reminderEnabled: true,
|
reminderEnabled: true,
|
||||||
reminderInterval: 'yearly',
|
reminderInterval: 'yearly',
|
||||||
reminderDaysBefore: 42
|
reminderBeforeValue: 42,
|
||||||
|
reminderBeforeUnit: 'days'
|
||||||
});
|
});
|
||||||
const [images, setImages] = useState([]);
|
const [images, setImages] = useState([]);
|
||||||
const [existingImages, setExistingImages] = useState([]);
|
const [existingImages, setExistingImages] = useState([]);
|
||||||
@@ -105,6 +116,45 @@ const JournalPage = ({ authorizedFetch, stores }) => {
|
|||||||
return 12;
|
return 12;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const getReminderOffset = useCallback((reminder) => {
|
||||||
|
const unit = ['days', 'weeks', 'months'].includes(reminder?.beforeUnit) ? reminder.beforeUnit : 'days';
|
||||||
|
const rawValue = Number(reminder?.beforeValue);
|
||||||
|
const fallbackValue = Number(reminder?.daysBefore);
|
||||||
|
const value = Number.isFinite(rawValue)
|
||||||
|
? Math.max(0, rawValue)
|
||||||
|
: Number.isFinite(fallbackValue)
|
||||||
|
? Math.max(0, fallbackValue)
|
||||||
|
: 42;
|
||||||
|
return { unit, value };
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const subtractReminderOffset = useCallback(
|
||||||
|
(date, reminder) => {
|
||||||
|
const { unit, value } = getReminderOffset(reminder);
|
||||||
|
const copy = new Date(date.getTime());
|
||||||
|
if (unit === 'weeks') {
|
||||||
|
copy.setDate(copy.getDate() - value * 7);
|
||||||
|
return copy;
|
||||||
|
}
|
||||||
|
if (unit === 'months') {
|
||||||
|
copy.setMonth(copy.getMonth() - value);
|
||||||
|
return copy;
|
||||||
|
}
|
||||||
|
copy.setDate(copy.getDate() - value);
|
||||||
|
return copy;
|
||||||
|
},
|
||||||
|
[getReminderOffset]
|
||||||
|
);
|
||||||
|
|
||||||
|
const formatReminderOffset = useCallback(
|
||||||
|
(reminder) => {
|
||||||
|
const { unit, value } = getReminderOffset(reminder);
|
||||||
|
const label = value === 1 ? reminderUnitSingular[unit] : reminderUnitLabels[unit];
|
||||||
|
return `${value} ${label}`;
|
||||||
|
},
|
||||||
|
[getReminderOffset]
|
||||||
|
);
|
||||||
|
|
||||||
const addMonths = useCallback((date, months) => {
|
const addMonths = useCallback((date, months) => {
|
||||||
const copy = new Date(date.getTime());
|
const copy = new Date(date.getTime());
|
||||||
copy.setMonth(copy.getMonth() + months);
|
copy.setMonth(copy.getMonth() + months);
|
||||||
@@ -125,9 +175,6 @@ const JournalPage = ({ authorizedFetch, stores }) => {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const intervalMonths = getIntervalMonths(entry.reminder.interval);
|
const intervalMonths = getIntervalMonths(entry.reminder.interval);
|
||||||
const daysBefore = Number.isFinite(entry.reminder.daysBefore)
|
|
||||||
? Math.max(0, entry.reminder.daysBefore)
|
|
||||||
: 42;
|
|
||||||
const todayStart = startOfDay(new Date());
|
const todayStart = startOfDay(new Date());
|
||||||
let occurrence = startOfDay(baseDate);
|
let occurrence = startOfDay(baseDate);
|
||||||
const guardYear = todayStart.getFullYear() + 200;
|
const guardYear = todayStart.getFullYear() + 200;
|
||||||
@@ -137,16 +184,14 @@ const JournalPage = ({ authorizedFetch, stores }) => {
|
|||||||
if (occurrence.getFullYear() >= guardYear) {
|
if (occurrence.getFullYear() >= guardYear) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
let reminderDate = new Date(occurrence.getTime());
|
let reminderDate = startOfDay(subtractReminderOffset(occurrence, entry.reminder));
|
||||||
reminderDate.setDate(reminderDate.getDate() - daysBefore);
|
|
||||||
if (reminderDate < todayStart) {
|
if (reminderDate < todayStart) {
|
||||||
occurrence = startOfDay(addMonths(occurrence, intervalMonths));
|
occurrence = startOfDay(addMonths(occurrence, intervalMonths));
|
||||||
reminderDate = new Date(occurrence.getTime());
|
reminderDate = startOfDay(subtractReminderOffset(occurrence, entry.reminder));
|
||||||
reminderDate.setDate(reminderDate.getDate() - daysBefore);
|
|
||||||
}
|
}
|
||||||
return reminderDate.getTime();
|
return reminderDate.getTime();
|
||||||
},
|
},
|
||||||
[addMonths, getIntervalMonths, startOfDay]
|
[addMonths, getIntervalMonths, startOfDay, subtractReminderOffset]
|
||||||
);
|
);
|
||||||
|
|
||||||
const filteredEntries = useMemo(() => {
|
const filteredEntries = useMemo(() => {
|
||||||
@@ -296,7 +341,8 @@ const JournalPage = ({ authorizedFetch, stores }) => {
|
|||||||
note: '',
|
note: '',
|
||||||
reminderEnabled: true,
|
reminderEnabled: true,
|
||||||
reminderInterval: 'yearly',
|
reminderInterval: 'yearly',
|
||||||
reminderDaysBefore: 42
|
reminderBeforeValue: 42,
|
||||||
|
reminderBeforeUnit: 'days'
|
||||||
});
|
});
|
||||||
}, [images]);
|
}, [images]);
|
||||||
|
|
||||||
@@ -389,7 +435,8 @@ const JournalPage = ({ authorizedFetch, stores }) => {
|
|||||||
reminder: {
|
reminder: {
|
||||||
enabled: form.reminderEnabled,
|
enabled: form.reminderEnabled,
|
||||||
interval: form.reminderInterval,
|
interval: form.reminderInterval,
|
||||||
daysBefore: Number(form.reminderDaysBefore)
|
beforeUnit: form.reminderBeforeUnit,
|
||||||
|
beforeValue: Number(form.reminderBeforeValue)
|
||||||
},
|
},
|
||||||
images: imagePayload,
|
images: imagePayload,
|
||||||
keepImageIds: existingImages.map((image) => image.id)
|
keepImageIds: existingImages.map((image) => image.id)
|
||||||
@@ -467,7 +514,8 @@ const JournalPage = ({ authorizedFetch, stores }) => {
|
|||||||
note: entry.note || '',
|
note: entry.note || '',
|
||||||
reminderEnabled: entry.reminder?.enabled ?? true,
|
reminderEnabled: entry.reminder?.enabled ?? true,
|
||||||
reminderInterval: entry.reminder?.interval || 'yearly',
|
reminderInterval: entry.reminder?.interval || 'yearly',
|
||||||
reminderDaysBefore: Number.isFinite(entry.reminder?.daysBefore) ? entry.reminder.daysBefore : 42
|
reminderBeforeValue: getReminderOffset(entry.reminder).value,
|
||||||
|
reminderBeforeUnit: getReminderOffset(entry.reminder).unit
|
||||||
});
|
});
|
||||||
setFormOpen(true);
|
setFormOpen(true);
|
||||||
};
|
};
|
||||||
@@ -530,26 +578,44 @@ const JournalPage = ({ authorizedFetch, stores }) => {
|
|||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div className={formOpen ? 'journal-content journal-content--blur' : 'journal-content'}>
|
<div className={formOpen ? 'journal-content journal-content--blur' : 'journal-content'}>
|
||||||
<div className="bg-white rounded-lg shadow p-6">
|
<div className="bg-white rounded-lg shadow p-6">
|
||||||
<div className="flex flex-wrap items-center justify-between gap-3 mb-4">
|
<div className="flex flex-wrap items-center justify-between gap-3 mb-4">
|
||||||
<h2 className="text-xl font-semibold text-gray-800">Journal-Einträge</h2>
|
<h2 className="text-xl font-semibold text-gray-800">Journal-Einträge</h2>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={loadEntries}
|
onClick={loadEntries}
|
||||||
className="text-sm px-3 py-2 border rounded-md hover:border-blue-400"
|
className="inline-flex items-center justify-center h-9 w-9 rounded-md border border-gray-200 text-gray-700 hover:bg-gray-50"
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
>
|
aria-label="Aktualisieren"
|
||||||
Aktualisieren
|
title="Aktualisieren"
|
||||||
</button>
|
>
|
||||||
<button
|
<svg viewBox="0 0 24 24" className="h-4 w-4" aria-hidden="true">
|
||||||
type="button"
|
<path
|
||||||
onClick={handleCreate}
|
d="M12 5.25A6.75 6.75 0 1 1 6.07 8.5a.75.75 0 1 1 1.286.77A5.25 5.25 0 1 0 12 6.75h-1.94a.75.75 0 0 1 0-1.5H12z"
|
||||||
className="text-sm px-3 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700"
|
fill="currentColor"
|
||||||
>
|
/>
|
||||||
Neuer Eintrag
|
<path
|
||||||
</button>
|
d="M12 3a.75.75 0 0 1 .75.75v2.69l1.72-1.72a.75.75 0 1 1 1.06 1.06l-3 3a.75.75 0 0 1-1.06 0l-3-3a.75.75 0 1 1 1.06-1.06l1.72 1.72V3.75A.75.75 0 0 1 12 3z"
|
||||||
</div>
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleCreate}
|
||||||
|
className="inline-flex items-center justify-center h-9 w-9 rounded-md border border-blue-600 bg-blue-600 text-white hover:bg-blue-700"
|
||||||
|
aria-label="Neuer Eintrag"
|
||||||
|
title="Neuer Eintrag"
|
||||||
|
>
|
||||||
|
<svg viewBox="0 0 24 24" className="h-4 w-4" aria-hidden="true">
|
||||||
|
<path
|
||||||
|
d="M12 5.25a.75.75 0 0 1 .75.75v5.25H18a.75.75 0 1 1 0 1.5h-5.25V18a.75.75 0 1 1-1.5 0v-5.25H6a.75.75 0 1 1 0-1.5h5.25V6a.75.75 0 0 1 .75-.75z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
{error ? (
|
{error ? (
|
||||||
<div className="status-banner error mb-4">
|
<div className="status-banner error mb-4">
|
||||||
<p>{error}</p>
|
<p>{error}</p>
|
||||||
@@ -613,7 +679,7 @@ const JournalPage = ({ authorizedFetch, stores }) => {
|
|||||||
{filteredEntries.map((entry) => {
|
{filteredEntries.map((entry) => {
|
||||||
const reminder = entry.reminder || {};
|
const reminder = entry.reminder || {};
|
||||||
const reminderLabel = reminder.enabled
|
const reminderLabel = reminder.enabled
|
||||||
? `${intervalLabels[reminder.interval] || 'Jährlich'}, ${reminder.daysBefore} Tage vorher`
|
? `${intervalLabels[reminder.interval] || 'Jährlich'}, ${formatReminderOffset(reminder)} vorher`
|
||||||
: 'Keine Erinnerung';
|
: 'Keine Erinnerung';
|
||||||
const storeLabel =
|
const storeLabel =
|
||||||
entry.storeName ||
|
entry.storeName ||
|
||||||
@@ -754,9 +820,16 @@ const JournalPage = ({ authorizedFetch, stores }) => {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={resetForm}
|
onClick={resetForm}
|
||||||
className="text-sm text-gray-600 hover:text-gray-800"
|
className="inline-flex items-center justify-center h-9 w-9 rounded-md border border-gray-200 text-gray-700 hover:bg-gray-50"
|
||||||
|
aria-label="Schließen"
|
||||||
|
title="Schließen"
|
||||||
>
|
>
|
||||||
Schließen
|
<svg viewBox="0 0 24 24" className="h-4 w-4" aria-hidden="true">
|
||||||
|
<path
|
||||||
|
d="M6.47 6.47a.75.75 0 0 1 1.06 0L12 10.94l4.47-4.47a.75.75 0 1 1 1.06 1.06L13.06 12l4.47 4.47a.75.75 0 1 1-1.06 1.06L12 13.06l-4.47 4.47a.75.75 0 1 1-1.06-1.06L10.94 12 6.47 7.53a.75.75 0 0 1 0-1.06z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="px-6 py-6">
|
<div className="px-6 py-6">
|
||||||
@@ -917,18 +990,33 @@ const JournalPage = ({ authorizedFetch, stores }) => {
|
|||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<label className="block text-sm text-gray-600">Tage vorher</label>
|
<label className="block text-sm text-gray-600">Vorher</label>
|
||||||
|
<div className="flex gap-2">
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
min="0"
|
min="0"
|
||||||
value={form.reminderDaysBefore}
|
value={form.reminderBeforeValue}
|
||||||
onChange={(event) =>
|
onChange={(event) =>
|
||||||
handleFormChange({ reminderDaysBefore: event.target.value })
|
handleFormChange({ reminderBeforeValue: event.target.value })
|
||||||
}
|
}
|
||||||
className="w-full border border-gray-300 rounded-md px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-200"
|
className="w-24 border border-gray-300 rounded-md px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-200"
|
||||||
/>
|
/>
|
||||||
|
<select
|
||||||
|
value={form.reminderBeforeUnit}
|
||||||
|
onChange={(event) =>
|
||||||
|
handleFormChange({ reminderBeforeUnit: event.target.value })
|
||||||
|
}
|
||||||
|
className="flex-1"
|
||||||
|
>
|
||||||
|
{Object.entries(reminderUnitLabels).map(([value, label]) => (
|
||||||
|
<option key={value} value={value}>
|
||||||
|
{label}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-gray-500">
|
<p className="text-xs text-gray-500">
|
||||||
Erinnerung wird standardmäßig jährlich eingeplant.
|
Erinnerung wird standardmäßig jährlich eingeplant.
|
||||||
|
|||||||
Reference in New Issue
Block a user