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 = {}) {
|
||||
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
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user