Files
musadeq/frontend/src/pages/staff/StaffPage.tsx

269 lines
16 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 (Premium Dark)
* ════════════════════════════════════════════════════════════
*/
import { useState, useEffect } from 'react';
import { Users, UserPlus, Search, Shield, Mail, Phone, MoreVertical, Trash2, Loader2, X } from 'lucide-react';
import { motion, AnimatePresence } from 'framer-motion';
import apiClient from '../../api/client';
export const StaffPage = () => {
const [staff, setStaff] = useState<any[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [searchTerm, setSearchTerm] = useState('');
const [isAddModalOpen, setIsAddModalOpen] = useState(false);
// Form State
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [role, setRole] = useState('manager');
const [isSubmitting, setIsSubmitting] = useState(false);
const fetchStaff = async () => {
try {
const { data } = await apiClient.get('/staff');
setStaff(data);
} catch (error) {
console.error('Failed to fetch staff', error);
} finally {
setIsLoading(false);
}
};
useEffect(() => {
fetchStaff();
}, []);
const handleCreateStaff = async (e: React.FormEvent) => {
e.preventDefault();
setIsSubmitting(true);
try {
await apiClient.post('/staff', { name, email, password, role });
setIsAddModalOpen(false);
setName('');
setEmail('');
setPassword('');
fetchStaff();
} catch (error) {
console.error('Failed to create staff', error);
alert('حدث خطأ أثناء إضافة الموظف');
} finally {
setIsSubmitting(false);
}
};
const handleDelete = async (id: string) => {
if (!confirm('هل أنت متأكد من حذف هذا الموظف؟')) return;
try {
await apiClient.post(`/staff/${id}/delete`);
fetchStaff();
} catch (error) {
alert('فشل حذف الموظف');
}
};
const filteredStaff = staff.filter(s =>
s.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
s.email.toLowerCase().includes(searchTerm.toLowerCase())
);
return (
<div className="space-y-8 animate-in fade-in duration-700">
<header className="flex items-center justify-between">
<div>
<h2 className="text-3xl font-black text-white">إدارة الموظفين</h2>
<p className="text-slate-400 mt-1">إدارة فريق العمل المالي لمكتب المحاسبة الخاص بك.</p>
</div>
<button
onClick={() => setIsAddModalOpen(true)}
className="bg-emerald-500 hover:bg-emerald-600 text-slate-950 font-bold py-3 px-8 rounded-xl flex items-center gap-2 shadow-lg shadow-emerald-500/20 transition-all active:scale-95"
>
<UserPlus className="w-5 h-5" />
إضافة موظف جديد
</button>
</header>
{/* ── Search Bar ──────────────────────────────── */}
<div className="bg-slate-900/50 backdrop-blur-xl border border-slate-800/60 rounded-xl px-4 py-3 flex items-center gap-3">
<Search className="w-5 h-5 text-slate-500" />
<input
type="text"
placeholder="ابحث بالاسم أو البريد الإلكتروني..."
className="bg-transparent border-none outline-none flex-1 text-slate-200 text-sm placeholder-slate-500"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
</div>
{/* ── Staff List ───────────────────────────────────── */}
<div className="bg-slate-900/50 backdrop-blur-xl border border-slate-800/60 rounded-2xl overflow-hidden min-h-[400px] flex flex-col">
{isLoading ? (
<div className="flex-1 flex flex-col justify-center items-center">
<Loader2 className="w-8 h-8 text-emerald-500 animate-spin mb-4" />
<p className="text-slate-500">جاري جلب بيانات الفريق...</p>
</div>
) : filteredStaff.length === 0 ? (
<div className="flex-1 flex flex-col items-center justify-center p-20 text-center">
<div className="w-20 h-20 bg-slate-800 rounded-2xl flex items-center justify-center mb-6 border border-slate-700">
<Users className="w-10 h-10 text-slate-600" />
</div>
<h3 className="text-xl font-bold text-white mb-2">لا يوجد موظفون مضافون</h3>
<p className="text-slate-500 max-w-sm mb-8">يمكنك إضافة موظفين لمساعدتك في إدارة ومعالجة فواتير الشركات.</p>
<button onClick={() => setIsAddModalOpen(true)} className="bg-emerald-500 hover:bg-emerald-600 text-slate-950 font-bold py-3 px-8 rounded-xl transition-all">
إضافة أول موظف
</button>
</div>
) : (
<div className="overflow-x-auto">
<table className="w-full text-right">
<thead className="bg-slate-800/50 border-b border-slate-800/60">
<tr>
<th className="px-6 py-4 text-xs font-bold text-slate-400">الاسم الكامل</th>
<th className="px-6 py-4 text-xs font-bold text-slate-400">البريد الإلكتروني</th>
<th className="px-6 py-4 text-xs font-bold text-slate-400">الدور الوظيفي</th>
<th className="px-6 py-4 text-xs font-bold text-slate-400 text-center">إجراءات</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-800/50">
{filteredStaff.map((s, idx) => (
<motion.tr
key={s.id}
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: idx * 0.05 }}
className="hover:bg-slate-800/30 transition-colors"
>
<td className="px-6 py-4 font-bold text-white">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-full bg-gradient-to-br from-slate-700 to-slate-800 flex items-center justify-center text-slate-300 font-black border border-slate-700">
{s.name[0]}
</div>
{s.name}
</div>
</td>
<td className="px-6 py-4 text-slate-400">
<div className="flex items-center gap-2">
<Mail className="w-4 h-4 text-slate-600" />
{s.email}
</div>
</td>
<td className="px-6 py-4">
<span className={`inline-flex items-center gap-1.5 px-3 py-1 rounded-md text-[10px] font-bold border ${
s.role === 'admin'
? 'text-emerald-400 bg-emerald-400/5 border-emerald-400/20'
: 'text-blue-400 bg-blue-400/5 border-blue-400/20'
} uppercase tracking-widest`}>
<Shield className="w-3 h-3" />
{s.role === 'admin' ? 'مدير نظام' : 'محاسب'}
</span>
</td>
<td className="px-6 py-4 text-center">
<div className="flex items-center justify-center gap-2">
<button className="p-2 text-slate-500 hover:text-white transition-colors">
<MoreVertical className="w-4 h-4" />
</button>
<button
onClick={() => handleDelete(s.id)}
className="p-2 text-slate-500 hover:text-red-400 hover:bg-red-500/10 rounded-lg transition-all"
>
<Trash2 className="w-4 h-4" />
</button>
</div>
</td>
</motion.tr>
))}
</tbody>
</table>
</div>
)}
</div>
{/* ── Add Staff Modal ─────────────────────────────────── */}
<AnimatePresence>
{isAddModalOpen && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-slate-950/80 backdrop-blur-md">
<motion.div
initial={{ scale: 0.9, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
exit={{ scale: 0.9, opacity: 0 }}
className="bg-slate-900 border border-slate-800 p-8 w-full max-w-md rounded-3xl shadow-2xl relative"
>
<button onClick={() => setIsAddModalOpen(false)} className="absolute top-6 left-6 text-slate-500 hover:text-white transition-colors">
<X className="w-6 h-6" />
</button>
<h3 className="text-2xl font-bold text-white mb-6">إضافة موظف جديد</h3>
<form onSubmit={handleCreateStaff} className="space-y-5">
<div>
<label className="block text-sm font-bold text-slate-400 mb-2">الاسم الكامل</label>
<input
type="text"
required
value={name}
onChange={e => setName(e.target.value)}
className="w-full bg-slate-800 border border-slate-700 rounded-xl px-4 py-3 outline-none focus:border-emerald-500/50 transition-all text-white placeholder-slate-600"
placeholder="مثال: أحمد محمد"
/>
</div>
<div>
<label className="block text-sm font-bold text-slate-400 mb-2">البريد الإلكتروني</label>
<input
type="email"
required
value={email}
onChange={e => setEmail(e.target.value)}
className="w-full bg-slate-800 border border-slate-700 rounded-xl px-4 py-3 outline-none focus:border-emerald-500/50 transition-all text-white placeholder-slate-600"
placeholder="ahmed@office.com"
/>
</div>
<div>
<label className="block text-sm font-bold text-slate-400 mb-2">كلمة المرور المؤقتة</label>
<input
type="password"
required
value={password}
onChange={e => setPassword(e.target.value)}
className="w-full bg-slate-800 border border-slate-700 rounded-xl px-4 py-3 outline-none focus:border-emerald-500/50 transition-all text-white placeholder-slate-600"
placeholder="••••••••"
/>
</div>
<div>
<label className="block text-sm font-bold text-slate-400 mb-2">الدور الوظيفي</label>
<select
value={role}
onChange={e => setRole(e.target.value)}
className="w-full bg-slate-800 border border-slate-700 rounded-xl px-4 py-3 outline-none focus:border-emerald-500/50 transition-all text-white appearance-none cursor-pointer"
>
<option value="manager">محاسب (عرض ومعالجة)</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-800 text-slate-400 font-bold py-3 rounded-xl hover:bg-slate-700 transition-all"
>
إلغاء
</button>
<button
type="submit"
disabled={isSubmitting}
className="flex-1 bg-emerald-500 text-slate-950 font-bold py-3 rounded-xl shadow-lg shadow-emerald-500/20 flex items-center justify-center gap-2 transition-all"
>
{isSubmitting && <Loader2 className="w-4 h-4 animate-spin" />}
حفظ البيانات
</button>
</div>
</form>
</motion.div>
</div>
)}
</AnimatePresence>
</div>
);
};