Add locations (Filialen) as a grouping layer above resources
Test backoffice (smb-crm) / test (push) Successful in 1m46s
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>
This commit is contained in:
+41
-1
@@ -101,6 +101,25 @@ CREATE TABLE IF NOT EXISTS credentials (
|
||||
-- Booking module (#15): resources, services, and the users / password-reset
|
||||
-- tables the owner-login tickets build on. All access goes through
|
||||
-- app/booking_db.py — see that module for the tenancy-safe data-access layer.
|
||||
--
|
||||
-- Locations (Filialen) group resources for display/selection purposes only --
|
||||
-- the actual bookable/concurrency-safe unit stays `resources` (a client with
|
||||
-- several staff at one location needs each staff member independently
|
||||
-- bookable at the same time, so hours/notice/buffer/the no-overlap
|
||||
-- constraint all stay keyed on resource_id, not location_id). No FK to
|
||||
-- clients or from resources.location_id to here, matching this whole
|
||||
-- schema's convention: no real FKs anywhere, tenant/existence integrity is
|
||||
-- enforced in app/booking_db.py via get_location()/get_resource()-style
|
||||
-- checks before every write.
|
||||
CREATE TABLE IF NOT EXISTS locations (
|
||||
location_id text PRIMARY KEY,
|
||||
client_id text NOT NULL,
|
||||
name text NOT NULL,
|
||||
active boolean NOT NULL DEFAULT true,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS resources (
|
||||
resource_id text PRIMARY KEY,
|
||||
client_id text NOT NULL,
|
||||
@@ -190,6 +209,25 @@ ALTER TABLE clients ADD COLUMN IF NOT EXISTS timezone text NOT NULL DEFAULT
|
||||
ALTER TABLE clients ADD COLUMN IF NOT EXISTS auto_confirm boolean NOT NULL DEFAULT true;
|
||||
ALTER TABLE clients ADD COLUMN IF NOT EXISTS ics_token text;
|
||||
|
||||
ALTER TABLE resources ADD COLUMN IF NOT EXISTS location_id text;
|
||||
|
||||
-- Generic, permanent backfill (not a one-off for any single client): any
|
||||
-- resource stuck in the old flat shape (location_id IS NULL) gets a brand
|
||||
-- new location auto-created with its own name and is attached to it. Keeps
|
||||
-- protecting against a future resource ending up without a location -- a
|
||||
-- provisioning bug, a manual INSERT, a restored backup -- since a resource
|
||||
-- with location_id already set is never touched again by this block.
|
||||
DO $$
|
||||
DECLARE r RECORD; new_loc_id text;
|
||||
BEGIN
|
||||
FOR r IN SELECT resource_id, client_id, name FROM resources WHERE location_id IS NULL LOOP
|
||||
new_loc_id := 'LOC-' || floor(extract(epoch FROM clock_timestamp()) * 1000)::bigint
|
||||
|| '-' || substr(md5(random()::text), 1, 6);
|
||||
INSERT INTO locations (location_id, client_id, name) VALUES (new_loc_id, r.client_id, r.name);
|
||||
UPDATE resources SET location_id = new_loc_id WHERE resource_id = r.resource_id;
|
||||
END LOOP;
|
||||
END $$;
|
||||
|
||||
-- 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;
|
||||
@@ -204,7 +242,7 @@ DO $$
|
||||
DECLARE t text;
|
||||
BEGIN
|
||||
FOREACH t IN ARRAY ARRAY['clients','leads','projects','bookings','invoices',
|
||||
'credentials','resources','services','users'] LOOP
|
||||
'credentials','locations','resources','services','users'] LOOP
|
||||
EXECUTE format(
|
||||
'CREATE OR REPLACE TRIGGER %I_touch BEFORE UPDATE ON %I FOR EACH ROW EXECUTE FUNCTION touch_updated_at()',
|
||||
t, t);
|
||||
@@ -215,7 +253,9 @@ 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);
|
||||
CREATE INDEX IF NOT EXISTS credentials_client_idx ON credentials (client_id);
|
||||
CREATE INDEX IF NOT EXISTS locations_client_idx ON locations (client_id);
|
||||
CREATE INDEX IF NOT EXISTS resources_client_idx ON resources (client_id);
|
||||
CREATE INDEX IF NOT EXISTS resources_location_idx ON resources (location_id);
|
||||
CREATE INDEX IF NOT EXISTS services_client_idx ON services (client_id);
|
||||
CREATE INDEX IF NOT EXISTS bookings_client_idx ON bookings (client_id);
|
||||
CREATE INDEX IF NOT EXISTS users_client_idx ON users (client_id);
|
||||
|
||||
Reference in New Issue
Block a user