Edit Admin

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

View File

@ -1,6 +1,6 @@
'use client';
import { useState } from 'react';
import { useState, useRef } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import {
CheckCircle,
@ -22,8 +22,12 @@ import {
FileText,
Download,
Printer,
History
History,
Loader2
} from 'lucide-react';
import jsPDF from 'jspdf';
import html2canvas from 'html2canvas';
import toast, { Toaster } from 'react-hot-toast';
const ReasonDialog = ({ isOpen, onClose, onConfirm, title, defaultReason = '' }) => {
const [reason, setReason] = useState(defaultReason);
@ -108,6 +112,427 @@ const ReasonDialog = ({ isOpen, onClose, onConfirm, title, defaultReason = '' })
);
};
const PDFExportButton = ({ request, onExportComplete }) => {
const [isExporting, setIsExporting] = useState(false);
const formatCurrency = (amount) => {
return amount?.toLocaleString() + ' ل.س';
};
const generatePDF = async () => {
if (!request) {
toast.error('لا توجد بيانات للتصدير');
return;
}
setIsExporting(true);
const loadingToast = toast.loading('جاري إنشاء ملف PDF...', { id: 'pdf-export' });
try {
const printContent = document.createElement('div');
printContent.style.width = '800px';
printContent.style.padding = '40px';
printContent.style.backgroundColor = 'white';
printContent.style.fontFamily = 'Cairo, Arial, sans-serif';
printContent.style.direction = 'rtl';
printContent.style.position = 'absolute';
printContent.style.left = '-9999px';
printContent.style.top = '-9999px';
printContent.style.zIndex = '-9999';
printContent.innerHTML = `
<!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 }) => {
if (!isOpen || !request) return null;
@ -130,16 +555,19 @@ const RequestDetailsDialog = ({ request, isOpen, onClose }) => {
className="bg-white rounded-2xl w-full max-w-3xl max-h-[90vh] overflow-y-auto shadow-2xl"
onClick={(e) => e.stopPropagation()}
>
<div className="sticky top-0 bg-white border-b p-4 flex justify-between items-center">
<h2 className="text-xl font-bold flex items-center gap-2">
<FileText className="w-5 h-5 text-blue-600" />
تفاصيل الطلب #{request.id}
</h2>
<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">
<FileText className="w-5 h-5" />
تفاصيل الطلب #{request.id}
</h2>
<p className="text-amber-100 text-sm mt-1">جميع معلومات الطلب في مكان واحد</p>
</div>
<button
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>
</div>
@ -149,7 +577,7 @@ const RequestDetailsDialog = ({ request, isOpen, onClose }) => {
<User className="w-4 h-4" />
معلومات المستأجر
</h3>
<div className="grid grid-cols-2 gap-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="text-xs text-gray-500">الاسم الكامل</label>
<div className="font-medium">{request.user}</div>
@ -183,7 +611,7 @@ const RequestDetailsDialog = ({ request, isOpen, onClose }) => {
<Home className="w-4 h-4" />
معلومات العقار
</h3>
<div className="grid grid-cols-2 gap-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="text-xs text-gray-500">العقار</label>
<div className="font-medium">{request.property}</div>
@ -200,7 +628,7 @@ const RequestDetailsDialog = ({ request, isOpen, onClose }) => {
<Calendar className="w-4 h-4" />
تفاصيل الحجز
</h3>
<div className="grid grid-cols-2 gap-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="text-xs text-gray-500">تاريخ البداية</label>
<div className="font-medium">{request.startDate}</div>
@ -225,7 +653,7 @@ const RequestDetailsDialog = ({ request, isOpen, onClose }) => {
<DollarSign className="w-4 h-4" />
المعلومات المالية
</h3>
<div className="grid grid-cols-2 gap-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="text-xs text-gray-500">سلفة الضمان</label>
<div className="font-medium text-blue-600">{formatCurrency(request.securityDeposit)}</div>
@ -245,7 +673,7 @@ const RequestDetailsDialog = ({ request, isOpen, onClose }) => {
</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">
<History className="w-4 h-4" />
سجل الإجراءات
@ -274,7 +702,7 @@ const RequestDetailsDialog = ({ request, isOpen, onClose }) => {
</div>
)}
{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>
</div>
)}
@ -282,7 +710,7 @@ const RequestDetailsDialog = ({ request, isOpen, onClose }) => {
</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
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"
@ -290,12 +718,8 @@ const RequestDetailsDialog = ({ request, isOpen, onClose }) => {
<Printer className="w-5 h-5" />
طباعة التفاصيل
</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"
>
<Download className="w-5 h-5" />
تصدير PDF
</button>
<PDFExportButton request={request} />
</div>
</motion.div>
</motion.div>
@ -332,12 +756,12 @@ const RequestCard = ({ request, onAction, onViewDetails }) => {
};
const labels = {
pending: ' بانتظار الموافقة',
owner_approved: ' موافقة المالك',
admin_approved: ' موافقة الإدارة',
active: ' إيجار نشط',
completed: ' منتهي',
rejected: ' مرفوض'
pending: 'بانتظار الموافقة',
owner_approved: 'موافقة المالك',
admin_approved: 'موافقة الإدارة',
active: 'إيجار نشط',
completed: 'منتهي',
rejected: 'مرفوض'
};
return (
@ -840,6 +1264,9 @@ export default function BookingRequests() {
return (
<div className="space-y-6">
<Toaster position="top-center" reverseOrder={false} />
{/* إحصائيات سريعة */}
<div className="grid grid-cols-4 gap-4">
<motion.div
initial={{ opacity: 0, y: 20 }}
@ -913,6 +1340,7 @@ export default function BookingRequests() {
key={request.id}
request={request}
onAction={handleAction}
onViewDetails={() => setDetailsDialog({ isOpen: true, request })}
/>
))}
</div>