13 Commits

Author SHA1 Message Date
mivanchenko 456ca3872f Add locations (Filialen) as a grouping layer above resources
Test backoffice (smb-crm) / test (push) Successful in 1m46s
Enables multiple barbers/staff bookable at the same location and time
-- previously "resource" conflated "location" and "the thing that
can't double-book itself" into one row, so a Filiale could only ever
have exactly one bookable slot at once.

- New `locations` table; `resources.location_id` with a generic,
  idempotent backfill migration (any resource without a location gets
  one auto-created matching its name -- not a one-off for any single
  client, protects any future resource stuck in the old flat shape too)
- `resources`/`resource_hours`/services keep everything they already
  had (hours, min-notice, max-advance, buffer, the no-overlap
  constraint) scoped to resource_id, not location_id -- two barbers at
  one location must stay independently bookable at the same time
- booking_db.py: new locations CRUD mirroring the existing
  resources/services pattern; create_resource now requires a
  location_id, guarded the same way every other tenant check here is
  (get_location existence check, no real FK -- matches this schema's
  existing no-FK convention throughout)
- app.py: new POST /api/locations provisioning route; POST
  /api/resources now requires location_id
- owner_settings.py + settings.html: new self-service "add a Filiale"
  / "add a barber" UI -- there was previously no way to create a
  resource at all outside the CRM/n8n provisioning API
- public_booking.py + book.html: new Filiale picker (reuses the
  existing wireOptionGroup button-group pattern), filtering the
  Mitarbeiter picker to the selected location -- a single-location
  client sees no extra click, same as before Filialen existed
- owner_booking.py + agenda.html: the Filiale show/hide toggle and
  hide-cancelled toggle (shipped earlier this session) now key off
  location_id instead of resource_id, so hiding a Filiale hides every
  barber's bookings at it; manual-booking dropdown grouped by Filiale
- n8n/onboarding.json: default provisioning now creates a "Hauptfiliale"
  location before its resource (inert until re-imported into the live
  n8n instance)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-12 03:02:56 +02:00
mivanchenko aabd1d56c4 New-client onboarding: provision resources/services/owner login, drop EA (#24)
Test backoffice (smb-crm) / test (push) Has been cancelled
n8n/onboarding.json now provisions a default resource (with Mon-Sat 09:00-18:00
hours so the public booking page has slots immediately), a starter service, and
an owner-login user for every new client, recording the temp password via the
existing credentials CRM entity -- gated behind an If check so a failed user
creation can't leave a stale credentials row. The EA-provisioning chain
(service/provider creation against Easy!Appointments) is removed entirely.

Adds POST /api/resources, /api/services, /api/owner_users to the backoffice API
for n8n to call, backed by booking_db.py's existing tenancy-safe create_*
helpers. Also adds "slug" to db.py's clients column list -- it was already a DB
column (#17) but the generic /api/clients POST silently dropped it, so
onboarding could never actually set a client's public-facing slug.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-04 13:11:38 +02:00
mivanchenko a27ee59125 Owner notification webhook: Telegram re-point (#23)
Test backoffice (smb-crm) / test (push) Has been cancelled
Flask fires a fire-and-forget internal webhook (owner_notify.py, mirroring
mailer.py's background-thread queue) on booking create/cancel/reschedule,
carrying the same {booking, business_name, notify_channel} shape the old
EA-driven "Build booking row" node produced, plus an event field so the
Telegram message can say what actually happened. n8n/booking-sync.json gets
a new webhook + IF node feeding the existing Telegram node directly, so it
no longer needs EA's API to build the notification payload; channel routing
(only telegram sends for now) lives in that IF node.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-04 12:09:39 +02:00
mivanchenko 319218ce21 Remove Google Sheets mirror entirely (#13)
Test backoffice (smb-crm) / test (push) Has been cancelled
Postgres is now the sole source of truth: delete sheets.py and
import_from_sheets.py, strip mirror_entity/mirror_async/_mirror_worker and
POST /api/sync from app.py, drop the tab/mirror keys from db.py's TABLES.
Re-point n8n/renewal-reminder.json at the CRM's own HTTP API (GET
/api/clients, POST /api/activity_log) instead of the Sheets nodes, and drop
SHEET_ID/GOOGLE_SA_JSON from deploy env/compose and requests from
requirements.txt (PyJWT stays — still used by booking_api.py). Updates
docs/README/playbooks accordingly and closes the old #5 (atomic mirror) as
moot.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-04 11:29:57 +02:00
mivanchenko 94ae2d578d Add credentials store to the CRM, docs cleanup, deploy pipeline TODO
Adds a `credentials` entity to the back office (never mirrored to
Sheets, gated by the CRM token even to read) so client logins like
the auto-generated Easy!Appointments provider password can be viewed
and copied from the dashboard instead of getting lost — the actual
cause of the happynails password going missing. Onboarding now saves
that generated password instead of discarding it. Also adds
Documentation.md, brings README/TODO in line with the current
Postgres-first architecture, and tidies the backlog.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-15 14:13:34 +02:00
mivanchenko 156166b4e5 Add booking stack, client deploys, and back-office updates
- deploy/booking: shared Easy!Appointments stack with brand-matched
  wizard (flatpickr recolor, single-tenant provider hide, iframe
  auto-fit height reporter)
- deploy/clients: per-client isolated nginx compose stacks with
  _template scaffold, new-client.sh, and happynails live site
- deploy/backup: smb-db backup script
- n8n: booking-sync workflow; onboarding tweaks
- playbooks: lead-to-customer lifecycle + outreach
- templates: nail-studio landing previews
- backoffice: app/db/init/compose updates

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 16:10:15 +02:00
mivanchenko d5a304ebe9 n8n ingest swap: lead-intake & onboarding write to the DB
Both flows now POST into the smb-crm DB API instead of appending to Sheets, so
Postgres stays the source of truth and the DB->Sheets mirror keeps the Sheet
current. Telegram notifications unchanged.

- lead-intake: Webhook -> Build lead row -> POST /api/leads -> Telegram
  (dropped the direct Sheets append + the redundant activity-log nodes; the DB
  service logs + mirrors activity itself).
- onboarding: Webhook -> Compute -> POST /api/clients (DB assigns C-id) ->
  Build project w/ returned id -> POST /api/projects -> Telegram. DB computes
  renewal_date; go-live = start + 14d.
- Token injected at deploy time; repo keeps the __CRM_TOKEN__ placeholder.

Verified end-to-end on the live webhooks: lead and client+project land in the
DB and mirror to Sheets; test data cleaned; DB/Sheets consistent.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 09:33:53 +02:00
mivanchenko a59a22b38b Phase 3: add onboarding workflow
Webhook /webhook/onboard → read Clients → compute next C-#### →
append Clients row (status onboarding, renewal_date auto from billing
cycle) → seed Projects row (checklist from services, +14d go-live) →
log Activity → Telegram confirmation. Sheets writes retry on fail.
Verified end-to-end (C-0002 created across all three tabs).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 15:13:12 +02:00
mivanchenko 2bd6499ae5 Phase 4: harden Sheets writes with retry-on-fail
Add retryOnFail (4 tries, 3s apart) to all Google Sheets nodes in
lead-intake and renewal-reminder so a transient API throttle/race
re-tries instead of silently dropping a row.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 14:53:17 +02:00
mivanchenko 4b7e201b91 Phase 4: wire demo lead forms to live n8n webhook
- demo-tier-a / demo-tier-b: forms POST JSON to /webhook/lead-intake
  (hidden client_id + source, named fields), async submit with
  success/error toast.
- lead-intake webhook: enable CORS (allowedOrigins=*) so the static
  demos (different origin) can submit; re-export n8n/lead-intake.json.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 14:48:56 +02:00
mivanchenko 1e6c46e9f0 Phase 3: add renewal-reminder workflow
Daily 08:00 schedule → read Clients → flag active clients whose
renewal_date is due (14/7/3/1/0 days out or overdue) → Telegram
summary → log to Activity Log. Quiet on days with nothing due.
Verified end-to-end via CLI run against a seeded due client.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 14:42:29 +02:00
mivanchenko 339fb1a1a7 Phase 3: add Telegram lead notification to lead-intake
- Notify Telegram node (bot @smb_leads_bot) between Leads append and
  Activity Log; notified flag set TRUE. Verified end-to-end (lead →
  Telegram + Leads row + Activity Log).
- Re-export n8n/lead-intake.json with the notify node.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 14:36:46 +02:00
mivanchenko 0009a138a9 Phase 3: export working lead-intake workflow; gitignore .secrets
- n8n/lead-intake.json: Webhook → Build lead row (sanitized) →
  Leads append → Activity Log append. Verified end-to-end.
- .gitignore: ensure .secrets/ excluded.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 14:15:07 +02:00