diff --git a/README.md b/README.md index 6929aa7..f520026 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,9 @@ A self-hosted web app for tracking cookie inventory and customers for a single t - **Inventory**: Add cookie products (name, price, quantity, low-stock threshold). Adjust stock (restock or deduct). Low-stock highlighting. - **Customers**: Store name, phone, email, address, notes. Search by name, email, or phone. -- **Orders**: Create orders with customer (or walk-in), line items (product + quantity), status (pending, paid, delivered). Inventory is deducted automatically. Edit or delete orders (stock is restored on delete). +- **Orders**: Create orders with customer (or walk-in), line items (product + quantity), status (pending, paid, delivered), payment method and amount paid. Inventory is deducted automatically. Edit or delete orders (stock is restored on delete). Balance due tracking. +- **Reports**: Sales by product, top customers, revenue over time, order status breakdown, and inventory summary. Filterable by date range (all time, this week, this month, custom). +- **Stock Audit Trail**: Every stock change (restock, order create/update/delete) is logged with reason and reference. - **Dashboard**: Summary counts, low-stock list, recent orders. ## Requirements @@ -63,10 +65,93 @@ Use the frontend URL in your browser. The database file is created automatically ## Deployment (self-hosted) -- Run the app behind a reverse proxy (e.g. nginx) if you want HTTPS or a different port. -- Set `DATABASE_PATH` to a persistent volume path so the SQLite file survives restarts. -- Set `APP_PASSWORD` and `APP_SECRET` in production so only people with the password can access the app. Session cookies are httpOnly and (in production) Secure when served over HTTPS. -- Back up the SQLite file regularly (e.g. cron job copying `data/cookies.db`). +### Prerequisites + +- A Linux server with Node.js 18+ and npm installed +- Git access to the repository +- (Recommended) PM2 for process management: `npm install -g pm2` +- (Recommended) A reverse proxy (e.g. nginx) for HTTPS + +### Initial setup + +1. Clone the repo to your server (e.g. `/opt/cookie-tracker`): + + ```bash + git clone https://your-git-host/your-repo/cookie-tracker.git /opt/cookie-tracker + cd /opt/cookie-tracker + ``` + +2. Install dependencies: + + ```bash + npm install + cd client && npm install && cd .. + ``` + +3. Create a `.env` file with production settings: + + ```bash + cp .env.example .env + ``` + + Edit `.env` and set: + - `APP_PASSWORD` — required in production so only authorized users can access the app + - `APP_SECRET` — a long random string for signing session cookies (generate with `openssl rand -hex 32`) + - `DATABASE_PATH` — path to a persistent location (e.g. `/var/data/cookies.db`) so the database survives restarts + - `PORT` — API port (default 3002) + +4. Build the client and start with PM2: + + ```bash + npm run build + pm2 start npm --name cookie-tracker -- start + pm2 save + pm2 startup # follow the instructions to enable auto-start on boot + ``` + +### Deploying updates + +After pushing changes to the repository: + +```bash +cd /opt/cookie-tracker +git pull origin master +npm run build +pm2 restart cookie-tracker +``` + +### Reverse proxy (nginx) + +If serving over HTTPS, add a site config like: + +```nginx +server { + listen 443 ssl; + server_name cookies.example.com; + + ssl_certificate /path/to/cert.pem; + ssl_certificate_key /path/to/key.pem; + + location / { + proxy_pass http://127.0.0.1:3002; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } +} +``` + +Session cookies are httpOnly and Secure when served over HTTPS in production. + +### Backups + +Back up the SQLite file regularly (e.g. via cron): + +```bash +# Add to crontab: daily backup at 2am +0 2 * * * cp /var/data/cookies.db /var/backups/cookies-$(date +\%Y\%m\%d).db +``` ## Project structure diff --git a/client/src/api.js b/client/src/api.js index fcc199c..dca37fc 100644 --- a/client/src/api.js +++ b/client/src/api.js @@ -18,5 +18,10 @@ export async function api(path, options = {}) { ? JSON.stringify(body) : body; } - return fetch(url, fetchOptions); + const res = await fetch(url, fetchOptions); + res.jsonSafe = async () => { + if (res.status === 204) return null; + return res.json(); + }; + return res; } diff --git a/client/src/pages/Inventory.jsx b/client/src/pages/Inventory.jsx index 3a1e9b7..791eec0 100644 --- a/client/src/pages/Inventory.jsx +++ b/client/src/pages/Inventory.jsx @@ -43,12 +43,11 @@ export default function Inventory() { function handleUpdate(id, form) { const name = form.name.value?.trim(); const price = Number(form.price.value) || 0; - const quantity_on_hand = Number(form.quantity_on_hand.value) || 0; const low_stock_threshold = Number(form.low_stock_threshold.value) || 0; if (!name) return; api(`/products/${id}`, { method: 'PUT', - body: { name, price, quantity_on_hand, low_stock_threshold }, + body: { name, price, low_stock_threshold }, }) .then((r) => (r.ok ? r.json() : r.json().then((e) => Promise.reject(new Error(e.error || 'Failed'))))) .then(() => { @@ -161,10 +160,6 @@ export default function Inventory() { -
- - -
diff --git a/client/src/pages/Login.jsx b/client/src/pages/Login.jsx index 6001256..af5caa8 100644 --- a/client/src/pages/Login.jsx +++ b/client/src/pages/Login.jsx @@ -17,7 +17,11 @@ export default function Login({ onLogin }) { try { const res = await api('/auth/login', { method: 'POST', body: { password } }); if (!res.ok) { - setError('Invalid password'); + if (res.status === 429) { + setError('Too many login attempts. Try again later.'); + } else { + setError('Invalid credentials'); + } setLoading(false); return; } diff --git a/client/src/pages/OrderDetail.jsx b/client/src/pages/OrderDetail.jsx index 83f4c8f..2520274 100644 --- a/client/src/pages/OrderDetail.jsx +++ b/client/src/pages/OrderDetail.jsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from 'react'; +import { useEffect, useState, useCallback } from 'react'; import { useParams, Link } from 'react-router-dom'; import { api } from '../api'; @@ -15,7 +15,7 @@ export default function OrderDetail() { const [amountPaid, setAmountPaid] = useState(''); const [items, setItems] = useState([]); - function load() { + const load = useCallback(() => { Promise.all([ api(`/orders/${id}`).then((r) => (r.ok ? r.json() : Promise.reject(new Error('Not found')))), api('/products').then((r) => r.json()), @@ -31,11 +31,11 @@ export default function OrderDetail() { }) .catch((e) => setError(e.message)) .finally(() => setLoading(false)); - } + }, [id]); useEffect(() => { load(); - }, [id]); + }, [load]); function handleUpdate(e) { e.preventDefault(); @@ -193,9 +193,9 @@ export default function OrderDetail() {