Files
smb-online/backoffice/db/init.sql
T
mivanchenko 18e346d67b Back office: Postgres source-of-truth + read dashboard
Stand up the DB-first CRM backbone (architecture pivot: Postgres is the
source of truth, Google Sheets becomes a one-way downstream mirror).

- backoffice/ stack: smb-db (Postgres 16) + smb-crm (Flask/waitress service).
- Schema mirrors the six Sheet tabs (clients, leads, projects, activity_log,
  bookings, invoices) with typed columns + updated_at triggers.
- Service-account Sheets client (PyJWT) for the one-time import + future mirror.
- import_from_sheets.py: idempotent seed of Postgres from the live Sheets.
- Read dashboard (Leads & Clients tables) at onboard.mivanchenko.de/crm,
  behind the existing Caddy basic-auth; JSON API reads straight from Postgres.

Deployed + verified: import seeded DB, dashboard/API live, no-auth blocked,
onboarding form unaffected. Add/edit/delete + DB->Sheets sync + n8n ingest
swap are the next steps.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 09:12:39 +02:00

106 lines
3.0 KiB
PL/PgSQL

-- smb-crm — source-of-truth schema (Postgres).
-- Google Sheets is a one-way downstream mirror fed by the DB->Sheets sync.
-- Column sets mirror the Sheet tabs so the mirror stays 1:1.
CREATE TABLE IF NOT EXISTS clients (
client_id text PRIMARY KEY,
business_name text,
owner_name text,
email text,
phone text,
niche text,
tier text,
status text,
domain text,
stack_notes text,
vault_ref text,
services text,
billing_cycle text,
monthly_fee_eur numeric,
start_date date,
renewal_date date,
created_at timestamptz,
notes text,
updated_at timestamptz NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS leads (
lead_id text PRIMARY KEY,
received_at timestamptz,
client_id text,
source text,
name text,
contact text,
service_interest text,
message text,
status text,
notified boolean,
updated_at timestamptz NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS projects (
project_id text PRIMARY KEY,
client_id text,
deliverable text,
tier text,
checklist text,
go_live_date date,
status text,
updated_at timestamptz NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS activity_log (
id bigserial PRIMARY KEY,
ts timestamptz,
workflow text,
client_id text,
action text,
detail text,
result text
);
CREATE TABLE IF NOT EXISTS bookings (
booking_id text PRIMARY KEY,
created_at timestamptz,
client_id text,
customer_name text,
customer_contact text,
service text,
start_time timestamptz,
end_time timestamptz,
source text,
status text,
updated_at timestamptz NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS invoices (
invoice_id text PRIMARY KEY,
client_id text,
issued_date date,
due_date date,
amount_eur numeric,
period text,
status text,
paid_date date,
updated_at timestamptz NOT NULL DEFAULT now()
);
-- keep updated_at fresh on row changes
CREATE OR REPLACE FUNCTION touch_updated_at() RETURNS trigger AS $$
BEGIN NEW.updated_at = now(); RETURN NEW; END;
$$ LANGUAGE plpgsql;
DO $$
DECLARE t text;
BEGIN
FOREACH t IN ARRAY ARRAY['clients','leads','projects','bookings','invoices'] LOOP
EXECUTE format(
'CREATE TRIGGER %I_touch BEFORE UPDATE ON %I FOR EACH ROW EXECUTE FUNCTION touch_updated_at()',
t, t);
END LOOP;
END $$;
CREATE INDEX IF NOT EXISTS leads_received_idx ON leads (received_at DESC);
CREATE INDEX IF NOT EXISTS clients_status_idx ON clients (status);
CREATE INDEX IF NOT EXISTS activity_ts_idx ON activity_log (ts DESC);