# Project: Receipt Scanner Web Application UI/UX Upgrade ## Architecture - **Framework & Runtime**: Next.js 15 App Router, React 19, TypeScript 5.7 (strict), Tailwind CSS 3.4 - **Design System**: Zenith Silver (`#F6F9FF` background, `#FFFFFF` surface cards, `#E2E8F0` borders, `#000000` accents, crisp 0px sharp corners, Hanken Grotesk / Inter / JetBrains Mono typography) - **Data & Storage**: Local-first IndexedDB (`idb`) with PostgreSQL / Drizzle ORM schema compatibility - **Export Formats**: Dual-sheet `.xlsx` (ExcelJS), DATEV-compliant `.csv` (UTF-8 BOM), structured `.json` ## Feature Inventory | # | Feature | Description | Milestone | Source | Status | |---|---------|-------------|-----------|--------|:------:| | 1 | Full-page Drag & Drop Overlay | High-visibility global drag-and-drop backdrop overlay across dashboard | M1 | ORIGINAL_REQUEST R1 | DONE | | 2 | Dedicated Dropzone Component | Visual upload card supporting PDF, PNG, JPEG, WebP with laser scanline | M1 | ORIGINAL_REQUEST R1 | DONE | | 3 | Batch Upload Drawer & Queue | Multi-file batch queue with instant thumbnail previews, live progress bars | M1 | ORIGINAL_REQUEST R1 | DONE | | 4 | Error Boundaries & File Retry | Isolated per-file failure handling, corrupt file rejection without halting queue, quick retry/remove | M1 | ORIGINAL_REQUEST R1 | DONE | | 5 | Side-by-Side Dual Pane Modal | 50/50 split review modal with document on LEFT and editable fields on RIGHT | M2 | ORIGINAL_REQUEST R2 | DONE | | 6 | Interactive Document Viewer | CSS transform Zoom (0.25x–5x), Pan (grab/trackpad), Rotate, Fit-to-Page | M2 | ORIGINAL_REQUEST R2 | DONE | | 7 | Bounding-Box Visual Cues | 2-way highlight synchronization between image bounding boxes and extracted form fields | M2 | ORIGINAL_REQUEST R2 | DONE | | 8 | Dynamic Line Items Editor | Inline-editable table for line items (description, quantity, price, total) with add/delete row controls | M2 | ORIGINAL_REQUEST R2 | DONE | | 9 | Field Audit & Immediate Save | "AI Extracted" vs "Manually Edited" indicators, 1-click revert, debounced IndexedDB auto-save, Prev/Next navigation | M2 | ORIGINAL_REQUEST R2 | DONE | | 10 | Payment Method & Schema Extension | Added payment method selection and extended receipt schema | M2 | ORIGINAL_REQUEST R2 | DONE | | 11 | Editable Table Affordances | Clear dotted underlines, hover edit cues, human-edited flags in LiveTable | M3 | ORIGINAL_REQUEST R3 | DONE | | 12 | 3-Tier Status Badges | Standardized badges: Scanned (Emerald), Pending Review (Amber), Confirmed (Slate/Blue) | M3 | ORIGINAL_REQUEST R3 | DONE | | 13 | Floating Batch Actions Toolbar | Multi-select toolbar for Bulk Export (Selected Only to XLSX/CSV/JSON), Bulk Categorize, Bulk Status Update, Bulk Delete | M3 | ORIGINAL_REQUEST R3 | DONE | | 14 | Quick Search & Filter Chips Bar | Clickable chip filters for temporal ranges (Today/Week/Month/Year), status counters, categories, amount brackets | M3 | ORIGINAL_REQUEST R3 | DONE | | 15 | Responsive Dashboard Navigation | Mobile-friendly sidebar drawer / bottom nav (`hidden md:flex`) eliminating horizontal clipping on <768px | M4 | ORIGINAL_REQUEST R4 | DONE | | 16 | Interactive KPI Statistics Cards | Total Scanned, Monthly Spend, Pending Reviews, Average Accuracy cards with click-to-filter micro-interactions | M4 | ORIGINAL_REQUEST R4 | DONE | | 17 | Accessible Information Hierarchy | WCAG AA contrast, crisp typography, clean header layout, zero horizontal overflow | M4 | ORIGINAL_REQUEST R4 | DONE | | 18 | E2E & Unit Test Coverage | Comprehensive tests verifying R1–R4 features, build integrity, and typechecks | M5 | ORIGINAL_REQUEST AC | DONE | ## Milestones | # | Name | Scope | Dependencies | Status | |---|------|-------|-------------|--------| | M1 | Ingestion & Batch Upload (R1) | Global dropzone, batch queue drawer, thumbnails, progress, retry/remove | none | DONE | | M2 | Side-by-Side Inspector & Split Review (R2) | Dual-pane modal, DocumentViewer (zoom/pan), BBox sync, LineItemsEditor, audit badges | none | DONE | | M3 | Interactive Table & Batch Operations (R3) | Cell edit affordances, status badges, BatchActionBar, FilterChipsBar, selection hooks | none | DONE | | M4 | Accessible Hierarchy & Responsive Shell (R4) | Responsive Sidebar/TopNav, interactive KPI cards, mobile drawer, zero-overflow | M1, M2, M3 | DONE | | M5 | E2E Test Suite & Final Verification | Test infra, Tier 1-5 tests, adversarial tests, `npm run build`, `npx tsc --noEmit` | M1, M2, M3, M4 | DONE | ## Interface Contracts ### M1 Ingestion ↔ Dashboard - `BatchUploadDrawer`: Accepts `onComplete: (receipts: ProcessedReceipt[]) => void`, `isOpen: boolean`, `onClose: () => void`. - `BatchUploadDropzone`: Accepts `onFilesSelected: (files: File[]) => void`. - `GlobalDropzoneOverlay`: Listens to `window` drag events; triggers batch queue on file drop. ### M2 Inspector ↔ Table & Storage - `ReceiptInspectorModal`: Accepts `receipt: ProcessedReceipt`, `isOpen: boolean`, `onClose: () => void`, `onSave: (updated: ProcessedReceipt) => void`, `onNavigate?: (direction: 'prev' | 'next') => void`. - `ProcessedReceipt` schema additions: - `paymentMethod?: string` - `boundingBoxes?: Record` - `editedFields?: Record` - `originalExtraction?: Partial` ### M3 LiveTable ↔ BatchActionBar & FilterChipsBar - `useReceiptFilters`: returns `{ filteredReceipts, activePeriod, setPeriod, activeStatus, setStatus, activeCategory, setCategory, searchQuery, setSearchQuery, amountRange, setAmountRange, resetFilters }`. - `useTableSelection`: returns `{ selectedIds, isSelected, toggleSelect, toggleSelectAll, clearSelection, selectAll, count }`. - `BatchActionBar`: Accepts `selectedIds: string[]`, `receipts: ProcessedReceipt[]`, `onBulkDelete`, `onBulkCategorize`, `onBulkStatusUpdate`, `onBulkExport`, `onClearSelection`. ### M4 Responsive Shell ↔ Dashboard Layout - `Sidebar`: Desktop fixed sidebar (`hidden md:flex`) and mobile drawer (`block md:hidden`) triggered via `TopNav` hamburger button with smooth backdrop transition and zero horizontal overflow. - `TopNav`: Header with breadcrumb/view indicator, quick search trigger, user/workspace menu, and mobile drawer toggle button. - KPI Statistics Cards in `page.tsx`: Interactive cards for `Total Scanned`, `Monthly Spend`, `Pending Reviews`, and `Average Accuracy` (calculated dynamically from AI confidence/status with micro-interaction hover states and click-to-filter triggers). ## Code Layout ``` src/ ├── app/ │ ├── dashboard/ │ │ ├── layout.tsx # Responsive shell (TopNav + mobile drawer + desktop Sidebar) │ │ ├── page.tsx # Overview with KPI cards + Ingestion dropzone + LiveTable │ │ ├── activity/page.tsx # Activity archive with FilterChipsBar + LiveTable + BatchActionBar │ │ ├── export/page.tsx # Export hub with summary statistics & date filters │ │ └── settings/page.tsx # Settings & preferences ├── components/ │ ├── dashboard/ │ │ ├── BatchUploadDropzone.tsx # [M1] Dedicated ingestion dropzone card (DONE) │ │ ├── BatchUploadDrawer.tsx # [M1] Multi-file upload queue & progress modal/drawer (DONE) │ │ ├── GlobalDropzoneOverlay.tsx # [M1] Full-screen dragover overlay (DONE) │ │ ├── ReceiptInspectorModal.tsx # [M2] 50/50 Dual-pane review modal (DONE) │ │ ├── DocumentViewer.tsx # [M2] Zoom, pan, rotate & bounding-box canvas (DONE) │ │ ├── LineItemsEditor.tsx # [M2] Line item table & editor (DONE) │ │ ├── StatusBadge.tsx # [M3] 3-tier status badges (DONE) │ │ ├── BatchActionBar.tsx # [M3] Multi-select batch operations floating bar (DONE) │ │ ├── FilterChipsBar.tsx # [M3] Responsive filter chips bar (DONE) │ │ ├── LiveTable.tsx # [M3] Spreadsheet table with cell edit indicators (DONE) │ │ ├── Sidebar.tsx # [M4] Responsive sidebar & mobile drawer (DONE) │ │ ├── TopNav.tsx # [M4] Header with search, mobile trigger, status (DONE) │ │ └── KPICards.tsx # [M4] Interactive KPI cards with micro-interactions (DONE) └── lib/ ├── hooks/ │ ├── useReceiptFilters.ts # [M3] Filtering hook (DONE) │ └── useTableSelection.ts # [M3] Selection hook (DONE) ├── schema/ │ └── receipt.ts # [M2] Extended receipt schema (DONE) └── utils/ └── boundingBoxes.ts # [M2] Bounding box calculation & heuristics (DONE) ``` ## Appendix: Database Least Privilege **Why.** The app must connect to PostgreSQL with only the rights it actually needs — not superuser. The docker-compose defaults make `receipt_user` the database superuser (`POSTGRES_USER`). If the application is compromised, an attacker holding the app's credentials would otherwise get full control of the database: read every user's receipts, drop or alter tables, or grant themselves rights. A restricted runtime role limits the blast radius to reading and modifying rows. **Two roles.** - `receipt_user` — **owner / migration role**. Keeps full rights (DDL) and is used for migrations and schema init (`src/lib/db/init.ts`). Never the runtime connection in production. - `receipt_app` — **runtime role** (created by `scripts/db-permissions.sql`). LOGIN, no superuser, no CREATEDB/CREATEROLE. Granted exactly: CONNECT on the database, USAGE on schema `public` (no CREATE), SELECT/INSERT/UPDATE/DELETE on all tables, USAGE/SELECT on all sequences, and matching `ALTER DEFAULT PRIVILEGES` so future tables/sequences created by the owner during migrations are covered automatically. `CREATE` on `public` is additionally revoked from the PUBLIC pseudo-role. **How to apply.** - Fresh volume (`docker compose up` with no existing data): the postgres service mounts `scripts/db-permissions.sql` into `/docker-entrypoint-initdb.d/10-db-permissions.sql`; the image runs it once as `POSTGRES_USER` (superuser) before the app starts. - Existing database (e.g. the local dev DB): `node scripts/apply-db-permissions.mjs` — connects with the owner URL from `.env.local`, executes the same SQL, safe to re-run. Set `APP_DATABASE_PASSWORD` to override the documented default password (also rotates it on an existing role). **How to verify.** `node scripts/verify-db-permissions.mjs` connects both as the owner and as `receipt_app` and asserts: no superuser/CREATEDB/CREATEROLE, CONNECT + schema USAGE, DML on all tables/sequences, no schema CREATE, no DDL (a real `CREATE TABLE` attempt is denied), default privileges are in place, and the owner can still run DDL. Prints PASS/FAIL and exits 1 on failure. **Limitation.** The runtime role has no DDL, so migrations and schema init must use the owner URL. In production: run migrations with `DATABASE_URL` set to the owner URL, then run the app with `DATABASE_URL` (or `APP_DATABASE_URL`, plumbed through docker-compose) set to the restricted `receipt_app` URL.