From 75567b6a7d4166cdb6073ec243ced9d7646b51ac Mon Sep 17 00:00:00 2001 From: Graeme Date: Mon, 30 Mar 2026 19:15:28 +0200 Subject: [PATCH 1/3] store hourly rate per client --- .github/ISSUES/01-unify-env.md | 13 ++++ .github/ISSUES/02-harden-nextauth.md | 13 ++++ .github/ISSUES/03-rate-limiting.md | 12 ++++ .github/ISSUES/04-centralized-validation.md | 12 ++++ .../ISSUES/05-standardize-getserversession.md | 12 ++++ .github/ISSUES/06-prisma-connection-safety.md | 12 ++++ .github/ISSUES/07-persist-timer.md | 13 ++++ .github/ISSUES/08-manual-time-entries.md | 13 ++++ .github/ISSUES/09-invoice-features.md | 14 +++++ .github/ISSUES/10-timezone-handling.md | 12 ++++ .../ISSUES/11-accessibility-improvements.md | 12 ++++ .github/ISSUES/12-offline-ux-optimistic.md | 12 ++++ .github/ISSUES/13-fix-prisma-ownership.md | 12 ++++ .github/ISSUES/14-centralize-types.md | 12 ++++ .github/ISSUES/15-normalize-api-fetch-urls.md | 12 ++++ .github/ISSUES/16-structured-logging.md | 13 ++++ .../ISSUES/17-disable-prisma-query-logging.md | 11 ++++ .github/ISSUES/18-add-lint-precommit-hooks.md | 12 ++++ .github/ISSUES/19-integration-api-tests.md | 12 ++++ .github/ISSUES/20-replace-fetch-with-msw.md | 12 ++++ .github/ISSUES/21-add-e2e-tests.md | 12 ++++ .github/ISSUES/22-harden-jest-setup.md | 12 ++++ .github/ISSUES/23-expand-api-route-tests.md | 12 ++++ .github/ISSUES/README.md | 11 ++++ .github/create_issues.sh | 28 +++++++++ __tests__/InvoicesPage.test.tsx | 59 ++++++++++++++++++- app/api/dashboard/route.ts | 9 +-- app/api/invoices/[id]/route.ts | 2 +- app/api/invoices/route.ts | 29 +++++---- app/invoices/page.tsx | 24 +++++++- app/timer/page.tsx | 4 +- .../migration.sql | 2 + prisma/schema.prisma | 6 +- 33 files changed, 435 insertions(+), 21 deletions(-) create mode 100644 .github/ISSUES/01-unify-env.md create mode 100644 .github/ISSUES/02-harden-nextauth.md create mode 100644 .github/ISSUES/03-rate-limiting.md create mode 100644 .github/ISSUES/04-centralized-validation.md create mode 100644 .github/ISSUES/05-standardize-getserversession.md create mode 100644 .github/ISSUES/06-prisma-connection-safety.md create mode 100644 .github/ISSUES/07-persist-timer.md create mode 100644 .github/ISSUES/08-manual-time-entries.md create mode 100644 .github/ISSUES/09-invoice-features.md create mode 100644 .github/ISSUES/10-timezone-handling.md create mode 100644 .github/ISSUES/11-accessibility-improvements.md create mode 100644 .github/ISSUES/12-offline-ux-optimistic.md create mode 100644 .github/ISSUES/13-fix-prisma-ownership.md create mode 100644 .github/ISSUES/14-centralize-types.md create mode 100644 .github/ISSUES/15-normalize-api-fetch-urls.md create mode 100644 .github/ISSUES/16-structured-logging.md create mode 100644 .github/ISSUES/17-disable-prisma-query-logging.md create mode 100644 .github/ISSUES/18-add-lint-precommit-hooks.md create mode 100644 .github/ISSUES/19-integration-api-tests.md create mode 100644 .github/ISSUES/20-replace-fetch-with-msw.md create mode 100644 .github/ISSUES/21-add-e2e-tests.md create mode 100644 .github/ISSUES/22-harden-jest-setup.md create mode 100644 .github/ISSUES/23-expand-api-route-tests.md create mode 100644 .github/ISSUES/README.md create mode 100644 .github/create_issues.sh create mode 100644 prisma/migrations/20260330120000_add_invoice_hourly_rate/migration.sql diff --git a/.github/ISSUES/01-unify-env.md b/.github/ISSUES/01-unify-env.md new file mode 100644 index 0000000..a501b86 --- /dev/null +++ b/.github/ISSUES/01-unify-env.md @@ -0,0 +1,13 @@ +Title: Unify and validate environment variable names + +Description: +README, runtime code, and `app/lib/env.ts` use inconsistent names for secrets (e.g., `AUTH_SECRET` vs `NEXTAUTH_SECRET`) which causes confusion and runtime errors. CI and docs don't enforce a single canonical set of env vars. + +Acceptance criteria: +- Update `.env.example` and `README.md` to list canonical env variable names. +- Add runtime validation in `app/lib/env.ts` that throws with a clear message when required vars are missing. +- Add a CI check (node script or lint rule) that ensures `.env.example` and code expectations match. +- Document env usage in README. + +Labels: enhancement, tech-debt +Priority: medium diff --git a/.github/ISSUES/02-harden-nextauth.md b/.github/ISSUES/02-harden-nextauth.md new file mode 100644 index 0000000..ca3a14f --- /dev/null +++ b/.github/ISSUES/02-harden-nextauth.md @@ -0,0 +1,13 @@ +Title: Harden NextAuth session and cookie settings + +Description: +Session/cookie settings are not explicitly configured; production should use secure, short-lived session cookies and appropriate SameSite settings to reduce risk. + +Acceptance criteria: +- Update `auth.ts` to specify secure cookies, `sameSite`, explicit session TTL, and cookie options for production. +- Add tests or runtime checks to ensure `secure` is enabled when `NODE_ENV=production`. +- Add a short README note describing session hardening choices. +- Ensure session cookies include `httpOnly`. + +Labels: security, enhancement +Priority: high diff --git a/.github/ISSUES/03-rate-limiting.md b/.github/ISSUES/03-rate-limiting.md new file mode 100644 index 0000000..cbff027 --- /dev/null +++ b/.github/ISSUES/03-rate-limiting.md @@ -0,0 +1,12 @@ +Title: Add rate limiting / abuse protection to API routes + +Description: +Public API routes (e.g., invoices, timer, clients) lack rate limits which may enable abuse or accidental overload. + +Acceptance criteria: +- Add a middleware or route-level rate limiter (IP + auth-aware) and apply to sensitive routes in `app/api/*`. +- Document limits and provide error responses with `429` and `Retry-After` header. +- Add basic tests simulating throttling behavior. + +Labels: security, enhancement +Priority: medium diff --git a/.github/ISSUES/04-centralized-validation.md b/.github/ISSUES/04-centralized-validation.md new file mode 100644 index 0000000..4ce8605 --- /dev/null +++ b/.github/ISSUES/04-centralized-validation.md @@ -0,0 +1,12 @@ +Title: Add centralized input validation for API endpoints + +Description: +Routes perform ad-hoc validation. Using a schema validator (e.g., `zod`) will standardize validation, produce consistent error messages, and avoid malformed data reaching Prisma. + +Acceptance criteria: +- Introduce schema definitions (Zod) for request payloads (clients, timer, invoices). +- Integrate validation in each API route with consistent 4xx error responses. +- Add unit tests verifying validation errors for bad inputs. + +Labels: security, tech-debt +Priority: high diff --git a/.github/ISSUES/05-standardize-getserversession.md b/.github/ISSUES/05-standardize-getserversession.md new file mode 100644 index 0000000..796cc9a --- /dev/null +++ b/.github/ISSUES/05-standardize-getserversession.md @@ -0,0 +1,12 @@ +Title: Standardize getServerSession / NextAuth usage across routes + +Description: +Codebase imports NextAuth helpers inconsistently (different import paths), which can lead to subtle bugs and makes review harder. + +Acceptance criteria: +- Pick one canonical import pattern for server session retrieval and document it (e.g., `import { getServerSession } from 'next-auth'`). +- Update all API routes to use the canonical pattern. +- Add a small lint rule or codeowner guidance stating the pattern. + +Labels: tech-debt +Priority: low diff --git a/.github/ISSUES/06-prisma-connection-safety.md b/.github/ISSUES/06-prisma-connection-safety.md new file mode 100644 index 0000000..864a448 --- /dev/null +++ b/.github/ISSUES/06-prisma-connection-safety.md @@ -0,0 +1,12 @@ +Title: Ensure Prisma connection handling is safe for serverless + +Description: +Database connection pooling and client lifecycle must follow recommended patterns for serverless environments to avoid connection exhaustion. + +Acceptance criteria: +- Confirm `app/lib/prisma.ts` follows the recommended global singleton pattern and conditional logging. +- Add environment-aware pooling or documentation on using a proxy (PgBouncer) in production. +- Add CI smoke test verifying the Prisma client can initialize in a simulated serverless environment. + +Labels: enhancement, tech-debt +Priority: medium diff --git a/.github/ISSUES/07-persist-timer.md b/.github/ISSUES/07-persist-timer.md new file mode 100644 index 0000000..601c5cf --- /dev/null +++ b/.github/ISSUES/07-persist-timer.md @@ -0,0 +1,13 @@ +Title: Persist running timer across reloads and show active timer in Nav + +Description: +The running timer resets on page reload and isn't surfaced globally. Users expect timers to persist across reloads and be visible in the site header. + +Acceptance criteria: +- Persist active timer state in `localStorage` (or IndexedDB) and reconcile with server state on load. +- Display active timer in `app/components/Nav.tsx`. +- Ensure synchronization handles clock drift and re-connect scenarios. +- Add unit tests for persistence and reconciliation logic. + +Labels: enhancement +Priority: high diff --git a/.github/ISSUES/08-manual-time-entries.md b/.github/ISSUES/08-manual-time-entries.md new file mode 100644 index 0000000..c5b144e --- /dev/null +++ b/.github/ISSUES/08-manual-time-entries.md @@ -0,0 +1,13 @@ +Title: Allow manual time entry creation, editing, and deletion + +Description: +Users need the ability to add or correct time entries manually (not only start/stop). Current UI lacks edit/delete flows for time entries. + +Acceptance criteria: +- Add UI for creating manual entries (start/end or duration + date) on `app/timer/page.tsx`. +- Add edit and delete flows in UI and corresponding API endpoints with ownership checks. +- Add validation for overlapping entries and sensible UX for edits. +- Add tests covering the CRUD flows. + +Labels: enhancement +Priority: high diff --git a/.github/ISSUES/09-invoice-features.md b/.github/ISSUES/09-invoice-features.md new file mode 100644 index 0000000..460b288 --- /dev/null +++ b/.github/ISSUES/09-invoice-features.md @@ -0,0 +1,14 @@ +Title: Improve invoice features (unique numbers, currency, templates, emailing) + +Description: +Invoice generation needs production features: deterministic unique invoice numbers, currency support, selectable templates, and ability to email/send invoices. + +Acceptance criteria: +- Add invoice numbering scheme (configurable prefix + counter or timestamp). +- Add currency support and formatting in invoice generation UI. +- Implement simple templates and ability to select/export PDF. +- Add server-side email sending (SMTP or transactional provider) with retry and audit logging. +- Add tests for generation and email sending (integration or E2E). + +Labels: enhancement +Priority: high diff --git a/.github/ISSUES/10-timezone-handling.md b/.github/ISSUES/10-timezone-handling.md new file mode 100644 index 0000000..6d63c6e --- /dev/null +++ b/.github/ISSUES/10-timezone-handling.md @@ -0,0 +1,12 @@ +Title: Add timezone-aware date handling and consistent formatting + +Description: +Dates/times are displayed inconsistently and may mislead users across time zones. + +Acceptance criteria: +- Adopt a timezone-aware library (e.g., `date-fns-tz` or `luxon`) and normalize storage/formatting (store UTC, display local). +- Update dashboard, invoices, and timer pages to use the same formatting function. +- Add tests ensuring consistent formatting across a sample set of time zones. + +Labels: enhancement +Priority: medium diff --git a/.github/ISSUES/11-accessibility-improvements.md b/.github/ISSUES/11-accessibility-improvements.md new file mode 100644 index 0000000..367b9db --- /dev/null +++ b/.github/ISSUES/11-accessibility-improvements.md @@ -0,0 +1,12 @@ +Title: Improve accessibility across UI + +Description: +Several UI components lack ARIA attributes, keyboard focus management, and may fail contrast checks. + +Acceptance criteria: +- Audit key pages (`Nav`, `Timer`, `Clients`, `Invoices`) and fix missing labels, focus order, and keyboard operability. +- Ensure color contrast meets WCAG AA for primary UI elements. +- Add accessibility checks to CI using axe or similar. + +Labels: enhancement +Priority: medium diff --git a/.github/ISSUES/12-offline-ux-optimistic.md b/.github/ISSUES/12-offline-ux-optimistic.md new file mode 100644 index 0000000..bd3919c --- /dev/null +++ b/.github/ISSUES/12-offline-ux-optimistic.md @@ -0,0 +1,12 @@ +Title: Improve offline UX and optimistic updates + +Description: +CRUD flows are synchronous and may leave the UI stale during network issues. Offline-first behavior and optimistic updates will improve perceived performance. + +Acceptance criteria: +- Integrate React Query (or SWR) for data fetching with optimistic updates and retry/backoff. +- Add UI states for offline mode and queued actions if possible (timer persistence already required). +- Add tests simulating offline/slow networks and verify UX. + +Labels: enhancement +Priority: medium diff --git a/.github/ISSUES/13-fix-prisma-ownership.md b/.github/ISSUES/13-fix-prisma-ownership.md new file mode 100644 index 0000000..1c45a2e --- /dev/null +++ b/.github/ISSUES/13-fix-prisma-ownership.md @@ -0,0 +1,12 @@ +Title: Fix Prisma ownership checks for update/delete operations + +Description: +Some API routes pass composite `where` objects (e.g., `{ id, userId }`) that are invalid unless a composite unique exists. Ownership must be enforced via `findFirst` or separate checks. + +Acceptance criteria: +- Replace problematic update/delete calls in API routes (`app/api/clients/[id]/route.ts`, `app/api/timer/[id]/route.ts`, and similar) with a `findFirst({ where: { id, userId } })` ownership check followed by `update({ where: { id } })` or `delete({ where: { id } })`. +- Add unit/integration tests validating ownership enforcement (401/404 on invalid access). +- Add a short comment/example showing the correct pattern for future contributors. + +Labels: bug, tech-debt +Priority: high diff --git a/.github/ISSUES/14-centralize-types.md b/.github/ISSUES/14-centralize-types.md new file mode 100644 index 0000000..b0ac738 --- /dev/null +++ b/.github/ISSUES/14-centralize-types.md @@ -0,0 +1,12 @@ +Title: Centralize shared types and interfaces + +Description: +Interfaces (Invoice, Client, TimeEntry) are duplicated across pages. Centralized types improve maintainability. + +Acceptance criteria: +- Add `app/lib/types.ts` (or `app/types.ts`) with shared interfaces. +- Update pages and API routes to import shared types. +- Add a lint rule or README note encouraging use of shared types. + +Labels: tech-debt +Priority: medium diff --git a/.github/ISSUES/15-normalize-api-fetch-urls.md b/.github/ISSUES/15-normalize-api-fetch-urls.md new file mode 100644 index 0000000..ec954b6 --- /dev/null +++ b/.github/ISSUES/15-normalize-api-fetch-urls.md @@ -0,0 +1,12 @@ +Title: Normalize API fetch URLs to absolute root + +Description: +Code inconsistently uses `api/clients` vs `/api/clients`, causing bugs when base paths differ. + +Acceptance criteria: +- Update client-side fetch calls to use absolute root URLs (leading `/api/...`) everywhere. +- Add a small utility `apiFetch('/clients')` to centralize base path handling (optional). +- Add unit tests covering fetch URL formation. + +Labels: tech-debt +Priority: low diff --git a/.github/ISSUES/16-structured-logging.md b/.github/ISSUES/16-structured-logging.md new file mode 100644 index 0000000..effdfbc --- /dev/null +++ b/.github/ISSUES/16-structured-logging.md @@ -0,0 +1,13 @@ +Title: Improve error handling and structured logging in API routes + +Description: +API routes return ad-hoc errors and lack structured logs. Adding a logger and request ids aids debugging. + +Acceptance criteria: +- Integrate a lightweight logger (pino/winston/console wrapper) supporting structured JSON logs. +- Add request id propagation (via middleware) and include in error responses/logs. +- Update a few representative routes to use the logger and add a sample log format in README. +- Add tests ensuring errors produce consistent shapes. + +Labels: enhancement, tech-debt +Priority: medium diff --git a/.github/ISSUES/17-disable-prisma-query-logging.md b/.github/ISSUES/17-disable-prisma-query-logging.md new file mode 100644 index 0000000..468639d --- /dev/null +++ b/.github/ISSUES/17-disable-prisma-query-logging.md @@ -0,0 +1,11 @@ +Title: Disable verbose Prisma query logging in production + +Description: +Prisma currently logs `query` events; in production this can leak data and increase noise. + +Acceptance criteria: +- Modify `app/lib/prisma.ts` to enable query logging only in development or when a DEBUG flag is set. +- Add CI check or README note describing logging behavior. + +Labels: tech-debt +Priority: low diff --git a/.github/ISSUES/18-add-lint-precommit-hooks.md b/.github/ISSUES/18-add-lint-precommit-hooks.md new file mode 100644 index 0000000..c8816d4 --- /dev/null +++ b/.github/ISSUES/18-add-lint-precommit-hooks.md @@ -0,0 +1,12 @@ +Title: Add ESLint/Prettier pre-commit hooks and CI enforcement + +Description: +Ensure consistent code style and that linting runs before PRs are merged. + +Acceptance criteria: +- Add `lint-staged` + `husky` pre-commit hooks to run `eslint --fix` and `prettier`. +- Ensure CI enforces linting and formatting and fails on violations. +- Document how to run linters locally. + +Labels: testing, tech-debt +Priority: medium diff --git a/.github/ISSUES/19-integration-api-tests.md b/.github/ISSUES/19-integration-api-tests.md new file mode 100644 index 0000000..9af6631 --- /dev/null +++ b/.github/ISSUES/19-integration-api-tests.md @@ -0,0 +1,12 @@ +Title: Add integration tests for API routes with a test DB + +Description: +API routes are only lightly covered; integration tests against a test Postgres will prevent regressions and validate ownership/validation logic. + +Acceptance criteria: +- Configure a test database and migrations for CI (use `DATABASE_URL` override or dockerized DB). +- Add integration tests for key routes (clients, timer, invoices) including auth, validation, and ownership checks. +- Ensure CI runs migrations and tests as part of the existing workflow. + +Labels: testing +Priority: high diff --git a/.github/ISSUES/20-replace-fetch-with-msw.md b/.github/ISSUES/20-replace-fetch-with-msw.md new file mode 100644 index 0000000..b985413 --- /dev/null +++ b/.github/ISSUES/20-replace-fetch-with-msw.md @@ -0,0 +1,12 @@ +Title: Replace global.fetch mocks with MSW in component tests + +Description: +Many tests set `global.fetch` directly; MSW provides more accurate and isolated request mocking and better test hygiene. + +Acceptance criteria: +- Add MSW dev/test setup and update `jest.setup.ts` to start/stop MSW. +- Replace global.fetch mocks in `__tests__/*` with MSW handlers. +- Ensure tests run reliably and restore network behavior between tests. + +Labels: testing +Priority: medium diff --git a/.github/ISSUES/21-add-e2e-tests.md b/.github/ISSUES/21-add-e2e-tests.md new file mode 100644 index 0000000..07772d1 --- /dev/null +++ b/.github/ISSUES/21-add-e2e-tests.md @@ -0,0 +1,12 @@ +Title: Add E2E tests covering auth flows, timer lifecycle, and invoice generation + +Description: +Critical flows need end-to-end coverage to detect regressions across frontend, API, and DB. + +Acceptance criteria: +- Add Playwright (or Cypress) and example E2E tests covering login, starting/stopping timers, creating manual entries, and generating/downloading an invoice. +- Integrate E2E into CI (optionally on nightly runs if heavy). +- Document how to run E2E locally. + +Labels: testing +Priority: high diff --git a/.github/ISSUES/22-harden-jest-setup.md b/.github/ISSUES/22-harden-jest-setup.md new file mode 100644 index 0000000..115ec7f --- /dev/null +++ b/.github/ISSUES/22-harden-jest-setup.md @@ -0,0 +1,12 @@ +Title: Harden Jest setup to avoid leaking mocks between tests + +Description: +Current tests override `global.fetch` in many places and may leak mocks across tests. + +Acceptance criteria: +- Update `jest.setup.ts` to call `jest.restoreAllMocks()` / `jest.resetAllMocks()` appropriately between tests. +- Add `afterEach` cleanup hooks in tests that mock global state. +- Ensure CI runs tests with isolated environment. + +Labels: testing +Priority: medium diff --git a/.github/ISSUES/23-expand-api-route-tests.md b/.github/ISSUES/23-expand-api-route-tests.md new file mode 100644 index 0000000..5999e52 --- /dev/null +++ b/.github/ISSUES/23-expand-api-route-tests.md @@ -0,0 +1,12 @@ +Title: Expand API route test coverage for auth failures and ownership checks + +Description: +Add tests that assert proper status codes and messages when unauthenticated or unauthorized requests happen. + +Acceptance criteria: +- Add tests verifying unauthenticated requests return 401 where appropriate, and authenticated-but-not-owner requests return 404/403 as designed. +- Include tests for validation error shapes. +- Add these tests to the integration test suite executed in CI. + +Labels: testing +Priority: high diff --git a/.github/ISSUES/README.md b/.github/ISSUES/README.md new file mode 100644 index 0000000..b88b2dc --- /dev/null +++ b/.github/ISSUES/README.md @@ -0,0 +1,11 @@ +# Issue creation files + +This folder contains issue drafts used by `.github/create_issues.sh` to create GitHub issues via the `gh` CLI. + +To create issues from these files, ensure you have the GitHub CLI (`gh`) installed and authenticated, then run the script in the repository root: + +```bash +./.github/create_issues.sh +``` + +Each issue file begins with `Title: ...` on the first line and the remainder is used as the issue body. diff --git a/.github/create_issues.sh b/.github/create_issues.sh new file mode 100644 index 0000000..f4f7ac2 --- /dev/null +++ b/.github/create_issues.sh @@ -0,0 +1,28 @@ +#/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" +ISSUES_DIR="$ROOT_DIR/.github/ISSUES" + +if ! command -v gh >/dev/null 2>&1; then + echo "gh CLI not found. Install it and authenticate (gh auth login)." + exit 2 +fi + +for f in "$ISSUES_DIR"/*.md; do + # skip README + ["$f" = "$ISSUES_DIR/README.md"] && continue + title=$(sed -n '1s/^Title: \(.*\)/\1/p' "$f" | sed -n '1p') + if [ -z "$title" ]; then + echo "Skipping $f: no Title line found" + continue + fi + echo "Creating issue: $title" + gh issue create --title "$title" --body-file "$f" || { + echo "Failed to create issue: $title" + exit 1 + } + sleep 0.2 +done + +echo "All issues processed." diff --git a/__tests__/InvoicesPage.test.tsx b/__tests__/InvoicesPage.test.tsx index bbd7b4b..b220051 100644 --- a/__tests__/InvoicesPage.test.tsx +++ b/__tests__/InvoicesPage.test.tsx @@ -1,6 +1,7 @@ -import { render, screen } from '@testing-library/react'; +import { render, screen, fireEvent } from '@testing-library/react'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import InvoicesPage from '../app/invoices/page'; +import userEvent from '@testing-library/user-event'; // Mock fetch for clients and invoices let paid = false; @@ -68,4 +69,60 @@ describe('Invoices page', () => { ); expect(await screen.findByText(/unpaid/i)).toBeInTheDocument(); }); + + it('prefills hourly rate from selected client and sends it on create', async () => { + const queryClient = new QueryClient(); + const user = userEvent.setup(); + + // capture POST body + type InvoicePostBody = { hourlyRate?: number; [key: string]: unknown }; + let lastPostBody: InvoicePostBody | null = null; + (global.fetch as jest.Mock).mockImplementation((url, options) => { + const urlStr = String(url); + if (urlStr.endsWith('api/clients') || urlStr === 'api/clients') { + return Promise.resolve({ + ok: true, + json: () => Promise.resolve({ clients: [{ id: '1', name: 'Test Client', hourlyRate: 100 }] }), + }); + } + if ((urlStr.endsWith('/api/invoices') || urlStr === 'api/invoices' || urlStr.endsWith('api/invoices')) && options?.method === 'POST') { + lastPostBody = JSON.parse(String(options.body)); + return Promise.resolve({ ok: true, json: () => Promise.resolve({}) }); + } + if (urlStr.endsWith('api/invoices') || urlStr === 'api/invoices') { + return Promise.resolve({ ok: true, json: () => Promise.resolve({ invoices: [] }) }); + } + return Promise.resolve({ ok: true, json: () => Promise.resolve({}) }); + }); + + const rendered = render( + + + , + ); + + // wait for client select (combobox) to appear and pick the client + const select = await screen.findByRole('combobox'); + await user.selectOptions(select, '1'); + + // hourly rate input should be prefilled (find by displayed value) + const rateInput = await screen.findByDisplayValue('100'); + expect((rateInput as HTMLInputElement).value).toBe('100'); + + // fill required date inputs (native validation prevents submit otherwise) + const { container } = rendered; + const dateInputs = container.querySelectorAll('input[type="date"]') as NodeListOf; + const today = new Date().toISOString().slice(0, 10); + if (dateInputs.length >= 2) { + fireEvent.change(dateInputs[0], { target: { value: today } }); + fireEvent.change(dateInputs[1], { target: { value: today } }); + } + + // click generate invoice + const button = screen.getByRole('button', { name: /generate invoice/i }); + await user.click(button); + + expect(lastPostBody).not.toBeNull(); + expect(Number(lastPostBody!.hourlyRate)).toBe(100); + }); }); diff --git a/app/api/dashboard/route.ts b/app/api/dashboard/route.ts index e074077..b5db9be 100644 --- a/app/api/dashboard/route.ts +++ b/app/api/dashboard/route.ts @@ -43,7 +43,8 @@ export async function GET() { const startTime = new Date(entry.startTime as Date); const endTime = new Date(entry.endTime as Date); const hours = (endTime.getTime() - startTime.getTime()) / (1000 * 60 * 60); - const earnings = hours * entry.client.hourlyRate; + const rate = Number(entry.client.hourlyRate); + const earnings = hours * rate; totalEarnings += earnings; @@ -66,7 +67,7 @@ export async function GET() { const startTime = new Date(entry.startTime as Date); const endTime = new Date(entry.endTime as Date); const hours = (endTime.getTime() - startTime.getTime()) / (1000 * 60 * 60); - const earnings = hours * entry.client.hourlyRate; + const earnings = hours * Number(entry.client.hourlyRate); return { id: entry.id, @@ -88,7 +89,7 @@ export async function GET() { const startTime = new Date(entry.startTime as Date); const endTime = new Date(entry.endTime as Date); const hours = (endTime.getTime() - startTime.getTime()) / (1000 * 60 * 60); - return sum + hours * entry.client.hourlyRate; + return sum + hours * Number(entry.client.hourlyRate); }, 0); // Calculate monthly earnings @@ -101,7 +102,7 @@ export async function GET() { const startTime = new Date(entry.startTime as Date); const endTime = new Date(entry.endTime as Date); const hours = (endTime.getTime() - startTime.getTime()) / (1000 * 60 * 60); - return sum + hours * entry.client.hourlyRate; + return sum + hours * Number(entry.client.hourlyRate); }, 0); return Response.json({ diff --git a/app/api/invoices/[id]/route.ts b/app/api/invoices/[id]/route.ts index 7b0d5d6..5dd1276 100644 --- a/app/api/invoices/[id]/route.ts +++ b/app/api/invoices/[id]/route.ts @@ -52,7 +52,7 @@ export async function GET(_req: Request, { params }: { params: Promise<{ id: str const start = new Date(entry.startTime as Date); const end = new Date(entry.endTime as Date); const hours = (end.getTime() - start.getTime()) / (1000 * 60 * 60); - const amount = hours * invoice.client.hourlyRate; + const amount = hours * Number(invoice.client.hourlyRate); return { id: entry.id, diff --git a/app/api/invoices/route.ts b/app/api/invoices/route.ts index fc134a0..9704f74 100644 --- a/app/api/invoices/route.ts +++ b/app/api/invoices/route.ts @@ -2,6 +2,7 @@ import { getServerSession } from 'next-auth'; import { authOptions } from '@/auth'; import { prisma } from '@/lib/prisma'; import { validateEnv } from '@/lib/env'; +import type { Prisma } from '@prisma/client'; export async function GET() { validateEnv(); @@ -36,7 +37,7 @@ export async function POST(req: Request) { return Response.json({ error: 'Unauthorized' }, { status: 401 }); } - const { clientId, startDate, endDate } = await req.json(); + const { clientId, startDate, endDate, hourlyRate } = await req.json(); if (!clientId || !startDate || !endDate) { return Response.json({ error: 'Missing required fields' }, { status: 400 }); @@ -86,17 +87,25 @@ export async function POST(req: Request) { return sum + hours; }, 0); - const totalAmount = totalHours * client.hourlyRate; + // Use provided hourlyRate (override) if present, otherwise fall back to client's stored rate + const rate = typeof hourlyRate !== 'undefined' && !isNaN(Number(hourlyRate)) + ? Number(hourlyRate) + : Number(client.hourlyRate); + + const totalAmount = totalHours * rate; + + const invoiceData: Prisma.InvoiceUncheckedCreateInput = { + totalHours: parseFloat(totalHours.toFixed(2)), + totalAmount: parseFloat(totalAmount.toFixed(2)), + hourlyRate: parseFloat(rate.toFixed(2)), + periodStart: from, + periodEnd: to, + clientId, + userId: user.id, + }; const invoice = await prisma.invoice.create({ - data: { - totalHours: parseFloat(totalHours.toFixed(2)), - totalAmount: parseFloat(totalAmount.toFixed(2)), - periodStart: from, - periodEnd: to, - clientId, - userId: user.id, - }, + data: invoiceData, include: { client: true }, }); diff --git a/app/invoices/page.tsx b/app/invoices/page.tsx index c0e012e..ea77f12 100644 --- a/app/invoices/page.tsx +++ b/app/invoices/page.tsx @@ -82,6 +82,7 @@ export default function InvoicesPage() { clientId: '', startDate: '', endDate: '', + hourlyRate: '' }); const queryClient = useQueryClient(); @@ -125,7 +126,7 @@ export default function InvoicesPage() { }); if (res.ok) { - setFormData({ clientId: '', startDate: '', endDate: '' }); + setFormData({ clientId: '', startDate: '', endDate: '', hourlyRate: '' }); queryClient.invalidateQueries({ queryKey: ['invoices'] }); } else { const error = await res.json(); @@ -190,7 +191,15 @@ export default function InvoicesPage() { setFormData({ ...formData, hourlyRate: e.target.value })} + className="w-full px-4 py-2 border border-slate-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-slate-900" + min="0" + step="0.01" + /> +