Skip to content
This repository was archived by the owner on Jun 19, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ jobs:
cache-dependency-path: app/package-lock.json
- name: Install
working-directory: app
run: npm ci || npm install --no-audit --no-fund
run: npm ci --no-audit --no-fund
- name: Lint
working-directory: app
run: npm run lint
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/frontend-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ jobs:

- name: Install dependencies
working-directory: app
run: npm ci || npm install --no-audit --no-fund
run: npm ci --no-audit --no-fund

- name: Run tests
working-directory: app
Expand Down
41 changes: 40 additions & 1 deletion app/src/__tests__/Dashboard.integration.test.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from 'react';
import { render, screen, waitFor } from '@testing-library/react';
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { MemoryRouter, Route, Routes } from 'react-router-dom';
import { Dashboard } from '@/pages/Dashboard';
Expand Down Expand Up @@ -53,6 +53,14 @@ describe('Dashboard integration', () => {
channel_whatsapp: false,
},
],
category_breakdown: [
{
category_id: 1,
category_name: 'Food',
amount: 500,
share_pct: 100,
},
],
errors: [],
});

Expand All @@ -68,6 +76,7 @@ describe('Dashboard integration', () => {
expect(screen.getByText(/financial dashboard/i)).toBeInTheDocument();
expect(screen.getByText(/salary/i)).toBeInTheDocument();
expect(screen.getByText(/internet/i)).toBeInTheDocument();
expect(screen.getByText(/category breakdown/i)).toBeInTheDocument();
});

it('navigates from dashboard action buttons', async () => {
Expand All @@ -83,6 +92,7 @@ describe('Dashboard integration', () => {
},
recent_transactions: [],
upcoming_bills: [],
category_breakdown: [],
errors: [],
});

Expand All @@ -100,4 +110,33 @@ describe('Dashboard integration', () => {
await user.click(screen.getByRole('button', { name: /add transaction/i }));
expect(await screen.findByText('Expenses Route')).toBeInTheDocument();
});

it('reloads summary when month filter changes', async () => {
getDashboardSummaryMock.mockResolvedValue({
period: { month: '2026-02' },
summary: {
net_flow: 0,
monthly_income: 0,
monthly_expenses: 0,
upcoming_bills_total: 0,
upcoming_bills_count: 0,
},
recent_transactions: [],
upcoming_bills: [],
category_breakdown: [],
errors: [],
});

render(
<MemoryRouter initialEntries={['/dashboard']}>
<Routes>
<Route path="/dashboard" element={<Dashboard />} />
</Routes>
</MemoryRouter>,
);

await waitFor(() => expect(getDashboardSummaryMock).toHaveBeenCalledTimes(1));
fireEvent.change(screen.getByLabelText(/dashboard month/i), { target: { value: '2026-01' } });
await waitFor(() => expect(getDashboardSummaryMock).toHaveBeenLastCalledWith('2026-01'));
});
});
2 changes: 1 addition & 1 deletion app/src/__tests__/SignIn.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ jest.mock('@/components/ui/button', () => ({
Button: ({ children, ...props }: React.PropsWithChildren & React.ButtonHTMLAttributes<HTMLButtonElement>) => <button {...props}>{children}</button>,
}));
jest.mock('@/components/ui/input', () => ({
Input: ({ children, ...props }: React.PropsWithChildren & React.InputHTMLAttributes<HTMLInputElement>) => <input {...props} />,
Input: (props: React.InputHTMLAttributes<HTMLInputElement>) => <input {...props} />,
}));
jest.mock('@/components/ui/label', () => ({
Label: ({ children, ...props }: React.PropsWithChildren & React.LabelHTMLAttributes<HTMLLabelElement>) => <label {...props}>{children}</label>,
Expand Down
6 changes: 3 additions & 3 deletions app/src/api/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,9 @@ export const baseURL = resolveApiBaseUrl();

export type HttpMethod = 'GET' | 'POST' | 'PATCH' | 'DELETE';

export async function api<T = any>(
export async function api<T = unknown>(
path: string,
opts: { method?: HttpMethod; body?: any; headers?: Record<string, string> } = {},
opts: { method?: HttpMethod; body?: unknown; headers?: Record<string, string> } = {},
): Promise<T> {
async function doFetch(withAuth = true): Promise<Response> {
const token = withAuth ? getToken() : null;
Expand Down Expand Up @@ -75,7 +75,7 @@ export async function api<T = any>(
const text = await res.text();
let msg = text;
try {
const obj = JSON.parse(text);
const obj = JSON.parse(text) as { error?: string; message?: string };
msg = (obj && (obj.error || obj.message)) || JSON.stringify(obj);
} catch {
msg = text;
Expand Down
11 changes: 9 additions & 2 deletions app/src/api/dashboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,16 @@ export type DashboardSummary = {
channel_email: boolean;
channel_whatsapp: boolean;
}>;
category_breakdown: Array<{
category_id: number | null;
category_name: string;
amount: number;
share_pct: number;
}>;
errors?: string[];
};

export async function getDashboardSummary(): Promise<DashboardSummary> {
return api<DashboardSummary>('/dashboard/summary');
export async function getDashboardSummary(month?: string): Promise<DashboardSummary> {
const query = month ? `?month=${encodeURIComponent(month)}` : '';
return api<DashboardSummary>(`/dashboard/summary${query}`);
}
4 changes: 2 additions & 2 deletions app/src/api/reminders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,10 @@ export async function updateReminder(id: number, payload: ReminderUpdate): Promi
return api<Reminder>(`/reminders/${id}`, { method: 'PATCH', body: payload });
}

export async function deleteReminder(id: number): Promise<{ message?: string } | {}> {
export async function deleteReminder(id: number): Promise<{ message?: string } | Record<string, never>> {
return api(`/reminders/${id}`, { method: 'DELETE' });
}

export async function runDue(): Promise<{ processed?: number } | {}> {
export async function runDue(): Promise<{ processed?: number } | Record<string, never>> {
return api('/reminders/run', { method: 'POST' });
}
4 changes: 2 additions & 2 deletions app/src/components/ui/calendar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,8 @@ function Calendar({
...classNames,
}}
components={{
IconLeft: ({ ..._props }) => <ChevronLeft className="h-4 w-4" />,
IconRight: ({ ..._props }) => <ChevronRight className="h-4 w-4" />,
IconLeft: () => <ChevronLeft className="h-4 w-4" />,
IconRight: () => <ChevronRight className="h-4 w-4" />,
}}
{...props}
/>
Expand Down
2 changes: 1 addition & 1 deletion app/src/components/ui/chart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ ChartContainer.displayName = "Chart"

const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
const colorConfig = Object.entries(config).filter(
([_, config]) => config.theme || config.color
([, itemConfig]) => itemConfig.theme || itemConfig.color
)

if (!colorConfig.length) {
Expand Down
2 changes: 1 addition & 1 deletion app/src/components/ui/command.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ const Command = React.forwardRef<
))
Command.displayName = CommandPrimitive.displayName

interface CommandDialogProps extends DialogProps {}
type CommandDialogProps = DialogProps

const CommandDialog = ({ children, ...props }: CommandDialogProps) => {
return (
Expand Down
3 changes: 1 addition & 2 deletions app/src/components/ui/textarea.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@ import * as React from "react"

import { cn } from "@/lib/utils"

export interface TextareaProps
extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
export type TextareaProps = React.TextareaHTMLAttributes<HTMLTextAreaElement>

const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
({ className, ...props }, ref) => {
Expand Down
17 changes: 4 additions & 13 deletions app/src/hooks/use-toast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,37 +15,28 @@ type ToasterToast = ToastProps & {
action?: ToastActionElement
}

const actionTypes = {
ADD_TOAST: "ADD_TOAST",
UPDATE_TOAST: "UPDATE_TOAST",
DISMISS_TOAST: "DISMISS_TOAST",
REMOVE_TOAST: "REMOVE_TOAST",
} as const

let count = 0

function genId() {
count = (count + 1) % Number.MAX_SAFE_INTEGER
return count.toString()
}

type ActionType = typeof actionTypes

type Action =
| {
type: ActionType["ADD_TOAST"]
type: "ADD_TOAST"
toast: ToasterToast
}
| {
type: ActionType["UPDATE_TOAST"]
type: "UPDATE_TOAST"
toast: Partial<ToasterToast>
}
| {
type: ActionType["DISMISS_TOAST"]
type: "DISMISS_TOAST"
toastId?: ToasterToast["id"]
}
| {
type: ActionType["REMOVE_TOAST"]
type: "REMOVE_TOAST"
toastId?: ToasterToast["id"]
}

Expand Down
37 changes: 21 additions & 16 deletions app/src/pages/Bills.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useState, useEffect } from 'react';
import { useState, useEffect, useCallback } from 'react';
import { FinancialCard, FinancialCardContent, FinancialCardDescription, FinancialCardFooter, FinancialCardHeader, FinancialCardTitle } from '@/components/ui/financial-card';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
Expand Down Expand Up @@ -134,23 +134,27 @@ export function Bills() {
const [due, setDue] = useState<string>(() => new Date().toISOString().slice(0, 10));
const [saving, setSaving] = useState(false);

useEffect(() => {
refresh();
}, []);
const getErrorMessage = (error: unknown, fallback: string) =>
error instanceof Error ? error.message : fallback;

async function refresh() {
const refresh = useCallback(async () => {
setLoading(true);
setError(null);
try {
const data = await listBills();
setItems(data);
} catch (e: any) {
setError(e?.message || 'Failed to load bills');
toast({ title: 'Failed to load bills', description: e?.message || 'Please try again.' });
} catch (error: unknown) {
const message = getErrorMessage(error, 'Failed to load bills');
setError(message);
toast({ title: 'Failed to load bills', description: message || 'Please try again.' });
} finally {
setLoading(false);
}
}
}, [toast]);

useEffect(() => {
void refresh();
}, [refresh]);

async function onCreate() {
if (!name.trim() || !amount) return;
Expand All @@ -163,9 +167,10 @@ export function Bills() {
setAmount('');
setDue(new Date().toISOString().slice(0, 10));
toast({ title: 'Bill created' });
} catch (e: any) {
setError(e?.message || 'Failed to create bill');
toast({ title: 'Failed to create bill', description: e?.message || 'Please try again.' });
} catch (error: unknown) {
const message = getErrorMessage(error, 'Failed to create bill');
setError(message);
toast({ title: 'Failed to create bill', description: message || 'Please try again.' });
} finally {
setSaving(false);
}
Expand All @@ -176,8 +181,8 @@ export function Bills() {
await markBillPaid(id);
toast({ title: 'Marked as paid' });
refresh();
} catch (e: any) {
toast({ title: 'Failed to mark paid', description: e?.message || 'Please try again.' });
} catch (error: unknown) {
toast({ title: 'Failed to mark paid', description: getErrorMessage(error, 'Please try again.') });
}
}

Expand Down Expand Up @@ -259,8 +264,8 @@ export function Bills() {
await deleteBill(b.id);
setItems((prev) => prev.filter((x) => x.id !== b.id));
toast({ title: 'Bill deleted' });
} catch (e: any) {
toast({ title: 'Failed to delete bill', description: e?.message || 'Please try again.' });
} catch (error: unknown) {
toast({ title: 'Failed to delete bill', description: getErrorMessage(error, 'Please try again.') });
}
}}
>
Expand Down
30 changes: 18 additions & 12 deletions app/src/pages/Categories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,10 @@ export default function Categories() {
try {
const data = await listCategories();
setItems(data);
} catch (e: any) {
setError(e?.message || 'Failed to load categories');
toast({ title: 'Failed to load categories', description: e?.message || 'Please try again.' });
} catch (error: unknown) {
const message = getErrorMessage(error, 'Failed to load categories');
setError(message);
toast({ title: 'Failed to load categories', description: message || 'Please try again.' });
} finally {
setLoading(false);
}
Expand All @@ -61,9 +62,10 @@ export default function Categories() {
setItems((prev) => [created, ...prev]);
setNewName('');
toast({ title: 'Category created' });
} catch (e: any) {
setError(e?.message || 'Failed to create');
toast({ title: 'Failed to create category', description: e?.message || 'Please try again.' });
} catch (error: unknown) {
const message = getErrorMessage(error, 'Failed to create');
setError(message);
toast({ title: 'Failed to create category', description: message || 'Please try again.' });
} finally {
setSaving(false);
}
Expand All @@ -78,9 +80,10 @@ export default function Categories() {
setEditingId(null);
setEditName('');
toast({ title: 'Category updated' });
} catch (e: any) {
setError(e?.message || 'Failed to update');
toast({ title: 'Failed to update category', description: e?.message || 'Please try again.' });
} catch (error: unknown) {
const message = getErrorMessage(error, 'Failed to update');
setError(message);
toast({ title: 'Failed to update category', description: message || 'Please try again.' });
} finally {
setSaving(false);
}
Expand All @@ -92,9 +95,10 @@ export default function Categories() {
await deleteCategory(id);
setItems((prev) => prev.filter((x) => x.id !== id));
toast({ title: 'Category deleted' });
} catch (e: any) {
setError(e?.message || 'Failed to delete');
toast({ title: 'Failed to delete category', description: e?.message || 'Please try again.' });
} catch (error: unknown) {
const message = getErrorMessage(error, 'Failed to delete');
setError(message);
toast({ title: 'Failed to delete category', description: message || 'Please try again.' });
} finally {
setSaving(false);
}
Expand Down Expand Up @@ -173,3 +177,5 @@ export default function Categories() {
</div>
);
}
const getErrorMessage = (error: unknown, fallback: string) =>
error instanceof Error ? error.message : fallback;
Loading
Loading