Design review
This commit is contained in:
@@ -41,6 +41,10 @@ and no API/JSON layer to maintain. TypeScript stays on the server where the logi
|
||||
Five tables plus one view. Money stored as **INTEGER cents** to avoid float rounding.
|
||||
Dates stored as ISO-8601 TEXT (`YYYY-MM-DD` or full datetime).
|
||||
|
||||
**Discounts:** entered as a line item with a negative `unit_price` (e.g. a `labor` line "10% discount" at `-$5.00`). No special column needed.
|
||||
|
||||
**Soft deletes:** orders are never hard-deleted. Setting `deleted_at` hides them from all normal queries; they can be recovered if needed.
|
||||
|
||||
```sql
|
||||
CREATE TABLE customers (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
@@ -79,7 +83,7 @@ CREATE TABLE service_orders (
|
||||
'WALK IN SERVICE')),
|
||||
service_rep TEXT,
|
||||
technician TEXT,
|
||||
status TEXT DEFAULT 'OPEN', -- OPEN / IN PROGRESS / DONE / PAID
|
||||
status TEXT DEFAULT 'OPEN' CHECK (status IN ('OPEN','IN PROGRESS','DONE','PAID')),
|
||||
date_in TEXT DEFAULT (date('now')),
|
||||
date_out TEXT,
|
||||
due_date TEXT,
|
||||
@@ -92,7 +96,8 @@ CREATE TABLE service_orders (
|
||||
service_call INTEGER DEFAULT 0,
|
||||
disposal INTEGER DEFAULT 0,
|
||||
amount_paid INTEGER DEFAULT 0,
|
||||
created_at TEXT DEFAULT (datetime('now'))
|
||||
created_at TEXT DEFAULT (datetime('now')),
|
||||
deleted_at TEXT -- soft delete; NULL = active
|
||||
);
|
||||
|
||||
CREATE TABLE line_items (
|
||||
@@ -113,22 +118,34 @@ CREATE TABLE work_log (
|
||||
logged_at TEXT DEFAULT (datetime('now'))
|
||||
);
|
||||
|
||||
-- ROUND() keeps cents integer despite REAL quantity (e.g. 1.5 hrs × 175_00 ¢).
|
||||
CREATE VIEW order_totals AS
|
||||
SELECT
|
||||
so.id AS service_order_id,
|
||||
COALESCE(SUM(CASE WHEN li.kind='part' THEN li.quantity*li.unit_price END),0) AS total_parts,
|
||||
COALESCE(SUM(CASE WHEN li.kind='labor' THEN li.quantity*li.unit_price END),0) AS total_labor,
|
||||
COALESCE(SUM(CASE WHEN li.kind='sublet' THEN li.quantity*li.unit_price END),0) AS total_sublet,
|
||||
COALESCE(SUM(li.quantity*li.unit_price),0)
|
||||
+ so.tax + so.shipping + so.pickup_delivery + so.service_call + so.disposal AS grand_total
|
||||
COALESCE(SUM(CASE WHEN li.kind='part' THEN ROUND(li.quantity*li.unit_price) END),0) AS total_parts,
|
||||
COALESCE(SUM(CASE WHEN li.kind='labor' THEN ROUND(li.quantity*li.unit_price) END),0) AS total_labor,
|
||||
COALESCE(SUM(CASE WHEN li.kind='sublet' THEN ROUND(li.quantity*li.unit_price) END),0) AS total_sublet,
|
||||
COALESCE(SUM(ROUND(li.quantity*li.unit_price)),0)
|
||||
+ so.tax + so.shipping + so.pickup_delivery + so.service_call + so.disposal AS grand_total,
|
||||
COALESCE(SUM(ROUND(li.quantity*li.unit_price)),0)
|
||||
+ so.tax + so.shipping + so.pickup_delivery + so.service_call + so.disposal
|
||||
- so.amount_paid AS balance_due
|
||||
FROM service_orders so
|
||||
LEFT JOIN line_items li ON li.service_order_id = so.id
|
||||
WHERE so.deleted_at IS NULL
|
||||
GROUP BY so.id;
|
||||
|
||||
CREATE INDEX idx_line_items_order ON line_items(service_order_id);
|
||||
CREATE INDEX idx_work_log_order ON work_log(service_order_id);
|
||||
CREATE INDEX idx_clocks_customer ON clocks(customer_id);
|
||||
CREATE INDEX idx_orders_customer ON service_orders(customer_id);
|
||||
```
|
||||
|
||||
**Order numbers:** `order_number` starts at 8247 (next after the sample) and
|
||||
increments. Simplest approach: `MAX(order_number)+1` at insert time inside a
|
||||
transaction. `better-sqlite3` is synchronous so there's no race on a single-user app.
|
||||
increments. Simplest approach: `COALESCE(MAX(order_number), 8246) + 1` at insert
|
||||
time inside a transaction — the `COALESCE` handles an empty table correctly.
|
||||
`better-sqlite3` is synchronous so there's no race on a single-user app. Gaps from
|
||||
soft-deleted orders are fine; numbers are never recycled.
|
||||
|
||||
## 5. Application Structure
|
||||
|
||||
@@ -136,7 +153,7 @@ transaction. `better-sqlite3` is synchronous so there's no race on a single-user
|
||||
clock-tracker/
|
||||
├─ src/
|
||||
│ ├─ server.ts # Express app, route wiring
|
||||
│ ├─ db.ts # better-sqlite3 connection + prepared statements
|
||||
│ ├─ db.ts # better-sqlite3 connection + prepared statements; runs PRAGMA foreign_keys=ON
|
||||
│ ├─ schema.sql # the DDL above (run on first boot if db missing)
|
||||
│ ├─ money.ts # cents <-> "$1.75" helpers
|
||||
│ ├─ routes/
|
||||
@@ -167,9 +184,10 @@ clock-tracker/
|
||||
| POST | `/orders` | redirect | Create order + customer/clock |
|
||||
| GET | `/orders/:id` | full page | Order detail (work + parts) |
|
||||
| POST | `/orders/:id/line-items` | HTML fragment | Add a labor/part line (HTMX) |
|
||||
| DELETE | `/orders/:id/line-items/:lineId` | empty/200 | Remove a line (HTMX) |
|
||||
| DELETE | `/orders/:id/line-items/:lineId` | HTML fragment | Remove a line + OOB totals update (HTMX) |
|
||||
| POST | `/orders/:id/work-log` | HTML fragment | Add a work-log note (HTMX) |
|
||||
| POST | `/orders/:id` | HTML fragment | Update status/dates/totals |
|
||||
| DELETE | `/orders/:id` | redirect | Soft-delete order (sets deleted_at) |
|
||||
| GET | `/orders/:id/invoice` | full page | Printable invoice |
|
||||
| GET | `/customers` | full page | Customer list/search |
|
||||
|
||||
@@ -213,13 +231,20 @@ The app itself never sees or stores credentials.
|
||||
## 10. Backups
|
||||
|
||||
The database is one file (`data/app.db`). A nightly `cron` job copies it somewhere
|
||||
safe:
|
||||
safe using `better-sqlite3`'s built-in backup API (avoids needing the `sqlite3` CLI):
|
||||
|
||||
```
|
||||
sqlite3 data/app.db ".backup '/backups/app-$(date +%F).db'"
|
||||
```ts
|
||||
// scripts/backup.ts
|
||||
import Database from 'better-sqlite3';
|
||||
import { execSync } from 'child_process';
|
||||
const date = execSync('date +%F').toString().trim();
|
||||
const db = new Database('data/app.db');
|
||||
await db.backup(`/backups/app-${date}.db`);
|
||||
db.close();
|
||||
```
|
||||
|
||||
Keep ~30 days. That's the entire disaster-recovery plan, and it's enough.
|
||||
Run with `tsx scripts/backup.ts` from cron. Keep ~30 days. That's the entire
|
||||
disaster-recovery plan, and it's enough.
|
||||
|
||||
## 11. Future (explicitly deferred)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user