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 = {}) { 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
}; };
} }

View File

@@ -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.