Compare commits
7 Commits
v1.1.1-dev.6
...
v1.1.2
| Author | SHA1 | Date | |
|---|---|---|---|
| b3e79a65c1 | |||
| b33b4eadc6 | |||
| 07a1cdb063 | |||
| 2942bcc380 | |||
| 349337e75f | |||
| 9bdd8261fe | |||
| 04c8505531 |
+38
-17
@@ -468,6 +468,10 @@ def _enforce_active_session_user():
|
||||
return None
|
||||
|
||||
username = session.get('username')
|
||||
current_user_id = session.get('user_id')
|
||||
if not current_user_id:
|
||||
current_user = us.get_user(username)
|
||||
current_user_id = str(current_user['_id']) if current_user and current_user.get('_id') else None
|
||||
if not username:
|
||||
return None
|
||||
|
||||
@@ -4228,9 +4232,12 @@ def api_library_scan_action():
|
||||
due_date = now + datetime.timedelta(days=borrow_duration_days)
|
||||
|
||||
it.update_item_status(item_id, False, borrower_name)
|
||||
borrower_user = us.get_user_by_student_card(student_card_id)
|
||||
if not borrower_user or not borrower_user.get('_id'):
|
||||
return jsonify({'ok': False, 'message': 'Kein Benutzer für diesen Schülerausweis gefunden.'}), 404
|
||||
au.add_ausleihung(
|
||||
item_id,
|
||||
borrower_name,
|
||||
str(borrower_user['_id']),
|
||||
now,
|
||||
end_date=due_date,
|
||||
due_date=due_date,
|
||||
@@ -5467,6 +5474,7 @@ def login():
|
||||
if user:
|
||||
app.logger.info(f"Login success: username={encrypt_text(username)!r} tenant={current_tenant_id or 'default'} db={current_tenant_db} host={request.host} ip={encrypt_text(request.remote_addr)}")
|
||||
session['username'] = username
|
||||
session['user_id'] = str(user.get('_id'))
|
||||
is_admin_user = bool(user.get('Admin', False))
|
||||
session['admin'] = is_admin_user
|
||||
session['is_admin'] = is_admin_user
|
||||
@@ -5867,10 +5875,7 @@ def get_bookings():
|
||||
result = []
|
||||
for booking in bookings:
|
||||
raw_booking_user = booking.get('User') or ''
|
||||
try:
|
||||
booking_user = decrypt_text(raw_booking_user) if raw_booking_user else ''
|
||||
except Exception:
|
||||
booking_user = str(raw_booking_user)
|
||||
booking_user = str(raw_booking_user)
|
||||
start_dt = booking.get('Start')
|
||||
if not start_dt:
|
||||
continue
|
||||
@@ -5916,7 +5921,7 @@ def get_bookings():
|
||||
'userName': str(booking_user),
|
||||
'notes': str(booking.get('Notes') or ''),
|
||||
'period': period,
|
||||
'isCurrentUser': str(booking_user) == username,
|
||||
'isCurrentUser': str(booking_user) == str(current_user_id),
|
||||
'itemBorrower': item_borrower,
|
||||
})
|
||||
|
||||
@@ -6036,7 +6041,10 @@ def api_booking_conflicts():
|
||||
current_permissions = us.get_effective_permissions(session['username'])
|
||||
|
||||
if not current_permissions['actions'].get('can_view_logs', False):
|
||||
query['User'] = session['username']
|
||||
current_user = us.get_user(session['username'])
|
||||
if not current_user or not current_user.get('_id'):
|
||||
return jsonify({'error': 'User account could not be resolved'}), 401
|
||||
query['User'] = str(current_user['_id'])
|
||||
|
||||
conflicts = list(ausleihungen.find(query))
|
||||
result = []
|
||||
@@ -7377,6 +7385,11 @@ def ausleihen(id):
|
||||
return redirect(url_for('login'))
|
||||
|
||||
username = session['username']
|
||||
current_user = us.get_user(username)
|
||||
if not current_user or not current_user.get('_id'):
|
||||
flash('Benutzerkonto konnte nicht eindeutig ermittelt werden.', 'error')
|
||||
return redirect(url_for('login'))
|
||||
effective_borrower_id = str(current_user['_id'])
|
||||
requested_return_to = (request.form.get('return_to') or '').strip().lower()
|
||||
if requested_return_to == 'library' and cfg.MODULES.is_enabled('library'):
|
||||
redirect_target = 'library_view'
|
||||
@@ -7450,6 +7463,7 @@ def ausleihen(id):
|
||||
flash('Keine Schülerin/kein Schüler mit dieser Ausweis-ID gefunden.', 'error')
|
||||
return redirect(url_for('library_view'))
|
||||
effective_borrower = student_user.get('Username') or student_user.get('username') or username
|
||||
effective_borrower_id = str(student_user['_id'])
|
||||
if borrow_duration_days is None:
|
||||
try:
|
||||
card_default = int(student_user.get('MaxBorrowDays', cfg.STUDENT_DEFAULT_BORROW_DAYS))
|
||||
@@ -7524,7 +7538,7 @@ def ausleihen(id):
|
||||
it.update_item_status(unit_id, False, effective_borrower)
|
||||
au.add_ausleihung(
|
||||
unit_id,
|
||||
effective_borrower,
|
||||
effective_borrower_id,
|
||||
start_date,
|
||||
end_date=end_date,
|
||||
due_date=end_date if is_library_item else None,
|
||||
@@ -7621,7 +7635,7 @@ def ausleihen(id):
|
||||
start_date = datetime.datetime.now(ZoneInfo("Europe/Berlin"))
|
||||
au.add_ausleihung(
|
||||
id,
|
||||
effective_borrower,
|
||||
effective_borrower_id,
|
||||
start_date,
|
||||
end_date=end_date,
|
||||
due_date=end_date if is_library_item else None,
|
||||
@@ -7672,7 +7686,7 @@ def ausleihen(id):
|
||||
exemplar_id = f"{id}_{exemplar['number']}"
|
||||
au.add_ausleihung(
|
||||
exemplar_id,
|
||||
effective_borrower,
|
||||
effective_borrower_id,
|
||||
start_date,
|
||||
end_date=end_date,
|
||||
due_date=end_date if is_library_item else None,
|
||||
@@ -7833,7 +7847,12 @@ def get_ausleihung_by_item_route(id):
|
||||
|
||||
current_permissions = us.get_effective_permissions(session['username'])
|
||||
|
||||
if ausleihung and (current_permissions['actions'].get('can_manage_users')) or ausleihung.get('User') == session['username']:
|
||||
current_user = us.get_user(session['username'])
|
||||
current_user_id = str(current_user['_id']) if current_user and current_user.get('_id') else ''
|
||||
if ausleihung and (
|
||||
current_permissions['actions'].get('can_manage_users')
|
||||
or ausleihung.get('User') == current_user_id
|
||||
):
|
||||
return {'ausleihung': ausleihung, 'status': 'success'}
|
||||
|
||||
item = it.get_item(id)
|
||||
@@ -8115,7 +8134,7 @@ def plan_booking():
|
||||
try:
|
||||
for _, period, period_times in requested_slots:
|
||||
booking_id = au.add_planned_booking(
|
||||
item_id, session['username'], period_times['start'],
|
||||
item_id, session.get('user_id') or str(us.get_user(session['username'])['_id']), period_times['start'],
|
||||
period_times['end'], notes, period=period
|
||||
)
|
||||
if not booking_id:
|
||||
@@ -8168,7 +8187,7 @@ def process_day_bookings(item_id, booking_date, periods, notes):
|
||||
# Create the booking
|
||||
booking_id = au.add_planned_booking(
|
||||
item_id,
|
||||
session['username'],
|
||||
session.get('user_id') or str(us.get_user(session['username'])['_id']),
|
||||
start_time,
|
||||
end_time,
|
||||
notes,
|
||||
@@ -8235,7 +8254,7 @@ def add_booking():
|
||||
# Continue with adding the booking
|
||||
booking_id = au.add_planned_booking(
|
||||
item_id=item_id,
|
||||
user=session['username'],
|
||||
user_id=session.get('user_id') or str(us.get_user(session['username'])['_id']),
|
||||
start_date=start_date,
|
||||
end_date=end_date,
|
||||
notes=notes,
|
||||
@@ -8266,8 +8285,10 @@ def cancel_booking(id):
|
||||
current_permissions = us.get_effective_permissions(session['username'])
|
||||
|
||||
# Check if user owns this booking
|
||||
booking_user = au.dp.decrypt_text(booking.get('User')) if booking.get('User') else ''
|
||||
if booking_user != session['username'] and not current_permissions['actions'].get('can_manage_users', False):
|
||||
current_user = us.get_user(session['username'])
|
||||
current_user_id = str(current_user['_id']) if current_user and current_user.get('_id') else ''
|
||||
booking_user_id = str(booking.get('User') or '')
|
||||
if booking_user_id != current_user_id and not current_permissions['actions'].get('can_manage_users', False):
|
||||
return {"success": False, "error": "Not authorized to cancel this booking"}, 403
|
||||
|
||||
# Cancel the booking
|
||||
@@ -12199,7 +12220,7 @@ def schedule_appointment():
|
||||
# Use add_ausleihung directly to set the correct initial status
|
||||
appointment_id = au.add_ausleihung(
|
||||
item_id=item_id,
|
||||
user=session['username'],
|
||||
user_id=session.get('user_id') or str(us.get_user(session['username'])['_id']),
|
||||
start_date=start_datetime,
|
||||
end_date=end_datetime,
|
||||
notes=notes,
|
||||
|
||||
@@ -185,13 +185,13 @@ def create_backup_database():
|
||||
|
||||
# === AUSLEIHUNG MANAGEMENT ===
|
||||
|
||||
def add_ausleihung(item_id, user, start_date, end_date=None, notes="", status="active", period=None, exemplar_data=None, due_date=None):
|
||||
def add_ausleihung(item_id, user_id, start_date, end_date=None, notes="", status="active", period=None, exemplar_data=None, due_date=None):
|
||||
"""
|
||||
Add a new borrowing record for an item.
|
||||
|
||||
Args:
|
||||
item_id (str): ID of the item borrowed
|
||||
user (str): Username of the borrower
|
||||
user_id (str): ID of the borrower
|
||||
start_date (datetime): Start date and time of the borrowing
|
||||
end_date (datetime, optional): End date and time if already returned
|
||||
notes (str, optional): Additional notes about the borrowing
|
||||
@@ -209,7 +209,7 @@ def add_ausleihung(item_id, user, start_date, end_date=None, notes="", status="a
|
||||
|
||||
ausleihung = {
|
||||
'Item': item_id,
|
||||
'User': dp.encrypt_text(user),
|
||||
'User': str(user_id),
|
||||
'Start': start_date,
|
||||
'Status': status
|
||||
}
|
||||
@@ -256,8 +256,7 @@ def update_ausleihung(id, item_id=None, user_id=None, start=None, end=None, note
|
||||
if item_id is not None:
|
||||
update_data['Item'] = item_id
|
||||
if user_id is not None:
|
||||
# WICHTIG: Gleiche Verschlüsselung wie in add_ausleihung nutzen!
|
||||
update_data['User'] = dp.encrypt_text(user_id) if hasattr(dp, 'encrypt_text') else user_id
|
||||
update_data['User'] = str(user_id)
|
||||
if start is not None:
|
||||
update_data['Start'] = ensure_timezone_aware(start)
|
||||
if end is not None:
|
||||
@@ -556,9 +555,7 @@ def get_ausleihung_by_user(user_id, status=None, use_client_side_verification=Tr
|
||||
db = client[cfg.MONGODB_DB]
|
||||
ausleihungen = db['ausleihungen']
|
||||
|
||||
# Ensure proper encrypted query string if user IDs are encrypted in DB
|
||||
search_user = dp.encrypt_text(user_id) if hasattr(dp, 'encrypt_text') else user_id
|
||||
query = {'User': search_user, 'Status': {'$ne': 'deleted'}}
|
||||
query = {'User': str(user_id), 'Status': {'$ne': 'deleted'}}
|
||||
|
||||
if not use_client_side_verification:
|
||||
if status is not None:
|
||||
@@ -864,8 +861,8 @@ def reset_item_completely(item_id):
|
||||
|
||||
# === KOMPATIBILITÄTSFUNKTIONEN ===
|
||||
|
||||
def add_planned_booking(item_id, user, start_date, end_date, notes="", period=None):
|
||||
return add_ausleihung(item_id, user, start_date, end_date, notes, status='planned', period=period)
|
||||
def add_planned_booking(item_id, user_id, start_date, end_date, notes="", period=None):
|
||||
return add_ausleihung(item_id, user_id, start_date, end_date, notes, status='planned', period=period)
|
||||
|
||||
|
||||
def check_booking_conflict(item_id, start_date, end_date, period=None):
|
||||
|
||||
@@ -921,6 +921,42 @@ html, body {
|
||||
}
|
||||
}
|
||||
|
||||
/* iPad layout: keep content airy while using the available tablet width. */
|
||||
@media screen and (min-width: 768px) and (max-width: 1180px) {
|
||||
.container {
|
||||
width: min(100% - 32px, 1120px);
|
||||
margin: 16px auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.form-control,
|
||||
.form-select,
|
||||
textarea,
|
||||
input,
|
||||
select {
|
||||
min-height: 44px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.btn,
|
||||
button,
|
||||
input[type="submit"],
|
||||
input[type="button"] {
|
||||
min-height: 44px;
|
||||
touch-action: manipulation;
|
||||
}
|
||||
|
||||
.table-responsive {
|
||||
border: 1px solid var(--ui-border);
|
||||
border-radius: var(--ui-radius);
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
.table-responsive > .table {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* ========================================================= */
|
||||
/* UNIVERSAL DARK MODE OVERRIDES FOR HARDCODED HTML STYLES */
|
||||
/* ========================================================= */
|
||||
|
||||
@@ -1301,8 +1301,6 @@
|
||||
<button class="function-search-btn" type="submit">Los</button>
|
||||
</form>
|
||||
</div>
|
||||
{% if current_tenant_db %}
|
||||
<span class="navbar-text tenant-badge" title="Aktive Tenant-Datenbank">{{ current_tenant_db }}</span>
|
||||
{% endif %}
|
||||
<span class="navbar-text text-light me-3">{{ session['username'] }}</span>
|
||||
<div class="dropdown me-2 user-menu-wrap">
|
||||
@@ -1332,7 +1330,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
{% endif %}
|
||||
|
||||
{% if library_module_enabled and CURRENT_MODULE == 'library' %}
|
||||
<nav class="navbar navbar-expand-lg navbar-dark" id="libraryNavbar">
|
||||
|
||||
+118
-11
@@ -1971,6 +1971,122 @@
|
||||
box-sizing: border-box;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* iPad: use the wider canvas for cards, filters, forms, and image previews. */
|
||||
@media screen and (min-width: 768px) and (max-width: 1180px) {
|
||||
.admin-content-container {
|
||||
width: min(100%, 1120px) !important;
|
||||
max-width: 1120px !important;
|
||||
margin: 0 auto !important;
|
||||
padding: 20px 24px 28px !important;
|
||||
box-sizing: border-box !important;
|
||||
}
|
||||
|
||||
.filter-container {
|
||||
display: grid !important;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr)) !important;
|
||||
align-items: start;
|
||||
gap: 14px !important;
|
||||
}
|
||||
|
||||
.filter-group {
|
||||
min-width: 0 !important;
|
||||
width: auto !important;
|
||||
padding: 14px !important;
|
||||
}
|
||||
|
||||
.filter-group.search-field {
|
||||
grid-column: span 2;
|
||||
}
|
||||
|
||||
.search-container {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.search-container input,
|
||||
.form-group input,
|
||||
.form-group select,
|
||||
.form-group textarea {
|
||||
min-height: 44px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.items-container {
|
||||
display: grid !important;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr)) !important;
|
||||
gap: 18px !important;
|
||||
padding: 14px 0 24px !important;
|
||||
overflow: visible !important;
|
||||
scroll-snap-type: none !important;
|
||||
}
|
||||
|
||||
.item-card {
|
||||
width: auto !important;
|
||||
max-width: none !important;
|
||||
min-width: 0 !important;
|
||||
padding: 16px !important;
|
||||
scroll-snap-align: none !important;
|
||||
}
|
||||
|
||||
.item-card .image-container {
|
||||
width: 100% !important;
|
||||
max-width: none !important;
|
||||
height: auto !important;
|
||||
aspect-ratio: 4 / 3;
|
||||
margin: 10px 0 14px !important;
|
||||
overflow: hidden !important;
|
||||
border: 1px solid var(--ui-border);
|
||||
border-radius: 12px !important;
|
||||
background: var(--ui-surface-soft);
|
||||
}
|
||||
|
||||
.item-card .image-container .item-image,
|
||||
.item-card .image-container .video-container {
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
}
|
||||
|
||||
.item-card .image-container .item-image {
|
||||
object-fit: contain !important;
|
||||
object-position: center;
|
||||
background: var(--ui-surface-soft);
|
||||
}
|
||||
|
||||
.item-card .image-container .item-image[data-index="0"] {
|
||||
display: block !important;
|
||||
position: relative !important;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
.item-card .image-container .item-image:not([data-index="0"]) {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.item-card .image-container .video-container {
|
||||
position: relative;
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.form-actions > * {
|
||||
min-height: 44px;
|
||||
}
|
||||
|
||||
#item-modal .modal-content {
|
||||
width: min(92vw, 880px);
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
<style>
|
||||
.bookmark-btn { position:absolute; top:8px; right:8px; width:46px; height:46px; background:rgba(255,255,255,.92); border:2px solid #ffc107; border-radius:50%; font-size:1.65rem; line-height:1; display:flex; align-items:center; justify-content:center; cursor:pointer; color:#ffc107; box-shadow:0 2px 6px rgba(0,0,0,.18); transition:background .2s, transform .15s, color .2s; z-index:6; pointer-events:auto; }
|
||||
@@ -3903,17 +4019,8 @@ document.addEventListener('DOMContentLoaded', ()=>{
|
||||
return;
|
||||
}
|
||||
input?.classList.add('searching');
|
||||
fetch(`/search_word/${encodeURIComponent(term)}`)
|
||||
.then(r=>r.json())
|
||||
.then(data=>{
|
||||
if (data && data.success && Array.isArray(data.response)){
|
||||
descSearchIds = new Set(data.response.map(String));
|
||||
} else {
|
||||
descSearchIds = new Set();
|
||||
}
|
||||
return loadItems(0, false, term, 'description');
|
||||
})
|
||||
.catch(()=>{ descSearchIds = new Set(); return loadItems(0, false, term, 'description'); })
|
||||
descSearchIds = null;
|
||||
loadItems(0, false, term, 'title_description')
|
||||
.finally(()=>{ setTimeout(()=> input?.classList.remove('searching'), 300); });
|
||||
}
|
||||
|
||||
|
||||
@@ -21,9 +21,8 @@
|
||||
<div class="card shadow-sm border-0 h-100 rounded-3">
|
||||
<div class="card-header bg-white border-bottom py-3 d-flex justify-content-between align-items-center">
|
||||
<h2 class="card-title h5 mb-0 fw-semibold text-primary">
|
||||
<i class="bi bi-funnel me-2"></i>{{ filter_names.get('1', 'Jahrgangsstufe') }}
|
||||
<i class="bi bi-funnel me-2"></i>{{ filter_names.get('2', 'Jahrgangsstufe') }}
|
||||
</h2>
|
||||
<span class="badge bg-primary-subtle text-primary rounded-pill">Filter 1</span>
|
||||
</div>
|
||||
<div class="card-body p-4">
|
||||
<form method="POST" action="{{ url_for('add_filter_value', filter_num=1) }}" class="mb-4">
|
||||
@@ -87,9 +86,8 @@
|
||||
<div class="card shadow-sm border-0 h-100 rounded-3">
|
||||
<div class="card-header bg-white border-bottom py-3 d-flex justify-content-between align-items-center">
|
||||
<h2 class="card-title h5 mb-0 fw-semibold text-primary">
|
||||
<i class="bi bi-funnel me-2"></i>{{ filter_names.get('2', 'Fachgebiet') }}
|
||||
<i class="bi bi-funnel me-2"></i>{{ filter_names.get('1', 'Fachgebiet') }}
|
||||
</h2>
|
||||
<span class="badge bg-primary-subtle text-primary rounded-pill">Filter 2</span>
|
||||
</div>
|
||||
<div class="card-body p-4">
|
||||
<form method="POST" action="{{ url_for('add_filter_value', filter_num=2) }}" class="mb-4">
|
||||
|
||||
Reference in New Issue
Block a user