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>
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
-- 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);
|
||||
Reference in New Issue
Block a user