car_mms/app/routes/financial-reports.tsx
2025-09-11 14:22:27 +03:00

376 lines
21 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import type { LoaderFunctionArgs } from "@remix-run/node";
import { json } from "@remix-run/node";
import { useLoaderData, useSearchParams } from "@remix-run/react";
import { requireAuth } from "~/lib/auth-middleware.server";
import {
getFinancialSummary,
getMonthlyFinancialData,
getIncomeByMaintenanceType,
getExpenseBreakdown,
getTopCustomersByRevenue,
getFinancialTrends
} from "~/lib/financial-reporting.server";
import { DashboardLayout } from "~/components/layout/DashboardLayout";
import { Button } from "~/components/ui/Button";
import { Input } from "~/components/ui/Input";
import { useSettings } from "~/contexts/SettingsContext";
export async function loader({ request }: LoaderFunctionArgs) {
const user = await requireAuth(request, 2); // Admin level required
const url = new URL(request.url);
const dateFrom = url.searchParams.get("dateFrom")
? new Date(url.searchParams.get("dateFrom")!)
: undefined;
const dateTo = url.searchParams.get("dateTo")
? new Date(url.searchParams.get("dateTo")!)
: undefined;
// Get all financial data
const [
financialSummary,
monthlyData,
incomeByType,
expenseBreakdown,
topCustomers,
trends
] = await Promise.all([
getFinancialSummary(dateFrom, dateTo),
getMonthlyFinancialData(),
getIncomeByMaintenanceType(dateFrom, dateTo),
getExpenseBreakdown(dateFrom, dateTo),
getTopCustomersByRevenue(10, dateFrom, dateTo),
dateFrom && dateTo ? getFinancialTrends(dateFrom, dateTo) : null,
]);
return json({
user,
financialSummary,
monthlyData,
incomeByType,
expenseBreakdown,
topCustomers,
trends,
dateFrom: dateFrom?.toISOString().split('T')[0] || "",
dateTo: dateTo?.toISOString().split('T')[0] || "",
});
}
export default function FinancialReportsPage() {
const { formatCurrency } = useSettings();
const {
user,
financialSummary,
monthlyData,
incomeByType,
expenseBreakdown,
topCustomers,
trends,
dateFrom,
dateTo
} = useLoaderData<typeof loader>();
const [searchParams, setSearchParams] = useSearchParams();
const handleDateFilter = (type: string, value: string) => {
const newParams = new URLSearchParams(searchParams);
if (value) {
newParams.set(type, value);
} else {
newParams.delete(type);
}
setSearchParams(newParams);
};
const clearFilters = () => {
setSearchParams({});
};
return (
<DashboardLayout user={user}>
<div className="space-y-6">
<div className="flex justify-between items-center">
<div>
<h1 className="text-2xl font-bold text-gray-900">التقارير المالية</h1>
<p className="text-gray-600">تحليل شامل للوضع المالي للمؤسسة</p>
</div>
</div>
{/* Date Filters */}
<div className="bg-white p-4 rounded-lg shadow">
<div className="flex flex-wrap gap-4 items-end">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
من تاريخ
</label>
<Input
type="date"
value={dateFrom}
onChange={(e) => handleDateFilter("dateFrom", e.target.value)}
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
إلى تاريخ
</label>
<Input
type="date"
value={dateTo}
onChange={(e) => handleDateFilter("dateTo", e.target.value)}
/>
</div>
<Button
variant="outline"
onClick={clearFilters}
>
مسح الفلاتر
</Button>
</div>
</div>
{/* Financial Summary Cards */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<div className="bg-white p-6 rounded-lg shadow">
<div className="flex items-center">
<div className="flex-1">
<p className="text-sm font-medium text-gray-600">إجمالي الإيرادات</p>
<p className="text-2xl font-bold text-green-600">
{formatCurrency(financialSummary.totalIncome)}
</p>
<p className="text-sm text-gray-500">
{financialSummary.incomeCount} عملية
</p>
</div>
<div className="w-12 h-12 bg-green-100 rounded-lg flex items-center justify-center">
<svg className="w-6 h-6 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1" />
</svg>
</div>
</div>
</div>
<div className="bg-white p-6 rounded-lg shadow">
<div className="flex items-center">
<div className="flex-1">
<p className="text-sm font-medium text-gray-600">إجمالي المصروفات</p>
<p className="text-2xl font-bold text-red-600">
{formatCurrency(financialSummary.totalExpenses)}
</p>
<p className="text-sm text-gray-500">
{financialSummary.expenseCount} مصروف
</p>
</div>
<div className="w-12 h-12 bg-red-100 rounded-lg flex items-center justify-center">
<svg className="w-6 h-6 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 9V7a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2m2 4h10a2 2 0 002-2v-6a2 2 0 00-2-2H9a2 2 0 00-2 2v6a2 2 0 002 2zm7-5a2 2 0 11-4 0 2 2 0 014 0z" />
</svg>
</div>
</div>
</div>
<div className="bg-white p-6 rounded-lg shadow">
<div className="flex items-center">
<div className="flex-1">
<p className="text-sm font-medium text-gray-600">صافي الربح</p>
<p className={`text-2xl font-bold ${financialSummary.netProfit >= 0 ? 'text-green-600' : 'text-red-600'}`}>
{formatCurrency(financialSummary.netProfit)}
</p>
<p className="text-sm text-gray-500">
هامش الربح: {financialSummary.profitMargin.toFixed(1)}%
</p>
</div>
<div className={`w-12 h-12 rounded-lg flex items-center justify-center ${
financialSummary.netProfit >= 0 ? 'bg-green-100' : 'bg-red-100'
}`}>
<svg className={`w-6 h-6 ${financialSummary.netProfit >= 0 ? 'text-green-600' : 'text-red-600'}`} fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
</svg>
</div>
</div>
</div>
<div className="bg-white p-6 rounded-lg shadow">
<div className="flex items-center">
<div className="flex-1">
<p className="text-sm font-medium text-gray-600">متوسط الإيراد الشهري</p>
<p className="text-2xl font-bold text-blue-600">
{formatCurrency(monthlyData.reduce((sum, month) => sum + month.income, 0) / Math.max(monthlyData.length, 1))}
</p>
<p className="text-sm text-gray-500">
آخر 12 شهر
</p>
</div>
<div className="w-12 h-12 bg-blue-100 rounded-lg flex items-center justify-center">
<svg className="w-6 h-6 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6" />
</svg>
</div>
</div>
</div>
</div>
{/* Trends (if date range is selected) */}
{trends && (
<div className="bg-white p-6 rounded-lg shadow">
<h2 className="text-lg font-semibold text-gray-900 mb-4">مقارنة الفترات</h2>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div className="text-center">
<p className="text-sm text-gray-600">نمو الإيرادات</p>
<p className={`text-2xl font-bold ${trends.trends.incomeGrowth >= 0 ? 'text-green-600' : 'text-red-600'}`}>
{trends.trends.incomeGrowth >= 0 ? '+' : ''}{trends.trends.incomeGrowth.toFixed(1)}%
</p>
</div>
<div className="text-center">
<p className="text-sm text-gray-600">نمو المصروفات</p>
<p className={`text-2xl font-bold ${trends.trends.expenseGrowth <= 0 ? 'text-green-600' : 'text-red-600'}`}>
{trends.trends.expenseGrowth >= 0 ? '+' : ''}{trends.trends.expenseGrowth.toFixed(1)}%
</p>
</div>
<div className="text-center">
<p className="text-sm text-gray-600">نمو الأرباح</p>
<p className={`text-2xl font-bold ${trends.trends.profitGrowth >= 0 ? 'text-green-600' : 'text-red-600'}`}>
{trends.trends.profitGrowth >= 0 ? '+' : ''}{trends.trends.profitGrowth.toFixed(1)}%
</p>
</div>
</div>
</div>
)}
{/* Charts and Breakdowns */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Income by Maintenance Type */}
<div className="bg-white p-6 rounded-lg shadow">
<h2 className="text-lg font-semibold text-gray-900 mb-4">الإيرادات حسب نوع الصيانة</h2>
<div className="space-y-3">
{incomeByType.map((item, index) => (
<div key={index} className="flex items-center justify-between">
<div className="flex-1">
<div className="flex justify-between items-center mb-1">
<span className="text-sm font-medium text-gray-700">
{item.category}
</span>
<span className="text-sm text-gray-500">
{item.percentage.toFixed(1)}%
</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-2">
<div
className="bg-blue-600 h-2 rounded-full"
style={{ width: `${item.percentage}%` }}
></div>
</div>
<div className="flex justify-between items-center mt-1">
<span className="text-sm font-semibold text-gray-900">
{formatCurrency(item.amount)}
</span>
<span className="text-xs text-gray-500">
{item.count} عملية
</span>
</div>
</div>
</div>
))}
</div>
</div>
{/* Expense Breakdown */}
<div className="bg-white p-6 rounded-lg shadow">
<h2 className="text-lg font-semibold text-gray-900 mb-4">تفصيل المصروفات</h2>
<div className="space-y-3">
{expenseBreakdown.map((item, index) => (
<div key={index} className="flex items-center justify-between">
<div className="flex-1">
<div className="flex justify-between items-center mb-1">
<span className="text-sm font-medium text-gray-700">
{item.category}
</span>
<span className="text-sm text-gray-500">
{item.percentage.toFixed(1)}%
</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-2">
<div
className="bg-red-600 h-2 rounded-full"
style={{ width: `${item.percentage}%` }}
></div>
</div>
<div className="flex justify-between items-center mt-1">
<span className="text-sm font-semibold text-gray-900">
{formatCurrency(item.amount)}
</span>
<span className="text-xs text-gray-500">
{item.count} مصروف
</span>
</div>
</div>
</div>
))}
</div>
</div>
</div>
{/* Top Customers and Monthly Data */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Top Customers */}
<div className="bg-white p-6 rounded-lg shadow">
<h2 className="text-lg font-semibold text-gray-900 mb-4">أفضل العملاء</h2>
<div className="space-y-3">
{topCustomers.map((customer, index) => (
<div key={customer.customerId} className="flex items-center justify-between p-3 bg-gray-50 rounded-lg">
<div className="flex items-center space-x-3 space-x-reverse">
<div className="w-8 h-8 bg-blue-100 rounded-full flex items-center justify-center">
<span className="text-sm font-semibold text-blue-600">
{index + 1}
</span>
</div>
<div>
<p className="font-medium text-gray-900">{customer.customerName}</p>
<p className="text-sm text-gray-500">{customer.visitCount} زيارة</p>
</div>
</div>
<div className="text-left">
<p className="font-semibold text-gray-900">
{formatCurrency(customer.totalRevenue)}
</p>
</div>
</div>
))}
</div>
</div>
{/* Monthly Performance */}
<div className="bg-white p-6 rounded-lg shadow">
<h2 className="text-lg font-semibold text-gray-900 mb-4">الأداء الشهري</h2>
<div className="space-y-3 max-h-96 overflow-y-auto">
{monthlyData.slice(-6).reverse().map((month, index) => (
<div key={`${month.year}-${month.month}`} className="p-3 bg-gray-50 rounded-lg">
<div className="flex justify-between items-center mb-2">
<span className="font-medium text-gray-900">
{month.month} {month.year}
</span>
<span className={`font-semibold ${month.profit >= 0 ? 'text-green-600' : 'text-red-600'}`}>
{formatCurrency(month.profit)}
</span>
</div>
<div className="grid grid-cols-2 gap-4 text-sm">
<div>
<span className="text-gray-600">الإيرادات: </span>
<span className="font-medium text-green-600">
{formatCurrency(month.income)}
</span>
</div>
<div>
<span className="text-gray-600">المصروفات: </span>
<span className="font-medium text-red-600">
{formatCurrency(month.expenses)}
</span>
</div>
</div>
</div>
))}
</div>
</div>
</div>
</div>
</DashboardLayout>
);
}