fixed notification system and added a new tenant notifications receipt table to track read status and filter messages by scope.

This commit is contained in:
Codex Agent
2025-12-17 10:57:19 +01:00
parent 0aae494945
commit d64839ba2f
31 changed files with 1089 additions and 127 deletions

View File

@@ -1,4 +1,5 @@
import React from 'react';
import { createPortal } from 'react-dom';
import { Link } from 'react-router-dom';
import AppearanceToggleDropdown from '@/components/appearance-dropdown';
import {
@@ -147,6 +148,7 @@ export default function Header({ eventToken, title = '' }: { eventToken?: string
const { event, status } = useEventData();
const notificationCenter = useOptionalNotificationCenter();
const [notificationsOpen, setNotificationsOpen] = React.useState(false);
const [statusFilter, setStatusFilter] = React.useState<'new' | 'read' | 'dismissed' | 'all'>('new');
const taskProgress = useGuestTaskProgress(eventToken);
const panelRef = React.useRef<HTMLDivElement | null>(null);
const checklistItems = React.useMemo(
@@ -277,11 +279,13 @@ type NotificationButtonProps = {
type PushState = ReturnType<typeof usePushSubscription>;
function NotificationButton({ center, eventToken, open, onToggle, panelRef, checklistItems, taskProgress, t }: NotificationButtonProps) {
const badgeCount = center.totalCount;
const badgeCount = center.unreadCount;
const progressRatio = taskProgress
? Math.min(1, taskProgress.completedCount / TASK_BADGE_TARGET)
: 0;
const [activeTab, setActiveTab] = React.useState<'unread' | 'all' | 'status'>(center.unreadCount > 0 ? 'unread' : 'all');
const [statusFilter, setStatusFilter] = React.useState<'new' | 'read' | 'dismissed' | 'all'>('new');
const [scopeFilter, setScopeFilter] = React.useState<'all' | 'uploads' | 'tips' | 'general'>('all');
const pushState = usePushSubscription(eventToken);
React.useEffect(() => {
@@ -300,18 +304,39 @@ function NotificationButton({ center, eventToken, open, onToggle, panelRef, chec
);
const filteredNotifications = React.useMemo(() => {
let base: typeof center.notifications = [];
switch (activeTab) {
case 'unread':
return unreadNotifications;
base = unreadNotifications;
break;
case 'status':
return uploadNotifications;
base = uploadNotifications;
break;
default:
return center.notifications;
base = center.notifications;
}
}, [activeTab, center.notifications, unreadNotifications, uploadNotifications]);
if (statusFilter === 'all') return base;
return base.filter((item) => item.status === (statusFilter === 'new' ? 'new' : statusFilter));
}, [activeTab, center.notifications, unreadNotifications, uploadNotifications, statusFilter]);
const scopedNotifications = React.useMemo(() => {
if (scopeFilter === 'all') {
return filteredNotifications;
}
return filteredNotifications.filter((item) => {
if (scopeFilter === 'uploads') {
return item.type === 'upload_alert' || item.type === 'photo_activity';
}
if (scopeFilter === 'tips') {
return item.type === 'support_tip' || item.type === 'achievement_major';
}
return item.type === 'broadcast' || item.type === 'feedback_request';
});
}, [filteredNotifications, scopeFilter]);
return (
<div className="relative">
<div className="relative z-50">
<button
type="button"
onClick={onToggle}
@@ -320,15 +345,15 @@ function NotificationButton({ center, eventToken, open, onToggle, panelRef, chec
>
<Bell className="h-5 w-5" aria-hidden />
{badgeCount > 0 && (
<span className="absolute -right-1 -top-1 rounded-full bg-white px-1.5 text-[10px] font-semibold text-pink-600">
<span className="absolute -right-1 -top-1 min-h-[18px] min-w-[18px] rounded-full bg-pink-500 px-1.5 text-[11px] font-semibold leading-[18px] text-white shadow-lg">
{badgeCount > 9 ? '9+' : badgeCount}
</span>
)}
</button>
{open && (
{open && createPortal(
<div
ref={panelRef}
className="absolute right-0 mt-2 w-80 rounded-2xl border border-white/30 bg-white/95 p-4 text-slate-900 shadow-2xl"
className="fixed right-4 top-16 z-[2147483000] w-80 rounded-2xl border border-white/30 bg-white/95 p-4 text-slate-900 shadow-2xl"
>
<div className="flex items-start justify-between gap-3">
<div>
@@ -358,6 +383,40 @@ function NotificationButton({ center, eventToken, open, onToggle, panelRef, chec
activeTab={activeTab}
onTabChange={(next) => setActiveTab(next as typeof activeTab)}
/>
<div className="mt-2">
<label className="sr-only" htmlFor="notification-scope">
{t('header.notifications.scopeLabel', 'Bereich filtern')}
</label>
<select
id="notification-scope"
value={scopeFilter}
onChange={(event) => {
const next = event.target.value as typeof scopeFilter;
setScopeFilter(next);
notificationCenter?.setFilters({ scope: next, status: statusFilter });
}}
className="w-full rounded-xl border border-slate-200 bg-white px-3 py-2 text-sm text-slate-700 shadow-sm"
>
<option value="all">{t('header.notifications.scope.all', 'Alle')}</option>
<option value="uploads">{t('header.notifications.scope.uploads', 'Uploads/Status')}</option>
<option value="tips">{t('header.notifications.scope.tips', 'Tipps & Achievements')}</option>
<option value="general">{t('header.notifications.scope.general', 'Allgemein')}</option>
</select>
<select
value={statusFilter}
onChange={(event) => {
const value = event.target.value as typeof statusFilter;
setStatusFilter(value);
notificationCenter?.setFilters({ status: value, scope: scopeFilter });
}}
className="mt-2 w-full rounded-xl border border-slate-200 bg-white px-3 py-2 text-sm text-slate-700 shadow-sm"
>
<option value="new">{t('header.notifications.filter.unread', 'Neu')}</option>
<option value="read">{t('header.notifications.filter.read', 'Gelesen')}</option>
<option value="dismissed">{t('header.notifications.filter.dismissed', 'Ausgeblendet')}</option>
<option value="all">{t('header.notifications.filter.all', 'Alle')}</option>
</select>
</div>
<NotificationStatusBar
lastFetchedAt={center.lastFetchedAt}
isOffline={center.isOffline}
@@ -367,7 +426,7 @@ function NotificationButton({ center, eventToken, open, onToggle, panelRef, chec
<div className="mt-3 max-h-80 space-y-2 overflow-y-auto pr-1">
{center.loading ? (
<NotificationSkeleton />
) : filteredNotifications.length === 0 ? (
) : scopedNotifications.length === 0 ? (
<NotificationEmptyState
t={t}
message={
@@ -379,7 +438,7 @@ function NotificationButton({ center, eventToken, open, onToggle, panelRef, chec
}
/>
) : (
filteredNotifications.map((item) => (
scopedNotifications.map((item) => (
<NotificationListItem
key={item.id}
item={item}
@@ -444,7 +503,8 @@ function NotificationButton({ center, eventToken, open, onToggle, panelRef, chec
</li>
))}
</ul>
</div>
</div>,
typeof document !== 'undefined' ? document.body : undefined
)}
</div>
);