320 lines
18 KiB
Markdown
320 lines
18 KiB
Markdown
# Changelog
|
||
|
||
All notable changes to the Cookie Tracker are documented in this file.
|
||
|
||
---
|
||
|
||
## 2026-02-09 (README Fix)
|
||
|
||
### Fix: Clone URL in Deployment Instructions
|
||
|
||
**Commit:** `b1d3e69` — *Fix clone URL in README deployment instructions*
|
||
|
||
- `README.md` — Updated the placeholder `git clone https://your-git-host/your-repo/cookie-tracker.git` to the actual repository URL `https://git.raylos.net/adamp/cookie-tracker.git` in the deployment setup instructions.
|
||
|
||
---
|
||
|
||
## 2026-02-09 (Mobile Reports Fix)
|
||
|
||
### Bugfix: Reports Page Mobile Layout
|
||
|
||
**Commit:** `de71565` — *Fix Reports page mobile layout*
|
||
|
||
**Problem**: On mobile (iPhone 13 Pro Max portrait, and any screen < 640px), the Reports page tables were squished and unreadable. The existing CSS converts `.table-wrap` tables to a stacked card layout on small screens by hiding `<thead>` and showing `data-label` attributes on each `<td>`, but the Reports tables were missing those attributes entirely. This meant cells rendered without labels and the 4–5 column tables were crammed into the narrow viewport.
|
||
|
||
**Fix**:
|
||
|
||
- `client/src/pages/Reports.jsx` — Added `data-label` attributes to every `<td>` across all 5 report tables:
|
||
- **Sales by Product**: Product, Units sold, Revenue (including `<tfoot>` total row)
|
||
- **Top Customers**: Customer, Orders, Total spent
|
||
- **Revenue Over Time**: Date, Orders, Revenue
|
||
- **Order Status Breakdown**: Status, Count
|
||
- **Inventory Summary**: Product, Current stock, Total restocked, Total sold, Restock count
|
||
- `client/src/pages/Reports.jsx` — Added `className="bar-cell"` to the Revenue Over Time inline bar chart column.
|
||
- `client/src/index.css` — Added `display: block` for `tbody` and `tfoot` inside `.table-wrap` at the `max-width: 639px` breakpoint so `<tfoot>` rows also get the mobile card treatment.
|
||
- `client/src/index.css` — Added `.table-wrap td.bar-cell { display: none }` at the mobile breakpoint to hide the bar chart column (doesn't translate well to stacked layout).
|
||
|
||
---
|
||
|
||
## 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
|