From 0199a21a6692cfd62f480202b705b57b0e476aaf Mon Sep 17 00:00:00 2001 From: Erik Thiele Date: Tue, 19 May 2026 20:22:18 +0200 Subject: [PATCH] um Poolfahrzeuge erweitert --- AGENTS.md | 2 +- README.md | 18 +- app.py | 138 ++++++++++++--- docker-compose.yml | 4 +- inventory.db | Bin 24576 -> 24576 bytes inventory.log | 277 +++++++++++++++++++++++++++++++ templates/assign_asset.html | 36 +++- templates/base.html | 9 +- templates/index.html | 18 ++ templates/login.html | 2 +- templates/print_transaction.html | 2 +- templates/return_asset.html | 24 ++- 12 files changed, 487 insertions(+), 43 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 56e0eca..54418e5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -7,7 +7,7 @@ ## Run - Create the environment exactly as documented in `README.md`: `python3 -m venv .venv`, `source .venv/bin/activate`, `pip install -r requirements.txt`. - Start the app with `python3 app.py`. `app.py` calls `app.run(debug=True)` directly under `if __name__ == "__main__"`. -- Default local URL is `http://127.0.0.1:5000`. +- Default local URL is `http://127.0.0.1:5006`. ## Data And Side Effects - The app writes to repo-local files next to `app.py`: SQLite database `inventory.db` and log file `inventory.log`. diff --git a/README.md b/README.md index 0f8ad87..0f830bf 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Verwaltung fuer Tuerchips und Parkkarten +# Verwaltung fuer Tuerchips, Parkkarten und Poolfahrzeuge ## Voraussetzungen - Python 3.10 oder neuer @@ -15,7 +15,14 @@ pip install -r requirements.txt python3 app.py ``` -Die Anwendung ist danach unter `http://127.0.0.1:5000` erreichbar. +Die Anwendung ist danach unter `http://127.0.0.1:5006` erreichbar. + +## Start mit Docker +```bash +docker compose up --build +``` + +Die Anwendung ist danach ebenfalls unter `http://127.0.0.1:5006` erreichbar. ## Funktionen - Anmeldung fuer Bearbeiter und Admins @@ -24,8 +31,8 @@ Die Anwendung ist danach unter `http://127.0.0.1:5000` erreichbar. - Admin kann Passwort-Reset fuer Bearbeiter ausloesen - Admin kann vorhandene Bestandsdaten per CSV importieren - User anlegen -- Ausgabe von Tuerchips und Parkkarten -- Rueckgabe von Tuerchips und Parkkarten +- Ausgabe von Tuerchips, Parkkarten und Poolfahrzeugen +- Rueckgabe von Tuerchips, Parkkarten und Poolfahrzeugen - Uebersicht mit Suche und letzten Bewegungen - Einfache Logdatei mit Datum, Medium und bearbeitendem Mitarbeiter - Anzeige der letzten Logeintraege im Webinterface @@ -60,7 +67,8 @@ Erika Muster;Parkkarte;PARK-2001;Import - Eine optionale Kopfzeile `User;Typ;Kennung;Aktion` wird automatisch erkannt und uebersprungen. -- Unterstuetzte Typen sind `Tuerchip` und `Parkkarte`. +- Unterstuetzte Typen sind `Tuerchip`, `Parkkarte` und `Poolfahrzeug`. +- Bei der Ausgabe von `Poolfahrzeug` wird das Kennzeichen als Kennung erfasst. - Die Aktion `Import` uebernimmt vorhandene aktive Bestandsdaten in die Datenbank. - Falls ein User noch nicht existiert, wird er beim Import automatisch angelegt. - Bereits vergebene Kennungen oder widerspruechliche aktive Zuordnungen werden gesammelt als Fehler angezeigt; der Import wird erst ausgefuehrt, wenn keine Fehler mehr vorhanden sind. diff --git a/app.py b/app.py index b9addd7..f4fe023 100644 --- a/app.py +++ b/app.py @@ -26,6 +26,7 @@ logging.basicConfig( ASSET_LABELS = { "chip": "Tuerchip", "parking_card": "Parkkarte", + "pool_vehicle": "Poolfahrzeug", } ACTION_LABELS = { @@ -38,6 +39,12 @@ PRINT_DESCRIPTIONS = { "return": "Rueckgabebestaetigung fuer die Ruecknahme eines Mediums", } +INPUT_PLACEHOLDERS = { + "chip": "z. B. CHIP-1001", + "parking_card": "z. B. PARK-2001", + "pool_vehicle": "z. B. GZ-CC-123", +} + IMPORT_ACTIONS = {"import", "ausgabe", "assign"} IMPORT_HEADER = ["user", "typ", "kennung", "aktion"] @@ -79,13 +86,15 @@ def init_db() -> None: chip_assigned_at TEXT, parking_card_code TEXT, parking_card_assigned_at TEXT, + pool_vehicle_code TEXT, + pool_vehicle_assigned_at TEXT, created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE IF NOT EXISTS transactions ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, - asset_type TEXT NOT NULL CHECK (asset_type IN ('chip', 'parking_card')), + asset_type TEXT NOT NULL CHECK (asset_type IN ('chip', 'parking_card', 'pool_vehicle')), asset_code TEXT NOT NULL, handled_by TEXT, action TEXT NOT NULL CHECK (action IN ('assign', 'return')), @@ -112,6 +121,35 @@ def init_db() -> None: if "handled_by" not in columns: db.execute("ALTER TABLE transactions ADD COLUMN handled_by TEXT") + transactions_sql = db.execute( + "SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'transactions'" + ).fetchone() + if transactions_sql and "pool_vehicle" not in (transactions_sql["sql"] or ""): + db.execute("DROP TABLE transactions") + db.execute( + """ + CREATE TABLE transactions ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL, + asset_type TEXT NOT NULL CHECK (asset_type IN ('chip', 'parking_card', 'pool_vehicle')), + asset_code TEXT NOT NULL, + handled_by TEXT, + action TEXT NOT NULL CHECK (action IN ('assign', 'return')), + created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (user_id) REFERENCES users(id) + ) + """ + ) + + user_columns = { + row["name"] + for row in db.execute("PRAGMA table_info(users)").fetchall() + } + if "pool_vehicle_code" not in user_columns: + db.execute("ALTER TABLE users ADD COLUMN pool_vehicle_code TEXT") + if "pool_vehicle_assigned_at" not in user_columns: + db.execute("ALTER TABLE users ADD COLUMN pool_vehicle_assigned_at TEXT") + admin_exists = db.execute( "SELECT id FROM staff_users WHERE role = 'admin' LIMIT 1" ).fetchone() @@ -194,7 +232,11 @@ def read_recent_logs(limit: int = 20) -> list[str]: def asset_code_in_use(db: sqlite3.Connection, asset_type: str, asset_code: str) -> bool: - column_name = "chip_code" if asset_type == "chip" else "parking_card_code" + column_name = { + "chip": "chip_code", + "parking_card": "parking_card_code", + "pool_vehicle": "pool_vehicle_code", + }[asset_type] existing = db.execute( f"SELECT id FROM users WHERE {column_name} = ?", (asset_code,), @@ -210,6 +252,9 @@ def normalize_asset_type(value: str) -> str | None: "parkkarte": "parking_card", "parking_card": "parking_card", "parking card": "parking_card", + "poolfahrzeug": "pool_vehicle", + "pool vehicle": "pool_vehicle", + "pool_vehicle": "pool_vehicle", } return aliases.get(normalized) @@ -461,17 +506,20 @@ def import_data() -> str: current_chip_code = user["chip_code"] if user else None current_card_code = user["parking_card_code"] if user else None + current_vehicle_code = user["pool_vehicle_code"] if user else None user_id = user["id"] if user else None if pending_user is not None: current_chip_code = pending_user["chip_code"] current_card_code = pending_user["parking_card_code"] + current_vehicle_code = pending_user["pool_vehicle_code"] if user is None and pending_user is None: pending_user = { "full_name": full_name, "chip_code": None, "parking_card_code": None, + "pool_vehicle_code": None, } operations.append(("user", pending_user)) @@ -480,6 +528,8 @@ def import_data() -> str: asset_type == "chip" and current_chip_code == asset_code ) or ( asset_type == "parking_card" and current_card_code == asset_code + ) or ( + asset_type == "pool_vehicle" and current_vehicle_code == asset_code ) if not assigned_to_same_user: errors.append(f"Zeile {index}: Kennung '{asset_code}' ist bereits vergeben.") @@ -504,12 +554,18 @@ def import_data() -> str: continue if pending_user is not None: pending_user["chip_code"] = asset_code - else: + elif asset_type == "parking_card": if current_card_code and current_card_code != asset_code: errors.append(f"Zeile {index}: User '{full_name}' hat bereits eine andere Parkkarte.") continue if pending_user is not None: pending_user["parking_card_code"] = asset_code + else: + if current_vehicle_code and current_vehicle_code != asset_code: + errors.append(f"Zeile {index}: User '{full_name}' hat bereits ein anderes Poolfahrzeug.") + continue + if pending_user is not None: + pending_user["pool_vehicle_code"] = asset_code operations.append( ( @@ -549,11 +605,16 @@ def import_data() -> str: "UPDATE users SET chip_code = ?, chip_assigned_at = CURRENT_TIMESTAMP WHERE id = ?", (payload["asset_code"], resolved_user_id), ) - else: + elif payload["asset_type"] == "parking_card": db.execute( "UPDATE users SET parking_card_code = ?, parking_card_assigned_at = CURRENT_TIMESTAMP WHERE id = ?", (payload["asset_code"], resolved_user_id), ) + else: + db.execute( + "UPDATE users SET pool_vehicle_code = ?, pool_vehicle_assigned_at = CURRENT_TIMESTAMP WHERE id = ?", + (payload["asset_code"], resolved_user_id), + ) db.execute( "INSERT INTO transactions (user_id, asset_type, asset_code, handled_by, action) VALUES (?, ?, ?, ?, 'assign')", @@ -577,7 +638,8 @@ def index() -> str: params: tuple[str, ...] = () user_query = """ SELECT id, full_name, email, department, chip_code, chip_assigned_at, - parking_card_code, parking_card_assigned_at + parking_card_code, parking_card_assigned_at, + pool_vehicle_code, pool_vehicle_assigned_at FROM users """ @@ -586,10 +648,11 @@ def index() -> str: user_query += """ WHERE full_name LIKE ? OR email LIKE ? - OR chip_code LIKE ? - OR parking_card_code LIKE ? + OR chip_code LIKE ? + OR parking_card_code LIKE ? + OR pool_vehicle_code LIKE ? """ - params = (like_value, like_value, like_value, like_value) + params = (like_value, like_value, like_value, like_value, like_value) user_query += " ORDER BY full_name COLLATE NOCASE" users = db.execute(user_query, params).fetchall() @@ -599,7 +662,8 @@ def index() -> str: SELECT COUNT(*) AS users, SUM(CASE WHEN chip_code IS NOT NULL AND chip_code != '' THEN 1 ELSE 0 END) AS active_chips, - SUM(CASE WHEN parking_card_code IS NOT NULL AND parking_card_code != '' THEN 1 ELSE 0 END) AS active_cards + SUM(CASE WHEN parking_card_code IS NOT NULL AND parking_card_code != '' THEN 1 ELSE 0 END) AS active_cards, + SUM(CASE WHEN pool_vehicle_code IS NOT NULL AND pool_vehicle_code != '' THEN 1 ELSE 0 END) AS active_pool_vehicles FROM users """ ).fetchone() @@ -624,6 +688,7 @@ def index() -> str: "users": stats["users"], "active_chips": stats["active_chips"], "active_cards": stats["active_cards"], + "active_pool_vehicles": stats["active_pool_vehicles"], }, search_query=search_query, asset_labels=ASSET_LABELS, @@ -660,14 +725,19 @@ def create_user() -> str: @login_required def assign_asset() -> str: db = get_db() + asset_type = request.form.get("asset_type", "chip").strip() if request.method == "POST" else request.args.get("asset_type", "chip").strip() + if asset_type not in {"chip", "parking_card", "pool_vehicle"}: + asset_type = "chip" if request.method == "POST": user_id = request.form.get("user_id", "").strip() - asset_type = request.form.get("asset_type", "").strip() asset_code = request.form.get("asset_code", "").strip() handled_by = g.current_staff["full_name"] - if not user_id or asset_type not in {"chip", "parking_card"} or not asset_code: + if not user_id or asset_type not in {"chip", "parking_card", "pool_vehicle"}: + flash("Bitte alle Felder fuer die Ausgabe ausfuellen.") + return redirect(url_for("assign_asset")) + if not asset_code: flash("Bitte alle Felder fuer die Ausgabe ausfuellen.") return redirect(url_for("assign_asset")) @@ -677,7 +747,7 @@ def assign_asset() -> str: return redirect(url_for("assign_asset")) if asset_code_in_use(db, asset_type, asset_code): - flash("Diese Kennung ist bereits vergeben.") + flash("Dieses Kennzeichen ist bereits vergeben." if asset_type == "pool_vehicle" else "Diese Kennung ist bereits vergeben.") return redirect(url_for("assign_asset")) if asset_type == "chip": @@ -688,7 +758,7 @@ def assign_asset() -> str: "UPDATE users SET chip_code = ?, chip_assigned_at = CURRENT_TIMESTAMP WHERE id = ?", (asset_code, user_id), ) - else: + elif asset_type == "parking_card": if user["parking_card_code"]: flash("Dieser User hat bereits eine aktive Parkkarte.") return redirect(url_for("assign_asset")) @@ -696,6 +766,14 @@ def assign_asset() -> str: "UPDATE users SET parking_card_code = ?, parking_card_assigned_at = CURRENT_TIMESTAMP WHERE id = ?", (asset_code, user_id), ) + else: + if user["pool_vehicle_code"]: + flash("Dieser User hat bereits ein aktives Poolfahrzeug.") + return redirect(url_for("assign_asset")) + db.execute( + "UPDATE users SET pool_vehicle_code = ?, pool_vehicle_assigned_at = CURRENT_TIMESTAMP WHERE id = ?", + (asset_code, user_id), + ) db.execute( "INSERT INTO transactions (user_id, asset_type, asset_code, handled_by, action) VALUES (?, ?, ?, ?, 'assign')", @@ -708,20 +786,27 @@ def assign_asset() -> str: return redirect(url_for("print_transaction", transaction_id=transaction_id)) users = db.execute("SELECT id, full_name FROM users ORDER BY full_name COLLATE NOCASE").fetchall() - return render_template("assign_asset.html", users=users) + return render_template( + "assign_asset.html", + users=users, + selected_asset_type=asset_type, + input_placeholder=INPUT_PLACEHOLDERS[asset_type], + ) @app.route("/return", methods=["GET", "POST"]) @login_required def return_asset() -> str: db = get_db() + asset_type = request.form.get("asset_type", "chip").strip() if request.method == "POST" else request.args.get("asset_type", "chip").strip() + if asset_type not in {"chip", "parking_card", "pool_vehicle"}: + asset_type = "chip" if request.method == "POST": user_id = request.form.get("user_id", "").strip() - asset_type = request.form.get("asset_type", "").strip() handled_by = g.current_staff["full_name"] - if not user_id or asset_type not in {"chip", "parking_card"}: + if not user_id or asset_type not in {"chip", "parking_card", "pool_vehicle"}: flash("Bitte User und Typ fuer die Rueckgabe waehlen.") return redirect(url_for("return_asset")) @@ -730,7 +815,13 @@ def return_asset() -> str: flash("Ausgewaehlter User wurde nicht gefunden.") return redirect(url_for("return_asset")) - asset_code = user["chip_code"] if asset_type == "chip" else user["parking_card_code"] + asset_code = ( + user["chip_code"] + if asset_type == "chip" + else user["parking_card_code"] + if asset_type == "parking_card" + else user["pool_vehicle_code"] + ) if not asset_code: flash("Fuer diesen User ist kein entsprechendes Medium aktiv hinterlegt.") return redirect(url_for("return_asset")) @@ -740,11 +831,16 @@ def return_asset() -> str: "UPDATE users SET chip_code = NULL, chip_assigned_at = NULL WHERE id = ?", (user_id,), ) - else: + elif asset_type == "parking_card": db.execute( "UPDATE users SET parking_card_code = NULL, parking_card_assigned_at = NULL WHERE id = ?", (user_id,), ) + else: + db.execute( + "UPDATE users SET pool_vehicle_code = NULL, pool_vehicle_assigned_at = NULL WHERE id = ?", + (user_id,), + ) db.execute( "INSERT INTO transactions (user_id, asset_type, asset_code, handled_by, action) VALUES (?, ?, ?, ?, 'return')", @@ -757,7 +853,11 @@ def return_asset() -> str: return redirect(url_for("print_transaction", transaction_id=transaction_id)) users = db.execute("SELECT id, full_name FROM users ORDER BY full_name COLLATE NOCASE").fetchall() - return render_template("return_asset.html", users=users) + return render_template( + "return_asset.html", + users=users, + selected_asset_type=asset_type, + ) @app.route("/transactions//print") diff --git a/docker-compose.yml b/docker-compose.yml index 9570687..e1e7434 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,7 +7,7 @@ services: dockerfile: Dockerfile platforms: - linux/amd64 - image: gitea.teamthiele.de/ethiele/keyVerwaltung:latest + image: gitea.teamthiele.de/ethiele/keyverwaltung:latest ports: - "5006:5006" - restart: unless-stopped \ No newline at end of file + restart: unless-stopped diff --git a/inventory.db b/inventory.db index fbce2d70ce67dd05277e11069a9ad2fa4df5033c..a1d78a461c63d34768f761a8524eba860f0abc0b 100644 GIT binary patch delta 1040 zcma)4&rcIU6rSn!CvH-ufNjwRew6}@h-M7emUeCA(kHqFHvGb&oE4X5p5*Aeszb@PzN| zgvADHtu|_IVM?Cl@>nW1%m<0`lF61C@mrrJ2Bsa+4yHPy-I(t@glb-rT}75>0&f;L zr!qB^j1l*A{+Z>#IHGC zn|$B)Z7u}*Q_-h4gXDgz?d0@otEE|sbMejjYhqz}aYHND>uly~sW6PInoL<$QJ92v zrmDI|Ws%@2mL;NNLt<3Bz*M(6COkijl>7R;4n{QBkZ) z2GI>k$*DO>kr~CJ$gr+ZEXhVHtIDEcIAkJbYEH$5?6%k0Oqv%}yEOM6c5&zkpkL?* eIxZZPa3^op>CF>#7Em)DnzcWC$PzXH delta 480 zcmZvYJxjw-6o&7;iJE@hW2hopYAdyVfFEhn)*4U|EN)W7y#!4)sHxnW&Wdj0P|4=s zNPj>VCqV~)fQwt{;-IUETCCs;XFBIRyzhI_#usheOvo{W(4ap(iMI>dSPU&)kHn(W zXdqlhv`&-Jg>X62_2l8FkXH`BV9`I9yt!x>OX+XTyGgn$UQs;caq4362EmOdr!OJG za2RD);&Cmdxy{tfh@vWLaZxEO=1S?Ds;jGdq3E_!%QUbez%zn(c!g(AuH3^K2to#P z$Eek+4Q?_{hKZ2H4l`_S>^oN7=Awj!vC!8t0Ej>kJis+<1M&U7QI~9}=eknT^Xlfo zsbyQ-|90wZQ;<+7n;6$-&kS~K*@xAAgB|2lEng_EFw

Ausgabe

-

Tuerchips und Parkkarten an bestehende User ausgeben.

+

Tuerchips, Parkkarten und Poolfahrzeuge an bestehende User ausgeben.

-

Chip oder Parkkarte ausgeben

+

Medium ausgeben

@@ -21,17 +21,39 @@
- + + +
- - + +
+ {% endblock %} diff --git a/templates/base.html b/templates/base.html index a218acd..f9caece 100644 --- a/templates/base.html +++ b/templates/base.html @@ -118,7 +118,7 @@ .stats-grid { display: grid; - grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); + grid-template-columns: repeat(auto-fit, minmax(195px, 1fr)); gap: 1rem; } @@ -162,6 +162,11 @@ border-radius: 999px; background: rgba(255, 255, 255, 0.18); font-size: 1.5rem; + flex: 0 0 auto; + } + + .card.stat-card-pool .stat-icon { + margin-right: 0.15rem; } .card.stat-card.stat-card-users { @@ -489,7 +494,7 @@ CANCOM BU Sued/West - Tuerchip und Parkkartenverwaltung + Tuerchip-, Parkkarten- und Poolfahrzeugverwaltung +
+
+
+
Poolfahrzeuge
+
{{ stats.active_pool_vehicles }}
+
+
+
+
@@ -70,6 +79,7 @@ Abteilung Tuerchip Parkkarte + Poolfahrzeug @@ -94,6 +104,14 @@ - {% endif %} + + {% if user.pool_vehicle_code %} + {{ user.pool_vehicle_code }}
+ seit {{ user.pool_vehicle_assigned_at }} + {% else %} + - + {% endif %} + {% endfor %} diff --git a/templates/login.html b/templates/login.html index c20c0e1..cb6ac24 100644 --- a/templates/login.html +++ b/templates/login.html @@ -3,7 +3,7 @@ {% block content %}