Einheit für Erinnerung hinzugefügt

This commit is contained in:
2026-01-08 10:06:14 +01:00
parent 4f2541144c
commit 6edddb8249
2 changed files with 139 additions and 40 deletions

View File

@@ -237,12 +237,23 @@ function getCachedStoreStatus(storeId) {
}
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 {
enabled: !!reminder.enabled,
interval: ['monthly', 'quarterly', 'yearly'].includes(reminder.interval)
? reminder.interval
: 'yearly',
daysBefore: Number.isFinite(reminder.daysBefore) ? Math.max(0, reminder.daysBefore) : 42
beforeUnit: unit,
beforeValue,
daysBefore
};
}

View File

@@ -7,6 +7,16 @@ const intervalLabels = {
quarterly: 'Vierteljä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 [entries, setEntries] = useState([]);
@@ -42,7 +52,8 @@ const JournalPage = ({ authorizedFetch, stores }) => {
note: '',
reminderEnabled: true,
reminderInterval: 'yearly',
reminderDaysBefore: 42
reminderBeforeValue: 42,
reminderBeforeUnit: 'days'
});
const [images, setImages] = useState([]);
const [existingImages, setExistingImages] = useState([]);
@@ -105,6 +116,45 @@ const JournalPage = ({ authorizedFetch, stores }) => {
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 copy = new Date(date.getTime());
copy.setMonth(copy.getMonth() + months);
@@ -125,9 +175,6 @@ const JournalPage = ({ authorizedFetch, stores }) => {
return null;
}
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());
let occurrence = startOfDay(baseDate);
const guardYear = todayStart.getFullYear() + 200;
@@ -137,16 +184,14 @@ const JournalPage = ({ authorizedFetch, stores }) => {
if (occurrence.getFullYear() >= guardYear) {
return null;
}
let reminderDate = new Date(occurrence.getTime());
reminderDate.setDate(reminderDate.getDate() - daysBefore);
let reminderDate = startOfDay(subtractReminderOffset(occurrence, entry.reminder));
if (reminderDate < todayStart) {
occurrence = startOfDay(addMonths(occurrence, intervalMonths));
reminderDate = new Date(occurrence.getTime());
reminderDate.setDate(reminderDate.getDate() - daysBefore);
reminderDate = startOfDay(subtractReminderOffset(occurrence, entry.reminder));
}
return reminderDate.getTime();
},
[addMonths, getIntervalMonths, startOfDay]
[addMonths, getIntervalMonths, startOfDay, subtractReminderOffset]
);
const filteredEntries = useMemo(() => {
@@ -296,7 +341,8 @@ const JournalPage = ({ authorizedFetch, stores }) => {
note: '',
reminderEnabled: true,
reminderInterval: 'yearly',
reminderDaysBefore: 42
reminderBeforeValue: 42,
reminderBeforeUnit: 'days'
});
}, [images]);
@@ -389,7 +435,8 @@ const JournalPage = ({ authorizedFetch, stores }) => {
reminder: {
enabled: form.reminderEnabled,
interval: form.reminderInterval,
daysBefore: Number(form.reminderDaysBefore)
beforeUnit: form.reminderBeforeUnit,
beforeValue: Number(form.reminderBeforeValue)
},
images: imagePayload,
keepImageIds: existingImages.map((image) => image.id)
@@ -467,7 +514,8 @@ const JournalPage = ({ authorizedFetch, stores }) => {
note: entry.note || '',
reminderEnabled: entry.reminder?.enabled ?? true,
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);
};
@@ -530,26 +578,44 @@ const JournalPage = ({ authorizedFetch, stores }) => {
<div className="space-y-6">
<div className={formOpen ? 'journal-content journal-content--blur' : 'journal-content'}>
<div className="bg-white rounded-lg shadow p-6">
<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>
<div className="flex items-center gap-2">
<button
type="button"
onClick={loadEntries}
className="text-sm px-3 py-2 border rounded-md hover:border-blue-400"
disabled={loading}
>
Aktualisieren
</button>
<button
type="button"
onClick={handleCreate}
className="text-sm px-3 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700"
>
Neuer Eintrag
</button>
</div>
<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>
<div className="flex items-center gap-2">
<button
type="button"
onClick={loadEntries}
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}
aria-label="Aktualisieren"
title="Aktualisieren"
>
<svg viewBox="0 0 24 24" className="h-4 w-4" aria-hidden="true">
<path
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"
fill="currentColor"
/>
<path
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"
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>
{error ? (
<div className="status-banner error mb-4">
<p>{error}</p>
@@ -613,7 +679,7 @@ const JournalPage = ({ authorizedFetch, stores }) => {
{filteredEntries.map((entry) => {
const reminder = entry.reminder || {};
const reminderLabel = reminder.enabled
? `${intervalLabels[reminder.interval] || 'Jährlich'}, ${reminder.daysBefore} Tage vorher`
? `${intervalLabels[reminder.interval] || 'Jährlich'}, ${formatReminderOffset(reminder)} vorher`
: 'Keine Erinnerung';
const storeLabel =
entry.storeName ||
@@ -754,9 +820,16 @@ const JournalPage = ({ authorizedFetch, stores }) => {
<button
type="button"
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>
</div>
<div className="px-6 py-6">
@@ -917,18 +990,33 @@ const JournalPage = ({ authorizedFetch, stores }) => {
))}
</select>
</div>
<div className="space-y-1">
<label className="block text-sm text-gray-600">Tage vorher</label>
<div className="space-y-1">
<label className="block text-sm text-gray-600">Vorher</label>
<div className="flex gap-2">
<input
type="number"
min="0"
value={form.reminderDaysBefore}
value={form.reminderBeforeValue}
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>
<p className="text-xs text-gray-500">
Erinnerung wird standardmäßig jährlich eingeplant.