Compare commits
2 Commits
ac1241583b
...
082f20da40
| Author | SHA1 | Date | |
|---|---|---|---|
| 082f20da40 | |||
| 157188d2e6 |
@ -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 (
|
||||||
|
|||||||
@ -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) => {
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -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
319
package-lock.json
generated
@ -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"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
Reference in New Issue
Block a user