🚀 Phase 3 Complete: Fix staff list, PDF preview, and functional Settings/Profile

This commit is contained in:
Hamza-Ayed
2026-04-22 02:31:01 +03:00
parent 09cb8efa80
commit c3f3d940e5
5 changed files with 228 additions and 83 deletions

View File

@@ -46,4 +46,9 @@ export class UsersController {
async remove(@CurrentUser() user: any, @Param('id') id: string) {
return this.usersService.remove(user.tenantId, id, user.id);
}
@Post('profile')
async updateProfile(@CurrentUser() user: any, @Body() dto: any) {
return this.usersService.update(user.id, dto);
}
}

View File

@@ -71,4 +71,21 @@ export class UsersService {
const user = await this.findOne(tenantId, id);
await this.userRepository.update(id, { is_active: false });
}
/**
* تحديث بيانات مستخدم
*/
async update(id: string, dto: any): Promise<User> {
const user = await this.userRepository.findOne({ where: { id } });
if (!user) throw new NotFoundException('User not found');
// Hash password if provided
if (dto.password) {
dto.password_hash = await bcrypt.hash(dto.password, 12);
delete dto.password;
}
Object.assign(user, dto);
return this.userRepository.save(user);
}
}

View File

@@ -373,25 +373,17 @@ export const InvoicesPage = () => {
</button>
</header>
<div className="flex-1 overflow-auto bg-slate-950 p-8 flex justify-center items-start">
<div className="bg-white rounded-lg overflow-hidden shadow-2xl">
<img
src={`${apiClient.defaults.baseURL}/invoices/${viewingInvoice.id}/file?token=${localStorage.getItem('access_token')}`}
alt="Invoice"
className="max-w-full h-auto"
onError={(e) => {
e.currentTarget.style.display = 'none';
const token = localStorage.getItem('access_token');
e.currentTarget.parentElement!.innerHTML = `
<div class="p-20 text-center bg-slate-900 text-slate-300 w-[600px]">
<FileText class="w-16 h-16 text-slate-700 mx-auto mb-4" />
<h4 class="text-xl font-bold text-white mb-2">تعذر عرض الملف</h4>
<p class="text-slate-500 mb-8">قد يكون الملف PDF أو حدث خطأ في التحميل.</p>
<a href="${apiClient.defaults.baseURL}/invoices/${viewingInvoice.id}/file?token=${token}" target="_blank" class="px-8 py-3 bg-emerald-500 text-slate-950 font-bold rounded-xl inline-block">تحميل الملف لفتحه</a>
</div>
`;
}}
<div className="flex-1 overflow-auto bg-slate-950 p-8 flex justify-center items-center">
<div className="w-full h-full max-w-4xl bg-white rounded-xl overflow-hidden shadow-2xl relative">
<iframe
src={`${apiClient.defaults.baseURL}/invoices/${viewingInvoice.id}/file?token=${localStorage.getItem('access_token')}#toolbar=0`}
className="w-full h-full border-none"
title="Invoice Preview"
/>
{/* Fallback overlay in case of loading issues */}
<div className="absolute inset-0 pointer-events-none flex items-center justify-center bg-slate-900/10 backdrop-blur-[2px] opacity-0 hover:opacity-100 transition-opacity">
<p className="bg-slate-900/80 text-white px-4 py-2 rounded-lg text-xs">جاري عرض الفاتورة...</p>
</div>
</div>
</div>

View File

@@ -4,25 +4,75 @@
* ════════════════════════════════════════════════════════════
*/
import { useState } from 'react';
import { useState, useEffect } from 'react';
import {
Settings,
User,
User as UserIcon,
Lock,
Bell,
Shield,
CreditCard,
Save,
Palette,
Moon
Moon,
Camera,
Loader2,
CheckCircle2
} from 'lucide-react';
import { motion } from 'framer-motion';
import { motion, AnimatePresence } from 'framer-motion';
import apiClient from '../../api/client';
export const SettingsPage = () => {
const [activeTab, setActiveTab] = useState('profile');
const [user, setUser] = useState<any>(null);
const [isLoading, setIsLoading] = useState(true);
const [isSaving, setIsSaving] = useState(false);
const [showSuccess, setShowSuccess] = useState(false);
// Form State
const [formData, setFormData] = useState({
name: '',
email: '',
phone: '',
language: 'العربية'
});
useEffect(() => {
const fetchProfile = async () => {
try {
// Get current user from auth state or fetch again
const { data } = await apiClient.get('/auth/me');
setUser(data);
setFormData({
name: data.name || '',
email: data.email || '',
phone: data.phone || '',
language: data.language || 'العربية'
});
} catch (err) {
console.error('Failed to fetch profile', err);
} finally {
setIsLoading(false);
}
};
fetchProfile();
}, []);
const handleSave = async () => {
setIsSaving(true);
try {
await apiClient.post('/users/profile', formData);
setShowSuccess(true);
setTimeout(() => setShowSuccess(false), 3000);
} catch (err) {
alert('حدث خطأ أثناء حفظ التغييرات');
} finally {
setIsSaving(false);
}
};
const tabs = [
{ id: 'profile', label: 'الملف الشخصي', icon: User },
{ id: 'profile', label: 'الملف الشخصي', icon: UserIcon },
{ id: 'security', label: 'الأمان والخصوصية', icon: Lock },
{ id: 'office', label: 'إعدادات المكتب', icon: Settings },
{ id: 'notifications', label: 'التنبيهات', icon: Bell },
@@ -30,16 +80,39 @@ export const SettingsPage = () => {
{ id: 'subscription', label: 'الاشتراك والدفع', icon: CreditCard },
];
if (isLoading) {
return (
<div className="flex-1 flex flex-col items-center justify-center">
<Loader2 className="w-10 h-10 text-emerald-500 animate-spin" />
</div>
);
}
return (
<div className="space-y-8 animate-in fade-in duration-700">
<header>
<h2 className="text-3xl font-black text-white">إعدادات النظام</h2>
<p className="text-slate-400 mt-1">تخصيص حسابك وتفضيلات مكتب المحاسبة الخاص بك.</p>
<div className="space-y-8 animate-in fade-in duration-700 max-w-7xl mx-auto">
<header className="flex items-end justify-between">
<div>
<h2 className="text-4xl font-black text-white tracking-tight">إعدادات النظام</h2>
<p className="text-slate-400 mt-2 text-lg font-medium">إدارة حسابك الشخصي وتخصيص تجربة "مُصادَق" الخاصة بك.</p>
</div>
<AnimatePresence>
{showSuccess && (
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0 }}
className="bg-emerald-500/10 border border-emerald-500/20 text-emerald-400 px-6 py-3 rounded-2xl flex items-center gap-3 font-bold"
>
<CheckCircle2 className="w-5 h-5" />
تم حفظ التغييرات بنجاح
</motion.div>
)}
</AnimatePresence>
</header>
<div className="flex flex-col lg:flex-row gap-8">
<div className="flex flex-col lg:flex-row gap-10">
{/* ── Tabs Sidebar ─────────────────────────── */}
<aside className="w-full lg:w-72 space-y-2">
<aside className="w-full lg:w-80 space-y-3">
{tabs.map((tab) => {
const Icon = tab.icon;
const isActive = activeTab === tab.id;
@@ -47,91 +120,149 @@ export const SettingsPage = () => {
<button
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className={`w-full flex items-center gap-3 px-6 py-4 rounded-xl font-bold transition-all ${
className={`w-full flex items-center gap-4 px-7 py-5 rounded-[20px] font-bold transition-all relative group ${
isActive
? 'bg-emerald-500 text-slate-950 shadow-lg shadow-emerald-500/20'
: 'text-slate-400 hover:bg-slate-800/50 hover:text-white'
? 'bg-emerald-500 text-slate-950 shadow-2xl shadow-emerald-500/30 scale-[1.02]'
: 'text-slate-500 hover:bg-slate-800/40 hover:text-slate-200'
}`}
>
<Icon className={`w-5 h-5 ${isActive ? 'text-slate-950' : 'text-slate-500'}`} />
{tab.label}
<Icon className={`w-6 h-6 ${isActive ? 'text-slate-950' : 'text-slate-600 group-hover:text-slate-400'}`} />
<span className="text-lg">{tab.label}</span>
{isActive && (
<motion.div
layoutId="activeTab"
className="absolute right-0 w-1.5 h-8 bg-slate-950 rounded-l-full"
/>
)}
</button>
);
})}
</aside>
{/* ── Content Area ─────────────────────────── */}
<main className="flex-1 bg-slate-900/50 backdrop-blur-xl border border-slate-800/60 rounded-[32px] overflow-hidden flex flex-col min-h-[600px]">
<div className="p-10 flex-1">
<main className="flex-1 bg-slate-900/40 backdrop-blur-3xl border border-slate-800/50 rounded-[40px] overflow-hidden flex flex-col min-h-[650px] shadow-2xl shadow-black/50">
<div className="p-12 flex-1">
{activeTab === 'profile' && (
<motion.div initial={{ opacity: 0, x: 20 }} animate={{ opacity: 1, x: 0 }} className="space-y-8">
<div className="flex items-center gap-6 pb-8 border-b border-slate-800/60">
<div className="w-24 h-24 rounded-3xl bg-gradient-to-br from-emerald-500 to-emerald-600 flex items-center justify-center text-4xl font-black text-slate-950 shadow-xl shadow-emerald-500/20">
H
<motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} className="space-y-12">
{/* Profile Header */}
<div className="flex flex-col md:flex-row items-center gap-10 pb-12 border-b border-slate-800/50">
<div className="relative group">
<div className="w-32 h-32 rounded-[32px] bg-gradient-to-br from-emerald-400 via-emerald-500 to-emerald-600 flex items-center justify-center text-5xl font-black text-slate-950 shadow-2xl shadow-emerald-500/20 group-hover:scale-105 transition-transform duration-500">
{formData.name[0] || 'U'}
</div>
<button className="absolute -bottom-2 -right-2 w-12 h-12 bg-slate-900 border border-slate-700 rounded-2xl flex items-center justify-center text-slate-400 hover:text-emerald-400 hover:border-emerald-500/50 transition-all shadow-xl">
<Camera className="w-6 h-6" />
</button>
</div>
<div>
<h3 className="text-2xl font-bold text-white mb-2">حمزة الغويريين</h3>
<p className="text-slate-500 font-medium">مدير مكتب حساب احترافي</p>
<div className="text-center md:text-right">
<h3 className="text-3xl font-black text-white mb-2">{formData.name || 'مستخدم جديد'}</h3>
<div className="flex flex-wrap items-center gap-3 justify-center md:justify-start">
<span className="bg-emerald-500/10 text-emerald-400 text-xs font-black px-4 py-1.5 rounded-full border border-emerald-500/20 uppercase tracking-widest">
{user?.role === 'admin' ? 'مدير مكتب' : 'محاسب'}
</span>
<span className="text-slate-500 font-bold text-sm"></span>
<span className="text-slate-500 font-bold text-sm">{formData.email}</span>
</div>
</div>
<button className="mr-auto bg-slate-800 hover:bg-slate-700 text-white px-6 py-2 rounded-xl text-sm font-bold transition-all">
تغيير الصورة
</button>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="space-y-2">
<label className="text-sm font-bold text-slate-400 mr-1">الاسم الكامل</label>
<input type="text" defaultValue="حمزة الغويريين" className="w-full bg-slate-800 border border-slate-700 rounded-xl px-5 py-3 text-white outline-none focus:border-emerald-500/50 transition-all" />
{/* Form Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-x-10 gap-y-8">
<div className="space-y-3">
<label className="text-sm font-black text-slate-500 uppercase tracking-widest ml-1">الاسم الكامل</label>
<input
type="text"
value={formData.name}
onChange={e => setFormData({...formData, name: e.target.value})}
className="w-full bg-slate-900/60 border border-slate-800 rounded-2xl px-6 py-4 text-white font-bold outline-none focus:border-emerald-500/50 focus:bg-slate-900 transition-all shadow-inner"
/>
</div>
<div className="space-y-2">
<label className="text-sm font-bold text-slate-400 mr-1">البريد الإلكتروني</label>
<input type="email" defaultValue="hamza@musadaq.jo" className="w-full bg-slate-800 border border-slate-700 rounded-xl px-5 py-3 text-white outline-none focus:border-emerald-500/50 transition-all" />
<div className="space-y-3">
<label className="text-sm font-black text-slate-500 uppercase tracking-widest ml-1">البريد الإلكتروني</label>
<input
type="email"
value={formData.email}
onChange={e => setFormData({...formData, email: e.target.value})}
className="w-full bg-slate-900/60 border border-slate-800 rounded-2xl px-6 py-4 text-slate-400 font-bold outline-none cursor-not-allowed opacity-70"
disabled
/>
</div>
<div className="space-y-2">
<label className="text-sm font-bold text-slate-400 mr-1">رقم الهاتف</label>
<input type="text" defaultValue="+962 79 000 0000" className="w-full bg-slate-800 border border-slate-700 rounded-xl px-5 py-3 text-white outline-none focus:border-emerald-500/50 transition-all" />
<div className="space-y-3">
<label className="text-sm font-black text-slate-500 uppercase tracking-widest ml-1">رقم الهاتف</label>
<input
type="text"
value={formData.phone}
onChange={e => setFormData({...formData, phone: e.target.value})}
className="w-full bg-slate-900/60 border border-slate-800 rounded-2xl px-6 py-4 text-white font-bold outline-none focus:border-emerald-500/50 focus:bg-slate-900 transition-all shadow-inner"
/>
</div>
<div className="space-y-2">
<label className="text-sm font-bold text-slate-400 mr-1">اللغة المفضلة</label>
<select className="w-full bg-slate-800 border border-slate-700 rounded-xl px-5 py-3 text-white outline-none focus:border-emerald-500/50 transition-all appearance-none cursor-pointer">
<option>العربية</option>
<option>English</option>
</select>
<div className="space-y-3">
<label className="text-sm font-black text-slate-500 uppercase tracking-widest ml-1">اللغة المفضلة</label>
<div className="relative">
<select
value={formData.language}
onChange={e => setFormData({...formData, language: e.target.value})}
className="w-full bg-slate-900/60 border border-slate-800 rounded-2xl px-6 py-4 text-white font-bold outline-none focus:border-emerald-500/50 focus:bg-slate-900 transition-all appearance-none cursor-pointer"
>
<option>العربية</option>
<option>English</option>
</select>
<div className="absolute left-6 top-1/2 -translate-y-1/2 pointer-events-none text-slate-600">
<Palette className="w-5 h-5" />
</div>
</div>
</div>
</div>
</motion.div>
)}
{activeTab === 'appearance' && (
<motion.div initial={{ opacity: 0, x: 20 }} animate={{ opacity: 1, x: 0 }} className="space-y-8">
<h3 className="text-2xl font-bold text-white mb-6">المظهر والنظام</h3>
<div className="grid grid-cols-2 gap-6">
<div className="p-6 rounded-2xl bg-emerald-500/10 border border-emerald-500/20 flex flex-col items-center gap-4 cursor-pointer">
<Moon className="w-12 h-12 text-emerald-500" />
<span className="font-bold text-white">الوضع الداكن (مفعل)</span>
<motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} className="space-y-10">
<h3 className="text-3xl font-black text-white">المظهر والنظام</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
<div className="p-8 rounded-3xl bg-emerald-500/10 border-2 border-emerald-500/40 flex flex-col items-center gap-6 cursor-pointer shadow-2xl shadow-emerald-500/10 transition-all hover:scale-[1.02]">
<div className="w-20 h-20 bg-slate-900 rounded-3xl flex items-center justify-center border border-emerald-500/30">
<Moon className="w-10 h-10 text-emerald-500" />
</div>
<div className="text-center">
<span className="font-black text-white text-xl block">الوضع الداكن (Premium)</span>
<span className="text-emerald-500/60 text-sm font-bold">الوضع الافتراضي مفعل الآن</span>
</div>
</div>
<div className="p-6 rounded-2xl bg-slate-800/30 border border-slate-700 flex flex-col items-center gap-4 opacity-50 cursor-not-allowed">
<div className="w-12 h-12 bg-white rounded-full" />
<span className="font-bold text-slate-500">الوضع الفاتح</span>
<div className="p-8 rounded-3xl bg-slate-800/20 border-2 border-slate-800 flex flex-col items-center gap-6 opacity-40 cursor-not-allowed grayscale">
<div className="w-20 h-20 bg-white rounded-3xl flex items-center justify-center border border-slate-700" />
<div className="text-center">
<span className="font-black text-slate-500 text-xl block">الوضع الفاتح</span>
<span className="text-slate-600 text-sm font-bold">غير متوفر في نسخة النخبة</span>
</div>
</div>
</div>
</motion.div>
)}
{(activeTab !== 'profile' && activeTab !== 'appearance') && (
<div className="flex flex-col items-center justify-center h-full text-center">
<Shield className="w-16 h-16 text-slate-800 mb-6" />
<h3 className="text-xl font-bold text-slate-500">هذه الصفحة قيد التطوير</h3>
<p className="text-slate-600 max-w-xs mt-2 text-sm">نحن نعمل على توفير المزيد من خيارات التخصيص والأمان قريباً.</p>
<div className="flex flex-col items-center justify-center h-full text-center py-20">
<div className="w-24 h-24 bg-slate-800/50 rounded-full flex items-center justify-center mb-8 border border-slate-700/50">
<Shield className="w-12 h-12 text-slate-700" />
</div>
<h3 className="text-2xl font-black text-slate-400">هذه الخيارات قيد التطوير</h3>
<p className="text-slate-600 max-w-sm mt-4 text-lg font-medium">نحن نعمل على بناء أقوى أدوات التحكم والأمان لتناسب احتياجات المحاسب المتميز.</p>
</div>
)}
</div>
<footer className="px-10 py-6 border-t border-slate-800/60 bg-slate-900/30 flex items-center justify-between">
<p className="text-xs text-slate-500">آخر تحديث: 22 أبريل 2026</p>
<button className="bg-emerald-500 hover:bg-emerald-600 text-slate-950 font-black px-10 py-3 rounded-xl flex items-center gap-2 shadow-lg shadow-emerald-500/20 transition-all active:scale-95">
<Save className="w-5 h-5" />
حفظ التغييرات
<footer className="px-12 py-8 border-t border-slate-800/50 bg-slate-900/60 flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="w-2 h-2 rounded-full bg-emerald-500 animate-pulse" />
<p className="text-sm text-slate-500 font-bold uppercase tracking-widest">تعديل البيانات متاح حالياً</p>
</div>
<button
onClick={handleSave}
disabled={isSaving}
className="bg-emerald-500 hover:bg-emerald-600 text-slate-950 font-black px-12 py-4 rounded-2xl flex items-center gap-3 shadow-2xl shadow-emerald-500/30 transition-all active:scale-95 disabled:opacity-50"
>
{isSaving ? <Loader2 className="w-6 h-6 animate-spin" /> : <Save className="w-6 h-6" />}
<span className="text-lg">حفظ التغييرات</span>
</button>
</footer>
</main>

View File

@@ -24,7 +24,7 @@ export const StaffPage = () => {
const fetchStaff = async () => {
try {
const { data } = await apiClient.get('/staff');
const { data } = await apiClient.get('/users');
setStaff(data);
} catch (error) {
console.error('Failed to fetch staff', error);
@@ -41,7 +41,7 @@ export const StaffPage = () => {
e.preventDefault();
setIsSubmitting(true);
try {
await apiClient.post('/staff', { name, email, password, role });
await apiClient.post('/users', { name, email, password, role });
setIsAddModalOpen(false);
setName('');
setEmail('');
@@ -58,7 +58,7 @@ export const StaffPage = () => {
const handleDelete = async (id: string) => {
if (!confirm('هل أنت متأكد من حذف هذا الموظف؟')) return;
try {
await apiClient.post(`/staff/${id}/delete`);
await apiClient.delete(`/users/${id}`);
fetchStaff();
} catch (error) {
alert('فشل حذف الموظف');