'use client'; import { FunctionComponent, useState } from 'react'; import Skeleton from '@mui/material/Skeleton'; import Stack from '@mui/material/Stack'; import Typography from '@mui/material/Typography'; import { useLocale, useTranslations } from 'next-intl'; import { useRouter } from 'next/navigation'; import AppButton from '@/components/common/AppButton'; import { AppIcon } from '@/components/common'; import { NOTIFICATIONS_PAGE_SIZE } from '@/services/notifications/constants'; import { notificationDeepLink, useMarkAllRead, useMarkNotificationRead, useNotifications, } from '@/services/notifications'; import type { AppNotification } from '@/services/notifications/types'; import { formatRelativeTime, formatShamsiDate } from '@/utils'; import NotificationRow from './NotificationRow'; export interface NotificationCenterProps { role: 'customer' | 'nurse'; } interface NotificationGroup { key: string; label: string; items: AppNotification[]; } /** * Buckets notifications into امروز/دیروز/این هفته, then per-day Shamsi headers for anything older — * emitted **in list order** (the server's unread-first-then-newest ordering is preserved; §5 "keep the * mark-read UX as is"), so a bucket can recur if an older unread item is pinned above newer read ones. * Compares already-formatted date strings (not raw ms diffs) so a day boundary is calendar-exact. */ function groupByDay(items: AppNotification[], locale: string, todayLabel: string, yesterdayLabel: string, thisWeekLabel: string): NotificationGroup[] { const now = new Date(); const todayStr = formatShamsiDate(now, locale); const yesterday = new Date(now); yesterday.setDate(yesterday.getDate() - 1); const yesterdayStr = formatShamsiDate(yesterday, locale); const thisWeekStrs = new Set(); for (let i = 2; i < 7; i += 1) { const d = new Date(now); d.setDate(d.getDate() - i); thisWeekStrs.add(formatShamsiDate(d, locale)); } const groups: NotificationGroup[] = []; for (const item of items) { const dayStr = formatShamsiDate(item.createdAt, locale); const bucket = dayStr === todayStr ? { key: 'today', label: todayLabel } : dayStr === yesterdayStr ? { key: 'yesterday', label: yesterdayLabel } : thisWeekStrs.has(dayStr) ? { key: 'this_week', label: thisWeekLabel } : { key: dayStr, label: dayStr }; const last = groups[groups.length - 1]; if (last && last.key === bucket.key) last.items.push(item); else groups.push({ ...bucket, items: [item] }); } return groups; } /** * The notification center — a paged, **unread-first** list, day-grouped (امروز/دیروز/این هفته, then Shamsi * dates) with relative timestamps decaying to Shamsi. Each row **marks itself read on open** (optimistic); * a row whose `data` deep-links renders interactive with a trailing chevron, a row with nothing to open * renders as a plain, non-rippling surface. A "mark all read" action clears the badge at once. Empty / * loading-skeleton / error→retry states. Shared by the customer and nurse notification pages (role decides * only the deep-link shell). * @component NotificationCenter */ const NotificationCenter: FunctionComponent = ({ role }) => { const t = useTranslations('notifications'); const locale = useLocale(); const router = useRouter(); const [limit, setLimit] = useState(NOTIFICATIONS_PAGE_SIZE); const { data, isLoading, isError, refetch, isFetching } = useNotifications(limit); const markRead = useMarkNotificationRead(); const markAll = useMarkAllRead(); const items = data?.items ?? []; const total = data?.total ?? 0; const hasUnread = items.some((n) => !n.isRead); const groups = groupByDay(items, locale, t('group_today'), t('group_yesterday'), t('group_this_week')); const openNotification = (notification: AppNotification) => { if (!notification.isRead) markRead.mutate(notification.id); const target = notificationDeepLink(notification, role); if (target) router.push(`/${locale}${target}`); }; return ( {t('title')} {hasUnread ? ( markAll.mutate()} disabled={markAll.isPending} > {t('mark_all_read')} ) : null} {isLoading ? ( {[0, 1, 2, 3].map((i) => ( ))} ) : isError ? ( {t('error_body')} refetch()}> {t('retry')} ) : items.length === 0 ? ( {t('empty_title')} {t('empty_body')} ) : ( {groups.map((group) => ( {group.label} {group.items.map((notification) => { const target = notificationDeepLink(notification, role); return ( openNotification(notification)} /> ); })} ))} {total > items.length ? ( setLimit((current) => current + NOTIFICATIONS_PAGE_SIZE)} disabled={isFetching} sx={{ alignSelf: 'center' }} > {t('load_more')} ) : null} )} ); }; export default NotificationCenter;