18e346d67b
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>
106 lines
3.0 KiB
PL/PgSQL
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);
|