Owner settings: add a real delete for Mitarbeiter (barbers)
Test backoffice (smb-crm) / test (push) Successful in 1m40s
Test backoffice (smb-crm) / test (push) Successful in 1m40s
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 <noreply@anthropic.com>
This commit is contained in:
@@ -183,6 +183,27 @@ def update_resource(client_id, resource_id, **fields):
|
|||||||
return row
|
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):
|
def get_resource_hours(client_id, resource_id):
|
||||||
"""Return {weekday: (opens_at, closes_at)} for client_id's own resource
|
"""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
|
(empty for a resource with no hours configured yet, or one that isn't
|
||||||
|
|||||||
@@ -168,6 +168,21 @@ def update_resource(resource_id):
|
|||||||
return _redirect()
|
return _redirect()
|
||||||
|
|
||||||
|
|
||||||
|
@bp.post("/resources/<resource_id>/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/<resource_id>/hours")
|
@bp.post("/resources/<resource_id>/hours")
|
||||||
@login_required
|
@login_required
|
||||||
def update_resource_hours(resource_id):
|
def update_resource_hours(resource_id):
|
||||||
|
|||||||
@@ -140,6 +140,12 @@
|
|||||||
<button type="submit" class="btn btn-small">Speichern</button>
|
<button type="submit" class="btn btn-small">Speichern</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
<form class="inline" method="post"
|
||||||
|
action="{{ url_for('owner_settings.delete_resource', resource_id=r.resource_id) }}"
|
||||||
|
onsubmit="return confirm('{{ r.name }} wirklich löschen? Das kann nicht rückgängig gemacht werden. Zum vorübergehenden Entfernen stattdessen oben „Aktiv" abwählen.');">
|
||||||
|
<button type="submit" class="btn btn-danger btn-small">Mitarbeiter löschen</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
<form method="post" action="{{ url_for('owner_settings.update_resource_hours', resource_id=r.resource_id) }}">
|
<form method="post" action="{{ url_for('owner_settings.update_resource_hours', resource_id=r.resource_id) }}">
|
||||||
{% set hours = hours_by_resource.get(r.resource_id, {}) %}
|
{% set hours = hours_by_resource.get(r.resource_id, {}) %}
|
||||||
{% set day_names = {0: 'Montag', 1: 'Dienstag', 2: 'Mittwoch', 3: 'Donnerstag',
|
{% set day_names = {0: 'Montag', 1: 'Dienstag', 2: 'Mittwoch', 3: 'Donnerstag',
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import threading
|
import threading
|
||||||
from datetime import datetime, timedelta, timezone
|
from datetime import datetime, time, timedelta, timezone
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
@@ -32,6 +32,30 @@ def test_get_resource_is_tenant_scoped():
|
|||||||
assert bdb.get_resource(CLIENT_B, r["resource_id"]) is None
|
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():
|
def test_create_and_get_service():
|
||||||
s = bdb.create_service(CLIENT_A, "Haircut", 30, price=25)
|
s = bdb.create_service(CLIENT_A, "Haircut", 30, price=25)
|
||||||
assert s["duration_minutes"] == 30
|
assert s["duration_minutes"] == 30
|
||||||
|
|||||||
@@ -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
|
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 ----
|
# ---- locations / resources self-service ----
|
||||||
|
|
||||||
def test_owner_can_create_location(client):
|
def test_owner_can_create_location(client):
|
||||||
|
|||||||
Reference in New Issue
Block a user