Add comprehensive CHANGELOG documenting all changes

Covers all work from this session:
- Restock history tracking (stock_adjustments table, logging)
- Payment tracking (payment_method, amount_paid on orders)
- Reports page (5 report sections, date filtering)
- Girl Scouts trefoil logo
- Mobile network access (Vite host: true)
- Bugs found and fixed during development iteration
- Full code review fixes (P0-P3): batching bug, ID validation,
  delete reference checks, audit trail enforcement, global error
  handler, input validation, client robustness, schema migrations,
  useEffect deps
- README and CLAUDE.md documentation updates

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
adamp 2026-02-09 21:40:11 -06:00
parent c00bb90cc0
commit 75caed2f29

287
CHANGELOG.md Normal file
View File

@ -0,0 +1,287 @@
# Changelog
All notable changes to the Cookie Tracker are documented in this file.
---
## 2026-02-09
### Feature: Restock History Tracking
**Commit:** `1ed2642` — *Add restock history tracking, payment tracking, and reports page*
Every stock change is now logged to a `stock_adjustments` table, creating a full audit trail for inventory movements.
**Schema** (`server/db.js`):
- Added `stock_adjustments` table with columns: `id`, `product_id` (FK → products), `adjustment` (integer, positive or negative), `reason` (text), `reference_id` (nullable, links to order ID), `created_at`.
**Server changes**:
- `server/routes/products.js``PATCH /:id/stock` now inserts a row into `stock_adjustments` with `reason='restock'` after updating the product quantity.
- `server/routes/products.js` — Added `GET /stock-history` endpoint (placed before `/:id` to avoid route collision). Accepts optional query params: `product_id`, `start`, `end`, `limit`. Returns stock adjustment rows joined with product name, ordered by most recent first.
- `server/routes/orders.js` — Stock adjustments are logged in all three transaction paths:
- **POST `/`** (create order): logs `reason='order_created'`, `adjustment=-qty`, `reference_id=orderId` for each item.
- **PUT `/:id`** (update order): logs `reason='order_updated'` with positive adjustment when restoring old stock, then negative adjustment when deducting new stock.
- **DELETE `/:id`** (delete order): logs `reason='order_deleted'` with positive adjustment for each restored item.
---
### Feature: Payment Tracking
**Commit:** `1ed2642` — *Add restock history tracking, payment tracking, and reports page*
Orders now track how they were paid and how much was paid, enabling balance-due tracking.
**Schema** (`server/db.js`):
- Added `payment_method` column (TEXT, nullable) to `orders` table. Valid values: `'cash'`, `'card'`, `'venmo'`, `'check'`, `'other'`, or `null`.
- Added `amount_paid` column (REAL, NOT NULL, default 0) to `orders` table.
- Both columns added via `ALTER TABLE` with try-catch for idempotency on existing databases. (Later migrated to versioned schema migrations — see below.)
**Server changes** (`server/routes/orders.js`):
- **POST `/`**: Accepts `payment_method` and `amount_paid` in request body. Validates `payment_method` against allowed values. Includes both in the INSERT statement.
- **PUT `/:id`**: Accepts `payment_method` and `amount_paid`. Includes in UPDATE statement, preserving existing values when not provided.
- GET endpoints already use `SELECT o.*`, so new columns are returned automatically.
**Client changes**:
- `client/src/pages/OrderNew.jsx`:
- Added `paymentMethod` state (dropdown: None/Cash/Card/Venmo/Check/Other) and `amountPaid` state (number input).
- Payment method and amount paid are on separate rows: Status + Payment method share a `form-row`, Amount paid sits below with `maxWidth: 200` to prevent overflow.
- When a payment method is selected and amount is empty, auto-fills with current order total.
- Both fields included in submit payload.
- `client/src/pages/OrderDetail.jsx`:
- **Display mode**: Shows payment method (capitalized or "Not set"), amount paid, and a red "Balance due" warning when `amount_paid < order total`.
- **Edit mode**: Payment method dropdown and amount paid input, initialized from order data in `load()`. When switching from "0" to a payment method, auto-fills with total. Both included in `handleUpdate` payload.
---
### Feature: Reports Page
**Commit:** `1ed2642` — *Add restock history tracking, payment tracking, and reports page*
A new Reports page provides five aggregated views of sales, customer, and inventory data, with date range filtering.
**Server** (`server/routes/reports.js` — new file):
- `GET /api/reports` accepts optional `start` and `end` query params (YYYY-MM-DD format).
- Returns a single JSON object with five sections:
| Section | Description | Key columns |
|---------|-------------|-------------|
| `salesByProduct` | LEFT JOIN products → order_items → orders (via subquery for date filtering). All products shown, zero-filled. | `product_name`, `units_sold`, `revenue` |
| `topCustomers` | JOIN customers → orders → order_items. Sorted by total spent descending. | `customer_name`, `order_count`, `total_spent` |
| `revenueOverTime` | JOIN orders → order_items, grouped by `date(created_at)`. | `date`, `revenue`, `order_count` |
| `orderStatusBreakdown` | GROUP BY `orders.status`. | `status`, `count` |
| `inventorySummary` | Products with correlated subqueries on `stock_adjustments` for restock totals, sold totals, and restock count. | `product_name`, `current_stock`, `low_stock_threshold`, `total_restocked`, `total_sold`, `restock_count` |
- Date filtering uses positional `?` params (not named params, which caused errors with better-sqlite3 when not all params are used in every query). Order-based queries use `orderParams`, inventory summary uses `invParams` (SA params repeated 3x for 3 subqueries).
- `salesByProduct` uses a subquery in the LEFT JOIN to properly filter order_items by date range (putting date filter directly on the LEFT JOIN ON clause didn't filter correctly).
**Server** (`server/index.js`):
- Imported and mounted `reportsRouter` at `/api/reports` (after dashboard mount, behind auth middleware).
**Client** (`client/src/pages/Reports.jsx` — new file):
- Date range toolbar with preset dropdown (All time / This week / This month / Custom) and conditional date inputs for custom range.
- Fetches `/api/reports` with start/end params, re-fetches when dates change via `useEffect`.
- Five sections:
1. **Sales by Product** — table with totals row in `<tfoot>`.
2. **Top Customers** — table sorted by total spent.
3. **Revenue Over Time** — table with inline bar indicators (`div` with width proportional to max revenue).
4. **Order Status Breakdown** — horizontal colored bar segments (yellow=pending, green=paid, blue=delivered) plus a table.
5. **Inventory Summary** — table with low-stock products highlighted (red background, "(low)" label).
**Client** (`client/src/App.jsx`):
- Imported `Reports` component, added `<Route path="/reports" element={<Reports />} />`.
**Client** (`client/src/components/Layout.jsx`):
- Added `{ path: '/reports', label: 'Reports' }` to `navItems` array.
---
### Enhancement: Girl Scouts Trefoil Logo
**Commit:** `1ed2642` — *Add restock history tracking, payment tracking, and reports page*
- Extracted the trefoil icon paths from the full Girl Scouts wordmark SVG and created `client/src/assets/gs-trefoil.svg` with a `viewBox="0 0 13 11.7"` containing just the two trefoil `<path>` elements.
- `client/src/components/Layout.jsx`: Imported the SVG as `trefoilLogo` and added an `<img>` tag next to "Cookie Tracker" in the header link, styled at `height: 1.5em` with `verticalAlign: 'middle'`.
---
### Enhancement: Mobile Network Access
**Commit:** `1ed2642` — *Add restock history tracking, payment tracking, and reports page*
- `client/vite.config.js`: Added `host: true` to the Vite dev server config so it binds to all network interfaces, allowing access from phones on the same Wi-Fi network.
---
### Bug Fixes and Iteration During Development
These issues were discovered during testing and fixed before the feature commit:
1. **Reports named params error**: better-sqlite3 threw "Missing named parameter" when using `$start`/`$saStart` named params across queries that didn't all use every param. Fixed by switching to positional `?` params with separate `orderParams` and `saParams` arrays.
2. **salesByProduct date filtering**: The initial LEFT JOIN approach with date filter on the orders ON clause didn't properly filter order_items — products still showed all-time sales data. Fixed by restructuring to use a subquery: `LEFT JOIN (SELECT ... FROM order_items JOIN orders WHERE date_filter) oi ON oi.product_id = p.id`.
3. **OrderNew layout overflow**: Three `flex: 1` form groups (Status, Payment method, Amount paid) in a single `form-row` inside a `maxWidth: 640` card caused the amount paid input to overflow the card boundary, especially on mobile. Fixed by moving Amount paid to its own row below the Status/Payment method row, with `maxWidth: 200`.
---
## 2026-02-09 (Code Review Fixes)
### Bugfix: OrderDetail Product Change Overwrites product_id
**Commit:** `7068ea3` — *Fix bugs, harden validation, and improve robustness*
**Priority:** P0
**Problem**: In `client/src/pages/OrderDetail.jsx` (edit mode), changing the product in a line item called `updateLine` twice — once for `product_id`, once for `price_at_sale`. Under React's state batching, the second `setItems` call used stale state from before the first call, so it overwrote the `product_id` back to the old value.
**Fix**: Replaced the two `updateLine` calls with a single `setItems` call that updates both `product_id` and `price_at_sale` in one state transition:
```jsx
setItems((prev) => prev.map((ln, idx) =>
idx === i ? { ...ln, product_id: newId, price_at_sale: p?.price ?? ln.price_at_sale } : ln
));
```
---
### Bugfix: Validate All `:id` Route Parameters
**Commit:** `7068ea3` — *Fix bugs, harden validation, and improve robustness*
**Priority:** P0
**Problem**: `req.params.id` was passed directly to SQLite queries without validation. Values like `"abc"`, `"-1"`, or `"0"` could produce 500 errors or unexpected behavior.
**Fix**:
- Created `server/utils.js` with a `parseId(raw)` helper that returns a positive integer or `null`.
- Added `parseId` validation at the top of every `:id` route handler in `products.js`, `customers.js`, and `orders.js` (10 routes total). Invalid IDs return `400` with a clear error message.
- All subsequent DB calls in those routes use the parsed integer `id` instead of `req.params.id`.
**Files changed**: `server/utils.js` (new), `server/routes/products.js`, `server/routes/customers.js`, `server/routes/orders.js`.
---
### Bugfix: Product/Customer Delete with Existing References
**Commit:** `7068ea3` — *Fix bugs, harden validation, and improve robustness*
**Priority:** P0
**Problem**: Deleting a product used in `order_items` or a customer with existing `orders` hit the SQLite foreign key constraint, producing an unhandled 500 error with a raw database error message.
**Fix**:
- `server/routes/products.js` — DELETE `/:id` now checks `SELECT COUNT(*) FROM order_items WHERE product_id = ?` before deleting. If references exist, returns `409 Conflict` with message "Cannot delete product that has been used in orders".
- `server/routes/customers.js` — DELETE `/:id` now checks `SELECT COUNT(*) FROM orders WHERE customer_id = ?` before deleting. If references exist, returns `409 Conflict` with message "Cannot delete customer that has existing orders".
- The Inventory page's delete handler (`client/src/pages/Inventory.jsx`) already had `.catch((e) => setError(e.message))`, so the 409 error message is now displayed to the user automatically.
---
### Improvement: Disallow quantity_on_hand in Product PUT
**Commit:** `7068ea3` — *Fix bugs, harden validation, and improve robustness*
**Priority:** P1
**Problem**: Product `quantity_on_hand` could be changed via two paths: `PUT /products/:id` (not logged) and `PATCH /products/:id/stock` (logged to `stock_adjustments`). This created an unauditable backdoor for stock changes.
**Fix**:
- `server/routes/products.js` — PUT `/:id` no longer accepts or applies `quantity_on_hand`. Only `name`, `price`, and `low_stock_threshold` are updated. The UPDATE query was changed from 4 columns to 3.
- `client/src/pages/Inventory.jsx` — Removed the "Qty" input field from the inline edit form. Removed `quantity_on_hand` from the `handleUpdate` payload. Stock changes now exclusively go through the "Adjust stock" button (which uses PATCH).
---
### Improvement: Global Error Handler
**Commit:** `7068ea3` — *Fix bugs, harden validation, and improve robustness*
**Priority:** P1
**Fix** (`server/index.js`):
- Added Express error middleware (4-arg `(err, req, res, next)`) after all routes. Logs the error stack and returns `500` with either the error message (development) or a generic "Internal server error" (production).
- Checks `res.headersSent` to avoid double-sending responses.
- Added `process.on('unhandledRejection')` handler that logs the rejection reason.
---
### Improvement: Validate Report Dates and Stock-History Limit
**Commit:** `7068ea3` — *Fix bugs, harden validation, and improve robustness*
**Priority:** P2
**Fix**:
- `server/utils.js` — Added `isValidDate(str)` (validates YYYY-MM-DD format and that it parses to a real date) and `parseLimit(raw, max)` (returns a positive integer capped at `max`, default 1000, or `null`).
- `server/routes/reports.js` — Validates `start` and `end` query params with `isValidDate()` before using them in queries. Returns 400 with clear message on invalid format.
- `server/routes/products.js` — Stock-history endpoint validates `start`, `end` with `isValidDate()`, `product_id` with `parseId()`, and `limit` with `parseLimit()`.
---
### Improvement: Client-Side Robustness
**Commit:** `7068ea3` — *Fix bugs, harden validation, and improve robustness*
**Priority:** P2
1. **api.js 204 handling** (`client/src/api.js`): Added `res.jsonSafe()` method to the response object. Returns `null` for 204 responses (no body), otherwise calls `res.json()`. This prevents parse errors if a caller accidentally tries to parse a 204 response body.
2. **OrderNew setSubmitting** (`client/src/pages/OrderNew.jsx`): Changed error-only `setSubmitting(false)` in the `.catch()` to a `.finally(() => setSubmitting(false))` so the button always resets, even if navigation is slow or fails.
3. **Login error messages** (`client/src/pages/Login.jsx`): Now checks `res.status === 429` and shows "Too many login attempts. Try again later." instead of the generic message. Other auth failures show "Invalid credentials" (generic, doesn't leak information about whether a user exists).
---
### Improvement: Schema Migrations
**Commit:** `7068ea3` — *Fix bugs, harden validation, and improve robustness*
**Priority:** P3
**Problem**: New columns were added with `try { database.exec("ALTER TABLE ..."); } catch (e) {}`, which silently swallows all errors including unexpected ones.
**Fix** (`server/db.js`):
- Added `schema_version` table (single row with `version` integer) in the main `CREATE TABLE IF NOT EXISTS` block.
- Added `getSchemaVersion()`, `setSchemaVersion()`, and `runMigrations()` functions.
- Migrations are version-gated: `if (version < 1) { ... setSchemaVersion(database, 1); }`. Future migrations increment the version number.
- ALTER TABLE errors are now only caught if they contain "duplicate column" (expected on re-run); unexpected errors are re-thrown.
---
### Improvement: OrderDetail useEffect Dependency
**Commit:** `7068ea3` — *Fix bugs, harden validation, and improve robustness*
**Priority:** P3
**Fix** (`client/src/pages/OrderDetail.jsx`): Wrapped `load` function in `useCallback` with `[id]` dependency. Changed `useEffect` dependency from `[id]` to `[load]`. This satisfies React's exhaustive-deps lint rule.
---
### Documentation: README Deployment Instructions
**Commit:** `7068ea3` — *Fix bugs, harden validation, and improve robustness*
Expanded the "Deployment (self-hosted)" section of `README.md` from 4 bullet points to comprehensive instructions:
- Prerequisites (Node.js, PM2, reverse proxy)
- Initial setup steps (clone, install, configure .env, PM2 start/save/startup)
- Deploying updates (`git pull`, `npm run build`, `pm2 restart`)
- Nginx reverse proxy example config with HTTPS
- Backup cron job example
Also updated the Features section to include Reports, Stock Audit Trail, and payment/balance tracking.
---
### Documentation: CLAUDE.md Architecture Update
**Commit:** `c00bb90` — *Update CLAUDE.md with current architecture and documentation policy*
- Added change documentation requirement
- Added `utils.js` to server architecture docs
- Updated routes description to include reports endpoint
- Updated pages list to include Reports, Restock
- Updated key data flow to reference `atomicDeductStock()` (corrected from `deductStockForOrder()`), stock_adjustments logging, and PUT restriction on quantity_on_hand
- Added validation patterns documentation
- Added schema migration approach documentation
- Updated database schema section: 6 tables (was 4), payment fields, stock_adjustments