Skip to content
Open
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
7 changes: 7 additions & 0 deletions .vscode/extensions.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"recommendations": [
"eamodio.gitlens"
]
}


13 changes: 6 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
<div align="center">
<img src="public/focal-icon.svg" alt="Focal Logo" width="120" height="120" />
<img src="public/financemate-icon.png" alt="FinanceMate Logo" width="120" height="120" />

# Focal Finance Tracker
# FinanceMate

A modern, privacy-focused expense tracking Progressive Web App (PWA) with AI-powered receipt scanning.

![Dashboard](images/dashboard.png)

[![Live Demo](https://img.shields.io/badge/demo-live-success)](https://focal.creative-geek.tech)
[![License](https://img.shields.io/badge/license-MIT-blue)]()

</div>
Expand All @@ -34,8 +33,8 @@ A modern, privacy-focused expense tracking Progressive Web App (PWA) with AI-pow

```bash
# Clone repository
git clone https://github.com/Creative-Geek/Focal.git
cd Focal
git clone https://github.com/yourusername/FinanceMate.git
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The repository URL contains a placeholder yourusername. Please update this to the correct repository path to ensure the clone command works for users.

cd FinanceMate

# Install dependencies
pnpm install
Expand Down Expand Up @@ -101,8 +100,8 @@ Built with [React](https://react.dev), [Cloudflare](https://cloudflare.com), [sh

<div align="center">

**[Live Demo](https://focal.creative-geek.tech)** • **[Documentation](docs/DEVELOPMENT.md)** • **[Report Bug](https://github.com/Creative-Geek/Focal/issues)**
**[Documentation](docs/DEVELOPMENT.md)** • **[Report Bug](https://github.com/yourusername/FinanceMate/issues)**
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The URL for reporting a bug also uses the yourusername placeholder. This should be updated to the correct repository issues URL.


Made with ❤️ by Creative Geek
Track smarter, spend better 💙

</div>
142 changes: 142 additions & 0 deletions e2e/user-flow.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import { test, expect } from '@playwright/test';

test.describe('User Flow', () => {
test('should allow user to login and see dashboard', async ({ page }) => {
// Mock login API
await page.route('**/api/auth/login', async route => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ token: 'fake-jwt-token', user: { id: 1, email: 'test@example.com' } }),
});
});

// Mock expenses API (empty list initially)
await page.route('**/api/expenses', async route => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify([]),
});
});

// Mock User API if needed (often checked on load)
await page.route('**/api/auth/me', async route => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ id: 1, email: 'test@example.com' }),
});
});

await page.goto('/login');

// Fill login form
await page.getByLabel('Email').fill('test@example.com');
await page.getByLabel('Password').fill('password123');
await page.getByRole('button', { name: 'Sign in' }).click();

// Verify redirect to dashboard
await expect(page).toHaveURL('/');
await expect(page.getByText('Current Balance')).toBeVisible();
});
Comment on lines +4 to +42
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

This test is failing because the assertions don't match the application's behavior after the redesign. The application now redirects to /home after login, not /. Additionally, the expected text 'Current Balance' is no longer present on the new home page. The test needs to be updated to reflect the new UI and routing logic.

For example, the URL assertion should be updated:

// Verify redirect to dashboard
await expect(page).toHaveURL('/home');
await expect(page.getByText('Track Your Spending with Ease')).toBeVisible();


test('should allow adding an expense', async ({ page }) => {
// Mock APIs
await page.route('**/api/auth/me', async route => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ id: 1, email: 'test@example.com' }),
});
});

await page.route('**/api/expenses', async route => {
if (route.request().method() === 'GET') {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify([]),
});
} else if (route.request().method() === 'POST') {
await route.fulfill({
status: 201,
contentType: 'application/json',
body: JSON.stringify({ id: 101, amount: 50, description: 'Lunch', date: new Date().toISOString() }),
});
}
});

// Bypass login by setting token (if app checks localstorage on load)
// Or just re-login. Re-login is safer with mocks.
await page.route('**/api/auth/login', async route => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ token: 'fake-jwt-token', user: { id: 1, email: 'test@example.com' } }),
});
});
Comment on lines +46 to +78
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The API mocking logic within this test is duplicated from the test above. To improve maintainability and reduce code duplication, this common setup should be extracted into a test.beforeEach block. This will ensure all tests in this suite start with the same consistent mock environment.


await page.goto('/login');
await page.getByLabel('Email').fill('test@example.com');
await page.getByLabel('Password').fill('password123');
await page.getByRole('button', { name: 'Sign in' }).click();
await expect(page).toHaveURL('/');

// Add Expense interaction
// Assuming there is a button to add expense.
// I need to know the UI. Usually a "+" button or "Add Expense".
// I will check the dashboard code or guess.
// Based on README images, there's likely an "Add Expense" button.

// For now, I'll pause there or look for the button.
// Let's assume there is an "Add Expense" button text or label.
// I'll wait for selector or just generic text.

// Looking at AddExpenseMenu.tsx might help knowing the trigger.
// But I'll write the test up to login for now and verify, then refine.
// Actually, I should write the full test if possible.
// I'll check AddExpenseMenu.tsx content quickly in next step if needed, but I'll write a basic check first.

const addBtn = page.getByRole('button', { name: /add expense/i });
await expect(addBtn).toBeVisible();
await addBtn.click();

// Choose Manual Entry
await page.getByRole('button', { name: /manual entry/i }).click();

// Fill form
await page.locator('#merchant').fill('Coffee Shop');
await page.locator('#total').fill('5.50');
// Date defaults to today usually, but let's leave it or fill it if key

// Select Category (shadcn select)
await page.getByRole('combobox').click();
await page.getByRole('option', { name: 'Food & Drink' }).click();

// Mock Save API
await page.route('**/api/expenses', async route => {
// Handle both GET (refresh) and POST (save)
if (route.request().method() === 'GET') {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify([{ id: 101, amount: 5.5, total: 5.5, merchant: 'Coffee Shop', category: 'Food & Drink', date: new Date().toISOString(), currency: 'USD' }]),
});
} else if (route.request().method() === 'POST') {
await route.fulfill({
status: 201,
contentType: 'application/json',
body: JSON.stringify({ success: true, data: { id: 101 } }),
});
}
});
Comment on lines +118 to +133
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Re-mocking an API route (**/api/expenses) inside a test after navigation has already occurred is unreliable and can lead to flaky tests. The initial page load might use the first mock, while subsequent interactions use the second, creating race conditions. All necessary API mocks should be defined once at the beginning of the test, before any page interactions occur, to ensure a consistent and predictable test environment.


// Click Save
await page.getByRole('button', { name: /save expense/i }).click();

// Verify expense appears (via GET mock)
await expect(page.getByText('Coffee Shop')).toBeVisible();
await expect(page.getByText('$5.50')).toBeVisible();
});
});
136 changes: 50 additions & 86 deletions index.html
Original file line number Diff line number Diff line change
@@ -1,93 +1,57 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/focal-icon.svg" type="image/svg+xml" />
<link rel="apple-touch-icon" href="/apple-touch-icon-180x180.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="theme-color" content="#111827" />

<!-- Primary Meta Tags -->
<title>Focal: AI-Powered Expense Tracker & Receipt Scanner</title>
<meta
name="title"
content="Focal: AI-Powered Expense Tracker & Receipt Scanner"
/>
<meta
name="description"
content="Modern expense tracking PWA with AI-powered receipt scanning. Track expenses, scan receipts with Google Gemini AI, and manage your finances securely on Cloudflare's edge network."
/>
<meta
name="keywords"
content="expense tracker, receipt scanner, AI OCR, finance app, PWA, budget tracking, Gemini AI, expense management"
/>
<meta name="author" content="Creative Geek" />
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/focal-icon.svg" type="image/svg+xml" />
<link rel="apple-touch-icon" href="/apple-touch-icon-180x180.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="theme-color" content="#111827" />

<!-- Open Graph / Facebook -->
<meta property="og:type" content="website" />
<meta property="og:url" content="https://focal.creative-geek.tech/" />
<meta
property="og:title"
content="Focal: AI-Powered Expense Tracker & Receipt Scanner"
/>
<meta
property="og:description"
content="Modern expense tracking PWA with AI-powered receipt scanning. Track expenses, scan receipts with Google Gemini AI, and manage your finances securely."
/>
<meta
property="og:image"
content="https://focal.creative-geek.tech/images/dashboard.png"
/>
<meta
property="og:image:alt"
content="Focal Dashboard - Expense tracking with AI receipt scanning"
/>
<meta property="og:site_name" content="Focal Finance Tracker" />
<!-- Primary Meta Tags -->
<title>FinanceMate: AI Expense Tracker</title>
<meta name="title" content="FinanceMate: AI Expense Tracker" />
<meta name="description"
content="Track your spending with AI-powered receipt scanning. FinanceMate makes expense management effortless with smart categorization and insights." />
<meta name="keywords"
content="expense tracker, receipt scanner, AI OCR, finance app, PWA, budget tracking, expense management, FinanceMate" />

<!-- Twitter -->
<meta property="twitter:card" content="summary_large_image" />
<meta property="twitter:url" content="https://focal.creative-geek.tech/" />
<meta
property="twitter:title"
content="Focal: AI-Powered Expense Tracker & Receipt Scanner"
/>
<meta
property="twitter:description"
content="Modern expense tracking PWA with AI-powered receipt scanning. Track expenses, scan receipts with Google Gemini AI, and manage your finances securely."
/>
<meta
property="twitter:image"
content="https://focal.creative-geek.tech/images/dashboard.png"
/>
<meta
property="twitter:image:alt"
content="Focal Dashboard - Expense tracking with AI receipt scanning"
/>
<!-- Open Graph / Facebook -->
<meta property="og:type" content="website" />
<meta property="og:url" content="https://focal.creative-geek.tech/" />
<meta property="og:title" content="FinanceMate: AI Expense Tracker" />
<meta property="og:description"
content="Track your spending with AI-powered receipt scanning. FinanceMate makes expense management effortless." />
<meta property="og:image" content="https://focal.creative-geek.tech/images/dashboard.png" />
<meta property="og:image:alt" content="Focal Dashboard - Expense tracking with AI receipt scanning" />
<meta property="og:site_name" content="FinanceMate" />

<!-- PWA -->
<link rel="manifest" href="/manifest.webmanifest" />
<meta name="mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta
name="apple-mobile-web-app-status-bar-style"
content="black-translucent"
/>
<meta name="apple-mobile-web-app-title" content="Focal" />
<!-- Twitter -->
<meta property="twitter:card" content="summary_large_image" />
<meta property="twitter:url" content="https://focal.creative-geek.tech/" />
<meta property="twitter:title" content="FinanceMate: AI Expense Tracker" />
<meta property="twitter:description"
content="Track your spending with AI-powered receipt scanning. FinanceMate makes expense management effortless." />
<meta property="twitter:image" content="https://focal.creative-geek.tech/images/dashboard.png" />
<meta property="twitter:image:alt" content="Focal Dashboard - Expense tracking with AI receipt scanning" />
Comment on lines +21 to +36
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The og:url, twitter:url, og:image, and alt text meta tags contain hardcoded URLs and text from the old 'Focal' project. These should be updated to reflect the new 'FinanceMate' branding and the correct production URL, or removed if a live demo is not available.


<!-- Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap"
rel="stylesheet"
/>
<link
href="https://api.fontshare.com/v2/css?f[]=cal-sans@600&display=swap"
rel="stylesheet"
/>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
<!-- PWA -->
<link rel="manifest" href="/manifest.webmanifest" />
<meta name="mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="apple-mobile-web-app-title" content="SpendLens" />

<!-- Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet" />
<link href="https://api.fontshare.com/v2/css?f[]=cal-sans@600&display=swap" rel="stylesheet" />
</head>

<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>

</html>
16 changes: 16 additions & 0 deletions migrations/007_budgets.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
-- Migration: 007_budgets.sql
-- Created table for tracking user budgets per category

CREATE TABLE budgets (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL,
category TEXT NOT NULL,
limit_amount REAL NOT NULL,
currency TEXT NOT NULL,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
UNIQUE(user_id, category)
);

CREATE INDEX idx_budgets_user_id ON budgets(user_id);
19 changes: 19 additions & 0 deletions migrations/008_admin_and_logs.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
-- Migration: 008_admin_and_logs.sql
-- Add role and is_active to users, and create system_logs table

-- Add role and is_active columns to users table
ALTER TABLE users ADD COLUMN role TEXT DEFAULT 'user';
ALTER TABLE users ADD COLUMN is_active INTEGER DEFAULT 1;

-- Create system_logs table
CREATE TABLE system_logs (
id TEXT PRIMARY KEY,
level TEXT NOT NULL, -- 'info', 'warn', 'error'
message TEXT NOT NULL,
details TEXT, -- JSON string
timestamp INTEGER NOT NULL
);

-- Index for logs
CREATE INDEX idx_system_logs_timestamp ON system_logs(timestamp);
CREATE INDEX idx_system_logs_level ON system_logs(level);
18 changes: 18 additions & 0 deletions migrations/009_recurring_expenses.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
-- Create recurring expenses table
CREATE TABLE IF NOT EXISTS recurring_expenses (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL,
amount REAL NOT NULL,
currency TEXT NOT NULL,
category TEXT NOT NULL,
merchant TEXT NOT NULL,
description TEXT,
frequency TEXT NOT NULL CHECK(frequency IN ('daily', 'weekly', 'monthly', 'yearly')),
next_due_date INTEGER NOT NULL,
is_active INTEGER NOT NULL DEFAULT 1,
created_at INTEGER NOT NULL,
FOREIGN KEY (user_id) REFERENCES users(id)
);

-- Index for querying active recurring expenses
CREATE INDEX IF NOT EXISTS idx_recurring_expenses_user_active ON recurring_expenses(user_id, is_active);
4 changes: 4 additions & 0 deletions migrations/010_api_keys.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
-- Migration number: 010 2024-03-22T00:00:00.000Z
-- This migration is skipped because the api_keys table already exists (as user_settings)
-- and we are creating api_auth_keys in migration 011 instead.
SELECT 1;
Loading