ui phase 10
This commit is contained in:
@@ -15,18 +15,64 @@ import {
|
||||
useNotifications,
|
||||
} from '@/services/notifications';
|
||||
import type { AppNotification } from '@/services/notifications/types';
|
||||
import { formatShamsiDateTime } from '@/utils';
|
||||
import { formatRelativeTime, formatShamsiDate } from '@/utils';
|
||||
import NotificationRow from './NotificationRow';
|
||||
|
||||
export interface NotificationCenterProps {
|
||||
role: 'customer' | 'nurse';
|
||||
}
|
||||
|
||||
interface NotificationGroup {
|
||||
key: string;
|
||||
label: string;
|
||||
items: AppNotification[];
|
||||
}
|
||||
|
||||
/**
|
||||
* The notification center — a paged, **unread-first** list. Each row **marks itself read on open** (optimistic)
|
||||
* and **deep-links via `notificationDeepLink`** (role-aware) when its `data` points somewhere. 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).
|
||||
* 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<string>();
|
||||
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<NotificationCenterProps> = ({ role }) => {
|
||||
@@ -42,6 +88,7 @@ const NotificationCenter: FunctionComponent<NotificationCenterProps> = ({ role }
|
||||
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);
|
||||
@@ -94,14 +141,27 @@ const NotificationCenter: FunctionComponent<NotificationCenterProps> = ({ role }
|
||||
</Typography>
|
||||
</Stack>
|
||||
) : (
|
||||
<Stack sx={{ gap: 1 }}>
|
||||
{items.map((notification) => (
|
||||
<NotificationRow
|
||||
key={notification.id}
|
||||
notification={notification}
|
||||
timeLabel={formatShamsiDateTime(notification.createdAt, locale)}
|
||||
onOpen={() => openNotification(notification)}
|
||||
/>
|
||||
<Stack sx={{ gap: 2 }}>
|
||||
{groups.map((group) => (
|
||||
<Stack key={`${group.key}-${group.items[0].id}`} sx={{ gap: 1 }}>
|
||||
<Typography variant="caption" sx={{ fontWeight: 700, color: 'var(--bal-text-secondary)' }}>
|
||||
{group.label}
|
||||
</Typography>
|
||||
<Stack sx={{ gap: 1 }}>
|
||||
{group.items.map((notification) => {
|
||||
const target = notificationDeepLink(notification, role);
|
||||
return (
|
||||
<NotificationRow
|
||||
key={notification.id}
|
||||
notification={notification}
|
||||
timeLabel={formatRelativeTime(notification.createdAt, locale, formatShamsiDate)}
|
||||
navigable={target != null}
|
||||
onOpen={() => openNotification(notification)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Stack>
|
||||
</Stack>
|
||||
))}
|
||||
{total > items.length ? (
|
||||
<AppButton
|
||||
|
||||
Reference in New Issue
Block a user