153 lines
3.9 KiB
TypeScript
153 lines
3.9 KiB
TypeScript
import { Category } from '@/types/category';
|
|
import { endOfMonth, format, startOfMonth } from 'date-fns';
|
|
import { useCallback, useEffect, useState } from 'react';
|
|
|
|
export interface CashflowSummary {
|
|
income: number;
|
|
expense: number;
|
|
net: number;
|
|
savings_rate: number;
|
|
}
|
|
|
|
export interface SankeyCategory {
|
|
category: Category;
|
|
category_id: string;
|
|
amount: number;
|
|
}
|
|
|
|
export interface SankeyData {
|
|
income_categories: SankeyCategory[];
|
|
expense_categories: SankeyCategory[];
|
|
total_income: number;
|
|
total_expense: number;
|
|
}
|
|
|
|
export interface TrendDataPoint {
|
|
month: string;
|
|
income: number;
|
|
expense: number;
|
|
net: number;
|
|
}
|
|
|
|
export interface BreakdownItem {
|
|
category: Category | null;
|
|
category_id: string | null;
|
|
amount: number;
|
|
percentage: number;
|
|
previous_amount: number;
|
|
}
|
|
|
|
export interface BreakdownData {
|
|
data: BreakdownItem[];
|
|
total: number;
|
|
previous_total: number;
|
|
}
|
|
|
|
export interface CashflowData {
|
|
summary: {
|
|
current: CashflowSummary;
|
|
previous: CashflowSummary;
|
|
};
|
|
sankey: SankeyData;
|
|
trend: TrendDataPoint[];
|
|
incomeBreakdown: BreakdownData;
|
|
expenseBreakdown: BreakdownData;
|
|
isLoading: boolean;
|
|
}
|
|
|
|
interface UseCashflowDataOptions {
|
|
from: Date;
|
|
to: Date;
|
|
}
|
|
|
|
const emptyBreakdown: BreakdownData = {
|
|
data: [],
|
|
total: 0,
|
|
previous_total: 0,
|
|
};
|
|
|
|
const emptySummary: CashflowSummary = {
|
|
income: 0,
|
|
expense: 0,
|
|
net: 0,
|
|
savings_rate: 0,
|
|
};
|
|
|
|
export function useCashflowData({
|
|
from,
|
|
to,
|
|
}: UseCashflowDataOptions): CashflowData & { refetch: () => void } {
|
|
const [data, setData] = useState<Omit<CashflowData, 'isLoading'>>({
|
|
summary: { current: emptySummary, previous: emptySummary },
|
|
sankey: {
|
|
income_categories: [],
|
|
expense_categories: [],
|
|
total_income: 0,
|
|
total_expense: 0,
|
|
},
|
|
trend: [],
|
|
incomeBreakdown: emptyBreakdown,
|
|
expenseBreakdown: emptyBreakdown,
|
|
});
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
|
|
const fetchData = useCallback(async () => {
|
|
setIsLoading(true);
|
|
try {
|
|
const fromStr = format(from, 'yyyy-MM-dd');
|
|
const toStr = format(to, 'yyyy-MM-dd');
|
|
|
|
const periodParams = new URLSearchParams({
|
|
from: fromStr,
|
|
to: toStr,
|
|
});
|
|
const periodQuery = `?${periodParams.toString()}`;
|
|
|
|
const [summary, sankey, trend, incomeBreakdown, expenseBreakdown] =
|
|
await Promise.all([
|
|
fetch(`/api/cashflow/summary${periodQuery}`).then((r) =>
|
|
r.json(),
|
|
),
|
|
fetch(`/api/cashflow/sankey${periodQuery}`).then((r) =>
|
|
r.json(),
|
|
),
|
|
fetch(`/api/cashflow/trend?months=12&to=${toStr}`).then(
|
|
(r) => r.json(),
|
|
),
|
|
fetch(
|
|
`/api/cashflow/breakdown${periodQuery}&type=income`,
|
|
).then((r) => r.json()),
|
|
fetch(
|
|
`/api/cashflow/breakdown${periodQuery}&type=expense`,
|
|
).then((r) => r.json()),
|
|
]);
|
|
|
|
setData({
|
|
summary,
|
|
sankey,
|
|
trend: trend.data,
|
|
incomeBreakdown,
|
|
expenseBreakdown,
|
|
});
|
|
} catch (error) {
|
|
console.error('Failed to fetch cashflow data:', error);
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
}, [from, to]);
|
|
|
|
useEffect(() => {
|
|
fetchData();
|
|
}, [fetchData]);
|
|
|
|
return { ...data, isLoading, refetch: fetchData };
|
|
}
|
|
|
|
export function getDefaultPeriod(): { from: Date; to: Date } {
|
|
const now = new Date();
|
|
return {
|
|
from: startOfMonth(now),
|
|
to: endOfMonth(now),
|
|
};
|
|
}
|