Contents
Stack Overview
| Layer | Technology |
|---|---|
| Frontend | React 18 + TypeScript, Vite 5, deployed to Cloudflare Pages |
| Backend API | Cloudflare Worker (TypeScript), deployed as aquarium-api |
| Database | Cloudflare D1 (SQLite) — aquarium_db |
| File Storage | Cloudflare D1 BLOB for logo (base64 stored in settings row) |
| PDF Generation | Browser window.print() with print-specific CSS; no server-side PDF |
| Auth | Admin: password hash stored in D1, JWT-like token in localStorage; Employee: phone + PIN stored in D1 |
File Structure
aquarium-portal/
โโโ src/
โ โโโ main.tsx # Vite entry
โ โโโ App.tsx # Router: public site vs admin vs team login
โ โโโ context/
โ โ โโโ AppContext.tsx # Global state, loadAll(), setView()
โ โโโ components/
โ โ โโโ PublicSite.tsx # Landing page (public)
โ โ โโโ AdminLogin.tsx # Password form
โ โ โโโ AdminLayout.tsx # Sidebar + outlet
โ โ โโโ TeamLogin.tsx # Employee phone+PIN login
โ โ โโโ admin/
โ โ โโโ DashboardView.tsx
โ โ โโโ JobsView.tsx
โ โ โโโ CalendarView.tsx
โ โ โโโ ClientsView.tsx
โ โ โโโ EmployeesView.tsx
โ โ โโโ EstimatesView.tsx
โ โ โโโ InvoicesView.tsx
โ โ โโโ ServiceRequestsView.tsx
โ โ โโโ ReportsView.tsx
โ โ โโโ SettingsView.tsx
โ โโโ types.ts # TypeScript interfaces
โโโ worker/
โ โโโ index.ts # Cloudflare Worker — all API routes
โโโ public/
โ โโโ user-guide.html # In-portal help guide (standalone HTML)
โโโ migrations/
โ โโโ 001_init.sql # D1 schema init migration
โโโ wrangler.toml
โโโ vite.config.ts
โโโ package.json
Auth Flow
Admin auth: The login form POSTs the password to /api/auth/login. The Worker hashes the input with SHA-256 and compares against the stored hash in the settings table. On match, it returns a signed token (a timestamped hash). The frontend stores this token in localStorage and sends it as Authorization: Bearer <token> on every subsequent request. The Worker validates the token on all /api/admin/* routes.
Employee auth: The Team Login form POSTs phone + PIN to /api/auth/employee-login. The Worker looks up the employee by phone, hashes the PIN, and compares. On match it returns an employee token scoped to that employee ID. The employee token only grants access to /api/employee/* routes.
localStorage — not httpOnly cookies. This is by design for a Cloudflare Pages + Worker setup where cookies require additional CORS coordination.AppContext & State
All global state lives in src/context/AppContext.tsx. The context exposes:
state— object containingjobs,clients,employees,estimates,invoices,requests,settings,adminView,selectedIdloadAll()— fetches all collections from the API in parallelsetView(view, id?)— dispatchesSET_VIEW, settingadminViewandselectedId
Pre-selection pattern (used in Jobs, Estimates, Invoices): when navigating from a client card, setView('jobs', jobId) sets selectedId. The target view uses a lazy initializer:
const preJob = state.selectedId
? (state.jobs.find(j => j.id === state.selectedId) ?? null)
: null;
const [selected, setSelected] = useState<Job | null>(preJob);
For JobsView specifically, selectedId can be either a job ID (navigate to job) or an employee ID (filter by employee). The view distinguishes these by checking if the ID matches a job first.
D1 Database Schema
-- Settings (single row, id=1)
CREATE TABLE settings (
id INTEGER PRIMARY KEY,
business_name TEXT,
business_phone TEXT,
business_email TEXT,
business_address TEXT,
logo_base64 TEXT,
service_types TEXT, -- JSON array
invoice_prefix TEXT,
invoice_next_number INTEGER,
blocked_days TEXT, -- JSON array of day numbers
admin_password_hash TEXT
);
-- Clients
CREATE TABLE clients (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
email TEXT,
phone TEXT,
address TEXT,
notes TEXT,
created_at TEXT DEFAULT (datetime('now'))
);
-- Employees
CREATE TABLE employees (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
phone TEXT NOT NULL UNIQUE,
pin_hash TEXT NOT NULL,
created_at TEXT DEFAULT (datetime('now'))
);
-- Jobs
CREATE TABLE jobs (
id TEXT PRIMARY KEY,
client_id TEXT REFERENCES clients(id),
employee_id TEXT REFERENCES employees(id),
service_type TEXT,
scheduled_date TEXT,
scheduled_time TEXT,
status TEXT DEFAULT 'scheduled', -- scheduled | in_progress | completed | cancelled
price REAL,
notes TEXT,
is_recurring INTEGER DEFAULT 0,
recurrence_type TEXT, -- weekly | monthly
created_at TEXT DEFAULT (datetime('now'))
);
-- Estimates
CREATE TABLE estimates (
id TEXT PRIMARY KEY,
client_id TEXT REFERENCES clients(id),
status TEXT DEFAULT 'draft', -- draft | sent | approved | declined
line_items TEXT, -- JSON array [{desc, amount}]
subtotal REAL,
notes TEXT,
expiry_date TEXT,
created_at TEXT DEFAULT (datetime('now'))
);
-- Invoices
CREATE TABLE invoices (
id TEXT PRIMARY KEY,
client_id TEXT REFERENCES clients(id),
invoice_number TEXT,
status TEXT DEFAULT 'unpaid', -- unpaid | paid
line_items TEXT, -- JSON array [{desc, amount}]
subtotal REAL,
paid_at TEXT,
notes TEXT,
created_at TEXT DEFAULT (datetime('now'))
);
-- Service Requests
CREATE TABLE service_requests (
id TEXT PRIMARY KEY,
name TEXT,
email TEXT,
phone TEXT,
service_type TEXT,
message TEXT,
status TEXT DEFAULT 'new', -- new | converted
created_at TEXT DEFAULT (datetime('now'))
);
API Routes
All routes are handled in worker/index.ts. The Worker is deployed at xyzaquarium-api.sonandigital.com.
| Method + Path | Purpose |
|---|---|
POST /api/auth/login | Admin login — returns token |
POST /api/auth/employee-login | Employee login — returns employee token |
GET /api/admin/settings | Load settings (admin) |
PUT /api/admin/settings | Update settings |
GET /api/admin/clients | List all clients |
POST /api/admin/clients | Create client |
PUT /api/admin/clients/:id | Update client |
DELETE /api/admin/clients/:id | Delete client |
GET /api/admin/jobs | List all jobs |
POST /api/admin/jobs | Create job |
PUT /api/admin/jobs/:id | Update job |
DELETE /api/admin/jobs/:id | Delete job |
GET /api/admin/employees | List employees |
POST /api/admin/employees | Create employee |
PUT /api/admin/employees/:id | Update employee (incl. PIN reset) |
DELETE /api/admin/employees/:id | Delete employee |
GET /api/admin/estimates | List estimates |
POST /api/admin/estimates | Create estimate |
PUT /api/admin/estimates/:id | Update estimate |
DELETE /api/admin/estimates/:id | Delete estimate |
GET /api/admin/invoices | List invoices |
POST /api/admin/invoices | Create invoice |
PUT /api/admin/invoices/:id | Update invoice (incl. mark paid) |
DELETE /api/admin/invoices/:id | Delete invoice |
GET /api/admin/requests | List service requests |
PUT /api/admin/requests/:id | Update request (mark converted) |
POST /api/public/contact | Submit contact/quote form (public, no auth) |
GET /api/public/settings | Business name, logo, services for public site (no auth) |
GET /api/employee/jobs | Employee's assigned jobs (employee token) |
PUT /api/employee/jobs/:id/status | Update job status (employee token) |
PUT /api/employee/pin | Change own PIN (employee token) |
Key Patterns
PDF generation: Estimates and invoices render into a hidden <div> with class print-only. The print CSS hides everything else. window.print() triggers the browser's PDF dialog. No server-side rendering required.
Logo storage: Logos are stored as base64 strings in the settings.logo_base64 column. On the frontend, they render as <img src={settings.logo_base64}> directly. Uploading fires a FileReader, converts to base64, and PUTs to /api/admin/settings.
Recurring jobs: When a job is created with is_recurring = 1, the Worker generates N future instances (weekly: 12 weeks, monthly: 6 months) as separate job rows at creation time. There is no background scheduler — all recurrences are materialized immediately.
CORS: The Worker sets Access-Control-Allow-Origin: https://xyzaquarium.sonandigital.com on all responses. In development, it allows http://localhost:5173. The ENVIRONMENT var controls which origin is allowed.
Theme & Styling
All colors are CSS custom properties defined in src/main.tsx (injected into :root). No hardcoded hex values in components.
| Variable | Value | Usage |
|---|---|---|
--primary | #0B7EA3 | Buttons, active states, links |
--sidebar-bg | #071F2D | Admin sidebar, public header |
--primary-light | #E0F2FE | Highlight backgrounds |
--surface | #FFFFFF | Card / panel backgrounds |
--background | #F3F9FB | Page background |
--text | #0F172A | Body text |
--muted | #64748B | Secondary text |
--border | #E2E8F0 | Card borders, dividers |
--success | #047857 | Paid status, completed |
--warning | #B45309 | Pending, in-progress |
--danger | #DC2626 | Errors, cancelled, declined |