Skip to content

Commit 6aa3235

Browse files
committed
feat: add loan payment functionality to transactions workflow and update related components
1 parent c58dcc1 commit 6aa3235

15 files changed

Lines changed: 470 additions & 251 deletions

File tree

actions/transactions.ts

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,12 @@ import {
1414
import { AccountModel } from "@/models/Account";
1515
import { TransactionModel } from "@/models/Transaction";
1616

17-
type TransactionType = "earning" | "expense";
17+
type EditableTransactionType = "earning" | "expense";
18+
type TransactionType =
19+
| EditableTransactionType
20+
| "transfer"
21+
| "transfer_fee"
22+
| "loan_repayment";
1823

1924
export type TransactionListItem = {
2025
id: string;
@@ -27,7 +32,7 @@ export type TransactionListItem = {
2732
createdAt: string;
2833
};
2934

30-
function effectFor(type: TransactionType, amount: number) {
35+
function effectFor(type: EditableTransactionType, amount: number) {
3136
return type === "earning" ? amount : -amount;
3237
}
3338

@@ -207,11 +212,11 @@ export async function updateTransactionForTenant(
207212
await applyBalanceEffectOrThrow(
208213
existing.accountId.toString(),
209214
tenantId,
210-
-effectFor(existing.type as TransactionType, existing.amount),
215+
-effectFor(existing.type as EditableTransactionType, existing.amount),
211216
);
212217

213218
const nextAccountId = payload.accountId ?? existing.accountId.toString();
214-
const nextType = (payload.type ?? existing.type) as TransactionType;
219+
const nextType = (payload.type ?? existing.type) as EditableTransactionType;
215220
const nextAmount = payload.amount ?? existing.amount;
216221

217222
await applyBalanceEffectOrThrow(
@@ -276,7 +281,7 @@ export async function deleteTransactionForTenant(
276281
await applyBalanceEffectOrThrow(
277282
existing.accountId.toString(),
278283
tenantId,
279-
-effectFor(existing.type as TransactionType, existing.amount),
284+
-effectFor(existing.type as EditableTransactionType, existing.amount),
280285
);
281286

282287
await existing.deleteOne();

app/(dashboard)/reports/page.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -308,7 +308,7 @@ export default async function ReportsPage({
308308
<div className='flex justify-between gap-3 text-sm'>
309309
<span className='font-medium text-slate-700 flex items-center gap-2'>
310310
<span
311-
className={`w-2 h-2 rounded-full ${category.type === 'INCOME' ? 'bg-emerald-500' : 'bg-rose-500'}`}
311+
className={`w-2 h-2 rounded-full ${category.type === 'earning' ? 'bg-emerald-500' : 'bg-rose-500'}`}
312312
></span>
313313
{category.category}
314314
</span>
@@ -322,7 +322,7 @@ export default async function ReportsPage({
322322
</div>
323323
<div className='h-2 rounded-full bg-slate-100 overflow-hidden'>
324324
<div
325-
className={`h-full rounded-full ${category.type === 'INCOME' ? 'bg-emerald-400' : 'bg-sky-400'}`}
325+
className={`h-full rounded-full ${category.type === 'earning' ? 'bg-emerald-400' : 'bg-sky-400'}`}
326326
style={{
327327
width: barWidth(category.total, maxCategoryTotal),
328328
}}

app/(dashboard)/transactions/new/page.tsx

Lines changed: 42 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,56 @@
1+
import Link from 'next/link';
2+
13
import { listAccountsByTenant } from '@/actions/accounts';
4+
import { listLoansByTenant } from '@/actions/loans';
25
import NewTransactionForm from '@/components/transactions/new-transaction-form';
36
import { getTenantIdOrThrow } from '@/lib/tenant/getTenant';
47

5-
export default async function NewTransactionPage() {
8+
type SearchParams = {
9+
mode?: string;
10+
};
11+
12+
export default async function NewTransactionPage({
13+
searchParams,
14+
}: {
15+
searchParams: Promise<SearchParams>;
16+
}) {
617
const tenantId = await getTenantIdOrThrow();
7-
const accounts = await listAccountsByTenant(tenantId);
18+
const params = await searchParams;
19+
const [accounts, loans] = await Promise.all([
20+
listAccountsByTenant(tenantId),
21+
listLoansByTenant(tenantId),
22+
]);
23+
const openLoans = loans.filter((loan) => !loan.isClosed);
24+
const initialMode =
25+
params.mode === 'loan_repayment' ? 'loan_repayment' : undefined;
826

927
return (
1028
<div className='max-w-2xl mx-auto space-y-6'>
11-
<header className='pb-4 border-b border-slate-200'>
12-
<h1 className='text-2xl font-bold tracking-tight text-slate-900'>
13-
Add Transaction
14-
</h1>
15-
<p className='text-sm text-slate-500 mt-1'>
16-
Record a new earning or expense.
17-
</p>
29+
<header className='flex flex-col sm:flex-row sm:items-center justify-between gap-4 pb-4 border-b border-slate-200'>
30+
<div>
31+
<h1 className='text-2xl font-bold tracking-tight text-slate-900'>
32+
Add Transaction
33+
</h1>
34+
<p className='text-sm text-slate-500 mt-1'>
35+
Record earning, expense, or loan payment activity.
36+
</p>
37+
</div>
38+
{openLoans.length === 0 ? (
39+
<Link
40+
className='btn-secondary text-sm text-center'
41+
href='/loans/new'
42+
>
43+
New Loan
44+
</Link>
45+
) : null}
1846
</header>
1947

2048
<div className='card p-6'>
21-
<NewTransactionForm accounts={accounts} />
49+
<NewTransactionForm
50+
accounts={accounts}
51+
loans={openLoans}
52+
initialMode={initialMode}
53+
/>
2254
</div>
2355
</div>
2456
);

app/(dashboard)/transactions/page.tsx

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,26 @@ export default async function TransactionsPage({
6969
</svg>
7070
Add Transaction
7171
</Link>
72+
<Link
73+
className='btn-secondary flex items-center gap-2 text-sm'
74+
href='/transactions/new?mode=loan_repayment'
75+
>
76+
<svg
77+
className='w-4 h-4'
78+
fill='none'
79+
stroke='currentColor'
80+
viewBox='0 0 24 24'
81+
xmlns='http://www.w3.org/2000/svg'
82+
>
83+
<path
84+
strokeLinecap='round'
85+
strokeLinejoin='round'
86+
strokeWidth={2}
87+
d='M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8V7m0 10v-1m9-4a9 9 0 11-18 0 9 9 0 0118 0z'
88+
/>
89+
</svg>
90+
Loan Payment
91+
</Link>
7292
<Link
7393
className='btn-secondary flex items-center gap-2 text-sm'
7494
href='/transactions/transfer'

app/api/admin/audits/export/route.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ export async function GET(request: Request) {
3333
audits.items.map((item) => ({
3434
id: item.id,
3535
action: item.action,
36-
status: item.status,
36+
status: item.status ?? "",
3737
tenantId: item.tenantId ?? undefined,
3838
actorId: item.actorId ?? undefined,
3939
ipAddress: item.ipAddress ?? undefined,

app/api/loans/[loanId]/route.ts

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
11
import { NextResponse } from "next/server";
22

3-
import {
4-
getLoanByIdForTenant,
5-
listLoanRepaymentsByTenant,
6-
} from "@/actions/loans";
3+
import { getLoanDetailForTenant } from "@/actions/loans";
74
import { getTenantIdOrThrow } from "@/lib/tenant/getTenant";
85

96
export async function GET(
@@ -14,16 +11,16 @@ export async function GET(
1411
const tenantId = await getTenantIdOrThrow();
1512
const { loanId } = await context.params;
1613

17-
const [loan, repayments] = await Promise.all([
18-
getLoanByIdForTenant(tenantId, loanId),
19-
listLoanRepaymentsByTenant(tenantId, loanId),
20-
]);
14+
const loan = await getLoanDetailForTenant(tenantId, loanId);
2115

2216
if (!loan) {
2317
return NextResponse.json({ error: "Loan not found" }, { status: 404 });
2418
}
2519

26-
return NextResponse.json({ loan, repayments }, { status: 200 });
20+
return NextResponse.json(
21+
{ loan, repayments: loan.repayments },
22+
{ status: 200 },
23+
);
2724
} catch (error) {
2825
const message =
2926
error instanceof Error ? error.message : "Failed to fetch loan details";

app/api/reports/export/route.ts

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,8 @@
11
import { NextResponse } from "next/server";
22

3-
import { getFinancialInsights, toCsvReport } from "@/actions/reports";
3+
import { getFinancialReportForTenant, reportToCsv } from "@/actions/reports";
44
import { getTenantIdOrThrow } from "@/lib/tenant/getTenant";
5-
import {
6-
exportFormatSchema,
7-
reportFilterSchema,
8-
} from "@/lib/validators/report";
5+
import { reportFilterSchema } from "@/lib/validators/report";
96

107
export async function GET(request: Request) {
118
try {
@@ -16,16 +13,14 @@ export async function GET(request: Request) {
1613
accountId: searchParams.get("accountId") ?? undefined,
1714
fromDate: searchParams.get("fromDate") ?? undefined,
1815
toDate: searchParams.get("toDate") ?? undefined,
16+
format: searchParams.get("format") ?? undefined,
1917
});
2018

21-
const format = exportFormatSchema.parse(
22-
searchParams.get("format") ?? "json",
23-
);
24-
25-
const insights = await getFinancialInsights(tenantId, filters);
19+
const format = filters.format ?? "json";
20+
const report = await getFinancialReportForTenant(tenantId, filters);
2621

2722
if (format === "csv") {
28-
const csv = toCsvReport(insights);
23+
const csv = reportToCsv(report);
2924

3025
return new NextResponse(csv, {
3126
status: 200,
@@ -36,7 +31,7 @@ export async function GET(request: Request) {
3631
});
3732
}
3833

39-
return NextResponse.json({ insights }, { status: 200 });
34+
return NextResponse.json({ report }, { status: 200 });
4035
} catch (error) {
4136
const message =
4237
error instanceof Error ? error.message : "Failed to export report";

app/api/reports/summary/route.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { NextResponse } from "next/server";
22

3-
import { getFinancialInsights } from "@/actions/reports";
3+
import { getFinancialReportForTenant } from "@/actions/reports";
44
import { getTenantIdOrThrow } from "@/lib/tenant/getTenant";
55
import { reportFilterSchema } from "@/lib/validators/report";
66

@@ -15,9 +15,9 @@ export async function GET(request: Request) {
1515
toDate: searchParams.get("toDate") ?? undefined,
1616
});
1717

18-
const insights = await getFinancialInsights(tenantId, filters);
18+
const report = await getFinancialReportForTenant(tenantId, filters);
1919

20-
return NextResponse.json({ insights }, { status: 200 });
20+
return NextResponse.json({ report }, { status: 200 });
2121
} catch (error) {
2222
const message =
2323
error instanceof Error ? error.message : "Failed to generate report";

components/loans/loan-repayment-form.tsx

Lines changed: 0 additions & 133 deletions
This file was deleted.

0 commit comments

Comments
 (0)