From 75caed2f296656be282697c512cd482ebe7d260d Mon Sep 17 00:00:00 2001 From: adamp Date: Mon, 9 Feb 2026 21:40:11 -0600 Subject: [PATCH] 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 --- CHANGELOG.md | 287 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 287 insertions(+) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..12a3d92 --- /dev/null +++ b/CHANGELOG.md @@ -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 ``. + 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 `} />`. + +**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 `` elements. +- `client/src/components/Layout.jsx`: Imported the SVG as `trefoilLogo` and added an `` 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