Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 | 5x 5x 5x 11x 6x 5x 2x 5x 5x 5x 6x 6x | // src/components/layout/NotificationBell.tsx
import {type ElementType, type ReactElement, useState} from 'react';
import {useNavigate} from 'react-router-dom';
import {
Badge,
Box,
Divider,
IconButton,
List,
ListItemButton,
ListItemIcon,
ListItemText,
Menu,
Tooltip,
Typography
} from '@mui/material';
import {Notifications as NotificationsIcon, NotificationsNone as NotificationsNoneIcon} from '@mui/icons-material';
// ============================================================================
// Types
// ============================================================================
export interface NotificationItem {
/** Stable key + display label, e.g. "Loan requests". */
label: string;
/** Count of pending items; rows with count <= 0 are not rendered. */
count: number;
/** Route to navigate to when the row is clicked. */
to: string;
icon: ElementType;
}
interface NotificationBellProps {
items: NotificationItem[];
}
// ============================================================================
// NotificationBell Component
// ============================================================================
/**
* Header notification bell (FSC-111). Surfaces pending actions (loan requests,
* disbursements, document reviews, etc.) as a badge + dropdown. The items are
* supplied by the layout, which already runs the underlying queries for the
* nav badges, so the bell adds no extra network traffic.
*/
export default function NotificationBell({items}: NotificationBellProps): ReactElement {
const navigate = useNavigate();
const [anchorEl, setAnchorEl] = useState<HTMLElement | null>(null);
const open = anchorEl !== null;
const pending = items.filter((item) => item.count > 0);
const total = pending.reduce((sum, item) => sum + item.count, 0);
const handleOpen = (event: React.MouseEvent<HTMLElement>): void => {
setAnchorEl(event.currentTarget);
};
const handleClose = (): void => {
setAnchorEl(null);
};
const handleSelect = (to: string): void => {
setAnchorEl(null);
navigate(to);
};
return (
<>
<Tooltip title={total > 0 ? `${total} pending action${total === 1 ? '' : 's'}` : 'No pending actions'}>
<IconButton
color="inherit"
aria-label={`notifications: ${total} pending`}
onClick={handleOpen}
>
<Badge badgeContent={total} color="error" overlap="circular">
{total > 0 ? <NotificationsIcon/> : <NotificationsNoneIcon/>}
</Badge>
</IconButton>
</Tooltip>
<Menu
anchorEl={anchorEl}
open={open}
onClose={handleClose}
anchorOrigin={{vertical: 'bottom', horizontal: 'right'}}
transformOrigin={{vertical: 'top', horizontal: 'right'}}
slotProps={{paper: {sx: {width: 320, maxWidth: '100%', mt: 1}}}}
>
<Box sx={{px: 2, py: 1.5}}>
<Typography variant="subtitle2" sx={{fontWeight: 600}}>
Notifications
</Typography>
</Box>
<Divider/>
{pending.length === 0 ? (
<Box sx={{px: 2, py: 3, textAlign: 'center'}}>
<Typography variant="body2" sx={{color: 'text.secondary'}}>
You're all caught up.
</Typography>
</Box>
) : (
<List disablePadding>
{pending.map((item) => {
const Icon = item.icon;
return (
<ListItemButton key={item.label} onClick={(): void => {
handleSelect(item.to);
}}>
<ListItemIcon sx={{minWidth: 40}}>
<Badge badgeContent={item.count} color="error">
<Icon fontSize="small"/>
</Badge>
</ListItemIcon>
<ListItemText
primary={item.label}
slotProps={{primary: {variant: 'body2', sx: {fontWeight: 500}}}}
/>
</ListItemButton>
);
})}
</List>
)}
</Menu>
</>
);
}
|