ui phase 10

This commit is contained in:
hamid
2026-07-19 17:13:32 +03:30
parent b638e25a0e
commit b4b8c9ea79
48 changed files with 1643 additions and 290 deletions
+5 -4
View File
@@ -1,7 +1,6 @@
'use client';
import { FunctionComponent, PropsWithChildren, useMemo } from 'react';
import { useTranslations } from 'next-intl';
import { NotificationBell } from '@/components/notifications';
import { ProfileSummary } from '@/components';
import { ROUTES } from '@/constants';
import { useAdminCapabilities } from '@/hooks';
@@ -13,8 +12,11 @@ import TopBarAndSideBarLayout from './TopBarAndSideBarLayout';
* Admin / backoffice shell — the desktop ops console (f15). The sidebar is **sectioned**
* (اعتماد/مالی/پشتیبانی/سیستم) and **role-gated**: each console appears only when the current
* admin role can act on it (`useAdminCapabilities`), unchanged from before — grouping never adds,
* removes, or loosens a gate. The TopBar carries a page title, the notification bell (widened to
* the `'admin'` role), and a compact identity chip showing the admin's fine-grained role.
* removes, or loosens a gate. The TopBar carries a page title and a compact identity chip showing
* the admin's fine-grained role. **No notification bell** (ui-phase-10): admin notifications have no
* real feed yet (phase 11 owns that decision) and the bell was the only entry into the dead
* `admin/notifications` placeholder — re-add it once phase 11 ships a feed, reusing
* `NotificationBellPopover` (already exported for that handoff).
* @layout AdminLayout
*/
const AdminLayout: FunctionComponent<PropsWithChildren> = ({ children }) => {
@@ -50,7 +52,6 @@ const AdminLayout: FunctionComponent<PropsWithChildren> = ({ children }) => {
return (
<TopBarAndSideBarLayout
sidebarItems={sidebarItems}
headerActions={<NotificationBell role="admin" />}
identity={
me && primaryRoleCode ? (
<ProfileSummary
+17 -7
View File
@@ -1,11 +1,12 @@
'use client';
import { FunctionComponent, PropsWithChildren, useMemo } from 'react';
import { Box, Stack, Tab, Tabs } from '@mui/material';
import { Badge, Box, Stack, Tab, Tabs } from '@mui/material';
import { useTranslations } from 'next-intl';
import { AppIcon, AppIconButton, ErrorBoundary } from '@/components';
import { NotificationBell } from '@/components/notifications';
import { CONTENT_MAX_WIDTH } from '@/components/config';
import { ROUTES } from '@/constants';
import { useSupportUnreadTotal } from '@/services/tickets';
import { LinkToPage } from '@/utils';
import { usePathname, useRouter } from '@/i18n/navigation';
import { TopBar, BottomBar } from './components';
@@ -50,18 +51,27 @@ const CustomerHeader: FunctionComponent<{ bottomNavItems: Array<LinkToPage> }> =
const router = useRouter();
const title = useRouteTitle();
const onRootTab = isCustomerRootTab(pathname);
const supportUnreadTotal = useSupportUnreadTotal();
return (
<TopBar
align={onRootTab ? 'center' : 'start'}
startNode={
onRootTab ? (
<AppIconButton
icon="support"
color="inherit"
title={t('support')}
onClick={() => router.push(ROUTES.SUPPORT_TICKETS)}
/>
<Badge
badgeContent={supportUnreadTotal ?? 0}
max={99}
overlap="circular"
invisible={!supportUnreadTotal}
sx={{ '& .MuiBadge-badge': { bgcolor: 'var(--bal-error)', color: 'var(--bal-error-contrast)' } }}
>
<AppIconButton
icon="support"
color="inherit"
title={t('support')}
onClick={() => router.push(ROUTES.SUPPORT_TICKETS)}
/>
</Badge>
) : (
<AppIconButton icon="back" title={tc('back')} onClick={() => router.back()} />
)
+10 -2
View File
@@ -8,6 +8,7 @@ import { ROUTES } from '@/constants';
import { LinkToPage } from '@/utils';
import { useMe } from '@/services/auth';
import { useNurseProfile } from '@/services/profiles';
import { useSupportUnreadTotal } from '@/services/tickets';
import { useVerificationStatus } from '@/services/verification';
import { ownBadgeState } from '@/services/verification/types';
import TopBarAndSideBarLayout from './TopBarAndSideBarLayout';
@@ -29,6 +30,7 @@ const NurseLayout: FunctionComponent<PropsWithChildren> = ({ children }) => {
const { data: me } = useMe();
const { data: nurseProfile } = useNurseProfile();
const { data: verification } = useVerificationStatus();
const supportUnreadTotal = useSupportUnreadTotal();
const identityLoading = !me;
const displayName = me ? [me.firstName, me.lastName].filter(Boolean).join(' ').trim() || me.phone : '';
@@ -48,9 +50,15 @@ const NurseLayout: FunctionComponent<PropsWithChildren> = ({ children }) => {
{ title: t('verification'), path: ROUTES.NURSE_VERIFICATION, icon: 'verification', group: groupProfession },
{ title: t('earnings'), path: ROUTES.NURSE_EARNINGS, icon: 'earnings', group: groupFinance },
{ title: t('bank'), path: ROUTES.NURSE_BANK, icon: 'bank', group: groupFinance },
{ title: t('support'), path: ROUTES.NURSE_SUPPORT_TICKETS, icon: 'support', group: groupSupport },
{
title: t('support'),
path: ROUTES.NURSE_SUPPORT_TICKETS,
icon: 'support',
group: groupSupport,
badgeCount: supportUnreadTotal ?? undefined,
},
];
}, [t]);
}, [t, supportUnreadTotal]);
const mobileTabs = useMemo(
(): Array<LinkToPage> => [
@@ -1,6 +1,6 @@
'use client';
import { FunctionComponent, MouseEventHandler } from 'react';
import { ListItemButton, ListItemIcon, ListItemText } from '@mui/material';
import { Badge, ListItemButton, ListItemIcon, ListItemText } from '@mui/material';
import { AppIcon } from '@/components';
import { Link } from '@/i18n/navigation';
import { LinkToPage } from '@/utils';
@@ -14,13 +14,36 @@ interface Props extends LinkToPage {
* Renders a single SideBar navigation item over the locale-aware `Link` (`@/i18n/navigation`) —
* `href` is the unprefixed `ROUTES.*` path; the wrapper adds the active locale, so a click is one
* navigation with no middleware redirect hop. `selected` is computed by the caller (`SideBarNavList`)
* via the shared `matchActivePath` helper.
* via the shared `matchActivePath` helper. `badgeCount` renders a small unread dot on the icon (the
* nurse support entry, §3.1) — omitted entirely when falsy, never a bare "0".
* @component SideBarNavItem
*/
const SideBarNavItem: FunctionComponent<Props> = ({ icon, path, selected = false, subtitle, title, onClick }) => {
const SideBarNavItem: FunctionComponent<Props> = ({
icon,
path,
selected = false,
subtitle,
title,
onClick,
badgeCount,
}) => {
const iconNode = icon && <AppIcon icon={icon} />;
return (
<ListItemButton component={Link} href={path ?? '#'} selected={selected} onClick={onClick}>
<ListItemIcon>{icon && <AppIcon icon={icon} />}</ListItemIcon>
<ListItemIcon>
{badgeCount ? (
<Badge
badgeContent={badgeCount}
max={99}
overlap="circular"
sx={{ '& .MuiBadge-badge': { bgcolor: 'var(--bal-error)', color: 'var(--bal-error-contrast)' } }}
>
{iconNode}
</Badge>
) : (
iconNode
)}
</ListItemIcon>
<ListItemText primary={title} secondary={subtitle} />
</ListItemButton>
);
@@ -42,6 +42,7 @@ const SideBarNavList: FunctionComponent<Props> = ({ items, showIcons, onClick })
subtitle={item.subtitle}
selected={item.path === activePath}
onClick={onClick}
badgeCount={item.badgeCount}
/>
</Fragment>
);