- Implemented GET /api/v1/dashboard/clinic to return clinic stats and today's schedule for clinic owners. - Implemented GET /api/v1/dashboard/doctor to return doctor's stats and today's schedule for doctors. - Implemented GET /api/v1/dashboard/secretary to return stats and conditional appointments for secretaries. feat(migrations): create user_active_context and mobile_verification_otp tables - Added migration to create user_active_context table for tracking active user sessions. - Added migration to create mobile_verification_otp table for handling mobile number verification. feat(migrations): create site_config table for application settings - Added migration to create site_config table to store various site configuration settings. feat(appointments): create MyAppointmentsController for user-specific appointments - Added MyAppointmentsController to handle fetching user-specific appointments with pagination and filtering. feat(auth): implement NotificationMobileController for mobile number verification - Added NotificationMobileController to handle OTP requests and verification for mobile number changes. feat(auth): create MobileVerificationOtp entity for OTP management - Created MobileVerificationOtp entity to manage OTP records for mobile verification. feat(auth): create UserActiveContext entity for user session management - Created UserActiveContext entity to manage user active sessions. feat(config): implement SiteConfigController for managing site settings - Added SiteConfigController to handle fetching and updating site configuration settings. feat(config): create SiteConfig entity and repository for configuration management - Created SiteConfig entity and repository to manage site configuration data.
229 lines
8.4 KiB
TypeScript
229 lines
8.4 KiB
TypeScript
import {
|
|
ArrowLeftOnRectangleIcon,
|
|
ArrowsRightLeftIcon,
|
|
BanknotesIcon,
|
|
BuildingOffice2Icon,
|
|
Cog6ToothIcon,
|
|
CalendarDaysIcon,
|
|
ChartBarIcon,
|
|
ChatBubbleLeftEllipsisIcon,
|
|
CreditCardIcon,
|
|
DevicePhoneMobileIcon,
|
|
DocumentTextIcon,
|
|
HeartIcon,
|
|
KeyIcon,
|
|
StarIcon,
|
|
TagIcon,
|
|
UserGroupIcon,
|
|
UsersIcon,
|
|
} from "@heroicons/react/24/outline";
|
|
import { NavLink, useNavigate } from "react-router-dom";
|
|
import { useAuthStore } from "../../stores/authStore";
|
|
import { useUiStore } from "../../stores/uiStore";
|
|
|
|
type SectionItem = { to: string; icon: React.ElementType; label: string };
|
|
type Section = { label: string; items: SectionItem[] };
|
|
|
|
function buildSections(primaryRole: string | null, dbUuid: string | null): Section[] {
|
|
if (primaryRole === 'admin') {
|
|
return [
|
|
{
|
|
label: 'عمومی',
|
|
items: [
|
|
{ to: '/admin/dashboard', icon: ChartBarIcon, label: 'داشبورد' },
|
|
{ to: '/admin/users', icon: UserGroupIcon, label: 'کاربران' },
|
|
{ to: '/admin/doctors', icon: HeartIcon, label: 'پزشکان' },
|
|
{ to: '/admin/clinics', icon: BuildingOffice2Icon, label: 'کلینیکها' },
|
|
],
|
|
},
|
|
{
|
|
label: 'مدیریت',
|
|
items: [
|
|
{ to: '/admin/appointments', icon: CalendarDaysIcon, label: 'نوبتها' },
|
|
{ to: '/admin/payments', icon: CreditCardIcon, label: 'پرداختها' },
|
|
{ to: '/admin/settlements', icon: BanknotesIcon, label: 'تسویهحساب' },
|
|
],
|
|
},
|
|
{
|
|
label: 'محتوا',
|
|
items: [
|
|
{ to: '/admin/comments', icon: ChatBubbleLeftEllipsisIcon, label: 'نظرات' },
|
|
{ to: '/admin/ratings', icon: StarIcon, label: 'امتیازها' },
|
|
{ to: '/admin/blogs', icon: DocumentTextIcon, label: 'بلاگ' },
|
|
{ to: '/admin/sms', icon: DevicePhoneMobileIcon, label: 'پیامک' },
|
|
],
|
|
},
|
|
{
|
|
label: 'سیستم',
|
|
items: [
|
|
{ to: '/admin/categories', icon: TagIcon, label: 'دستهبندیها' },
|
|
{ to: '/admin/representations', icon: UsersIcon, label: 'نمایندگان' },
|
|
{ to: '/admin/secretaries', icon: KeyIcon, label: 'منشیها' },
|
|
{ to: '/admin/settings', icon: Cog6ToothIcon, label: 'تنظیمات' },
|
|
],
|
|
},
|
|
];
|
|
}
|
|
|
|
if (primaryRole === 'clinic') {
|
|
const clinicTo = dbUuid ? `/admin/clinics/${dbUuid}` : '/admin/my-clinic';
|
|
return [
|
|
{
|
|
label: 'عمومی',
|
|
items: [
|
|
{ to: '/admin/dashboard', icon: ChartBarIcon, label: 'داشبورد' },
|
|
{ to: clinicTo, icon: BuildingOffice2Icon, label: 'کلینیک من' },
|
|
{ to: '/admin/doctors', icon: HeartIcon, label: 'پزشکان' },
|
|
],
|
|
},
|
|
{
|
|
label: 'مدیریت',
|
|
items: [
|
|
{ to: '/admin/appointments', icon: CalendarDaysIcon, label: 'نوبتها' },
|
|
],
|
|
},
|
|
];
|
|
}
|
|
|
|
if (primaryRole === 'doctor') {
|
|
return [
|
|
{
|
|
label: 'عمومی',
|
|
items: [
|
|
{ to: '/admin/dashboard', icon: ChartBarIcon, label: 'داشبورد' },
|
|
],
|
|
},
|
|
{
|
|
label: 'مدیریت',
|
|
items: [
|
|
{ to: '/admin/appointments', icon: CalendarDaysIcon, label: 'نوبتهای من' },
|
|
],
|
|
},
|
|
];
|
|
}
|
|
|
|
if (primaryRole === 'secretary') {
|
|
return [
|
|
{
|
|
label: 'عمومی',
|
|
items: [
|
|
{ to: '/admin/dashboard', icon: ChartBarIcon, label: 'داشبورد' },
|
|
],
|
|
},
|
|
{
|
|
label: 'مدیریت',
|
|
items: [
|
|
{ to: '/admin/appointments', icon: CalendarDaysIcon, label: 'نوبتها' },
|
|
],
|
|
},
|
|
];
|
|
}
|
|
|
|
return [
|
|
{
|
|
label: 'عمومی',
|
|
items: [{ to: '/admin/dashboard', icon: ChartBarIcon, label: 'داشبورد' }],
|
|
},
|
|
];
|
|
}
|
|
|
|
const ROLE_LABELS: Record<string, string> = {
|
|
admin: 'مدیر کل',
|
|
clinic: 'مالک کلینیک',
|
|
doctor: 'پزشک',
|
|
secretary: 'منشی',
|
|
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}))`;
|
|
}
|
|
|
|
interface Props {
|
|
mobileOpen?: boolean;
|
|
onMobileClose?: () => void;
|
|
}
|
|
|
|
export default function Sidebar({ mobileOpen: _m, onMobileClose: _c }: Props) {
|
|
const sidebarOpen = useUiStore((s) => s.sidebarOpen);
|
|
const { logout, primaryRole, userName, availableContexts, dbUuid } = useAuthStore();
|
|
const navigate = useNavigate();
|
|
|
|
const sections = buildSections(primaryRole, dbUuid);
|
|
const initials = (userName ?? 'U').charAt(0).toUpperCase();
|
|
|
|
return (
|
|
<aside className="sidebar">
|
|
{/* Brand */}
|
|
<div className="sidebar-brand">
|
|
<div className="brand-logo">
|
|
<HeartIcon style={{ width: 20, height: 20 }} />
|
|
</div>
|
|
<div className="brand-text">
|
|
<b>ClinicPro</b>
|
|
<span>پنل مدیریت</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Navigation */}
|
|
<nav className="nav">
|
|
{sections.map((section) => (
|
|
<div className="nav-group" key={section.label}>
|
|
<span className="nav-label">{section.label}</span>
|
|
{section.items.map(({ to, icon: Icon, label }) => (
|
|
<NavLink
|
|
key={to}
|
|
to={to}
|
|
title={!sidebarOpen ? label : undefined}
|
|
className={({ isActive }) => `nav-item${isActive ? ' active' : ''}`}
|
|
>
|
|
<Icon style={{ width: 19, height: 19, flexShrink: 0 }} />
|
|
<span>{label}</span>
|
|
</NavLink>
|
|
))}
|
|
</div>
|
|
))}
|
|
|
|
{/* تغییر محیط کاری — فقط اگر چند context دارد */}
|
|
{availableContexts.length > 1 && (
|
|
<div className="nav-group">
|
|
<span className="nav-label">محیط کاری</span>
|
|
<NavLink
|
|
to="/admin/select-context"
|
|
title={!sidebarOpen ? 'تغییر محیط' : undefined}
|
|
className={({ isActive }) => `nav-item${isActive ? ' active' : ''}`}
|
|
>
|
|
<ArrowsRightLeftIcon style={{ width: 19, height: 19, flexShrink: 0 }} />
|
|
<span>تغییر محیط</span>
|
|
</NavLink>
|
|
</div>
|
|
)}
|
|
</nav>
|
|
|
|
{/* User footer */}
|
|
<div className="sidebar-foot">
|
|
<div
|
|
className="user-chip"
|
|
onClick={() => { logout(); navigate('/admin/login'); }}
|
|
title="خروج از سیستم"
|
|
>
|
|
<div
|
|
className="avatar sm"
|
|
style={{ background: avatarBg(userName ?? 'U'), flexShrink: 0 }}
|
|
>
|
|
{initials}
|
|
</div>
|
|
<div className="user-meta">
|
|
<b>{userName ?? 'کاربر'}</b>
|
|
<span>{ROLE_LABELS[primaryRole ?? ''] ?? primaryRole ?? ''}</span>
|
|
</div>
|
|
<ArrowLeftOnRectangleIcon style={{ width: 16, height: 16, flexShrink: 0, color: 'var(--text-3)' }} />
|
|
</div>
|
|
</div>
|
|
</aside>
|
|
);
|
|
}
|