From 68430a5c0c18f3b2ca2b55c5a01322a3558888da Mon Sep 17 00:00:00 2001 From: mivanchenko Date: Sat, 12 Sep 2026 03:13:14 +0200 Subject: [PATCH] Owner settings: add a real delete for Mitarbeiter (barbers) Rename + active-toggle already existed (this session, earlier commit) -- this adds a genuine, permanent delete alongside them, guarded by a confirm() prompt that points to the "Aktiv" checkbox as the reversible alternative for someone just temporarily off. Existing bookings against a deleted resource are left as-is (no FK in this schema, matching its existing convention) -- the owner agenda already falls back to the raw resource_id for a booking whose resource no longer resolves, same as it does today for a deactivated one. Co-Authored-By: Claude Sonnet 5 --- backoffice/app/booking_db.py | 21 ++++++++++++++++ backoffice/app/owner_settings.py | 15 +++++++++++ backoffice/app/templates/owner/settings.html | 6 +++++ backoffice/app/tests/test_booking_db.py | 26 +++++++++++++++++++- backoffice/app/tests/test_owner_settings.py | 17 +++++++++++++ 5 files changed, 84 insertions(+), 1 deletion(-) diff --git a/backoffice/app/booking_db.py b/backoffice/app/booking_db.py index a37fe37..226698a 100644 --- a/backoffice/app/booking_db.py +++ b/backoffice/app/booking_db.py @@ -183,6 +183,27 @@ def update_resource(client_id, resource_id, **fields): return row +def delete_resource(client_id, resource_id): + """Permanently remove a resource (and its resource_hours), scoped to + client_id. Returns resource_id on success, None if no such resource + exists for this client. Existing bookings against this resource_id are + left as-is (no FK, matching this schema's convention) -- the owner + agenda already falls back to showing the raw resource_id in place of a + name for a booking whose resource no longer resolves (see + owner_booking.py's _agenda_days), same as it does for a deactivated one.""" + with db.connect() as conn, conn.cursor() as cur: + cur.execute( + "DELETE FROM resources WHERE client_id = %s AND resource_id = %s RETURNING resource_id", + (client_id, resource_id)) + row = cur.fetchone() + if row is None: + conn.commit() + return None + cur.execute("DELETE FROM resource_hours WHERE resource_id = %s", (resource_id,)) + conn.commit() + return row["resource_id"] + + def get_resource_hours(client_id, resource_id): """Return {weekday: (opens_at, closes_at)} for client_id's own resource (empty for a resource with no hours configured yet, or one that isn't diff --git a/backoffice/app/owner_settings.py b/backoffice/app/owner_settings.py index 5f24832..8feca69 100644 --- a/backoffice/app/owner_settings.py +++ b/backoffice/app/owner_settings.py @@ -168,6 +168,21 @@ def update_resource(resource_id): return _redirect() +@bp.post("/resources//delete") +@login_required +def delete_resource(resource_id): + """Permanently removes a barber (#21 follow-up). Past bookings against + them are untouched -- see booking_db.delete_resource's docstring -- so + this is a real delete, not just deactivation; the "Aktiv" checkbox on the + resource's own edit form is the softer, reversible alternative for + someone temporarily off (on leave, etc.).""" + client_id = session["client_id"] + deleted = bdb.delete_resource(client_id, resource_id) + if deleted is None: + return _redirect(error="not_found") + return _redirect() + + @bp.post("/resources//hours") @login_required def update_resource_hours(resource_id): diff --git a/backoffice/app/templates/owner/settings.html b/backoffice/app/templates/owner/settings.html index 6804ed5..acdb00c 100644 --- a/backoffice/app/templates/owner/settings.html +++ b/backoffice/app/templates/owner/settings.html @@ -140,6 +140,12 @@ +
+ +
+
{% set hours = hours_by_resource.get(r.resource_id, {}) %} {% set day_names = {0: 'Montag', 1: 'Dienstag', 2: 'Mittwoch', 3: 'Donnerstag', diff --git a/backoffice/app/tests/test_booking_db.py b/backoffice/app/tests/test_booking_db.py index 3cc494a..6ad5f5b 100644 --- a/backoffice/app/tests/test_booking_db.py +++ b/backoffice/app/tests/test_booking_db.py @@ -1,5 +1,5 @@ import threading -from datetime import datetime, timedelta, timezone +from datetime import datetime, time, timedelta, timezone import pytest @@ -32,6 +32,30 @@ def test_get_resource_is_tenant_scoped(): assert bdb.get_resource(CLIENT_B, r["resource_id"]) is None +def test_delete_resource_removes_it_and_its_hours(): + r = bdb.create_resource(CLIENT_A, _loc(CLIENT_A), "Chair 1") + bdb.set_resource_hours(CLIENT_A, r["resource_id"], 0, time(9, 0), time(17, 0)) + assert bdb.delete_resource(CLIENT_A, r["resource_id"]) == r["resource_id"] + assert bdb.get_resource(CLIENT_A, r["resource_id"]) is None + assert bdb.get_resource_hours(CLIENT_A, r["resource_id"]) == {} + + +def test_delete_resource_is_tenant_scoped(): + r = bdb.create_resource(CLIENT_B, _loc(CLIENT_B), "Chair 1") + assert bdb.delete_resource(CLIENT_A, r["resource_id"]) is None + assert bdb.get_resource(CLIENT_B, r["resource_id"]) is not None + + +def test_delete_resource_leaves_existing_bookings_in_place(): + r = bdb.create_resource(CLIENT_A, _loc(CLIENT_A), "Chair 1") + b = bdb.create_booking(CLIENT_A, r["resource_id"], "Alice", "a@x.com", + "Haircut", _dt(10), _dt(11)) + bdb.delete_resource(CLIENT_A, r["resource_id"]) + still_there = bdb.get_booking(CLIENT_A, b["booking_id"]) + assert still_there is not None + assert still_there["resource_id"] == r["resource_id"] + + def test_create_and_get_service(): s = bdb.create_service(CLIENT_A, "Haircut", 30, price=25) assert s["duration_minutes"] == 30 diff --git a/backoffice/app/tests/test_owner_settings.py b/backoffice/app/tests/test_owner_settings.py index ee83d36..dd96737 100644 --- a/backoffice/app/tests/test_owner_settings.py +++ b/backoffice/app/tests/test_owner_settings.py @@ -166,6 +166,23 @@ def test_owner_cannot_update_another_tenants_resource(client): assert bdb.get_resource(CLIENT_B, resource_b["resource_id"])["max_advance_days"] != 1 +def test_owner_can_delete_a_resource(client): + resource, service = _setup(CLIENT_A) + _login(client, CLIENT_A) + resp = client.post(f"/owner/settings/resources/{resource['resource_id']}/delete") + assert "error" not in resp.headers["Location"] + assert bdb.get_resource(CLIENT_A, resource["resource_id"]) is None + + +def test_owner_cannot_delete_another_tenants_resource(client): + resource_b, service_b = _setup(CLIENT_B) + _setup(CLIENT_A) + _login(client, CLIENT_A) + resp = client.post(f"/owner/settings/resources/{resource_b['resource_id']}/delete") + assert "error=not_found" in resp.headers["Location"] + assert bdb.get_resource(CLIENT_B, resource_b["resource_id"]) is not None + + # ---- locations / resources self-service ---- def test_owner_can_create_location(client):