HR-ATS-Portal/frontend/src/app/Topbar.jsx

196 lines
7.7 KiB
JavaScript

import { Link, useNavigate } from 'react-router-dom'
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import Dropdown, { DropdownGroup } from '../ui/Dropdown'
import GlobalSearch from './GlobalSearch'
import { Avatar, Icon } from '../ui/primitives'
import { seedQuery } from '../data/seedQueries'
import { useToast } from '../ui/Toast'
import { useTheme } from '../theme/ThemeProvider'
import { useAuth } from '../auth/AuthContext'
import { qk } from '../lib/queryKeys'
import { friendlyAuthError } from '../lib/errors'
import { formatRole } from '../lib/format'
import * as notificationsApi from '../api/notifications'
function initialsFromName(name) {
const parts = String(name || '').trim().split(/\s+/).filter(Boolean)
if (!parts.length) return '?'
if (parts.length === 1) return parts[0].slice(0, 2).toUpperCase()
return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase()
}
async function fetchNotifications() {
const res = await notificationsApi.list({ top: 6 })
const rows = Array.isArray(res?.data) ? res.data : []
return {
items: rows.map(notificationsApi.toNotificationView),
unread: res?.unread ?? 0,
}
}
export default function Topbar({ onOpenNav, searchRef }) {
const { theme, toggleTheme } = useTheme()
const { user, signOut } = useAuth()
const { toast } = useToast()
const navigate = useNavigate()
const qc = useQueryClient()
const notifQuery = useQuery({ queryKey: qk.notifications.list({ top: 6 }), queryFn: fetchNotifications })
const notifications = notifQuery.data?.items ?? []
const unread = notifQuery.data?.unread ?? 0
const { data: messages = [] } = useQuery(seedQuery('messages'))
const markAll = useMutation({
mutationFn: () => notificationsApi.markAllRead(),
onError: (err) => toast(friendlyAuthError(err, 'Could not mark all as read.'), 'error'),
onSuccess: () => toast('All notifications marked as read', 'success'),
onSettled: () => qc.invalidateQueries({ queryKey: qk.notifications.all() }),
})
const markOne = useMutation({
mutationFn: (id) => notificationsApi.markRead(id),
onSettled: () => qc.invalidateQueries({ queryKey: qk.notifications.all() }),
})
const name = user?.name || 'Guest'
const email = user?.email || ''
const role = formatRole(user?.role_name || user?.role) || 'Member'
function openNotif(n) {
if (n.unread) markOne.mutate(n.id)
if (n.linkPath) navigate(n.linkPath)
else navigate('/notifications')
}
return (
<header className="topbar">
<button className="icon-btn menu-toggle" onClick={onOpenNav} aria-label="Toggle menu">
<Icon name="menu" />
</button>
<GlobalSearch inputRef={searchRef} />
<div className="topbar-actions">
<button
className="icon-btn"
onClick={toggleTheme}
aria-pressed={theme === 'dark'}
title={theme === 'dark' ? 'Switch to light mode' : 'Switch to dark mode'}
aria-label={theme === 'dark' ? 'Switch to light mode' : 'Switch to dark mode'}
>
<span className="icon-sun"><Icon name="sun" /></span>
<span className="icon-moon"><Icon name="moon" /></span>
</button>
<DropdownGroup>
<Dropdown
panelClassName="dropdown-menu-wide"
trigger={({ toggle }) => (
<button className="icon-btn" onClick={toggle} title="Messages" aria-label="Messages">
<Icon name="message" />
{messages.some((m) => m.unread) && <span className="dot dot-blue" />}
</button>
)}
>
<div className="dropdown-head">Messages</div>
<div className="dd-scroll">
{messages.map((m) => (
<div key={m.id ?? m.name} className={`notif-row${m.unread ? ' unread' : ''}`}>
<Avatar name={m.name} initials={m.initials} color={m.color} />
<div className="notif-body">
<div className="notif-title">{m.name}</div>
<div className="notif-text">{m.text}</div>
<div className="notif-time">{m.time} ago</div>
</div>
</div>
))}
</div>
<div className="dropdown-foot">
<Link to="/inbox">Open inbox</Link>
</div>
</Dropdown>
<Dropdown
panelClassName="dropdown-menu-wide"
trigger={({ toggle }) => (
<button className="icon-btn" onClick={toggle} title="Notifications" aria-label="Notifications">
<Icon name="bell" />
{unread > 0 && <span className="dot dot-red" />}
</button>
)}
>
<div className="dropdown-head">
Notifications
<button className="link-btn" disabled={markAll.isPending} onClick={() => markAll.mutate()}>
Mark all read
</button>
</div>
<div className="dd-scroll">
{notifQuery.isPending && <div className="notif-row"><div className="notif-text">Loading</div></div>}
{notifQuery.isError && (
<div className="notif-row">
<div className="notif-text">{friendlyAuthError(notifQuery.error, 'Could not load notifications.')}</div>
</div>
)}
{notifQuery.isSuccess && notifications.length === 0 && (
<div className="notif-row"><div className="notif-text">No notifications yet.</div></div>
)}
{notifications.map((n) => (
<button
key={n.id}
type="button"
className={`notif-row${n.unread ? ' unread' : ''}`}
onClick={() => openNotif(n)}
>
<span className={`notif-icn ${n.color}`}><Icon name={n.icon} /></span>
<div className="notif-body">
<div className="notif-title">{n.title}</div>
{n.text && <div className="notif-text">{n.text}</div>}
<div className="notif-time">{n.time}</div>
</div>
</button>
))}
</div>
<div className="dropdown-foot">
<Link to="/notifications">View all</Link>
</div>
</Dropdown>
<div className="topbar-divider" />
<Dropdown
trigger={({ toggle }) => (
<button className="profile-btn" onClick={toggle}>
<span className="avatar avatar-grad">{initialsFromName(name)}</span>
<span className="profile-meta">
<span className="profile-name">{name}</span>
<span className="profile-role">{role}</span>
</span>
<Icon name="chevron-down" className="chev" />
</button>
)}
>
<div className="dropdown-profile">
<span className="avatar avatar-grad avatar-lg">{initialsFromName(name)}</span>
<div>
<div className="dp-name">{name}</div>
<div className="dp-email">{email}</div>
</div>
</div>
<div className="dropdown-divider" />
<Link className="dropdown-link" to="/settings"><Icon name="user" />My Profile</Link>
<Link className="dropdown-link" to="/settings"><Icon name="settings" />Settings</Link>
<Link className="dropdown-link" to="/help"><Icon name="help" />Help Center</Link>
<div className="dropdown-divider" />
<button className="dropdown-link danger" onClick={signOut}>
<Icon name="logout" />Sign out
</button>
</Dropdown>
</DropdownGroup>
</div>
</header>
)
}