Files
SweetHome/app/components/admin/LedgerBook.js

607 lines
22 KiB
JavaScript
Raw Normal View History

'use client';
import { useState, useEffect } from 'react';
import { motion } from 'framer-motion';
import {
DollarSign,
Calendar,
User,
Home,
Download,
Filter,
Search,
TrendingUp,
TrendingDown,
Wallet,
2026-03-27 00:34:59 +03:00
Shield,
FileText,
Printer,
X,
CheckCircle
} from 'lucide-react';
import { formatCurrency } from '@/app/utils/calculations';
2026-03-27 00:34:59 +03:00
import toast, { Toaster } from 'react-hot-toast';
import * as XLSX from 'xlsx';
export default function LedgerBook({ userType = 'admin' }) {
const [transactions, setTransactions] = useState([]);
const [filteredTransactions, setFilteredTransactions] = useState([]);
const [dateRange, setDateRange] = useState({ start: '', end: '' });
const [searchTerm, setSearchTerm] = useState('');
const [summary, setSummary] = useState({
totalRevenue: 0,
pendingPayments: 0,
securityDeposits: 0,
commissionEarned: 0
});
2026-03-27 00:34:59 +03:00
const [isExporting, setIsExporting] = useState(false);
useEffect(() => {
loadTransactions();
}, []);
useEffect(() => {
filterTransactions();
calculateSummary();
}, [transactions, dateRange, searchTerm]);
const loadTransactions = async () => {
const mockTransactions = [
{
id: 'T001',
date: '2024-02-20',
type: 'rent_payment',
description: 'دفعة إيجار - فيلا في دمشق',
amount: 500000,
commission: 25000,
fromUser: 'أحمد محمد',
toUser: 'مالك العقار',
propertyId: 1,
propertyName: 'luxuryVillaDamascus',
status: 'completed',
paymentMethod: 'cash'
},
{
id: 'T002',
date: '2024-02-19',
type: 'security_deposit',
description: 'سلفة ضمان - شقة في حلب',
amount: 250000,
commission: 0,
fromUser: 'سارة أحمد',
toUser: 'مالك العقار',
propertyId: 2,
propertyName: 'modernApartmentAleppo',
status: 'pending_refund',
paymentMethod: 'cash'
},
{
id: 'T003',
date: '2024-02-18',
type: 'commission',
description: 'عمولة منصة - فيلا في درعا',
amount: 30000,
commission: 30000,
fromUser: 'محمد الحلبي',
toUser: 'المنصة',
propertyId: 5,
propertyName: 'villaDaraa',
status: 'completed',
paymentMethod: 'cash'
}
];
setTransactions(mockTransactions);
};
const filterTransactions = () => {
let filtered = [...transactions];
if (dateRange.start && dateRange.end) {
filtered = filtered.filter(t =>
t.date >= dateRange.start && t.date <= dateRange.end
);
}
if (searchTerm) {
filtered = filtered.filter(t =>
t.description.includes(searchTerm) ||
t.fromUser.includes(searchTerm) ||
t.toUser.includes(searchTerm)
);
}
setFilteredTransactions(filtered);
};
const calculateSummary = () => {
const summary = filteredTransactions.reduce((acc, t) => {
if (t.type === 'rent_payment' || t.type === 'commission') {
acc.totalRevenue += t.amount;
}
if (t.type === 'security_deposit' && t.status === 'pending_refund') {
acc.securityDeposits += t.amount;
}
if (t.commission) {
acc.commissionEarned += t.commission;
}
if (t.status === 'pending') {
acc.pendingPayments += t.amount;
}
return acc;
}, {
totalRevenue: 0,
pendingPayments: 0,
securityDeposits: 0,
commissionEarned: 0
});
setSummary(summary);
};
const getTransactionIcon = (type) => {
switch(type) {
case 'rent_payment':
return <Home className="w-4 h-4 text-blue-600" />;
case 'security_deposit':
return <Shield className="w-4 h-4 text-green-600" />;
case 'commission':
return <TrendingUp className="w-4 h-4 text-amber-600" />;
default:
return <DollarSign className="w-4 h-4" />;
}
};
2026-03-27 00:34:59 +03:00
const exportToExcel = async () => {
if (filteredTransactions.length === 0) {
toast.error('لا توجد معاملات للتصدير');
return;
}
setIsExporting(true);
toast.loading('جاري تصدير البيانات...', { id: 'export' });
try {
const exportData = filteredTransactions.map(t => ({
'رقم العملية': t.id,
'التاريخ': t.date,
'نوع العملية': t.type === 'rent_payment' ? 'دفعة إيجار' :
t.type === 'security_deposit' ? 'سلفة ضمان' :
t.type === 'commission' ? 'عمولة' : 'أخرى',
'الوصف': t.description,
'من': t.fromUser,
'إلى': t.toUser,
'المبلغ (ل.س)': t.amount,
'العمولة (ل.س)': t.commission || 0,
'الحالة': t.status === 'completed' ? 'مكتمل' :
t.status === 'pending' ? 'معلق' :
t.status === 'pending_refund' ? 'بإنتظار الاسترداد' : 'مؤكد',
}));
const summaryRow = {
'رقم العملية': '',
'التاريخ': '',
'نوع العملية': '',
'الوصف': '',
'من': '',
'إلى': '',
'المبلغ (ل.س)': summary.totalRevenue,
'العمولة (ل.س)': summary.commissionEarned,
'الحالة': ''
};
exportData.push(summaryRow);
const worksheet = XLSX.utils.json_to_sheet(exportData);
const columnWidths = [
{ wch: 12 }, // رقم العملية
{ wch: 12 }, // التاريخ
{ wch: 12 }, // نوع العملية
{ wch: 30 }, // الوصف
{ wch: 20 }, // من
{ wch: 20 }, // إلى
{ wch: 15 }, // المبلغ
{ wch: 15 }, // العمولة
{ wch: 12 }, // الحالة
];
worksheet['!cols'] = columnWidths;
const range = XLSX.utils.decode_range(worksheet['!ref']);
for (let C = range.s.c; C <= range.e.c; ++C) {
const address = XLSX.utils.encode_col(C) + '1';
if (!worksheet[address]) continue;
worksheet[address].s = {
font: { bold: true, sz: 12 },
fill: { fgColor: { rgb: "F59E0B" } },
alignment: { horizontal: "center", vertical: "center" }
};
}
2026-03-27 00:34:59 +03:00
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 (
<div className="space-y-6">
2026-03-27 00:34:59 +03:00
<Toaster position="top-center" reverseOrder={false} />
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="bg-gradient-to-br from-blue-600 to-blue-700 text-white rounded-xl p-5"
>
<div className="flex items-center justify-between mb-3">
<Wallet className="w-8 h-8 opacity-80" />
<span className="text-sm opacity-90">إجمالي الإيرادات</span>
</div>
<div className="text-2xl font-bold">{formatCurrency(summary.totalRevenue)}</div>
</motion.div>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.1 }}
className="bg-gradient-to-br from-amber-600 to-amber-700 text-white rounded-xl p-5"
>
<div className="flex items-center justify-between mb-3">
<TrendingUp className="w-8 h-8 opacity-80" />
<span className="text-sm opacity-90">أرباح المنصة</span>
</div>
<div className="text-2xl font-bold">{formatCurrency(summary.commissionEarned)}</div>
</motion.div>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.2 }}
className="bg-gradient-to-br from-green-600 to-green-700 text-white rounded-xl p-5"
>
<div className="flex items-center justify-between mb-3">
<Shield className="w-8 h-8 opacity-80" />
<span className="text-sm opacity-90">سلف الضمان</span>
</div>
<div className="text-2xl font-bold">{formatCurrency(summary.securityDeposits)}</div>
</motion.div>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.3 }}
className="bg-gradient-to-br from-red-600 to-red-700 text-white rounded-xl p-5"
>
<div className="flex items-center justify-between mb-3">
<TrendingDown className="w-8 h-8 opacity-80" />
<span className="text-sm opacity-90">المدفوعات المعلقة</span>
</div>
<div className="text-2xl font-bold">{formatCurrency(summary.pendingPayments)}</div>
</motion.div>
</div>
<div className="bg-white rounded-xl p-5 shadow-sm border">
<div className="flex flex-col md:flex-row gap-4">
<div className="flex-1 relative">
2026-03-27 00:34:59 +03:00
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-5 h-5 text-gray-400" />
<input
type="text"
placeholder="بحث في المعاملات..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
2026-03-27 00:34:59 +03:00
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 className="flex gap-2">
<input
type="date"
value={dateRange.start}
onChange={(e) => setDateRange({...dateRange, start: e.target.value})}
2026-03-27 00:34:59 +03:00
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>
<input
type="date"
value={dateRange.end}
onChange={(e) => setDateRange({...dateRange, end: e.target.value})}
2026-03-27 00:34:59 +03:00
className="px-4 py-3 border border-gray-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
2026-03-27 00:34:59 +03:00
<div className="flex gap-2">
<button
onClick={exportToExcel}
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"
>
{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>
</div>
</div>
2026-03-27 00:34:59 +03:00
{(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="overflow-x-auto">
<table className="w-full">
<thead className="bg-gray-50 border-b">
<tr>
<th className="px-6 py-4 text-right text-sm font-semibold text-gray-900">التاريخ</th>
<th className="px-6 py-4 text-right text-sm font-semibold text-gray-900">الوصف</th>
<th className="px-6 py-4 text-right text-sm font-semibold text-gray-900">من</th>
<th className="px-6 py-4 text-right text-sm font-semibold text-gray-900">إلى</th>
<th className="px-6 py-4 text-right text-sm font-semibold text-gray-900">المبلغ</th>
<th className="px-6 py-4 text-right text-sm font-semibold text-gray-900">العمولة</th>
<th className="px-6 py-4 text-right text-sm font-semibold text-gray-900">الحالة</th>
</tr>
</thead>
<tbody className="divide-y">
{filteredTransactions.map((transaction, index) => (
<motion.tr
key={transaction.id}
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: index * 0.05 }}
className="hover:bg-gray-50"
>
<td className="px-6 py-4 text-sm">
<div className="flex items-center gap-2">
<Calendar className="w-4 h-4 text-gray-400" />
{transaction.date}
</div>
</td>
<td className="px-6 py-4">
<div className="flex items-center gap-2">
{getTransactionIcon(transaction.type)}
<span className="text-sm font-medium">{transaction.description}</span>
</div>
</td>
<td className="px-6 py-4">
<div className="flex items-center gap-2">
<User className="w-4 h-4 text-gray-400" />
<span className="text-sm">{transaction.fromUser}</span>
</div>
</td>
<td className="px-6 py-4">
<div className="flex items-center gap-2">
<User className="w-4 h-4 text-gray-400" />
<span className="text-sm">{transaction.toUser}</span>
</div>
</td>
2026-03-27 00:34:59 +03:00
<td className="px-6 py-4 text-sm font-bold text-green-600">
{formatCurrency(transaction.amount)}
</td>
<td className="px-6 py-4 text-sm text-amber-600">
{transaction.commission ? formatCurrency(transaction.commission) : '-'}
</td>
<td className="px-6 py-4">
2026-03-27 00:34:59 +03:00
<span className={`px-2 py-1 rounded-full text-xs font-medium ${
transaction.status === 'completed' ? 'bg-green-100 text-green-800' :
transaction.status === 'pending' ? 'bg-yellow-100 text-yellow-800' :
'bg-blue-100 text-blue-800'
}`}>
{transaction.status === 'completed' ? 'مكتمل' :
transaction.status === 'pending' ? 'معلق' : 'بإنتظار الرد'}
</span>
</td>
</motion.tr>
))}
</tbody>
</table>
</div>
{filteredTransactions.length === 0 && (
<div className="text-center py-12">
<Wallet className="w-12 h-12 text-gray-300 mx-auto mb-3" />
<p className="text-gray-500">لا توجد معاملات في هذه الفترة</p>
</div>
)}
</div>
{userType === 'owner' && (
<div className="bg-blue-50 rounded-xl p-5">
<h3 className="font-bold mb-4 flex items-center gap-2">
<User className="w-5 h-5" />
أرصدة المستأجرين
</h3>
<div className="space-y-3">
2026-03-27 00:34:59 +03:00
<p className="text-gray-500 text-sm">لا توجد أرصدة حالياً</p>
</div>
</div>
)}
</div>
);
}