feat(260929-if2): Erinnerungen anlegen und zur Faelligkeit benachrichtigen (Tracer)
- Reminder-Tabelle mit Zeilenschutz (Mandant+Benutzer, Systemlesen fuer den E-Mail-Planer), API reminders (Liste, Anlegen) - Kachel "Erinnerungen", globaler Melder im Portalrahmen (Browser und Desktop, je Faelligkeit einmal) - Desktop: Laufzeit-Berechtigung fuer Benachrichtigungen nur fuer die gespeicherte Server-Adresse - Zugriffsklassifikation nachgemessen Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
'use client';
|
||||
|
||||
import { useLocale, useTranslations } from 'next-intl';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import {
|
||||
REMINDERS_CHANGED_EVENT,
|
||||
type Reminder,
|
||||
listReminders,
|
||||
} from '@/lib/reminders-api';
|
||||
import type { WidgetProps } from '../widget-registry';
|
||||
import { ReminderFormModal } from './reminder-form-modal';
|
||||
|
||||
/** Abstand der Auffrischung beim Server. */
|
||||
const REFRESH_MS = 60_000;
|
||||
|
||||
/**
|
||||
* Kachel „Erinnerungen“ (quick-260929-if2): die persoenlichen Erinnerungen des
|
||||
* Benutzers, die naechste Faelligkeit zuerst, dazu „Neue Erinnerung“. Die
|
||||
* Benachrichtigung selbst macht der globale `ReminderNotifier` im Portal-
|
||||
* Rahmen — die Kachel muss dafuer nicht sichtbar sein. Nach jeder Aenderung
|
||||
* laedt sie neu und meldet `REMINDERS_CHANGED_EVENT`, damit der Melder eine
|
||||
* neue Faelligkeit sofort kennt.
|
||||
*/
|
||||
export function ReminderWidget(_props: WidgetProps) {
|
||||
const t = useTranslations('widgets');
|
||||
const locale = useLocale();
|
||||
const [reminders, setReminders] = useState<Reminder[] | null>(null);
|
||||
const [loadError, setLoadError] = useState(false);
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
|
||||
const refetch = useCallback(async () => {
|
||||
try {
|
||||
setReminders(await listReminders());
|
||||
setLoadError(false);
|
||||
} catch {
|
||||
setLoadError(true);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
void refetch();
|
||||
const timer = setInterval(() => void refetch(), REFRESH_MS);
|
||||
const onChanged = () => void refetch();
|
||||
window.addEventListener(REMINDERS_CHANGED_EVENT, onChanged);
|
||||
return () => {
|
||||
clearInterval(timer);
|
||||
window.removeEventListener(REMINDERS_CHANGED_EVENT, onChanged);
|
||||
};
|
||||
}, [refetch]);
|
||||
|
||||
const sorted = useMemo(
|
||||
() =>
|
||||
[...(reminders ?? [])].sort(
|
||||
(a, b) => new Date(a.dueAt).getTime() - new Date(b.dueAt).getTime(),
|
||||
),
|
||||
[reminders],
|
||||
);
|
||||
|
||||
const formatter = useMemo(
|
||||
() => new Intl.DateTimeFormat(locale, { dateStyle: 'medium', timeStyle: 'short' }),
|
||||
[locale],
|
||||
);
|
||||
|
||||
const handleSaved = () => {
|
||||
void refetch();
|
||||
window.dispatchEvent(new Event(REMINDERS_CHANGED_EVENT));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col gap-2 p-3" data-testid="reminder-widget">
|
||||
<div className="min-h-0 flex-1 space-y-2 overflow-y-auto">
|
||||
{loadError && (
|
||||
<p role="alert" className="text-sm text-destructive">
|
||||
{t('reminder.loadError')}
|
||||
</p>
|
||||
)}
|
||||
{!loadError && reminders !== null && sorted.length === 0 && (
|
||||
<p className="text-sm text-muted-foreground">{t('reminder.empty')}</p>
|
||||
)}
|
||||
{sorted.map((r) => (
|
||||
<div
|
||||
key={r.id}
|
||||
data-testid="reminder-row"
|
||||
className="rounded-md border border-border bg-background/60 px-3 py-2"
|
||||
>
|
||||
<p className="truncate text-sm font-medium text-foreground">{r.title}</p>
|
||||
<p className="text-xs text-muted-foreground">{formatter.format(new Date(r.dueAt))}</p>
|
||||
{r.description !== '' && (
|
||||
<p className="mt-1 line-clamp-2 whitespace-pre-line text-xs text-muted-foreground">
|
||||
{r.description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setModalOpen(true)}
|
||||
className="btn btn-secondary shrink-0 self-start"
|
||||
>
|
||||
{t('reminder.add')}
|
||||
</button>
|
||||
{modalOpen && (
|
||||
<ReminderFormModal onClose={() => setModalOpen(false)} onSaved={handleSaved} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user