Skip to content

Commit

Permalink
Split trend by transaction type
Browse files Browse the repository at this point in the history
  • Loading branch information
drishit96 committed Aug 2, 2023
1 parent a65e237 commit b02324d
Show file tree
Hide file tree
Showing 9 changed files with 923 additions and 251 deletions.
2 changes: 1 addition & 1 deletion app/components/StatisticsCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export function StatisticsCard({
num: string;
positiveIsBetter?: boolean;
perc?: number;
color: Color;
color?: Color;
currency: Currency;
locale: string;
}) {
Expand Down
144 changes: 120 additions & 24 deletions app/modules/reports/reports.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
} from "~/utils/date.utils";
import type { Prisma } from "@prisma/client";
import Decimal from "decimal.js";
import type { TransactionType } from "../transaction/transaction.schema";

export type ThisMonthReportResponse = {
budget: Prisma.Decimal;
Expand Down Expand Up @@ -47,11 +48,7 @@ export type ComparisonReportResponse = {
maxExpense: Prisma.Decimal;
};

export type TrendingReportResponse = {
startMonth: number;
startYear: number;
endMonth: number;
endYear: number;
type TrendReportResponse = {
targets: {
date: string;
budget: Prisma.Decimal;
Expand All @@ -64,9 +61,26 @@ export type TrendingReportResponse = {
totalExpense: Prisma.Decimal;
totalIncomeEarned: Prisma.Decimal;
totalInvestmentDone: Prisma.Decimal;
};

export type ExpenseTrendReportResponse = Omit<
TrendReportResponse,
"totalIncomeEarned" | "totalInvestmentDone"
> & {
categoryExpensesByCategory: { [key: string]: CategoryExpenseDate[] };
};

export type InvestmentTrendReportResponse = Omit<
TrendReportResponse,
"totalExpense" | "totalIncomeEarned"
> & {
categoryInvestmentsByCategory: { [key: string]: CategoryInvestmentDate[] };
};

export type IncomeTrendReportResponse = TrendReportResponse & {
categoryIncomeByCategory: { [key: string]: CategoryIncomeDate[] };
};

export async function getThisMonthReport(
userId: string,
timezone: string
Expand Down Expand Up @@ -236,13 +250,13 @@ export async function getComparisonReports(
return data;
}

export async function getTrendingReport(
export async function getExpenseTrendReport(
userId: string,
timezone: string,
isActiveSubscription: boolean,
startMonth?: string,
endMonth?: string
): Promise<TrendingReportResponse> {
): Promise<ExpenseTrendReportResponse> {
if (!isActiveSubscription) {
startMonth = getFirstDateOfXMonthsBeforeFormatted(2, timezone);
endMonth = getFirstDateOfXMonthsBeforeFormatted(0, timezone);
Expand All @@ -260,7 +274,85 @@ export async function getTrendingReport(

const [targets, categoryExpenses] = await Promise.all([
getTargets(userId, startDate, endDate),
getExpensePerCategoryForTimeRange(userId, startDate, endDate),
getAmountPerCategoryForTimeRange(userId, "expense", startDate, endDate),
]);

const totalExpense =
targets.length > 0 ? Decimal.sum(...targets.map((t) => t.expense)) : new Decimal(0);

return {
targets,
totalExpense,
categoryExpensesByCategory: Object.fromEntries(groupBy(categoryExpenses, "category")),
};
}

export async function getInvestmentTrendReport(
userId: string,
timezone: string,
isActiveSubscription: boolean,
startMonth?: string,
endMonth?: string
): Promise<InvestmentTrendReportResponse> {
if (!isActiveSubscription) {
startMonth = getFirstDateOfXMonthsBeforeFormatted(2, timezone);
endMonth = getFirstDateOfXMonthsBeforeFormatted(0, timezone);
} else {
startMonth = startMonth ?? getFirstDateOfXMonthsBeforeFormatted(5, timezone);
endMonth = endMonth ?? getFirstDateOfXMonthsBeforeFormatted(0, timezone);
}

let startDate = parseDate(startMonth);
let endDate = parseDate(endMonth);

if (endDate < startDate) {
[startDate, endDate] = [endDate, startDate];
}

const [targets, categoryInvestments] = await Promise.all([
getTargets(userId, startDate, endDate),
getAmountPerCategoryForTimeRange(userId, "investment", startDate, endDate),
]);

const totalInvestmentDone =
targets.length > 0
? Decimal.sum(...targets.map((t) => t.investmentDone))
: new Decimal(0);

return {
targets,
totalInvestmentDone,
categoryInvestmentsByCategory: Object.fromEntries(
groupBy(categoryInvestments, "category")
),
};
}

export async function getIncomeTrendReport(
userId: string,
timezone: string,
isActiveSubscription: boolean,
startMonth?: string,
endMonth?: string
): Promise<IncomeTrendReportResponse> {
if (!isActiveSubscription) {
startMonth = getFirstDateOfXMonthsBeforeFormatted(2, timezone);
endMonth = getFirstDateOfXMonthsBeforeFormatted(0, timezone);
} else {
startMonth = startMonth ?? getFirstDateOfXMonthsBeforeFormatted(5, timezone);
endMonth = endMonth ?? getFirstDateOfXMonthsBeforeFormatted(0, timezone);
}

let startDate = parseDate(startMonth);
let endDate = parseDate(endMonth);

if (endDate < startDate) {
[startDate, endDate] = [endDate, startDate];
}

const [targets, categoryIncomes] = await Promise.all([
getTargets(userId, startDate, endDate),
getAmountPerCategoryForTimeRange(userId, "income", startDate, endDate),
]);

const totalExpense =
Expand All @@ -275,15 +367,11 @@ export async function getTrendingReport(
: new Decimal(0);

return {
startMonth: startDate.getMonth() + 1,
startYear: startDate.getFullYear(),
endMonth: endDate.getMonth() + 1,
endYear: endDate.getFullYear(),
targets,
totalExpense,
totalIncomeEarned,
totalInvestmentDone,
categoryExpensesByCategory: Object.fromEntries(groupBy(categoryExpenses, "category")),
categoryIncomeByCategory: Object.fromEntries(groupBy(categoryIncomes, "category")),
};
}

Expand Down Expand Up @@ -460,23 +548,31 @@ export type CategoryExpense = {
expense: Prisma.Decimal;
};

export type CategoryExpenseDate = {
type CategoryDate = {
category: string;
expense: Prisma.Decimal;
date: string;
};

async function getExpensePerCategoryForTimeRange(
type CategoryAmount<T, K extends TransactionType> = CategoryDate & {
[prop in K]: T;
};

export type CategoryExpenseDate = CategoryAmount<Prisma.Decimal, "expense">;
export type CategoryInvestmentDate = CategoryAmount<Prisma.Decimal, "investment">;
export type CategoryIncomeDate = CategoryAmount<Prisma.Decimal, "income">;

async function getAmountPerCategoryForTimeRange(
userId: string,
type: TransactionType,
startDate: Date,
endDate: Date
): Promise<CategoryExpenseDate[]> {
) {
try {
const categoryExpenses = await prisma.categoryAmount.findMany({
const categoryAmounts = await prisma.categoryAmount.findMany({
where: {
userId,
date: { gte: startDate, lte: endDate },
type: "expense",
type,
amount: { gt: 0 },
},
select: {
Expand All @@ -487,11 +583,11 @@ async function getExpensePerCategoryForTimeRange(
orderBy: { date: "asc" },
});

return categoryExpenses.map((categoryExpense) => ({
category: categoryExpense.category,
expense: categoryExpense.amount,
date: formatDate_MMM_YYYY(categoryExpense.date),
}));
return categoryAmounts.map((categoryAmount) => ({
category: categoryAmount.category,
[type]: categoryAmount.amount,
date: formatDate_MMM_YYYY(categoryAmount.date),
})) as CategoryAmount<Prisma.Decimal, TransactionType>[];
} catch (error) {
console.log(error);
return [];
Expand Down
2 changes: 1 addition & 1 deletion app/routes/reports.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ export default function Reports() {
</p>
</Ripple>
</Link>
<Link className="w-1/3" to="/reports/trend" replace>
<Link className="w-1/3" to="/reports/trend/expense" replace>
<Ripple>
<p
className={`p-2 text-sm font-bold sm:text-base sm:font-normal text-center border-r-2 border-t-2 border-b-2 border-emerald-700 rounded-r-lg ${
Expand Down
Loading

0 comments on commit b02324d

Please sign in to comment.