Files
zerp/frontend/src/app/dashboard/page.tsx
Talal Sharabi 72ed9a2ff5 feat(hr): Complete HR module with Employee Portal, Loans, Leave, Purchase Requests, Contracts
- Database: Add Loan, LoanInstallment, PurchaseRequest, LeaveEntitlement, EmployeeContract models
- Database: Extend Attendance with ZK Tico fields (sourceDeviceId, externalId, rawData)
- Database: Add Employee.attendancePin for device mapping
- Backend: HR admin - Loans, Purchase Requests, Leave entitlements, Employee contracts CRUD
- Backend: Leave reject, bulk attendance sync (ZK Tico ready)
- Backend: Employee Portal API - scoped by employeeId (loans, leaves, purchase-requests, attendance, salaries)
- Frontend: Employee Portal - dashboard, loans, leave, purchase-requests, attendance, salaries
- Frontend: HR Admin - new tabs for Leaves, Loans, Purchase Requests, Contracts (approve/reject)
- Dashboard: Add My Portal link
- No destructive schema changes; additive migrations only

Made-with: Cursor
2026-03-04 19:44:09 +04:00

310 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client'
import { useState, useEffect } from 'react'
import ProtectedRoute from '@/components/ProtectedRoute'
import { useAuth } from '@/contexts/AuthContext'
import { useLanguage } from '@/contexts/LanguageContext'
import LanguageSwitcher from '@/components/LanguageSwitcher'
import Link from 'next/link'
import {
Users,
User,
TrendingUp,
Package,
CheckSquare,
UserCheck,
Megaphone,
LogOut,
Building2,
Settings,
Bell,
Shield
} from 'lucide-react'
import { dashboardAPI } from '@/lib/api'
function DashboardContent() {
const { user, logout, hasPermission } = useAuth()
const { t, language, dir } = useLanguage()
const [stats, setStats] = useState({ contacts: 0, activeTasks: 0, notifications: 0 })
useEffect(() => {
dashboardAPI.getStats()
.then((res) => {
if (res.data?.data) setStats(res.data.data)
})
.catch(() => {})
}, [])
const allModules = [
{
id: 'contacts',
name: 'إدارة جهات الاتصال',
nameEn: 'Contact Management',
icon: Users,
color: 'bg-blue-500',
href: '/contacts',
description: 'إدارة العملاء والموردين وجهات الاتصال',
permission: 'contacts'
},
{
id: 'crm',
name: 'إدارة علاقات العملاء',
nameEn: 'CRM',
icon: TrendingUp,
color: 'bg-green-500',
href: '/crm',
description: 'الفرص التجارية والعروض والصفقات',
permission: 'crm'
},
{
id: 'inventory',
name: 'المخزون والأصول',
nameEn: 'Inventory & Assets',
icon: Package,
color: 'bg-purple-500',
href: '/inventory',
description: 'المنتجات والمخازن والأصول الثابتة',
permission: 'inventory'
},
{
id: 'projects',
name: 'المهام والمشاريع',
nameEn: 'Tasks & Projects',
icon: CheckSquare,
color: 'bg-orange-500',
href: '/projects',
description: 'إدارة المشاريع والمهام والموارد',
permission: 'projects'
},
{
id: 'hr',
name: 'الموارد البشرية',
nameEn: 'Human Resources',
icon: UserCheck,
color: 'bg-teal-500',
href: '/hr',
description: 'الموظفين والإجازات والرواتب',
permission: 'hr'
},
{
id: 'portal',
name: 'البوابة الذاتية',
nameEn: 'My Portal',
icon: User,
color: 'bg-cyan-500',
href: '/portal',
description: 'قروضي، إجازاتي، طلبات الشراء والرواتب',
permission: 'hr'
},
{
id: 'marketing',
name: 'التسويق',
nameEn: 'Marketing',
icon: Megaphone,
color: 'bg-pink-500',
href: '/marketing',
description: 'الحملات التسويقية والعملاء المحتملين',
permission: 'marketing'
},
{
id: 'admin',
name: 'لوحة الإدارة',
nameEn: 'Admin Panel',
icon: Shield,
color: 'bg-red-500',
href: '/admin',
description: 'إدارة المستخدمين والأدوار وسجل العمليات',
permission: 'admin'
}
]
// Filter modules based on user permissions
const availableModules = allModules.filter(module =>
hasPermission(module.permission, 'view')
)
return (
<div className="min-h-screen bg-gradient-to-br from-gray-50 to-gray-100">
{/* Header */}
<header className="bg-white shadow-sm border-b sticky top-0 z-50">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="bg-primary-600 p-2 rounded-lg">
<Building2 className="h-8 w-8 text-white" />
</div>
<div>
<h1 className="text-2xl font-bold text-gray-900">Z.CRM</h1>
<p className="text-sm text-gray-600">نظام إدارة علاقات العملاء</p>
</div>
</div>
<div className="flex items-center gap-4">
{/* Language Switcher */}
<LanguageSwitcher />
{/* User Info */}
<div className="text-right">
<p className="text-sm font-semibold text-gray-900">{user?.username}</p>
<p className="text-xs text-gray-600">{user?.role?.name || 'مستخدم'}</p>
</div>
{/* Admin Panel Link - Only for admins */}
{hasPermission('admin', 'view') && (
<Link
href="/admin"
className="p-2 hover:bg-red-50 rounded-lg transition-colors relative group"
title="لوحة تحكم المدير"
>
<Shield className="h-5 w-5 text-red-600" />
<span className="absolute -bottom-8 right-0 bg-red-600 text-white text-xs px-2 py-1 rounded opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap">
لوحة الإدارة
</span>
</Link>
)}
{/* Notifications */}
<button className="p-2 hover:bg-gray-100 rounded-lg transition-colors relative">
<Bell className="h-5 w-5 text-gray-600" />
{stats.notifications > 0 && (
<span className="absolute top-1 right-1 h-2 w-2 bg-red-500 rounded-full"></span>
)}
</button>
{/* Settings */}
<button className="p-2 hover:bg-gray-100 rounded-lg transition-colors">
<Settings className="h-5 w-5 text-gray-600" />
</button>
{/* Logout */}
<button
onClick={logout}
className="flex items-center gap-2 px-4 py-2 text-red-600 hover:bg-red-50 rounded-lg transition-colors"
>
<LogOut className="h-5 w-5" />
<span className="font-medium">خروج</span>
</button>
</div>
</div>
</div>
</header>
{/* Main Content */}
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
{/* Welcome Section */}
<div className="bg-gradient-to-l from-primary-600 to-primary-700 rounded-2xl shadow-lg p-8 mb-8 text-white">
<h2 className="text-3xl font-bold mb-2">مرحباً، {user?.username}! 👋</h2>
<p className="text-primary-100 text-lg">
{user?.role?.name} - {availableModules.length} وحدة متاحة
</p>
</div>
{/* Stats Cards */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8">
<div className="bg-white rounded-xl shadow-md p-6 border border-gray-100">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-gray-600">الوحدات المتاحة</p>
<p className="text-3xl font-bold text-gray-900 mt-1">{availableModules.length}</p>
</div>
<div className="bg-blue-100 p-3 rounded-lg">
<Package className="h-8 w-8 text-blue-600" />
</div>
</div>
</div>
<div className="bg-white rounded-xl shadow-md p-6 border border-gray-100">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-gray-600">المهام النشطة</p>
<p className="text-3xl font-bold text-gray-900 mt-1">{stats.activeTasks}</p>
</div>
<div className="bg-green-100 p-3 rounded-lg">
<CheckSquare className="h-8 w-8 text-green-600" />
</div>
</div>
</div>
<div className="bg-white rounded-xl shadow-md p-6 border border-gray-100">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-gray-600">الإشعارات</p>
<p className="text-3xl font-bold text-gray-900 mt-1">{stats.notifications}</p>
</div>
<div className="bg-orange-100 p-3 rounded-lg">
<Bell className="h-8 w-8 text-orange-600" />
</div>
</div>
</div>
<div className="bg-white rounded-xl shadow-md p-6 border border-gray-100">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-gray-600">جهات الاتصال</p>
<p className="text-3xl font-bold text-gray-900 mt-1">{stats.contacts}</p>
</div>
<div className="bg-purple-100 p-3 rounded-lg">
<Users className="h-8 w-8 text-purple-600" />
</div>
</div>
</div>
</div>
{/* Available Modules */}
<div className="mb-8">
<h3 className="text-2xl font-bold text-gray-900 mb-6">الوحدات المتاحة</h3>
{availableModules.length > 0 ? (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{availableModules.map((module) => {
const Icon = module.icon
return (
<Link
key={module.id}
href={module.href}
className="bg-white rounded-xl shadow-md hover:shadow-xl transition-all duration-300 transform hover:-translate-y-1 p-6 border border-gray-200 group"
>
<div className="flex items-start gap-4">
<div className={`${module.color} p-3 rounded-lg group-hover:scale-110 transition-transform`}>
<Icon className="h-6 w-6 text-white" />
</div>
<div className="flex-1">
<h4 className="text-lg font-bold text-gray-900 mb-1">
{module.name}
</h4>
<p className="text-sm text-gray-500 mb-2">{module.nameEn}</p>
<p className="text-sm text-gray-600">{module.description}</p>
</div>
</div>
</Link>
)
})}
</div>
) : (
<div className="bg-yellow-50 border border-yellow-200 rounded-xl p-8 text-center">
<p className="text-yellow-800 text-lg">
لا توجد وحدات متاحة لحسابك. الرجاء التواصل مع المسؤول لمنح الصلاحيات المناسبة.
</p>
</div>
)}
</div>
{/* Recent Activity */}
<div className="bg-white rounded-xl shadow-md p-6 border border-gray-100">
<h3 className="text-xl font-bold text-gray-900 mb-4">النشاط الأخير</h3>
<p className="text-gray-500 text-center py-6">لا يوجد نشاط حديث</p>
</div>
</main>
</div>
)
}
export default function DashboardPage() {
return (
<ProtectedRoute>
<DashboardContent />
</ProtectedRoute>
)
}