Edit Admin

This commit is contained in:
Rahaf
2026-03-27 00:34:59 +03:00
parent f6c6119c18
commit 157188d2e6
8 changed files with 2243 additions and 168 deletions

View File

@ -32,7 +32,7 @@ export default function AdminPage() {
{ id: 'bookings', label: 'طلبات الحجز', icon: Calendar, badge: notifications }, { id: 'bookings', label: 'طلبات الحجز', icon: Calendar, badge: notifications },
{ id: 'users', label: 'المستخدمين', icon: Users }, { id: 'users', label: 'المستخدمين', icon: Users },
{ id: 'ledger', label: 'دفتر الحسابات', icon: DollarSign }, { id: 'ledger', label: 'دفتر الحسابات', icon: DollarSign },
{ id: 'reports', label: 'التقارير', icon: TrendingUp } // { id: 'reports', label: 'التقارير', icon: TrendingUp }
]; ];
return ( return (

View File

@ -38,10 +38,20 @@ export default function AddPropertyForm({ onClose, onSuccess }) {
const [selectedFeatures, setSelectedFeatures] = useState([]); const [selectedFeatures, setSelectedFeatures] = useState([]);
const featuresList = [ const featuresList = [
'swimmingPool', 'privateGarden', 'parking', 'superLuxFinish', 'مسبح',
'equippedKitchen', 'centralHeating', 'balcony', 'securitySystem', 'حديقة خاصة',
'largeGarden', 'receptionHall', 'maidRoom', 'garage', 'موقف سيارات',
'seaView', 'centralAC', 'fruitGarden', 'storage' 'مطبخ مجهز',
'تدفئة مركزية',
'بلكونة',
'نظام أمني',
'حديقة كبيرة',
'صالة استقبال',
'غرفة خادمة',
'كراج',
'إطلالة بحرية',
'تكييف مركزي',
'مخزن'
]; ];
const handleSubmit = async (e) => { const handleSubmit = async (e) => {

View File

@ -1,6 +1,6 @@
'use client'; 'use client';
import { useState } from 'react'; import { useState, useRef } from 'react';
import { motion, AnimatePresence } from 'framer-motion'; import { motion, AnimatePresence } from 'framer-motion';
import { import {
CheckCircle, CheckCircle,
@ -22,8 +22,12 @@ import {
FileText, FileText,
Download, Download,
Printer, Printer,
History History,
Loader2
} from 'lucide-react'; } from 'lucide-react';
import jsPDF from 'jspdf';
import html2canvas from 'html2canvas';
import toast, { Toaster } from 'react-hot-toast';
const ReasonDialog = ({ isOpen, onClose, onConfirm, title, defaultReason = '' }) => { const ReasonDialog = ({ isOpen, onClose, onConfirm, title, defaultReason = '' }) => {
const [reason, setReason] = useState(defaultReason); const [reason, setReason] = useState(defaultReason);
@ -108,6 +112,427 @@ const ReasonDialog = ({ isOpen, onClose, onConfirm, title, defaultReason = '' })
); );
}; };
const PDFExportButton = ({ request, onExportComplete }) => {
const [isExporting, setIsExporting] = useState(false);
const formatCurrency = (amount) => {
return amount?.toLocaleString() + ' ل.س';
};
const generatePDF = async () => {
if (!request) {
toast.error('لا توجد بيانات للتصدير');
return;
}
setIsExporting(true);
const loadingToast = toast.loading('جاري إنشاء ملف PDF...', { id: 'pdf-export' });
try {
const printContent = document.createElement('div');
printContent.style.width = '800px';
printContent.style.padding = '40px';
printContent.style.backgroundColor = 'white';
printContent.style.fontFamily = 'Cairo, Arial, sans-serif';
printContent.style.direction = 'rtl';
printContent.style.position = 'absolute';
printContent.style.left = '-9999px';
printContent.style.top = '-9999px';
printContent.style.zIndex = '-9999';
printContent.innerHTML = `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: 'Cairo', 'Arial', sans-serif;
}
body {
background: white;
padding: 20px;
}
.pdf-container {
max-width: 800px;
margin: 0 auto;
background: white;
}
.pdf-header {
text-align: center;
margin-bottom: 30px;
padding-bottom: 20px;
border-bottom: 3px solid #f59e0b;
}
.pdf-logo {
width: 60px;
height: 60px;
background: linear-gradient(135deg, #f59e0b, #d97706);
border-radius: 15px;
display: inline-flex;
align-items: center;
justify-content: center;
margin-bottom: 15px;
}
.pdf-logo svg {
width: 32px;
height: 32px;
}
.pdf-title {
font-size: 24px;
font-weight: bold;
color: #1f2937;
margin-bottom: 5px;
}
.pdf-subtitle {
font-size: 12px;
color: #6b7280;
}
.pdf-section {
margin-bottom: 25px;
background: #f9fafb;
border-radius: 16px;
padding: 20px;
break-inside: avoid;
}
.pdf-section-title {
font-size: 18px;
font-weight: bold;
color: #f59e0b;
margin-bottom: 15px;
padding-bottom: 8px;
border-bottom: 2px solid #e5e7eb;
display: flex;
align-items: center;
gap: 8px;
}
.pdf-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 12px;
}
.pdf-info-item {
display: flex;
justify-content: space-between;
padding: 10px 0;
border-bottom: 1px solid #f3f4f6;
}
.pdf-label {
font-weight: 600;
color: #4b5563;
font-size: 14px;
}
.pdf-value {
color: #1f2937;
font-weight: 500;
font-size: 14px;
}
.pdf-status {
display: inline-block;
padding: 4px 12px;
border-radius: 20px;
font-size: 12px;
font-weight: 600;
}
.pdf-status-pending { background: #fef3c7; color: #d97706; }
.pdf-status-owner_approved { background: #dbeafe; color: #2563eb; }
.pdf-status-admin_approved { background: #d1fae5; color: #059669; }
.pdf-status-active { background: #e0e7ff; color: #4f46e5; }
.pdf-status-completed { background: #e5e7eb; color: #4b5563; }
.pdf-status-rejected { background: #fee2e2; color: #dc2626; }
.pdf-amount {
font-size: 18px;
font-weight: bold;
color: #f59e0b;
}
.pdf-footer {
margin-top: 30px;
text-align: center;
padding-top: 20px;
border-top: 1px solid #e5e7eb;
font-size: 11px;
color: #9ca3af;
}
.pdf-badge {
display: inline-block;
padding: 2px 8px;
border-radius: 12px;
font-size: 11px;
font-weight: 500;
background: #f3f4f6;
color: #374151;
}
@media print {
body {
padding: 0;
margin: 0;
}
.pdf-section {
break-inside: avoid;
}
}
</style>
</head>
<body>
<div class="pdf-container">
<div class="pdf-header">
<div class="pdf-logo">
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2">
<path d="M3 9L12 3L21 9L12 15L3 9Z" stroke="white" fill="none"/>
<path d="M12 15V21" stroke="white"/>
<path d="M8 12L4 14" stroke="white"/>
<path d="M16 12L20 14" stroke="white"/>
</svg>
</div>
<div class="pdf-title">تقرير طلب حجز #${request.id}</div>
<div class="pdf-subtitle">تاريخ التقرير: ${new Date().toLocaleDateString('ar-SA')} | ${new Date().toLocaleTimeString('ar-SA')}</div>
</div>
<div class="pdf-section">
<div class="pdf-section-title">
<span></span> معلومات المستأجر
</div>
<div class="pdf-grid">
<div class="pdf-info-item">
<span class="pdf-label">الاسم الكامل:</span>
<span class="pdf-value">${request.user || 'غير محدد'}</span>
</div>
<div class="pdf-info-item">
<span class="pdf-label">نوع الهوية:</span>
<span class="pdf-value">${request.userType === 'syrian' ? 'هوية سورية' : 'جواز سفر'}</span>
</div>
<div class="pdf-info-item">
<span class="pdf-label">رقم الهوية:</span>
<span class="pdf-value">${request.identityNumber || 'غير محدد'}</span>
</div>
<div class="pdf-info-item">
<span class="pdf-label">البريد الإلكتروني:</span>
<span class="pdf-value">${request.userEmail || 'غير محدد'}</span>
</div>
<div class="pdf-info-item">
<span class="pdf-label">رقم الهاتف:</span>
<span class="pdf-value">${request.userPhone || 'غير محدد'}</span>
</div>
</div>
</div>
<div class="pdf-section">
<div class="pdf-section-title">
<span></span> معلومات العقار
</div>
<div class="pdf-grid">
<div class="pdf-info-item">
<span class="pdf-label">العقار:</span>
<span class="pdf-value">${request.property || 'غير محدد'}</span>
</div>
<div class="pdf-info-item">
<span class="pdf-label">السعر اليومي:</span>
<span class="pdf-value">${formatCurrency(request.dailyPrice)}</span>
</div>
</div>
</div>
<div class="pdf-section">
<div class="pdf-section-title">
<span></span> تفاصيل الحجز
</div>
<div class="pdf-grid">
<div class="pdf-info-item">
<span class="pdf-label">تاريخ البداية:</span>
<span class="pdf-value">${request.startDate || 'غير محدد'}</span>
</div>
<div class="pdf-info-item">
<span class="pdf-label">تاريخ النهاية:</span>
<span class="pdf-value">${request.endDate || 'غير محدد'}</span>
</div>
<div class="pdf-info-item">
<span class="pdf-label">عدد الأيام:</span>
<span class="pdf-value">${request.days || 0} يوم</span>
</div>
<div class="pdf-info-item">
<span class="pdf-label">الحالة:</span>
<span class="pdf-value">
<span class="pdf-status pdf-status-${request.status}">
${request.status === 'pending' ? '⏳ قيد الانتظار' :
request.status === 'owner_approved' ? '✓ موافقة المالك' :
request.status === 'admin_approved' ? '✓ موافقة الإدارة' :
request.status === 'active' ? '🏠 إيجار نشط' :
request.status === 'completed' ? '✔️ منتهي' : '❌ مرفوض'}
</span>
</span>
</div>
</div>
</div>
<div class="pdf-section">
<div class="pdf-section-title">
<span></span> المعلومات المالية
</div>
<div class="pdf-grid">
<div class="pdf-info-item">
<span class="pdf-label">سلفة الضمان:</span>
<span class="pdf-value">${formatCurrency(request.securityDeposit)}</span>
</div>
<div class="pdf-info-item">
<span class="pdf-label">المبلغ الإجمالي:</span>
<span class="pdf-value pdf-amount">${formatCurrency(request.totalAmount)}</span>
</div>
<div class="pdf-info-item">
<span class="pdf-label">نسبة العمولة:</span>
<span class="pdf-value">${request.commissionRate || 0}%</span>
</div>
<div class="pdf-info-item">
<span class="pdf-label">نوع العمولة:</span>
<span class="pdf-value">${request.commissionType || 'غير محدد'}</span>
</div>
<div class="pdf-info-item">
<span class="pdf-label">قيمة العمولة:</span>
<span class="pdf-value">${formatCurrency(request.commissionAmount)}</span>
</div>
</div>
</div>
${request.notes ? `
<div class="pdf-section">
<div class="pdf-section-title">
<span></span> ملاحظات
</div>
<div class="pdf-info-item" style="display: block;">
<span class="pdf-value">${request.notes}</span>
</div>
</div>
` : ''}
<div class="pdf-section">
<div class="pdf-section-title">
<span></span> سجل الإجراءات
</div>
<div style="display: flex; flex-direction: column; gap: 8px;">
<div style="display: flex; align-items: center; gap: 10px;">
<span class="pdf-badge"> ${request.requestDate}</span>
<span class="pdf-value">تم إنشاء الطلب</span>
</div>
${request.ownerApproved ? `
<div style="display: flex; align-items: center; gap: 10px;">
<span class="pdf-badge">✓</span>
<span class="pdf-value">تمت موافقة المالك</span>
</div>
` : ''}
${request.adminApproved ? `
<div style="display: flex; align-items: center; gap: 10px;">
<span class="pdf-badge">✓</span>
<span class="pdf-value">تمت موافقة الإدارة</span>
</div>
` : ''}
${request.ownerDelivered ? `
<div style="display: flex; align-items: center; gap: 10px;">
<span class="pdf-badge"></span>
<span class="pdf-value">تم تسليم المفتاح</span>
</div>
` : ''}
${request.tenantReceived ? `
<div style="display: flex; align-items: center; gap: 10px;">
<span class="pdf-badge"></span>
<span class="pdf-value">تم استلام العقار</span>
</div>
` : ''}
</div>
</div>
<div class="pdf-footer">
<div>تقرير صادر عن نظام SweetHome لإدارة العقارات</div>
<div style="margin-top: 5px;">جميع الحقوق محفوظة © ${new Date().getFullYear()} SweetHome</div>
<div style="margin-top: 5px; font-size: 9px;">هذا التقرير تم إنشاؤه تلقائياً ولا يحتاج إلى توقيع</div>
</div>
</div>
</body>
</html>
`;
document.body.appendChild(printContent);
await new Promise(resolve => setTimeout(resolve, 200));
const canvas = await html2canvas(printContent, {
scale: 2.5,
backgroundColor: '#ffffff',
logging: false,
useCORS: true,
windowWidth: printContent.scrollWidth,
windowHeight: printContent.scrollHeight,
onclone: (clonedDoc, element) => {
}
});
const imgData = canvas.toDataURL('image/png');
const pdf = new jsPDF({
orientation: 'portrait',
unit: 'mm',
format: 'a4',
compress: true
});
const imgWidth = 210;
const pageHeight = 297;
const imgHeight = (canvas.height * imgWidth) / canvas.width;
let heightLeft = imgHeight;
let position = 0;
pdf.addImage(imgData, 'PNG', 0, position, imgWidth, imgHeight);
heightLeft -= pageHeight;
let pageCount = 1;
while (heightLeft > 0) {
position = heightLeft - imgHeight;
pdf.addPage();
pdf.addImage(imgData, 'PNG', 0, position, imgWidth, imgHeight);
heightLeft -= pageHeight;
pageCount++;
}
const fileName = `تقرير_طلب_${request.id}_${new Date().toISOString().split('T')[0]}.pdf`;
pdf.save(fileName);
document.body.removeChild(printContent);
toast.success(`تم تصدير التقرير بنجاح! (${pageCount} صفحة)`, { id: 'pdf-export' });
if (onExportComplete) {
onExportComplete();
}
} catch (error) {
console.error('Error generating PDF:', error);
toast.error('حدث خطأ أثناء إنشاء ملف PDF', { id: 'pdf-export' });
} finally {
setIsExporting(false);
}
};
return (
<button
onClick={generatePDF}
disabled={isExporting}
className="flex-1 bg-green-600 text-white py-3 rounded-xl font-medium hover:bg-green-700 transition-all duration-200 flex items-center justify-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed shadow-sm hover:shadow-md"
>
{isExporting ? (
<>
<Loader2 className="w-5 h-5 animate-spin" />
جاري التصدير...
</>
) : (
<>
<Download className="w-5 h-5" />
تصدير PDF
</>
)}
</button>
);
};
const RequestDetailsDialog = ({ request, isOpen, onClose }) => { const RequestDetailsDialog = ({ request, isOpen, onClose }) => {
if (!isOpen || !request) return null; if (!isOpen || !request) return null;
@ -130,16 +555,19 @@ const RequestDetailsDialog = ({ request, isOpen, onClose }) => {
className="bg-white rounded-2xl w-full max-w-3xl max-h-[90vh] overflow-y-auto shadow-2xl" className="bg-white rounded-2xl w-full max-w-3xl max-h-[90vh] overflow-y-auto shadow-2xl"
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
> >
<div className="sticky top-0 bg-white border-b p-4 flex justify-between items-center"> <div className="sticky top-0 bg-gradient-to-r from-amber-500 to-amber-600 p-5 text-white flex justify-between items-center">
<div>
<h2 className="text-xl font-bold flex items-center gap-2"> <h2 className="text-xl font-bold flex items-center gap-2">
<FileText className="w-5 h-5 text-blue-600" /> <FileText className="w-5 h-5" />
تفاصيل الطلب #{request.id} تفاصيل الطلب #{request.id}
</h2> </h2>
<p className="text-amber-100 text-sm mt-1">جميع معلومات الطلب في مكان واحد</p>
</div>
<button <button
onClick={onClose} onClick={onClose}
className="p-2 hover:bg-gray-100 rounded-full transition-colors" className="p-2 hover:bg-white/20 rounded-full transition-colors"
> >
<XCircle className="w-5 h-5 text-gray-500" /> <XCircle className="w-6 h-6" />
</button> </button>
</div> </div>
@ -149,7 +577,7 @@ const RequestDetailsDialog = ({ request, isOpen, onClose }) => {
<User className="w-4 h-4" /> <User className="w-4 h-4" />
معلومات المستأجر معلومات المستأجر
</h3> </h3>
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div> <div>
<label className="text-xs text-gray-500">الاسم الكامل</label> <label className="text-xs text-gray-500">الاسم الكامل</label>
<div className="font-medium">{request.user}</div> <div className="font-medium">{request.user}</div>
@ -183,7 +611,7 @@ const RequestDetailsDialog = ({ request, isOpen, onClose }) => {
<Home className="w-4 h-4" /> <Home className="w-4 h-4" />
معلومات العقار معلومات العقار
</h3> </h3>
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div> <div>
<label className="text-xs text-gray-500">العقار</label> <label className="text-xs text-gray-500">العقار</label>
<div className="font-medium">{request.property}</div> <div className="font-medium">{request.property}</div>
@ -200,7 +628,7 @@ const RequestDetailsDialog = ({ request, isOpen, onClose }) => {
<Calendar className="w-4 h-4" /> <Calendar className="w-4 h-4" />
تفاصيل الحجز تفاصيل الحجز
</h3> </h3>
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div> <div>
<label className="text-xs text-gray-500">تاريخ البداية</label> <label className="text-xs text-gray-500">تاريخ البداية</label>
<div className="font-medium">{request.startDate}</div> <div className="font-medium">{request.startDate}</div>
@ -225,7 +653,7 @@ const RequestDetailsDialog = ({ request, isOpen, onClose }) => {
<DollarSign className="w-4 h-4" /> <DollarSign className="w-4 h-4" />
المعلومات المالية المعلومات المالية
</h3> </h3>
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div> <div>
<label className="text-xs text-gray-500">سلفة الضمان</label> <label className="text-xs text-gray-500">سلفة الضمان</label>
<div className="font-medium text-blue-600">{formatCurrency(request.securityDeposit)}</div> <div className="font-medium text-blue-600">{formatCurrency(request.securityDeposit)}</div>
@ -245,7 +673,7 @@ const RequestDetailsDialog = ({ request, isOpen, onClose }) => {
</div> </div>
</div> </div>
<div className="border-t pt-4"> <div className="bg-gray-50 p-4 rounded-xl">
<h3 className="font-bold mb-3 flex items-center gap-2"> <h3 className="font-bold mb-3 flex items-center gap-2">
<History className="w-4 h-4" /> <History className="w-4 h-4" />
سجل الإجراءات سجل الإجراءات
@ -274,7 +702,7 @@ const RequestDetailsDialog = ({ request, isOpen, onClose }) => {
</div> </div>
)} )}
{request.notes && ( {request.notes && (
<div className="mt-3 p-3 bg-gray-50 rounded-lg"> <div className="mt-3 p-3 bg-white rounded-lg border">
<p className="text-sm text-gray-600">{request.notes}</p> <p className="text-sm text-gray-600">{request.notes}</p>
</div> </div>
)} )}
@ -282,7 +710,7 @@ const RequestDetailsDialog = ({ request, isOpen, onClose }) => {
</div> </div>
</div> </div>
<div className="sticky bottom-0 bg-gray-50 border-t p-4 flex gap-3"> <div className="sticky bottom-0 bg-white border-t p-4 flex gap-3 shadow-lg">
<button <button
onClick={() => window.print()} onClick={() => window.print()}
className="flex-1 bg-blue-600 text-white py-3 rounded-xl font-medium hover:bg-blue-700 transition-colors flex items-center justify-center gap-2" className="flex-1 bg-blue-600 text-white py-3 rounded-xl font-medium hover:bg-blue-700 transition-colors flex items-center justify-center gap-2"
@ -290,12 +718,8 @@ const RequestDetailsDialog = ({ request, isOpen, onClose }) => {
<Printer className="w-5 h-5" /> <Printer className="w-5 h-5" />
طباعة التفاصيل طباعة التفاصيل
</button> </button>
<button
className="flex-1 bg-green-600 text-white py-3 rounded-xl font-medium hover:bg-green-700 transition-colors flex items-center justify-center gap-2" <PDFExportButton request={request} />
>
<Download className="w-5 h-5" />
تصدير PDF
</button>
</div> </div>
</motion.div> </motion.div>
</motion.div> </motion.div>
@ -840,6 +1264,9 @@ export default function BookingRequests() {
return ( return (
<div className="space-y-6"> <div className="space-y-6">
<Toaster position="top-center" reverseOrder={false} />
{/* إحصائيات سريعة */}
<div className="grid grid-cols-4 gap-4"> <div className="grid grid-cols-4 gap-4">
<motion.div <motion.div
initial={{ opacity: 0, y: 20 }} initial={{ opacity: 0, y: 20 }}
@ -913,6 +1340,7 @@ export default function BookingRequests() {
key={request.id} key={request.id}
request={request} request={request}
onAction={handleAction} onAction={handleAction}
onViewDetails={() => setDetailsDialog({ isOpen: true, request })}
/> />
))} ))}
</div> </div>

View File

@ -13,9 +13,15 @@ import {
TrendingUp, TrendingUp,
TrendingDown, TrendingDown,
Wallet, Wallet,
Shield Shield,
FileText,
Printer,
X,
CheckCircle
} from 'lucide-react'; } from 'lucide-react';
import { formatCurrency } from '@/app/utils/calculations'; import { formatCurrency } from '@/app/utils/calculations';
import toast, { Toaster } from 'react-hot-toast';
import * as XLSX from 'xlsx';
export default function LedgerBook({ userType = 'admin' }) { export default function LedgerBook({ userType = 'admin' }) {
const [transactions, setTransactions] = useState([]); const [transactions, setTransactions] = useState([]);
@ -28,6 +34,7 @@ export default function LedgerBook({ userType = 'admin' }) {
securityDeposits: 0, securityDeposits: 0,
commissionEarned: 0 commissionEarned: 0
}); });
const [isExporting, setIsExporting] = useState(false);
useEffect(() => { useEffect(() => {
loadTransactions(); loadTransactions();
@ -144,30 +151,239 @@ export default function LedgerBook({ userType = 'admin' }) {
} }
}; };
const exportToExcel = () => { const exportToExcel = async () => {
const csvContent = [ if (filteredTransactions.length === 0) {
['التاريخ', 'الوصف', 'من', 'إلى', 'المبلغ', 'العمولة', 'الحالة'], toast.error('لا توجد معاملات للتصدير');
...filteredTransactions.map(t => [ return;
t.date, }
t.description,
t.fromUser,
t.toUser,
t.amount,
t.commission,
t.status
])
].map(row => row.join(',')).join('\n');
const blob = new Blob([csvContent], { type: 'text/csv' }); setIsExporting(true);
const url = window.URL.createObjectURL(blob); toast.loading('جاري تصدير البيانات...', { id: 'export' });
const a = document.createElement('a');
a.href = url; try {
a.download = `ledger_${new Date().toISOString()}.csv`; const exportData = filteredTransactions.map(t => ({
a.click(); 'رقم العملية': t.id,
'التاريخ': t.date,
'نوع العملية': t.type === 'rent_payment' ? 'دفعة إيجار' :
t.type === 'security_deposit' ? 'سلفة ضمان' :
t.type === 'commission' ? 'عمولة' : 'أخرى',
'الوصف': t.description,
'من': t.fromUser,
'إلى': t.toUser,
'المبلغ (ل.س)': t.amount,
'العمولة (ل.س)': t.commission || 0,
'الحالة': t.status === 'completed' ? 'مكتمل' :
t.status === 'pending' ? 'معلق' :
t.status === 'pending_refund' ? 'بإنتظار الاسترداد' : 'مؤكد',
}));
const summaryRow = {
'رقم العملية': '',
'التاريخ': '',
'نوع العملية': '',
'الوصف': '',
'من': '',
'إلى': '',
'المبلغ (ل.س)': summary.totalRevenue,
'العمولة (ل.س)': summary.commissionEarned,
'الحالة': ''
};
exportData.push(summaryRow);
const worksheet = XLSX.utils.json_to_sheet(exportData);
const columnWidths = [
{ wch: 12 }, // رقم العملية
{ wch: 12 }, // التاريخ
{ wch: 12 }, // نوع العملية
{ wch: 30 }, // الوصف
{ wch: 20 }, // من
{ wch: 20 }, // إلى
{ wch: 15 }, // المبلغ
{ wch: 15 }, // العمولة
{ wch: 12 }, // الحالة
];
worksheet['!cols'] = columnWidths;
const range = XLSX.utils.decode_range(worksheet['!ref']);
for (let C = range.s.c; C <= range.e.c; ++C) {
const address = XLSX.utils.encode_col(C) + '1';
if (!worksheet[address]) continue;
worksheet[address].s = {
font: { bold: true, sz: 12 },
fill: { fgColor: { rgb: "F59E0B" } },
alignment: { horizontal: "center", vertical: "center" }
};
}
const workbook = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(workbook, worksheet, 'دفتر الحسابات');
const fileName = `دفتر_الحسابات_${new Date().toISOString().split('T')[0]}.xlsx`;
XLSX.writeFile(workbook, fileName);
toast.success(`تم تصدير ${filteredTransactions.length} معاملة بنجاح!`, { id: 'export' });
} catch (error) {
console.error('Error exporting to Excel:', error);
toast.error('حدث خطأ أثناء تصدير البيانات', { id: 'export' });
} finally {
setIsExporting(false);
}
};
const printReport = () => {
const printWindow = window.open('', '_blank');
printWindow.document.write(`
<!DOCTYPE html>
<html dir="rtl">
<head>
<meta charset="UTF-8">
<title>تقرير دفتر الحسابات</title>
<style>
body {
font-family: 'Cairo', Arial, sans-serif;
padding: 20px;
direction: rtl;
}
.header {
text-align: center;
margin-bottom: 30px;
padding-bottom: 20px;
border-bottom: 2px solid #f59e0b;
}
.title {
font-size: 24px;
font-weight: bold;
color: #1f2937;
}
.subtitle {
color: #6b7280;
margin-top: 5px;
}
.summary {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 15px;
margin-bottom: 30px;
}
.summary-card {
background: #f9fafb;
padding: 15px;
border-radius: 12px;
text-align: center;
}
.summary-value {
font-size: 20px;
font-weight: bold;
color: #f59e0b;
}
table {
width: 100%;
border-collapse: collapse;
margin-top: 20px;
}
th, td {
border: 1px solid #e5e7eb;
padding: 10px;
text-align: right;
}
th {
background: #f59e0b;
color: white;
font-weight: bold;
}
.footer {
text-align: center;
margin-top: 30px;
padding-top: 20px;
border-top: 1px solid #e5e7eb;
color: #9ca3af;
font-size: 12px;
}
@media print {
.no-print {
display: none;
}
}
</style>
</head>
<body>
<div class="header">
<div class="title">تقرير دفتر الحسابات</div>
<div class="subtitle">الفترة: ${dateRange.start || 'بداية السجلات'} - ${dateRange.end || 'حتى الآن'}</div>
<div class="subtitle">تاريخ التقرير: ${new Date().toLocaleDateString('ar-SA')}</div>
</div>
<div class="summary">
<div class="summary-card">
<div>إجمالي الإيرادات</div>
<div class="summary-value">${formatCurrency(summary.totalRevenue)}</div>
</div>
<div class="summary-card">
<div>أرباح المنصة</div>
<div class="summary-value">${formatCurrency(summary.commissionEarned)}</div>
</div>
<div class="summary-card">
<div>سلف الضمان</div>
<div class="summary-value">${formatCurrency(summary.securityDeposits)}</div>
</div>
<div class="summary-card">
<div>المدفوعات المعلقة</div>
<div class="summary-value">${formatCurrency(summary.pendingPayments)}</div>
</div>
</div>
<table>
<thead>
<tr>
<th>التاريخ</th>
<th>الوصف</th>
<th>من</th>
<th>إلى</th>
<th>المبلغ</th>
<th>العمولة</th>
<th>الحالة</th>
</tr>
</thead>
<tbody>
${filteredTransactions.map(t => `
<tr>
<td>${t.date}</td>
<td>${t.description}</td>
<td>${t.fromUser}</td>
<td>${t.toUser}</td>
<td>${formatCurrency(t.amount)}</td>
<td>${t.commission ? formatCurrency(t.commission) : '-'}</td>
<td>${t.status === 'completed' ? 'مكتمل' : t.status === 'pending' ? 'معلق' : 'بإنتظار الرد'}</td>
</tr>
`).join('')}
</tbody>
</table>
<div class="footer">
<p>تقرير صادر عن نظام SweetHome لإدارة العقارات</p>
<p>جميع الحقوق محفوظة © ${new Date().getFullYear()}</p>
</div>
<div class="no-print" style="text-align: center; margin-top: 20px;">
<button onclick="window.print()" style="padding: 10px 20px; background: #f59e0b; color: white; border: none; border-radius: 8px; cursor: pointer;">
طباعة التقرير
</button>
</div>
</body>
</html>
`);
printWindow.document.close();
}; };
return ( return (
<div className="space-y-6"> <div className="space-y-6">
<Toaster position="top-center" reverseOrder={false} />
<div className="grid grid-cols-1 md:grid-cols-4 gap-4"> <div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<motion.div <motion.div
initial={{ opacity: 0, y: 20 }} initial={{ opacity: 0, y: 20 }}
@ -224,13 +440,13 @@ export default function LedgerBook({ userType = 'admin' }) {
<div className="bg-white rounded-xl p-5 shadow-sm border"> <div className="bg-white rounded-xl p-5 shadow-sm border">
<div className="flex flex-col md:flex-row gap-4"> <div className="flex flex-col md:flex-row gap-4">
<div className="flex-1 relative"> <div className="flex-1 relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-gray-400" /> <Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-5 h-5 text-gray-400" />
<input <input
type="text" type="text"
placeholder="بحث في المعاملات..." placeholder="بحث في المعاملات..."
value={searchTerm} value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)} onChange={(e) => setSearchTerm(e.target.value)}
className="w-full pl-10 pr-4 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500" className="w-full pl-12 pr-4 py-3 border border-gray-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500"
/> />
</div> </div>
@ -239,27 +455,65 @@ export default function LedgerBook({ userType = 'admin' }) {
type="date" type="date"
value={dateRange.start} value={dateRange.start}
onChange={(e) => setDateRange({...dateRange, start: e.target.value})} onChange={(e) => setDateRange({...dateRange, start: e.target.value})}
className="px-3 py-2 border rounded-lg" className="px-4 py-3 border border-gray-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500"
/> />
<span className="text-gray-500 self-center">إلى</span> <span className="text-gray-500 self-center">إلى</span>
<input <input
type="date" type="date"
value={dateRange.end} value={dateRange.end}
onChange={(e) => setDateRange({...dateRange, end: e.target.value})} onChange={(e) => setDateRange({...dateRange, end: e.target.value})}
className="px-3 py-2 border rounded-lg" className="px-4 py-3 border border-gray-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500"
/> />
</div> </div>
<div className="flex gap-2">
<button <button
onClick={exportToExcel} onClick={exportToExcel}
className="px-4 py-2 bg-green-600 text-white rounded-lg flex items-center gap-2 hover:bg-green-700" disabled={isExporting || filteredTransactions.length === 0}
className="px-5 py-3 bg-green-600 text-white rounded-xl flex items-center gap-2 hover:bg-green-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
> >
<Download className="w-4 h-4" /> {isExporting ? (
تصدير <>
<div className="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin" />
جاري التصدير...
</>
) : (
<>
<Download className="w-5 h-5" />
تصدير Excel
</>
)}
</button>
<button
onClick={printReport}
disabled={filteredTransactions.length === 0}
className="px-5 py-3 bg-blue-600 text-white rounded-xl flex items-center gap-2 hover:bg-blue-700 transition-colors disabled:opacity-50"
>
<Printer className="w-5 h-5" />
طباعة
</button> </button>
</div> </div>
</div> </div>
{(dateRange.start || dateRange.end || searchTerm) && (
<div className="mt-4 pt-4 border-t flex justify-between items-center">
<div className="text-sm text-gray-500">
<span className="font-medium">{filteredTransactions.length}</span> معاملة من إجمالي <span className="font-medium">{transactions.length}</span>
</div>
<button
onClick={() => {
setDateRange({ start: '', end: '' });
setSearchTerm('');
}}
className="text-sm text-red-500 hover:text-red-600 flex items-center gap-1"
>
<X className="w-4 h-4" />
إلغاء الفلترة
</button>
</div>
)}
</div>
<div className="bg-white rounded-xl shadow-sm border overflow-hidden"> <div className="bg-white rounded-xl shadow-sm border overflow-hidden">
<div className="overflow-x-auto"> <div className="overflow-x-auto">
<table className="w-full"> <table className="w-full">
@ -307,14 +561,14 @@ export default function LedgerBook({ userType = 'admin' }) {
<span className="text-sm">{transaction.toUser}</span> <span className="text-sm">{transaction.toUser}</span>
</div> </div>
</td> </td>
<td className="px-6 py-4 text-sm font-bold"> <td className="px-6 py-4 text-sm font-bold text-green-600">
{formatCurrency(transaction.amount)} {formatCurrency(transaction.amount)}
</td> </td>
<td className="px-6 py-4 text-sm text-amber-600"> <td className="px-6 py-4 text-sm text-amber-600">
{transaction.commission ? formatCurrency(transaction.commission) : '-'} {transaction.commission ? formatCurrency(transaction.commission) : '-'}
</td> </td>
<td className="px-6 py-4"> <td className="px-6 py-4">
<span className={`px-2 py-1 rounded-full text-xs ${ <span className={`px-2 py-1 rounded-full text-xs font-medium ${
transaction.status === 'completed' ? 'bg-green-100 text-green-800' : transaction.status === 'completed' ? 'bg-green-100 text-green-800' :
transaction.status === 'pending' ? 'bg-yellow-100 text-yellow-800' : transaction.status === 'pending' ? 'bg-yellow-100 text-yellow-800' :
'bg-blue-100 text-blue-800' 'bg-blue-100 text-blue-800'
@ -344,6 +598,7 @@ export default function LedgerBook({ userType = 'admin' }) {
أرصدة المستأجرين أرصدة المستأجرين
</h3> </h3>
<div className="space-y-3"> <div className="space-y-3">
<p className="text-gray-500 text-sm">لا توجد أرصدة حالياً</p>
</div> </div>
</div> </div>
)} )}

View File

@ -1,7 +1,7 @@
'use client'; 'use client';
import { useState } from 'react'; import { useState } from 'react';
import { motion } from 'framer-motion'; import { motion, AnimatePresence } from 'framer-motion';
import { import {
Edit, Edit,
Trash2, Trash2,
@ -12,14 +12,404 @@ import {
Square, Square,
DollarSign, DollarSign,
Percent, Percent,
MoreVertical MoreVertical,
X,
CheckCircle,
AlertCircle,
Calendar,
User,
Home,
Building,
Clock
} from 'lucide-react'; } from 'lucide-react';
import toast, { Toaster } from 'react-hot-toast';
const DeleteConfirmationModal = ({ isOpen, onClose, onConfirm, propertyTitle }) => {
if (!isOpen) return null;
return (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center p-4 z-50"
onClick={onClose}
>
<motion.div
initial={{ scale: 0.9, y: 20 }}
animate={{ scale: 1, y: 0 }}
exit={{ scale: 0.9, y: 20 }}
className="bg-white rounded-2xl w-full max-w-md p-6 shadow-2xl"
onClick={(e) => e.stopPropagation()}
>
<div className="text-center mb-4">
<div className="w-16 h-16 bg-red-100 rounded-full flex items-center justify-center mx-auto mb-3">
<AlertCircle className="w-8 h-8 text-red-600" />
</div>
<h3 className="text-xl font-bold text-gray-900">تأكيد الحذف</h3>
<p className="text-sm text-gray-500 mt-2">
هل أنت متأكد من حذف العقار: <span className="font-bold text-gray-700">"{propertyTitle}"</span>؟
</p>
<p className="text-xs text-red-500 mt-1">هذا الإجراء لا يمكن التراجع عنه</p>
</div>
<div className="flex gap-3 pt-3">
<button
onClick={onClose}
className="flex-1 bg-gray-100 text-gray-700 py-3 rounded-xl font-medium hover:bg-gray-200 transition-colors"
>
إلغاء
</button>
<button
onClick={onConfirm}
className="flex-1 bg-red-600 text-white py-3 rounded-xl font-medium hover:bg-red-700 transition-colors"
>
نعم، احذف
</button>
</div>
</motion.div>
</motion.div>
);
};
const PropertyViewModal = ({ property, isOpen, onClose }) => {
if (!isOpen || !property) return null;
const formatCurrency = (amount) => {
return amount?.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",") + ' ل.س';
};
return (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center p-4 z-50"
onClick={onClose}
>
<motion.div
initial={{ scale: 0.9, y: 20 }}
animate={{ scale: 1, y: 0 }}
exit={{ scale: 0.9, y: 20 }}
className="bg-white rounded-2xl w-full max-w-2xl max-h-[90vh] overflow-y-auto shadow-2xl"
onClick={(e) => e.stopPropagation()}
>
<div className="sticky top-0 bg-gradient-to-r from-amber-500 to-amber-600 p-6 text-white">
<div className="flex justify-between items-center">
<div>
<h2 className="text-xl font-bold">{property.title}</h2>
<p className="text-amber-100 text-sm mt-1">{property.location}</p>
</div>
<button onClick={onClose} className="p-1 hover:bg-white/20 rounded-full">
<X className="w-6 h-6" />
</button>
</div>
</div>
<div className="p-6 space-y-6">
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<div className="bg-gray-50 p-3 rounded-xl text-center">
<Home className="w-5 h-5 text-amber-500 mx-auto mb-1" />
<div className="text-sm font-bold">{property.type === 'villa' ? 'فيلا' : property.type === 'apartment' ? 'شقة' : 'بيت'}</div>
<div className="text-xs text-gray-500">نوع العقار</div>
</div>
<div className="bg-gray-50 p-3 rounded-xl text-center">
<DollarSign className="w-5 h-5 text-amber-500 mx-auto mb-1" />
<div className="text-sm font-bold">{formatCurrency(property.price)}</div>
<div className="text-xs text-gray-500">السعر اليومي</div>
</div>
<div className="bg-gray-50 p-3 rounded-xl text-center">
<Percent className="w-5 h-5 text-amber-500 mx-auto mb-1" />
<div className="text-sm font-bold">{property.commission}%</div>
<div className="text-xs text-gray-500">نسبة العمولة</div>
</div>
<div className="bg-gray-50 p-3 rounded-xl text-center">
<Calendar className="w-5 h-5 text-amber-500 mx-auto mb-1" />
<div className="text-sm font-bold">{property.bookings || 0}</div>
<div className="text-xs text-gray-500">عدد الحجوزات</div>
</div>
</div>
<div className="bg-gray-50 p-4 rounded-xl">
<h3 className="font-bold text-gray-900 mb-3 flex items-center gap-2">
<MapPin className="w-5 h-5 text-amber-500" />
الموقع
</h3>
<p className="text-gray-700">{property.location}</p>
</div>
<div className="bg-gray-50 p-4 rounded-xl">
<h3 className="font-bold text-gray-900 mb-3">المواصفات</h3>
<div className="grid grid-cols-3 gap-4">
<div className="text-center">
<Bed className="w-5 h-5 text-amber-500 mx-auto mb-1" />
<div className="text-lg font-bold">{property.bedrooms}</div>
<div className="text-xs text-gray-500">غرف نوم</div>
</div>
<div className="text-center">
<Bath className="w-5 h-5 text-amber-500 mx-auto mb-1" />
<div className="text-lg font-bold">{property.bathrooms}</div>
<div className="text-xs text-gray-500">حمامات</div>
</div>
<div className="text-center">
<Square className="w-5 h-5 text-amber-500 mx-auto mb-1" />
<div className="text-lg font-bold">{property.area}</div>
<div className="text-xs text-gray-500">م²</div>
</div>
</div>
</div>
<div className="bg-amber-50 p-4 rounded-xl">
<h3 className="font-bold text-amber-700 mb-3 flex items-center gap-2">
<Percent className="w-5 h-5" />
معلومات العمولة
</h3>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="text-xs text-gray-500">نسبة العمولة</label>
<div className="font-bold text-amber-600">{property.commission}%</div>
</div>
<div>
<label className="text-xs text-gray-500">مصدر العمولة</label>
<div className="font-bold text-amber-600">{property.commissionType}</div>
</div>
<div>
<label className="text-xs text-gray-500">قيمة العمولة</label>
<div className="font-bold text-amber-600">
{formatCurrency((property.price * property.commission) / 100)}
</div>
</div>
<div>
<label className="text-xs text-gray-500">حالة العقار</label>
<div className={`inline-block px-2 py-1 rounded-lg text-xs font-medium ${
property.status === 'available'
? 'bg-green-100 text-green-800'
: 'bg-red-100 text-red-800'
}`}>
{property.status === 'available' ? 'متاح' : 'محجوز'}
</div>
</div>
</div>
</div>
</div>
</motion.div>
</motion.div>
);
};
const PropertyEditModal = ({ property, isOpen, onClose, onSave }) => {
const [formData, setFormData] = useState({ ...property });
const [isSaving, setIsSaving] = useState(false);
const formatCurrency = (amount) => {
return amount?.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
};
const handleSave = () => {
setIsSaving(true);
setTimeout(() => {
onSave(formData);
setIsSaving(false);
onClose();
toast.success('تم تحديث العقار بنجاح');
}, 1000);
};
if (!isOpen || !property) return null;
return (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center p-4 z-50"
onClick={onClose}
>
<motion.div
initial={{ scale: 0.9, y: 20 }}
animate={{ scale: 1, y: 0 }}
exit={{ scale: 0.9, y: 20 }}
className="bg-white rounded-2xl w-full max-w-2xl max-h-[90vh] overflow-y-auto shadow-2xl"
onClick={(e) => e.stopPropagation()}
>
<div className="sticky top-0 bg-gradient-to-r from-amber-500 to-amber-600 p-6 text-white">
<div className="flex justify-between items-center">
<h2 className="text-xl font-bold">تعديل العقار</h2>
<button onClick={onClose} className="p-1 hover:bg-white/20 rounded-full">
<X className="w-6 h-6" />
</button>
</div>
<p className="text-amber-100 text-sm mt-1">يمكنك تعديل معلومات العقار</p>
</div>
<div className="p-6 space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
اسم العقار
</label>
<input
type="text"
value={formData.title}
onChange={(e) => setFormData({...formData, title: e.target.value})}
className="w-full px-4 py-3 border border-gray-300 rounded-xl focus:ring-2 focus:ring-amber-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
نوع العقار
</label>
<select
value={formData.type}
onChange={(e) => setFormData({...formData, type: e.target.value})}
className="w-full px-4 py-3 border border-gray-300 rounded-xl focus:ring-2 focus:ring-amber-500"
>
<option value="villa">فيلا</option>
<option value="apartment">شقة</option>
<option value="house">بيت</option>
<option value="studio">استوديو</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
الموقع
</label>
<input
type="text"
value={formData.location}
onChange={(e) => setFormData({...formData, location: e.target.value})}
className="w-full px-4 py-3 border border-gray-300 rounded-xl focus:ring-2 focus:ring-amber-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
السعر اليومي (ل.س)
</label>
<input
type="number"
value={formData.price}
onChange={(e) => setFormData({...formData, price: parseInt(e.target.value)})}
className="w-full px-4 py-3 border border-gray-300 rounded-xl focus:ring-2 focus:ring-amber-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
نسبة العمولة (%)
</label>
<input
type="number"
step="0.1"
value={formData.commission}
onChange={(e) => setFormData({...formData, commission: parseFloat(e.target.value)})}
className="w-full px-4 py-3 border border-gray-300 rounded-xl focus:ring-2 focus:ring-amber-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
مصدر العمولة
</label>
<select
value={formData.commissionType}
onChange={(e) => setFormData({...formData, commissionType: e.target.value})}
className="w-full px-4 py-3 border border-gray-300 rounded-xl focus:ring-2 focus:ring-amber-500"
>
<option value="من المالك">من المالك</option>
<option value="من المستأجر">من المستأجر</option>
<option value="من الاثنين">من الاثنين</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
عدد الغرف
</label>
<input
type="number"
value={formData.bedrooms}
onChange={(e) => setFormData({...formData, bedrooms: parseInt(e.target.value)})}
className="w-full px-4 py-3 border border-gray-300 rounded-xl focus:ring-2 focus:ring-amber-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
عدد الحمامات
</label>
<input
type="number"
value={formData.bathrooms}
onChange={(e) => setFormData({...formData, bathrooms: parseInt(e.target.value)})}
className="w-full px-4 py-3 border border-gray-300 rounded-xl focus:ring-2 focus:ring-amber-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
المساحة (م²)
</label>
<input
type="number"
value={formData.area}
onChange={(e) => setFormData({...formData, area: parseInt(e.target.value)})}
className="w-full px-4 py-3 border border-gray-300 rounded-xl focus:ring-2 focus:ring-amber-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
حالة العقار
</label>
<select
value={formData.status}
onChange={(e) => setFormData({...formData, status: e.target.value})}
className="w-full px-4 py-3 border border-gray-300 rounded-xl focus:ring-2 focus:ring-amber-500"
>
<option value="available">متاح</option>
<option value="booked">محجوز</option>
<option value="maintenance">صيانة</option>
</select>
</div>
</div>
</div>
<div className="sticky bottom-0 bg-gray-50 border-t p-4 flex gap-3">
<button
onClick={onClose}
className="flex-1 bg-gray-100 text-gray-700 py-3 rounded-xl font-medium hover:bg-gray-200 transition-colors"
>
إلغاء
</button>
<button
onClick={handleSave}
disabled={isSaving}
className="flex-1 bg-amber-500 text-white py-3 rounded-xl font-medium hover:bg-amber-600 transition-colors disabled:opacity-50"
>
{isSaving ? 'جاري الحفظ...' : 'حفظ التغييرات'}
</button>
</div>
</motion.div>
</motion.div>
);
};
const MoreActionsMenu = ({ property, isOpen, onClose, onViewBookings, onViewReports }) => {
if (!isOpen) return null;
return (
<>
<div className="fixed inset-0 z-40" onClick={onClose} />
<motion.div
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.9 }}
className="absolute left-0 mt-2 w-56 bg-white rounded-xl shadow-xl border border-gray-200 overflow-hidden z-50"
>
</motion.div>
</>
);
};
export default function PropertiesTable() { export default function PropertiesTable() {
const [properties, setProperties] = useState([ const [properties, setProperties] = useState([
{ {
id: 1, id: 1,
title: 'luxuryVillaDamascus', title: 'فيلا فاخرة في المزة',
type: 'villa', type: 'villa',
location: 'دمشق, المزة', location: 'دمشق, المزة',
price: 500000, price: 500000,
@ -33,7 +423,7 @@ export default function PropertiesTable() {
}, },
{ {
id: 2, id: 2,
title: 'modernApartmentAleppo', title: 'شقة حديثة في الشهباء',
type: 'apartment', type: 'apartment',
location: 'حلب, الشهباء', location: 'حلب, الشهباء',
price: 250000, price: 250000,
@ -47,6 +437,11 @@ export default function PropertiesTable() {
} }
]); ]);
const [viewModal, setViewModal] = useState({ isOpen: false, property: null });
const [editModal, setEditModal] = useState({ isOpen: false, property: null });
const [deleteModal, setDeleteModal] = useState({ isOpen: false, property: null });
const [moreMenu, setMoreMenu] = useState({ isOpen: false, property: null, anchorEl: null });
const formatCurrency = (amount) => { const formatCurrency = (amount) => {
return amount.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",") + ' ل.س'; return amount.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",") + ' ل.س';
}; };
@ -71,8 +466,50 @@ export default function PropertiesTable() {
); );
}; };
const handleView = (property) => {
setViewModal({ isOpen: true, property });
};
const handleEdit = (property) => {
setEditModal({ isOpen: true, property });
};
const handleDelete = (property) => {
setDeleteModal({ isOpen: true, property });
};
const confirmDelete = () => {
if (deleteModal.property) {
setProperties(prev => prev.filter(p => p.id !== deleteModal.property.id));
setDeleteModal({ isOpen: false, property: null });
toast.success('تم حذف العقار بنجاح');
}
};
const handleSaveEdit = (updatedProperty) => {
setProperties(prev => prev.map(p =>
p.id === updatedProperty.id ? updatedProperty : p
));
toast.success('تم تحديث العقار بنجاح');
};
const handleMoreClick = (event, property) => {
event.stopPropagation();
setMoreMenu({ isOpen: true, property, anchorEl: event.currentTarget });
};
const handleViewBookings = (property) => {
toast.success(`جاري عرض حجوزات ${property.title}`);
};
const handleViewReports = (property) => {
toast.success(`جاري عرض تقرير أرباح ${property.title}`);
};
return ( return (
<div className="overflow-x-auto"> <div className="overflow-x-auto">
<Toaster position="top-center" reverseOrder={false} />
<table className="w-full"> <table className="w-full">
<thead className="bg-gray-50 border-b"> <thead className="bg-gray-50 border-b">
<tr> <tr>
@ -97,7 +534,11 @@ export default function PropertiesTable() {
> >
<td className="px-4 py-3"> <td className="px-4 py-3">
<div className="font-medium">{property.title}</div> <div className="font-medium">{property.title}</div>
<div className="text-xs text-gray-500">{property.type}</div> <div className="text-xs text-gray-500">
{property.type === 'villa' ? 'فيلا' :
property.type === 'apartment' ? 'شقة' :
property.type === 'house' ? 'بيت' : 'استوديو'}
</div>
</td> </td>
<td className="px-4 py-3"> <td className="px-4 py-3">
<div className="flex items-center gap-1 text-sm"> <div className="flex items-center gap-1 text-sm">
@ -125,20 +566,38 @@ export default function PropertiesTable() {
<td className="px-4 py-3"> <td className="px-4 py-3">
{getStatusBadge(property.status)} {getStatusBadge(property.status)}
</td> </td>
<td className="px-4 py-3"> <td className="px-4 py-3 relative">
<div className="flex items-center justify-center gap-2"> <div className="flex items-center justify-center gap-2">
<button className="p-1 hover:bg-blue-100 rounded text-blue-600"> <button
onClick={() => handleView(property)}
className="p-1 hover:bg-blue-100 rounded text-blue-600 transition-colors"
title="عرض التفاصيل"
>
<Eye className="w-4 h-4" /> <Eye className="w-4 h-4" />
</button> </button>
<button className="p-1 hover:bg-amber-100 rounded text-amber-600"> <button
onClick={() => handleEdit(property)}
className="p-1 hover:bg-amber-100 rounded text-amber-600 transition-colors"
title="تعديل العقار"
>
<Edit className="w-4 h-4" /> <Edit className="w-4 h-4" />
</button> </button>
<button className="p-1 hover:bg-red-100 rounded text-red-600"> <button
onClick={() => handleDelete(property)}
className="p-1 hover:bg-red-100 rounded text-red-600 transition-colors"
title="حذف العقار"
>
<Trash2 className="w-4 h-4" /> <Trash2 className="w-4 h-4" />
</button> </button>
<button className="p-1 hover:bg-gray-100 rounded"> {moreMenu.isOpen && moreMenu.property?.id === property.id && (
<MoreVertical className="w-4 h-4" /> <MoreActionsMenu
</button> property={property}
isOpen={moreMenu.isOpen}
onClose={() => setMoreMenu({ isOpen: false, property: null, anchorEl: null })}
onViewBookings={handleViewBookings}
onViewReports={handleViewReports}
/>
)}
</div> </div>
</td> </td>
</motion.tr> </motion.tr>
@ -152,6 +611,26 @@ export default function PropertiesTable() {
<p className="text-gray-500">لا توجد عقارات مضافة بعد</p> <p className="text-gray-500">لا توجد عقارات مضافة بعد</p>
</div> </div>
)} )}
<PropertyViewModal
property={viewModal.property}
isOpen={viewModal.isOpen}
onClose={() => setViewModal({ isOpen: false, property: null })}
/>
<PropertyEditModal
property={editModal.property}
isOpen={editModal.isOpen}
onClose={() => setEditModal({ isOpen: false, property: null })}
onSave={handleSaveEdit}
/>
<DeleteConfirmationModal
isOpen={deleteModal.isOpen}
onClose={() => setDeleteModal({ isOpen: false, property: null })}
onConfirm={confirmDelete}
propertyTitle={deleteModal.property?.title}
/>
</div> </div>
); );
} }

View File

@ -1,7 +1,7 @@
'use client'; 'use client';
import { useState } from 'react'; import { useState } from 'react';
import { motion } from 'framer-motion'; import { motion, AnimatePresence } from 'framer-motion';
import { import {
User, User,
Mail, Mail,
@ -11,8 +11,445 @@ import {
DollarSign, DollarSign,
Search, Search,
Filter, Filter,
Eye Eye,
X,
CheckCircle,
XCircle,
ChevronDown,
Users,
Award,
Clock,
TrendingUp,
CalendarDays,
Shield
} from 'lucide-react'; } from 'lucide-react';
import toast, { Toaster } from 'react-hot-toast';
const FilterDialog = ({ isOpen, onClose, filters, onApplyFilters, onResetFilters }) => {
const [localFilters, setLocalFilters] = useState({ ...filters });
const identityTypes = [
{ id: 'all', label: 'الكل' },
{ id: 'syrian', label: 'هوية سورية' },
{ id: 'passport', label: 'جواز سفر' }
];
const bookingRanges = [
{ id: 'all', label: 'الكل' },
{ id: '0-5', label: '0 - 5 حجوزات' },
{ id: '5-10', label: '5 - 10 حجوزات' },
{ id: '10-20', label: '10 - 20 حجوزات' },
{ id: '20+', label: 'أكثر من 20 حجز' }
];
const spendingRanges = [
{ id: 'all', label: 'الكل' },
{ id: '0-500000', label: 'أقل من 500,000 ل.س' },
{ id: '500000-1000000', label: '500,000 - 1,000,000 ل.س' },
{ id: '1000000-5000000', label: '1,000,000 - 5,000,000 ل.س' },
{ id: '5000000+', label: 'أكثر من 5,000,000 ل.س' }
];
const dateRanges = [
{ id: 'all', label: 'الكل' },
{ id: 'today', label: 'اليوم' },
{ id: 'week', label: 'آخر 7 أيام' },
{ id: 'month', label: 'آخر 30 يوم' },
{ id: 'year', label: 'آخر 12 شهر' }
];
const applyFilters = () => {
onApplyFilters(localFilters);
onClose();
toast.success('تم تطبيق الفلاتر بنجاح');
};
const resetFilters = () => {
const resetData = {
identityType: 'all',
minBookings: '',
maxBookings: '',
minSpending: '',
maxSpending: '',
dateRange: 'all',
activeOnly: false,
inactiveOnly: false
};
setLocalFilters(resetData);
onResetFilters();
onClose();
toast.success('تم إعادة تعيين الفلاتر');
};
if (!isOpen) return null;
return (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center p-4 z-50"
onClick={onClose}
>
<motion.div
initial={{ scale: 0.9, y: 20 }}
animate={{ scale: 1, y: 0 }}
exit={{ scale: 0.9, y: 20 }}
className="bg-white rounded-2xl w-full max-w-2xl max-h-[90vh] overflow-y-auto shadow-2xl"
onClick={(e) => e.stopPropagation()}
>
<div className="sticky top-0 bg-gradient-to-r from-blue-600 to-blue-700 p-6 text-white">
<div className="flex justify-between items-center">
<div>
<h2 className="text-xl font-bold flex items-center gap-2">
<Filter className="w-5 h-5" />
تصفية متقدمة
</h2>
<p className="text-blue-100 text-sm mt-1">حدد معايير التصفية المطلوبة</p>
</div>
<button onClick={onClose} className="p-1 hover:bg-white/20 rounded-full">
<X className="w-6 h-6" />
</button>
</div>
</div>
<div className="p-6 space-y-6">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
نوع الهوية
</label>
<div className="grid grid-cols-3 gap-2">
{identityTypes.map((type) => (
<button
key={type.id}
onClick={() => setLocalFilters({...localFilters, identityType: type.id})}
className={`px-4 py-2 rounded-xl text-sm font-medium transition-all ${
localFilters.identityType === type.id
? 'bg-blue-600 text-white shadow-md'
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
}`}
>
{type.label}
</button>
))}
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
عدد الحجوزات
</label>
<div className="grid grid-cols-2 gap-2 mb-3">
<input
type="number"
placeholder="من"
value={localFilters.minBookings}
onChange={(e) => setLocalFilters({...localFilters, minBookings: e.target.value})}
className="px-4 py-2 border border-gray-300 rounded-xl focus:ring-2 focus:ring-blue-500"
/>
<input
type="number"
placeholder="إلى"
value={localFilters.maxBookings}
onChange={(e) => setLocalFilters({...localFilters, maxBookings: e.target.value})}
className="px-4 py-2 border border-gray-300 rounded-xl focus:ring-2 focus:ring-blue-500"
/>
</div>
<div className="flex flex-wrap gap-2">
{bookingRanges.slice(1).map((range) => (
<button
key={range.id}
onClick={() => {
const [min, max] = range.id.split('-');
setLocalFilters({
...localFilters,
minBookings: min,
maxBookings: max === '5' ? '5' : max === '10' ? '10' : max === '20' ? '20' : '1000'
});
}}
className="px-3 py-1 text-xs bg-gray-100 text-gray-600 rounded-lg hover:bg-gray-200"
>
{range.label}
</button>
))}
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
إجمالي الإنفاق (ل.س)
</label>
<div className="grid grid-cols-2 gap-2 mb-3">
<input
type="number"
placeholder="من"
value={localFilters.minSpending}
onChange={(e) => setLocalFilters({...localFilters, minSpending: e.target.value})}
className="px-4 py-2 border border-gray-300 rounded-xl focus:ring-2 focus:ring-blue-500"
/>
<input
type="number"
placeholder="إلى"
value={localFilters.maxSpending}
onChange={(e) => setLocalFilters({...localFilters, maxSpending: e.target.value})}
className="px-4 py-2 border border-gray-300 rounded-xl focus:ring-2 focus:ring-blue-500"
/>
</div>
<div className="flex flex-wrap gap-2">
{spendingRanges.slice(1).map((range) => (
<button
key={range.id}
onClick={() => {
const [min, max] = range.id.split('-');
setLocalFilters({
...localFilters,
minSpending: min,
maxSpending: max === '500000' ? '500000' : max === '1000000' ? '1000000' : max === '5000000' ? '5000000' : '999999999'
});
}}
className="px-3 py-1 text-xs bg-gray-100 text-gray-600 rounded-lg hover:bg-gray-200"
>
{range.label}
</button>
))}
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
فترة التسجيل
</label>
<div className="grid grid-cols-2 md:grid-cols-5 gap-2">
{dateRanges.map((range) => (
<button
key={range.id}
onClick={() => setLocalFilters({...localFilters, dateRange: range.id})}
className={`px-3 py-2 rounded-xl text-sm font-medium transition-all ${
localFilters.dateRange === range.id
? 'bg-blue-600 text-white'
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
}`}
>
{range.label}
</button>
))}
</div>
</div>
<div className="flex gap-4">
<label className="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
checked={localFilters.activeOnly}
onChange={(e) => setLocalFilters({...localFilters, activeOnly: e.target.checked, inactiveOnly: false})}
className="w-4 h-4 text-blue-600 rounded focus:ring-blue-500"
/>
<span className="text-sm text-gray-700">مستخدمون لديهم حجوزات نشطة فقط</span>
</label>
<label className="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
checked={localFilters.inactiveOnly}
onChange={(e) => setLocalFilters({...localFilters, inactiveOnly: e.target.checked, activeOnly: false})}
className="w-4 h-4 text-blue-600 rounded focus:ring-blue-500"
/>
<span className="text-sm text-gray-700">مستخدمون بدون حجوزات نشطة</span>
</label>
</div>
</div>
<div className="sticky bottom-0 bg-gray-50 border-t p-4 flex gap-3">
<button
onClick={resetFilters}
className="flex-1 bg-gray-100 text-gray-700 py-3 rounded-xl font-medium hover:bg-gray-200 transition-colors"
>
إعادة تعيين
</button>
<button
onClick={applyFilters}
className="flex-1 bg-blue-600 text-white py-3 rounded-xl font-medium hover:bg-blue-700 transition-colors"
>
تطبيق الفلاتر
</button>
</div>
</motion.div>
</motion.div>
);
};
const UserDetailsModal = ({ user, isOpen, onClose }) => {
if (!isOpen || !user) return null;
const formatCurrency = (amount) => {
return amount?.toLocaleString() + ' ل.س';
};
const userBookings = [
{
id: 'BK001',
property: 'فيلا فاخرة في المزة',
startDate: '2024-03-10',
endDate: '2024-03-15',
amount: 2500000,
status: 'completed'
},
{
id: 'BK002',
property: 'شقة حديثة في الشهباء',
startDate: '2024-02-20',
endDate: '2024-02-25',
amount: 1250000,
status: 'completed'
},
{
id: 'BK003',
property: 'بيت عائلي في بابا عمرو',
startDate: '2024-04-01',
endDate: '2024-04-10',
amount: 3500000,
status: 'confirmed'
}
];
return (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center p-4 z-50"
onClick={onClose}
>
<motion.div
initial={{ scale: 0.9, y: 20 }}
animate={{ scale: 1, y: 0 }}
exit={{ scale: 0.9, y: 20 }}
className="bg-white rounded-2xl w-full max-w-3xl max-h-[90vh] overflow-y-auto shadow-2xl"
onClick={(e) => e.stopPropagation()}
>
<div className="sticky top-0 bg-gradient-to-r from-blue-600 to-blue-700 p-6 text-white">
<div className="flex justify-between items-center">
<div>
<h2 className="text-xl font-bold flex items-center gap-2">
<User className="w-5 h-5" />
تفاصيل المستخدم
</h2>
<p className="text-blue-100 text-sm mt-1">{user.name}</p>
</div>
<button onClick={onClose} className="p-1 hover:bg-white/20 rounded-full">
<X className="w-6 h-6" />
</button>
</div>
</div>
<div className="p-6 space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="bg-gray-50 p-4 rounded-xl">
<h3 className="font-bold text-gray-900 mb-3 flex items-center gap-2">
<User className="w-4 h-4 text-blue-500" />
معلومات شخصية
</h3>
<div className="space-y-2">
<div className="flex justify-between">
<span className="text-gray-500">الاسم الكامل:</span>
<span className="font-medium">{user.name}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-500">البريد الإلكتروني:</span>
<span className="font-medium">{user.email}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-500">رقم الهاتف:</span>
<span className="font-medium">{user.phone}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-500">تاريخ التسجيل:</span>
<span className="font-medium">{user.joinDate}</span>
</div>
</div>
</div>
<div className="bg-gray-50 p-4 rounded-xl">
<h3 className="font-bold text-gray-900 mb-3 flex items-center gap-2">
<Shield className="w-4 h-4 text-blue-500" />
معلومات الهوية
</h3>
<div className="space-y-2">
<div className="flex justify-between">
<span className="text-gray-500">نوع الهوية:</span>
<span className="font-medium">
{user.identityType === 'syrian' ? 'هوية سورية' : 'جواز سفر'}
</span>
</div>
<div className="flex justify-between">
<span className="text-gray-500">رقم الهوية:</span>
<span className="font-medium">{user.identityNumber}</span>
</div>
</div>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="bg-blue-50 p-4 rounded-xl text-center">
<div className="text-2xl font-bold text-blue-600">{user.totalBookings}</div>
<div className="text-sm text-gray-600">إجمالي الحجوزات</div>
</div>
<div className="bg-green-50 p-4 rounded-xl text-center">
<div className="text-2xl font-bold text-green-600">{user.activeBookings}</div>
<div className="text-sm text-gray-600">حجوزات نشطة</div>
</div>
<div className="bg-amber-50 p-4 rounded-xl text-center">
<div className="text-2xl font-bold text-amber-600">{formatCurrency(user.totalSpent)}</div>
<div className="text-sm text-gray-600">إجمالي المنصرف</div>
</div>
</div>
<div>
<h3 className="font-bold text-gray-900 mb-3 flex items-center gap-2">
<Calendar className="w-4 h-4 text-blue-500" />
سجل الحجوزات
</h3>
<div className="space-y-3">
{userBookings.map((booking) => (
<div key={booking.id} className="bg-gray-50 p-4 rounded-xl flex flex-col md:flex-row justify-between items-start md:items-center gap-3">
<div>
<p className="font-medium text-gray-900">{booking.property}</p>
<div className="flex items-center gap-2 text-sm text-gray-500 mt-1">
<CalendarDays className="w-3 h-3" />
{booking.startDate} - {booking.endDate}
</div>
</div>
<div className="flex items-center gap-4">
<div className="text-right">
<div className="text-lg font-bold text-amber-600">{formatCurrency(booking.amount)}</div>
<div className="text-xs text-gray-500">المبلغ الإجمالي</div>
</div>
<span className={`px-2 py-1 rounded-lg text-xs font-medium ${
booking.status === 'completed'
? 'bg-green-100 text-green-800'
: 'bg-blue-100 text-blue-800'
}`}>
{booking.status === 'completed' ? 'مكتمل' : 'مؤكد'}
</span>
</div>
</div>
))}
{userBookings.length === 0 && (
<div className="text-center py-8 text-gray-500">
<Calendar className="w-12 h-12 text-gray-300 mx-auto mb-2" />
<p>لا توجد حجوزات سابقة</p>
</div>
)}
</div>
</div>
</div>
<div className="sticky bottom-0 bg-gray-50 border-t p-4 flex gap-3">
</div>
</motion.div>
</motion.div>
);
};
export default function UsersList() { export default function UsersList() {
const [users, setUsers] = useState([ const [users, setUsers] = useState([
@ -44,30 +481,194 @@ export default function UsersList() {
const [searchTerm, setSearchTerm] = useState(''); const [searchTerm, setSearchTerm] = useState('');
const [selectedUser, setSelectedUser] = useState(null); const [selectedUser, setSelectedUser] = useState(null);
const [showFilterDialog, setShowFilterDialog] = useState(false);
const [filters, setFilters] = useState({
identityType: 'all',
minBookings: '',
maxBookings: '',
minSpending: '',
maxSpending: '',
dateRange: 'all',
activeOnly: false,
inactiveOnly: false
});
const filteredUsers = users.filter(user => const applyFilters = (newFilters) => {
user.name.includes(searchTerm) || setFilters(newFilters);
user.email.includes(searchTerm) || };
user.phone.includes(searchTerm)
); const resetFilters = () => {
setFilters({
identityType: 'all',
minBookings: '',
maxBookings: '',
minSpending: '',
maxSpending: '',
dateRange: 'all',
activeOnly: false,
inactiveOnly: false
});
setSearchTerm('');
};
const filteredUsers = users.filter(user => {
if (searchTerm && !user.name.includes(searchTerm) && !user.email.includes(searchTerm) && !user.phone.includes(searchTerm)) {
return false;
}
if (filters.identityType !== 'all' && user.identityType !== filters.identityType) {
return false;
}
if (filters.minBookings && user.totalBookings < parseInt(filters.minBookings)) {
return false;
}
if (filters.maxBookings && user.totalBookings > parseInt(filters.maxBookings)) {
return false;
}
if (filters.minSpending && user.totalSpent < parseInt(filters.minSpending)) {
return false;
}
if (filters.maxSpending && user.totalSpent > parseInt(filters.maxSpending)) {
return false;
}
if (filters.activeOnly && user.activeBookings === 0) {
return false;
}
if (filters.inactiveOnly && user.activeBookings > 0) {
return false;
}
if (filters.dateRange !== 'all') {
const joinDate = new Date(user.joinDate);
const today = new Date();
const diffDays = Math.floor((today - joinDate) / (1000 * 60 * 60 * 24));
switch(filters.dateRange) {
case 'today':
if (joinDate.toDateString() !== today.toDateString()) return false;
break;
case 'week':
if (diffDays > 7) return false;
break;
case 'month':
if (diffDays > 30) return false;
break;
case 'year':
if (diffDays > 365) return false;
break;
}
}
return true;
});
const filterStats = {
total: filteredUsers.length,
filtered: filteredUsers.length !== users.length
};
const getActiveFiltersCount = () => {
let count = 0;
if (filters.identityType !== 'all') count++;
if (filters.minBookings || filters.maxBookings) count++;
if (filters.minSpending || filters.maxSpending) count++;
if (filters.dateRange !== 'all') count++;
if (filters.activeOnly || filters.inactiveOnly) count++;
return count;
};
return ( return (
<div className="space-y-4"> <div className="space-y-4">
<div className="flex gap-4"> <Toaster position="top-center" reverseOrder={false} />
<div className="flex flex-col md:flex-row gap-4">
<div className="flex-1 relative"> <div className="flex-1 relative">
<Search className="absolute right-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-gray-400" /> <Search className="absolute right-3 top-1/2 transform -translate-y-1/2 w-5 h-5 text-gray-400" />
<input <input
type="text" type="text"
placeholder="بحث عن مستخدم..." placeholder="بحث عن مستخدم بالاسم أو البريد أو الهاتف..."
value={searchTerm} value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)} onChange={(e) => setSearchTerm(e.target.value)}
className="w-full pr-10 px-4 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500" className="w-full pr-12 px-4 py-3 border border-gray-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500"
/> />
</div> </div>
<button className="px-4 py-2 border rounded-lg flex items-center gap-2 hover:bg-gray-50"> <div className="flex gap-2">
<Filter className="w-4 h-4" /> <button
تصفية onClick={() => setShowFilterDialog(true)}
className={`px-5 py-3 rounded-xl font-medium flex items-center gap-2 transition-all ${
getActiveFiltersCount() > 0
? 'bg-blue-600 text-white hover:bg-blue-700'
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
}`}
>
<Filter className="w-5 h-5" />
تصفية متقدمة
{getActiveFiltersCount() > 0 && (
<span className="ml-1 bg-white text-blue-600 rounded-full w-5 h-5 text-xs flex items-center justify-center">
{getActiveFiltersCount()}
</span>
)}
</button> </button>
{filterStats.filtered && (
<button
onClick={resetFilters}
className="px-5 py-3 bg-gray-100 text-gray-700 rounded-xl font-medium hover:bg-gray-200 transition-colors flex items-center gap-2"
>
<X className="w-4 h-4" />
إعادة تعيين
</button>
)}
</div>
</div>
{getActiveFiltersCount() > 0 && (
<div className="flex flex-wrap gap-2 p-3 bg-blue-50 rounded-xl">
<span className="text-sm text-blue-800 font-medium">الفلاتر النشطة:</span>
{filters.identityType !== 'all' && (
<span className="px-2 py-1 bg-blue-100 text-blue-700 rounded-lg text-xs">
{filters.identityType === 'syrian' ? 'هوية سورية' : 'جواز سفر'}
</span>
)}
{(filters.minBookings || filters.maxBookings) && (
<span className="px-2 py-1 bg-blue-100 text-blue-700 rounded-lg text-xs">
الحجوزات: {filters.minBookings || '0'} - {filters.maxBookings || '∞'}
</span>
)}
{(filters.minSpending || filters.maxSpending) && (
<span className="px-2 py-1 bg-blue-100 text-blue-700 rounded-lg text-xs">
الإنفاق: {parseInt(filters.minSpending || 0).toLocaleString()} - {parseInt(filters.maxSpending || '∞').toLocaleString()} ل.س
</span>
)}
{filters.dateRange !== 'all' && (
<span className="px-2 py-1 bg-blue-100 text-blue-700 rounded-lg text-xs">
{filters.dateRange === 'today' ? 'اليوم' :
filters.dateRange === 'week' ? 'آخر 7 أيام' :
filters.dateRange === 'month' ? 'آخر 30 يوم' : 'آخر 12 شهر'}
</span>
)}
{filters.activeOnly && (
<span className="px-2 py-1 bg-green-100 text-green-700 rounded-lg text-xs">
لديهم حجوزات نشطة
</span>
)}
{filters.inactiveOnly && (
<span className="px-2 py-1 bg-gray-100 text-gray-700 rounded-lg text-xs">
بدون حجوزات نشطة
</span>
)}
</div>
)}
<div className="flex justify-between items-center">
<div className="text-sm text-gray-600">
عرض <span className="font-bold text-gray-900">{filteredUsers.length}</span> مستخدم
{filterStats.filtered && (
<span className="text-gray-500 mr-1">(من {users.length})</span>
)}
</div>
</div> </div>
<div className="space-y-3"> <div className="space-y-3">
@ -76,40 +677,46 @@ export default function UsersList() {
key={user.id} key={user.id}
initial={{ opacity: 0, y: 20 }} initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
transition={{ delay: index * 0.1 }} transition={{ delay: index * 0.05 }}
className="bg-white border rounded-lg p-4 hover:shadow-md transition-shadow" className="bg-white border border-gray-200 rounded-xl p-5 hover:shadow-md transition-all"
> >
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4"> <div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<div className="w-12 h-12 bg-blue-100 rounded-full flex items-center justify-center"> <div className="w-14 h-14 bg-gradient-to-br from-blue-500 to-blue-600 rounded-full flex items-center justify-center text-white text-xl font-bold shadow-lg">
<User className="w-6 h-6 text-blue-600" /> {user.name.charAt(0).toUpperCase()}
</div> </div>
<div> <div>
<h3 className="font-bold">{user.name}</h3> <h3 className="font-bold text-gray-900 text-lg">{user.name}</h3>
<div className="flex flex-wrap gap-3 mt-1 text-sm text-gray-600"> <div className="flex flex-wrap gap-3 mt-1 text-sm text-gray-500">
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<Mail className="w-3 h-3" /> <Mail className="w-4 h-4" />
{user.email} {user.email}
</div> </div>
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<Phone className="w-3 h-3" /> <Phone className="w-4 h-4" />
{user.phone} {user.phone}
</div> </div>
<div className="flex items-center gap-1">
<Shield className="w-4 h-4" />
{user.identityType === 'syrian' ? 'هوية سورية' : 'جواز سفر'}
</div>
</div> </div>
</div> </div>
</div> </div>
<div className="flex gap-4"> <div className="flex gap-6">
<div className="text-center"> <div className="text-center min-w-[80px]">
<div className="text-lg font-bold text-blue-600">{user.totalBookings}</div> <div className="text-xl font-bold text-blue-600">{user.totalBookings}</div>
<div className="text-xs text-gray-500">إجمالي الحجوزات</div> <div className="text-xs text-gray-500">إجمالي الحجوزات</div>
</div> </div>
<div className="text-center"> <div className="text-center min-w-[80px]">
<div className="text-lg font-bold text-green-600">{user.activeBookings}</div> <div className={`text-xl font-bold ${user.activeBookings > 0 ? 'text-green-600' : 'text-gray-400'}`}>
{user.activeBookings}
</div>
<div className="text-xs text-gray-500">حجوزات نشطة</div> <div className="text-xs text-gray-500">حجوزات نشطة</div>
</div> </div>
<div className="text-center"> <div className="text-center min-w-[100px]">
<div className="text-lg font-bold text-amber-600"> <div className="text-xl font-bold text-amber-600">
{user.totalSpent.toLocaleString()} {user.totalSpent.toLocaleString()}
</div> </div>
<div className="text-xs text-gray-500">إجمالي المنصرف</div> <div className="text-xs text-gray-500">إجمالي المنصرف</div>
@ -118,7 +725,7 @@ export default function UsersList() {
<button <button
onClick={() => setSelectedUser(user)} onClick={() => setSelectedUser(user)}
className="px-3 py-1.5 bg-blue-600 text-white rounded-lg text-sm flex items-center gap-1 hover:bg-blue-700" className="px-4 py-2 bg-blue-600 text-white rounded-xl text-sm font-medium hover:bg-blue-700 transition-colors flex items-center gap-2"
> >
<Eye className="w-4 h-4" /> <Eye className="w-4 h-4" />
عرض التفاصيل عرض التفاصيل
@ -128,63 +735,39 @@ export default function UsersList() {
))} ))}
</div> </div>
{selectedUser && ( {filteredUsers.length === 0 && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center p-4 z-50">
<motion.div <motion.div
initial={{ opacity: 0, scale: 0.9 }} initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }} animate={{ opacity: 1, scale: 1 }}
className="bg-white rounded-xl w-full max-w-2xl p-6" className="text-center py-16 bg-white rounded-2xl border-2 border-dashed border-gray-300"
> >
<div className="flex justify-between items-center mb-4"> <Users className="w-16 h-16 text-gray-300 mx-auto mb-4" />
<h2 className="text-xl font-bold">تفاصيل المستخدم</h2> <h3 className="text-xl font-bold text-gray-700 mb-2">لا توجد نتائج</h3>
<p className="text-gray-500">لا يوجد مستخدمون يطابقون معايير البحث</p>
{(searchTerm || getActiveFiltersCount() > 0) && (
<button <button
onClick={() => setSelectedUser(null)} onClick={resetFilters}
className="p-1 hover:bg-gray-100 rounded" className="mt-4 px-6 py-2 bg-blue-600 text-white rounded-xl text-sm font-medium hover:bg-blue-700"
> >
إعادة تعيين الفلاتر
</button> </button>
</div>
<div className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div>
<label className="text-sm text-gray-500">الاسم</label>
<div className="font-medium">{selectedUser.name}</div>
</div>
<div>
<label className="text-sm text-gray-500">البريد الإلكتروني</label>
<div className="font-medium">{selectedUser.email}</div>
</div>
<div>
<label className="text-sm text-gray-500">رقم الهاتف</label>
<div className="font-medium">{selectedUser.phone}</div>
</div>
<div>
<label className="text-sm text-gray-500">نوع الهوية</label>
<div className="font-medium">
{selectedUser.identityType === 'syrian' ? 'هوية سورية' : 'جواز سفر'}
</div>
</div>
<div>
<label className="text-sm text-gray-500">رقم الهوية</label>
<div className="font-medium">{selectedUser.identityNumber}</div>
</div>
<div>
<label className="text-sm text-gray-500">تاريخ التسجيل</label>
<div className="font-medium">{selectedUser.joinDate}</div>
</div>
</div>
<div className="border-t pt-4">
<h3 className="font-bold mb-3">سجل الحجوزات</h3>
<p className="text-gray-500 text-center py-4">
لا توجد حجوزات سابقة
</p>
</div>
</div>
</motion.div>
</div>
)} )}
</motion.div>
)}
<FilterDialog
isOpen={showFilterDialog}
onClose={() => setShowFilterDialog(false)}
filters={filters}
onApplyFilters={applyFilters}
onResetFilters={resetFilters}
/>
<UserDetailsModal
user={selectedUser}
isOpen={!!selectedUser}
onClose={() => setSelectedUser(null)}
/>
</div> </div>
); );
} }

319
package-lock.json generated
View File

@ -12,8 +12,10 @@
"flowbite": "^4.0.1", "flowbite": "^4.0.1",
"flowbite-react": "^0.12.16", "flowbite-react": "^0.12.16",
"framer-motion": "^12.29.2", "framer-motion": "^12.29.2",
"html2canvas": "^1.4.1",
"i18next": "^25.8.0", "i18next": "^25.8.0",
"i18next-browser-languagedetector": "^8.2.0", "i18next-browser-languagedetector": "^8.2.0",
"jspdf": "^4.2.1",
"leaflet": "^1.9.4", "leaflet": "^1.9.4",
"lucide-react": "^0.563.0", "lucide-react": "^0.563.0",
"next": "16.1.6", "next": "16.1.6",
@ -22,7 +24,8 @@
"react-hot-toast": "^2.6.0", "react-hot-toast": "^2.6.0",
"react-i18next": "^16.5.4", "react-i18next": "^16.5.4",
"react-intersection-observer": "^10.0.3", "react-intersection-observer": "^10.0.3",
"react-leaflet": "^4.2.1" "react-leaflet": "^4.2.1",
"xlsx": "^0.18.5"
}, },
"devDependencies": { "devDependencies": {
"@tailwindcss/postcss": "^4", "@tailwindcss/postcss": "^4",
@ -1199,12 +1202,32 @@
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/pako": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/@types/pako/-/pako-2.0.4.tgz",
"integrity": "sha512-VWDCbrLeVXJM9fihYodcLiIv0ku+AlOa/TQ1SvYOaBuyrSKgEcro95LJyIsJ4vSo6BXIxOKxiJAat04CmST9Fw==",
"license": "MIT"
},
"node_modules/@types/raf": {
"version": "3.4.3",
"resolved": "https://registry.npmjs.org/@types/raf/-/raf-3.4.3.tgz",
"integrity": "sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw==",
"license": "MIT",
"optional": true
},
"node_modules/@types/resolve": { "node_modules/@types/resolve": {
"version": "1.20.2", "version": "1.20.2",
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz",
"integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==", "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/trusted-types": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
"license": "MIT",
"optional": true
},
"node_modules/@types/yandex-maps": { "node_modules/@types/yandex-maps": {
"version": "2.1.29", "version": "2.1.29",
"resolved": "https://registry.npmjs.org/@types/yandex-maps/-/yandex-maps-2.1.29.tgz", "resolved": "https://registry.npmjs.org/@types/yandex-maps/-/yandex-maps-2.1.29.tgz",
@ -1306,6 +1329,15 @@
"url": "https://opencollective.com/typescript-eslint" "url": "https://opencollective.com/typescript-eslint"
} }
}, },
"node_modules/adler-32": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz",
"integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.8"
}
},
"node_modules/array-timsort": { "node_modules/array-timsort": {
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/array-timsort/-/array-timsort-1.0.3.tgz", "resolved": "https://registry.npmjs.org/array-timsort/-/array-timsort-1.0.3.tgz",
@ -1377,6 +1409,15 @@
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/base64-arraybuffer": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz",
"integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==",
"license": "MIT",
"engines": {
"node": ">= 0.6.0"
}
},
"node_modules/baseline-browser-mapping": { "node_modules/baseline-browser-mapping": {
"version": "2.9.19", "version": "2.9.19",
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz",
@ -1461,6 +1502,39 @@
], ],
"license": "CC-BY-4.0" "license": "CC-BY-4.0"
}, },
"node_modules/canvg": {
"version": "3.0.11",
"resolved": "https://registry.npmjs.org/canvg/-/canvg-3.0.11.tgz",
"integrity": "sha512-5ON+q7jCTgMp9cjpu4Jo6XbvfYwSB2Ow3kzHKfIyJfaCAOHLbdKPQqGKgfED/R5B+3TFFfe8pegYA+b423SRyA==",
"license": "MIT",
"optional": true,
"dependencies": {
"@babel/runtime": "^7.12.5",
"@types/raf": "^3.4.0",
"core-js": "^3.8.3",
"raf": "^3.4.1",
"regenerator-runtime": "^0.13.7",
"rgbcolor": "^1.0.1",
"stackblur-canvas": "^2.0.0",
"svg-pathdata": "^6.0.3"
},
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/cfb": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz",
"integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==",
"license": "Apache-2.0",
"dependencies": {
"adler-32": "~1.3.0",
"crc-32": "~1.2.0"
},
"engines": {
"node": ">=0.8"
}
},
"node_modules/chokidar": { "node_modules/chokidar": {
"version": "4.0.3", "version": "4.0.3",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
@ -1488,6 +1562,15 @@
"integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/codepage": {
"version": "1.15.0",
"resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz",
"integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.8"
}
},
"node_modules/comment-json": { "node_modules/comment-json": {
"version": "4.4.1", "version": "4.4.1",
"resolved": "https://registry.npmjs.org/comment-json/-/comment-json-4.4.1.tgz", "resolved": "https://registry.npmjs.org/comment-json/-/comment-json-4.4.1.tgz",
@ -1502,12 +1585,45 @@
"node": ">= 6" "node": ">= 6"
} }
}, },
"node_modules/core-js": {
"version": "3.49.0",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.49.0.tgz",
"integrity": "sha512-es1U2+YTtzpwkxVLwAFdSpaIMyQaq0PBgm3YD1W3Qpsn1NAmO3KSgZfu+oGSWVu6NvLHoHCV/aYcsE5wiB7ALg==",
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/core-js"
}
},
"node_modules/core-util-is": { "node_modules/core-util-is": {
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/crc-32": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz",
"integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==",
"license": "Apache-2.0",
"bin": {
"crc32": "bin/crc32.njs"
},
"engines": {
"node": ">=0.8"
}
},
"node_modules/css-line-break": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz",
"integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==",
"license": "MIT",
"dependencies": {
"utrie": "^1.0.2"
}
},
"node_modules/csstype": { "node_modules/csstype": {
"version": "3.2.3", "version": "3.2.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
@ -1580,6 +1696,16 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/dompurify": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.3.tgz",
"integrity": "sha512-Oj6pzI2+RqBfFG+qOaOLbFXLQ90ARpcGG6UePL82bJLtdsa6CYJD7nmiU8MW9nQNOtCHV3lZ/Bzq1X0QYbBZCA==",
"license": "(MPL-2.0 OR Apache-2.0)",
"optional": true,
"optionalDependencies": {
"@types/trusted-types": "^2.0.7"
}
},
"node_modules/electron-to-chromium": { "node_modules/electron-to-chromium": {
"version": "1.5.279", "version": "1.5.279",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.279.tgz", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.279.tgz",
@ -1657,6 +1783,17 @@
"node": ">=8.6.0" "node": ">=8.6.0"
} }
}, },
"node_modules/fast-png": {
"version": "6.4.0",
"resolved": "https://registry.npmjs.org/fast-png/-/fast-png-6.4.0.tgz",
"integrity": "sha512-kAqZq1TlgBjZcLr5mcN6NP5Rv4V2f22z00c3g8vRrwkcqjerx7BEhPbOnWCPqaHUl2XWQBJQvOT/FQhdMT7X/Q==",
"license": "MIT",
"dependencies": {
"@types/pako": "^2.0.3",
"iobuffer": "^5.3.2",
"pako": "^2.1.0"
}
},
"node_modules/fastq": { "node_modules/fastq": {
"version": "1.20.1", "version": "1.20.1",
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz",
@ -1666,6 +1803,12 @@
"reusify": "^1.0.4" "reusify": "^1.0.4"
} }
}, },
"node_modules/fflate": {
"version": "0.8.2",
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz",
"integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==",
"license": "MIT"
},
"node_modules/fill-range": { "node_modules/fill-range": {
"version": "7.1.1", "version": "7.1.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
@ -1731,6 +1874,15 @@
"tailwindcss": "^3 || ^4" "tailwindcss": "^3 || ^4"
} }
}, },
"node_modules/frac": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz",
"integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.8"
}
},
"node_modules/fraction.js": { "node_modules/fraction.js": {
"version": "5.3.4", "version": "5.3.4",
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz",
@ -1829,6 +1981,19 @@
"void-elements": "3.1.0" "void-elements": "3.1.0"
} }
}, },
"node_modules/html2canvas": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz",
"integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==",
"license": "MIT",
"dependencies": {
"css-line-break": "^2.1.0",
"text-segmentation": "^1.0.3"
},
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/i18next": { "node_modules/i18next": {
"version": "25.8.0", "version": "25.8.0",
"resolved": "https://registry.npmjs.org/i18next/-/i18next-25.8.0.tgz", "resolved": "https://registry.npmjs.org/i18next/-/i18next-25.8.0.tgz",
@ -1869,6 +2034,12 @@
"@babel/runtime": "^7.23.2" "@babel/runtime": "^7.23.2"
} }
}, },
"node_modules/iobuffer": {
"version": "5.4.0",
"resolved": "https://registry.npmjs.org/iobuffer/-/iobuffer-5.4.0.tgz",
"integrity": "sha512-DRebOWuqDvxunfkNJAlc3IzWIPD5xVxwUNbHr7xKB8E6aLJxIPfNX3CoMJghcFjpv6RWQsrcJbghtEwSPoJqMA==",
"license": "MIT"
},
"node_modules/is-core-module": { "node_modules/is-core-module": {
"version": "2.16.1", "version": "2.16.1",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
@ -1935,6 +2106,23 @@
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/jspdf": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/jspdf/-/jspdf-4.2.1.tgz",
"integrity": "sha512-YyAXyvnmjTbR4bHQRLzex3CuINCDlQnBqoSYyjJwTP2x9jDLuKDzy7aKUl0hgx3uhcl7xzg32agn5vlie6HIlQ==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.28.6",
"fast-png": "^6.2.0",
"fflate": "^0.8.1"
},
"optionalDependencies": {
"canvg": "^3.0.11",
"core-js": "^3.6.0",
"dompurify": "^3.3.1",
"html2canvas": "^1.0.0-rc.5"
}
},
"node_modules/klona": { "node_modules/klona": {
"version": "2.0.6", "version": "2.0.6",
"resolved": "https://registry.npmjs.org/klona/-/klona-2.0.6.tgz", "resolved": "https://registry.npmjs.org/klona/-/klona-2.0.6.tgz",
@ -2420,12 +2608,25 @@
"integrity": "sha512-uBj69dVlYe/+wxj8JOpr97XfsxH/eumMt6HqjNTmJDf/6NO9s+0uxeOneIz3AsPt2m6y9PqzDzd3ATcU17MNfw==", "integrity": "sha512-uBj69dVlYe/+wxj8JOpr97XfsxH/eumMt6HqjNTmJDf/6NO9s+0uxeOneIz3AsPt2m6y9PqzDzd3ATcU17MNfw==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/pako": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz",
"integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==",
"license": "(MIT AND Zlib)"
},
"node_modules/path-parse": { "node_modules/path-parse": {
"version": "1.0.7", "version": "1.0.7",
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/performance-now": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
"integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==",
"license": "MIT",
"optional": true
},
"node_modules/picocolors": { "node_modules/picocolors": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
@ -2499,6 +2700,16 @@
], ],
"license": "MIT" "license": "MIT"
}, },
"node_modules/raf": {
"version": "3.4.1",
"resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz",
"integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==",
"license": "MIT",
"optional": true,
"dependencies": {
"performance-now": "^2.1.0"
}
},
"node_modules/react": { "node_modules/react": {
"version": "18.3.1", "version": "18.3.1",
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
@ -2626,6 +2837,13 @@
"node": ">= 4" "node": ">= 4"
} }
}, },
"node_modules/regenerator-runtime": {
"version": "0.13.11",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
"integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==",
"license": "MIT",
"optional": true
},
"node_modules/resolve": { "node_modules/resolve": {
"version": "1.22.11", "version": "1.22.11",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz",
@ -2656,6 +2874,16 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/rgbcolor": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/rgbcolor/-/rgbcolor-1.0.1.tgz",
"integrity": "sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==",
"license": "MIT OR SEE LICENSE IN FEEL-FREE.md",
"optional": true,
"engines": {
"node": ">= 0.8.15"
}
},
"node_modules/run-parallel": { "node_modules/run-parallel": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
@ -2763,6 +2991,28 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/ssf": {
"version": "0.11.2",
"resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz",
"integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==",
"license": "Apache-2.0",
"dependencies": {
"frac": "~1.1.2"
},
"engines": {
"node": ">=0.8"
}
},
"node_modules/stackblur-canvas": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/stackblur-canvas/-/stackblur-canvas-2.7.0.tgz",
"integrity": "sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ==",
"license": "MIT",
"optional": true,
"engines": {
"node": ">=0.1.14"
}
},
"node_modules/styled-jsx": { "node_modules/styled-jsx": {
"version": "5.1.6", "version": "5.1.6",
"resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz",
@ -2798,6 +3048,16 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/svg-pathdata": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/svg-pathdata/-/svg-pathdata-6.0.3.tgz",
"integrity": "sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==",
"license": "MIT",
"optional": true,
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/tabbable": { "node_modules/tabbable": {
"version": "6.4.0", "version": "6.4.0",
"resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.4.0.tgz", "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.4.0.tgz",
@ -2845,6 +3105,15 @@
"url": "https://opencollective.com/webpack" "url": "https://opencollective.com/webpack"
} }
}, },
"node_modules/text-segmentation": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz",
"integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==",
"license": "MIT",
"dependencies": {
"utrie": "^1.0.2"
}
},
"node_modules/tiny-invariant": { "node_modules/tiny-invariant": {
"version": "1.3.3", "version": "1.3.3",
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
@ -2935,6 +3204,15 @@
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
} }
}, },
"node_modules/utrie": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz",
"integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==",
"license": "MIT",
"dependencies": {
"base64-arraybuffer": "^1.0.2"
}
},
"node_modules/void-elements": { "node_modules/void-elements": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz",
@ -2943,6 +3221,45 @@
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
} }
},
"node_modules/wmf": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz",
"integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.8"
}
},
"node_modules/word": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/word/-/word-0.3.0.tgz",
"integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.8"
}
},
"node_modules/xlsx": {
"version": "0.18.5",
"resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz",
"integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==",
"license": "Apache-2.0",
"dependencies": {
"adler-32": "~1.3.0",
"cfb": "~1.2.1",
"codepage": "~1.15.0",
"crc-32": "~1.2.1",
"ssf": "~0.11.2",
"wmf": "~1.0.1",
"word": "~0.3.0"
},
"bin": {
"xlsx": "bin/xlsx.njs"
},
"engines": {
"node": ">=0.8"
}
} }
} }
} }

View File

@ -12,8 +12,10 @@
"flowbite": "^4.0.1", "flowbite": "^4.0.1",
"flowbite-react": "^0.12.16", "flowbite-react": "^0.12.16",
"framer-motion": "^12.29.2", "framer-motion": "^12.29.2",
"html2canvas": "^1.4.1",
"i18next": "^25.8.0", "i18next": "^25.8.0",
"i18next-browser-languagedetector": "^8.2.0", "i18next-browser-languagedetector": "^8.2.0",
"jspdf": "^4.2.1",
"leaflet": "^1.9.4", "leaflet": "^1.9.4",
"lucide-react": "^0.563.0", "lucide-react": "^0.563.0",
"next": "16.1.6", "next": "16.1.6",
@ -22,7 +24,8 @@
"react-hot-toast": "^2.6.0", "react-hot-toast": "^2.6.0",
"react-i18next": "^16.5.4", "react-i18next": "^16.5.4",
"react-intersection-observer": "^10.0.3", "react-intersection-observer": "^10.0.3",
"react-leaflet": "^4.2.1" "react-leaflet": "^4.2.1",
"xlsx": "^0.18.5"
}, },
"devDependencies": { "devDependencies": {
"@tailwindcss/postcss": "^4", "@tailwindcss/postcss": "^4",