car_mms/app/lib/financial-reporting.server.ts
2025-09-11 14:22:27 +03:00

340 lines
9.4 KiB
TypeScript

import { prisma } from "./db.server";
import { getTotalExpenses, getExpensesByCategory } from "./expense-management.server";
// Financial summary interface
export interface FinancialSummary {
totalIncome: number;
totalExpenses: number;
netProfit: number;
incomeCount: number;
expenseCount: number;
profitMargin: number;
}
// Monthly financial data interface
export interface MonthlyFinancialData {
month: string;
year: number;
income: number;
expenses: number;
profit: number;
}
// Category breakdown interface
export interface CategoryBreakdown {
category: string;
amount: number;
count: number;
percentage: number;
}
// Get financial summary for a date range
export async function getFinancialSummary(
dateFrom?: Date,
dateTo?: Date
): Promise<FinancialSummary> {
const whereClause: any = {};
if (dateFrom || dateTo) {
whereClause.incomeDate = {};
if (dateFrom) {
whereClause.incomeDate.gte = dateFrom;
}
if (dateTo) {
whereClause.incomeDate.lte = dateTo;
}
}
// Get income data
const incomeResult = await prisma.income.aggregate({
where: whereClause,
_sum: {
amount: true,
},
_count: {
id: true,
},
});
const totalIncome = incomeResult._sum.amount || 0;
const incomeCount = incomeResult._count.id;
// Get expense data
const totalExpenses = await getTotalExpenses(dateFrom, dateTo);
const expenseCount = await prisma.expense.count({
where: dateFrom || dateTo ? {
expenseDate: {
...(dateFrom && { gte: dateFrom }),
...(dateTo && { lte: dateTo }),
},
} : undefined,
});
// Calculate derived metrics
const netProfit = totalIncome - totalExpenses;
const profitMargin = totalIncome > 0 ? (netProfit / totalIncome) * 100 : 0;
return {
totalIncome,
totalExpenses,
netProfit,
incomeCount,
expenseCount,
profitMargin,
};
}
// Get monthly financial data for the last 12 months
export async function getMonthlyFinancialData(): Promise<MonthlyFinancialData[]> {
const months: MonthlyFinancialData[] = [];
const currentDate = new Date();
for (let i = 11; i >= 0; i--) {
const date = new Date(currentDate.getFullYear(), currentDate.getMonth() - i, 1);
const nextMonth = new Date(date.getFullYear(), date.getMonth() + 1, 1);
const monthName = date.toLocaleDateString('ar-SA', { month: 'long' });
const year = date.getFullYear();
// Get income for this month
const incomeResult = await prisma.income.aggregate({
where: {
incomeDate: {
gte: date,
lt: nextMonth,
},
},
_sum: {
amount: true,
},
});
// Get expenses for this month
const expenseResult = await prisma.expense.aggregate({
where: {
expenseDate: {
gte: date,
lt: nextMonth,
},
},
_sum: {
amount: true,
},
});
const income = incomeResult._sum.amount || 0;
const expenses = expenseResult._sum.amount || 0;
const profit = income - expenses;
months.push({
month: monthName,
year,
income,
expenses,
profit,
});
}
return months;
}
// Get income breakdown by maintenance type
export async function getIncomeByMaintenanceType(
dateFrom?: Date,
dateTo?: Date
): Promise<CategoryBreakdown[]> {
const whereClause: any = {};
if (dateFrom || dateTo) {
whereClause.incomeDate = {};
if (dateFrom) {
whereClause.incomeDate.gte = dateFrom;
}
if (dateTo) {
whereClause.incomeDate.lte = dateTo;
}
}
// Get income grouped by maintenance type
const result = await prisma.income.findMany({
where: whereClause,
include: {
maintenanceVisit: {
select: {
maintenanceJobs: true,
},
},
},
});
// Group by maintenance type
const grouped = result.reduce((acc, income) => {
try {
const jobs = JSON.parse(income.maintenanceVisit.maintenanceJobs);
const types = jobs.map((job: any) => job.job || 'غير محدد');
types.forEach((type: string) => {
if (!acc[type]) {
acc[type] = { amount: 0, count: 0 };
}
acc[type].amount += income.amount / types.length; // Distribute amount across multiple jobs
acc[type].count += 1;
});
} catch {
// Fallback for invalid JSON
const type = 'غير محدد';
if (!acc[type]) {
acc[type] = { amount: 0, count: 0 };
}
acc[type].amount += income.amount;
acc[type].count += 1;
}
return acc;
}, {} as Record<string, { amount: number; count: number }>);
// Calculate total for percentage calculation
const total = Object.values(grouped).reduce((sum, item) => sum + item.amount, 0);
// Convert to array with percentages
return Object.entries(grouped)
.map(([category, data]) => ({
category,
amount: data.amount,
count: data.count,
percentage: total > 0 ? (data.amount / total) * 100 : 0,
}))
.sort((a, b) => b.amount - a.amount);
}
// Get expense breakdown by category
export async function getExpenseBreakdown(
dateFrom?: Date,
dateTo?: Date
): Promise<CategoryBreakdown[]> {
const categoryData = await getExpensesByCategory(dateFrom, dateTo);
const total = categoryData.reduce((sum, item) => sum + item.total, 0);
return categoryData.map(item => ({
category: item.category,
amount: item.total,
count: item.count,
percentage: total > 0 ? (item.total / total) * 100 : 0,
}));
}
// Get top customers by revenue
export async function getTopCustomersByRevenue(
limit: number = 10,
dateFrom?: Date,
dateTo?: Date
): Promise<{
customerId: number;
customerName: string;
totalRevenue: number;
visitCount: number;
}[]> {
const whereClause: any = {};
if (dateFrom || dateTo) {
whereClause.incomeDate = {};
if (dateFrom) {
whereClause.incomeDate.gte = dateFrom;
}
if (dateTo) {
whereClause.incomeDate.lte = dateTo;
}
}
const result = await prisma.income.findMany({
where: whereClause,
include: {
maintenanceVisit: {
include: {
customer: {
select: {
id: true,
name: true,
},
},
},
},
},
});
// Group by customer
const customerRevenue = result.reduce((acc, income) => {
const customer = income.maintenanceVisit.customer;
const customerId = customer.id;
if (!acc[customerId]) {
acc[customerId] = {
customerId,
customerName: customer.name,
totalRevenue: 0,
visitCount: 0,
};
}
acc[customerId].totalRevenue += income.amount;
acc[customerId].visitCount += 1;
return acc;
}, {} as Record<number, {
customerId: number;
customerName: string;
totalRevenue: number;
visitCount: number;
}>);
return Object.values(customerRevenue)
.sort((a, b) => b.totalRevenue - a.totalRevenue)
.slice(0, limit);
}
// Get financial trends (comparing current period with previous period)
export async function getFinancialTrends(
dateFrom: Date,
dateTo: Date
): Promise<{
currentPeriod: FinancialSummary;
previousPeriod: FinancialSummary;
trends: {
incomeGrowth: number;
expenseGrowth: number;
profitGrowth: number;
};
}> {
// Calculate previous period dates
const periodLength = dateTo.getTime() - dateFrom.getTime();
const previousDateTo = new Date(dateFrom.getTime() - 1);
const previousDateFrom = new Date(previousDateTo.getTime() - periodLength);
// Get current and previous period summaries
const [currentPeriod, previousPeriod] = await Promise.all([
getFinancialSummary(dateFrom, dateTo),
getFinancialSummary(previousDateFrom, previousDateTo),
]);
// Calculate growth percentages
const incomeGrowth = previousPeriod.totalIncome > 0
? ((currentPeriod.totalIncome - previousPeriod.totalIncome) / previousPeriod.totalIncome) * 100
: 0;
const expenseGrowth = previousPeriod.totalExpenses > 0
? ((currentPeriod.totalExpenses - previousPeriod.totalExpenses) / previousPeriod.totalExpenses) * 100
: 0;
const profitGrowth = previousPeriod.netProfit !== 0
? ((currentPeriod.netProfit - previousPeriod.netProfit) / Math.abs(previousPeriod.netProfit)) * 100
: 0;
return {
currentPeriod,
previousPeriod,
trends: {
incomeGrowth,
expenseGrowth,
profitGrowth,
},
};
}