diff --git a/app/admin/page.js b/app/admin/page.js index 93103be..ece9978 100644 --- a/app/admin/page.js +++ b/app/admin/page.js @@ -32,7 +32,7 @@ export default function AdminPage() { { id: 'bookings', label: 'طلبات الحجز', icon: Calendar, badge: notifications }, { id: 'users', label: 'المستخدمين', icon: Users }, { id: 'ledger', label: 'دفتر الحسابات', icon: DollarSign }, - { id: 'reports', label: 'التقارير', icon: TrendingUp } + // { id: 'reports', label: 'التقارير', icon: TrendingUp } ]; return ( diff --git a/app/components/admin/AddPropertyForm.js b/app/components/admin/AddPropertyForm.js index aab1ae0..85d5da4 100644 --- a/app/components/admin/AddPropertyForm.js +++ b/app/components/admin/AddPropertyForm.js @@ -38,11 +38,21 @@ export default function AddPropertyForm({ onClose, onSuccess }) { const [selectedFeatures, setSelectedFeatures] = useState([]); const featuresList = [ - 'swimmingPool', 'privateGarden', 'parking', 'superLuxFinish', - 'equippedKitchen', 'centralHeating', 'balcony', 'securitySystem', - 'largeGarden', 'receptionHall', 'maidRoom', 'garage', - 'seaView', 'centralAC', 'fruitGarden', 'storage' - ]; + 'مسبح', + 'حديقة خاصة', + 'موقف سيارات', + 'مطبخ مجهز', + 'تدفئة مركزية', + 'بلكونة', + 'نظام أمني', + 'حديقة كبيرة', + 'صالة استقبال', + 'غرفة خادمة', + 'كراج', + 'إطلالة بحرية', + 'تكييف مركزي', + 'مخزن' +]; const handleSubmit = async (e) => { e.preventDefault(); diff --git a/app/components/admin/BookingRequests.js b/app/components/admin/BookingRequests.js index cdb94c5..8cc2bcd 100644 --- a/app/components/admin/BookingRequests.js +++ b/app/components/admin/BookingRequests.js @@ -1,6 +1,6 @@ 'use client'; -import { useState } from 'react'; +import { useState, useRef } from 'react'; import { motion, AnimatePresence } from 'framer-motion'; import { CheckCircle, @@ -22,8 +22,12 @@ import { FileText, Download, Printer, - History + History, + Loader2 } 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 [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 = ` + + + + + + + +
+
+ +
تقرير طلب حجز #${request.id}
+
تاريخ التقرير: ${new Date().toLocaleDateString('ar-SA')} | ${new Date().toLocaleTimeString('ar-SA')}
+
+ +
+
+ معلومات المستأجر +
+
+
+ الاسم الكامل: + ${request.user || 'غير محدد'} +
+
+ نوع الهوية: + ${request.userType === 'syrian' ? 'هوية سورية' : 'جواز سفر'} +
+
+ رقم الهوية: + ${request.identityNumber || 'غير محدد'} +
+
+ البريد الإلكتروني: + ${request.userEmail || 'غير محدد'} +
+
+ رقم الهاتف: + ${request.userPhone || 'غير محدد'} +
+
+
+ +
+
+ معلومات العقار +
+
+
+ العقار: + ${request.property || 'غير محدد'} +
+
+ السعر اليومي: + ${formatCurrency(request.dailyPrice)} +
+
+
+ +
+
+ تفاصيل الحجز +
+
+
+ تاريخ البداية: + ${request.startDate || 'غير محدد'} +
+
+ تاريخ النهاية: + ${request.endDate || 'غير محدد'} +
+
+ عدد الأيام: + ${request.days || 0} يوم +
+
+ الحالة: + + + ${request.status === 'pending' ? '⏳ قيد الانتظار' : + request.status === 'owner_approved' ? '✓ موافقة المالك' : + request.status === 'admin_approved' ? '✓ موافقة الإدارة' : + request.status === 'active' ? '🏠 إيجار نشط' : + request.status === 'completed' ? '✔️ منتهي' : '❌ مرفوض'} + + +
+
+
+ +
+
+ المعلومات المالية +
+
+
+ سلفة الضمان: + ${formatCurrency(request.securityDeposit)} +
+
+ المبلغ الإجمالي: + ${formatCurrency(request.totalAmount)} +
+
+ نسبة العمولة: + ${request.commissionRate || 0}% +
+
+ نوع العمولة: + ${request.commissionType || 'غير محدد'} +
+
+ قيمة العمولة: + ${formatCurrency(request.commissionAmount)} +
+
+
+ + ${request.notes ? ` +
+
+ ملاحظات +
+
+ ${request.notes} +
+
+ ` : ''} + +
+
+ سجل الإجراءات +
+
+
+ ${request.requestDate} + تم إنشاء الطلب +
+ ${request.ownerApproved ? ` +
+ + تمت موافقة المالك +
+ ` : ''} + ${request.adminApproved ? ` +
+ + تمت موافقة الإدارة +
+ ` : ''} + ${request.ownerDelivered ? ` +
+ + تم تسليم المفتاح +
+ ` : ''} + ${request.tenantReceived ? ` +
+ + تم استلام العقار +
+ ` : ''} +
+
+ + +
+ + + `; + + 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 ( + + ); +}; + const RequestDetailsDialog = ({ request, isOpen, onClose }) => { 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" onClick={(e) => e.stopPropagation()} > -
-

- - تفاصيل الطلب #{request.id} -

+
+
+

+ + تفاصيل الطلب #{request.id} +

+

جميع معلومات الطلب في مكان واحد

+
@@ -149,7 +577,7 @@ const RequestDetailsDialog = ({ request, isOpen, onClose }) => { معلومات المستأجر -
+
{request.user}
@@ -183,7 +611,7 @@ const RequestDetailsDialog = ({ request, isOpen, onClose }) => { معلومات العقار -
+
{request.property}
@@ -200,7 +628,7 @@ const RequestDetailsDialog = ({ request, isOpen, onClose }) => { تفاصيل الحجز -
+
{request.startDate}
@@ -225,7 +653,7 @@ const RequestDetailsDialog = ({ request, isOpen, onClose }) => { المعلومات المالية -
+
{formatCurrency(request.securityDeposit)}
@@ -245,7 +673,7 @@ const RequestDetailsDialog = ({ request, isOpen, onClose }) => {
-
+

سجل الإجراءات @@ -274,7 +702,7 @@ const RequestDetailsDialog = ({ request, isOpen, onClose }) => {

)} {request.notes && ( -
+

{request.notes}

)} @@ -282,7 +710,7 @@ const RequestDetailsDialog = ({ request, isOpen, onClose }) => {
-
+
- + +
@@ -332,12 +756,12 @@ const RequestCard = ({ request, onAction, onViewDetails }) => { }; const labels = { - pending: ' بانتظار الموافقة', - owner_approved: ' موافقة المالك', - admin_approved: ' موافقة الإدارة', - active: ' إيجار نشط', - completed: ' منتهي', - rejected: ' مرفوض' + pending: 'بانتظار الموافقة', + owner_approved: 'موافقة المالك', + admin_approved: 'موافقة الإدارة', + active: 'إيجار نشط', + completed: 'منتهي', + rejected: 'مرفوض' }; return ( @@ -840,6 +1264,9 @@ export default function BookingRequests() { return (
+ + + {/* إحصائيات سريعة */}
setDetailsDialog({ isOpen: true, request })} /> ))}
diff --git a/app/components/admin/LedgerBook.js b/app/components/admin/LedgerBook.js index 65bef51..fe86650 100644 --- a/app/components/admin/LedgerBook.js +++ b/app/components/admin/LedgerBook.js @@ -13,9 +13,15 @@ import { TrendingUp, TrendingDown, Wallet, - Shield + Shield, + FileText, + Printer, + X, + CheckCircle } from 'lucide-react'; import { formatCurrency } from '@/app/utils/calculations'; +import toast, { Toaster } from 'react-hot-toast'; +import * as XLSX from 'xlsx'; export default function LedgerBook({ userType = 'admin' }) { const [transactions, setTransactions] = useState([]); @@ -28,6 +34,7 @@ export default function LedgerBook({ userType = 'admin' }) { securityDeposits: 0, commissionEarned: 0 }); + const [isExporting, setIsExporting] = useState(false); useEffect(() => { loadTransactions(); @@ -144,30 +151,239 @@ export default function LedgerBook({ userType = 'admin' }) { } }; - const exportToExcel = () => { - const csvContent = [ - ['التاريخ', 'الوصف', 'من', 'إلى', 'المبلغ', 'العمولة', 'الحالة'], - ...filteredTransactions.map(t => [ - t.date, - t.description, - t.fromUser, - t.toUser, - t.amount, - t.commission, - t.status - ]) - ].map(row => row.join(',')).join('\n'); + const exportToExcel = async () => { + if (filteredTransactions.length === 0) { + toast.error('لا توجد معاملات للتصدير'); + return; + } - const blob = new Blob([csvContent], { type: 'text/csv' }); - const url = window.URL.createObjectURL(blob); - const a = document.createElement('a'); - a.href = url; - a.download = `ledger_${new Date().toISOString()}.csv`; - a.click(); + setIsExporting(true); + toast.loading('جاري تصدير البيانات...', { id: 'export' }); + + try { + const exportData = filteredTransactions.map(t => ({ + 'رقم العملية': 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(` + + + + + تقرير دفتر الحسابات + + + +
+
تقرير دفتر الحسابات
+
الفترة: ${dateRange.start || 'بداية السجلات'} - ${dateRange.end || 'حتى الآن'}
+
تاريخ التقرير: ${new Date().toLocaleDateString('ar-SA')}
+
+ +
+
+
إجمالي الإيرادات
+
${formatCurrency(summary.totalRevenue)}
+
+
+
أرباح المنصة
+
${formatCurrency(summary.commissionEarned)}
+
+
+
سلف الضمان
+
${formatCurrency(summary.securityDeposits)}
+
+
+
المدفوعات المعلقة
+
${formatCurrency(summary.pendingPayments)}
+
+
+ + + + + + + + + + + + + + + ${filteredTransactions.map(t => ` + + + + + + + + + + `).join('')} + +
التاريخالوصفمنإلىالمبلغالعمولةالحالة
${t.date}${t.description}${t.fromUser}${t.toUser}${formatCurrency(t.amount)}${t.commission ? formatCurrency(t.commission) : '-'}${t.status === 'completed' ? 'مكتمل' : t.status === 'pending' ? 'معلق' : 'بإنتظار الرد'}
+ + + +
+ +
+ + + `); + printWindow.document.close(); }; return (
+ +
- + 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" />
@@ -239,25 +455,63 @@ export default function LedgerBook({ userType = 'admin' }) { type="date" value={dateRange.start} 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" /> إلى 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" />
- +
+ + +
+ + {(dateRange.start || dateRange.end || searchTerm) && ( +
+
+ {filteredTransactions.length} معاملة من إجمالي {transactions.length} +
+ +
+ )}
@@ -307,14 +561,14 @@ export default function LedgerBook({ userType = 'admin' }) { {transaction.toUser}
- + {formatCurrency(transaction.amount)} {transaction.commission ? formatCurrency(transaction.commission) : '-'} -
+

لا توجد أرصدة حالياً

)} diff --git a/app/components/admin/PropertiesTable.js b/app/components/admin/PropertiesTable.js index de975a2..d35f71b 100644 --- a/app/components/admin/PropertiesTable.js +++ b/app/components/admin/PropertiesTable.js @@ -1,7 +1,7 @@ 'use client'; import { useState } from 'react'; -import { motion } from 'framer-motion'; +import { motion, AnimatePresence } from 'framer-motion'; import { Edit, Trash2, @@ -12,14 +12,404 @@ import { Square, DollarSign, Percent, - MoreVertical + MoreVertical, + X, + CheckCircle, + AlertCircle, + Calendar, + User, + Home, + Building, + Clock } from 'lucide-react'; +import toast, { Toaster } from 'react-hot-toast'; + +const DeleteConfirmationModal = ({ isOpen, onClose, onConfirm, propertyTitle }) => { + if (!isOpen) return null; + + return ( + + e.stopPropagation()} + > +
+
+ +
+

تأكيد الحذف

+

+ هل أنت متأكد من حذف العقار: "{propertyTitle}"؟ +

+

هذا الإجراء لا يمكن التراجع عنه

+
+ +
+ + +
+
+
+ ); +}; + +const PropertyViewModal = ({ property, isOpen, onClose }) => { + if (!isOpen || !property) return null; + + const formatCurrency = (amount) => { + return amount?.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",") + ' ل.س'; + }; + + return ( + + e.stopPropagation()} + > +
+
+
+

{property.title}

+

{property.location}

+
+ +
+
+ +
+
+
+ +
{property.type === 'villa' ? 'فيلا' : property.type === 'apartment' ? 'شقة' : 'بيت'}
+
نوع العقار
+
+
+ +
{formatCurrency(property.price)}
+
السعر اليومي
+
+
+ +
{property.commission}%
+
نسبة العمولة
+
+
+ +
{property.bookings || 0}
+
عدد الحجوزات
+
+
+ +
+

+ + الموقع +

+

{property.location}

+
+ +
+

المواصفات

+
+
+ +
{property.bedrooms}
+
غرف نوم
+
+
+ +
{property.bathrooms}
+
حمامات
+
+
+ +
{property.area}
+
م²
+
+
+
+ +
+

+ + معلومات العمولة +

+
+
+ +
{property.commission}%
+
+
+ +
{property.commissionType}
+
+
+ +
+ {formatCurrency((property.price * property.commission) / 100)} +
+
+
+ +
+ {property.status === 'available' ? 'متاح' : 'محجوز'} +
+
+
+
+
+
+
+ ); +}; + +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 ( + + e.stopPropagation()} + > +
+
+

تعديل العقار

+ +
+

يمكنك تعديل معلومات العقار

+
+ +
+
+
+ + 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" + /> +
+
+ + +
+
+ + 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" + /> +
+
+ + 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" + /> +
+
+ + 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" + /> +
+
+ + +
+
+ + 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" + /> +
+
+ + 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" + /> +
+
+ + 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" + /> +
+
+ + +
+
+
+ +
+ + +
+
+
+ ); +}; + +const MoreActionsMenu = ({ property, isOpen, onClose, onViewBookings, onViewReports }) => { + if (!isOpen) return null; + + return ( + <> +
+ + + + ); +}; export default function PropertiesTable() { const [properties, setProperties] = useState([ { id: 1, - title: 'luxuryVillaDamascus', + title: 'فيلا فاخرة في المزة', type: 'villa', location: 'دمشق, المزة', price: 500000, @@ -33,7 +423,7 @@ export default function PropertiesTable() { }, { id: 2, - title: 'modernApartmentAleppo', + title: 'شقة حديثة في الشهباء', type: 'apartment', location: 'حلب, الشهباء', 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) => { 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 (
+ + @@ -97,7 +534,11 @@ export default function PropertiesTable() { > - @@ -152,6 +611,26 @@ export default function PropertiesTable() {

لا توجد عقارات مضافة بعد

)} + + setViewModal({ isOpen: false, property: null })} + /> + + setEditModal({ isOpen: false, property: null })} + onSave={handleSaveEdit} + /> + + setDeleteModal({ isOpen: false, property: null })} + onConfirm={confirmDelete} + propertyTitle={deleteModal.property?.title} + /> ); } \ No newline at end of file diff --git a/app/components/admin/UsersList.js b/app/components/admin/UsersList.js index ef7713e..8085bc2 100644 --- a/app/components/admin/UsersList.js +++ b/app/components/admin/UsersList.js @@ -1,7 +1,7 @@ 'use client'; import { useState } from 'react'; -import { motion } from 'framer-motion'; +import { motion, AnimatePresence } from 'framer-motion'; import { User, Mail, @@ -11,8 +11,445 @@ import { DollarSign, Search, Filter, - Eye + Eye, + X, + CheckCircle, + XCircle, + ChevronDown, + Users, + Award, + Clock, + TrendingUp, + CalendarDays, + Shield } 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 ( + + e.stopPropagation()} + > +
+
+
+

+ + تصفية متقدمة +

+

حدد معايير التصفية المطلوبة

+
+ +
+
+ +
+
+ +
+ {identityTypes.map((type) => ( + + ))} +
+
+ +
+ +
+ setLocalFilters({...localFilters, minBookings: e.target.value})} + className="px-4 py-2 border border-gray-300 rounded-xl focus:ring-2 focus:ring-blue-500" + /> + setLocalFilters({...localFilters, maxBookings: e.target.value})} + className="px-4 py-2 border border-gray-300 rounded-xl focus:ring-2 focus:ring-blue-500" + /> +
+
+ {bookingRanges.slice(1).map((range) => ( + + ))} +
+
+ +
+ +
+ setLocalFilters({...localFilters, minSpending: e.target.value})} + className="px-4 py-2 border border-gray-300 rounded-xl focus:ring-2 focus:ring-blue-500" + /> + setLocalFilters({...localFilters, maxSpending: e.target.value})} + className="px-4 py-2 border border-gray-300 rounded-xl focus:ring-2 focus:ring-blue-500" + /> +
+
+ {spendingRanges.slice(1).map((range) => ( + + ))} +
+
+ +
+ +
+ {dateRanges.map((range) => ( + + ))} +
+
+ +
+ + +
+
+ +
+ + +
+
+
+ ); +}; + +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 ( + + e.stopPropagation()} + > +
+
+
+

+ + تفاصيل المستخدم +

+

{user.name}

+
+ +
+
+ +
+
+
+

+ + معلومات شخصية +

+
+
+ الاسم الكامل: + {user.name} +
+
+ البريد الإلكتروني: + {user.email} +
+
+ رقم الهاتف: + {user.phone} +
+
+ تاريخ التسجيل: + {user.joinDate} +
+
+
+ +
+

+ + معلومات الهوية +

+
+
+ نوع الهوية: + + {user.identityType === 'syrian' ? 'هوية سورية' : 'جواز سفر'} + +
+
+ رقم الهوية: + {user.identityNumber} +
+
+
+
+ +
+
+
{user.totalBookings}
+
إجمالي الحجوزات
+
+
+
{user.activeBookings}
+
حجوزات نشطة
+
+
+
{formatCurrency(user.totalSpent)}
+
إجمالي المنصرف
+
+
+ +
+

+ + سجل الحجوزات +

+
+ {userBookings.map((booking) => ( +
+
+

{booking.property}

+
+ + {booking.startDate} - {booking.endDate} +
+
+
+
+
{formatCurrency(booking.amount)}
+
المبلغ الإجمالي
+
+ + {booking.status === 'completed' ? 'مكتمل' : 'مؤكد'} + +
+
+ ))} + + {userBookings.length === 0 && ( +
+ +

لا توجد حجوزات سابقة

+
+ )} +
+
+
+ +
+
+
+
+ ); +}; export default function UsersList() { const [users, setUsers] = useState([ @@ -44,30 +481,194 @@ export default function UsersList() { const [searchTerm, setSearchTerm] = useState(''); 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 => - user.name.includes(searchTerm) || - user.email.includes(searchTerm) || - user.phone.includes(searchTerm) - ); + const applyFilters = (newFilters) => { + setFilters(newFilters); + }; + + 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 (
-
+ + +
- + 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" />
- +
+ + {filterStats.filtered && ( + + )} +
+
+ + {getActiveFiltersCount() > 0 && ( +
+ الفلاتر النشطة: + {filters.identityType !== 'all' && ( + + {filters.identityType === 'syrian' ? 'هوية سورية' : 'جواز سفر'} + + )} + {(filters.minBookings || filters.maxBookings) && ( + + الحجوزات: {filters.minBookings || '0'} - {filters.maxBookings || '∞'} + + )} + {(filters.minSpending || filters.maxSpending) && ( + + الإنفاق: {parseInt(filters.minSpending || 0).toLocaleString()} - {parseInt(filters.maxSpending || '∞').toLocaleString()} ل.س + + )} + {filters.dateRange !== 'all' && ( + + {filters.dateRange === 'today' ? 'اليوم' : + filters.dateRange === 'week' ? 'آخر 7 أيام' : + filters.dateRange === 'month' ? 'آخر 30 يوم' : 'آخر 12 شهر'} + + )} + {filters.activeOnly && ( + + لديهم حجوزات نشطة + + )} + {filters.inactiveOnly && ( + + بدون حجوزات نشطة + + )} +
+ )} + +
+
+ عرض {filteredUsers.length} مستخدم + {filterStats.filtered && ( + (من {users.length}) + )} +
@@ -76,40 +677,46 @@ export default function UsersList() { key={user.id} initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} - transition={{ delay: index * 0.1 }} - className="bg-white border rounded-lg p-4 hover:shadow-md transition-shadow" + transition={{ delay: index * 0.05 }} + className="bg-white border border-gray-200 rounded-xl p-5 hover:shadow-md transition-all" >
-
- +
+ {user.name.charAt(0).toUpperCase()}
-

{user.name}

-
+

{user.name}

+
- + {user.email}
- + {user.phone}
+
+ + {user.identityType === 'syrian' ? 'هوية سورية' : 'جواز سفر'} +
-
-
-
{user.totalBookings}
+
+
+
{user.totalBookings}
إجمالي الحجوزات
-
-
{user.activeBookings}
+
+
0 ? 'text-green-600' : 'text-gray-400'}`}> + {user.activeBookings} +
حجوزات نشطة
-
-
+
+
{user.totalSpent.toLocaleString()}
إجمالي المنصرف
@@ -118,7 +725,7 @@ export default function UsersList() {
- {selectedUser && ( -
- -
-

تفاصيل المستخدم

- -
- -
-
-
- -
{selectedUser.name}
-
-
- -
{selectedUser.email}
-
-
- -
{selectedUser.phone}
-
-
- -
- {selectedUser.identityType === 'syrian' ? 'هوية سورية' : 'جواز سفر'} -
-
-
- -
{selectedUser.identityNumber}
-
-
- -
{selectedUser.joinDate}
-
-
- -
-

سجل الحجوزات

-

- لا توجد حجوزات سابقة -

-
-
-
-
+ {filteredUsers.length === 0 && ( + + +

لا توجد نتائج

+

لا يوجد مستخدمون يطابقون معايير البحث

+ {(searchTerm || getActiveFiltersCount() > 0) && ( + + )} +
)} + + setShowFilterDialog(false)} + filters={filters} + onApplyFilters={applyFilters} + onResetFilters={resetFilters} + /> + + setSelectedUser(null)} + />
); } \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index e94ee17..c8319d8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,8 +12,10 @@ "flowbite": "^4.0.1", "flowbite-react": "^0.12.16", "framer-motion": "^12.29.2", + "html2canvas": "^1.4.1", "i18next": "^25.8.0", "i18next-browser-languagedetector": "^8.2.0", + "jspdf": "^4.2.1", "leaflet": "^1.9.4", "lucide-react": "^0.563.0", "next": "16.1.6", @@ -22,7 +24,8 @@ "react-hot-toast": "^2.6.0", "react-i18next": "^16.5.4", "react-intersection-observer": "^10.0.3", - "react-leaflet": "^4.2.1" + "react-leaflet": "^4.2.1", + "xlsx": "^0.18.5" }, "devDependencies": { "@tailwindcss/postcss": "^4", @@ -1199,12 +1202,32 @@ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", "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": { "version": "1.20.2", "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==", "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": { "version": "2.1.29", "resolved": "https://registry.npmjs.org/@types/yandex-maps/-/yandex-maps-2.1.29.tgz", @@ -1306,6 +1329,15 @@ "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": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/array-timsort/-/array-timsort-1.0.3.tgz", @@ -1377,6 +1409,15 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "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": { "version": "2.9.19", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz", @@ -1461,6 +1502,39 @@ ], "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": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", @@ -1488,6 +1562,15 @@ "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", "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": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/comment-json/-/comment-json-4.4.1.tgz", @@ -1502,12 +1585,45 @@ "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": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", "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": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", @@ -1580,6 +1696,16 @@ "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": { "version": "1.5.279", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.279.tgz", @@ -1657,6 +1783,17 @@ "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": { "version": "1.20.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", @@ -1666,6 +1803,12 @@ "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": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -1731,6 +1874,15 @@ "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": { "version": "5.3.4", "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", @@ -1829,6 +1981,19 @@ "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": { "version": "25.8.0", "resolved": "https://registry.npmjs.org/i18next/-/i18next-25.8.0.tgz", @@ -1869,6 +2034,12 @@ "@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": { "version": "2.16.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", @@ -1935,6 +2106,23 @@ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", "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": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/klona/-/klona-2.0.6.tgz", @@ -2420,12 +2608,25 @@ "integrity": "sha512-uBj69dVlYe/+wxj8JOpr97XfsxH/eumMt6HqjNTmJDf/6NO9s+0uxeOneIz3AsPt2m6y9PqzDzd3ATcU17MNfw==", "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": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "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": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -2499,6 +2700,16 @@ ], "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": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", @@ -2626,6 +2837,13 @@ "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": { "version": "1.22.11", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", @@ -2656,6 +2874,16 @@ "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": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -2763,6 +2991,28 @@ "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": { "version": "5.1.6", "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", @@ -2798,6 +3048,16 @@ "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": { "version": "6.4.0", "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.4.0.tgz", @@ -2845,6 +3105,15 @@ "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": { "version": "1.3.3", "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" } }, + "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": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", @@ -2943,6 +3221,45 @@ "engines": { "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" + } } } } diff --git a/package.json b/package.json index 0c0f2c0..a662f8b 100644 --- a/package.json +++ b/package.json @@ -12,8 +12,10 @@ "flowbite": "^4.0.1", "flowbite-react": "^0.12.16", "framer-motion": "^12.29.2", + "html2canvas": "^1.4.1", "i18next": "^25.8.0", "i18next-browser-languagedetector": "^8.2.0", + "jspdf": "^4.2.1", "leaflet": "^1.9.4", "lucide-react": "^0.563.0", "next": "16.1.6", @@ -22,7 +24,8 @@ "react-hot-toast": "^2.6.0", "react-i18next": "^16.5.4", "react-intersection-observer": "^10.0.3", - "react-leaflet": "^4.2.1" + "react-leaflet": "^4.2.1", + "xlsx": "^0.18.5" }, "devDependencies": { "@tailwindcss/postcss": "^4",
{property.title}
-
{property.type}
+
+ {property.type === 'villa' ? 'فيلا' : + property.type === 'apartment' ? 'شقة' : + property.type === 'house' ? 'بيت' : 'استوديو'} +
@@ -125,20 +566,38 @@ export default function PropertiesTable() {
{getStatusBadge(property.status)} +
- - - - + {moreMenu.isOpen && moreMenu.property?.id === property.id && ( + setMoreMenu({ isOpen: false, property: null, anchorEl: null })} + onViewBookings={handleViewBookings} + onViewReports={handleViewReports} + /> + )}