diff --git a/app/main.py b/app/main.py
index 2ea2be1..dacc0e2 100755
--- a/app/main.py
+++ b/app/main.py
@@ -4,8 +4,9 @@ import smtplib
from email.mime.text import MIMEText
from xml.etree.ElementTree import Element, SubElement, tostring
+from bibapi import catalogue
from fastapi import FastAPI, Form, HTTPException, Request, status
-from fastapi.responses import HTMLResponse, RedirectResponse
+from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
@@ -15,6 +16,9 @@ templates = Jinja2Templates(directory="app/templates")
# Serve static files (CSS, images)
app.mount("/static", StaticFiles(directory="app/static"), name="static")
+# Initialize catalogue for signature validation
+cat = catalogue.Catalogue()
+
# add somewhere near the top-level constants
EMAIL_REGEX = re.compile(r"^[^@\s]+@[^@\s]+\.[^@\s]+$")
@@ -44,6 +48,39 @@ async def elsa_form(request: Request):
return templates.TemplateResponse("elsa_mono_form.html", {"request": request})
+@app.get("/api/validate-signature")
+async def validate_signature(signature: str):
+ """Validate a book signature and return total pages"""
+ try:
+ book_result = cat.get_book_with_data(signature)
+ if book_result and hasattr(book_result, "pages") and book_result.pages:
+ # Try to extract numeric page count
+ pages_str = str(book_result.pages)
+ # Extract first number from pages string (e.g., "245 S." -> 245)
+ match = re.search(r"(\d+)", pages_str)
+ if match:
+ total_pages = int(match.group(1))
+ return JSONResponse(
+ {"valid": True, "total_pages": total_pages, "signature": signature}
+ )
+
+ return JSONResponse(
+ {
+ "valid": False,
+ "error": "Signatur nicht gefunden oder keine Seitenzahl verfügbar",
+ "signature": signature,
+ }
+ )
+ except Exception as e:
+ return JSONResponse(
+ {
+ "valid": False,
+ "error": f"Fehler bei der Validierung: {str(e)}",
+ "signature": signature,
+ }
+ )
+
+
@app.post("/submit")
async def handle_form(
request: Request,
diff --git a/app/static/styles.css b/app/static/styles.css
index 33b94b8..bb489ae 100644
--- a/app/static/styles.css
+++ b/app/static/styles.css
@@ -725,3 +725,52 @@ input[type="radio"] { accent-color: var(--control-accent); }
padding: 6px 8px;
}
}
+
+/* Signature validation states */
+.signature-validating {
+ border-color: #3b82f6 !important;
+ background-color: #eff6ff !important;
+}
+
+[data-theme="dark"] .signature-validating {
+ background-color: #1e3a8a !important;
+}
+
+.signature-valid {
+ border-color: #22c55e !important;
+ background-color: #f0fdf4 !important;
+}
+
+[data-theme="dark"] .signature-valid {
+ background-color: #14532d !important;
+}
+
+.signature-invalid {
+ border-color: #ef4444 !important;
+ background-color: #fef2f2 !important;
+}
+
+[data-theme="dark"] .signature-invalid {
+ background-color: #7f1d1d !important;
+}
+
+/* Threshold exceeded - entire row in red */
+.threshold-exceeded {
+ background-color: #fee2e2 !important;
+}
+
+[data-theme="dark"] .threshold-exceeded {
+ background-color: #991b1b !important;
+}
+
+.threshold-exceeded input,
+.threshold-exceeded select {
+ border-color: #ef4444 !important;
+}
+
+/* Disabled submit button styling */
+button[type="submit"]:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+ background-color: #9ca3af !important;
+}
diff --git a/app/templates/elsa_mono_form.html b/app/templates/elsa_mono_form.html
index 1f1a081..7441c93 100644
--- a/app/templates/elsa_mono_form.html
+++ b/app/templates/elsa_mono_form.html
@@ -136,6 +136,12 @@
};
let sectionCounter = 0;
+
+ // Track signature validations: { signature: { totalPages: int, requestedPages: int, inputs: [elements] } }
+ let signatureTracking = {};
+
+ // Debounce timer for signature validation
+ let validationTimers = {};
// Theme toggle functionality (in IIFE to avoid polluting global scope)
(function() {
@@ -266,6 +272,7 @@
'
| ' +
' | ' +
' | ';
+ attachSignatureListeners(row);
} else if (type === 'zeitschriftenartikel') {
row.innerHTML = ' | ' +
' | ' +
@@ -287,13 +294,19 @@
' | ' +
' | ' +
' | ';
+ attachSignatureListeners(row);
}
}
function removeRow(rowId) {
const row = document.getElementById(rowId);
if (row) {
+ const signatureInput = row.querySelector('input[name*="_signature"]');
+ if (signatureInput) {
+ cleanupSignatureTracking(signatureInput);
+ }
row.remove();
+ updateSubmitButton();
}
}
@@ -301,10 +314,202 @@
const section = document.getElementById(sectionId);
if (section) {
if (confirm('Möchten Sie diese Sektion wirklich entfernen?')) {
+ // Clean up tracking for removed rows
+ const rows = section.querySelectorAll('tr[id]');
+ rows.forEach(row => {
+ const signatureInput = row.querySelector('input[name*="_signature"]');
+ if (signatureInput) {
+ cleanupSignatureTracking(signatureInput);
+ }
+ });
section.remove();
+ updateSubmitButton();
}
}
}
+
+ function validateSignature(signatureInput, pagesFromInput, pagesToInput) {
+ const signature = signatureInput.value.trim();
+
+ if (!signature) {
+ // Clear validation state if signature is empty
+ signatureInput.classList.remove('signature-validating', 'signature-valid', 'signature-invalid');
+ return;
+ }
+
+ // Clear any existing timer for this input
+ const inputId = signatureInput.id || signatureInput.name;
+ if (validationTimers[inputId]) {
+ clearTimeout(validationTimers[inputId]);
+ }
+
+ // Show validating state
+ signatureInput.classList.add('signature-validating');
+ signatureInput.classList.remove('signature-valid', 'signature-invalid');
+
+ // Debounce the API call
+ validationTimers[inputId] = setTimeout(async () => {
+ try {
+ const response = await fetch('/api/validate-signature?signature=' + encodeURIComponent(signature));
+ const data = await response.json();
+
+ if (data.valid) {
+ // Initialize tracking for this signature if needed
+ if (!signatureTracking[signature]) {
+ signatureTracking[signature] = {
+ totalPages: data.total_pages,
+ inputs: []
+ };
+ } else {
+ signatureTracking[signature].totalPages = data.total_pages;
+ }
+
+ // Track this input
+ const existingIndex = signatureTracking[signature].inputs.findIndex(
+ item => item.signature === signatureInput
+ );
+ if (existingIndex === -1) {
+ signatureTracking[signature].inputs.push({
+ signature: signatureInput,
+ pagesFrom: pagesFromInput,
+ pagesTo: pagesToInput
+ });
+ }
+
+ signatureInput.classList.remove('signature-validating');
+ signatureInput.classList.add('signature-valid');
+ signatureInput.title = 'Signatur gefunden: ' + data.total_pages + ' Seiten';
+
+ // Recalculate all pages for this signature
+ checkSignatureThreshold(signature);
+ } else {
+ signatureInput.classList.remove('signature-validating');
+ signatureInput.classList.add('signature-invalid');
+ signatureInput.title = data.error || 'Signatur nicht gefunden';
+ updateSubmitButton();
+ }
+ } catch (error) {
+ signatureInput.classList.remove('signature-validating');
+ signatureInput.classList.add('signature-invalid');
+ signatureInput.title = 'Validierungsfehler';
+ updateSubmitButton();
+ }
+ }, 800);
+ }
+
+ function checkSignatureThreshold(signature) {
+ const tracking = signatureTracking[signature];
+ if (!tracking) return;
+
+ let totalRequestedPages = 0;
+ const threshold = Math.ceil(tracking.totalPages * 0.15);
+
+ // Calculate total requested pages across all inputs for this signature
+ tracking.inputs.forEach(item => {
+ const from = parseInt(item.pagesFrom.value) || 0;
+ const to = parseInt(item.pagesTo.value) || 0;
+ if (from > 0 && to > 0 && to >= from) {
+ totalRequestedPages += (to - from + 1);
+ }
+ });
+
+ const isOverThreshold = totalRequestedPages > threshold;
+
+ // Update all rows with this signature
+ tracking.inputs.forEach(item => {
+ const row = item.signature.closest('tr');
+ if (row) {
+ if (isOverThreshold) {
+ row.classList.add('threshold-exceeded');
+ item.signature.title = 'Warnung: Gesamtanzahl der Seiten (' + totalRequestedPages + ') überschreitet 15% (' + threshold + ' Seiten)';
+ } else {
+ row.classList.remove('threshold-exceeded');
+ item.signature.title = 'Signatur gefunden: ' + tracking.totalPages + ' Seiten (Aktuell: ' + totalRequestedPages + '/' + threshold + ')';
+ }
+ }
+ });
+
+ updateSubmitButton();
+ }
+
+ function cleanupSignatureTracking(signatureInput) {
+ const signature = signatureInput.value.trim();
+ if (signature && signatureTracking[signature]) {
+ signatureTracking[signature].inputs = signatureTracking[signature].inputs.filter(
+ item => item.signature !== signatureInput
+ );
+
+ // Remove signature from tracking if no more inputs
+ if (signatureTracking[signature].inputs.length === 0) {
+ delete signatureTracking[signature];
+ } else {
+ // Recalculate for remaining inputs
+ checkSignatureThreshold(signature);
+ }
+ }
+ }
+
+ function updateSubmitButton() {
+ const submitButton = document.querySelector('button[type="submit"]');
+ const hasInvalidSignatures = document.querySelectorAll('.signature-invalid').length > 0;
+ const hasThresholdExceeded = document.querySelectorAll('.threshold-exceeded').length > 0;
+
+ if (hasInvalidSignatures || hasThresholdExceeded) {
+ submitButton.disabled = true;
+ if (hasThresholdExceeded) {
+ submitButton.title = 'Formular kann nicht abgesendet werden: 15%-Grenze überschritten';
+ } else {
+ submitButton.title = 'Formular kann nicht abgesendet werden: Ungültige Signaturen';
+ }
+ } else {
+ submitButton.disabled = false;
+ submitButton.title = '';
+ }
+ }
+
+ function attachSignatureListeners(row) {
+ const signatureInput = row.querySelector('input[name*="_signature"]');
+ const pagesFromInput = row.querySelector('input[name*="_pages_from"]');
+ const pagesToInput = row.querySelector('input[name*="_pages_to"]');
+
+ if (signatureInput && pagesFromInput && pagesToInput) {
+ // Generate unique ID if not present
+ if (!signatureInput.id) {
+ signatureInput.id = 'sig-' + Date.now() + '-' + Math.random().toString(36).substr(2, 9);
+ }
+
+ // Validate on signature change
+ signatureInput.addEventListener('input', () => {
+ const oldSignature = signatureInput.dataset.lastSignature || '';
+ const newSignature = signatureInput.value.trim();
+
+ // Cleanup old signature tracking if it changed
+ if (oldSignature && oldSignature !== newSignature) {
+ const tempInput = document.createElement('input');
+ tempInput.value = oldSignature;
+ cleanupSignatureTracking(tempInput);
+ }
+
+ signatureInput.dataset.lastSignature = newSignature;
+ validateSignature(signatureInput, pagesFromInput, pagesToInput);
+ });
+
+ // Recalculate when page range changes
+ pagesFromInput.addEventListener('input', () => {
+ const signature = signatureInput.value.trim();
+ if (signature && signatureTracking[signature]) {
+ checkSignatureThreshold(signature);
+ }
+ });
+
+ pagesToInput.addEventListener('input', () => {
+ const signature = signatureInput.value.trim();
+ if (signature && signatureTracking[signature]) {
+ checkSignatureThreshold(signature);
+ }
+ });
+ }
+ }