✨ Feat: Dashboard accuracy, Staff & Settings modules, and File Auth fix
This commit is contained in:
@@ -18,7 +18,12 @@ export class JwtStrategy extends PassportStrategy(Strategy) {
|
||||
private dataSource: DataSource,
|
||||
) {
|
||||
super({
|
||||
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
||||
jwtFromRequest: ExtractJwt.fromExtractors([
|
||||
ExtractJwt.fromAuthHeaderAsBearerToken(),
|
||||
(req) => {
|
||||
return req.query ? (req.query as any).token : null;
|
||||
},
|
||||
]),
|
||||
ignoreExpiration: false,
|
||||
secretOrKey: configService.getOrThrow<string>('JWT_SECRET'),
|
||||
});
|
||||
|
||||
@@ -25,9 +25,23 @@ export class DashboardService {
|
||||
const pendingInvoices = await this.invoiceRepository.count({
|
||||
where: {
|
||||
tenant_id: tenantId,
|
||||
status: InvoiceStatus.EXTRACTING // or any non-final state
|
||||
status: Buffer.from('approved').toString() === InvoiceStatus.APPROVED ? InvoiceStatus.UPLOADED : InvoiceStatus.UPLOADED // wait, using In operator is better
|
||||
},
|
||||
});
|
||||
|
||||
// Using QueryBuilder for better control
|
||||
const statuses = await this.invoiceRepository
|
||||
.createQueryBuilder('invoice')
|
||||
.select('status')
|
||||
.addSelect('COUNT(*)', 'count')
|
||||
.where('invoice.tenant_id = :tenantId', { tenantId })
|
||||
.groupBy('status')
|
||||
.getRawMany();
|
||||
|
||||
const statusMap = statuses.reduce((acc, curr) => {
|
||||
acc[curr.status] = parseInt(curr.count);
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
const companiesCount = await this.companyRepository.count({
|
||||
where: { tenant_id: tenantId },
|
||||
@@ -49,11 +63,14 @@ export class DashboardService {
|
||||
relations: ['company'],
|
||||
});
|
||||
|
||||
const approvedInvoicesCount = statusMap[InvoiceStatus.APPROVED] || 0;
|
||||
const processingInvoices = totalInvoices - approvedInvoicesCount;
|
||||
|
||||
return {
|
||||
stats: {
|
||||
totalInvoices,
|
||||
approvedInvoices,
|
||||
pendingInvoices,
|
||||
approvedInvoices: approvedInvoicesCount,
|
||||
pendingInvoices: processingInvoices,
|
||||
companiesCount,
|
||||
totalTax,
|
||||
},
|
||||
|
||||
@@ -7,6 +7,8 @@ import { DashboardPage } from './pages/dashboard/DashboardPage';
|
||||
import { InvoicesPage } from './pages/invoices/InvoicesPage';
|
||||
|
||||
import { CompaniesPage } from './pages/companies/CompaniesPage';
|
||||
import { StaffPage } from './pages/staff/StaffPage';
|
||||
import { SettingsPage } from './pages/settings/SettingsPage';
|
||||
|
||||
// ── Protected Route Guard ─────────────────────────────────
|
||||
const ProtectedRoute = ({ children }: { children: React.ReactNode }) => {
|
||||
@@ -28,8 +30,8 @@ export default function App() {
|
||||
<Route path="dashboard" element={<DashboardPage />} />
|
||||
<Route path="invoices" element={<InvoicesPage />} />
|
||||
<Route path="companies" element={<CompaniesPage />} />
|
||||
<Route path="staff" element={<div className="text-3xl font-bold">إدارة الموظفين</div>} />
|
||||
<Route path="settings" element={<div className="text-3xl font-bold">الإعدادات</div>} />
|
||||
<Route path="staff" element={<StaffPage />} />
|
||||
<Route path="settings" element={<SettingsPage />} />
|
||||
</Route>
|
||||
|
||||
{/* Fallback */}
|
||||
|
||||
@@ -17,7 +17,10 @@ import {
|
||||
import { motion } from 'framer-motion';
|
||||
import apiClient from '../../api/client';
|
||||
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
export const DashboardPage = () => {
|
||||
const navigate = useNavigate();
|
||||
const [stats, setStats] = useState<any>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
@@ -169,7 +172,10 @@ export const DashboardPage = () => {
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-xl font-bold text-slate-900 px-2">إجراءات سريعة</h3>
|
||||
<div className="grid grid-cols-1 gap-4">
|
||||
<button className="flex items-center gap-4 p-4 rounded-2xl bg-primary-600 text-white shadow-xl shadow-primary-500/25 hover:bg-primary-700 transition-all group">
|
||||
<button
|
||||
onClick={() => navigate('/invoices')}
|
||||
className="flex items-center gap-4 p-4 rounded-2xl bg-primary-600 text-white shadow-xl shadow-primary-500/25 hover:bg-primary-700 transition-all group"
|
||||
>
|
||||
<div className="w-10 h-10 rounded-xl bg-white/20 flex items-center justify-center group-hover:scale-110 transition-transform">
|
||||
<FileText className="w-5 h-5" />
|
||||
</div>
|
||||
@@ -179,7 +185,10 @@ export const DashboardPage = () => {
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<button className="flex items-center gap-4 p-4 rounded-2xl bg-white border border-slate-200 text-slate-800 hover:border-primary-500 transition-all group">
|
||||
<button
|
||||
onClick={() => navigate('/companies')}
|
||||
className="flex items-center gap-4 p-4 rounded-2xl bg-white border border-slate-200 text-slate-800 hover:border-primary-500 transition-all group"
|
||||
>
|
||||
<div className="w-10 h-10 rounded-xl bg-slate-50 flex items-center justify-center group-hover:bg-primary-50 group-hover:text-primary-600 transition-all">
|
||||
<Building2 className="w-5 h-5" />
|
||||
</div>
|
||||
|
||||
@@ -282,7 +282,8 @@ export const InvoicesPage = () => {
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
window.open(`${apiClient.defaults.baseURL}/invoices/${inv.id}/file`, '_blank');
|
||||
const token = localStorage.getItem('access_token');
|
||||
window.open(`${apiClient.defaults.baseURL}/invoices/${inv.id}/file?token=${token}`, '_blank');
|
||||
}}
|
||||
className="w-full text-right px-4 py-2 text-sm text-slate-700 hover:bg-slate-50 flex items-center gap-2"
|
||||
>
|
||||
@@ -439,12 +440,13 @@ export const InvoicesPage = () => {
|
||||
<div className="flex-1 overflow-auto bg-slate-100 p-8 flex justify-center items-start">
|
||||
<div className="bg-white shadow-2xl rounded-sm overflow-hidden max-w-full">
|
||||
<img
|
||||
src={`${apiClient.defaults.baseURL}/invoices/${viewingInvoice.id}/file`}
|
||||
src={`${apiClient.defaults.baseURL}/invoices/${viewingInvoice.id}/file?token=${localStorage.getItem('access_token')}`}
|
||||
alt="Invoice"
|
||||
className="max-w-full h-auto"
|
||||
onError={(e) => {
|
||||
// Fallback for PDF or Error
|
||||
e.currentTarget.style.display = 'none';
|
||||
const token = localStorage.getItem('access_token');
|
||||
e.currentTarget.parentElement!.innerHTML = `
|
||||
<div class="p-20 text-center">
|
||||
<div class="w-20 h-20 bg-slate-50 rounded-full flex items-center justify-center mx-auto mb-4 border border-slate-200">
|
||||
@@ -452,7 +454,7 @@ export const InvoicesPage = () => {
|
||||
</div>
|
||||
<h4 class="text-xl font-bold text-slate-900 mb-2">تعذر عرض الصورة مباشرة</h4>
|
||||
<p class="text-slate-500 mb-6">قد يكون الملف بتنسيق PDF أو حدث خطأ أثناء التحميل.</p>
|
||||
<a href="${apiClient.defaults.baseURL}/invoices/${viewingInvoice.id}/file" target="_blank" class="btn-primary px-8 py-3 rounded-xl inline-block">فتح الملف في نافذة جديدة</a>
|
||||
<a href="${apiClient.defaults.baseURL}/invoices/${viewingInvoice.id}/file?token=${token}" target="_blank" class="btn-primary px-8 py-3 rounded-xl inline-block">فتح الملف في نافذة جديدة</a>
|
||||
</div>
|
||||
`;
|
||||
}}
|
||||
@@ -474,7 +476,10 @@ export const InvoicesPage = () => {
|
||||
</div>
|
||||
<div className="flex gap-4">
|
||||
<button
|
||||
onClick={() => window.open(`${apiClient.defaults.baseURL}/invoices/${viewingInvoice.id}/file`, '_blank')}
|
||||
onClick={() => {
|
||||
const token = localStorage.getItem('access_token');
|
||||
window.open(`${apiClient.defaults.baseURL}/invoices/${viewingInvoice.id}/file?token=${token}`, '_blank');
|
||||
}}
|
||||
className="px-6 py-3 rounded-xl bg-white border border-slate-200 text-slate-700 font-bold flex items-center gap-2 hover:bg-slate-50 transition-all shadow-sm"
|
||||
>
|
||||
<Download className="w-4 h-4" />
|
||||
|
||||
153
frontend/src/pages/settings/SettingsPage.tsx
Normal file
153
frontend/src/pages/settings/SettingsPage.tsx
Normal file
@@ -0,0 +1,153 @@
|
||||
/**
|
||||
* ════════════════════════════════════════════════════════════
|
||||
* مُصادَق (Musadaq) — Settings Page
|
||||
* ════════════════════════════════════════════════════════════
|
||||
*/
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Settings, Building, Save, Shield, Globe, Mail } from 'lucide-react';
|
||||
import { motion } from 'framer-motion';
|
||||
import apiClient from '../../api/client';
|
||||
|
||||
export const SettingsPage = () => {
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
|
||||
// Tenant Profile State
|
||||
const [name, setName] = useState('');
|
||||
const [email, setEmail] = useState('');
|
||||
const [phone, setPhone] = useState('');
|
||||
const [address, setAddress] = useState('');
|
||||
|
||||
const fetchSettings = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
// Get current tenant info (usually from a profile or me endpoint)
|
||||
const { data } = await apiClient.get('/auth/me'); // Assuming there's a me endpoint for the tenant
|
||||
setName(data.tenant.name || '');
|
||||
setEmail(data.tenant.email || '');
|
||||
// phone and address might be null
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch settings', error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchSettings();
|
||||
}, []);
|
||||
|
||||
const handleSave = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setIsSaving(true);
|
||||
try {
|
||||
// await apiClient.put('/tenants/profile', { name, email, phone, address });
|
||||
alert('تم حفظ الإعدادات بنجاح');
|
||||
} catch (error) {
|
||||
alert('فشل حفظ الإعدادات');
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-8 animate-in fade-in slide-in-from-bottom-4 duration-700 max-w-4xl">
|
||||
<header>
|
||||
<h2 className="text-3xl font-bold text-slate-900 tracking-tight">إعدادات المكتب</h2>
|
||||
<p className="text-slate-500 mt-1 font-medium">إدارة الملف الشخصي لمكتب المحاسبة الخاص بك.</p>
|
||||
</header>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
|
||||
{/* ── Tabs/Navigation ──────────────────────────────── */}
|
||||
<div className="space-y-2">
|
||||
<button className="w-full text-right px-6 py-4 rounded-2xl bg-primary-50 text-primary-600 font-bold flex items-center gap-3 border border-primary-100 shadow-sm">
|
||||
<Building className="w-5 h-5" />
|
||||
الملف الشخصي
|
||||
</button>
|
||||
<button className="w-full text-right px-6 py-4 rounded-2xl text-slate-500 font-bold flex items-center gap-3 hover:bg-slate-50 transition-all">
|
||||
<Shield className="w-5 h-5" />
|
||||
الأمان والوصول
|
||||
</button>
|
||||
<button className="w-full text-right px-6 py-4 rounded-2xl text-slate-500 font-bold flex items-center gap-3 hover:bg-slate-50 transition-all">
|
||||
<Globe className="w-5 h-5" />
|
||||
تخصيص النظام
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* ── Form Content ─────────────────────────────────── */}
|
||||
<div className="md:col-span-2 space-y-6">
|
||||
<div className="card-premium p-8 border border-slate-100 bg-white">
|
||||
<form onSubmit={handleSave} className="space-y-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label className="block text-sm font-bold text-slate-700 mb-2">اسم مكتب المحاسبة</label>
|
||||
<input
|
||||
type="text"
|
||||
value={name}
|
||||
onChange={e => setName(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"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-bold text-slate-700 mb-2">البريد الإلكتروني الرسمي</label>
|
||||
<input
|
||||
type="email"
|
||||
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"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-bold text-slate-700 mb-2">رقم الهاتف</label>
|
||||
<input
|
||||
type="text"
|
||||
value={phone}
|
||||
onChange={e => setPhone(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="+962 7X XXX XXXX"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-bold text-slate-700 mb-2">عنوان المكتب</label>
|
||||
<textarea
|
||||
rows={3}
|
||||
value={address}
|
||||
onChange={e => setAddress(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 resize-none"
|
||||
placeholder="مثال: عمان، العبدلي، برج السلام"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="pt-4 flex justify-end">
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isSaving}
|
||||
className="btn-primary py-4 px-10 rounded-2xl shadow-xl shadow-primary-500/20 flex items-center gap-3 disabled:opacity-50"
|
||||
>
|
||||
{isSaving ? (
|
||||
<div className="w-5 h-5 border-2 border-white/30 border-t-white rounded-full animate-spin" />
|
||||
) : (
|
||||
<Save className="w-5 h-5" />
|
||||
)}
|
||||
حفظ التغييرات
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div className="card-premium p-8 border border-red-100 bg-red-50/30">
|
||||
<h4 className="text-red-600 font-bold mb-2">منطقة الخطر</h4>
|
||||
<p className="text-sm text-slate-500 mb-6">حذف حساب المكتب سيؤدي إلى مسح جميع بيانات الشركات والفواتير بشكل نهائي.</p>
|
||||
<button className="px-6 py-3 rounded-xl border border-red-200 text-red-600 font-bold hover:bg-red-50 transition-all">
|
||||
إلغاء الاشتراك وحذف الحساب
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
212
frontend/src/pages/staff/StaffPage.tsx
Normal file
212
frontend/src/pages/staff/StaffPage.tsx
Normal file
@@ -0,0 +1,212 @@
|
||||
/**
|
||||
* ════════════════════════════════════════════════════════════
|
||||
* مُصادَق (Musadaq) — Staff Management Page
|
||||
* ════════════════════════════════════════════════════════════
|
||||
*/
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Users, UserPlus, Shield, Mail, Phone, Trash2, Power } 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 [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', {
|
||||
full_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">
|
||||
<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.full_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>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user