340 lines
9.4 KiB
TypeScript
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,
|
|
},
|
|
};
|
|
} |