[CLAUDE] FE: TopBar + NotificationBell + UserMenu — ERP shell foundation
Some checks failed
Deploy SOLUTION_ERP / build-deploy (push) Has been cancelled
Some checks failed
Deploy SOLUTION_ERP / build-deploy (push) Has been cancelled
Kiến trúc Layout giờ tách thành [sidebar] [topbar + content], foundation để scale thêm module trong tương lai (HR, Accounting, Inventory...). TopBar: title placeholder + NotificationBell + UserMenu (initials avatar + role badges + logout). UserMenu thay cho bottom-of-sidebar (cleaner). NotificationBell: - fe-admin: cảnh báo SLA (HĐ quá/sắp quá hạn, 24h window) - fe-user: hộp thư chờ xử lý (items trong /contracts/inbox) - refetchInterval: 60s - Placeholder cho SignalR/email notifications thật sẽ thay bằng /api/notifications endpoint ở Tier 3. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
79
fe-admin/src/components/TopBar.tsx
Normal file
79
fe-admin/src/components/TopBar.tsx
Normal file
@ -0,0 +1,79 @@
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { ChevronDown, LogOut } from 'lucide-react'
|
||||
import { useAuth } from '@/contexts/AuthContext'
|
||||
import { NotificationBell } from '@/components/NotificationBell'
|
||||
import { cn } from '@/lib/cn'
|
||||
|
||||
function UserMenu() {
|
||||
const { user, logout } = useAuth()
|
||||
const [open, setOpen] = useState(false)
|
||||
const ref = useRef<HTMLDivElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return
|
||||
const close = (e: MouseEvent) => {
|
||||
if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false)
|
||||
}
|
||||
document.addEventListener('mousedown', close)
|
||||
return () => document.removeEventListener('mousedown', close)
|
||||
}, [open])
|
||||
|
||||
const initials = (user?.fullName ?? user?.email ?? '?')
|
||||
.split(' ')
|
||||
.filter(Boolean)
|
||||
.map(s => s[0])
|
||||
.slice(-2)
|
||||
.join('')
|
||||
.toUpperCase()
|
||||
|
||||
return (
|
||||
<div className="relative" ref={ref}>
|
||||
<button
|
||||
onClick={() => setOpen(o => !o)}
|
||||
className="flex items-center gap-2 rounded-md px-2 py-1.5 text-sm transition hover:bg-slate-100"
|
||||
>
|
||||
<span className="flex h-7 w-7 items-center justify-center rounded-full bg-brand-100 text-xs font-bold text-brand-700">
|
||||
{initials}
|
||||
</span>
|
||||
<span className="hidden max-w-32 truncate text-slate-700 md:inline">{user?.fullName ?? user?.email}</span>
|
||||
<ChevronDown className={cn('h-3.5 w-3.5 text-slate-400 transition', open && 'rotate-180')} />
|
||||
</button>
|
||||
{open && (
|
||||
<div className="absolute right-0 z-50 mt-2 w-60 overflow-hidden rounded-lg border border-slate-200 bg-white shadow-lg">
|
||||
<div className="border-b border-slate-100 px-4 py-3">
|
||||
<div className="truncate text-sm font-medium text-slate-800">{user?.fullName}</div>
|
||||
<div className="truncate text-xs text-slate-500">{user?.email}</div>
|
||||
{user && user.roles.length > 0 && (
|
||||
<div className="mt-1.5 flex flex-wrap gap-1">
|
||||
{user.roles.map(r => (
|
||||
<span key={r} className="rounded-full bg-slate-100 px-2 py-0.5 text-[10px] font-medium text-slate-600">
|
||||
{r}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
onClick={logout}
|
||||
className="flex w-full items-center gap-2 px-4 py-2 text-sm text-slate-600 transition hover:bg-slate-50"
|
||||
>
|
||||
<LogOut className="h-4 w-4" />
|
||||
Đăng xuất
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function TopBar({ title }: { title?: string }) {
|
||||
return (
|
||||
<header className="flex h-14 items-center justify-between border-b border-slate-200 bg-white px-6">
|
||||
<div className="text-sm font-medium text-slate-500">{title}</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<NotificationBell />
|
||||
<UserMenu />
|
||||
</div>
|
||||
</header>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user