Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ebb266e099 | |||
| df677908d0 | |||
| bc61e87ab0 | |||
| 2d37ded5be | |||
| 31de22d460 | |||
| 96f918828c | |||
| eb912c3a73 | |||
| 3044b36e9f | |||
| e464628ab7 | |||
| a5a6f89f18 | |||
| 6835ee87a7 | |||
| 3a9d45327a | |||
| bfbc8b6796 | |||
| 2f6ab9c3fb | |||
| 7a4c3b82c0 |
@@ -229,8 +229,16 @@ jobs:
|
||||
cp config.json release-bundle/config.json
|
||||
cp update.sh release-bundle/update.sh
|
||||
cp init-admin.sh release-bundle/init-admin.sh
|
||||
|
||||
# Multitenant scripts & docs
|
||||
cp docker-compose-multitenant.yml release-bundle/docker-compose-multitenant.yml
|
||||
cp docker/nginx/multitenant.conf release-bundle/docker/nginx/multitenant.conf
|
||||
cp manage-tenant.sh release-bundle/manage-tenant.sh
|
||||
cp run-tenant-cmd.sh release-bundle/run-tenant-cmd.sh
|
||||
cp MULTITENANT_DEPLOYMENT.md release-bundle/MULTITENANT_DEPLOYMENT.md
|
||||
cp MULTITENANT_PYTHON_API.md release-bundle/MULTITENANT_PYTHON_API.md
|
||||
|
||||
chmod +x release-bundle/start.sh release-bundle/stop.sh release-bundle/restart.sh release-bundle/backup.sh release-bundle/update.sh release-bundle/init-admin.sh
|
||||
chmod +x release-bundle/start.sh release-bundle/stop.sh release-bundle/restart.sh release-bundle/backup.sh release-bundle/update.sh release-bundle/init-admin.sh release-bundle/manage-tenant.sh release-bundle/run-tenant-cmd.sh
|
||||
tar -czf inventarsystem-docker-bundle.tar.gz -C release-bundle .
|
||||
|
||||
- name: Create or update GitHub Release
|
||||
|
||||
@@ -1,177 +0,0 @@
|
||||
# Fehlersuche: Geplante Ausleihen werden nicht automatisch aktiviert
|
||||
|
||||
## 🔍 Mögliche Ursachen
|
||||
|
||||
### 1. **Termin wurde für falsches Datum erstellt**
|
||||
Die meisten Probleme entstehen, weil der Termin für ein **falsches Datum** (z.B. morgen statt heute) erstellt wurde.
|
||||
|
||||
### 2. **Scheduler läuft nicht**
|
||||
Der automatische Scheduler kann deaktiviert sein oder nicht starten.
|
||||
|
||||
### 3. **Zeitzonen-Problem**
|
||||
Die Startzeit kann in einer anderen Zeitzone gespeichert sein als erwartet.
|
||||
|
||||
---
|
||||
|
||||
## ✅ SOFORT-FIX: Manuell aktivieren
|
||||
|
||||
Falls du eine geplante Ausleihe sofort aktivieren möchtest, nutze diesen Befehl auf dem Server:
|
||||
|
||||
```bash
|
||||
docker exec inventarsystem-mongodb mongosh --eval "
|
||||
db.ausleihungen.updateOne(
|
||||
{_id: ObjectId('BOOKING_ID_HIER')},
|
||||
{\$set: {Status: 'active', LastUpdated: new Date()}}
|
||||
)" Inventarsystem
|
||||
```
|
||||
|
||||
Ersetze `BOOKING_ID_HIER` mit der ID deiner Ausleihe.
|
||||
|
||||
---
|
||||
|
||||
## 🔧 DEBUG: Alle geplanten Ausleihen anschauen
|
||||
|
||||
Führe auf deinem Server aus:
|
||||
|
||||
```bash
|
||||
docker exec inventarsystem-mongodb mongosh --eval "
|
||||
db.ausleihungen.find({Status: 'planned'}).pretty()
|
||||
" Inventarsystem
|
||||
```
|
||||
|
||||
Das zeigt dir:
|
||||
- **Start**: Das gespeicherte Startdatum/Uhrzeit
|
||||
- **Periode**: Die Schulstunde (falls verwendet)
|
||||
- **Erstellungs-Zeit vs. aktuelle Zeit**: Vergleich
|
||||
|
||||
---
|
||||
|
||||
## 🎯 PROBLEM 1: Falsches Datum beim Erstellen
|
||||
|
||||
**Symptom**: Start-Zeit ist in der Zukunft (z.B. morgen statt heute)
|
||||
|
||||
**Lösung**:
|
||||
1. In der Kalender-Ansicht das **richtige Datum** auswählen
|
||||
2. Die **aktuelle Uhrzeit** für den Beginn nutzen (nicht eine zukünftige Zeit)
|
||||
3. Für Schulstunden: Stelle sicher die **heutige Schulstunde** zu wählen
|
||||
|
||||
---
|
||||
|
||||
## 🎯 PROBLEM 2: Scheduler läuft nicht
|
||||
|
||||
**Symptom**: Keine Logs über Termin-Aktivierungen
|
||||
|
||||
**Überprüfung**:
|
||||
|
||||
```bash
|
||||
# 1. Container-Logs anschauen
|
||||
docker logs inventarsystem-app | grep -i "scheduler\|appointment\|status update"
|
||||
|
||||
# 2. Scheduler ist aktiviert?
|
||||
docker exec inventarsystem-app python3 -c "
|
||||
import sys
|
||||
sys.path.insert(0, '/app/Web')
|
||||
import settings as cfg
|
||||
print(f'Scheduler aktiviert: {cfg.SCHEDULER_ENABLED}')
|
||||
print(f'Scheduler Intervall: {cfg.SCHEDULER_INTERVAL_MIN} Minuten')
|
||||
"
|
||||
```
|
||||
|
||||
**Lösung wenn deaktiviert**:
|
||||
- In `config.json` ändern:
|
||||
```json
|
||||
"scheduler": {
|
||||
"enabled": true,
|
||||
"interval_minutes": 1
|
||||
}
|
||||
```
|
||||
- Container neu starten: `docker-compose restart app`
|
||||
|
||||
---
|
||||
|
||||
## 🎯 PROBLEM 3: Startzeit als nur-Zeit (kein Datum)
|
||||
|
||||
**Symptom**: Start-Zeit zeigt nur "13:30" ohne Datum
|
||||
|
||||
**Überprüfung**: In der Datenbank nach `{Start: {$type: "string"}}` suchen statt datetime
|
||||
|
||||
**Lösung**: Die Ausleihe muss mit einem vollständigen Datum+Uhrzeit erstellt werden.
|
||||
|
||||
---
|
||||
|
||||
## 🚀 AUTOMATISCHE AKTIVIERUNG TESTEN
|
||||
|
||||
Nach dem Fix: Erstelle eine Ausleihe für:
|
||||
- **Heute** (das aktuelle Datum)
|
||||
- **Schulstunde 7** (13:30-14:15)
|
||||
- Startzeitpunkt sollte **jetzt oder in 1 Minute** sein
|
||||
|
||||
Warte 2 Minuten. Der Status sollte von `planned` zu `active` wechseln.
|
||||
|
||||
---
|
||||
|
||||
## 📊 SCHEDULER STATUS ÜBERPRÜFEN
|
||||
|
||||
```bash
|
||||
# Zeige die letzten 50 Log-Zeilen
|
||||
docker logs --tail 50 inventarsystem-app | tail -20
|
||||
|
||||
# Suche nach "Appointment status update"
|
||||
docker logs inventarsystem-app 2>/dev/null | grep -i "Appointment status" | tail -5
|
||||
```
|
||||
|
||||
Erwartete Log-Ausgabe:
|
||||
```
|
||||
Appointment status update finished: X changed (Y active, Z completed)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔍 DETAILLIERTE DEBUG-INFOS
|
||||
|
||||
Wenn obiges nicht funktioniert, führe dies aus:
|
||||
|
||||
```bash
|
||||
docker exec inventarsystem-app python3 -c "
|
||||
from pymongo import MongoClient
|
||||
import datetime
|
||||
|
||||
client = MongoClient('mongodb', 27017)
|
||||
db = client['Inventarsystem']
|
||||
aus = db['ausleihungen']
|
||||
|
||||
print('=== GEPLANTE AUSLEIHEN ===')
|
||||
for b in aus.find({'Status': 'planned'}).limit(3):
|
||||
print(f\"Start: {b.get('Start')} | Typ: {type(b.get('Start')).__name__}\")
|
||||
print(f\"Periode: {b.get('Period')} | User: {b.get('User')}\")
|
||||
print(f\"Jetzt > Start? {datetime.datetime.now() > b.get('Start', datetime.datetime.max)}\")
|
||||
print()
|
||||
"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 💡 EMPFOHLENE EINSTELLUNG FÜR SCHULEN
|
||||
|
||||
In `config.json` verwende diese Werte:
|
||||
|
||||
```json
|
||||
{
|
||||
"scheduler": {
|
||||
"enabled": true,
|
||||
"interval_minutes": 1,
|
||||
"backup_interval_hours": 24
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Dies checkt **jede Minute** ob Termine aktiviert werden sollen - perfekt für Schulstunden.
|
||||
|
||||
---
|
||||
|
||||
## Weitere Hilfe?
|
||||
|
||||
Falls das nicht funktioniert:
|
||||
1. Schreib mir die **exakte Uhrzeit** wann du die Ausleihe erstellt hast
|
||||
2. Und wann sie hätte aktiviert werden sollen
|
||||
3. Zeige mir ein Beispiel aus der DB mit `docker exec inventarsystem-mongodb mongosh...`
|
||||
@@ -452,3 +452,44 @@ Für Support: Siehe Legal/LICENSE
|
||||
---
|
||||
|
||||
**Version**: 1.0 | **Letzte Aktualisierung**: 2026-04-17 | **Kompatibilität**: Python 3.11+, MongoDB 7.0+, Redis 7+
|
||||
|
||||
## Tenant Management Operationen (manage-tenant.sh)
|
||||
|
||||
Um einzelne Tenants im Multi-Tenant-Umfeld im laufenden Betrieb und ohne globale Downtime zu verwalten, kann das neue CLI-Skript `manage-tenant.sh` verwendet werden.
|
||||
|
||||
### 1. Neuen Tenant hinzufügen
|
||||
Initialisiert die MongoDB-Datenbankstruktur isoliert für einen neuen Tenant und legt initiale Admin-Zugangsdaten an.
|
||||
```bash
|
||||
./manage-tenant.sh add <tenant_id>
|
||||
```
|
||||
|
||||
### 2. Bestimmten Tenant neu starten (Soft-Restart)
|
||||
Erzwingt sofortigen Logout und einen Cache/Session-Reset für die Nutzer *eines spezifischen* Tenants, ohne andere laufende Instanzen zu beeinträchtigen. Ideal bei Konfigurationsänderungen oder feststeckenden Sessions.
|
||||
```bash
|
||||
./manage-tenant.sh restart-tenant <tenant_id>
|
||||
```
|
||||
|
||||
### 3. Tenant sicher entfernen
|
||||
Löscht die dedizierte MongoDB-Datenbank des gewählten Tenants vollständig (erfordert Bestätigung).
|
||||
```bash
|
||||
./manage-tenant.sh remove <tenant_id>
|
||||
```
|
||||
|
||||
### 4. Globale Operationen
|
||||
```bash
|
||||
# Zeigt alle aktiven isolierten Tenant-Datenbanken an
|
||||
./manage-tenant.sh list
|
||||
|
||||
# Führt einen Zero-Downtime Rolling-Restart aller Application-Container durch
|
||||
./manage-tenant.sh restart-all
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Aktuelle UI- & Funktionsoptimierungen (Release April 2026)
|
||||
|
||||
Neben der Docker-Auslagerung wurden spezifische Caching-, Parsing-, und DOM-Tricks integriert, die das Setup weiter entschlacken:
|
||||
|
||||
* **DOM Array Slicing für Bilder:** Bei großen Beständen (hunderte Artikel) rendert der Client im Listen/Kachel-Modus künftig nur noch das primäre Bild (`slice(0, 1)`), was den DOM-Memory-Footprint drastisch reduziert und das Einfrieren von Browsern verhindert.
|
||||
* **Auto-Ingestion von Excel-Filtern:** Der Excel-Importer prüft nun dynamisch neue `categories/filter`, die noch nicht in der Datenbank existieren, und speichert sie direkt in die MongoDB `filter_presets`-Kollektion (Zero-Config für Administratoren).
|
||||
* **Responsive UI Synchronisierung:** Die Standardansicht (`main.html`) der Smartphones wurde CSS-technisch exakt an das skalierungsfähigere Profil der Admin-Mobile-Ansicht (`main_admin.html`) angeglichen.
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
# Multi-Tenant Python Management API
|
||||
|
||||
This document explains how the multi-tenant architecture isolates data within Python, what the return values are, and how developers can build internal administrative scripts using native Python instead of the Docker CLI.
|
||||
|
||||
## 1. Architectural Concept
|
||||
|
||||
In the system, each "tenant" is essentially a dedicated MongoDB database identified by a dynamically generated string based on a subdomain or header (`inventar_<tenant_id>`).
|
||||
App containers share a connection pool using `pymongo.MongoClient`, and requests are routed to specific databases dynamically based on the current Flask `g.tenant_context`.
|
||||
|
||||
All MongoDB administrative tasks (creating tenants, restarting apps, fetching lists) are done via standard MongoDB Python drivers because the core multi-tenancy happens at the **database level**.
|
||||
|
||||
## 2. Managing Tenants via Python
|
||||
|
||||
If you want to perform multi-tenant administrative operations without traversing through `manage-tenant.sh`, you can execute native Python scripts connecting to the system's `MongoClient`.
|
||||
|
||||
### Basic Connection Boilerplate
|
||||
Whenever automating an administrative task in Python, you simply need to connect to MongoDB using the properties defined in `settings.py`.
|
||||
|
||||
```python
|
||||
import sys
|
||||
import os
|
||||
|
||||
# Append Web folder so we can access configuration
|
||||
sys.path.insert(0, '/app/Web')
|
||||
import settings
|
||||
from pymongo import MongoClient
|
||||
|
||||
# Establish connection pooling
|
||||
client = MongoClient(settings.MONGODB_HOST, int(settings.MONGODB_PORT))
|
||||
```
|
||||
|
||||
### A. Adding a New Tenant (Database Initialization)
|
||||
A new tenant database isn’t provisioned until the first actual data insert happens. We trigger this manually by creating an `admin` user for them.
|
||||
|
||||
**Operation:**
|
||||
```python
|
||||
def create_tenant(tenant_id, admin_password="hashed_password_here"):
|
||||
db_name = f"{settings.MONGODB_DB}_{tenant_id}"
|
||||
db = client[db_name]
|
||||
|
||||
# MongoDB creates the DB automatically on first insert
|
||||
result = db.users.insert_one({
|
||||
'username': 'admin',
|
||||
'password': admin_password,
|
||||
'role': 'admin'
|
||||
})
|
||||
return result.inserted_id # Returns the BSON ObjectId of the new user
|
||||
```
|
||||
|
||||
### B. List Active Tenants
|
||||
To find out how many isolated tenants have active databases, you query the raw `MongoClient` for all databases and search for your configured MongoDB prefix (default: `inventar_`).
|
||||
|
||||
**Operation:**
|
||||
```python
|
||||
def list_tenants():
|
||||
prefix = f"{settings.MONGODB_DB}_"
|
||||
|
||||
# Returns a Python list of string database names
|
||||
all_dbs = client.list_database_names()
|
||||
|
||||
# Filter and strip the prefix to return just the tenant_ids
|
||||
active_tenants = [d.replace(prefix, "") for d in all_dbs if d.startswith(prefix)]
|
||||
|
||||
return active_tenants # e.g., ['schule1', 'schule2', 'test']
|
||||
```
|
||||
|
||||
### C. Soft-Restarting a Tenant (Invalidating Sessions)
|
||||
"Restarting" a single tenant means signing out all of their users and forcing an application refresh. Because Session data is coupled to the tenant database, dropping their `sessions` collection achieves an instant sign-out.
|
||||
|
||||
**Operation:**
|
||||
```python
|
||||
def restart_tenant(tenant_id):
|
||||
db_name = f"{settings.MONGODB_DB}_{tenant_id}"
|
||||
db = client[db_name]
|
||||
|
||||
# Drops the collection. All active user cookies immediately become invalid.
|
||||
result = db.sessions.drop()
|
||||
|
||||
return result # Returns None. Raises PyMongoError if connection fails.
|
||||
```
|
||||
|
||||
### D. Removing a Tenant Completely (Wipe Data)
|
||||
If a tenant is removed from the service or their lease expires, you can permanently obliterate their data container footprint.
|
||||
|
||||
**Operation:**
|
||||
```python
|
||||
def remove_tenant(tenant_id):
|
||||
db_name = f"{settings.MONGODB_DB}_{tenant_id}"
|
||||
|
||||
# Erases the isolated database. Can't be undone.
|
||||
client.drop_database(db_name)
|
||||
|
||||
return True # Returns True. Raises PyMongoError if connection fails.
|
||||
```
|
||||
|
||||
## 3. Resolving Context Inside Flask (app.py)
|
||||
|
||||
If you are building custom application endpoints inside `Web/app.py`, you shouldn't use the direct MongoDB `client` manually. Instead, you rely on the built-in Flask context manager (`Web/tenant.py`) to give you the correct isolated scope.
|
||||
|
||||
### The `get_tenant_db()` function
|
||||
Every route must use `get_tenant_db(client)` to ensure users can only ever access their own school/domain's database.
|
||||
|
||||
```python
|
||||
from pymongo import MongoClient
|
||||
import settings
|
||||
from tenant import get_tenant_db
|
||||
|
||||
# Example Route
|
||||
@app.route('/api/items')
|
||||
def get_items():
|
||||
# 1. Establish/reuse pooling connection
|
||||
client = MongoClient(settings.MONGODB_HOST, settings.MONGODB_PORT)
|
||||
|
||||
# 2. Get the dynamically routed DB for THIS user
|
||||
# (Based on Nginx Subdomain or X-Tenant-Id header)
|
||||
db = get_tenant_db(client)
|
||||
|
||||
# 3. Runs query solely on `inventar_schule1.items`
|
||||
items = list(db.items.find())
|
||||
|
||||
return items # List of BSON Dictionaries
|
||||
```
|
||||
|
||||
**What it returns internally:**
|
||||
The `get_tenant_db` function queries `g.tenant_context` inside Flask, calculates the database name from the subdomain, and returns a live `pymongo.database.Database` object.
|
||||
|
||||
This ensures that scaling is extremely cheap on resources because 1 Application Container connects to 100 separate Tenant Databases using just 1 shared `MongoClient` pool.
|
||||
|
||||
+31
@@ -2063,6 +2063,11 @@ def _upload_excel_items(scope='inventory'):
|
||||
validation_warnings = []
|
||||
reserved_codes = set()
|
||||
known_locations = set(it.get_predefined_locations())
|
||||
known_filters = {
|
||||
1: set(it.get_predefined_filter_values(1)),
|
||||
2: set(it.get_predefined_filter_values(2)),
|
||||
3: set(it.get_predefined_filter_values(3))
|
||||
}
|
||||
|
||||
def is_code_available(candidate_code):
|
||||
return candidate_code not in reserved_codes and it.is_code_unique(candidate_code)
|
||||
@@ -2114,8 +2119,19 @@ def _upload_excel_items(scope='inventory'):
|
||||
validation_warnings.append((row_number, f'Ort "{ort}" wird neu angelegt'))
|
||||
|
||||
filter1 = expand_filter_selection(_excel_list(val('filter1')), 1)
|
||||
for f in filter1:
|
||||
if f and f not in known_filters[1]:
|
||||
validation_warnings.append((row_number, f'Kategorie 1 "{f}" wird neu angelegt'))
|
||||
|
||||
filter2 = expand_filter_selection(_excel_list(val('filter2')), 2)
|
||||
for f in filter2:
|
||||
if f and f not in known_filters[2]:
|
||||
validation_warnings.append((row_number, f'Kategorie 2 "{f}" wird neu angelegt'))
|
||||
|
||||
filter3 = _excel_list(val('filter3'))
|
||||
for f in filter3:
|
||||
if f and f not in known_filters[3]:
|
||||
validation_warnings.append((row_number, f'Kategorie 3 "{f}" wird neu angelegt'))
|
||||
|
||||
anschaffungsjahr = _excel_int(val('anschaffungsjahr'))
|
||||
raw_year = val('anschaffungsjahr')
|
||||
@@ -2217,6 +2233,21 @@ def _upload_excel_items(scope='inventory'):
|
||||
it.add_predefined_location(row['ort'])
|
||||
known_locations.add(row['ort'])
|
||||
|
||||
for f in row.get('filter1', []):
|
||||
if f and f not in known_filters[1]:
|
||||
it.add_predefined_filter_value(1, f)
|
||||
known_filters[1].add(f)
|
||||
|
||||
for f in row.get('filter2', []):
|
||||
if f and f not in known_filters[2]:
|
||||
it.add_predefined_filter_value(2, f)
|
||||
known_filters[2].add(f)
|
||||
|
||||
for f in row.get('filter3', []):
|
||||
if f and f not in known_filters[3]:
|
||||
it.add_predefined_filter_value(3, f)
|
||||
known_filters[3].add(f)
|
||||
|
||||
series_group_id = str(uuid.uuid4()) if row['count'] > 1 else None
|
||||
row_created_ids = []
|
||||
|
||||
|
||||
+64
-33
@@ -21,6 +21,28 @@
|
||||
--ui-radius: 12px;
|
||||
}
|
||||
|
||||
:root[data-theme="dark"] {
|
||||
--ui-bg: #0f172a;
|
||||
--ui-bg-accent: #1e293b;
|
||||
--ui-surface: #1e293b;
|
||||
--ui-surface-soft: #25314a;
|
||||
--ui-border: #334155;
|
||||
--ui-text: #f1f5f9;
|
||||
--ui-text-muted: #94a3b8;
|
||||
--ui-title: #ffffff;
|
||||
--ui-shadow-sm: 0 2px 8px rgba(0, 0, 0, 0.4);
|
||||
--ui-shadow-md: 0 10px 24px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
/* Make inputs visible in dark mode */
|
||||
:root[data-theme="dark"] input,
|
||||
:root[data-theme="dark"] select,
|
||||
:root[data-theme="dark"] textarea {
|
||||
background-color: var(--ui-surface-soft) !important;
|
||||
color: var(--ui-text) !important;
|
||||
border-color: var(--ui-border) !important;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: "Manrope", "Segoe UI", "Noto Sans", sans-serif;
|
||||
color: var(--ui-text);
|
||||
@@ -92,7 +114,7 @@ span {
|
||||
.card-header,
|
||||
.modal-header,
|
||||
thead th {
|
||||
background: linear-gradient(180deg, #f7fafd, #edf3f8) !important;
|
||||
background: var(--ui-surface-soft) !important;
|
||||
border-bottom: 1px solid var(--ui-border) !important;
|
||||
color: var(--ui-title) !important;
|
||||
}
|
||||
@@ -107,10 +129,10 @@ input[type="email"],
|
||||
input[type="date"],
|
||||
select {
|
||||
border-radius: 10px !important;
|
||||
border: 1px solid #cad5e0 !important;
|
||||
border: 1px solid var(--ui-border) !important;
|
||||
min-height: 40px;
|
||||
color: var(--ui-text) !important;
|
||||
background-color: #fff !important;
|
||||
background-color: var(--ui-surface) !important;
|
||||
transition: border-color 0.15s ease, box-shadow 0.15s ease;
|
||||
}
|
||||
|
||||
@@ -131,11 +153,13 @@ select:focus {
|
||||
.table th,
|
||||
.table td {
|
||||
vertical-align: middle;
|
||||
border-color: #dee6ee !important;
|
||||
border-color: var(--ui-border) !important;
|
||||
background-color: var(--ui-surface) !important;
|
||||
color: var(--ui-text) !important;
|
||||
}
|
||||
|
||||
.table-striped > tbody > tr:nth-of-type(odd) {
|
||||
background-color: #f8fbff;
|
||||
background-color: var(--ui-surface-soft) !important;
|
||||
}
|
||||
|
||||
.alert {
|
||||
@@ -144,8 +168,10 @@ select:focus {
|
||||
}
|
||||
|
||||
.navbar {
|
||||
border-bottom: 1px solid #273341;
|
||||
border-bottom: 1px solid var(--ui-border);
|
||||
box-shadow: none;
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.navbar-brand {
|
||||
@@ -164,21 +190,13 @@ select:focus {
|
||||
}
|
||||
}
|
||||
|
||||
/* Edit and generate buttons with black text for better visibility */
|
||||
.edit-button {
|
||||
color: black !important; /* Override any existing color */
|
||||
}
|
||||
|
||||
.duplicate-button {
|
||||
color: black !important; /* Override any existing color */
|
||||
}
|
||||
|
||||
.generate-qr-button {
|
||||
color: black !important; /* Override any existing color */
|
||||
/* Edit and generate buttons with theme-aware text for better visibility */
|
||||
.edit-button, .duplicate-button, .generate-qr-button {
|
||||
color: var(--ui-text) !important; /* Override any existing color */
|
||||
}
|
||||
|
||||
.ausleihen {
|
||||
color: black !important; /* Override any existing color */
|
||||
/* Color handled securely below */
|
||||
}
|
||||
|
||||
/* Modal image display fix */
|
||||
@@ -245,12 +263,12 @@ select:focus {
|
||||
.view-toggle-btn {
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
border: 1px solid #c7ced6;
|
||||
border: 1px solid var(--ui-border);
|
||||
border-radius: 50%;
|
||||
background: #ffffff;
|
||||
background: var(--ui-surface);
|
||||
font-size: 1.15rem;
|
||||
cursor: pointer;
|
||||
color: #2f3e4e;
|
||||
color: var(--ui-text);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -259,8 +277,8 @@ select:focus {
|
||||
|
||||
.view-toggle-btn:hover {
|
||||
transform: translateY(-1px);
|
||||
border-color: #8fa2b7;
|
||||
background: #f8fbff;
|
||||
border-color: var(--ui-border);
|
||||
background: var(--ui-surface-soft);
|
||||
box-shadow: 0 3px 8px rgba(33, 37, 41, 0.12);
|
||||
}
|
||||
|
||||
@@ -269,9 +287,9 @@ select:focus {
|
||||
}
|
||||
|
||||
.view-toggle-btn[aria-pressed='true'] {
|
||||
background: #e9f2ff;
|
||||
border-color: #7ea1c8;
|
||||
color: #1f4f7a;
|
||||
background: var(--module-primary-color);
|
||||
border-color: var(--module-primary-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.table-view-header {
|
||||
@@ -288,7 +306,7 @@ body.table-view .table-view-header {
|
||||
border: 0;
|
||||
border-radius: 10px;
|
||||
background: transparent;
|
||||
color: #3f4e5d;
|
||||
color: var(--ui-text-muted);
|
||||
font-size: 0.82rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
@@ -307,9 +325,9 @@ body.table-view .item-card {
|
||||
max-width: none;
|
||||
margin: 0 0 10px 0;
|
||||
padding: 12px 14px;
|
||||
border: 1px solid #e1e6ed;
|
||||
border: 1px solid var(--ui-border);
|
||||
border-radius: 10px;
|
||||
background: #ffffff;
|
||||
background: var(--ui-surface);
|
||||
box-shadow: none;
|
||||
scroll-snap-align: none;
|
||||
}
|
||||
@@ -335,7 +353,7 @@ body.table-view .item-card .card-content .item-name-cell,
|
||||
body.table-view .item-card .card-content .item-col-name {
|
||||
font-size: 0.98rem;
|
||||
font-weight: 700;
|
||||
color: #1f2d3d;
|
||||
color: var(--ui-title);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
@@ -352,7 +370,7 @@ body.table-view .item-card .card-content .item-col-filter3,
|
||||
body.table-view .item-card .card-content .item-code-cell,
|
||||
body.table-view .item-card .card-content .item-col-code,
|
||||
body.table-view .item-card .card-content .item-col-count {
|
||||
color: #445465;
|
||||
color: var(--ui-text-muted);
|
||||
font-size: 0.9rem;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
@@ -369,7 +387,7 @@ body.table-view .item-card .damage-badge {
|
||||
body.table-view .item-card .actions {
|
||||
margin-top: 10px;
|
||||
padding-top: 10px;
|
||||
border-top: 1px dashed #d9e0e8;
|
||||
border-top: 1px dashed var(--ui-border);
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
@@ -805,4 +823,17 @@ body.table-view .item-card .actions {
|
||||
padding: 7px 12px !important;
|
||||
font-size: 0.84rem !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
/* iOS and Mobile optimization fixes */
|
||||
html, body {
|
||||
-webkit-text-size-adjust: 100%;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
.scrollable-container, .items-container {
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
@supports (padding-bottom: env(safe-area-inset-bottom)) {
|
||||
.fixed-bottom, .modal-footer {
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
</div>
|
||||
|
||||
<div style="display:grid; grid-template-columns:repeat(auto-fit,minmax(220px,1fr)); gap:12px; margin:16px 0 20px;">
|
||||
<div style="padding:14px; border:1px solid #e2e8f0; border-radius:10px; background:#fff;">
|
||||
<div style="padding:14px; border:1px solid #e2e8f0; border-radius:10px; background: var(--ui-surface);">
|
||||
<div style="font-size:0.85rem; color:#64748b;">Chain Status</div>
|
||||
{% if verify_result.ok %}
|
||||
<div style="font-size:1.35rem; font-weight:700; color:#166534;">OK</div>
|
||||
@@ -19,15 +19,15 @@
|
||||
<div style="font-size:1.35rem; font-weight:700; color:#991b1b;">FEHLER</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div style="padding:14px; border:1px solid #e2e8f0; border-radius:10px; background:#fff;">
|
||||
<div style="padding:14px; border:1px solid #e2e8f0; border-radius:10px; background: var(--ui-surface);">
|
||||
<div style="font-size:0.85rem; color:#64748b;">Einträge</div>
|
||||
<div style="font-size:1.35rem; font-weight:700;">{{ verify_result.count }}</div>
|
||||
</div>
|
||||
<div style="padding:14px; border:1px solid #e2e8f0; border-radius:10px; background:#fff;">
|
||||
<div style="padding:14px; border:1px solid #e2e8f0; border-radius:10px; background: var(--ui-surface);">
|
||||
<div style="font-size:0.85rem; color:#64748b;">Letzter Index</div>
|
||||
<div style="font-size:1.35rem; font-weight:700;">{{ verify_result.last_chain_index }}</div>
|
||||
</div>
|
||||
<div style="padding:14px; border:1px solid #e2e8f0; border-radius:10px; background:#fff;">
|
||||
<div style="padding:14px; border:1px solid #e2e8f0; border-radius:10px; background: var(--ui-surface);">
|
||||
<div style="font-size:0.85rem; color:#64748b;">Mismatch Count</div>
|
||||
<div style="font-size:1.35rem; font-weight:700;">{{ verify_result.mismatches|length }}</div>
|
||||
</div>
|
||||
@@ -61,7 +61,7 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div style="border:1px solid #e2e8f0; border-radius:10px; background:#fff; padding:12px;">
|
||||
<div style="border:1px solid #e2e8f0; border-radius:10px; background: var(--ui-surface); padding:12px;">
|
||||
<h3 style="margin-top:0;">Letzte Audit-Ereignisse</h3>
|
||||
<div style="max-height:560px; overflow:auto;">
|
||||
<table class="table" style="width:100%; border-collapse:collapse;">
|
||||
|
||||
@@ -161,7 +161,7 @@
|
||||
</div>
|
||||
|
||||
<div id="invoice-modal" style="display:none; position:fixed; inset:0; background:rgba(15,23,42,0.72); z-index:9999; padding:20px; overflow:auto;">
|
||||
<div style="max-width:760px; margin:40px auto; background:#fff; border-radius:12px; padding:24px; box-shadow:0 20px 60px rgba(0,0,0,0.3);">
|
||||
<div style="max-width:760px; margin:40px auto; background: var(--ui-surface); border-radius:12px; padding:24px; box-shadow:0 20px 60px rgba(0,0,0,0.3);">
|
||||
<div style="display:flex; justify-content:space-between; align-items:center; gap:12px; margin-bottom:18px;">
|
||||
<div>
|
||||
<h2 style="margin:0;">Rechnung erstellen</h2>
|
||||
@@ -174,15 +174,15 @@
|
||||
<div style="display:grid; grid-template-columns:repeat(auto-fit, minmax(240px, 1fr)); gap:16px; margin-bottom:16px;">
|
||||
<div>
|
||||
<label for="invoice-item" style="display:block; font-weight:700; margin-bottom:6px;">Element</label>
|
||||
<input id="invoice-item" type="text" readonly style="width:100%; padding:10px; border:1px solid #ddd; border-radius:6px; background:#f8fafc;">
|
||||
<input id="invoice-item" type="text" readonly style="width:100%; padding:10px; border:1px solid #ddd; border-radius:6px; background: var(--ui-surface-soft);">
|
||||
</div>
|
||||
<div>
|
||||
<label for="invoice-borrower" style="display:block; font-weight:700; margin-bottom:6px;">Schüler</label>
|
||||
<input id="invoice-borrower" type="text" readonly style="width:100%; padding:10px; border:1px solid #ddd; border-radius:6px; background:#f8fafc;">
|
||||
<input id="invoice-borrower" type="text" readonly style="width:100%; padding:10px; border:1px solid #ddd; border-radius:6px; background: var(--ui-surface-soft);">
|
||||
</div>
|
||||
<div>
|
||||
<label for="invoice-code" style="display:block; font-weight:700; margin-bottom:6px;">Element-ID / Code</label>
|
||||
<input id="invoice-code" type="text" readonly style="width:100%; padding:10px; border:1px solid #ddd; border-radius:6px; background:#f8fafc;">
|
||||
<input id="invoice-code" type="text" readonly style="width:100%; padding:10px; border:1px solid #ddd; border-radius:6px; background: var(--ui-surface-soft);">
|
||||
</div>
|
||||
<div>
|
||||
<label for="invoice-amount" style="display:block; font-weight:700; margin-bottom:6px;">Preis</label>
|
||||
|
||||
@@ -17,14 +17,14 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="background:#fff; border:1px solid #e2e8f0; border-radius:14px; padding:14px; margin-bottom:12px;">
|
||||
<div style="background: var(--ui-surface); border:1px solid #e2e8f0; border-radius:14px; padding:14px; margin-bottom:12px;">
|
||||
<input id="damage-search" type="text" placeholder="Suche nach Name, Code, Typ, Benutzer oder Schaden..." style="width:100%; padding:10px 12px; border:1px solid #d1d5db; border-radius:10px;">
|
||||
</div>
|
||||
|
||||
<div style="background:#fff; border:1px solid #e2e8f0; border-radius:14px; padding:0; overflow:hidden;">
|
||||
<div style="background: var(--ui-surface); border:1px solid #e2e8f0; border-radius:14px; padding:0; overflow:hidden;">
|
||||
<table id="damage-table" style="width:100%; border-collapse:collapse;">
|
||||
<thead>
|
||||
<tr style="background:#f8fafc;">
|
||||
<tr style="background: var(--ui-surface-soft);">
|
||||
<th style="text-align:left; padding:11px; border-bottom:1px solid #e5e7eb;">Item</th>
|
||||
<th style="text-align:left; padding:11px; border-bottom:1px solid #e5e7eb;">Status</th>
|
||||
<th style="text-align:left; padding:11px; border-bottom:1px solid #e5e7eb;">Letzte Meldung</th>
|
||||
|
||||
+157
-27
@@ -10,10 +10,24 @@
|
||||
<html lang="de" data-module="{{ CURRENT_MODULE }}">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=5.0, user-scalable=yes">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=5.0, user-scalable=yes, viewport-fit=cover">
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||||
<meta name="theme-color" content="#2c3e50" id="meta-theme-color">
|
||||
<script>
|
||||
// Init theme early to prevent FOUC (Flash of Unstyled Content)
|
||||
(function() {
|
||||
var theme = localStorage.getItem('inventarsystem-theme');
|
||||
if (theme === 'dark' || (!theme && window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
|
||||
document.documentElement.setAttribute('data-theme', 'dark');
|
||||
document.getElementById('meta-theme-color').setAttribute('content', '#1a252f');
|
||||
} else {
|
||||
document.documentElement.setAttribute('data-theme', 'light');
|
||||
document.getElementById('meta-theme-color').setAttribute('content', '#2c3e50');
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
<meta name="csrf-token" content="{{ csrf_token }}">
|
||||
<title>{% block title %}Inventarsystem{% endblock %}</title>
|
||||
{% block head %}
|
||||
@@ -102,6 +116,9 @@
|
||||
--module-accent-color: #3498db;
|
||||
--module-accent-light: rgba(52, 152, 219, 0.1);
|
||||
--module-brand-icon: "📦";
|
||||
--page-bg-gradient: linear-gradient(180deg, #f2f5f8, #e7edf4);
|
||||
--page-bg-overlay1: rgba(86, 145, 200, 0.08);
|
||||
--page-bg-overlay2: rgba(52, 98, 150, 0.09);
|
||||
}
|
||||
|
||||
html[data-module="library"] {
|
||||
@@ -112,17 +129,34 @@
|
||||
--module-brand-icon: "📚";
|
||||
}
|
||||
|
||||
:root[data-theme="dark"] {
|
||||
--module-primary-color: #1a252f;
|
||||
--module-primary-light: #2c3e50;
|
||||
--module-accent-color: #2980b9;
|
||||
--module-brand-icon: "📦";
|
||||
--page-bg-gradient: linear-gradient(180deg, #0f172a, #1e293b);
|
||||
--page-bg-overlay1: rgba(56, 189, 248, 0.05);
|
||||
--page-bg-overlay2: rgba(59, 130, 246, 0.06);
|
||||
}
|
||||
|
||||
html[data-theme="dark"][data-module="library"] {
|
||||
--module-primary-color: #4a354d;
|
||||
--module-primary-light: #614666;
|
||||
--module-accent-color: #8e44ad;
|
||||
}
|
||||
|
||||
/* Global styles */
|
||||
body {
|
||||
font-family: "Manrope", "Segoe UI", "Noto Sans", sans-serif;
|
||||
background:
|
||||
radial-gradient(1200px 500px at 10% -10%, rgba(86, 145, 200, 0.08), transparent 55%),
|
||||
radial-gradient(900px 420px at 95% 0%, rgba(52, 98, 150, 0.09), transparent 52%),
|
||||
linear-gradient(180deg, #f2f5f8, #e7edf4);
|
||||
radial-gradient(1200px 500px at 10% -10%, var(--page-bg-overlay1), transparent 55%),
|
||||
radial-gradient(900px 420px at 95% 0%, var(--page-bg-overlay2), transparent 52%),
|
||||
var(--page-bg-gradient);
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
padding-bottom: 30px;
|
||||
color: #1f2d3d;
|
||||
padding-bottom: auto; /* Fallback for dynamic calculation */
|
||||
padding-bottom: env(safe-area-inset-bottom, 30px);
|
||||
color: var(--ui-text, #1f2d3d);
|
||||
}
|
||||
|
||||
.container {
|
||||
@@ -135,11 +169,13 @@
|
||||
background-color: var(--module-primary-color) !important;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
padding-top: env(safe-area-inset-top, 0); /* iOS Safe Area */
|
||||
z-index: 1900;
|
||||
}
|
||||
|
||||
.navbar .container-fluid {
|
||||
gap: 10px;
|
||||
padding-bottom: env(safe-area-inset-bottom, 0); /* Fallback */
|
||||
}
|
||||
|
||||
.navbar-brand {
|
||||
@@ -351,18 +387,19 @@
|
||||
|
||||
/* Dropdown menus - consistent with system design */
|
||||
.dropdown-menu {
|
||||
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
|
||||
border: 1px solid #dee2e6;
|
||||
box-shadow: var(--ui-shadow-md);
|
||||
border: 1px solid var(--ui-border);
|
||||
border-radius: 8px;
|
||||
padding: 8px 0;
|
||||
min-width: 200px;
|
||||
margin-top: 5px;
|
||||
background-color: var(--ui-surface);
|
||||
}
|
||||
|
||||
.dropdown-item {
|
||||
padding: 10px 16px;
|
||||
font-size: 0.9rem;
|
||||
color: #495057;
|
||||
color: var(--ui-text);
|
||||
transition: all 0.2s ease;
|
||||
border: none;
|
||||
background: none;
|
||||
@@ -370,8 +407,8 @@
|
||||
|
||||
.dropdown-item:hover,
|
||||
.dropdown-item:focus {
|
||||
background-color: #f8f9fa;
|
||||
color: #007bff;
|
||||
background-color: var(--ui-surface-soft);
|
||||
color: var(--module-accent-color);
|
||||
transform: translateX(2px);
|
||||
}
|
||||
|
||||
@@ -434,12 +471,16 @@
|
||||
}
|
||||
|
||||
.nav-item.dropdown .dropdown-toggle {
|
||||
padding: 8px 12px;
|
||||
padding: 12px 12px;
|
||||
min-height: 48px;
|
||||
}
|
||||
|
||||
.navbar-toggler {
|
||||
padding: 0.5rem 0.65rem;
|
||||
padding: 0.6rem 0.8rem;
|
||||
border-width: 2px;
|
||||
min-height: 48px;
|
||||
min-width: 48px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.function-search-wrap {
|
||||
@@ -465,7 +506,7 @@
|
||||
|
||||
/* Account dropdown stays light theme on mobile */
|
||||
.dropdown-menu-end {
|
||||
background-color: #ffffff;
|
||||
background-color: var(--ui-surface);
|
||||
border: 1px solid #dee2e6;
|
||||
}
|
||||
|
||||
@@ -474,7 +515,7 @@
|
||||
}
|
||||
|
||||
.dropdown-menu-end .dropdown-item:hover {
|
||||
background-color: #f8f9fa;
|
||||
background-color: var(--ui-surface-soft);
|
||||
color: #007bff;
|
||||
}
|
||||
}
|
||||
@@ -573,17 +614,38 @@
|
||||
margin-left: auto;
|
||||
flex: 0 0 auto;
|
||||
align-self: flex-start;
|
||||
min-width: 48px;
|
||||
min-height: 48px;
|
||||
border: none;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: background 0.2s ease;
|
||||
}
|
||||
|
||||
.navbar-toggler:active,
|
||||
.navbar-toggler:focus {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.navbar-collapse {
|
||||
order: 4;
|
||||
width: 100%;
|
||||
margin-top: 8px;
|
||||
padding: 10px 10px 6px;
|
||||
border-radius: 14px;
|
||||
background: rgba(15, 23, 42, 0.18);
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
margin-top: 12px;
|
||||
padding: 12px;
|
||||
border-radius: 16px;
|
||||
background: rgba(15, 23, 42, 0.85);
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
backdrop-filter: blur(16px);
|
||||
-webkit-backdrop-filter: blur(16px);
|
||||
max-height: calc(100vh - 90px); /* fallback */
|
||||
max-height: calc(100dvh - 90px);
|
||||
overflow-y: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
.navbar-collapse.show,
|
||||
@@ -595,7 +657,7 @@
|
||||
width: 100%;
|
||||
align-items: stretch;
|
||||
gap: 8px;
|
||||
padding-top: 2px;
|
||||
padding-top: 4px;
|
||||
}
|
||||
|
||||
.navbar-nav .nav-item {
|
||||
@@ -605,11 +667,12 @@
|
||||
.navbar-nav .nav-link {
|
||||
width: 100%;
|
||||
justify-content: flex-start;
|
||||
min-height: 46px;
|
||||
padding: 0.8rem 0.95rem;
|
||||
min-height: 48px;
|
||||
padding: 0.9rem 1.1rem;
|
||||
margin-bottom: 2px;
|
||||
border-radius: 12px;
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.08);
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.navbar-nav .nav-link.nav-active,
|
||||
@@ -632,6 +695,23 @@
|
||||
margin-left: 0;
|
||||
margin-top: 6px;
|
||||
border-radius: 12px;
|
||||
background: rgba(255, 255, 255, 0.08); /* Match nav-link slightly */
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.navbar-nav .dropdown-item {
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
padding: 12px 16px;
|
||||
border-radius: 8px;
|
||||
margin: 2px 8px;
|
||||
width: calc(100% - 16px);
|
||||
}
|
||||
|
||||
.navbar-nav .dropdown-item:hover,
|
||||
.navbar-nav .dropdown-item:focus {
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.module-selector-bar {
|
||||
@@ -792,6 +872,15 @@
|
||||
<a class="nav-link nav-priority-link {% if current_path == url_for('upload_admin') %}nav-active{% endif %}" href="{{ url_for('upload_admin') }}">➕ Hochladen</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
|
||||
<li class="nav-item">
|
||||
<button id="themeToggleBtn" class="btn btn-link nav-link px-3" aria-label="Dark Mode umschalten" title="Theme umschalten">
|
||||
<span class="theme-icon-light" style="display: none;">☀️</span>
|
||||
<span class="theme-icon-dark" style="display: none;">🌙</span>
|
||||
</button>
|
||||
</li>
|
||||
|
||||
<li class="nav-item dropdown ms-lg-auto">
|
||||
<a class="nav-link dropdown-toggle" href="#" id="invMoreDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false" title="Weitere Optionen">
|
||||
Mehr Optionen
|
||||
@@ -913,6 +1002,15 @@
|
||||
<a class="nav-link nav-priority-link {% if current_path == url_for('library_admin') %}nav-active{% endif %}" href="{{ url_for('library_admin') }}">📖 Hochladen</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
|
||||
<li class="nav-item">
|
||||
<button id="themeToggleBtn" class="btn btn-link nav-link px-3" aria-label="Dark Mode umschalten" title="Theme umschalten">
|
||||
<span class="theme-icon-light" style="display: none;">☀️</span>
|
||||
<span class="theme-icon-dark" style="display: none;">🌙</span>
|
||||
</button>
|
||||
</li>
|
||||
|
||||
<li class="nav-item dropdown ms-lg-auto">
|
||||
<a class="nav-link dropdown-toggle" href="#" id="libMoreDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false" title="Weitere Optionen">
|
||||
Mehr Optionen
|
||||
@@ -1029,7 +1127,7 @@
|
||||
|
||||
#onboarding-modal {
|
||||
width: min(680px, 100%);
|
||||
background: #ffffff;
|
||||
background: var(--ui-surface);
|
||||
border-radius: 14px;
|
||||
border: 1px solid #dbe3ee;
|
||||
box-shadow: 0 18px 35px rgba(15, 23, 42, 0.25);
|
||||
@@ -1756,5 +1854,37 @@
|
||||
|
||||
<!-- Push Notifications Script -->
|
||||
<script src="{{ url_for('static', filename='js/push-notifications.js') }}"></script>
|
||||
|
||||
<!-- Theme Toggle Script -->
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const toggleBtns = document.querySelectorAll('#themeToggleBtn');
|
||||
if (toggleBtns.length === 0) return;
|
||||
|
||||
function updateIcons(theme) {
|
||||
const isDark = theme === 'dark';
|
||||
document.querySelectorAll('.theme-icon-light').forEach(icon => icon.style.display = isDark ? 'inline' : 'none');
|
||||
document.querySelectorAll('.theme-icon-dark').forEach(icon => icon.style.display = isDark ? 'none' : 'inline');
|
||||
}
|
||||
|
||||
// Get current setup from initial script in head
|
||||
let currentTheme = document.documentElement.getAttribute('data-theme') || 'light';
|
||||
updateIcons(currentTheme);
|
||||
|
||||
toggleBtns.forEach(btn => {
|
||||
btn.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
currentTheme = currentTheme === 'light' ? 'dark' : 'light';
|
||||
|
||||
document.documentElement.setAttribute('data-theme', currentTheme);
|
||||
localStorage.setItem('inventarsystem-theme', currentTheme);
|
||||
document.getElementById('meta-theme-color').setAttribute('content', currentTheme === 'dark' ? '#1a252f' : '#2c3e50');
|
||||
|
||||
updateIcons(currentTheme);
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -53,7 +53,7 @@
|
||||
max-width: 500px;
|
||||
margin: 50px auto;
|
||||
padding: 30px;
|
||||
background-color: #f8f9fa;
|
||||
background-color: var(--ui-surface-soft);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@
|
||||
}
|
||||
|
||||
.summary-card {
|
||||
background: #fff;
|
||||
background: var(--ui-surface);
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 14px;
|
||||
padding: 14px 16px;
|
||||
@@ -85,11 +85,11 @@
|
||||
padding: 11px 14px;
|
||||
border: 1px solid #d6dde6;
|
||||
border-radius: 12px;
|
||||
background: #fff;
|
||||
background: var(--ui-surface);
|
||||
}
|
||||
|
||||
.panel {
|
||||
background: #fff;
|
||||
background: var(--ui-surface);
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 18px;
|
||||
padding: 18px;
|
||||
@@ -120,7 +120,7 @@
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
color: #64748b;
|
||||
background: #f8fafc;
|
||||
background: var(--ui-surface-soft);
|
||||
}
|
||||
|
||||
.library-table tr:hover td {
|
||||
@@ -408,7 +408,7 @@
|
||||
</div>
|
||||
|
||||
<div id="damage-invoice-modal" style="display:none; position:fixed; inset:0; background:rgba(15,23,42,0.72); z-index:9999; padding:20px; overflow:auto;">
|
||||
<div style="max-width:760px; margin:40px auto; background:#fff; border-radius:12px; padding:24px; box-shadow:0 20px 60px rgba(0,0,0,0.3);">
|
||||
<div style="max-width:760px; margin:40px auto; background: var(--ui-surface); border-radius:12px; padding:24px; box-shadow:0 20px 60px rgba(0,0,0,0.3);">
|
||||
<div style="display:flex; justify-content:space-between; align-items:center; gap:12px; margin-bottom:18px;">
|
||||
<div>
|
||||
<h2 style="margin:0;">Rechnung erstellen</h2>
|
||||
@@ -421,15 +421,15 @@
|
||||
<div style="display:grid; grid-template-columns:repeat(auto-fit, minmax(240px, 1fr)); gap:16px; margin-bottom:16px;">
|
||||
<div>
|
||||
<label for="damage-invoice-item" style="display:block; font-weight:700; margin-bottom:6px;">Element</label>
|
||||
<input id="damage-invoice-item" type="text" readonly style="width:100%; padding:10px; border:1px solid #ddd; border-radius:6px; background:#f8fafc;">
|
||||
<input id="damage-invoice-item" type="text" readonly style="width:100%; padding:10px; border:1px solid #ddd; border-radius:6px; background: var(--ui-surface-soft);">
|
||||
</div>
|
||||
<div>
|
||||
<label for="damage-invoice-borrower" style="display:block; font-weight:700; margin-bottom:6px;">Schüler</label>
|
||||
<input id="damage-invoice-borrower" type="text" readonly style="width:100%; padding:10px; border:1px solid #ddd; border-radius:6px; background:#f8fafc;">
|
||||
<input id="damage-invoice-borrower" type="text" readonly style="width:100%; padding:10px; border:1px solid #ddd; border-radius:6px; background: var(--ui-surface-soft);">
|
||||
</div>
|
||||
<div>
|
||||
<label for="damage-invoice-code" style="display:block; font-weight:700; margin-bottom:6px;">Element-ID / Code</label>
|
||||
<input id="damage-invoice-code" type="text" readonly style="width:100%; padding:10px; border:1px solid #ddd; border-radius:6px; background:#f8fafc;">
|
||||
<input id="damage-invoice-code" type="text" readonly style="width:100%; padding:10px; border:1px solid #ddd; border-radius:6px; background: var(--ui-surface-soft);">
|
||||
</div>
|
||||
<div>
|
||||
<label for="damage-invoice-amount" style="display:block; font-weight:700; margin-bottom:6px;">Preis</label>
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
}
|
||||
|
||||
.invoice-card {
|
||||
background: #fff;
|
||||
background: var(--ui-surface);
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 16px;
|
||||
padding: 18px;
|
||||
@@ -65,7 +65,7 @@
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
color: #64748b;
|
||||
background: #f8fafc;
|
||||
background: var(--ui-surface-soft);
|
||||
}
|
||||
|
||||
.invoice-table tr:hover td {
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
.library-header h1 {
|
||||
margin: 0 0 5px 0;
|
||||
font-size: 2em;
|
||||
color: #333;
|
||||
color: var(--ui-text);
|
||||
}
|
||||
|
||||
.library-header p {
|
||||
@@ -48,7 +48,7 @@
|
||||
|
||||
/* Scanner panel */
|
||||
.library-scan-panel {
|
||||
background: #ffffff;
|
||||
background: var(--ui-surface);
|
||||
border: 1px solid #d9dde4;
|
||||
border-radius: 8px;
|
||||
padding: 14px;
|
||||
@@ -68,7 +68,7 @@
|
||||
border: 1px solid #d0d7e2;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
background: #fff;
|
||||
background: var(--ui-surface);
|
||||
}
|
||||
|
||||
.library-scan-status {
|
||||
@@ -95,7 +95,7 @@
|
||||
border: 1px solid #d9dde4;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
background: #fff;
|
||||
background: var(--ui-surface);
|
||||
max-width: 460px;
|
||||
}
|
||||
|
||||
@@ -111,7 +111,7 @@
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
color: var(--ui-text);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
@@ -160,7 +160,7 @@
|
||||
.filter-item label {
|
||||
font-weight: 600;
|
||||
font-size: 0.9em;
|
||||
color: #333;
|
||||
color: var(--ui-text);
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
@@ -206,7 +206,7 @@
|
||||
}
|
||||
|
||||
.library-items-table thead {
|
||||
background: #f8f9fa;
|
||||
background: var(--ui-surface-soft);
|
||||
border-bottom: 2px solid #ddd;
|
||||
}
|
||||
|
||||
@@ -214,7 +214,7 @@
|
||||
padding: 14px 16px;
|
||||
text-align: left;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
color: var(--ui-text);
|
||||
font-size: 0.9em;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
@@ -228,7 +228,7 @@
|
||||
}
|
||||
|
||||
.library-items-table tbody tr:hover {
|
||||
background: #f8f9fa;
|
||||
background: var(--ui-surface-soft);
|
||||
}
|
||||
|
||||
.library-items-table tbody tr:last-child td {
|
||||
@@ -238,7 +238,7 @@
|
||||
/* Table cells */
|
||||
.table-title {
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
color: var(--ui-text);
|
||||
}
|
||||
|
||||
.table-status {
|
||||
@@ -320,7 +320,7 @@
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background-color: #fff;
|
||||
background-color: var(--ui-surface);
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
max-width: 900px;
|
||||
@@ -373,7 +373,7 @@
|
||||
}
|
||||
|
||||
.close:hover {
|
||||
color: #333;
|
||||
color: var(--ui-text);
|
||||
}
|
||||
|
||||
/* Mobile responsive */
|
||||
@@ -494,7 +494,7 @@
|
||||
</div>
|
||||
<div class="filter-buttons">
|
||||
<button id="applyFilterBtn" class="button" style="background: #4f46e5; color: white;">Filter anwenden</button>
|
||||
<button id="clearFilterBtn" class="button" style="background: #f0f2f5; color: #333;">Zurücksetzen</button>
|
||||
<button id="clearFilterBtn" class="button" style="background: #f0f2f5; color: var(--ui-text);">Zurücksetzen</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -337,7 +337,7 @@
|
||||
color: #0d6efd;
|
||||
}
|
||||
.license-content {
|
||||
background-color: #f8f9fa;
|
||||
background-color: var(--ui-surface-soft);
|
||||
padding: 20px 30px;
|
||||
border-radius: 0 0 5px 5px;
|
||||
border: 1px solid #dee2e6;
|
||||
|
||||
@@ -217,7 +217,7 @@
|
||||
h1 {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
color: #333;
|
||||
color: var(--ui-text);
|
||||
}
|
||||
|
||||
.filter-controls {
|
||||
|
||||
+181
-92
@@ -6,7 +6,7 @@
|
||||
|
||||
/* Calendar styles for detailed view */
|
||||
.calendar-wrapper {
|
||||
background: #fff;
|
||||
background: var(--ui-surface);
|
||||
border: 1px solid #dbe3ee;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
@@ -19,8 +19,8 @@
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px;
|
||||
border-bottom: 1px solid #e7edf5;
|
||||
background: #f8fafc;
|
||||
border-bottom: 1px solid var(--ui-border);
|
||||
background: var(--ui-surface-soft);
|
||||
}
|
||||
|
||||
.calendar-header button {
|
||||
@@ -28,15 +28,15 @@
|
||||
height: 34px;
|
||||
border: 1px solid #dbe3ee;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
color: #334155;
|
||||
background: var(--ui-surface);
|
||||
color: var(--ui-text);
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.calendar-header button:hover {
|
||||
background: #f1f5f9;
|
||||
background: var(--ui-bg);
|
||||
}
|
||||
|
||||
.calendar-header span {
|
||||
@@ -60,7 +60,7 @@
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 8px;
|
||||
color: #334155;
|
||||
color: var(--ui-text);
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
line-height: 1;
|
||||
@@ -80,7 +80,7 @@
|
||||
}
|
||||
|
||||
.cal-cell:not(.header):not(.empty):hover {
|
||||
background: #f1f5f9;
|
||||
background: var(--ui-bg);
|
||||
}
|
||||
|
||||
.cal-cell.empty {
|
||||
@@ -109,13 +109,13 @@
|
||||
}
|
||||
|
||||
.cal-cell.selected.has-booking::after {
|
||||
background: #fff;
|
||||
background: var(--ui-surface);
|
||||
}
|
||||
|
||||
.calendar-day-details {
|
||||
border-top: 1px solid #e7edf5;
|
||||
padding: 12px;
|
||||
background: #f8fafc;
|
||||
background: var(--ui-surface-soft);
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -138,7 +138,7 @@
|
||||
}
|
||||
|
||||
.cal-booking-item {
|
||||
background: #fff;
|
||||
background: var(--ui-surface);
|
||||
border: 1px solid #dbe3ee;
|
||||
border-left: 3px solid #2563eb;
|
||||
border-radius: 10px;
|
||||
@@ -173,7 +173,7 @@
|
||||
|
||||
.cal-booking-period {
|
||||
margin-left: 8px;
|
||||
color: #475569;
|
||||
color: var(--ui-text);
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
@@ -225,14 +225,14 @@
|
||||
|
||||
.filter-option-group {
|
||||
margin-bottom: 12px;
|
||||
border-bottom: 1px solid #eee;
|
||||
border-bottom: 1px solid var(--ui-border);
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
.filter-group-header {
|
||||
font-weight: bold;
|
||||
margin-bottom: 5px;
|
||||
color: #495057;
|
||||
color: var(--ui-text-muted);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
@@ -253,7 +253,7 @@
|
||||
}
|
||||
|
||||
.filter-tag-group {
|
||||
color: #6c757d;
|
||||
color: var(--ui-text-muted);
|
||||
font-size: 0.8em;
|
||||
font-style: italic;
|
||||
margin-right: 2px;
|
||||
@@ -262,7 +262,7 @@
|
||||
.filter-tag-remove {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #6c757d;
|
||||
color: var(--ui-text-muted);
|
||||
margin-left: 5px;
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
@@ -296,7 +296,7 @@
|
||||
#code-search {
|
||||
flex: 1;
|
||||
padding: 8px;
|
||||
border: 1px solid #ddd;
|
||||
border: 1px solid var(--ui-border);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
@@ -321,7 +321,7 @@
|
||||
border: 1px solid #dc3545 !important;
|
||||
}
|
||||
</style>
|
||||
<div class="container">
|
||||
<div class="container admin-content-container">
|
||||
<!-- Conflict warning banner (populated by JS) -->
|
||||
<div id="conflict-banner" style="display:none; background:#fff3cd; border:1px solid #ffc107; border-left:4px solid #fd7e14; color:#856404; padding:12px 16px; border-radius:4px; margin-bottom:16px; position:relative;">
|
||||
<strong>⚠ Reservierungskonflikt</strong>
|
||||
@@ -904,7 +904,7 @@
|
||||
const filter2More = filter2Array.length > 1 ? ` (+${filter2Array.length - 1} mehr)` : '';
|
||||
const filter3More = filter3Array.length > 1 ? ` (+${filter3Array.length - 1} mehr)` : '';
|
||||
|
||||
const imagesHtml = item.Images ? item.Images.map((file, index) => {
|
||||
const imagesHtml = item.Images && item.Images.length > 0 ? item.Images.slice(0, 1).map((file, index) => {
|
||||
// Get file with correct case handling
|
||||
const image = file;
|
||||
|
||||
@@ -1005,14 +1005,14 @@
|
||||
|
||||
card.innerHTML = `
|
||||
<div class="card-content" data-item-id="${item._id}">
|
||||
<h3 class="item-name-cell">${item.Name}</h3>
|
||||
<p class="item-ort-cell"><strong>Ort:</strong> ${item.Ort || '-'}</p>
|
||||
<p class="item-filter1-cell"><strong>Unterrichtsfach:</strong> ${filter1Display}${filter1More}</p>
|
||||
<p class="item-filter2-cell"><strong>Jahrgangsstufe:</strong> ${filter2Display}${filter2More}</p>
|
||||
<p class="item-filter3-cell"><strong>Thema:</strong> ${filter3Display}${filter3More}</p>
|
||||
<p class="item-code-cell"><strong>Barcode:</strong> ${item.Code_4 || '-'}</p>
|
||||
<p class="item-count-cell"><strong>Anzahl:</strong> ${groupedCount}</p>
|
||||
${isGroupedItem ? `<p class="item-count-cell"><strong>Verfügbar:</strong> ${availableGroupedCount}</p>` : ''}
|
||||
<h3 class="item-col-name">${item.Name}</h3>
|
||||
<p class="item-col-location"><strong>Ort:</strong> ${item.Ort || '-'}</p>
|
||||
<p class="item-col-filter1"><strong>Unterrichtsfach:</strong> ${filter1Display}${filter1More}</p>
|
||||
<p class="item-col-filter2"><strong>Jahrgangsstufe:</strong> ${filter2Display}${filter2More}</p>
|
||||
<p class="item-col-filter3"><strong>Thema:</strong> ${filter3Display}${filter3More}</p>
|
||||
<p class="item-col-code"><strong>Barcode:</strong> ${item.Code_4 || '-'}</p>
|
||||
<p class="item-col-count"><strong>Anzahl:</strong> ${groupedCount}</p>
|
||||
${isGroupedItem ? `<p class="item-col-count"><strong>Verfügbar:</strong> ${availableGroupedCount}</p>` : ''}
|
||||
<div class="image-container">
|
||||
${imagesHtml}
|
||||
</div>
|
||||
@@ -1469,7 +1469,7 @@
|
||||
const badgeText = entry.type === 'repair' ? 'Repariert' : 'Schaden';
|
||||
const metaLine = entry.meta ? `<div style="font-size:0.84rem;color:#4b5563;">${escapeHtml(entry.meta)}</div>` : '';
|
||||
return `
|
||||
<div style="border:1px solid #dbe3ee;border-radius:8px;padding:10px;background:#fff;display:grid;gap:6px;">
|
||||
<div style="border:1px solid var(--ui-border);border-radius:8px;padding:10px;background:var(--ui-surface);display:grid;gap:6px;">
|
||||
<div style="display:flex;flex-wrap:wrap;gap:8px;align-items:center;">
|
||||
<span style="display:inline-block;padding:2px 8px;border-radius:999px;font-size:0.75rem;font-weight:700;${badgeStyle}">${badgeText}</span>
|
||||
<span style="font-size:0.84rem;color:#475569;">${entry.dateLabel}</span>
|
||||
@@ -1631,7 +1631,7 @@
|
||||
|
||||
<div class="detail-group">
|
||||
<div class="detail-label">Anschaffungskosten:</div>
|
||||
<div class="detail-value">${item.Anschaffungskosten || '-'}</div>
|
||||
<div class="detail-value">${item.Anschaffungskosten ? item.Anschaffungskosten + ' €' : '-'}</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-group full-width">
|
||||
@@ -1646,7 +1646,7 @@
|
||||
<span>🛠️</span>
|
||||
<span id="toggle-damage-history-text">Historie anzeigen</span>
|
||||
</button>
|
||||
<div id="damage-history-panel" style="display:none; margin-top:8px; border:1px solid #e7edf5; border-radius:10px; padding:12px; background:#f8fafc;">
|
||||
<div id="damage-history-panel" style="display:none; margin-top:8px; border:1px solid #e7edf5; border-radius:10px; padding:12px; background: var(--ui-surface-soft);">
|
||||
<div style="display:grid; gap:10px;">${damageHistoryHtml}</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1719,7 +1719,7 @@
|
||||
: `<button class="ausleihen disabled-button" disabled>${item.BlockedNow ? 'Reserviert' : 'Ausgeliehen'}</button>`
|
||||
}
|
||||
${canScheduleItem ? `<button class="schedule-button" onclick="openScheduleModal('${item._id}')">Termin planen</button>` : ''}
|
||||
<button type="button" class="close-modal secondary-close" onclick="document.getElementById('item-modal').style.display='none';">Schließen</button>
|
||||
<button type="button" class="details-button" onclick="document.getElementById('item-modal').style.display='none';">Schließen</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -2808,7 +2808,7 @@
|
||||
}
|
||||
|
||||
.container {
|
||||
background-color: #fff;
|
||||
background-color: var(--ui-surface);
|
||||
padding: 20px;
|
||||
border-radius: 5px;
|
||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
|
||||
@@ -2833,8 +2833,8 @@
|
||||
.filter-group {
|
||||
position: relative;
|
||||
min-width: 200px;
|
||||
background-color: #fff;
|
||||
border: 1px solid #ddd;
|
||||
background-color: var(--ui-surface);
|
||||
border: 1px solid var(--ui-border);
|
||||
border-radius: 5px;
|
||||
padding: 10px;
|
||||
}
|
||||
@@ -2847,7 +2847,7 @@
|
||||
}
|
||||
|
||||
.filter-toggle, .clear-filter {
|
||||
background-color: #f8f9fa;
|
||||
background-color: var(--ui-surface-soft);
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 3px;
|
||||
padding: 3px 8px;
|
||||
@@ -2856,7 +2856,7 @@
|
||||
|
||||
.clear-filter {
|
||||
font-size: 0.8rem;
|
||||
color: #6c757d;
|
||||
color: var(--ui-text-muted);
|
||||
}
|
||||
|
||||
.filter-dropdown {
|
||||
@@ -2868,7 +2868,7 @@
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
background-color: white;
|
||||
border: 1px solid #ddd;
|
||||
border: 1px solid var(--ui-border);
|
||||
border-radius: 5px;
|
||||
box-shadow: 0 4px 8px rgba(0,0,0,0.2);
|
||||
z-index: 100;
|
||||
@@ -2903,14 +2903,14 @@
|
||||
|
||||
.filter-option-group {
|
||||
margin-bottom: 12px;
|
||||
border-bottom: 1px solid #eee;
|
||||
border-bottom: 1px solid var(--ui-border);
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
.filter-group-header {
|
||||
font-weight: bold;
|
||||
margin-bottom: 5px;
|
||||
color: #495057;
|
||||
color: var(--ui-text-muted);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
@@ -2931,7 +2931,7 @@
|
||||
}
|
||||
|
||||
.filter-tag-group {
|
||||
color: #6c757d;
|
||||
color: var(--ui-text-muted);
|
||||
font-size: 0.8em;
|
||||
font-style: italic;
|
||||
margin-right: 2px;
|
||||
@@ -2940,7 +2940,7 @@
|
||||
.filter-tag-remove {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #6c757d;
|
||||
color: var(--ui-text-muted);
|
||||
margin-left: 5px;
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
@@ -2957,7 +2957,7 @@
|
||||
#code-search {
|
||||
flex: 1;
|
||||
padding: 8px;
|
||||
border: 1px solid #ddd;
|
||||
border: 1px solid var(--ui-border);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
@@ -3028,8 +3028,8 @@
|
||||
margin-right: 8px;
|
||||
border: 1px solid #dbe3ee;
|
||||
border-radius: 10px;
|
||||
background: #f8fafc;
|
||||
color: #334155;
|
||||
background: var(--ui-surface-soft);
|
||||
color: var(--ui-text);
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
@@ -3086,19 +3086,19 @@
|
||||
}
|
||||
|
||||
.item-card {
|
||||
background-color: #fff;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 5px;
|
||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
|
||||
background-color: var(--ui-surface);
|
||||
border: 1px solid var(--ui-border);
|
||||
border-radius: var(--ui-radius);
|
||||
box-shadow: var(--ui-shadow-sm);
|
||||
padding: 20px;
|
||||
max-width: 300px;
|
||||
flex-shrink: 0;
|
||||
width: 100%;
|
||||
max-width: 340px;
|
||||
flex: 1 1 300px;
|
||||
scroll-snap-align: center;
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s;
|
||||
position: relative;
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.item-card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
|
||||
@@ -3143,7 +3143,7 @@
|
||||
.item-modal {
|
||||
display: none;
|
||||
position: fixed;
|
||||
z-index: 1000;
|
||||
z-index: 2500;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
@@ -3194,7 +3194,7 @@
|
||||
.form-group textarea {
|
||||
width: 100%;
|
||||
padding: 8px;
|
||||
border: 1px solid #ddd;
|
||||
border: 1px solid var(--ui-border);
|
||||
border-radius: 4px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
@@ -3238,7 +3238,7 @@
|
||||
}
|
||||
|
||||
.cancel-button {
|
||||
background-color: #6c757d;
|
||||
background-color: var(--ui-text-muted);
|
||||
color: white;
|
||||
}
|
||||
|
||||
@@ -3470,6 +3470,19 @@
|
||||
|
||||
/* Responsive adjustments */
|
||||
@media (max-width: 768px) {
|
||||
/* Mobile viewport and scrolling improvements */
|
||||
html, body {
|
||||
overflow-x: hidden !important;
|
||||
-webkit-overflow-scrolling: touch !important;
|
||||
touch-action: pan-y !important;
|
||||
height: 100% !important;
|
||||
}
|
||||
|
||||
body {
|
||||
padding: 0 !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
.filter-container {
|
||||
flex-direction: column;
|
||||
}
|
||||
@@ -3555,8 +3568,9 @@
|
||||
display: grid !important;
|
||||
grid-template-columns: 1fr !important;
|
||||
gap: 15px !important;
|
||||
padding: 10px 0 0 !important;
|
||||
padding: 10px 0 !important;
|
||||
overflow-x: visible !important;
|
||||
scroll-snap-type: none !important;
|
||||
}
|
||||
|
||||
.item-card {
|
||||
@@ -3640,7 +3654,7 @@
|
||||
display: block !important;
|
||||
margin-bottom: 8px !important;
|
||||
font-weight: 600 !important;
|
||||
color: #333 !important;
|
||||
color: var(--ui-title) !important;
|
||||
}
|
||||
|
||||
.form-group input, .form-group select, .form-group textarea {
|
||||
@@ -3711,8 +3725,8 @@
|
||||
}
|
||||
|
||||
/* Element text colors for better visibility */
|
||||
.edit-button, .duplicate-button, .generate-qr-button, .ausleihen {
|
||||
color: black !important;
|
||||
.edit-button, .duplicate-button, .generate-qr-button {
|
||||
color: var(--ui-title) !important;
|
||||
}
|
||||
|
||||
/* Standardized button styles across the application */
|
||||
@@ -3845,7 +3859,7 @@
|
||||
max-height: 400px;
|
||||
overflow: hidden;
|
||||
border-radius: 8px;
|
||||
background-color: #f8f9fa;
|
||||
background-color: var(--ui-surface-soft);
|
||||
}
|
||||
|
||||
/* Borrower and appointment info panels in modal */
|
||||
@@ -3873,11 +3887,11 @@
|
||||
.modal-details { margin: 20px 0; }
|
||||
.detail-group { display: flex; margin-bottom: 10px; align-items: flex-start; }
|
||||
.detail-group.full-width { flex-direction: column; margin-bottom: 15px; }
|
||||
.detail-label { font-weight: bold; color: #495057; min-width: 150px; margin-right: 10px; }
|
||||
.detail-label { font-weight: bold; color: var(--ui-text-muted); min-width: 150px; margin-right: 10px; }
|
||||
.detail-value { color: #212529; flex: 1; word-wrap: break-word; }
|
||||
.detail-group.full-width .detail-label { margin-bottom: 5px; }
|
||||
.detail-group.full-width .detail-value {
|
||||
background-color: #f8f9fa;
|
||||
background-color: var(--ui-surface-soft);
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #dee2e6;
|
||||
@@ -3941,7 +3955,7 @@
|
||||
|
||||
/* Calendar Styles - Optimized & Mobile Friendly */
|
||||
.calendar-wrapper {
|
||||
background: #fff;
|
||||
background: var(--ui-surface);
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
border: 1px solid #e5e7eb;
|
||||
@@ -3954,8 +3968,8 @@
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 15px;
|
||||
background-color: #fff;
|
||||
border-bottom: 1px solid #f3f4f6;
|
||||
background-color: var(--ui-surface);
|
||||
border-bottom: 1px solid var(--ui-border);
|
||||
}
|
||||
|
||||
.calendar-header button {
|
||||
@@ -3979,7 +3993,7 @@
|
||||
.calendar-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(7, 1fr);
|
||||
background-color: #fff;
|
||||
background-color: var(--ui-surface);
|
||||
padding: 10px;
|
||||
gap: 5px;
|
||||
}
|
||||
@@ -4041,7 +4055,7 @@
|
||||
}
|
||||
|
||||
.cal-cell.selected.has-booking::after {
|
||||
background-color: #fff;
|
||||
background-color: var(--ui-surface);
|
||||
}
|
||||
|
||||
.calendar-day-details {
|
||||
@@ -4069,7 +4083,7 @@
|
||||
|
||||
.cal-booking-item {
|
||||
padding: 10px;
|
||||
background: #fff;
|
||||
background: var(--ui-surface);
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 8px;
|
||||
@@ -4100,6 +4114,7 @@
|
||||
border: none !important;
|
||||
border-radius: 0 !important;
|
||||
box-shadow: none !important;
|
||||
box-sizing: border-box !important;
|
||||
}
|
||||
|
||||
.filter-group {
|
||||
@@ -4121,7 +4136,7 @@
|
||||
font-size: 0.95rem !important;
|
||||
font-weight: 600 !important;
|
||||
line-height: 1.35 !important;
|
||||
color: #212529 !important;
|
||||
color: var(--ui-title) !important;
|
||||
word-break: break-word !important;
|
||||
overflow-wrap: anywhere !important;
|
||||
}
|
||||
@@ -4159,7 +4174,7 @@
|
||||
overflow-wrap: break-word !important;
|
||||
}
|
||||
|
||||
body.table-view .item-card .card-content .item-name-cell {
|
||||
body.table-view .item-card .card-content .item-col-name {
|
||||
font-size: 0.95rem !important;
|
||||
}
|
||||
|
||||
@@ -4199,7 +4214,7 @@
|
||||
display: block !important;
|
||||
margin-right: 0 !important;
|
||||
margin-bottom: 2px !important;
|
||||
color: #6c757d !important;
|
||||
color: var(--ui-text-muted) !important;
|
||||
font-size: 0.8rem !important;
|
||||
}
|
||||
|
||||
@@ -4219,8 +4234,12 @@
|
||||
}
|
||||
|
||||
.items-container {
|
||||
padding: 6px 0 !important;
|
||||
gap: 10px !important;
|
||||
display: grid !important;
|
||||
grid-template-columns: 1fr !important;
|
||||
gap: 15px !important;
|
||||
padding: 10px 0 !important;
|
||||
overflow-x: visible !important;
|
||||
scroll-snap-type: none !important;
|
||||
}
|
||||
|
||||
body.table-view .table-view-header {
|
||||
@@ -4237,16 +4256,17 @@
|
||||
}
|
||||
|
||||
.item-card {
|
||||
border: none !important;
|
||||
border-radius: 0 !important;
|
||||
box-shadow: none !important;
|
||||
border-bottom: 1px solid #e5e7eb !important;
|
||||
padding: 12px 0 !important;
|
||||
max-width: 100% !important;
|
||||
margin: 0 !important;
|
||||
padding: 15px !important;
|
||||
scroll-snap-align: none !important;
|
||||
content-visibility: auto;
|
||||
contain-intrinsic-size: auto 300px;
|
||||
}
|
||||
|
||||
.item-card:hover {
|
||||
transform: none !important;
|
||||
box-shadow: none !important;
|
||||
transform: translateY(-2px) !important;
|
||||
box-shadow: var(--ui-shadow-md) !important;
|
||||
}
|
||||
|
||||
.item-card .image-container {
|
||||
@@ -4311,13 +4331,12 @@
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
height: 100vh !important;
|
||||
max-height: 100vh !important;
|
||||
margin: 0 !important;
|
||||
border: none !important;
|
||||
border-radius: 0 !important;
|
||||
width: calc(100vw - 16px) !important;
|
||||
max-width: none !important;
|
||||
margin: 80px auto 10px !important;
|
||||
max-height: 85vh !important;
|
||||
overflow-y: auto !important;
|
||||
border-radius: 8px !important;
|
||||
padding: 16px !important;
|
||||
}
|
||||
|
||||
@@ -4436,7 +4455,7 @@ function openItemQuick(id){
|
||||
}
|
||||
|
||||
.filter-group {
|
||||
background: #ffffff;
|
||||
background: var(--ui-surface);
|
||||
border: 1px solid #cfd8e3;
|
||||
border-radius: 12px;
|
||||
padding: 12px;
|
||||
@@ -4484,7 +4503,7 @@ function openItemQuick(id){
|
||||
}
|
||||
|
||||
.clear-filter {
|
||||
background: #fff;
|
||||
background: var(--ui-surface);
|
||||
color: #495868;
|
||||
}
|
||||
|
||||
@@ -4493,7 +4512,7 @@ function openItemQuick(id){
|
||||
border: 1px solid #cfd8e3;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 8px 20px rgba(20, 35, 60, 0.12);
|
||||
background: #ffffff;
|
||||
background: var(--ui-surface);
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
@@ -4579,4 +4598,74 @@ function openItemQuick(id){
|
||||
}
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
<style>
|
||||
/* Admin/Main Content Container align */
|
||||
.admin-content-container {
|
||||
--table-columns: minmax(190px, 2fr) repeat(6, minmax(110px, 1fr));
|
||||
}
|
||||
@media screen and (min-width: 769px) and (max-width: 1024px) {
|
||||
.admin-content-container {
|
||||
width: 90% !important;
|
||||
}
|
||||
.items-container {
|
||||
display: grid !important;
|
||||
grid-template-columns: repeat(2, 1fr) !important;
|
||||
gap: 20px !important;
|
||||
}
|
||||
}
|
||||
@media screen and (max-width: 768px) {
|
||||
.admin-content-container {
|
||||
width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
margin: 0 !important;
|
||||
padding: 12px 12px 18px !important;
|
||||
box-sizing: border-box !important;
|
||||
overflow-x: hidden !important;
|
||||
}
|
||||
.item-card .item-image {
|
||||
width: 100% !important;
|
||||
height: auto !important;
|
||||
max-height: 300px !important;
|
||||
display: block !important;
|
||||
object-fit: cover !important;
|
||||
}
|
||||
.item-card .image-container {
|
||||
height: 200px !important;
|
||||
overflow: hidden !important;
|
||||
position: relative !important;
|
||||
margin-bottom: 15px !important;
|
||||
}
|
||||
.actions {
|
||||
gap: 8px !important;
|
||||
border-top: none !important;
|
||||
padding-top: 8px !important;
|
||||
flex-direction: column !important;
|
||||
align-items: stretch !important;
|
||||
}
|
||||
.actions form,
|
||||
.actions button,
|
||||
.actions a {
|
||||
width: 100% !important;
|
||||
}
|
||||
.items-container, .item-card, .image-container, .item-image {
|
||||
touch-action: pan-y !important;
|
||||
-webkit-overflow-scrolling: touch !important;
|
||||
}
|
||||
}
|
||||
@media (max-width: 767px) {
|
||||
body {
|
||||
overflow-x: hidden;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
touch-action: pan-y;
|
||||
}
|
||||
.admin-content-container {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
margin: 0;
|
||||
padding: 12px 12px 18px;
|
||||
box-sizing: border-box;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
.items-indicator {
|
||||
text-align: center;
|
||||
font-size: 0.9rem;
|
||||
color: #495057;
|
||||
color: var(--ui-text-muted);
|
||||
margin: 6px 0 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
@@ -55,7 +55,7 @@
|
||||
max-width: 980px;
|
||||
margin: 0 auto 18px;
|
||||
border: 1px solid #dbe3ee;
|
||||
background: #ffffff;
|
||||
background: var(--ui-surface);
|
||||
border-radius: 10px;
|
||||
padding: 14px;
|
||||
box-shadow: 0 8px 18px rgba(15, 23, 42, 0.08);
|
||||
@@ -78,7 +78,7 @@
|
||||
}
|
||||
|
||||
.bulk-delete-summary {
|
||||
color: #334155;
|
||||
color: var(--ui-text);
|
||||
font-size: 0.92rem;
|
||||
margin-top: 4px;
|
||||
}
|
||||
@@ -104,8 +104,8 @@
|
||||
border-radius: 8px;
|
||||
padding: 9px 12px;
|
||||
cursor: pointer;
|
||||
background: #f8fafc;
|
||||
color: #1e293b;
|
||||
background: var(--ui-surface-soft);
|
||||
color: var(--ui-title);
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
}
|
||||
@@ -138,8 +138,8 @@
|
||||
.filter-group {
|
||||
position: relative;
|
||||
min-width: 200px;
|
||||
background-color: #fff;
|
||||
border: 1px solid #ddd;
|
||||
background-color: var(--ui-surface);
|
||||
border: 1px solid var(--ui-border);
|
||||
border-radius: 5px;
|
||||
padding: 10px;
|
||||
}
|
||||
@@ -152,7 +152,7 @@
|
||||
}
|
||||
|
||||
.filter-toggle, .clear-filter {
|
||||
background-color: #f8f9fa;
|
||||
background-color: var(--ui-surface-soft);
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 3px;
|
||||
padding: 3px 8px;
|
||||
@@ -161,7 +161,7 @@
|
||||
|
||||
.clear-filter {
|
||||
font-size: 0.8rem;
|
||||
color: #6c757d;
|
||||
color: var(--ui-text-muted);
|
||||
}
|
||||
|
||||
.filter-dropdown {
|
||||
@@ -173,7 +173,7 @@
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
background-color: white;
|
||||
border: 1px solid #ddd;
|
||||
border: 1px solid var(--ui-border);
|
||||
border-radius: 5px;
|
||||
box-shadow: 0 4px 8px rgba(0,0,0,0.2);
|
||||
z-index: 100;
|
||||
@@ -208,14 +208,14 @@
|
||||
|
||||
.filter-option-group {
|
||||
margin-bottom: 12px;
|
||||
border-bottom: 1px solid #eee;
|
||||
border-bottom: 1px solid var(--ui-border);
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
.filter-group-header {
|
||||
font-weight: bold;
|
||||
margin-bottom: 5px;
|
||||
color: #495057;
|
||||
color: var(--ui-text-muted);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
@@ -236,7 +236,7 @@
|
||||
}
|
||||
|
||||
.filter-tag-group {
|
||||
color: #6c757d;
|
||||
color: var(--ui-text-muted);
|
||||
font-size: 0.8em;
|
||||
font-style: italic;
|
||||
margin-right: 2px;
|
||||
@@ -245,7 +245,7 @@
|
||||
.filter-tag-remove {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #6c757d;
|
||||
color: var(--ui-text-muted);
|
||||
margin-left: 5px;
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
@@ -262,7 +262,7 @@
|
||||
#code-search {
|
||||
flex: 1;
|
||||
padding: 8px;
|
||||
border: 1px solid #ddd;
|
||||
border: 1px solid var(--ui-border);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
@@ -333,8 +333,8 @@
|
||||
margin-right: 8px;
|
||||
border: 1px solid #dbe3ee;
|
||||
border-radius: 10px;
|
||||
background: #f8fafc;
|
||||
color: #334155;
|
||||
background: var(--ui-surface-soft);
|
||||
color: var(--ui-text);
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
@@ -391,16 +391,17 @@
|
||||
}
|
||||
|
||||
.item-card {
|
||||
background-color: #fff;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 5px;
|
||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
|
||||
background-color: var(--ui-surface);
|
||||
border: 1px solid var(--ui-border);
|
||||
border-radius: var(--ui-radius);
|
||||
box-shadow: var(--ui-shadow-sm);
|
||||
padding: 20px;
|
||||
max-width: 300px;
|
||||
flex-shrink: 0;
|
||||
width: 100%;
|
||||
max-width: 340px;
|
||||
flex: 1 1 300px;
|
||||
scroll-snap-align: center;
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s;
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
@@ -447,7 +448,7 @@
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 0.9rem;
|
||||
color: #475569;
|
||||
color: var(--ui-text);
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
@@ -581,7 +582,7 @@
|
||||
.item-modal {
|
||||
display: none;
|
||||
position: fixed;
|
||||
z-index: 1000;
|
||||
z-index: 2500;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
@@ -632,7 +633,7 @@
|
||||
.form-group textarea {
|
||||
width: 100%;
|
||||
padding: 8px;
|
||||
border: 1px solid #ddd;
|
||||
border: 1px solid var(--ui-border);
|
||||
border-radius: 4px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
@@ -676,7 +677,7 @@
|
||||
}
|
||||
|
||||
.cancel-button {
|
||||
background-color: #6c757d;
|
||||
background-color: var(--ui-text-muted);
|
||||
color: white;
|
||||
}
|
||||
|
||||
@@ -703,15 +704,15 @@
|
||||
.filter-inputs {
|
||||
margin: 20px 0;
|
||||
padding: 15px;
|
||||
border: 1px solid #ddd;
|
||||
border: 1px solid var(--ui-border);
|
||||
border-radius: 5px;
|
||||
background-color: #f8f9fa;
|
||||
background-color: var(--ui-surface-soft);
|
||||
}
|
||||
|
||||
.filter-inputs h3 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 15px;
|
||||
color: #495057;
|
||||
color: var(--ui-text-muted);
|
||||
}
|
||||
|
||||
.multi-filter {
|
||||
@@ -723,7 +724,7 @@
|
||||
.filter-dropdown-select {
|
||||
width: 100%;
|
||||
padding: 8px;
|
||||
border: 1px solid #ddd;
|
||||
border: 1px solid var(--ui-border);
|
||||
border-radius: 4px;
|
||||
background-color: white;
|
||||
}
|
||||
@@ -1000,16 +1001,16 @@
|
||||
|
||||
/* Disabled Button */
|
||||
.disabled-button {
|
||||
background-color: #6c757d !important;
|
||||
border-color: #6c757d !important;
|
||||
background-color: var(--ui-text-muted) !important;
|
||||
border-color: var(--ui-text-muted) !important;
|
||||
color: #fff !important;
|
||||
cursor: not-allowed !important;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.disabled-button:hover {
|
||||
background-color: #6c757d !important;
|
||||
border-color: #6c757d !important;
|
||||
background-color: var(--ui-text-muted) !important;
|
||||
border-color: var(--ui-text-muted) !important;
|
||||
transform: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
@@ -1032,7 +1033,7 @@
|
||||
|
||||
.detail-label {
|
||||
font-weight: bold;
|
||||
color: #495057;
|
||||
color: var(--ui-text-muted);
|
||||
min-width: 150px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
@@ -1048,7 +1049,7 @@
|
||||
}
|
||||
|
||||
.detail-group.full-width .detail-value {
|
||||
background-color: #f8f9fa;
|
||||
background-color: var(--ui-surface-soft);
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #dee2e6;
|
||||
@@ -1068,7 +1069,7 @@
|
||||
max-height: 400px;
|
||||
overflow: hidden;
|
||||
border-radius: 8px;
|
||||
background-color: #f8f9fa;
|
||||
background-color: var(--ui-surface-soft);
|
||||
}
|
||||
|
||||
.modal-image {
|
||||
@@ -1351,7 +1352,7 @@
|
||||
display: block !important;
|
||||
margin-bottom: 8px !important;
|
||||
font-weight: 600 !important;
|
||||
color: #333 !important;
|
||||
color: var(--ui-title) !important;
|
||||
}
|
||||
|
||||
.form-group input, .form-group select, .form-group textarea {
|
||||
@@ -1389,7 +1390,7 @@
|
||||
/* Tab navigation improvements */
|
||||
.nav-tabs {
|
||||
flex-wrap: wrap !important;
|
||||
border-bottom: 1px solid #dee2e6 !important;
|
||||
border-bottom: 1px solid var(--ui-border) !important;
|
||||
}
|
||||
|
||||
.nav-tabs .nav-link {
|
||||
@@ -1605,6 +1606,7 @@
|
||||
gap: 15px !important;
|
||||
padding: 10px 0 !important;
|
||||
overflow-x: visible !important;
|
||||
scroll-snap-type: none !important;
|
||||
}
|
||||
|
||||
.item-card {
|
||||
@@ -1688,7 +1690,7 @@
|
||||
display: block !important;
|
||||
margin-bottom: 8px !important;
|
||||
font-weight: 600 !important;
|
||||
color: #333 !important;
|
||||
color: var(--ui-title) !important;
|
||||
}
|
||||
|
||||
.form-group input, .form-group select, .form-group textarea {
|
||||
@@ -1962,8 +1964,8 @@
|
||||
}
|
||||
|
||||
/* Element text colors for better visibility */
|
||||
.edit-button, .duplicate-button, .generate-qr-button, .ausleihen {
|
||||
color: black !important;
|
||||
.edit-button, .duplicate-button, .generate-qr-button {
|
||||
color: var(--ui-title) !important;
|
||||
}
|
||||
|
||||
/* Standardized button styles across the application */
|
||||
@@ -2006,7 +2008,7 @@
|
||||
}
|
||||
/* Calendar styles for detailed view */
|
||||
.calendar-wrapper {
|
||||
background: #fff;
|
||||
background: var(--ui-surface);
|
||||
border: 1px solid #dbe3ee;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
@@ -2019,8 +2021,8 @@
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px;
|
||||
border-bottom: 1px solid #e7edf5;
|
||||
background: #f8fafc;
|
||||
border-bottom: 1px solid var(--ui-border);
|
||||
background: var(--ui-surface-soft);
|
||||
}
|
||||
|
||||
.calendar-header button {
|
||||
@@ -2028,15 +2030,15 @@
|
||||
height: 34px;
|
||||
border: 1px solid #dbe3ee;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
color: #334155;
|
||||
background: var(--ui-surface);
|
||||
color: var(--ui-text);
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.calendar-header button:hover {
|
||||
background: #f1f5f9;
|
||||
background: var(--ui-bg);
|
||||
}
|
||||
|
||||
.calendar-header span {
|
||||
@@ -2060,7 +2062,7 @@
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 8px;
|
||||
color: #334155;
|
||||
color: var(--ui-text);
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
line-height: 1;
|
||||
@@ -2080,7 +2082,7 @@
|
||||
}
|
||||
|
||||
.cal-cell:not(.header):not(.empty):hover {
|
||||
background: #f1f5f9;
|
||||
background: var(--ui-bg);
|
||||
}
|
||||
|
||||
.cal-cell.empty {
|
||||
@@ -2109,13 +2111,13 @@
|
||||
}
|
||||
|
||||
.cal-cell.selected.has-booking::after {
|
||||
background: #fff;
|
||||
background: var(--ui-surface);
|
||||
}
|
||||
|
||||
.calendar-day-details {
|
||||
border-top: 1px solid #e7edf5;
|
||||
padding: 12px;
|
||||
background: #f8fafc;
|
||||
background: var(--ui-surface-soft);
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -2138,7 +2140,7 @@
|
||||
}
|
||||
|
||||
.cal-booking-item {
|
||||
background: #fff;
|
||||
background: var(--ui-surface);
|
||||
border: 1px solid #dbe3ee;
|
||||
border-left: 3px solid #2563eb;
|
||||
border-radius: 10px;
|
||||
@@ -2173,7 +2175,7 @@
|
||||
|
||||
.cal-booking-period {
|
||||
margin-left: 8px;
|
||||
color: #475569;
|
||||
color: var(--ui-text);
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
@@ -2582,7 +2584,7 @@ document.addEventListener('DOMContentLoaded', ()=>{
|
||||
<input type="date" id="edit-year" name="anschaffungsjahr">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="edit-cost">Anschaffungskosten:</label>
|
||||
<label for="edit-cost">Anschaffungskosten (€):</label>
|
||||
<input id="edit-cost" name="anschaffungskosten">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
@@ -3528,7 +3530,7 @@ document.addEventListener('DOMContentLoaded', ()=>{
|
||||
const filter2More = filter2Array.length > 1 ? ` (+${filter2Array.length - 1} mehr)` : '';
|
||||
const filter3More = filter3Array.length > 1 ? ` (+${filter3Array.length - 1} mehr)` : '';
|
||||
|
||||
const imagesHtml = item.Images ? item.Images.map((image, index) => {
|
||||
const imagesHtml = item.Images && item.Images.length > 0 ? item.Images.slice(0, 1).map((image, index) => {
|
||||
// Check if the image already has a full URL path or just the filename
|
||||
const imageSrc = image.startsWith('/uploads/') || image.startsWith('http') ?
|
||||
image :
|
||||
@@ -4361,7 +4363,7 @@ document.addEventListener('DOMContentLoaded', ()=>{
|
||||
const badgeText = entry.type === 'repair' ? 'Repariert' : 'Schaden';
|
||||
const metaLine = entry.meta ? `<div style="font-size:0.84rem;color:#4b5563;">${escapeHtml(entry.meta)}</div>` : '';
|
||||
return `
|
||||
<div style="border:1px solid #dbe3ee;border-radius:8px;padding:10px;background:#fff;display:grid;gap:6px;">
|
||||
<div style="border:1px solid var(--ui-border);border-radius:8px;padding:10px;background:var(--ui-surface);display:grid;gap:6px;">
|
||||
<div style="display:flex;flex-wrap:wrap;gap:8px;align-items:center;">
|
||||
<span style="display:inline-block;padding:2px 8px;border-radius:999px;font-size:0.75rem;font-weight:700;${badgeStyle}">${badgeText}</span>
|
||||
<span style="font-size:0.84rem;color:#475569;">${entry.dateLabel}</span>
|
||||
@@ -4439,7 +4441,7 @@ document.addEventListener('DOMContentLoaded', ()=>{
|
||||
|
||||
<div class="detail-group">
|
||||
<div class="detail-label">Anschaffungskosten:</div>
|
||||
<div class="detail-value">${item.Anschaffungskosten || '-'}</div>
|
||||
<div class="detail-value">${item.Anschaffungskosten ? item.Anschaffungskosten + ' €' : '-'}</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-group full-width">
|
||||
@@ -4454,7 +4456,7 @@ document.addEventListener('DOMContentLoaded', ()=>{
|
||||
<span>🛠️</span>
|
||||
<span id="toggle-damage-history-text">Historie anzeigen</span>
|
||||
</button>
|
||||
<div id="damage-history-panel" style="display:none; margin-top:8px; border:1px solid #e7edf5; border-radius:10px; padding:12px; background:#f8fafc;">
|
||||
<div id="damage-history-panel" style="display:none; margin-top:8px; border:1px solid #e7edf5; border-radius:10px; padding:12px; background: var(--ui-surface-soft);">
|
||||
<div style="display:grid; gap:10px;">${damageHistoryHtml}</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -4533,6 +4535,7 @@ document.addEventListener('DOMContentLoaded', ()=>{
|
||||
<form method="POST" action="/delete_item/${item._id}" style="display:inline;" onsubmit="return confirm('Sind Sie sicher?')">
|
||||
<button class="delete-button" type="submit">Löschen</button>
|
||||
</form>
|
||||
<button type="button" class="details-button" onclick="document.getElementById('item-modal').style.display='none';">Schließen</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -5500,7 +5503,7 @@ document.addEventListener('DOMContentLoaded', ()=>{
|
||||
}
|
||||
|
||||
.filter-group {
|
||||
background: #ffffff;
|
||||
background: var(--ui-surface);
|
||||
border: 1px solid #cfd8e3;
|
||||
border-radius: 12px;
|
||||
padding: 12px;
|
||||
@@ -5548,7 +5551,7 @@ document.addEventListener('DOMContentLoaded', ()=>{
|
||||
}
|
||||
|
||||
.clear-filter {
|
||||
background: #fff;
|
||||
background: var(--ui-surface);
|
||||
color: #495868;
|
||||
}
|
||||
|
||||
@@ -5557,7 +5560,7 @@ document.addEventListener('DOMContentLoaded', ()=>{
|
||||
border: 1px solid #cfd8e3;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 8px 20px rgba(20, 35, 60, 0.12);
|
||||
background: #ffffff;
|
||||
background: var(--ui-surface);
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
@@ -5667,7 +5670,7 @@ document.addEventListener('DOMContentLoaded', ()=>{
|
||||
display: block !important;
|
||||
margin-right: 0 !important;
|
||||
margin-bottom: 2px !important;
|
||||
color: #6c757d !important;
|
||||
color: var(--ui-text-muted) !important;
|
||||
font-size: 0.8rem !important;
|
||||
}
|
||||
|
||||
|
||||
@@ -78,12 +78,12 @@
|
||||
border-radius: 5px;
|
||||
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
|
||||
margin-bottom: 20px;
|
||||
background-color: #fff;
|
||||
background-color: var(--ui-surface);
|
||||
border: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
background-color: #f8f9fa;
|
||||
background-color: var(--ui-surface-soft);
|
||||
padding: 15px 20px;
|
||||
border-bottom: 1px solid #e9ecef;
|
||||
}
|
||||
@@ -170,7 +170,7 @@
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 10px;
|
||||
background-color: #f8f9fa;
|
||||
background-color: var(--ui-surface-soft);
|
||||
border-radius: 4px;
|
||||
border-left: 3px solid #6c757d;
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@
|
||||
bottom: 125%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background-color: #333;
|
||||
background-color: var(--ui-text);
|
||||
color: white;
|
||||
padding: 5px 10px;
|
||||
border-radius: 4px;
|
||||
@@ -405,7 +405,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
}
|
||||
|
||||
.borrowed-item {
|
||||
background-color: #fff;
|
||||
background-color: var(--ui-surface);
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
@@ -457,7 +457,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
|
||||
.exemplar-details {
|
||||
margin-top: 10px;
|
||||
background-color: #f8f9fa;
|
||||
background-color: var(--ui-surface-soft);
|
||||
padding: 10px;
|
||||
border-radius: 5px;
|
||||
border-left: 3px solid #6c757d;
|
||||
@@ -515,7 +515,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
.no-items-message {
|
||||
text-align: center;
|
||||
padding: 30px;
|
||||
background-color: #f8f9fa;
|
||||
background-color: var(--ui-surface-soft);
|
||||
border-radius: 5px;
|
||||
color: #6c757d;
|
||||
margin-top: 20px;
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<div id="push-notification-settings" style="margin-bottom: 24px;"></div>
|
||||
|
||||
<div style="display:grid; grid-template-columns:1fr; gap:14px;">
|
||||
<section style="background:#fff; border:1px solid #e2e8f0; border-radius:14px; padding:16px;">
|
||||
<section style="background: var(--ui-surface); border:1px solid #e2e8f0; border-radius:14px; padding:16px;">
|
||||
<h2 style="margin:0 0 12px; font-size:1.15rem;">Meine Benachrichtigungen</h2>
|
||||
{% if user_notifications %}
|
||||
<div style="display:grid; gap:10px;">
|
||||
@@ -53,7 +53,7 @@
|
||||
</section>
|
||||
|
||||
{% if is_admin_user %}
|
||||
<section style="background:#fff; border:1px solid #e2e8f0; border-radius:14px; padding:16px;">
|
||||
<section style="background: var(--ui-surface); border:1px solid #e2e8f0; border-radius:14px; padding:16px;">
|
||||
<h2 style="margin:0 0 12px; font-size:1.15rem;">Admin-Benachrichtigungen</h2>
|
||||
{% if admin_notifications %}
|
||||
<div style="display:grid; gap:10px;">
|
||||
|
||||
@@ -121,7 +121,7 @@
|
||||
--primary-dark: #2980b9;
|
||||
--success-color: #2ecc71;
|
||||
--error-color: #e74c3c;
|
||||
--text-color: #333;
|
||||
--text-color: var(--ui-text);
|
||||
--light-bg: #f9f9f9;
|
||||
--border-radius: 8px;
|
||||
--shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
@@ -163,7 +163,7 @@ body {
|
||||
}
|
||||
|
||||
.content {
|
||||
background-color: #fff;
|
||||
background-color: var(--ui-surface);
|
||||
padding: 2rem;
|
||||
border-radius: var(--border-radius);
|
||||
box-shadow: var(--shadow);
|
||||
@@ -345,7 +345,7 @@ input::placeholder {
|
||||
padding: 10px 12px;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 8px;
|
||||
background: #f8fafc;
|
||||
background: var(--ui-surface-soft);
|
||||
}
|
||||
|
||||
.password-rules-title {
|
||||
@@ -406,7 +406,7 @@ input::placeholder {
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 8px;
|
||||
padding: 10px;
|
||||
background: #f8fafc;
|
||||
background: var(--ui-surface-soft);
|
||||
}
|
||||
|
||||
.permission-panel h4 {
|
||||
|
||||
@@ -129,7 +129,7 @@ function createLoadingIndicator(message) {
|
||||
}
|
||||
|
||||
.reset-loading-message {
|
||||
color: #333;
|
||||
color: var(--ui-text);
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
@@ -207,7 +207,7 @@
|
||||
}
|
||||
|
||||
.reset-item-info {
|
||||
background: #f8f9fa;
|
||||
background: var(--ui-surface-soft);
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
margin-bottom: 25px;
|
||||
@@ -216,7 +216,7 @@
|
||||
|
||||
.reset-item-info h3 {
|
||||
margin: 0 0 15px 0;
|
||||
color: #333;
|
||||
color: var(--ui-text);
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
|
||||
@@ -232,7 +232,7 @@
|
||||
}
|
||||
|
||||
.reset-item-details strong {
|
||||
color: #333;
|
||||
color: var(--ui-text);
|
||||
}
|
||||
|
||||
.reset-warning-section {
|
||||
@@ -285,7 +285,7 @@
|
||||
flex-direction: column;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
color: var(--ui-text);
|
||||
}
|
||||
|
||||
.reset-action-desc {
|
||||
@@ -298,7 +298,7 @@
|
||||
.active-borrowings {
|
||||
margin-top: 10px;
|
||||
padding: 10px;
|
||||
background: #f8f9fa;
|
||||
background: var(--ui-surface-soft);
|
||||
border-radius: 5px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
@@ -321,7 +321,7 @@
|
||||
|
||||
.reset-options-section h4 {
|
||||
margin: 0 0 15px 0;
|
||||
color: #333;
|
||||
color: var(--ui-text);
|
||||
}
|
||||
|
||||
.reset-option {
|
||||
@@ -365,7 +365,7 @@
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
color: var(--ui-text);
|
||||
}
|
||||
|
||||
.reset-reason-section textarea {
|
||||
@@ -391,7 +391,7 @@
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 15px;
|
||||
background: #f8f9fa;
|
||||
background: var(--ui-surface-soft);
|
||||
border-radius: 0 0 12px 12px;
|
||||
}
|
||||
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: #333;
|
||||
color: var(--ui-text);
|
||||
font-size: 28px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
@@ -67,7 +67,7 @@
|
||||
}
|
||||
|
||||
.info-box strong {
|
||||
color: #333;
|
||||
color: var(--ui-text);
|
||||
}
|
||||
|
||||
.info-box p {
|
||||
@@ -106,7 +106,7 @@
|
||||
|
||||
.btn-secondary {
|
||||
background: #e0e0e0;
|
||||
color: #333;
|
||||
color: var(--ui-text);
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
}
|
||||
|
||||
.student-card-form {
|
||||
background: #f5f5f5;
|
||||
background: var(--ui-bg);
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 30px;
|
||||
@@ -43,7 +43,7 @@
|
||||
.form-group label {
|
||||
font-weight: 600;
|
||||
margin-bottom: 5px;
|
||||
color: #333;
|
||||
color: var(--ui-text);
|
||||
}
|
||||
|
||||
.form-group input,
|
||||
|
||||
@@ -1601,7 +1601,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
.booking-summary {
|
||||
margin: 15px 0;
|
||||
padding: 10px;
|
||||
background-color: #f8f9fa;
|
||||
background-color: var(--ui-surface-soft);
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
font-size: 0.9em;
|
||||
|
||||
@@ -72,7 +72,7 @@
|
||||
|
||||
.tutorial-side {
|
||||
grid-column: span 12;
|
||||
background: #ffffff;
|
||||
background: var(--ui-surface);
|
||||
border: 1px solid #cfdbe8;
|
||||
border-radius: 16px;
|
||||
padding: 16px;
|
||||
@@ -102,7 +102,7 @@
|
||||
|
||||
.workflow-nav button {
|
||||
border: 1px solid #dbe3ee;
|
||||
background: #f8fafc;
|
||||
background: var(--ui-surface-soft);
|
||||
color: #0f172a;
|
||||
border-radius: 12px;
|
||||
padding: 11px 12px;
|
||||
@@ -133,7 +133,7 @@
|
||||
|
||||
.workflow-step {
|
||||
grid-column: span 12;
|
||||
background: #ffffff;
|
||||
background: var(--ui-surface);
|
||||
border: 1px solid #cfdbe8;
|
||||
border-radius: 16px;
|
||||
padding: 18px;
|
||||
@@ -331,17 +331,17 @@
|
||||
<h3>Tägliche Aufgaben</h3>
|
||||
<p>Das ist der gewöhnliche Arbeitsablauf während eines Schultages.</p>
|
||||
|
||||
<div style="background: #f8fafc; border: 1px solid #dbe3ee; border-radius: 12px; padding: 14px; margin: 14px 0;">
|
||||
<div style="background: var(--ui-surface-soft); border: 1px solid #dbe3ee; border-radius: 12px; padding: 14px; margin: 14px 0;">
|
||||
<p style="margin-bottom: 10px; font-weight: 700; color: #0f172a;">Morgens:</p>
|
||||
<p style="margin: 0; color: #334155;">Öffnen Sie die Startseite und prüfen Sie offene Reservierungen oder Rückgabefristen.</p>
|
||||
</div>
|
||||
|
||||
<div style="background: #f8fafc; border: 1px solid #dbe3ee; border-radius: 12px; padding: 14px; margin: 14px 0;">
|
||||
<div style="background: var(--ui-surface-soft); border: 1px solid #dbe3ee; border-radius: 12px; padding: 14px; margin: 14px 0;">
|
||||
<p style="margin-bottom: 10px; font-weight: 700; color: #0f172a;">Mittags:</p>
|
||||
<p style="margin: 0; color: #334155;">Neue Artikel oder Bücher hinzufügen oder Ausleihungen begründen.</p>
|
||||
</div>
|
||||
|
||||
<div style="background: #f8fafc; border: 1px solid #dbe3ee; border-radius: 12px; padding: 14px; margin: 14px 0;">
|
||||
<div style="background: var(--ui-surface-soft); border: 1px solid #dbe3ee; border-radius: 12px; padding: 14px; margin: 14px 0;">
|
||||
<p style="margin-bottom: 10px; font-weight: 700; color: #0f172a;">Nachmittags:</p>
|
||||
<p style="margin: 0; color: #334155;">Rückgaben buchen und kurz prüfen, ob alles in Ordnung ist.</p>
|
||||
</div>
|
||||
|
||||
@@ -78,14 +78,14 @@
|
||||
|
||||
.book-info {
|
||||
padding: 15px;
|
||||
background-color: #fff;
|
||||
background-color: var(--ui-surface);
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 5px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.book-info h4 {
|
||||
color: #333;
|
||||
color: var(--ui-text);
|
||||
margin: 0 0 10px 0;
|
||||
font-size: 1.2em;
|
||||
font-weight: bold;
|
||||
@@ -115,7 +115,7 @@
|
||||
}
|
||||
|
||||
.book-description h5 {
|
||||
color: #333;
|
||||
color: var(--ui-text);
|
||||
margin: 0 0 8px 0;
|
||||
font-size: 1em;
|
||||
font-weight: bold;
|
||||
@@ -125,7 +125,7 @@
|
||||
max-height: 150px;
|
||||
overflow-y: auto;
|
||||
padding: 5px;
|
||||
background-color: #f8f9fa;
|
||||
background-color: var(--ui-surface-soft);
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: 3px;
|
||||
font-size: 0.9em;
|
||||
@@ -187,7 +187,7 @@
|
||||
font-style: italic;
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
background-color: #f8f9fa;
|
||||
background-color: var(--ui-surface-soft);
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 4px;
|
||||
}
|
||||
@@ -334,7 +334,7 @@
|
||||
}
|
||||
|
||||
.popup-content p {
|
||||
color: #333;
|
||||
color: var(--ui-text);
|
||||
margin: 15px 0;
|
||||
line-height: 1.5;
|
||||
font-size: 1em;
|
||||
@@ -380,7 +380,7 @@
|
||||
}
|
||||
|
||||
.popup-close-x:hover {
|
||||
color: #333;
|
||||
color: var(--ui-text);
|
||||
}
|
||||
|
||||
/* Upload form styles */
|
||||
@@ -388,13 +388,13 @@
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
background-color: #fff;
|
||||
background-color: var(--ui-surface);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.upload-form h1 {
|
||||
color: #333;
|
||||
color: var(--ui-text);
|
||||
margin-bottom: 30px;
|
||||
text-align: center;
|
||||
font-size: 2em;
|
||||
@@ -408,7 +408,7 @@
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
color: var(--ui-text);
|
||||
}
|
||||
|
||||
.form-group input,
|
||||
@@ -428,14 +428,14 @@
|
||||
}
|
||||
|
||||
.filter-inputs {
|
||||
background-color: #f8f9fa;
|
||||
background-color: var(--ui-surface-soft);
|
||||
padding: 20px;
|
||||
border-radius: 5px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.filter-inputs h3 {
|
||||
color: #333;
|
||||
color: var(--ui-text);
|
||||
margin-bottom: 15px;
|
||||
font-size: 1.2em;
|
||||
}
|
||||
@@ -863,7 +863,7 @@
|
||||
<input type="date" id="anschaffungsjahr" name="anschaffungsjahr">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="anschaffungskosten">Anschaffungskosten</label>
|
||||
<label for="anschaffungskosten">Anschaffungskosten (€)</label>
|
||||
<input id="anschaffungskosten" name="anschaffungskosten">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
|
||||
@@ -407,14 +407,14 @@
|
||||
}
|
||||
|
||||
.filter-bar {
|
||||
background-color: #f8f9fa;
|
||||
background-color: var(--ui-surface-soft);
|
||||
padding: 15px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #dee2e6;
|
||||
}
|
||||
|
||||
.password-requirements {
|
||||
background-color: #f8f9fa;
|
||||
background-color: var(--ui-surface-soft);
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
margin-top: 10px;
|
||||
@@ -439,7 +439,7 @@
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
background: #f8fafc;
|
||||
background: var(--ui-surface-soft);
|
||||
}
|
||||
|
||||
.permissions-group h6 {
|
||||
|
||||
-107
@@ -1,107 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
cd "$SCRIPT_DIR"
|
||||
|
||||
SUDO=""
|
||||
if [ "$(id -u)" -ne 0 ] && command -v sudo >/dev/null 2>&1; then
|
||||
SUDO="sudo"
|
||||
fi
|
||||
|
||||
VENV_PYTHON="$SCRIPT_DIR/.venv/bin/python"
|
||||
if [ ! -x "$VENV_PYTHON" ]; then
|
||||
echo "Error: .venv python not found at $VENV_PYTHON"
|
||||
echo "Create it first, e.g.: python3 -m venv .venv && source .venv/bin/activate && pip install -r requirements.txt"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "$(id -u)" -ne 0 ] && [ ! -w "$SCRIPT_DIR/.venv" ]; then
|
||||
echo "Fixing ownership of .venv for current user..."
|
||||
if [ -n "$SUDO" ]; then
|
||||
$SUDO chown -R "$(id -un):$(id -gn)" "$SCRIPT_DIR/.venv"
|
||||
else
|
||||
echo "Error: .venv is not writable and sudo is unavailable."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
for path in "$SCRIPT_DIR/dist" "$SCRIPT_DIR/build"; do
|
||||
if [ -e "$path" ] && [ "$(id -u)" -ne 0 ]; then
|
||||
echo "Ensuring ownership of $(basename "$path") for current user..."
|
||||
if [ -n "$SUDO" ]; then
|
||||
$SUDO chown -R "$(id -un):$(id -gn)" "$path"
|
||||
else
|
||||
if [ ! -w "$path" ]; then
|
||||
echo "Error: $path is not writable and sudo is unavailable."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
echo "Installing Nuitka build dependencies in .venv..."
|
||||
"$VENV_PYTHON" -m pip install --upgrade pip setuptools wheel
|
||||
"$VENV_PYTHON" -m pip install --upgrade "nuitka==2.8.10" ordered-set zstandard
|
||||
|
||||
DIST_DIR="$SCRIPT_DIR/dist"
|
||||
BUILD_DIR="$SCRIPT_DIR/build"
|
||||
OUTPUT_NAME="inventarsystem"
|
||||
|
||||
mkdir -p "$DIST_DIR" "$BUILD_DIR"
|
||||
|
||||
NUITKA_DATA_ARGS=()
|
||||
|
||||
if [ -d "$SCRIPT_DIR/Web/templates" ]; then
|
||||
NUITKA_DATA_ARGS+=("--include-data-dir=$SCRIPT_DIR/Web/templates=templates")
|
||||
fi
|
||||
|
||||
if [ -d "$SCRIPT_DIR/Web/static" ]; then
|
||||
NUITKA_DATA_ARGS+=("--include-data-dir=$SCRIPT_DIR/Web/static=static")
|
||||
fi
|
||||
|
||||
if [ -d "$SCRIPT_DIR/uploads" ]; then
|
||||
NUITKA_DATA_ARGS+=("--include-data-dir=$SCRIPT_DIR/uploads=uploads")
|
||||
fi
|
||||
|
||||
ORIGINAL_PERF_PARANOID=""
|
||||
if [ -r /proc/sys/kernel/perf_event_paranoid ]; then
|
||||
ORIGINAL_PERF_PARANOID="$(cat /proc/sys/kernel/perf_event_paranoid)"
|
||||
if [ "$ORIGINAL_PERF_PARANOID" -gt 1 ]; then
|
||||
echo "Temporarily setting kernel.perf_event_paranoid=1 for Nuitka build compatibility..."
|
||||
if ! $SUDO sh -c 'echo 1 > /proc/sys/kernel/perf_event_paranoid'; then
|
||||
echo "Warning: Could not adjust kernel.perf_event_paranoid automatically."
|
||||
echo "Run: sudo sh -c 'echo 1 > /proc/sys/kernel/perf_event_paranoid' and retry."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
restore_perf_setting() {
|
||||
if [ -n "$ORIGINAL_PERF_PARANOID" ] && [ "$ORIGINAL_PERF_PARANOID" -gt 1 ]; then
|
||||
$SUDO sh -c "echo $ORIGINAL_PERF_PARANOID > /proc/sys/kernel/perf_event_paranoid" >/dev/null 2>&1 || true
|
||||
fi
|
||||
}
|
||||
|
||||
trap restore_perf_setting EXIT
|
||||
|
||||
echo "Building standalone binary with Nuitka..."
|
||||
"$VENV_PYTHON" -m nuitka \
|
||||
--standalone \
|
||||
--assume-yes-for-downloads \
|
||||
--follow-imports \
|
||||
--output-dir="$DIST_DIR" \
|
||||
--output-filename="$OUTPUT_NAME" \
|
||||
"${NUITKA_DATA_ARGS[@]}" \
|
||||
--remove-output \
|
||||
"$SCRIPT_DIR/Web/app.py"
|
||||
|
||||
APP_DIST_DIR="$DIST_DIR/app.dist"
|
||||
if [ -d "$APP_DIST_DIR" ]; then
|
||||
echo "Nuitka build complete."
|
||||
echo "Run with: $APP_DIST_DIR/$OUTPUT_NAME"
|
||||
else
|
||||
echo "Build finished, but expected output directory not found: $APP_DIST_DIR"
|
||||
echo "Check Nuitka output above."
|
||||
exit 1
|
||||
fi
|
||||
@@ -196,3 +196,15 @@ networks:
|
||||
# - Medium (5-10 tenants): 4GB RAM, 2-4 CPU cores
|
||||
# - Large (10-20 tenants): 8GB RAM, 4-8 CPU cores
|
||||
# - Jumbo (20+ tenants): 16GB+ RAM, 8+ CPU cores with clustering
|
||||
|
||||
# Management Container for multi-tenant scripts
|
||||
tenant-manager:
|
||||
image: bash
|
||||
container_name: tenant-manager
|
||||
restart: "no"
|
||||
profiles:
|
||||
- tools
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
- .:/workspace
|
||||
entrypoint: ["bash", "-c", "cd /workspace && ./manage-tenant.sh \"$$@\"", "--"]
|
||||
|
||||
Executable
+142
@@ -0,0 +1,142 @@
|
||||
#!/bin/bash
|
||||
# Script to manage multitenant deployment
|
||||
# Allows adding, removing, and restarting tenants without downtime for others
|
||||
|
||||
if [ ! -f "docker-compose-multitenant.yml" ]; then
|
||||
echo "Error: docker-compose-multitenant.yml not found."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
show_help() {
|
||||
echo "Usage: ./manage-tenant.sh [COMMAND] [OPTIONS]"
|
||||
echo ""
|
||||
echo "Commands:"
|
||||
echo " add <tenant_id> Add a new tenant (initializes database)"
|
||||
echo " remove <tenant_id> Remove a tenant completely (deletes data!)"
|
||||
echo " restart-tenant <id> 'Restart' a single tenant (clears cache/sessions)"
|
||||
echo " restart-all Restart all application containers (zero-downtime reload)"
|
||||
echo " list List active tenants"
|
||||
echo ""
|
||||
echo "Examples:"
|
||||
echo " ./manage-tenant.sh add school_a"
|
||||
echo " ./manage-tenant.sh remove test_tenant"
|
||||
echo " ./manage-tenant.sh restart-all"
|
||||
exit 1
|
||||
}
|
||||
|
||||
if [ -z "$1" ]; then
|
||||
show_help
|
||||
fi
|
||||
|
||||
COMMAND=$1
|
||||
TENANT_ID=$2
|
||||
|
||||
case "$COMMAND" in
|
||||
add)
|
||||
if [ -z "$TENANT_ID" ]; then
|
||||
echo "Error: Please provide a tenant_id."
|
||||
exit 1
|
||||
fi
|
||||
echo "Adding new tenant '$TENANT_ID'..."
|
||||
# Add Nginx configuration
|
||||
if [ -f "docker/nginx/multitenant.conf" ]; then
|
||||
echo "Assuming dynamic routing based on subdomain ($TENANT_ID)..."
|
||||
fi
|
||||
|
||||
# Initialize tenant database via Python inside container
|
||||
echo "Initializing database for $TENANT_ID..."
|
||||
APP_CONTAINER=$(docker ps -qf "name=app" | head -n 1)
|
||||
if [ -n "$APP_CONTAINER" ]; then
|
||||
docker exec $APP_CONTAINER python3 -c "
|
||||
import sys; sys.path.insert(0, '/app/Web'); import settings; from pymongo import MongoClient
|
||||
client = MongoClient(settings.MONGODB_HOST, int(settings.MONGODB_PORT))
|
||||
db = client[f'{settings.MONGODB_DB}_{sys.argv[1]}']
|
||||
db.users.insert_one({'username': 'admin', 'password': 'hashed_password_here', 'role': 'admin'})
|
||||
print(f'Tenant {sys.argv[1]} database initialized.')
|
||||
" "$TENANT_ID"
|
||||
echo "Tenant '$TENANT_ID' successfully added. Ready to use."
|
||||
else
|
||||
echo "Warning: Application container is not running. Please start the multi-tenant system first."
|
||||
echo "Data will be initialized upon first access by the tenant."
|
||||
fi
|
||||
;;
|
||||
|
||||
remove)
|
||||
if [ -z "$TENANT_ID" ]; then
|
||||
echo "Error: Please provide a tenant_id to remove."
|
||||
exit 1
|
||||
fi
|
||||
echo -n "WARNING: Are you sure you want to permanently delete all data for tenant '$TENANT_ID'? (y/N) "
|
||||
read confirm
|
||||
if [ "$confirm" = "y" ] || [ "$confirm" = "Y" ]; then
|
||||
echo "Removing tenant '$TENANT_ID'..."
|
||||
APP_CONTAINER=$(docker ps -qf "name=app" | head -n 1)
|
||||
if [ -n "$APP_CONTAINER" ]; then
|
||||
docker exec $APP_CONTAINER python3 -c "
|
||||
import sys; sys.path.insert(0, '/app/Web'); import settings; from pymongo import MongoClient
|
||||
client = MongoClient(settings.MONGODB_HOST, int(settings.MONGODB_PORT))
|
||||
client.drop_database(f'{settings.MONGODB_DB}_{sys.argv[1]}')
|
||||
print(f'Database for tenant {sys.argv[1]} dropped.')
|
||||
" "$TENANT_ID"
|
||||
echo "Tenant '$TENANT_ID' removed successfully."
|
||||
else
|
||||
echo "Error: Application container not running."
|
||||
fi
|
||||
else
|
||||
echo "Removal canceled."
|
||||
fi
|
||||
;;
|
||||
|
||||
restart-tenant)
|
||||
if [ -z "$TENANT_ID" ]; then
|
||||
echo "Error: Please provide a tenant_id."
|
||||
exit 1
|
||||
fi
|
||||
echo "Restarting tenant '$TENANT_ID' (clearing session/cache)..."
|
||||
# To restart a single tenant without restarting the global python processes,
|
||||
# we can invalidate their cache or drop their sessions collection to sign everyone out
|
||||
APP_CONTAINER=$(docker ps -qf "name=app" | head -n 1)
|
||||
if [ -n "$APP_CONTAINER" ]; then
|
||||
docker exec $APP_CONTAINER python3 -c "
|
||||
import sys; sys.path.insert(0, '/app/Web'); import settings; from pymongo import MongoClient
|
||||
client = MongoClient(settings.MONGODB_HOST, int(settings.MONGODB_PORT))
|
||||
db = client[f'{settings.MONGODB_DB}_{sys.argv[1]}']
|
||||
db.sessions.drop() # Force sign-out / session clear
|
||||
print(f'Tenant {sys.argv[1]} session cache cleared. Tenant restarted.')
|
||||
" "$TENANT_ID"
|
||||
echo "Tenant '$TENANT_ID' has been refreshed without impacting others."
|
||||
else
|
||||
echo "Error: Application container not running."
|
||||
fi
|
||||
;;
|
||||
|
||||
restart-all)
|
||||
echo "Restarting all application instances with zero-downtime rolling restart..."
|
||||
docker restart $(docker ps -qf "name=app")
|
||||
echo "Global restart complete."
|
||||
;;
|
||||
|
||||
list)
|
||||
echo "Listing active tenants (Databases):"
|
||||
APP_CONTAINER=$(docker ps -qf "name=app" | head -n 1)
|
||||
if [ -n "$APP_CONTAINER" ]; then
|
||||
docker exec $APP_CONTAINER python3 -c "
|
||||
import sys; sys.path.insert(0, '/app/Web'); import settings; from pymongo import MongoClient
|
||||
client = MongoClient(settings.MONGODB_HOST, int(settings.MONGODB_PORT))
|
||||
prefix = f'{settings.MONGODB_DB}_'
|
||||
dbs = [d for d in client.list_database_names() if d.startswith(prefix)]
|
||||
for db in dbs:
|
||||
print(f'- {db.replace(prefix, \"\")}')
|
||||
"
|
||||
else
|
||||
echo "Error: Application container not running."
|
||||
fi
|
||||
;;
|
||||
|
||||
*)
|
||||
echo "Unknown command: $COMMAND"
|
||||
show_help
|
||||
;;
|
||||
esac
|
||||
|
||||
exit 0
|
||||
Executable
+3
@@ -0,0 +1,3 @@
|
||||
#!/bin/bash
|
||||
# Wrapper to run tenant management fully containerized via docker-compose
|
||||
docker-compose -f docker-compose-multitenant.yml --profile tools run --rm tenant-manager "$@"
|
||||
@@ -325,12 +325,35 @@ download_and_extract_bundle() {
|
||||
cp -f "$tmp_dir/update.sh" "$PROJECT_DIR/update.sh"
|
||||
fi
|
||||
|
||||
# Neu: Multi-Tenant Ressourcen kopieren, falls vorhanden
|
||||
if [ -f "$tmp_dir/docker-compose-multitenant.yml" ]; then
|
||||
cp -f "$tmp_dir/docker-compose-multitenant.yml" "$PROJECT_DIR/docker-compose-multitenant.yml"
|
||||
fi
|
||||
if [ -f "$tmp_dir/docker/nginx/multitenant.conf" ]; then
|
||||
cp -f "$tmp_dir/docker/nginx/multitenant.conf" "$PROJECT_DIR/docker/nginx/multitenant.conf"
|
||||
fi
|
||||
if [ -f "$tmp_dir/manage-tenant.sh" ]; then
|
||||
cp -f "$tmp_dir/manage-tenant.sh" "$PROJECT_DIR/manage-tenant.sh"
|
||||
fi
|
||||
if [ -f "$tmp_dir/run-tenant-cmd.sh" ]; then
|
||||
cp -f "$tmp_dir/run-tenant-cmd.sh" "$PROJECT_DIR/run-tenant-cmd.sh"
|
||||
fi
|
||||
for file in "MULTITENANT_DEPLOYMENT.md" "MULTITENANT_PYTHON_API.md"; do
|
||||
if [ -f "$tmp_dir/$file" ]; then
|
||||
cp -f "$tmp_dir/$file" "$PROJECT_DIR/$file"
|
||||
fi
|
||||
done
|
||||
|
||||
# Ensure executable permissions on all copied scripts
|
||||
chmod +x "$PROJECT_DIR/start.sh" "$PROJECT_DIR/stop.sh" "$PROJECT_DIR/restart.sh" "$PROJECT_DIR/update.sh" "$PROJECT_DIR/backup.sh" "$PROJECT_DIR/init-admin.sh" "$PROJECT_DIR/manage-tenant.sh" "$PROJECT_DIR/run-tenant-cmd.sh" 2>/dev/null || true 2>/dev/null || true
|
||||
chmod +x "$PROJECT_DIR"/manage-tenant.sh "$PROJECT_DIR"/run-tenant-cmd.sh 2>/dev/null || true
|
||||
|
||||
if [ ! -f "$PROJECT_DIR/config.json" ] && [ -f "$tmp_dir/config.json" ]; then
|
||||
cp -f "$tmp_dir/config.json" "$PROJECT_DIR/config.json"
|
||||
log_message "Installed default config.json from release bundle"
|
||||
fi
|
||||
|
||||
chmod +x "$PROJECT_DIR/start.sh" "$PROJECT_DIR/stop.sh" "$PROJECT_DIR/restart.sh" "$PROJECT_DIR/update.sh" "$PROJECT_DIR/backup.sh"
|
||||
chmod +x "$PROJECT_DIR/start.sh" "$PROJECT_DIR/stop.sh" "$PROJECT_DIR/restart.sh" "$PROJECT_DIR/update.sh" "$PROJECT_DIR/backup.sh" "$PROJECT_DIR/init-admin.sh" "$PROJECT_DIR/manage-tenant.sh" "$PROJECT_DIR/run-tenant-cmd.sh" 2>/dev/null || true
|
||||
}
|
||||
|
||||
deploy() {
|
||||
|
||||
Reference in New Issue
Block a user