From c5b34aee2b86406878867fc6da67c5c649fa80f8 Mon Sep 17 00:00:00 2001 From: Jaden Fix <163031992+jadenfix@users.noreply.github.com> Date: Mon, 12 Oct 2026 17:04:23 -0700 Subject: [PATCH 01/39] Restore styled and reachable invoice exception review workflow --- README.md | 26 +++++++++- frontend/.eslintrc.cjs | 14 ++++++ frontend/package-lock.json | 48 +++++++++++++++++++ frontend/package.json | 7 ++- frontend/postcss.config.cjs | 1 + frontend/src/App.tsx | 11 +++-- frontend/src/__tests__/App.test.tsx | 34 +++++++++++++ .../exception/components/InvoiceViewer.tsx | 2 - .../src/exception/components/Pagination.tsx | 25 +++++++++- .../src/exception/components/QueueTable.tsx | 38 +++++++++++---- .../src/exception/components/ReviewForm.tsx | 3 +- .../components/__tests__/Pagination.test.tsx | 22 +++++++++ .../components/__tests__/QueueTable.test.tsx | 20 ++++---- frontend/src/exception/hooks/useApi.ts | 1 - frontend/src/exception/index.tsx | 1 - frontend/src/exception/pages/DetailPage.tsx | 2 +- frontend/src/exception/pages/QueuePage.tsx | 2 +- frontend/src/hooks/useSearch.ts | 4 +- frontend/src/setupTests.ts | 5 +- frontend/tailwind.config.cjs | 5 ++ frontend/tsconfig.json | 1 + 21 files changed, 235 insertions(+), 37 deletions(-) create mode 100644 frontend/.eslintrc.cjs create mode 100644 frontend/postcss.config.cjs create mode 100644 frontend/src/__tests__/App.test.tsx create mode 100644 frontend/src/exception/components/__tests__/Pagination.test.tsx create mode 100644 frontend/tailwind.config.cjs diff --git a/README.md b/README.md index 74f4e89..2cc8a83 100644 --- a/README.md +++ b/README.md @@ -1205,4 +1205,28 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file --- -Built with ❤️ by the InvoiceFlow team \ No newline at end of file +Built with ❤️ by the InvoiceFlow team + +## Frontend review workflow + +```sh +cd frontend +npm ci +npm run lint +npm test -- --run --threads=false +npm run build +npm run dev +``` + +The signed-in dashboard links to `/exception`, where the existing review service +supplies the queue and invoice details. Queue pagination, accessible filtering, +keyboard sorting, and approve/reject forms are available. Set +`VITE_EXCEPTION_API_URL` to the exception service URL (default +`http://localhost:8007`) and run the authentication and exception services before +using the frontend. The frontend does not supply synthetic invoices when the +service is unavailable; it shows a retryable error. + +The component suite verifies queue interactions and pagination. These tests and +a production build do not establish live OCR/model accuracy or the full deployed +microservice pipeline. Record a demo only after exercising authentication → real +queue → invoice review → persisted approval/rejection against the intended stack. diff --git a/frontend/.eslintrc.cjs b/frontend/.eslintrc.cjs new file mode 100644 index 0000000..381d905 --- /dev/null +++ b/frontend/.eslintrc.cjs @@ -0,0 +1,14 @@ +module.exports = { + root: true, + env: { browser: true, es2020: true }, + parser: '@typescript-eslint/parser', + plugins: ['@typescript-eslint', 'react-hooks'], + extends: ['eslint:recommended', 'plugin:react-hooks/recommended'], + parserOptions: { ecmaVersion: 'latest', sourceType: 'module' }, + rules: { + 'no-unused-vars': 'off', + 'no-undef': 'off', + '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }], + }, + ignorePatterns: ['dist', 'node_modules'], +}; diff --git a/frontend/package-lock.json b/frontend/package-lock.json index d5592e9..2949229 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -8,9 +8,12 @@ "name": "invoiceflow-frontend", "version": "1.0.0", "dependencies": { + "@heroicons/react": "^2.2.0", + "@tanstack/react-query": "^5.104.2", "@types/mapbox-gl": "^2.7.18", "axios": "^1.6.0", "clsx": "^2.0.0", + "date-fns": "^3.6.0", "lucide-react": "^0.292.0", "mapbox-gl": "^2.15.0", "react": "^18.2.0", @@ -907,6 +910,15 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@heroicons/react": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@heroicons/react/-/react-2.2.0.tgz", + "integrity": "sha512-LMcepvRaS9LYHJGsF0zzmgKCUim/X3N/DQKc4jepAXJ7l8QxJ1PmxJzqplF2Z3FE4PqBAIGyJAQ/w4B5dsqbtQ==", + "license": "MIT", + "peerDependencies": { + "react": ">= 16 || ^19.0.0-rc" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.13.0", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", @@ -1417,6 +1429,32 @@ "dev": true, "license": "MIT" }, + "node_modules/@tanstack/query-core": { + "version": "5.104.2", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.104.2.tgz", + "integrity": "sha512-/qbsxJK5c3Ve1aFIULWYNcT2jeRUAiD4HME6Eu2ZdpofHoVgAjpkQhLg5xHuOoBkTPQUJLfP7zOxy0qsBcqROA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.104.2", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.104.2.tgz", + "integrity": "sha512-xKmzWGhadbE5wGS/ZmXShRITnI9oItox5GO14KvFZco3+vhTAyXS1erYzJWdiY280zapAGJXFUL9nHgwP6SV/g==", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "5.104.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18 || ^19" + } + }, "node_modules/@testing-library/dom": { "version": "10.4.0", "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz", @@ -2975,6 +3013,16 @@ "node": ">=14" } }, + "node_modules/date-fns": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.6.0.tgz", + "integrity": "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, "node_modules/debug": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", diff --git a/frontend/package.json b/frontend/package.json index 45d0ed0..e9f08a0 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -31,7 +31,10 @@ "react-hot-toast": "^2.4.1", "lucide-react": "^0.292.0", "clsx": "^2.0.0", - "tailwind-merge": "^2.0.0" + "tailwind-merge": "^2.0.0", + "@heroicons/react": "^2.2.0", + "date-fns": "^3.6.0", + "@tanstack/react-query": "^5.104.2" }, "devDependencies": { "@types/react": "^18.2.37", @@ -61,4 +64,4 @@ "node": ">=18.0.0", "npm": ">=9.0.0" } -} \ No newline at end of file +} diff --git a/frontend/postcss.config.cjs b/frontend/postcss.config.cjs new file mode 100644 index 0000000..cce4985 --- /dev/null +++ b/frontend/postcss.config.cjs @@ -0,0 +1 @@ +module.exports = { plugins: { tailwindcss: {}, autoprefixer: {} } }; diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 68b5fe0..f24269a 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,7 +1,8 @@ import React, { useEffect } from 'react' -import { Routes, Route, Navigate } from 'react-router-dom' +import { Routes, Route, Navigate, Link } from 'react-router-dom' import { useAuthStore } from '@/stores/authStore' import { LoginForm } from '@/components/LoginForm' +import { ExceptionApp } from './exception' // Protected Route Component interface ProtectedRouteProps { @@ -22,7 +23,7 @@ const ProtectedRoute: React.FC = ({ children }) => { return isAuthenticated ? <>{children} : } -// Dashboard Component (placeholder for now) +// Invoice review entry point const Dashboard: React.FC = () => { const { user, logout } = useAuthStore() @@ -51,8 +52,9 @@ const Dashboard: React.FC = () => {
-

Dashboard Coming Soon

-

Your InvoiceFlow dashboard will be available here.

+

Review invoices with confidence

+

Inspect extracted line items, compare matching confidence, and approve or reject exceptions.

+ Open review queue
@@ -88,6 +90,7 @@ const App: React.FC = () => {
} /> + } /> false, + clearToken: vi.fn(), + loadUser: vi.fn().mockResolvedValue(undefined), +}; +vi.mock('@/stores/authStore', () => ({ useAuthStore: () => state })); + +describe('review workflow entry point', () => { + it('opens the real review queue from the dashboard', async () => { + const fetchMock = vi.spyOn(globalThis, 'fetch').mockResolvedValue({ + ok: true, + json: async () => ({ items: [], total: 0, page: 1, page_size: 20, has_next: false, has_prev: false }), + } as Response); + try { + render(); + fireEvent.click(screen.getByRole('link', { name: 'Open review queue' })); + expect(await screen.findByRole('heading', { name: 'Exception Review Queue' })).toBeInTheDocument(); + expect(await screen.findByText('All invoices have been processed and reviewed.')).toBeInTheDocument(); + expect(fetchMock).toHaveBeenCalledWith(expect.stringContaining('/api/v1/review/queue'), expect.any(Object)); + } finally { + fetchMock.mockRestore(); + } + }); +}); diff --git a/frontend/src/exception/components/InvoiceViewer.tsx b/frontend/src/exception/components/InvoiceViewer.tsx index e2f2231..ea7f142 100644 --- a/frontend/src/exception/components/InvoiceViewer.tsx +++ b/frontend/src/exception/components/InvoiceViewer.tsx @@ -2,11 +2,9 @@ * Invoice Viewer Component for displaying detailed invoice information */ -import React from 'react'; import { format } from 'date-fns'; import { DocumentTextIcon, - CurrencyDollarIcon, CalendarIcon, UserIcon, ClockIcon, diff --git a/frontend/src/exception/components/Pagination.tsx b/frontend/src/exception/components/Pagination.tsx index 0519ecb..5a4e411 100644 --- a/frontend/src/exception/components/Pagination.tsx +++ b/frontend/src/exception/components/Pagination.tsx @@ -1 +1,24 @@ - \ No newline at end of file +interface PaginationProps { + page: number; + page_size: number; + total: number; + has_next: boolean; + has_prev: boolean; + onPageChange: (page: number) => void; +} + +export function Pagination({ page, page_size, total, has_next, has_prev, onPageChange }: PaginationProps) { + const pages = Math.max(1, Math.ceil(total / page_size)); + return ( + + ); +} diff --git a/frontend/src/exception/components/QueueTable.tsx b/frontend/src/exception/components/QueueTable.tsx index e55c35f..f4244f3 100644 --- a/frontend/src/exception/components/QueueTable.tsx +++ b/frontend/src/exception/components/QueueTable.tsx @@ -2,14 +2,13 @@ * Queue Table Component for displaying invoices needing review */ -import React, { useState } from 'react'; +import { useState } from 'react'; import { format } from 'date-fns'; import { ChevronUpIcon, ChevronDownIcon, EyeIcon, FunnelIcon, - XMarkIcon } from '@heroicons/react/24/outline'; import { InvoiceQueueItem, QueueFilters } from '../types'; @@ -64,7 +63,7 @@ export function QueueTable({ }; const formatAmount = (amount?: number) => { - if (!amount) return '-'; + if (amount == null) return '-'; return new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', @@ -81,7 +80,7 @@ export function QueueTable({ }; const formatConfidence = (score?: number) => { - if (!score) return '-'; + if (score == null) return '-'; return `${(score * 100).toFixed(1)}%`; }; @@ -107,11 +106,12 @@ export function QueueTable({
-
-
-
@@ -187,22 +229,28 @@ export function InvoiceViewer({ invoice }: InvoiceViewerProps) {
-
Confidence Score:
+
+ Confidence Score: +
- 0.8 - ? 'bg-green-100 text-green-800' - : (invoice.confidence_score || 0) > 0.6 - ? 'bg-yellow-100 text-yellow-800' - : 'bg-red-100 text-red-800' - }`}> + 0.8 + ? "bg-green-100 text-green-800" + : (invoice.confidence_score || 0) > 0.6 + ? "bg-yellow-100 text-yellow-800" + : "bg-red-100 text-red-800" + }`} + > {formatConfidence(invoice.confidence_score)}
{invoice.match_details && (
-
Match Details:
+
+ Match Details: +
@@ -226,27 +274,39 @@ export function InvoiceViewer({ invoice }: InvoiceViewerProps) {
             
             
-
Reviewed By:
+
+ Reviewed By: +
{invoice.reviewed_by}
-
Reviewed At:
-
{formatDateTime(invoice.reviewed_at)}
+
+ Reviewed At: +
+
+ {formatDateTime(invoice.reviewed_at)} +
Status:
- + {getStatusIcon(invoice.matched_status)} - {invoice.matched_status.replace('_', ' ')} + + {invoice.matched_status.replace("_", " ")} +
- + {invoice.review_notes && (
-
Review Notes:
+
+ Review Notes: +
{invoice.review_notes} @@ -266,15 +326,19 @@ export function InvoiceViewer({ invoice }: InvoiceViewerProps) {
Created At:
-
{formatDateTime(invoice.created_at)}
+
+ {formatDateTime(invoice.created_at)} +
Updated At:
-
{formatDateTime(invoice.updated_at)}
+
+ {formatDateTime(invoice.updated_at)} +
); -} \ No newline at end of file +} diff --git a/frontend/src/exception/components/Pagination.tsx b/frontend/src/exception/components/Pagination.tsx index 5a4e411..d281509 100644 --- a/frontend/src/exception/components/Pagination.tsx +++ b/frontend/src/exception/components/Pagination.tsx @@ -7,17 +7,45 @@ interface PaginationProps { onPageChange: (page: number) => void; } -export function Pagination({ page, page_size, total, has_next, has_prev, onPageChange }: PaginationProps) { +export function Pagination({ + page, + page_size, + total, + has_next, + has_prev, + onPageChange, +}: PaginationProps) { const pages = Math.max(1, Math.ceil(total / page_size)); return ( -