feat: port tauri header top-menu to admin topbar
Rebuild admin Topbar to match clinic-pro-tauri header: - Icon order: theme -> settings -> bell (was theme -> bell -> settings) - Settings gear now navigates to the role-based settings page instead of opening the personalization panel - Search placeholder changed to «جستجو», dropped the ⌘K hint - Add ProfileMenu: avatar + user name + context/role subtitle + caret, with a dropdown (profile / payments / settings / personalization / logout). Links are role-aware; personalization keeps the existing settings panel. Frontend-only, no API changes. Adds ProfileMenu.test + Topbar.test. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,154 @@
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import {
|
||||
ChevronDownIcon,
|
||||
UserCircleIcon,
|
||||
CreditCardIcon,
|
||||
Cog6ToothIcon,
|
||||
SwatchIcon,
|
||||
ArrowLeftOnRectangleIcon,
|
||||
} from '@heroicons/react/24/outline';
|
||||
import { useAuthStore } from '../../stores/authStore';
|
||||
import { useUiStore } from '../../stores/uiStore';
|
||||
|
||||
type Role = string | null;
|
||||
|
||||
// نگاشت نقش → برچسب فارسی (همتراز با Sidebar).
|
||||
const ROLE_LABELS: Record<string, string> = {
|
||||
admin: 'مدیر کل',
|
||||
clinic: 'مالک کلینیک',
|
||||
doctor: 'پزشک',
|
||||
secretary: 'منشی',
|
||||
representation: 'نماینده',
|
||||
user: 'کاربر',
|
||||
};
|
||||
|
||||
const HUES = [256, 205, 162, 295, 272];
|
||||
function avatarBg(name: string): string {
|
||||
const hue = HUES[(name.charCodeAt(0) ?? 0) % HUES.length];
|
||||
return `linear-gradient(145deg, oklch(0.62 0.15 ${hue}), oklch(0.48 0.16 ${hue}))`;
|
||||
}
|
||||
|
||||
// مسیرهای نقشمحور — معادلِ لینکهای MenuProfile در clinic-pro-tauri.
|
||||
function profilePath(role: Role): string {
|
||||
if (role === 'doctor') return '/admin/profile';
|
||||
if (role === 'representation') return '/admin/representation-profile';
|
||||
return '/admin/account-settings';
|
||||
}
|
||||
function paymentsPath(role: Role): string {
|
||||
return role === 'admin' ? '/admin/payments' : '/admin/my-payments';
|
||||
}
|
||||
export function settingsPath(role: Role): string {
|
||||
if (role === 'admin') return '/admin/settings';
|
||||
if (role === 'doctor' || role === 'clinic') return '/admin/settings-menu';
|
||||
return '/admin/account-settings';
|
||||
}
|
||||
|
||||
export default function ProfileMenu() {
|
||||
const navigate = useNavigate();
|
||||
const userName = useAuthStore((s) => s.userName);
|
||||
const primaryRole = useAuthStore((s) => s.primaryRole);
|
||||
const context = useAuthStore((s) => s.context);
|
||||
const logout = useAuthStore((s) => s.logout);
|
||||
const toggleSettingsPanel = useUiStore((s) => s.toggleSettingsPanel);
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
const [menuPos, setMenuPos] = useState<{ top: number; right: number } | null>(null);
|
||||
const btnRef = useRef<HTMLButtonElement>(null);
|
||||
const menuRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const name = userName ?? 'کاربر';
|
||||
const initial = name.charAt(0).toUpperCase();
|
||||
// زیرعنوان: نام محیط کاری (context) وگرنه برچسب نقش — معادلِ «expertise» در tauri.
|
||||
const subtitle = context?.name ?? ROLE_LABELS[primaryRole ?? ''] ?? primaryRole ?? '';
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
function handler(e: MouseEvent) {
|
||||
const target = e.target as Node;
|
||||
if (!btnRef.current?.contains(target) && !menuRef.current?.contains(target)) {
|
||||
setOpen(false);
|
||||
}
|
||||
}
|
||||
document.addEventListener('mousedown', handler);
|
||||
return () => document.removeEventListener('mousedown', handler);
|
||||
}, [open]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
function reposition() {
|
||||
if (btnRef.current) {
|
||||
const rect = btnRef.current.getBoundingClientRect();
|
||||
setMenuPos({ top: rect.bottom + 6, right: window.innerWidth - rect.right });
|
||||
}
|
||||
}
|
||||
window.addEventListener('scroll', reposition, true);
|
||||
window.addEventListener('resize', reposition);
|
||||
return () => {
|
||||
window.removeEventListener('scroll', reposition, true);
|
||||
window.removeEventListener('resize', reposition);
|
||||
};
|
||||
}, [open]);
|
||||
|
||||
function handleToggle() {
|
||||
if (!open && btnRef.current) {
|
||||
const rect = btnRef.current.getBoundingClientRect();
|
||||
setMenuPos({ top: rect.bottom + 6, right: window.innerWidth - rect.right });
|
||||
}
|
||||
setOpen((o) => !o);
|
||||
}
|
||||
|
||||
function go(path: string) {
|
||||
setOpen(false);
|
||||
navigate(path);
|
||||
}
|
||||
|
||||
function handleLogout() {
|
||||
setOpen(false);
|
||||
logout();
|
||||
navigate('/admin/login');
|
||||
}
|
||||
|
||||
const items = [
|
||||
{ label: 'پروفایل من', icon: UserCircleIcon, onClick: () => go(profilePath(primaryRole)) },
|
||||
{ label: 'پرداختها', icon: CreditCardIcon, onClick: () => go(paymentsPath(primaryRole)) },
|
||||
{ label: 'تنظیمات', icon: Cog6ToothIcon, onClick: () => go(settingsPath(primaryRole)) },
|
||||
{ label: 'شخصیسازی', icon: SwatchIcon, onClick: () => { setOpen(false); toggleSettingsPanel(); } },
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<button ref={btnRef} className="profile-trigger" onClick={handleToggle} title={name}>
|
||||
<div className="avatar sm" style={{ background: avatarBg(name), flexShrink: 0 }}>
|
||||
{initial}
|
||||
</div>
|
||||
<div className="profile-meta">
|
||||
<b>{name}</b>
|
||||
{subtitle && <span>{subtitle}</span>}
|
||||
</div>
|
||||
<ChevronDownIcon className="profile-caret" style={{ width: 15, height: 15, flexShrink: 0 }} />
|
||||
</button>
|
||||
|
||||
{open && menuPos && ReactDOM.createPortal(
|
||||
<div
|
||||
ref={menuRef}
|
||||
className="profile-menu"
|
||||
style={{ position: 'fixed', top: menuPos.top, right: menuPos.right }}
|
||||
>
|
||||
{items.map((item) => (
|
||||
<button key={item.label} className="profile-item" onClick={item.onClick}>
|
||||
<item.icon style={{ width: 17, height: 17, flexShrink: 0 }} />
|
||||
{item.label}
|
||||
</button>
|
||||
))}
|
||||
<button className="profile-item danger" onClick={handleLogout}>
|
||||
<ArrowLeftOnRectangleIcon style={{ width: 17, height: 17, flexShrink: 0 }} />
|
||||
خروج
|
||||
</button>
|
||||
</div>,
|
||||
document.body,
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user