Compare commits

...

8 Commits

Author SHA1 Message Date
Aiirondev_dev 0ef928efd8 slight fix of the scanner
Release Inventarsystem / release-docker (push) Successful in 2m14s
2026-08-16 15:50:48 +02:00
Aiirondev_dev 1299140823 Fix of the encryption processing with the Student class
Release Inventarsystem / release-docker (push) Successful in 2m14s
2026-08-16 13:17:20 +02:00
Aiirondev_dev ea48d5c28a decryption of the parsed fields
Release Inventarsystem / release-docker (push) Successful in 2m15s
2026-08-16 13:08:34 +02:00
Aiirondev_dev 514065f4af fixe of a list error
Release Inventarsystem / release-docker (push) Successful in 3m7s
2026-08-16 12:58:31 +02:00
Aiirondev_dev 90da8487f2 feat(student-cards): add class-specific PDF export with dynamic dropdown
Release Inventarsystem / release-docker (push) Successful in 2m16s
- Implement /student_card_class_barcode_download route to filter student cards by class
- Dynamically extract unique class names from the database for frontend selection
- Update student_cards_admin.html to replace the text input with a class selection dropdown
2026-08-15 18:10:05 +02:00
Aiirondev_dev b1ca358d41 Cleanup of the front end and backend of the upload function to only allow images and no videos because of space and memory reasons as well as hardening the uploading process.
Release Inventarsystem / release-docker (push) Successful in 2m15s
2026-08-15 01:38:35 +02:00
Aiirondev_dev 47cba5865d Removal of dupolicate function.
Release Inventarsystem / release-docker (push) Successful in 4m55s
2026-08-15 01:33:38 +02:00
Aiirondev_dev 4a776872e0 Improvement of the style for the Library Module
Release Inventarsystem / release-docker (push) Successful in 2m15s
2026-08-15 00:08:13 +02:00
4 changed files with 517 additions and 463 deletions
+321 -206
View File
@@ -2177,7 +2177,7 @@ def _upload_student_cards_excel():
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT) client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
try: try:
db = client[cfg.MONGODB_DB] db = client[cfg.MONGODB_DB]
student_cards = db['student_cards'] student_cards = list(db['student_cards'].find())
existing_ids.update( existing_ids.update(
str(card.get('AusweisId', '')).strip().upper() str(card.get('AusweisId', '')).strip().upper()
for card in student_cards.find({}, {'AusweisId': 1}) for card in student_cards.find({}, {'AusweisId': 1})
@@ -4108,20 +4108,21 @@ def student_cards_admin():
app.logger.error(f"Error adding student card: {e}") app.logger.error(f"Error adding student card: {e}")
flash('Fehler beim Hinzufügen des Ausweises.', 'error') flash('Fehler beim Hinzufügen des Ausweises.', 'error')
# Get all student cards # Get all student cards and decrypt them
all_cards = list(student_cards.find().sort('AusweisId', 1)) all_cards = list(student_cards.find().sort('AusweisId', 1))
all_cards = [_decrypt_student_card_doc(card) for card in all_cards] all_cards = [_decrypt_student_card_doc(card) for card in all_cards]
client.close() client.close()
raw_classes = set(card.get('Klasse', '').strip() for card in all_cards if card.get('Klasse'))
available_classes = sorted(list(raw_classes))
return render_template( return render_template(
'student_cards_admin.html', 'student_cards_admin.html',
username=session['username'], student_cards=all_cards, # Hier all_cards übergeben
student_cards=all_cards,
edit_mode=edit_mode, edit_mode=edit_mode,
form_data=form_data, form_data=form_data,
config={'default': cfg.STUDENT_DEFAULT_BORROW_DAYS}, available_classes=available_classes, # Enthält nun die lesbaren Klassen
library_module_enabled=cfg.MODULES.is_enabled('library'), config=cfg.get_school_info()
student_cards_module_enabled=cfg.MODULES.is_enabled('student_cards')
) )
@@ -4215,6 +4216,7 @@ def student_card_barcode_download():
from reportlab.pdfgen import canvas from reportlab.pdfgen import canvas
from reportlab.lib.units import mm from reportlab.lib.units import mm
from reportlab.lib.colors import HexColor, white, black from reportlab.lib.colors import HexColor, white, black
from reportlab.pdfbase.pdfmetrics import stringWidth
from io import BytesIO from io import BytesIO
import barcode import barcode
from barcode.writer import ImageWriter from barcode.writer import ImageWriter
@@ -4265,7 +4267,7 @@ def student_card_barcode_download():
# Card shadow effect (unter border) # Card shadow effect (unter border)
c.setFillColor(HexColor("#E2E8F0")) c.setFillColor(HexColor("#E2E8F0"))
c.rect(x_pos + 0.5*mm, y_pos - card_height - 0.5*mm, card_width, card_height, fill=1, stroke=0) c.rect(x_pos + 0.5 * mm, y_pos - card_height - 0.5 * mm, card_width, card_height, fill=1, stroke=0)
# Card background # Card background
c.setLineWidth(1) c.setLineWidth(1)
@@ -4280,84 +4282,98 @@ def student_card_barcode_download():
# Header bar # Header bar
c.setFillColor(header_color) c.setFillColor(header_color)
c.rect(x_pos, y_pos - 10*mm, card_width, 10*mm, fill=1, stroke=0) c.rect(x_pos, y_pos - 10 * mm, card_width, 10 * mm, fill=1, stroke=0)
# Header accent line # Header accent line
c.setStrokeColor(accent_color) c.setStrokeColor(accent_color)
c.setLineWidth(2) c.setLineWidth(2)
c.line(x_pos, y_pos - 10*mm, x_pos + card_width, y_pos - 10*mm) c.line(x_pos, y_pos - 10 * mm, x_pos + card_width, y_pos - 10 * mm)
school_name = cfg.get_school_info() school_name = cfg.get_school_info()
school_name = school_name["name"] school_name_str = str(school_name["name"])
# "SCHÜLERAUSWEIS" text in header
c.setFont("Helvetica-Bold", 9) # Zwei-Zeilen-Header: 1. BIBLIOTHEKSAUSWEIS, 2. Schulname (dynamisch)
c.setFont("Helvetica-Bold", 8)
c.setFillColor(white) c.setFillColor(white)
c.drawString(x_pos + 3*mm, y_pos - 6.5*mm, f"BIBLIOTHEKSAUSWEIS - {str(school_name)}") c.drawString(x_pos + 3 * mm, y_pos - 4 * mm, "BIBLIOTHEKSAUSWEIS")
# Dynamische Anpassung des Schulnamens an die Breite
font_size = 8
while stringWidth(school_name_str, "Helvetica-Bold", font_size) > (card_width - 6 * mm) and font_size > 4:
font_size -= 0.5
c.setFont("Helvetica-Bold", font_size)
c.drawString(x_pos + 3 * mm, y_pos - 8 * mm, school_name_str)
# Student name - large and bold # Student name - large and bold
c.setFillColor(text_dark) c.setFillColor(text_dark)
c.setFont("Helvetica-Bold", 10) c.setFont("Helvetica-Bold", 10)
name = card['SchülerName'][:20] name = card['SchülerName'][:20]
c.drawString(x_pos + 3*mm, y_pos - 14*mm, name) c.drawString(x_pos + 3 * mm, y_pos - 16 * mm, name)
# ID with label # ID with label
c.setFont("Helvetica", 8) c.setFont("Helvetica", 8)
c.setFillColor(text_gray) c.setFillColor(text_gray)
c.drawString(x_pos + 3*mm, y_pos - 18*mm, "Ausweis ID:") c.drawString(x_pos + 3 * mm, y_pos - 20 * mm, "Ausweis ID:")
c.setFillColor(text_dark) c.setFillColor(text_dark)
c.setFont("Helvetica-Bold", 9) c.setFont("Helvetica-Bold", 9)
c.drawString(x_pos + 3*mm, y_pos - 21*mm, str(card['AusweisId'])) c.drawString(x_pos + 3 * mm, y_pos - 23 * mm, str(card['AusweisId']))
# Class with label # Class with label
if card.get('Klasse'): if card.get('Klasse'):
c.setFont("Helvetica", 8) c.setFont("Helvetica", 8)
c.setFillColor(text_gray) c.setFillColor(text_gray)
c.drawString(x_pos + 3*mm, y_pos - 25*mm, "Klasse:") c.drawString(x_pos + 3 * mm, y_pos - 27 * mm, "Klasse:")
c.setFillColor(text_dark) c.setFillColor(text_dark)
c.setFont("Helvetica-Bold", 9) c.setFont("Helvetica-Bold", 9)
c.drawString(x_pos + 3*mm, y_pos - 28*mm, card['Klasse']) c.drawString(x_pos + 3 * mm, y_pos - 30 * mm, card['Klasse'])
c.drawString(x_pos + 3*mm, y_pos - 31*mm, "-") c.drawString(x_pos + 3 * mm, y_pos - 33 * mm, "-")
c.drawString(x_pos + 3*mm, y_pos - 34*mm, "-") c.drawString(x_pos + 3 * mm, y_pos - 36 * mm, "-")
c.drawString(x_pos + 3*mm, y_pos - 37*mm, "-") c.drawString(x_pos + 3 * mm, y_pos - 39 * mm, "-")
c.drawString(x_pos + 3*mm, y_pos - 40*mm, "-") c.drawString(x_pos + 3 * mm, y_pos - 42 * mm, "-")
# Right barcode section with border highlight # Right barcode section with border highlight
barcode_x_start = x_pos + info_width + 1*mm barcode_x_start = x_pos + info_width + 1 * mm
c.setFillColor(accent_color) c.setFillColor(accent_color)
c.setLineWidth(0) c.setLineWidth(0)
c.rect(barcode_x_start - 1*mm, y_pos - card_height, c.rect(barcode_x_start - 1 * mm, y_pos - card_height,
card_width - info_width + 1*mm, 2*mm, fill=1, stroke=0) card_width - info_width + 1 * mm, 2 * mm, fill=1, stroke=0)
# Generate and add larger barcode # Generate and add dynamically sized barcode
try: try:
temp_dir = tempfile.gettempdir() temp_dir = tempfile.gettempdir()
barcode_path = os.path.join(temp_dir, f"barcode_{card['AusweisId']}") barcode_path = os.path.join(temp_dir, f"barcode_{card['AusweisId']}")
# Generate barcode with higher module width for better scanning # ImageWriter Settings für maximalen Platz und Lesbarkeit
options = {
'write_text': False, # Text entfernen (spart Platz)
'quiet_zone': 1.0, # Minimaler Rand
'dpi': 300 # Hohe Auflösung
}
barcode_obj = barcode.get('code128', str(card['AusweisId']), writer=ImageWriter()) barcode_obj = barcode.get('code128', str(card['AusweisId']), writer=ImageWriter())
barcode_obj.save(barcode_path) barcode_obj.save(barcode_path, options=options)
barcode_file = f"{barcode_path}.png" barcode_file = f"{barcode_path}.png"
if os.path.exists(barcode_file): if os.path.exists(barcode_file):
# Larger barcode taking up most of right section # Barcode über den fast kompletten restlichen Bereich strecken
barcode_width = (card_width - info_width - 4*mm) barcode_width = (card_width - info_width - 4 * mm)
barcode_height = 16*mm barcode_height = card_height - 15 * mm
barcode_y = y_pos - card_height + (card_height - barcode_height) / 2 + 2*mm barcode_y = y_pos - card_height + 2.5 * mm
c.drawImage(barcode_file, c.drawImage(barcode_file,
barcode_x_start + 1*mm, barcode_x_start + 1 * mm,
barcode_y, barcode_y,
width=barcode_width, width=barcode_width,
height=barcode_height, height=barcode_height,
preserveAspectRatio=True) preserveAspectRatio=False) # Strecken erlauben für maximalen Scanbereich
os.remove(barcode_file) os.remove(barcode_file)
else: else:
raise Exception("Barcode file not created") raise Exception("Barcode file not created")
except Exception as e: except Exception as e:
c.setFont("Helvetica", 7) c.setFont("Helvetica", 7)
c.setFillColor(HexColor("#DC2626")) c.setFillColor(HexColor("#DC2626"))
c.drawString(barcode_x_start + 2*mm, y_pos - card_height + 20*mm, "⚠️ Barcode Error") c.drawString(barcode_x_start + 2 * mm, y_pos - card_height + 20 * mm, "⚠️ Barcode Error")
col_idx += 1 col_idx += 1
if col_idx >= cols: if col_idx >= cols:
@@ -4378,6 +4394,196 @@ def student_card_barcode_download():
return redirect(url_for('student_cards_admin')) return redirect(url_for('student_cards_admin'))
@app.route('/student_card_class_barcode_download', methods=['GET'])
def student_card_class_barcode_download():
"""
Download PDF with student card barcodes filtered by a specific class from dropdown.
"""
from flask import request, session, redirect, url_for, send_file, flash
if 'username' not in session:
return redirect(url_for('login'))
current_permissions = us.get_effective_permissions(session['username'])
if not current_permissions['actions'].get('can_manage_users', False):
flash('Ihnen fehlen die nötigen Berechtigungen, um diese Aktion auszuführen.', 'error')
return redirect(url_for('library_view'))
class_name = request.args.get('class_name', '').strip()
if not class_name:
flash('Bitte wählen Sie eine Klasse aus.', 'error')
return redirect(url_for('student_cards_admin'))
try:
from reportlab.lib.pagesizes import A4
from reportlab.pdfgen import canvas
from reportlab.lib.units import mm
from reportlab.lib.colors import HexColor, white
from reportlab.pdfbase.pdfmetrics import stringWidth
from io import BytesIO
import barcode
from barcode.writer import ImageWriter
import tempfile
import os
import datetime
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
db = client[cfg.MONGODB_DB]
student_cards = db['student_cards']
# 1. Alle Ausweise laden
all_cards = list(student_cards.find().sort('AusweisId', 1))
client.close()
# 2. Alle entschlüsseln
decrypted_cards = [_decrypt_student_card_doc(card) for card in all_cards]
# 3. Im Speicher nach der Klartext-Klasse filtern
class_cards = [
card for card in decrypted_cards
if card.get('Klasse', '').strip() == class_name
]
if not class_cards:
flash(f'Keine Ausweise für die Klasse "{class_name}" gefunden.', 'error')
return redirect(url_for('student_cards_admin'))
pdf_buffer = BytesIO()
c = canvas.Canvas(pdf_buffer, pagesize=A4)
page_width, page_height = A4
margin = 10 * mm
card_width = 88 * mm
card_height = 56 * mm
cols = 2
gap_x = 8 * mm
gap_y = 8 * mm
x_positions = [margin, margin + card_width + gap_x]
y_start = page_height - margin
y_pos = y_start
col_idx = 0
header_color = HexColor("#0F172A")
accent_color = HexColor("#2563EB")
light_bg = HexColor("#F8FAFC")
card_bg_color = HexColor("#FFFFFF")
text_dark = HexColor("#1E293B")
text_gray = HexColor("#64748B")
for i, card in enumerate(class_cards):
if col_idx == 0 and i > 0:
y_pos -= (card_height + gap_y)
if y_pos - card_height < margin:
c.showPage()
y_pos = y_start
col_idx = 0
x_pos = x_positions[col_idx]
c.setFillColor(HexColor("#E2E8F0"))
c.rect(x_pos + 0.5 * mm, y_pos - card_height - 0.5 * mm, card_width, card_height, fill=1, stroke=0)
c.setLineWidth(1)
c.setFillColor(card_bg_color)
c.setStrokeColor(HexColor("#CBD5E1"))
c.rect(x_pos, y_pos - card_height, card_width, card_height, fill=1, stroke=1)
info_width = 38 * mm
c.setFillColor(light_bg)
c.rect(x_pos, y_pos - card_height, info_width, card_height, fill=1, stroke=0)
c.setFillColor(header_color)
c.rect(x_pos, y_pos - 10 * mm, card_width, 10 * mm, fill=1, stroke=0)
c.setStrokeColor(accent_color)
c.setLineWidth(2)
c.line(x_pos, y_pos - 10 * mm, x_pos + card_width, y_pos - 10 * mm)
school_name = cfg.get_school_info()
school_name_str = str(school_name["name"])
c.setFont("Helvetica-Bold", 8)
c.setFillColor(white)
c.drawString(x_pos + 3 * mm, y_pos - 4 * mm, "BIBLIOTHEKSAUSWEIS")
font_size = 8
while stringWidth(school_name_str, "Helvetica-Bold", font_size) > (card_width - 6 * mm) and font_size > 4:
font_size -= 0.5
c.setFont("Helvetica-Bold", font_size)
c.drawString(x_pos + 3 * mm, y_pos - 8 * mm, school_name_str)
c.setFillColor(text_dark)
c.setFont("Helvetica-Bold", 10)
name = card.get('SchülerName', '')[:20]
c.drawString(x_pos + 3 * mm, y_pos - 16 * mm, name)
c.setFont("Helvetica", 8)
c.setFillColor(text_gray)
c.drawString(x_pos + 3 * mm, y_pos - 20 * mm, "Ausweis ID:")
c.setFillColor(text_dark)
c.setFont("Helvetica-Bold", 9)
c.drawString(x_pos + 3 * mm, y_pos - 23 * mm, str(card.get('AusweisId', '')))
if card.get('Klasse'):
c.setFont("Helvetica", 8)
c.setFillColor(text_gray)
c.drawString(x_pos + 3 * mm, y_pos - 27 * mm, "Klasse:")
c.setFillColor(text_dark)
c.setFont("Helvetica-Bold", 9)
c.drawString(x_pos + 3 * mm, y_pos - 30 * mm, card['Klasse'])
barcode_x_start = x_pos + info_width + 1 * mm
c.setFillColor(accent_color)
c.setLineWidth(0)
c.rect(barcode_x_start - 1 * mm, y_pos - card_height, card_width - info_width + 1 * mm, 2 * mm, fill=1,
stroke=0)
try:
temp_dir = tempfile.gettempdir()
barcode_path = os.path.join(temp_dir, f"barcode_{card.get('AusweisId', i)}")
options = {
'write_text': False,
'quiet_zone': 1.0,
'dpi': 300
}
barcode_obj = barcode.get('code128', str(card.get('AusweisId', '')), writer=ImageWriter())
barcode_obj.save(barcode_path, options=options)
barcode_file = f"{barcode_path}.png"
if os.path.exists(barcode_file):
barcode_width = (card_width - info_width - 4 * mm)
barcode_height = card_height - 15 * mm
barcode_y = y_pos - card_height + 2.5 * mm
c.drawImage(barcode_file, barcode_x_start + 1 * mm, barcode_y,
width=barcode_width, height=barcode_height, preserveAspectRatio=False)
os.remove(barcode_file)
except Exception as e:
pass
col_idx += 1
if col_idx >= cols:
col_idx = 0
c.save()
pdf_buffer.seek(0)
filename = f'ausweise_klasse_{class_name.replace(" ", "_")}_{datetime.datetime.now().strftime("%Y%m%d")}.pdf'
return send_file(
pdf_buffer,
mimetype='application/pdf',
as_attachment=True,
download_name=filename
)
except Exception as e:
flash('Fehler beim PDF-Download', 'error')
return redirect(url_for('student_cards_admin'))
@app.route('/student_card_single_barcode_download/<card_id>', methods=['GET']) @app.route('/student_card_single_barcode_download/<card_id>', methods=['GET'])
def student_card_single_barcode_download(card_id): def student_card_single_barcode_download(card_id):
""" """
@@ -4399,6 +4605,7 @@ def student_card_single_barcode_download(card_id):
from reportlab.pdfgen import canvas from reportlab.pdfgen import canvas
from reportlab.lib.units import mm from reportlab.lib.units import mm
from reportlab.lib.colors import HexColor, white, black from reportlab.lib.colors import HexColor, white, black
from reportlab.pdfbase.pdfmetrics import stringWidth
from io import BytesIO from io import BytesIO
from bson.objectid import ObjectId from bson.objectid import ObjectId
import barcode import barcode
@@ -4421,29 +4628,30 @@ def student_card_single_barcode_download(card_id):
c = canvas.Canvas(pdf_buffer, pagesize=A4) c = canvas.Canvas(pdf_buffer, pagesize=A4)
page_width, page_height = A4 page_width, page_height = A4
margin = 20 * mm
card_width = 105 * mm card_width = 105 * mm
card_height = 70 * mm card_height = 70 * mm
x_pos = (page_width - card_width) / 2 x_pos = (page_width - card_width) / 2
y_pos = (page_height - card_height) / 2
# Korrektur des y_pos auf die OBERKANTE, damit die nachfolgenden Rechnungen bündig sind
y_pos = (page_height + card_height) / 2
# Professional color palette # Professional color palette
header_color = HexColor("#0F172A") # Sehr dunkles blau header_color = HexColor("#0F172A")
accent_color = HexColor("#2563EB") # Helles blau accent_color = HexColor("#2563EB")
light_bg = HexColor("#F8FAFC") # Sehr heller grau light_bg = HexColor("#F8FAFC")
card_bg_color = HexColor("#FFFFFF") # Weiß card_bg_color = HexColor("#FFFFFF")
text_dark = HexColor("#1E293B") # Dunkler text text_dark = HexColor("#1E293B")
text_gray = HexColor("#64748B") # Grauer text text_gray = HexColor("#64748B")
# Card shadow effect # Card shadow effect
c.setFillColor(HexColor("#E2E8F0")) c.setFillColor(HexColor("#E2E8F0"))
c.rect(x_pos + 1*mm, y_pos - card_height - 1*mm, card_width, card_height, fill=1, stroke=0) c.rect(x_pos + 1 * mm, y_pos - card_height - 1 * mm, card_width, card_height, fill=1, stroke=0)
# Card background # Card background
c.setLineWidth(1.5) c.setLineWidth(1.5)
c.setFillColor(card_bg_color) c.setFillColor(card_bg_color)
c.setStrokeColor(HexColor("#CBD5E1")) c.setStrokeColor(HexColor("#CBD5E1"))
c.rect(x_pos, y_pos, card_width, card_height, fill=1, stroke=1) c.rect(x_pos, y_pos - card_height, card_width, card_height, fill=1, stroke=1)
# Left info section (50mm) # Left info section (50mm)
info_width = 50 * mm info_width = 50 * mm
@@ -4457,86 +4665,101 @@ def student_card_single_barcode_download(card_id):
# Header bar # Header bar
c.setFillColor(header_color) c.setFillColor(header_color)
c.rect(x_pos, y_pos - 10*mm, card_width, 10*mm, fill=1, stroke=0) c.rect(x_pos, y_pos - 10 * mm, card_width, 10 * mm, fill=1, stroke=0)
# Header accent line # Header accent line
c.setStrokeColor(accent_color) c.setStrokeColor(accent_color)
c.setLineWidth(2.5) c.setLineWidth(2.5)
c.line(x_pos, y_pos - 10*mm, x_pos + card_width, y_pos - 10*mm) c.line(x_pos, y_pos - 10 * mm, x_pos + card_width, y_pos - 10 * mm)
school_name = cfg.get_school_info() school_name = cfg.get_school_info()
school_name = school_name["name"] school_name_str = str(school_name["name"])
# "SCHÜLERAUSWEIS" text in header
c.setFont("Helvetica-Bold", 11) # Zwei-Zeilen-Header
c.setFont("Helvetica-Bold", 10)
c.setFillColor(white) c.setFillColor(white)
c.drawString(x_pos + 4*mm, y_pos - 6.5*mm, f"BIBLIOTHEKSAUSWEIS - {str(school_name)}") c.drawString(x_pos + 4 * mm, y_pos - 4.5 * mm, "BIBLIOTHEKSAUSWEIS")
# Dynamische Größe für den Schulnamen
font_size = 10
while stringWidth(school_name_str, "Helvetica-Bold", font_size) > (card_width - 8 * mm) and font_size > 4:
font_size -= 0.5
c.setFont("Helvetica-Bold", font_size)
c.drawString(x_pos + 4 * mm, y_pos - 8.5 * mm, school_name_str)
# Student name - large and prominent # Student name - large and prominent
c.setFillColor(text_dark) c.setFillColor(text_dark)
c.setFont("Helvetica-Bold", 12) c.setFont("Helvetica-Bold", 12)
name = card['SchülerName'][:25] name = card['SchülerName'][:25]
c.drawString(x_pos + 4*mm, y_pos - 16*mm, name) c.drawString(x_pos + 4 * mm, y_pos - 18 * mm, name)
# ID section with label # ID section with label
c.setFont("Helvetica", 9) c.setFont("Helvetica", 9)
c.setFillColor(text_gray) c.setFillColor(text_gray)
c.drawString(x_pos + 4*mm, y_pos - 21*mm, "Ausweis-ID:") c.drawString(x_pos + 4 * mm, y_pos - 24 * mm, "Ausweis-ID:")
c.setFillColor(text_dark) c.setFillColor(text_dark)
c.setFont("Helvetica-Bold", 11) c.setFont("Helvetica-Bold", 11)
c.drawString(x_pos + 4*mm, y_pos - 25*mm, str(card['AusweisId'])) c.drawString(x_pos + 4 * mm, y_pos - 28 * mm, str(card['AusweisId']))
# Class section # Class section
if card.get('Klasse'): if card.get('Klasse'):
c.setFont("Helvetica", 9) c.setFont("Helvetica", 9)
c.setFillColor(text_gray) c.setFillColor(text_gray)
c.drawString(x_pos + 4*mm, y_pos - 30*mm, "Klasse:") c.drawString(x_pos + 4 * mm, y_pos - 34 * mm, "Klasse:")
c.setFillColor(text_dark) c.setFillColor(text_dark)
c.setFont("Helvetica-Bold", 11) c.setFont("Helvetica-Bold", 11)
c.drawString(x_pos + 4*mm, y_pos - 34*mm, card['Klasse']) c.drawString(x_pos + 4 * mm, y_pos - 38 * mm, card['Klasse'])
# Barcode section - right side with blue accent # Barcode section - right side with blue accent
barcode_x_start = x_pos + info_width + 3*mm barcode_x_start = x_pos + info_width + 3 * mm
c.setFillColor(accent_color) c.setFillColor(accent_color)
c.setLineWidth(0) c.setLineWidth(0)
c.rect(x_pos + info_width, y_pos - card_height, c.rect(x_pos + info_width, y_pos - card_height,
card_width - info_width, 3*mm, fill=1, stroke=0) card_width - info_width, 3 * mm, fill=1, stroke=0)
# Generate and add large barcode # Generate and add large dynamically sized barcode
try: try:
temp_dir = tempfile.gettempdir() temp_dir = tempfile.gettempdir()
barcode_path = os.path.join(temp_dir, f"barcode_{card['AusweisId']}") barcode_path = os.path.join(temp_dir, f"barcode_{card['AusweisId']}")
# Generate barcode with better sizing # Options anpassen für Scanbarkeit
options = {
'write_text': False,
'quiet_zone': 1.0,
'dpi': 300
}
barcode_obj = barcode.get('code128', str(card['AusweisId']), writer=ImageWriter()) barcode_obj = barcode.get('code128', str(card['AusweisId']), writer=ImageWriter())
barcode_obj.save(barcode_path) barcode_obj.save(barcode_path, options=options)
barcode_file = f"{barcode_path}.png" barcode_file = f"{barcode_path}.png"
if os.path.exists(barcode_file): if os.path.exists(barcode_file):
# Large barcode on right side # Den gesamten Platz auf der rechten Seite ausnutzen
barcode_width = (card_width - info_width - 8*mm) barcode_width = (card_width - info_width - 8 * mm)
barcode_height = 20*mm barcode_height = card_height - 18 * mm
barcode_y_offset = (card_height - 10*mm - barcode_height) / 2 barcode_y = y_pos - card_height + 5 * mm
c.drawImage(barcode_file, c.drawImage(barcode_file,
barcode_x_start, barcode_x_start + 1 * mm,
y_pos - card_height + barcode_y_offset + 5*mm, barcode_y,
width=barcode_width, width=barcode_width,
height=barcode_height, height=barcode_height,
preserveAspectRatio=True) preserveAspectRatio=False) # Ausdehnung in alle Richtungen
os.remove(barcode_file) os.remove(barcode_file)
else: else:
raise Exception("Barcode file not created") raise Exception("Barcode file not created")
except Exception as e: except Exception as e:
c.setFont("Helvetica", 9) c.setFont("Helvetica", 9)
c.setFillColor(HexColor("#DC2626")) c.setFillColor(HexColor("#DC2626"))
c.drawString(barcode_x_start + 2*mm, y_pos - card_height + 25*mm, "⚠️ Barcode Error") c.drawString(barcode_x_start + 2 * mm, y_pos - card_height + 25 * mm, "⚠️ Barcode Error")
# Bottom info line # Bottom info line
c.setStrokeColor(HexColor("#CBD5E1")) c.setStrokeColor(HexColor("#CBD5E1"))
c.setLineWidth(0.5) c.setLineWidth(0.5)
c.line(x_pos, y_pos - card_height + 3*mm, x_pos + card_width, y_pos - card_height + 3*mm) c.line(x_pos, y_pos - card_height + 3 * mm, x_pos + card_width, y_pos - card_height + 3 * mm)
c.save() c.save()
pdf_buffer.seek(0) pdf_buffer.seek(0)
@@ -4552,7 +4775,6 @@ def student_card_single_barcode_download(card_id):
flash('Fehler beim PDF-Download', 'error') flash('Fehler beim PDF-Download', 'error')
return redirect(url_for('student_cards_admin')) return redirect(url_for('student_cards_admin'))
@app.route('/login', methods=['GET', 'POST']) @app.route('/login', methods=['GET', 'POST'])
def login(): def login():
""" """
@@ -5239,7 +5461,7 @@ def debug_favorites():
def upload_item(): def upload_item():
""" """
Route for adding new items to the inventory. Route for adding new items to the inventory.
Handles file uploads and creates QR codes. Handles image file uploads and creates QR codes.
Enhanced for mobile browser compatibility. Enhanced for mobile browser compatibility.
Returns: Returns:
@@ -5266,9 +5488,9 @@ def upload_item():
# Detect if request is from mobile device # Detect if request is from mobile device
is_mobile = 'Mobile' in request.headers.get('User-Agent', '') is_mobile = 'Mobile' in request.headers.get('User-Agent', '')
# Log mobile request for debugging
if is_mobile: if is_mobile:
app.logger.info(f"Mobile upload from {request.headers.get('User-Agent', 'unknown')} by {encrypt_text(username)}") app.logger.info(
f"Mobile upload from {request.headers.get('User-Agent', 'unknown')} by {encrypt_text(username)}")
try: try:
# Strip whitespace from all text fields # Strip whitespace from all text fields
@@ -5301,46 +5523,30 @@ def upload_item():
item_count = 1 item_count = 1
item_count = max(1, min(item_count, 100)) item_count = max(1, min(item_count, 100))
# Optional list of per-item codes (one code per line) # Optional list of per-item codes
individual_codes = [] individual_codes = []
if individual_codes_raw: if individual_codes_raw:
individual_codes = [c.strip() for c in str(individual_codes_raw).replace(',', '\n').splitlines() if c.strip()] individual_codes = [c.strip() for c in str(individual_codes_raw).replace(',', '\n').splitlines() if
c.strip()]
# Check if this is a duplication
is_duplicating = request.form.get('is_duplicating') == 'true'
# Get duplicate_images if duplicating
duplicate_images = request.form.getlist('duplicate_images') if is_duplicating else []
# Make sure duplicate_images is always a list, even if there's only one
if is_duplicating and duplicate_images and not isinstance(duplicate_images, list):
duplicate_images = [duplicate_images]
# Log details about each image
if is_duplicating and duplicate_images:
for i, img in enumerate(duplicate_images):
print(f"DEBUG: Duplicate image {i+1}/{len(duplicate_images)}: {img}")
# Get book cover image if downloaded # Get book cover image if downloaded
book_cover_image = request.form.get('book_cover_image') book_cover_image = request.form.get('book_cover_image')
# Special handling for mobile browsers that might send data differently # Special handling for mobile browsers
if is_mobile and 'mobile_data' in request.form: if is_mobile and 'mobile_data' in request.form:
try: try:
mobile_data = json.loads(request.form['mobile_data']) mobile_data = json.loads(request.form['mobile_data'])
# Override values with mobile data if available
if 'filters' in mobile_data: if 'filters' in mobile_data:
filter_upload = mobile_data.get('filters', []) filter_upload = mobile_data.get('filters', [])
if 'filters2' in mobile_data: if 'filters2' in mobile_data:
filter_upload2 = mobile_data.get('filters2', []) filter_upload2 = mobile_data.get('filters2', [])
if 'filters3' in mobile_data: if 'filters3' in mobile_data:
filter_upload3 = mobile_data.get('filters3', []) filter_upload3 = mobile_data.get('filters3', [])
if 'duplicate_images' in mobile_data and mobile_data['duplicate_images']:
duplicate_images = mobile_data.get('duplicate_images', [])
except json.JSONDecodeError as e: except json.JSONDecodeError as e:
app.logger.error(f"Error parsing mobile data: {str(e)}") app.logger.error(f"Error parsing mobile data: {str(e)}")
except:
except Exception as e:
app.logger.error(f"Upload Form Data Parsing Error: {str(e)}")
flash('Fehler beim Verarbeiten der Formulardaten. Bitte versuchen Sie es erneut.', 'error') flash('Fehler beim Verarbeiten der Formulardaten. Bitte versuchen Sie es erneut.', 'error')
return redirect(url_for(success_redirect_endpoint)) return redirect(url_for(success_redirect_endpoint))
@@ -5348,7 +5554,7 @@ def upload_item():
filter_upload = expand_filter_selection(filter_upload, 1) filter_upload = expand_filter_selection(filter_upload, 1)
filter_upload2 = expand_filter_selection(filter_upload2, 2) filter_upload2 = expand_filter_selection(filter_upload2, 2)
# Validation # Base Validation
if not name or not ort or not beschreibung: if not name or not ort or not beschreibung:
error_msg = 'Bitte füllen Sie alle erforderlichen Felder aus' error_msg = 'Bitte füllen Sie alle erforderlichen Felder aus'
if is_mobile: if is_mobile:
@@ -5384,9 +5590,8 @@ def upload_item():
flash(error_msg, 'error') flash(error_msg, 'error')
return redirect(url_for('library_admin')) return redirect(url_for('library_admin'))
# Only check for images if not duplicating and no duplicate images provided and no book cover # Strict Image Validation (Since duplication is removed)
# For library mode, skip this check as images come only from ISBN fetch if upload_mode != 'library' and not images and not book_cover_image:
if upload_mode != 'library' and not is_duplicating and not images and not duplicate_images and not book_cover_image:
error_msg = 'Bitte laden Sie mindestens ein Bild hoch' error_msg = 'Bitte laden Sie mindestens ein Bild hoch'
if is_mobile: if is_mobile:
return jsonify({'success': False, 'message': error_msg}), 400 return jsonify({'success': False, 'message': error_msg}), 400
@@ -5395,36 +5600,20 @@ def upload_item():
return redirect(url_for(success_redirect_endpoint)) return redirect(url_for(success_redirect_endpoint))
primary_code_raw = request.form.get('code_4', '').strip() primary_code_raw = request.form.get('code_4', '').strip()
individual_codes_raw = request.form.get('individual_codes', '').strip()
all_item_codes = [] all_item_codes = []
if primary_code_raw: if primary_code_raw:
all_item_codes.append(primary_code_raw) all_item_codes.append(primary_code_raw)
if individual_codes_raw: if individual_codes:
extra_codes = [c.strip() for c in individual_codes_raw.replace('\r', '').split('\n') if c.strip()] for c in individual_codes:
for c in extra_codes:
if c not in all_item_codes: if c not in all_item_codes:
all_item_codes.append(c) all_item_codes.append(c)
if len(all_item_codes) > 0: if len(all_item_codes) > 0:
item_count = len(all_item_codes) item_count = len(all_item_codes)
else:
try:
item_count = int(request.form.get('item_count', 1))
except ValueError:
item_count = 1
if upload_mode != 'library' and not is_duplicating and not images and not duplicate_images and not book_cover_image:
error_msg = 'Bitte laden Sie mindestens ein Bild hoch'
if is_mobile:
return jsonify({'success': False, 'message': error_msg}), 400
else:
flash(error_msg, 'error')
return redirect(url_for(success_redirect_endpoint))
# Check unique codes
for code in all_item_codes: for code in all_item_codes:
if not it.is_code_unique(code): if not it.is_code_unique(code):
app.logger.info(f"DEBUG: Code '{code}' is not unique.") app.logger.info(f"DEBUG: Code '{code}' is not unique.")
@@ -5451,14 +5640,11 @@ def upload_item():
return redirect(url_for(success_redirect_endpoint)) return redirect(url_for(success_redirect_endpoint))
def generate_unique_batch_code(base_code, position): def generate_unique_batch_code(base_code, position):
"""Generate a unique code for every item in a batch if no specific code is provided."""
if not base_code: if not base_code:
return None return None
candidate = base_code if position == 1 else f"{base_code}" candidate = base_code if position == 1 else f"{base_code}"
if it.is_code_unique(candidate): if it.is_code_unique(candidate):
return candidate return candidate
suffix = 1 suffix = 1
while suffix <= 1000: while suffix <= 1000:
alternative = f"{candidate}" alternative = f"{candidate}"
@@ -5473,14 +5659,11 @@ def upload_item():
skipped_count = 0 skipped_count = 0
upload_session_id = str(uuid.uuid4())[:8] upload_session_id = str(uuid.uuid4())[:8]
app.logger.info(f"Starting image upload session {upload_session_id} - Files: {len(images)}, User: {encrypt_text(username)}") app.logger.info(
f"Starting image upload session {upload_session_id} - Files: {len(images)}, User: {encrypt_text(username)}")
# 1. Process Manual Image Uploads
for index, image in enumerate(images): for index, image in enumerate(images):
#if upload_mode == 'library':
# app.logger.info(f"[Upload {upload_session_id}] Skipping manual upload (Library Mode)")
# skipped_count += 1
# continue
if not image or not image.filename: if not image or not image.filename:
skipped_count += 1 skipped_count += 1
continue continue
@@ -5498,9 +5681,7 @@ def upload_item():
continue continue
secure_name = secure_filename(image.filename) secure_name = secure_filename(image.filename)
image.seek(0) image.seek(0)
image_bytes = image.read() image_bytes = image.read()
if not image_bytes: if not image_bytes:
@@ -5535,7 +5716,6 @@ def upload_item():
) )
image_filenames.append(new_filename) image_filenames.append(new_filename)
processed_count += 1 processed_count += 1
final_size_kb = len(optimized_io.getvalue()) / 1024 final_size_kb = len(optimized_io.getvalue()) / 1024
app.logger.info( app.logger.info(
@@ -5545,79 +5725,13 @@ def upload_item():
app.logger.error(f"{image_log_prefix} Processing failed: {str(e)}") app.logger.error(f"{image_log_prefix} Processing failed: {str(e)}")
error_count += 1 error_count += 1
app.logger.info(f"Upload session {upload_session_id} completed: {processed_count} processed, {error_count} errors, {skipped_count} skipped") # 2. Process Book Cover
# 1. Duplikate verarbeiten (Direkt via MongoDB / GridFS)
if duplicate_images:
app.logger.info(f"Processing {len(duplicate_images)} duplicate images from GridFS: {duplicate_images}")
duplicate_image_copies = []
placeholder_used = False
for i, dup_img in enumerate(duplicate_images):
try:
# Suche das Originalbild direkt in der GridFS-Datenbank
existing_file = fs.find_one({"filename": dup_img})
# Fallback: Falls der exakte Name nicht gefunden wird, suche über Regex nach dem Basisnamen
if not existing_file:
base_name = re.sub(r'_\d+(?=\.[^.]+$)', '', dup_img) # Entfernt Suffixe wie _800
base_name_no_ext = os.path.splitext(base_name)[0]
if len(base_name_no_ext) > 5:
app.logger.info(f"Trying regex search for base name: {base_name_no_ext}")
existing_file = fs.find_one({"filename": {"$regex": f"^{re.escape(base_name_no_ext)}"}})
new_filename = f"{uuid.uuid4().hex}_{int(time.time())}.webp"
if existing_file:
app.logger.info(f"Found image in GridFS for {dup_img}. Duplicating...")
# Bilddaten in den Speicher laden und direkt als neue Datei in GridFS ablegen
image_data = existing_file.read()
fs.put(
image_data,
filename=new_filename,
content_type=existing_file.content_type,
metadata={'original_duplicate_of': dup_img}
)
duplicate_image_copies.append(new_filename)
processed_count += 1
else:
app.logger.warning(f"Could not find {dup_img} in GridFS. Using placeholder.")
placeholder_path = os.path.join(app.static_folder, 'img', 'no-image.png')
if os.path.exists(placeholder_path):
with open(placeholder_path, 'rb') as pf:
placeholder_data = pf.read()
fs.put(
placeholder_data,
filename=new_filename,
content_type='image/png',
metadata={'is_placeholder': True}
)
duplicate_image_copies.append(new_filename)
placeholder_used = True
except Exception as e:
app.logger.error(f"Error duplicating image {dup_img}: {str(e)}")
error_count += 1
if placeholder_used:
app.logger.warning("Used placeholders for some missing images during duplication")
image_filenames.extend(duplicate_image_copies)
# 2. Buchcover verarbeiten
if book_cover_image: if book_cover_image:
new_filename = f"{uuid.uuid4().hex}_{int(time.time())}_book_cover.webp" new_filename = f"{uuid.uuid4().hex}_{int(time.time())}_book_cover.webp"
# Prüfen, ob das Cover noch als lokale temporäre Datei existiert (z.B. von einer API geladen)
local_path = os.path.join(app.config.get('UPLOAD_FOLDER', ''), book_cover_image) local_path = os.path.join(app.config.get('UPLOAD_FOLDER', ''), book_cover_image)
try: try:
if os.path.exists(local_path): if os.path.exists(local_path):
# Lokales Bild einlesen, zu WebP optimieren und in GridFS schieben
with Image.open(local_path) as img: with Image.open(local_path) as img:
if img.mode not in ('RGB', 'RGBA'): if img.mode not in ('RGB', 'RGBA'):
img = img.convert('RGBA') img = img.convert('RGBA')
@@ -5635,7 +5749,6 @@ def upload_item():
image_filenames.append(new_filename) image_filenames.append(new_filename)
app.logger.info(f"Processed local book cover and saved to GridFS: {new_filename}") app.logger.info(f"Processed local book cover and saved to GridFS: {new_filename}")
else: else:
# Falls es bereits in GridFS liegt, einfach duplizieren
existing_cover = fs.find_one({"filename": book_cover_image}) existing_cover = fs.find_one({"filename": book_cover_image})
if existing_cover: if existing_cover:
fs.put( fs.put(
@@ -5651,7 +5764,10 @@ def upload_item():
except Exception as e: except Exception as e:
app.logger.error(f"Error processing book cover {book_cover_image}: {str(e)}") app.logger.error(f"Error processing book cover {book_cover_image}: {str(e)}")
# 3. Item-Erstellung (Logik bleibt identisch, da it.add_item nun die GridFS-Filenames erhält) app.logger.info(
f"Upload session {upload_session_id} completed: {processed_count} processed, {error_count} errors, {skipped_count} skipped")
# 3. Item Creation
predefined_locations = it.get_predefined_locations() predefined_locations = it.get_predefined_locations()
if ort and ort not in predefined_locations: if ort and ort not in predefined_locations:
it.add_predefined_location(ort) it.add_predefined_location(ort)
@@ -5677,7 +5793,6 @@ def upload_item():
parent_item_id = str(created_item_ids[0]) if created_item_ids else None parent_item_id = str(created_item_ids[0]) if created_item_ids else None
# image_filenames enthält jetzt ausschließlich GridFS-Filenames (oder ObjectIds, je nach deinem Setup)
item_id = it.add_item( item_id = it.add_item(
name, ort, beschreibung, image_filenames, filter_upload, name, ort, beschreibung, image_filenames, filter_upload,
filter_upload2, filter_upload3, filter_upload2, filter_upload3,
@@ -5723,8 +5838,8 @@ def upload_item():
'codes': created_item_ids and [it.get_item(cid).get('Code_4') for cid in created_item_ids] or [] 'codes': created_item_ids and [it.get_item(cid).get('Code_4') for cid in created_item_ids] or []
} }
) )
except Exception: except Exception as e:
app.logger.warning('Audit write failed for library_item_created') app.logger.warning(f"Audit write failed for library_item_created: {str(e)}")
flash(success_msg, 'success') flash(success_msg, 'success')
return redirect(url_for(success_redirect_endpoint)) return redirect(url_for(success_redirect_endpoint))
+101 -158
View File
@@ -4,59 +4,60 @@
{% block content %} {% block content %}
<style> <style>
/* The Dark Background Overlay */ /* =========================================
1. Modals & Overlays
========================================= */
.modal { .modal {
display: none; /* Managed by JS (none/flex) */
position: fixed; position: fixed;
top: 0; inset: 0; /* Modern shorthand for top: 0, right: 0, bottom: 0, left: 0 */
left: 0; z-index: 1000;
width: 100vw;
height: 100vh;
background-color: rgba(0, 0, 0, 0.6); background-color: rgba(0, 0, 0, 0.6);
/* Display is managed by JS (none/flex) */
align-items: center; align-items: center;
justify-content: center; justify-content: center;
z-index: 1000; padding: 20px;
} }
/* The White Modal Box */
.modal-content { .modal-content {
background-color: #fff; position: relative;
padding: 20px;
width: 80%;
max-width: 600px;
border-radius: 8px;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
/* Flexbox allows the header to stay fixed while the body scrolls */
display: flex; display: flex;
flex-direction: column; flex-direction: column;
max-height: 85vh; width: 100%;
max-width: 900px; /* Consolidated from conflicting 600px/900px rules */
max-height: 90vh;
background-color: var(--ui-surface, #fff);
border-radius: 8px;
padding: 20px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
overflow-y: auto;
} }
/* The Close Button */
.close { .close {
align-self: flex-end; position: absolute;
top: 15px;
right: 20px;
color: #aaa; color: #aaa;
font-size: 28px; font-size: 28px;
font-weight: bold; font-weight: bold;
cursor: pointer;
line-height: 1; line-height: 1;
margin-bottom: 10px; cursor: pointer;
transition: color 0.2s ease;
} }
.close:hover, .close:hover,
.close:focus { .close:focus {
color: #000; color: var(--ui-text, #000);
text-decoration: none; text-decoration: none;
} }
/* The Scrollable Content Area */
#detailContent { #detailContent {
overflow-y: auto; overflow-y: auto;
padding-right: 10px; padding-right: 10px;
} }
/* Library table-only view styles */ /* =========================================
2. Layout & Headers
========================================= */
.library-table-container { .library-table-container {
max-width: 1400px; max-width: 1400px;
margin: 0 auto; margin: 0 auto;
@@ -79,13 +80,15 @@
font-size: 0.95em; font-size: 0.95em;
} }
/* Library search bar */ /* =========================================
3. Search, Filter & Scan Panels
========================================= */
.library-search-bar { .library-search-bar {
margin-bottom: 20px;
display: flex; display: flex;
gap: 10px;
flex-wrap: wrap; flex-wrap: wrap;
align-items: center; align-items: center;
gap: 10px;
margin-bottom: 20px;
} }
.library-search-input { .library-search-input {
@@ -95,10 +98,9 @@
border: 1px solid #ddd; border: 1px solid #ddd;
border-radius: 6px; border-radius: 6px;
font-size: 16px; font-size: 16px;
background: white; background: #fff;
} }
/* Scanner panel */
.library-scan-panel { .library-scan-panel {
background: var(--ui-surface); background: var(--ui-surface);
border: 1px solid #d9dde4; border: 1px solid #d9dde4;
@@ -129,26 +131,18 @@
color: #4b5563; color: #4b5563;
} }
.library-scan-status.ok { .library-scan-status.ok { color: #1f7a38; }
color: #1f7a38; .library-scan-status.warn { color: #9a6700; }
} .library-scan-status.error { color: #b42318; }
.library-scan-status.warn {
color: #9a6700;
}
.library-scan-status.error {
color: #b42318;
}
.library-scan-reader-wrap { .library-scan-reader-wrap {
display: none; display: none;
margin-top: 12px; margin-top: 12px;
max-width: 460px;
background: var(--ui-surface);
border: 1px solid #d9dde4; border: 1px solid #d9dde4;
border-radius: 8px; border-radius: 8px;
overflow: hidden; overflow: hidden;
background: var(--ui-surface);
max-width: 460px;
} }
.library-scan-reader { .library-scan-reader {
@@ -156,14 +150,15 @@
min-height: 280px; min-height: 280px;
} }
/* Filters */
.library-filter-toggle-btn { .library-filter-toggle-btn {
padding: 10px 16px; padding: 10px 16px;
background: #f0f2f5; background: #f0f2f5;
border: 1px solid #ddd; border: 1px solid #ddd;
border-radius: 6px; border-radius: 6px;
cursor: pointer;
font-weight: 500; font-weight: 500;
color: var(--ui-text); color: var(--ui-text);
cursor: pointer;
transition: all 0.2s ease; transition: all 0.2s ease;
} }
@@ -178,10 +173,9 @@
color: #4f46e5; color: #4f46e5;
} }
/* Filter panel */
.library-filter-panel { .library-filter-panel {
display: none; display: none;
background: white; background: #fff;
border: 1px solid #ddd; border: 1px solid #ddd;
border-radius: 8px; border-radius: 8px;
padding: 20px; padding: 20px;
@@ -210,10 +204,10 @@
} }
.filter-item label { .filter-item label {
margin-bottom: 6px;
font-weight: 600; font-weight: 600;
font-size: 0.9em; font-size: 0.9em;
color: var(--ui-text); color: var(--ui-text);
margin-bottom: 6px;
} }
.filter-item input, .filter-item input,
@@ -222,7 +216,8 @@
border: 1px solid #ddd; border: 1px solid #ddd;
border-radius: 4px; border-radius: 4px;
font-size: 14px; font-size: 14px;
background: white; background: #fff;
transition: border-color 0.2s ease, box-shadow 0.2s ease;
} }
.filter-item input:focus, .filter-item input:focus,
@@ -236,8 +231,8 @@
display: flex; display: flex;
gap: 10px; gap: 10px;
margin-top: 15px; margin-top: 15px;
border-top: 1px solid #eee;
padding-top: 15px; padding-top: 15px;
border-top: 1px solid #eee;
} }
.filter-buttons .button { .filter-buttons .button {
@@ -246,11 +241,13 @@
font-size: 14px; font-size: 14px;
} }
/* Library table */ /* =========================================
4. Tables & Data Display
========================================= */
.library-items-table { .library-items-table {
width: 100%; width: 100%;
background: #fff;
border-collapse: collapse; border-collapse: collapse;
background: white;
border: 1px solid #ddd; border: 1px solid #ddd;
border-radius: 8px; border-radius: 8px;
overflow: hidden; overflow: hidden;
@@ -266,8 +263,8 @@
padding: 14px 16px; padding: 14px 16px;
text-align: left; text-align: left;
font-weight: 600; font-weight: 600;
color: var(--ui-text);
font-size: 0.9em; font-size: 0.9em;
color: var(--ui-text);
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 0.5px; letter-spacing: 0.5px;
} }
@@ -288,7 +285,6 @@
border-bottom: none; border-bottom: none;
} }
/* Table cells */
.table-title { .table-title {
font-weight: 600; font-weight: 600;
color: var(--ui-text); color: var(--ui-text);
@@ -302,26 +298,14 @@
font-weight: 500; font-weight: 500;
} }
.status-available { .status-available { background: #e8f5e9; color: #2e7d32; }
background: #e8f5e9; .status-borrowed { background: #fff3e0; color: #e65100; }
color: #2e7d32; .status-damaged { background: #fee2e2; color: #991b1b; }
}
.status-borrowed {
background: #fff3e0;
color: #e65100;
}
.status-damaged {
background: #fee2e2;
color: #991b1b;
}
/* Actions */
.table-actions { .table-actions {
display: flex; display: flex;
gap: 8px;
flex-wrap: wrap; flex-wrap: wrap;
gap: 8px;
} }
.table-actions .button { .table-actions .button {
@@ -330,26 +314,25 @@
white-space: nowrap; white-space: nowrap;
} }
/* Empty state */
.library-empty-state { .library-empty-state {
text-align: center;
padding: 60px 20px; padding: 60px 20px;
text-align: center;
color: #999; color: #999;
} }
.library-empty-state h3 { .library-empty-state h3 {
font-size: 1.3em;
margin-bottom: 10px; margin-bottom: 10px;
font-size: 1.3em;
color: #666; color: #666;
} }
.library-load-row { .library-load-row {
margin-top: 12px;
display: flex; display: flex;
flex-wrap: wrap;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
gap: 12px; gap: 12px;
flex-wrap: wrap; margin-top: 12px;
} }
.library-load-hint { .library-load-hint {
@@ -357,54 +340,9 @@
color: #6b7280; color: #6b7280;
} }
/* Small confirmation popup (toast) */ /* =========================================
.small-popup { 5. Edit Grid Forms
position: fixed; ========================================= */
bottom: 24px;
left: 50%;
transform: translateX(-50%);
background: rgba(17, 24, 39, 0.96);
color: #fff;
padding: 12px 16px;
border-radius: 8px;
box-shadow: 0 6px 24px rgba(2,6,23,0.6);
z-index: 2000;
display: flex;
gap: 10px;
align-items: center;
max-width: 90%;
font-size: 0.95em;
}
.small-popup.ok { background: rgba(16,185,129,0.95); color: #032; }
.small-popup.error { background: rgba(239,68,68,0.95); color: #210; }
.small-popup .close-x { margin-left: 8px; cursor: pointer; font-weight: 700; }
/* Modal styles */
.modal {
display: none;
position: fixed;
z-index: 1000;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
align-items: center;
justify-content: center;
padding: 20px;
}
.modal-content {
background-color: var(--ui-surface);
border-radius: 8px;
padding: 20px;
max-width: 900px;
max-height: 90vh;
overflow-y: auto;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
position: relative;
}
.edit-grid { .edit-grid {
display: grid; display: grid;
grid-template-columns: 1fr 1fr; grid-template-columns: 1fr 1fr;
@@ -416,10 +354,10 @@
} }
.edit-grid label { .edit-grid label {
display: block;
margin-bottom: 4px;
font-weight: 600; font-weight: 600;
font-size: 0.9em; font-size: 0.9em;
margin-bottom: 4px;
display: block;
} }
.edit-grid input, .edit-grid input,
@@ -437,37 +375,44 @@
margin-top: 14px; margin-top: 14px;
} }
.close { /* =========================================
position: absolute; 6. Notifications / Toasts
right: 20px; ========================================= */
top: 10px; .small-popup {
font-size: 28px; position: fixed;
font-weight: bold; bottom: 24px;
color: #999; left: 50%;
z-index: 2000;
display: flex;
align-items: center;
gap: 10px;
max-width: 90%;
padding: 12px 16px;
background: rgba(17, 24, 39, 0.96);
color: #fff;
border-radius: 8px;
font-size: 0.95em;
transform: translateX(-50%);
box-shadow: 0 6px 24px rgba(2,6,23,0.6);
}
.small-popup.ok { background: rgba(16,185,129,0.95); color: #032; }
.small-popup.error { background: rgba(239,68,68,0.95); color: #210; }
.small-popup .close-x {
margin-left: 8px;
font-weight: 700;
cursor: pointer; cursor: pointer;
} }
.close:hover { /* =========================================
color: var(--ui-text); 7. Mobile Responsiveness
} ========================================= */
/* Mobile responsive */
@media (max-width: 768px) { @media (max-width: 768px) {
.library-table-container { .library-table-container { padding: 10px; }
padding: 10px; .library-search-bar { flex-direction: column; }
} .library-search-input { width: 100%; }
.library-items-table { font-size: 0.85em; }
.library-search-bar {
flex-direction: column;
}
.library-search-input {
width: 100%;
}
.library-items-table {
font-size: 0.85em;
}
.library-items-table th, .library-items-table th,
.library-items-table td { .library-items-table td {
@@ -479,17 +424,12 @@
font-size: 0.75em; font-size: 0.75em;
} }
.filter-row { .filter-row,
grid-template-columns: 1fr;
}
.library-scan-controls { .library-scan-controls {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
.library-scan-reader-wrap { .library-scan-reader-wrap { max-width: 100%; }
max-width: 100%;
}
} }
</style> </style>
@@ -877,11 +817,14 @@
if (readerWrap) readerWrap.style.display = 'block'; if (readerWrap) readerWrap.style.display = 'block';
setScanStatus('Initializing camera...', 'warn'); setScanStatus('Initializing camera...', 'warn');
const scannerContainer = document.querySelector('.library-scan-reader-wrap');
scannerContainer.style.display = 'block';
Quagga.init({ Quagga.init({
inputStream: { inputStream: {
name: "Live", name: "Live",
type: "LiveStream", type: "LiveStream",
target: document.querySelector('#library-scanner-container'), target: document.querySelector('.library-scan-reader'),
constraints: { constraints: {
width: 640, width: 640,
height: 480, height: 480,
+10
View File
@@ -232,6 +232,16 @@
<h1>📚 Bibliotheksausweise (Bibliothek)</h1> <h1>📚 Bibliotheksausweise (Bibliothek)</h1>
</div> </div>
<div class="export-buttons"> <div class="export-buttons">
<form method="GET" action="{{ url_for('student_card_class_barcode_download') }}" style="display: inline-flex; gap: 5px; align-items: center; background: white; padding: 2px; border-radius: 4px; border: 1px solid #ddd;">
<select name="class_name" required style="border: none; padding: 8px; outline: none; font-size: 14px; background: transparent; cursor: pointer;">
<option value="" disabled selected>-- Klasse wählen --</option>
{% for cls in available_classes %}
<option value="{{ cls }}">{{ cls }}</option>
{% endfor %}
</select>
<button type="submit" class="btn-print" style="background: #17a2b8; padding: 8px 12px; margin: 0;">📤 PDF</button>
</form>
<a href="{{ url_for('student_card_barcode_download') }}" class="btn-print" style="background: #28a745;">📥 Alle Ausweise (PDF)</a> <a href="{{ url_for('student_card_barcode_download') }}" class="btn-print" style="background: #28a745;">📥 Alle Ausweise (PDF)</a>
<a href="{{ url_for('library_admin') }}" class="btn btn-primary">← Zur Bibliotheks-Upload</a> <a href="{{ url_for('library_admin') }}" class="btn btn-primary">← Zur Bibliotheks-Upload</a>
</div> </div>
+9 -23
View File
@@ -836,7 +836,7 @@
<!-- Options will be loaded by JavaScript --> <!-- Options will be loaded by JavaScript -->
</select> </select>
</div> </div>
<div class="formupload_admin-group"> <div class="form-group">
<label for="filter2-4">Wert 4:</label> <label for="filter2-4">Wert 4:</label>
<select id="filter2-4" name="filter2" class="filter-dropdown-select"> <select id="filter2-4" name="filter2" class="filter-dropdown-select">
<option value="">-- Optional --</option> <option value="">-- Optional --</option>
@@ -876,11 +876,11 @@
</div> </div>
<!-- Image upload (hidden for library mode) --> <!-- Image upload -->
<div class="form-group"> <div class="form-group">
<label for="images">Bilder/Videos:</label> <label for="images">Bilder:</label>
<input type="file" id="images" name="images" accept=".jpg, .jpeg, .png, .gif, .mp4, .mov, .avi, .mkv, .webm, .flv, .m4v, .3gp" multiple> <input type="file" id="images" name="images" accept=".jpg, .jpeg, .png, .gif" multiple>
<div class="allowed-formats">Erlaubte Formate: JPG, JPEG, PNG, GIF, MP4, MOV, AVI, MKV, WEBM, FLV, M4V, 3GP</div> <div class="allowed-formats">Erlaubte Formate: JPG, JPEG, PNG, GIF</div>
<!-- Add image preview area --> <!-- Add image preview area -->
<div class="image-preview-container" id="image-preview-container"></div> <div class="image-preview-container" id="image-preview-container"></div>
</div> </div>
@@ -1864,21 +1864,18 @@
const files = e.target.files; const files = e.target.files;
// Validate file types before preview // Validate file types before preview (Strictly Images)
const allowedTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/gif', const allowedTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/gif'];
'video/mp4', 'video/quicktime', 'video/x-msvideo', 'video/x-matroska',
'video/webm', 'video/x-flv', 'video/mp4', 'video/3gpp'];
let hasInvalidFile = false;
for (let i = 0; i < files.length; i++) { for (let i = 0; i < files.length; i++) {
if (!allowedTypes.includes(files[i].type)) { if (!allowedTypes.includes(files[i].type)) {
hasInvalidFile = true;
// Clear the file input to prevent submission // Clear the file input to prevent submission
imageInput.value = ''; imageInput.value = '';
previewContainer.innerHTML = '<div class="error-message">Fehler: Datei "' + files[i].name + '" hat ein nicht unterstütztes Format. Erlaubte Formate: JPG, JPEG, PNG, GIF, MP4, MOV, AVI, MKV, WEBM, FLV, M4V, 3GP</div>'; previewContainer.innerHTML = '<div class="error-message">Fehler: Datei "' + files[i].name + '" hat ein nicht unterstütztes Format. Erlaubte Formate: JPG, JPEG, PNG, GIF</div>';
return; // Stop processing return; // Stop processing
} }
} }
for (let i = 0; i < files.length; i++) { for (let i = 0; i < files.length; i++) {
const file = files[i]; const file = files[i];
if (file.type.startsWith('image/')) { if (file.type.startsWith('image/')) {
@@ -1893,17 +1890,6 @@
previewContainer.appendChild(preview); previewContainer.appendChild(preview);
}; };
reader.readAsDataURL(file); reader.readAsDataURL(file);
} else if (file.type.startsWith('video/')) {
const preview = document.createElement('div');
preview.className = 'image-preview video-preview';
preview.innerHTML = `
<div class="video-placeholder">
<div class="video-icon">🎥</div>
<div class="video-name">${file.name}</div>
</div>
<button type="button" class="remove-image" onclick="removeImagePreview(this, ${i})">×</button>
`;
previewContainer.appendChild(preview);
} }
} }
}); });