Compare commits

...

11 Commits

Author SHA1 Message Date
Aiirondev_dev b3e79a65c1 Umgesetzt:
Ausleihungen verwenden jetzt user_id statt Benutzername als Funktionsargument.
Das Feld User speichert nun die Benutzer-ID statt des Benutzernamens.
Login speichert zusätzlich session['user_id'].
Normale Ausleihen, Schülerausweis-Ausleihen und Reservierungen verwenden die Benutzer-ID.
Berechtigungsprüfungen für eigene Ausleihen/Buchungen vergleichen IDs.
Kalenderbuchungen erkennen den aktuellen Benutzer jetzt ebenfalls über die ID.
update_ausleihung() und get_ausleihung_by_user() wurden angepasst.
2026-10-15 20:58:40 +02:00
Aiirondev_dev b33b4eadc6 Implemented an iPad-focused responsive styling improvement.
Changes made
Added a dedicated iPad breakpoint for 768–1180px widths.
Improved inventory card layout:
Two-column card grid on iPads.
Cleaner spacing and card sizing.
Image previews now preserve aspect ratio with object-fit: contain.
Added consistent image backgrounds and rounded borders.
Fixed first-image visibility inside video/image containers.
Improved forms:
Two-column form layout on iPad.
Larger 44px touch-friendly controls.
Better spacing for filter groups and search fields.
Improved tables:
Rounded scroll container.
Better horizontal touch scrolling.
Preserved safe-area and touch interaction behavior.
2026-10-15 20:52:14 +02:00
Aiirondev_dev 07a1cdb063 changes to the filter preset 2026-10-07 16:30:13 +02:00
Aiirondev_dev 2942bcc380 Der Jinja-Fehler in base.html ist behoben. 2026-10-07 16:12:31 +02:00
Aiirondev_dev 349337e75f changes to dont reflekt the testing database name 2026-10-07 16:03:32 +02:00
Aiirondev_dev 9bdd8261fe changes to dont include the testing information 2026-10-07 16:02:39 +02:00
Aiirondev_dev 04c8505531 Die Suche wurde angepasst:Titel und Beschreibung werden jetzt direkt gemeinsam durchsucht.
Teiltreffer funktionieren, z. B.:
Rechen findet Rechenschiffchen
ein Fragment aus der Beschreibung findet den Artikel ebenfalls
Groß-/Kleinschreibung spielt keine Rolle
Die zusätzliche lokale ID-Filterung wurde entfernt, da sie Treffer teilweise wieder ausgeblendet hat.
Die Suche verwendet nun die serverseitige MongoDB-Abfrage mit regulären Teiltreffern.
2026-10-07 15:55:07 +02:00
Aiirondev_dev c9edf4a4c8 Die Inventarsuche wurde korrigiert. 2026-10-07 15:45:47 +02:00
Aiirondev_dev 0e0a99569e Implementation of the right Code ussage 2026-10-02 15:13:46 +02:00
Aiirondev_dev 6e94ca20c4 changes to the Mahnungssystem level fix 2026-09-27 14:40:21 +02:00
Aiirondev_dev 72fdfb0898 changes to the style of the mahnungs zurücksetzungs Modal 2026-09-27 14:35:01 +02:00
7 changed files with 331 additions and 59 deletions
+98 -21
View File
@@ -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
@@ -1560,7 +1564,7 @@ def update_appointment_statuses():
pass
# STUFE 2: >= 2 * student_ausleih_dauer Tage überfällig -> Ausweis sperren, Stufe 2 setzen & Admins benachrichtigen
if days_overdue >= (int(student_ausleih_dauer) * 2) and mahnstufe < 2:
if days_overdue >= (int(student_ausleih_dauer) * 2) and mahnstufe == 1 and card.get('is_blocked') is not True:
ausleihungen.update_one(
{'_id': appt['_id']},
{'$set': {'Mahnstufe': 2, 'LastUpdated': current_time}}
@@ -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
@@ -5597,6 +5605,8 @@ def get_items():
favorites = set(session.get('favorites', []))
available_only = str(request.args.get('available_only', '')).strip().lower() in ('1', 'true', 'yes', 'on')
search_term = str(request.args.get('search', '')).strip()
search_scope = str(request.args.get('search_scope', 'all')).strip().lower()
offset_raw = request.args.get('offset')
limit_raw = request.args.get('limit')
light_mode_param = str(request.args.get('light_mode', '')).strip().lower()
@@ -5632,6 +5642,42 @@ def get_items():
'Deleted': {'$ne': True},
}
if search_term:
search_pattern = re.escape(search_term[:200])
if search_scope in ('description', 'title_description'):
search_fields = ['Name', 'Beschreibung']
else:
search_fields = [
'Name', 'Beschreibung', 'Code_4', 'ISBN',
'Author', 'Autor',
]
direct_matches = [{field: {'$regex': search_pattern, '$options': 'i'}}
for field in search_fields]
if search_scope not in ('description', 'title_description') and search_term.isdigit():
direct_matches.append({'Code_4': int(search_term)})
grouped_parent_ids = [
child.get('ParentItemId')
for child in items_col.find(
{
'IsGroupedSubItem': True,
'Deleted': {'$ne': True},
'$or': direct_matches,
},
{'ParentItemId': 1},
)
if child.get('ParentItemId')
]
if grouped_parent_ids:
direct_matches.append({'_id': {'$in': grouped_parent_ids}})
base_query = {
'$and': [
base_query,
{'$or': direct_matches},
]
}
total_count = items_col.count_documents(base_query)
light_projection = {
@@ -5698,6 +5744,7 @@ def get_items():
'ParentItemId': 1,
'Code_4': 1,
'Verfuegbar': 1,
'User': 1,
'Name': 1,
}
# Full mode: complete child data with all details
@@ -5706,6 +5753,7 @@ def get_items():
'ParentItemId': 1,
'Code_4': 1,
'Verfuegbar': 1,
'User': 1,
'Name': 1,
'Images': 1,
'ThumbnailInfo': 1,
@@ -5733,13 +5781,26 @@ def get_items():
grouped_units = [itm] + grouped_children
available_units = []
grouped_all_codes = []
grouped_unit_states = []
for unit in grouped_units:
unit_id = str(unit['_id']) if not isinstance(unit['_id'], str) else unit['_id']
unit_code = unit.get('Code_4')
if unit_code is not None and str(unit_code).strip() != '':
grouped_all_codes.append(str(unit_code).strip())
normalized_code = str(unit_code).strip()
grouped_all_codes.append(normalized_code)
else:
normalized_code = ''
grouped_unit_states.append({
'id': unit_id,
'code': normalized_code,
'available': bool(unit.get('Verfuegbar', True)),
'borrowed_by_current_user': bool(
username and str(unit.get('User') or '') == str(username)
),
})
if unit.get('Verfuegbar', True):
unit_id = str(unit['_id']) if not isinstance(unit['_id'], str) else unit['_id']
code = unit.get('Code_4') or '-'
available_units.append({
'id': unit_id,
@@ -5752,6 +5813,7 @@ def get_items():
itm['AvailableGroupedCount'] = len(available_units)
itm['GroupedAvailableUnits'] = available_units
itm['GroupedAllCodes'] = grouped_all_codes
itm['GroupedUnitStates'] = grouped_unit_states
if grouped_count > 1:
itm['Verfuegbar'] = len(available_units) > 0
if available_only and not itm.get('Verfuegbar', False):
@@ -5813,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
@@ -5862,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,
})
@@ -5982,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 = []
@@ -7323,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'
@@ -7396,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))
@@ -7470,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,
@@ -7567,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,
@@ -7618,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,
@@ -7779,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)
@@ -8061,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:
@@ -8114,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,
@@ -8181,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,
@@ -8212,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
@@ -10775,7 +10850,9 @@ def search_word(word):
if (term_lower in text.lower()) or (term_lower in title_text.lower()):
_id = i.get("_id")
if _id is not None:
id_set.add(str(_id))
item_id = str(_id)
parent_id = i.get('ParentItemId')
id_set.add(str(parent_id or item_id))
return jsonify({"success": True, "response": list(id_set)})
except Exception as e:
@@ -12143,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,
+7 -10
View File
@@ -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):
+36
View File
@@ -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 */
/* ========================================================= */
-3
View File
@@ -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">
+2 -2
View File
@@ -214,7 +214,7 @@
<!-- E-Mail Modal -->
<div class="modal fade" id="emailModal" tabindex="-1" aria-labelledby="emailModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="emailModalLabel">Mahnungs-E-Mail senden</h5>
@@ -247,7 +247,7 @@
<!-- Custom Reset Days Modal -->
<div class="modal fade" id="customResetModal" tabindex="-1" aria-labelledby="customResetModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="customResetModalLabel">Mahnung zurücksetzen</h5>
+186 -19
View File
@@ -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; }
@@ -3206,9 +3322,12 @@ document.addEventListener('DOMContentLoaded', ()=>{
});
}
function loadItems(offset = 0, append = false) {
function loadItems(offset = 0, append = false, searchTerm = '', searchScope = 'all') {
// Keep list payload lightweight; full details are fetched on-demand in openItemQuick.
return fetch(`{{ url_for('get_items') }}?offset=${offset}&limit=${MAIN_ADMIN_ITEMS_PAGE_SIZE}&light_mode=true`)
const searchParams = searchTerm
? `&search=${encodeURIComponent(searchTerm)}&search_scope=${encodeURIComponent(searchScope)}`
: '';
return fetch(`{{ url_for('get_items') }}?offset=${offset}&limit=${MAIN_ADMIN_ITEMS_PAGE_SIZE}&light_mode=true${searchParams}`)
.then(response => response.json())
.then(data => {
const itemsContainer = document.querySelector('#items-container');
@@ -3241,7 +3360,7 @@ document.addEventListener('DOMContentLoaded', ()=>{
}
pageItems.sort((a, b) => {
const nameA = a.Name ? a.Name.toLowerCase() : '';
const nameB = a.Name ? a.Name.toLowerCase() : '';
const nameB = b.Name ? b.Name.toLowerCase() : '';
return nameA.localeCompare(nameB);
});
@@ -3285,6 +3404,7 @@ document.addEventListener('DOMContentLoaded', ()=>{
card.dataset.code = (item.Code_4 !== undefined && item.Code_4 !== null) ?
String(item.Code_4).trim() : '';
card.dataset.groupCodes = JSON.stringify(Array.isArray(item.GroupedAllCodes) ? item.GroupedAllCodes : []);
card.dataset.groupedUnits = JSON.stringify(Array.isArray(item.GroupedUnitStates) ? item.GroupedUnitStates : []);
// Add filter values to their respective sets
filter1Array.forEach(value => {
@@ -3415,7 +3535,7 @@ document.addEventListener('DOMContentLoaded', ()=>{
</div>
{% if current_permissions.actions.get('can_borrow', False) %}
${isAvailableForBorrow && !item.BlockedNow ?
`<form method="POST" action="{{ url_for('ausleihen', id='') }}${item._id}">
`<form method="POST" action="{{ url_for('ausleihen', id='') }}${item._id}" data-grouped-borrow-form>
${isGroupedItem ? `
<div class="grouped-borrow-controls" style="display:flex; gap:8px; flex-wrap:wrap; margin-bottom:8px; align-items:center;">
<label style="font-size:0.85rem; margin:0;">Anzahl:</label>
@@ -3445,6 +3565,7 @@ document.addEventListener('DOMContentLoaded', ()=>{
:
`<button class="ausleihen disabled-button" disabled>${item.BlockedNow ? 'Reserviert' : 'Ausgeliehen'}</button>`
}
${isGroupedItem ? '<div class="grouped-searched-action" data-grouped-searched-action style="display:none;"></div>' : ''}
{% endif %}
{% if current_permissions.actions.get('can_delete', False) %}
<form method="POST" action="{{ url_for('delete_item', id='') }}${item._id}" style="display:inline;" onsubmit="return confirm('Sind Sie sicher, dass Sie dieses Objekt löschen möchten?')">
@@ -3679,13 +3800,66 @@ document.addEventListener('DOMContentLoaded', ()=>{
card.classList.remove('highlight-item', 'code-match', 'exact-code-match');
});
applyFilters(true);
if (codeSearchTerm) {
loadItems(0, false, codeSearchTerm, 'all');
} else {
loadItems();
}
setTimeout(() => {
searchInput.classList.remove('searching');
}, 500);
}
function syncSearchedGroupedAction(card, searchTerm, exactCodeMatch) {
const searchedAction = card.querySelector('[data-grouped-searched-action]');
if (!searchedAction) return;
const borrowForm = card.querySelector('[data-grouped-borrow-form]');
searchedAction.style.display = 'none';
searchedAction.innerHTML = '';
if (!exactCodeMatch || !searchTerm) {
if (borrowForm) borrowForm.style.display = '';
return;
}
let groupedUnits = [];
try {
groupedUnits = JSON.parse(card.dataset.groupedUnits || '[]');
} catch {
groupedUnits = [];
}
const matchedUnit = groupedUnits.find(unit =>
String(unit.code || '').trim().toLowerCase() === searchTerm
);
if (!matchedUnit) return;
if (matchedUnit.available) {
if (borrowForm) borrowForm.style.display = '';
const specificSelect = borrowForm?.querySelector('[name="specific_item_id"]');
if (specificSelect) {
specificSelect.value = matchedUnit.id;
}
return;
}
if (borrowForm) borrowForm.style.display = 'none';
searchedAction.style.display = 'block';
if (matchedUnit.borrowed_by_current_user) {
searchedAction.innerHTML = `
<form method="POST" action="/zurueckgeben/${encodeURIComponent(matchedUnit.id)}">
<button class="ausleihen" type="submit">Zurückgeben (${escapeHtml(matchedUnit.code || 'Code')})</button>
</form>`;
} else {
searchedAction.innerHTML = `
<button class="ausleihen disabled-button" type="button" disabled>
${escapeHtml(matchedUnit.code || 'Code')} ausgeliehen
</button>`;
}
}
function applyFilters(isDirectSearch = false) {
const itemsContainer = document.querySelector('#items-container');
const items = itemsContainer.querySelectorAll('.item-card');
@@ -3732,6 +3906,7 @@ document.addEventListener('DOMContentLoaded', ()=>{
if (passesAllFilters && matchesCodeSearch && matchesDesc && (!favoritesOnly || isFavoriteCard)) {
item.style.display = 'block';
visibleCount++;
syncSearchedGroupedAction(item, searchTerm, exactCodeMatch);
if (exactCodeMatch) {
item.classList.add('highlight-item', 'exact-code-match');
@@ -3756,6 +3931,7 @@ document.addEventListener('DOMContentLoaded', ()=>{
}
} else {
item.style.display = 'none';
syncSearchedGroupedAction(item, '', false);
}
});
@@ -3830,7 +4006,7 @@ document.addEventListener('DOMContentLoaded', ()=>{
card.classList.remove('highlight-item', 'code-match', 'exact-code-match');
});
applyFilters();
loadItems();
}
// Description search using backend substring match
@@ -3839,21 +4015,12 @@ document.addEventListener('DOMContentLoaded', ()=>{
const term = (input?.value || '').trim();
if (!term){
descSearchIds = null;
applyFilters(true);
loadItems(0, false, codeSearchTerm, 'all');
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();
}
applyFilters(true);
})
.catch(()=>{ descSearchIds = new Set(); applyFilters(true); })
descSearchIds = null;
loadItems(0, false, term, 'title_description')
.finally(()=>{ setTimeout(()=> input?.classList.remove('searching'), 300); });
}
@@ -3861,7 +4028,7 @@ document.addEventListener('DOMContentLoaded', ()=>{
const input = document.getElementById('desc-search');
if (input) input.value = '';
descSearchIds = null;
applyFilters();
loadItems(0, false, codeSearchTerm, 'all');
}
// Open item modal with fresh details from backend.
+2 -4
View File
@@ -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">