Files
musadeq/frontend/src/pages/staff/StaffPage.tsx
2026-04-19 15:54:50 +03:00

217 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.
/**
* ════════════════════════════════════════════════════════════
* مُصادَق (Musadaq) — Staff Management Page
* ════════════════════════════════════════════════════════════
*/
import { useState, useEffect } from 'react';
import { Users, UserPlus, Mail, Power } from 'lucide-react';
import { motion, AnimatePresence } from 'framer-motion';
import apiClient from '../../api/client';
import { useAuthStore } from '../../store/authStore';
export const StaffPage = () => {
const currentUser = useAuthStore((state) => state.user);
const [staff, setStaff] = useState<any[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [isAddModalOpen, setIsAddModalOpen] = useState(false);
// Form State
const [fullName, setFullName] = useState('');
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [role, setRole] = useState('accountant');
const fetchStaff = async () => {
setIsLoading(true);
try {
const { data } = await apiClient.get('/users');
setStaff(data);
} catch (error) {
console.error('Failed to fetch staff', error);
} finally {
setIsLoading(false);
}
};
useEffect(() => {
fetchStaff();
}, []);
const handleAddStaff = async (e: React.FormEvent) => {
e.preventDefault();
try {
await apiClient.post('/users', {
name: fullName,
email,
password,
role
});
setIsAddModalOpen(false);
setFullName('');
setEmail('');
setPassword('');
fetchStaff();
} catch (error: any) {
alert(error.response?.data?.message || 'فشل إضافة الموظف');
}
};
const toggleStatus = async (id: string) => {
try {
await apiClient.delete(`/users/${id}`);
fetchStaff();
} catch (error) {
alert('فشل تغيير حالة الموظف');
}
};
return (
<div className="space-y-8 animate-in fade-in slide-in-from-bottom-4 duration-700">
<header className="flex items-center justify-between">
<div>
<h2 className="text-3xl font-bold text-slate-900 tracking-tight">إدارة الموظفين</h2>
<p className="text-slate-500 mt-1 font-medium">إدارة المحاسبين والمديرين في مكتبك.</p>
</div>
<button
onClick={() => setIsAddModalOpen(true)}
className="btn-primary py-3 px-8 rounded-2xl flex items-center gap-2 shadow-xl shadow-primary-500/25 active:scale-95 transition-all"
>
<UserPlus className="w-5 h-5" />
إضافة موظف جديد
</button>
</header>
{isLoading ? (
<div className="flex justify-center p-20">
<div className="animate-spin rounded-full h-10 w-10 border-b-2 border-primary-600"></div>
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{staff.map((member) => (
<motion.div
key={member.id}
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
className="card-premium p-6 group hover:border-primary-200 transition-all border border-slate-100"
>
<div className="flex justify-between items-start mb-6">
<div className="w-14 h-14 rounded-2xl bg-slate-50 text-slate-400 flex items-center justify-center group-hover:bg-primary-50 group-hover:text-primary-600 transition-all">
<Users className="w-7 h-7" />
</div>
<div className="flex gap-2">
{member.id !== currentUser?.id && (
<button
onClick={() => toggleStatus(member.id)}
className={`p-2 rounded-xl transition-all ${member.is_active ? 'text-emerald-500 bg-emerald-50' : 'text-slate-400 bg-slate-50'}`}
title={member.is_active ? 'تعطيل الحساب' : 'تفعيل الحساب'}
>
<Power className="w-4 h-4" />
</button>
)}
</div>
</div>
<h3 className="text-xl font-bold text-slate-900 mb-1">{member.name}</h3>
<div className="flex items-center gap-2 mb-6">
<span className={`text-[10px] font-black uppercase tracking-widest px-2 py-0.5 rounded-md ${
member.role === 'admin' ? 'bg-indigo-50 text-indigo-600' : 'bg-slate-50 text-slate-600'
}`}>
{member.role === 'admin' ? 'مدير نظام' : 'محاسب'}
</span>
</div>
<div className="space-y-3">
<div className="flex items-center gap-3 text-sm text-slate-500">
<Mail className="w-4 h-4" />
{member.email}
</div>
</div>
</motion.div>
))}
</div>
)}
{/* ── Add Staff Modal ──────────────────────────────────── */}
<AnimatePresence>
{isAddModalOpen && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-slate-900/60 backdrop-blur-sm">
<motion.div
initial={{ scale: 0.9, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
exit={{ scale: 0.9, opacity: 0 }}
className="bg-white rounded-[32px] p-8 w-full max-w-md shadow-2xl relative"
>
<h3 className="text-2xl font-bold text-slate-900 mb-2">إضافة موظف جديد</h3>
<p className="text-slate-500 mb-8">سيتمكن الموظف من الدخول لمكتبك ومساعدة الشركات.</p>
<form onSubmit={handleAddStaff} className="space-y-5">
<div>
<label className="block text-sm font-bold text-slate-700 mb-2">الاسم الكامل</label>
<input
type="text"
required
value={fullName}
onChange={e => setFullName(e.target.value)}
className="w-full bg-slate-50 border border-slate-200 rounded-2xl px-5 py-4 outline-none focus:border-primary-500 transition-all"
placeholder="مثال: أحمد محمد"
/>
</div>
<div>
<label className="block text-sm font-bold text-slate-700 mb-2">البريد الإلكتروني</label>
<input
type="email"
required
value={email}
onChange={e => setEmail(e.target.value)}
className="w-full bg-slate-50 border border-slate-200 rounded-2xl px-5 py-4 outline-none focus:border-primary-500 transition-all"
placeholder="email@example.com"
/>
</div>
<div>
<label className="block text-sm font-bold text-slate-700 mb-2">كلمة المرور المؤقتة</label>
<input
type="password"
required
value={password}
onChange={e => setPassword(e.target.value)}
className="w-full bg-slate-50 border border-slate-200 rounded-2xl px-5 py-4 outline-none focus:border-primary-500 transition-all"
placeholder="••••••••"
/>
</div>
<div>
<label className="block text-sm font-bold text-slate-700 mb-2">الصلاحية</label>
<select
value={role}
onChange={e => setRole(e.target.value)}
className="w-full bg-slate-50 border border-slate-200 rounded-2xl px-5 py-4 outline-none focus:border-primary-500 transition-all appearance-none"
>
<option value="accountant">محاسب</option>
<option value="admin">مدير نظام</option>
</select>
</div>
<div className="flex gap-4 pt-4">
<button
type="button"
onClick={() => setIsAddModalOpen(false)}
className="flex-1 bg-slate-100 text-slate-700 font-bold py-4 rounded-2xl hover:bg-slate-200 transition-all"
>
إلغاء
</button>
<button
type="submit"
className="flex-[2] btn-primary py-4 rounded-2xl shadow-lg shadow-primary-500/30"
>
حفظ وإضافة
</button>
</div>
</form>
</motion.div>
</div>
)}
</AnimatePresence>
</div>
);
};