Compare commits

...

26 Commits

Author SHA1 Message Date
Aiirondev_dev 1b9bfe1b14 Fix of the edeting funktion in the Library Module 2026-06-10 11:24:33 +02:00
Aiirondev_dev 3cdd1552d9 Fix of a library funktion 2026-06-10 11:08:43 +02:00
Aiirondev_dev 80a206f524 Finisched the removal of the Author 2026-06-07 23:16:05 +02:00
Aiirondev_dev 3217da8b15 Improved ISBN Funktionality 2026-06-07 22:57:32 +02:00
Aiirondev_dev 22bc7c9c3c upload barcode implementation 2026-06-07 22:28:04 +02:00
Aiirondev_dev f156cc178a library author removing 2026-06-07 22:21:01 +02:00
Aiirondev_dev 116261d334 Slight fixes of thew Code scanning and Items processing 2026-06-07 12:42:27 +02:00
Aiirondev_dev 042afc19e2 slight changes to the Bar code scanner 2026-06-07 12:33:13 +02:00
Aiirondev_dev 28d049b52a smal changes fixes for the library Code Scanner 2026-06-07 00:14:57 +02:00
Aiirondev_dev 8e478aeba4 slight fixes 2026-06-07 00:03:21 +02:00
Aiirondev_dev 1b38d1f0c8 changed the wrapper of the new code scanning library 2026-06-06 23:52:19 +02:00
Aiirondev_dev e7e24b3fae further changes to the new Barcode system 2026-06-06 23:41:37 +02:00
Aiirondev_dev fc80857fbe minor changes to the Inpout scanning 2026-06-06 23:31:50 +02:00
Aiirondev_dev f763ad064d Changes from the deprecated qrcode scanning tool, to a more modern option in the library 2026-06-06 22:46:50 +02:00
Aiirondev_dev bc94ffdc18 further fixes 2026-06-06 17:17:15 +02:00
Aiirondev_dev 76e9670cee implementation of a new Framework for the Barcode scanning via Quagga2 -> only test implementation 2026-06-06 16:53:11 +02:00
Aiirondev_dev 577c6c4bed Removing more of the broken funktionaliti 2026-06-06 15:39:51 +02:00
Aiirondev_dev 3aa19b10d1 Reverted Faulti changes that have proven not worth implementing 2026-06-06 15:03:43 +02:00
Aiirondev_dev ff742d018e removed obsolet file 2026-06-01 14:34:05 +02:00
Aiirondev_dev 6fa40f26b0 Generall fixes, to manage the unused overhead 2026-06-01 13:23:52 +02:00
Aiirondev_dev 777fc81064 fix for the latest Tag 2026-06-01 13:20:10 +02:00
Aiirondev_dev cf5e38e319 removed development release version 2026-06-01 13:15:54 +02:00
Aiirondev_dev b76941d5fe Fix in the release versioning 2026-06-01 13:07:52 +02:00
Aiirondev_dev f46f2674b0 Library style adjustments for mobile 2026-06-01 12:51:28 +02:00
github-actions[bot] 8ebbdcfd54 chore: bump version to v0.8.31.1 2026-06-01 10:35:56 +00:00
Aiirondev_dev 92b5235035 feat: Improve mobile video playback for barcode scanning with autoplay and mute settings 2026-06-01 12:32:42 +02:00
12 changed files with 985 additions and 1662 deletions
+14 -89
View File
@@ -15,15 +15,6 @@ on:
- patch
- minor
- major
- development
push_dev:
description: "If true, push the :dev image to GHCR for development releases"
required: false
default: "false"
type: choice
options:
- "true"
- "false"
permissions:
contents: write
@@ -48,13 +39,12 @@ jobs:
EVENT_NAME: ${{ github.event_name }}
REF_NAME: ${{ github.ref_name }}
BUMP_TYPE: ${{ github.event.inputs.bump || 'patch' }}
PUSH_DEV: ${{ github.event.inputs.push_dev || 'false' }}
run: |
if [ "$EVENT_NAME" = "push" ] && [ -n "$REF_NAME" ]; then
TAG="$REF_NAME"
else
# Fetch latest release tag via GitHub API (fall back to v3.0.0)
latest_tag="v3.0.0"
# Fetch latest release tag via GitHub API (fall back to v0.8.31)
latest_tag="v0.8.31"
if meta_json=$(curl -fsSL -H "Authorization: Bearer $GH_TOKEN" -H "Accept: application/vnd.github+json" "https://api.github.com/repos/$REPO/releases/latest" 2>/dev/null); then
tag_name=$(printf "%s" "$meta_json" | sed -n 's/.*"tag_name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1)
if [ -n "$tag_name" ]; then
@@ -67,7 +57,7 @@ jobs:
minor=${BASH_REMATCH[2]}
patch=${BASH_REMATCH[3]}
else
major=3; minor=0; patch=0
major=0; minor=8; patch=31
fi
# Bump strategy: major / minor / patch
@@ -78,30 +68,28 @@ jobs:
else
patch=$((patch + 1))
fi
if [ "${BUMP_TYPE:-}" = "development" ]; then
TAG="v${major}.${minor}.${patch}-dev"
else
TAG="v${major}.${minor}.${patch}"
fi
# Zusammenbau des Tags für den manuellen Run
TAG="v${major}.${minor}.${patch}"
fi
# Validierung des erzeugten oder übergebenen Tags
if ! echo "$TAG" | grep -Eq '^v[0-9]+\.[0-9]+\.[0-9]+(-dev(\.[0-9]+)?)?$'; then
echo "Error: tag '$TAG' is not valid semver (vX.Y.Z or vX.Y.Z-dev)"
echo "Error: tag '$TAG' is not valid semver (vX.Y.Z)"
exit 1
fi
git fetch --tags --force
LATEST_TAG="$(git tag -l 'v*' | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | sort -V | tail -n1)"
if [ -z "$LATEST_TAG" ]; then
LATEST_TAG="v3.0.0"
LATEST_TAG="v0.8.31"
fi
TAG_MAJOR="${TAG#v}"
TAG_MAJOR="${TAG_MAJOR%%.*}"
LATEST_MAJOR="$(echo "$LATEST_TAG" | grep -Eo '^v[0-9]+' | tr -d 'v')"
if [ -z "$LATEST_MAJOR" ]; then
LATEST_MAJOR="3"
LATEST_MAJOR="0"
fi
# If not explicitly bumping major, disallow changing major version
@@ -123,33 +111,6 @@ jobs:
IMAGE="ghcr.io/aiirondev/legendary-octo-garbanzo:${TAG}"
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
echo "image=$IMAGE" >> "$GITHUB_OUTPUT"
if [ "${BUMP_TYPE:-}" = "development" ]; then
echo "is_development=true" >> "$GITHUB_OUTPUT"
else
echo "is_development=false" >> "$GITHUB_OUTPUT"
fi
if [ "${BUMP_TYPE:-}" = "development" ] && [ "${PUSH_DEV:-}" = "true" ]; then
echo "push_dev=true" >> "$GITHUB_OUTPUT"
else
echo "push_dev=false" >> "$GITHUB_OUTPUT"
fi
- name: Update .release-version file
run: |
echo "${{ steps.meta.outputs.tag }}" > .release-version
cat .release-version
- name: Create and push tag for manual releases
if: github.event_name == 'workflow_dispatch'
run: |
TAG="${{ steps.meta.outputs.tag }}"
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add .release-version
git commit -m "chore: bump version to $TAG" || true
git tag "$TAG"
git push origin "$TAG"
git push origin HEAD:${{ github.ref_name }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
@@ -162,26 +123,16 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push release image
if: steps.meta.outputs.is_development != 'true'
uses: docker/build-push-action@v6
with:
context: .
file: ./Dockerfile
push: true
load: true # Lädt das Image in den lokalen Docker-Daemon für den 'docker save' Schritt
tags: |
${{ steps.meta.outputs.image }}
ghcr.io/aiirondev/legendary-octo-garbanzo:latest
- name: Build and push development image (:dev)
if: steps.meta.outputs.is_development == 'true' && steps.meta.outputs.push_dev == 'true'
uses: docker/build-push-action@v6
with:
context: .
file: ./Dockerfile
push: true
tags: |
ghcr.io/aiirondev/legendary-octo-garbanzo:dev
- name: Build local image tar for offline deploy
run: |
set -euo pipefail
@@ -204,30 +155,17 @@ jobs:
docker build -t "$IMG" .
docker save "$IMG" | gzip > "inventarsystem-image-${TAG}.tar.gz"
# development tar omitted: dev releases will be versioned (vX.Y.Z-dev) and handled by update.sh using the tag
- name: Commit .release-version for tag pushes
if: github.event_name == 'push'
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
if ! git diff --quiet .release-version; then
git add .release-version
git commit -m "chore: update version to ${{ steps.meta.outputs.tag }}"
git push origin HEAD:${{ github.ref_name }}
fi
- name: Create release-only docker bundle
run: |
mkdir -p release-bundle
cat > release-bundle/docker-compose.yml <<EOF
services:
app:
image: ${INVENTAR_APP_IMAGE:-ghcr.io/aiirondev/legendary-octo-garbanzo:${{ steps.meta.outputs.tag }}}
image: \${INVENTAR_APP_IMAGE:-ghcr.io/aiirondev/legendary-octo-garbanzo:${{ steps.meta.outputs.tag }}}
container_name: inventarsystem-app
restart: unless-stopped
ports:
- "${INVENTAR_HTTP_PORT:-10000}:8000"
- "\${INVENTAR_HTTP_PORT:-10000}:8000"
depends_on:
- mongodb
- redis
@@ -300,17 +238,6 @@ jobs:
fi
done
cat > release-bundle/DEVELOPMENT.md <<EOF
This is a development prerelease bundle for tag: ${{ steps.meta.outputs.tag }}
This prerelease is intentionally separate from normal releases and will not be used by default.
To install this prerelease on a target host run:
./update.sh ${{ steps.meta.outputs.tag }}
EOF
# Make any shipped scripts executable
find release-bundle -maxdepth 1 -type f -name '*.sh' -exec chmod +x {} \; || true
tar -czf inventarsystem-docker-bundle.tar.gz -C release-bundle .
@@ -319,10 +246,8 @@ jobs:
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ steps.meta.outputs.tag }}
prerelease: ${{ steps.meta.outputs.is_development }}
files: |
inventarsystem-docker-bundle.tar.gz
inventarsystem-image-${{ steps.meta.outputs.tag }}.tar.gz
inventarsystem-image-dev.tar.gz
fail_on_unmatched_files: false
generate_release_notes: true
generate_release_notes: true
+1 -1
View File
@@ -1 +1 @@
v0.8.30
v0.8.31.1
+148 -134
View File
@@ -3484,66 +3484,6 @@ def api_library_scan_action():
client.close()
@app.route('/api/scan_upload', methods=['POST'])
def api_scan_upload():
"""
Server-side barcode/QR decoding endpoint.
Accepts image upload and returns detected barcodes (fallback for client-side scanner).
"""
if 'username' not in session:
return jsonify({'ok': False, 'message': 'Nicht angemeldet.'}), 401
try:
from pyzbar.pyzbar import decode
from PIL import Image
import io
except ImportError:
return jsonify({'ok': False, 'message': 'pyzbar library not installed on server.'}), 500
if 'image' not in request.files:
return jsonify({'ok': False, 'message': 'Keine Bilddatei bereitgestellt.'}), 400
file = request.files['image']
if file.filename == '':
return jsonify({'ok': False, 'message': 'Datei ist leer.'}), 400
try:
# Decode image
image = Image.open(io.BytesIO(file.read()))
image = image.convert('RGB')
# Detect barcodes using pyzbar
results = decode(image)
if not results:
return jsonify({'ok': False, 'message': 'Kein Barcode im Bild erkannt.'}), 400
# Return all detected barcodes, sorted by confidence (quality)
barcodes = [
{
'type': result.type,
'value': result.data.decode('utf-8'),
'rect': {
'x': result.rect.left,
'y': result.rect.top,
'width': result.rect.width,
'height': result.rect.height,
}
}
for result in results
]
return jsonify({
'ok': True,
'barcodes': barcodes,
'message': f'{len(barcodes)} Barcode(s) erkannt.'
}), 200
except Exception as e:
app.logger.error(f"Error decoding barcode image: {e}")
return jsonify({'ok': False, 'message': f'Fehler beim Dekodieren: {str(e)}'}), 500
@app.route('/api/item_detail/<item_id>')
def api_item_detail(item_id):
"""
@@ -9436,17 +9376,137 @@ def search_word(word):
except Exception as e:
return jsonify({"success": False, "response": str(e)})
def _fetch_from_google_books(clean_isbn):
"""Source 1: Google Books API (Free, No Key required for basic use)"""
try:
url = f"https://www.googleapis.com/books/v1/volumes?q=isbn:{clean_isbn}"
response = requests.get(url, timeout=5)
if response.status_code != 200:
return None
data = response.json()
if data.get('totalItems', 0) > 0 and data.get('items'):
book_info = data['items'][0].get('volumeInfo', {})
sale_info = data['items'][0].get('saleInfo', {})
price = None
retail_price = sale_info.get('retailPrice', {})
list_price = sale_info.get('listPrice', {})
if retail_price and 'amount' in retail_price:
price = f"{retail_price['amount']} {retail_price.get('currencyCode', '')}"
elif list_price and 'amount' in list_price:
price = f"{list_price['amount']} {list_price.get('currencyCode', '')}"
thumbnail = book_info.get('imageLinks', {}).get('thumbnail', '')
if thumbnail:
thumbnail = thumbnail.replace('http:', 'https:')
return {
"title": book_info.get('title', 'Unknown Title'),
"authors": ', '.join(book_info.get('authors', ['Unknown Author'])),
"publisher": book_info.get('publisher', 'Unknown Publisher'),
"publishedDate": book_info.get('publishedDate', 'Unknown Date'),
"description": book_info.get('description', 'Keine Beschreibung verfügbar'),
"pageCount": book_info.get('pageCount', 'Unknown'),
"price": price,
"thumbnail": thumbnail,
"source": "google-books"
}
except Exception as e:
print(f"Google Books error: {e}")
return None
def _fetch_from_lobid_germany(clean_isbn):
"""
Source 2: Lobid.org (hbz Network)
The ultimate free, keyless fallback for German Schoolbooks and DACH literature.
"""
try:
url = f"https://lobid.org/resources?q=isbn:{clean_isbn}&format=json"
response = requests.get(url, timeout=5)
if response.status_code != 200:
return None
data = response.json()
if 'member' in data and len(data['member']) > 0:
book = data['member'][0]
# Extract Authors (Lobid uses a nested contribution array)
authors = []
for contrib in book.get('contribution', []):
if 'agent' in contrib and 'label' in contrib['agent']:
authors.append(contrib['agent']['label'])
# Extract Publishers
publishers = []
for pub in book.get('publication', []):
if 'publishedBy' in pub:
# sometimes publishedBy is a list, sometimes a string
pub_data = pub['publishedBy']
if isinstance(pub_data, list):
publishers.extend(pub_data)
else:
publishers.append(pub_data)
# Extract Date
pub_date = 'Unknown Date'
if book.get('publication'):
pub_date = book['publication'][0].get('startDate', 'Unknown Date')
# Extract Pages
pages = book.get('extent', ['Unknown'])[0] if 'extent' in book else 'Unknown'
return {
"title": book.get('title', 'Unknown Title'),
"authors": ', '.join(authors) if authors else 'Unknown Author',
"publisher": ', '.join(publishers) if publishers else 'Unknown Publisher',
"publishedDate": str(pub_date),
"description": 'Keine Beschreibung verfügbar (Schulbuch / Fachliteratur)',
"pageCount": pages,
"price": None,
"thumbnail": "", # Lobid rarely has covers, we will fallback to OpenLibrary cover below
"source": "lobid-germany"
}
except Exception as e:
print(f"Lobid error: {e}")
return None
def _fetch_from_open_library(clean_isbn):
"""Source 3: Open Library Search API (Free, No Key required)"""
try:
url = f"https://openlibrary.org/search.json?isbn={clean_isbn}"
response = requests.get(url, timeout=5)
if response.status_code != 200:
return None
data = response.json()
if data.get('numFound', 0) > 0 and data.get('docs'):
book_info = data['docs'][0]
return {
"title": book_info.get('title', 'Unknown Title'),
"authors": ', '.join(book_info.get('author_name', ['Unknown Author'])),
"publisher": ', '.join(book_info.get('publisher', ['Unknown Publisher'])),
"publishedDate": str(book_info.get('publish_date', ['Unknown Date'])[0]),
"description": 'Keine Beschreibung verfügbar',
"pageCount": book_info.get('number_of_pages_median', 'Unknown'),
"price": None,
"thumbnail": f"https://covers.openlibrary.org/b/isbn/{clean_isbn}-L.jpg",
"source": "openlibrary"
}
except Exception as e:
print(f"OpenLibrary Search error: {e}")
return None
@app.route('/fetch_book_info/<isbn>')
def fetch_book_info(isbn):
"""
API endpoint to fetch book information by ISBN using Google Books API
Args:
isbn (str): ISBN to look up
Returns:
dict: Book information or error message
API endpoint to fetch book information by ISBN using multiple open sources.
Optimized for global literature AND German educational books.
"""
# Authorization Checks
if 'username' not in session or not us.check_admin(session['username']):
return jsonify({"error": "Not authorized"}), 403
@@ -9454,79 +9514,33 @@ def fetch_book_info(isbn):
return jsonify({"error": "Bibliotheks-Modul ist deaktiviert."}), 403
try:
# Validation
clean_isbn = normalize_and_validate_isbn(isbn)
if not clean_isbn:
return jsonify({"error": "Ungültige ISBN. Bitte ISBN-10 oder ISBN-13 verwenden."}), 400
# First source: Google Books
response = requests.get(
f"https://www.googleapis.com/books/v1/volumes?q=isbn:{clean_isbn}",
timeout=10
)
# Define the Provider Chain (Order matters: General -> German/School -> Global Fallback)
providers = [
_fetch_from_google_books,
_fetch_from_lobid_germany,
_fetch_from_open_library
]
if response.status_code == 200:
data = response.json()
if data.get('totalItems', 0) > 0 and data.get('items'):
book_info = data['items'][0].get('volumeInfo', {})
sale_info = data['items'][0].get('saleInfo', {})
price = None
retail_price = sale_info.get('retailPrice', {})
list_price = sale_info.get('listPrice', {})
if retail_price and 'amount' in retail_price:
price = f"{retail_price['amount']} {retail_price.get('currencyCode', '')}"
elif list_price and 'amount' in list_price:
price = f"{list_price['amount']} {list_price.get('currencyCode', '')}"
thumbnail = book_info.get('imageLinks', {}).get('thumbnail', '')
if thumbnail:
thumbnail = thumbnail.replace('http:', 'https:')
return jsonify({
"title": book_info.get('title', 'Unknown Title'),
"authors": ', '.join(book_info.get('authors', ['Unknown Author'])),
"publisher": book_info.get('publisher', 'Unknown Publisher'),
"publishedDate": book_info.get('publishedDate', 'Unknown Date'),
"description": book_info.get('description', 'No description available'),
"pageCount": book_info.get('pageCount', 'Unknown'),
"price": price,
"thumbnail": thumbnail,
"isbn": clean_isbn,
"source": "google-books"
})
# Fallback: OpenLibrary
ol_response = requests.get(
f"https://openlibrary.org/isbn/{clean_isbn}.json",
timeout=10
)
if ol_response.status_code == 200:
ol_data = ol_response.json()
author_names = []
for author_ref in ol_data.get('authors', []):
key = author_ref.get('key')
if not key:
continue
try:
author_resp = requests.get(f"https://openlibrary.org{key}.json", timeout=8)
if author_resp.status_code == 200:
author_names.append(author_resp.json().get('name'))
except Exception:
continue
return jsonify({
"title": ol_data.get('title', 'Unknown Title'),
"authors": ', '.join([a for a in author_names if a]) if author_names else 'Unknown Author',
"publisher": ', '.join(ol_data.get('publishers', [])) if ol_data.get('publishers') else 'Unknown Publisher',
"publishedDate": ol_data.get('publish_date', 'Unknown Date'),
"description": (ol_data.get('description', {}).get('value') if isinstance(ol_data.get('description'), dict) else ol_data.get('description')) or 'No description available',
"pageCount": ol_data.get('number_of_pages', 'Unknown'),
"price": None,
"thumbnail": f"https://covers.openlibrary.org/b/isbn/{clean_isbn}-L.jpg",
"isbn": clean_isbn,
"source": "openlibrary"
})
# Iterate through providers until a book is found
for provider in providers:
book_data = provider(clean_isbn)
if book_data:
# Add the ISBN back into the response payload
book_data["isbn"] = clean_isbn
# If a provider found the book but had no cover, assign a default OpenLibrary cover fallback
if not book_data.get("thumbnail"):
book_data["thumbnail"] = f"https://covers.openlibrary.org/b/isbn/{clean_isbn}-L.jpg"
return jsonify(book_data)
# If all providers fail
return jsonify({"error": f"Kein Buch zu dieser ISBN gefunden: {clean_isbn}"}), 404
except Exception as e:
-1
View File
@@ -15,4 +15,3 @@ openpyxl
cryptography>=42.0.0
pywebpush
py-vapid>=1.9.0
pyzbar
-328
View File
@@ -1,328 +0,0 @@
/**
* Hybrid Barcode Scanner
* Supports: Real-time camera scanning (ZXing.js), image upload fallback (server-side pyzbar),
* and USB/keyboard scanner emulation.
*/
class HybridScanner {
constructor(options = {}) {
this.options = {
videoId: options.videoId || 'scanner-video',
canvasId: options.canvasId || 'scanner-canvas',
formats: options.formats || ['QR_CODE', 'CODE_128', 'EAN_13', 'ISBN', 'CODE_39', 'UPC_A'],
fps: options.fps || 10,
onSuccess: options.onSuccess || (() => {}),
onError: options.onError || (() => {}),
facingMode: options.facingMode || 'environment', // 'environment' for rear, 'user' for front
};
this.state = {
isRunning: false,
stream: null,
reader: null,
canvas: null,
videoElement: null,
zxingReady: false,
lastScanTime: 0,
scanDebounceMs: 300, // Prevent rapid duplicate scans
};
this.keyboardInputBuffer = '';
this.keyboardInputTimeout = null;
}
/**
* Initialize ZXing library from CDN
*/
async initZXing() {
if (this.state.zxingReady) {
return true;
}
return new Promise((resolve) => {
// Load ZXing library from CDN
if (typeof ZXing !== 'undefined') {
this.state.zxingReady = true;
resolve(true);
return;
}
// Use fallback URL for better mobile compatibility
let scriptUrl = 'https://unpkg.com/@zxing/library@latest/umd/index.min.js';
// Check if already loading
const existingScript = document.querySelector(`script[src="${scriptUrl}"]`);
if (existingScript) {
existingScript.addEventListener('load', () => {
this.state.zxingReady = typeof ZXing !== 'undefined';
resolve(this.state.zxingReady);
}, { once: true });
existingScript.addEventListener('error', () => {
console.error('Failed to load ZXing library');
resolve(false);
}, { once: true });
return;
}
const script = document.createElement('script');
script.src = scriptUrl;
script.async = true;
script.onload = () => {
this.state.zxingReady = typeof ZXing !== 'undefined';
resolve(this.state.zxingReady);
};
script.onerror = () => {
console.error('Failed to load ZXing library');
resolve(false);
};
document.head.appendChild(script);
});
}
/**
* Start real-time barcode scanning via camera
*/
async start() {
if (this.state.isRunning) {
return;
}
try {
// Ensure ZXing is loaded
const zxingReady = await this.initZXing();
if (!zxingReady) {
this.options.onError('ZXing library failed to load. Please use image upload fallback.');
return;
}
// Get DOM elements
this.state.videoElement = document.getElementById(this.options.videoId);
this.state.canvas = document.getElementById(this.options.canvasId);
if (!this.state.videoElement || !this.state.canvas) {
this.options.onError('Scanner video or canvas element not found.');
return;
}
// Request camera access
const stream = await navigator.mediaDevices.getUserMedia({
video: {
facingMode: this.options.facingMode,
width: { ideal: 1280 },
height: { ideal: 720 },
},
audio: false,
});
this.state.stream = stream;
this.state.videoElement.srcObject = stream;
// Make video element visible (ensure it's not hidden by display: none)
this.state.videoElement.style.display = 'block';
// Initialize ZXing reader
const hints = new Map();
hints.set(ZXing.DecodeHintType.POSSIBLE_FORMATS, this.options.formats);
hints.set(ZXing.DecodeHintType.TRY_HARDER, true);
this.state.reader = new ZXing.BrowserMultiFormatReader(hints);
this.state.isRunning = true;
// Start scanning loop
this.scanLoop();
// Setup keyboard input fallback for USB scanners
this.setupKeyboardInput();
} catch (err) {
this.options.onError(`Camera error: ${err.message || err}`);
}
}
/**
* Stop barcode scanning
*/
stop() {
if (!this.state.isRunning) {
return;
}
this.state.isRunning = false;
if (this.state.stream) {
this.state.stream.getTracks().forEach((track) => track.stop());
this.state.stream = null;
}
if (this.state.videoElement) {
this.state.videoElement.srcObject = null;
this.state.videoElement.style.display = 'none';
}
this.removeKeyboardInput();
}
/**
* Main scanning loop for real-time frame processing
*/
scanLoop() {
if (!this.state.isRunning) {
return;
}
const canvas = this.state.canvas;
const video = this.state.videoElement;
if (!video || !canvas) {
setTimeout(() => this.scanLoop(), 100);
return;
}
if (video.readyState === video.HAVE_ENOUGH_DATA) {
try {
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
if (canvas.width === 0 || canvas.height === 0) {
setTimeout(() => this.scanLoop(), 100);
return;
}
const context = canvas.getContext('2d');
if (context) {
context.drawImage(video, 0, 0);
try {
const luminanceSource = new ZXing.HTMLCanvasElementLuminanceSource(canvas);
const binaryBitmap = new ZXing.BinaryBitmap(new ZXing.HybridBinarizer(luminanceSource));
try {
const result = this.state.reader.decodeFromBitmap(binaryBitmap);
this.handleScanResult(result.text);
} catch (e) {
// No barcode found in frame; continue scanning
}
} catch (err) {
// Silently ignore canvas errors
}
}
} catch (err) {
console.warn('Scan loop error:', err);
}
}
// Throttle scan loop based on FPS setting
setTimeout(() => this.scanLoop(), 1000 / this.options.fps);
}
/**
* Handle successful barcode scan with debouncing
*/
handleScanResult(scannedText) {
const now = Date.now();
if (now - this.state.lastScanTime < this.state.scanDebounceMs) {
return; // Ignore rapid duplicates
}
this.state.lastScanTime = now;
this.options.onSuccess(scannedText.trim());
}
/**
* Setup keyboard input listener for USB scanner emulation
* Most USB scanners emulate keyboard input (barcode followed by Enter)
*/
setupKeyboardInput() {
this.keyboardListener = (event) => {
// Allow only alphanumeric, dash, colon (common in barcodes)
if (/^[a-zA-Z0-9:_\-]$/.test(event.key)) {
event.preventDefault();
this.keyboardInputBuffer += event.key;
// Reset timeout for multi-part barcodes
if (this.keyboardInputTimeout) {
clearTimeout(this.keyboardInputTimeout);
}
} else if (event.key === 'Enter') {
event.preventDefault();
if (this.keyboardInputBuffer.length > 0) {
this.handleScanResult(this.keyboardInputBuffer);
this.keyboardInputBuffer = '';
}
} else if (event.key === 'Escape') {
this.keyboardInputBuffer = '';
this.stop();
}
// Auto-trigger if buffer looks complete (common barcode lengths)
if ([8, 12, 13, 17].includes(this.keyboardInputBuffer.length)) {
this.keyboardInputTimeout = setTimeout(() => {
if (this.keyboardInputBuffer.length > 0) {
this.handleScanResult(this.keyboardInputBuffer);
this.keyboardInputBuffer = '';
}
}, 200);
}
};
document.addEventListener('keydown', this.keyboardListener);
}
/**
* Remove keyboard input listener
*/
removeKeyboardInput() {
if (this.keyboardListener) {
document.removeEventListener('keydown', this.keyboardListener);
this.keyboardListener = null;
}
this.keyboardInputBuffer = '';
}
/**
* Upload image for server-side decoding (fallback)
* Requires /api/scan_upload endpoint
*/
static async uploadImageForDecoding(file, onSuccess, onError) {
try {
const formData = new FormData();
formData.append('image', file);
const response = await fetch('/api/scan_upload', {
method: 'POST',
body: formData,
});
const data = await response.json();
if (data.ok && data.barcodes && data.barcodes.length > 0) {
// Return first detected barcode (highest confidence)
onSuccess(data.barcodes[0].value);
} else {
onError(data.message || 'No barcode detected in image.');
}
} catch (err) {
onError(`Upload error: ${err.message}`);
}
}
/**
* Toggle camera (front/rear on mobile)
*/
toggleCamera() {
if (this.options.facingMode === 'environment') {
this.options.facingMode = 'user';
} else {
this.options.facingMode = 'environment';
}
// Restart scanning with new camera
this.stop();
this.start();
}
}
// Export for use in templates
if (typeof window !== 'undefined') {
window.HybridScanner = HybridScanner;
}
-5
View File
@@ -1295,11 +1295,6 @@
</li>
{% endif %}
{% if 'username' in session %}
{% if current_permissions.pages.get('my_borrowed_items', True) %}
<li class="nav-item">
<a class="nav-link quick-link-pill {% if current_path == url_for('my_borrowed_items') %}nav-active{% endif %}" href="{{ url_for('my_borrowed_items') }}" data-tutorial-tip="Hier sehen Sie Ihre bibliotheksbezogenen Ausleihen.">Meine Medien</a>
</li>
{% endif %}
{% if current_permissions.pages.get('tutorial_page', True) %}
<li class="nav-item">
<a class="nav-link quick-link-pill {% if current_path == url_for('tutorial_page') %}nav-active{% endif %}" href="{{ url_for('tutorial_page') }}" data-tutorial-tip="Nutzen Sie das Tutorial, um die Bibliotheksfunktionen kennenzulernen.">Tutorial</a>
+59 -243
View File
@@ -10,24 +10,6 @@
<h1>📚 Bibliothek</h1>
<p class="subtitle">Bücher, CDs und weitere Medien</p>
</div>
<div class="header-controls">
<!-- Favorites Toggle -->
<div class="view-switch">
<button id="favoriteToggle" class="toggle-btn" aria-label="Favoriten anzeigen/verbergen">
<span class="toggle-icon"></span>
<span class="toggle-label">Favoriten</span>
</button>
</div>
<!-- View Mode Toggle -->
<div class="view-switch">
<button id="viewModeToggle" class="toggle-btn" aria-label="Ansichtsmodus wechseln">
<span class="toggle-icon"></span>
<span class="toggle-label">Tabelle</span>
</button>
</div>
</div>
</div>
<!-- Search & Filter Section -->
@@ -67,22 +49,8 @@
<!-- Scanner Panel -->
<div id="qrContainer" class="qr-container" style="display: none;">
<div class="scanner-header">
<h3>Barcode/QR-Code Scanner</h3>
<div class="scanner-controls">
<button type="button" id="cameraToggle" class="scanner-button" title="Kamera wechseln">📷 Kamera</button>
<label for="uploadScanImage" class="scanner-button" title="Bild hochladen zum Dekodieren">📤 Upload</label>
<input type="file" id="uploadScanImage" accept="image/*" style="display: none;">
</div>
</div>
<div class="scanner-video-wrapper">
<video id="qr-reader" playsinline style="width: 100%; max-width: 100%; height: auto; aspect-ratio: 4/3; object-fit: cover; border-radius: 8px;"></video>
<canvas id="scanner-canvas" style="display: none;"></canvas>
</div>
<div id="scanMessage" class="scanner-message" style="margin-top: 10px; text-align: center; font-weight: bold; color: #666;"></div>
<p style="font-size: 0.85em; color: #999; margin-top: 10px; margin-bottom: 0; text-align: center;">
💡 Hinweis: Scan-Ergebnis wird automatisch in das Feld oben übernommen. USB-Scanner werden als Tastatureingabe erkannt.
</p>
<div id="qr-reader" style="width: auto; height: 300px;"></div>
<span id="qr-result" style="display: none;"></span>
</div>
<!-- Table Header (only in table mode) -->
@@ -219,9 +187,7 @@
</form>
</div>
</div>
<script src="https://unpkg.com/@zxing/library@latest/umd/index.min.js"></script>
<script src="/static/js/scanner.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@ericblade/quagga2/dist/quagga.js"></script>
<script>
// View mode persistence
const LIBRARY_VIEW_MODE_KEY = 'inventarLibraryViewMode';
@@ -640,103 +606,80 @@ document.getElementById('clearFiltersBtn').addEventListener('click', function()
closeAllFilters();
});
// Favorites toggle
document.getElementById('favoriteToggle').addEventListener('click', function() {
this.classList.toggle('open');
// TODO: Implement favorites filtering
});
// Scanner instance (hybrid: real-time + upload + keyboard)
let scanner = null;
// Scanner toggle
// Scanner state tracking
let isScanning = false;
document.getElementById('scannerBtn').addEventListener('click', function(e) {
e.stopPropagation();
const container = document.getElementById('qrContainer');
const isOpen = container.style.display !== 'none';
const btn = this;
if (isOpen) {
// Close the scanner
container.style.display = 'none';
if (scanner) {
scanner.stop();
scanner = null;
}
this.classList.remove('open');
this.setAttribute('aria-expanded', 'false');
Quagga.stop();
isScanning = false;
btn.classList.remove('open');
btn.setAttribute('aria-expanded', 'false');
} else {
// Open the scanner
container.style.display = 'block';
this.classList.add('open');
this.setAttribute('aria-expanded', 'true');
btn.classList.add('open');
btn.setAttribute('aria-expanded', 'true');
if (!scanner) {
initScanner();
}
}
});
function initScanner() {
scanner = new HybridScanner({
videoId: 'qr-reader',
canvasId: 'scanner-canvas',
formats: ['QR_CODE', 'CODE_128', 'EAN_13', 'ISBN', 'CODE_39', 'UPC_A', 'EAN_8'],
fps: 10,
facingMode: 'environment',
onSuccess: function(decodedText) {
const studentId = decodedText.trim();
document.getElementById('studentIdInput').value = studentId;
document.getElementById('scanMessage').textContent = '✓ Erkannt: ' + studentId;
document.getElementById('scanMessage').style.color = '#4CAF50';
},
onError: function(error) {
document.getElementById('scanMessage').textContent = '✗ ' + error;
document.getElementById('scanMessage').style.color = '#f44336';
}
});
scanner.start();
}
// Camera toggle
document.getElementById('cameraToggle')?.addEventListener('click', function() {
if (scanner) {
scanner.toggleCamera();
this.textContent = scanner.options.facingMode === 'user' ? '📷 Rückkamera' : '🤳 Frontkamera';
}
});
// Image upload fallback
document.getElementById('uploadScanImage')?.addEventListener('change', function(e) {
const file = e.target.files[0];
if (file) {
const messageEl = document.getElementById('scanMessage');
messageEl.textContent = 'Wird dekodiert...';
messageEl.style.color = '#2196F3';
HybridScanner.uploadImageForDecoding(
file,
(decodedText) => {
document.getElementById('studentIdInput').value = decodedText;
messageEl.textContent = '✓ Erkannt (Upload): ' + decodedText;
messageEl.style.color = '#4CAF50';
e.target.value = ''; // Reset file input
Quagga.init({
inputStream: {
name: "Live",
type: "LiveStream",
// Targets the element where the video stream will inject
target: document.querySelector('#qr-reader'),
constraints: {
width: 640,
height: 480,
facingMode: "environment" // Forces back camera
},
},
(error) => {
messageEl.textContent = '✗ ' + error;
messageEl.style.color = '#f44336';
e.target.value = '';
decoder: {
// Optimized for 1D barcodes (e.g., student IDs, member cards)
readers: ["code_128_reader", "ean_reader", "code_39_reader", "upc_reader"]
}
}, function(err) {
if (err) {
console.error("Initialization error:", err);
alert("Kamera konnte nicht gestartet werden.");
return;
}
);
Quagga.start();
isScanning = true;
});
}
});
function onScanSuccess(decodedText, decodedResult) {
const studentId = decodedText.trim();
// Single Quagga event listener for successful scans
Quagga.onDetected(function(data) {
const decodedText = data.codeResult.code;
const studentId = String(decodedText || '').trim();
// Stop scanning immediately to prevent multiple triggers
Quagga.stop();
isScanning = false;
// Reset UI states to closed
const container = document.getElementById('qrContainer');
const btn = document.getElementById('scannerBtn');
if (container) container.style.display = 'none';
if (btn) {
btn.classList.remove('open');
btn.setAttribute('aria-expanded', 'false');
}
// Route the scanned data to your input
document.getElementById('studentIdInput').value = studentId;
alert('Ausweis gescannt: ' + studentId);
}
function onScanError(error) {
// Silently ignore scan errors
}
});
</script>
<style>
@@ -797,9 +740,6 @@ function onScanError(error) {
overflow-x: auto;
-webkit-overflow-scrolling: touch;
width: 100%;
box-sizing: border-box;
margin: 0 -12px;
padding: 0 12px;
}
.library-table-wrapper .table-row {
min-width: 720px; /* allow table to have intrinsic width and be scrolled */
@@ -808,129 +748,5 @@ function onScanError(error) {
display: block; /* keep rows stacked but allow horizontal scroll */
}
}
/* Hybrid Scanner Styles */
.qr-container {
background: #f5f5f5;
border: 1px solid #ddd;
border-radius: 8px;
padding: 12px;
margin: 12px 0;
overflow-x: hidden;
width: 100%;
box-sizing: border-box;
}
.scanner-header {
display: flex;
justify-content: flex-start;
align-items: center;
margin-bottom: 12px;
flex-wrap: wrap;
gap: 8px;
width: 100%;
box-sizing: border-box;
}
.scanner-header h3 {
margin: 0;
color: #333;
font-size: 1em;
flex: 0 0 auto;
white-space: nowrap;
}
.scanner-controls {
display: flex;
gap: 6px;
flex-wrap: wrap;
flex: 1 1 auto;
min-width: 0;
}
.scanner-button {
background: #2196F3;
color: white;
border: none;
padding: 6px 10px;
border-radius: 4px;
cursor: pointer;
font-size: 0.85em;
white-space: nowrap;
flex: 0 1 auto;
transition: background-color 0.2s;
}
.scanner-button:hover {
background: #1976D2;
}
.scanner-button:active {
background: #1565C0;
}
.scanner-video-wrapper {
display: flex;
justify-content: center;
align-items: center;
margin: 12px 0;
width: 100%;
box-sizing: border-box;
overflow: hidden;
}
#qr-reader {
width: 100%;
max-width: 100%;
height: auto;
border: 2px solid #2196F3;
border-radius: 8px;
display: block;
}
.scanner-message {
min-height: 20px;
font-size: 0.9em;
text-align: center;
word-wrap: break-word;
}
@media (max-width: 600px) {
.qr-container {
padding: 10px;
margin: 10px 0;
}
.scanner-header {
flex-direction: column;
align-items: stretch;
gap: 8px;
}
.scanner-header h3 {
width: 100%;
font-size: 0.95em;
}
.scanner-controls {
width: 100%;
justify-content: space-between;
}
.scanner-button {
flex: 1 1 auto;
padding: 8px 6px;
font-size: 0.8em;
}
#qr-reader {
max-width: 100%;
border-radius: 6px;
}
.scanner-message {
font-size: 0.85em;
}
}
</style>
{% endblock %}
+421 -376
View File
@@ -431,7 +431,7 @@
type="text"
id="librarySearch"
class="library-search-input"
placeholder="Nach Titel, Autor, ISBN suchen..."
placeholder="Nach Titel, ISBN suchen..."
>
<button
id="filterToggleBtn"
@@ -445,7 +445,7 @@
<!-- Customizable Filter Panel -->
<!-- Student card / quick scan workflow -->
<div class="library-scan-panel">
<div class="library-scan-panel">
<div class="library-scan-controls">
<select id="scanModeSelect" aria-label="Scan-Modus">
<option value="card_only">Nur Ausweis erfassen</option>
@@ -458,19 +458,14 @@
<div id="scanStatus" class="library-scan-status">
Hinweis: Im Schnellmodus zuerst den Schülerausweis scannen, danach den Buch-/Mediencode.
</div>
<div id="scanReaderWrap" class="library-scan-reader-wrap">
<div id="scanReaderWrap" class="library-scan-reader-wrap" style="display: none;">
<div id="libraryQrReader" class="library-scan-reader">
<video id="libraryQrReaderVideo" playsinline style="width: 100%; border-radius: 8px;"></video>
<canvas id="libraryQrReaderCanvas" style="display: none;"></canvas>
<div id="library-scanner-container"></div>
</div>
</div>
</div>
<div id="filterPanel" class="library-filter-panel">
<div class="filter-row">
<div class="filter-item">
<label for="filterAuthor">Autor/Künstler:</label>
<input type="text" id="filterAuthor" placeholder="z.B. Goethe">
</div>
<div class="filter-item">
<label for="filterISBN">ISBN:</label>
<input type="text" id="filterISBN" placeholder="z.B. 978-3...">
@@ -517,7 +512,6 @@
<thead>
<tr>
<th style="width: 24%;">Titel</th>
<th style="width: 14%;">Autor/Künstler</th>
<th style="width: 12%;">ISBN/Code</th>
<th style="width: 8%;">Typ</th>
<th style="width: 8%;">Anzahl</th>
@@ -550,16 +544,17 @@
</div>
</div>
<script src="https://unpkg.com/@zxing/library@latest/umd/index.min.js"></script>
<script src="/static/js/scanner.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@ericblade/quagga2/dist/quagga.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@ericblade/quagga2/dist/quagga.js"></script>
<script>
// State
// =========================================================================
// 1. GLOBAL STATE DEFINITIONS
// =========================================================================
let libraryItems = [];
let filteredItems = [];
let visibleItems = [];
let currentSearchTerm = '';
let activeFilters = {
author: '',
isbn: '',
type: '',
status: ''
@@ -575,13 +570,20 @@
const RENDER_BATCH_COUNT = 120;
let renderedCount = INITIAL_RENDER_COUNT;
let filterPanelOpen = false;
// Scanner Related State Variables
let scannerInstance = null;
let scannerRunning = false;
let scannerRunning = false;
let activeScannerCallback = null;
let activeStudentCardId = '';
let lastScanValue = '';
let lastScanAt = 0;
const canEditLibraryItems = (document.getElementById('libraryTableContainer')?.dataset.canEdit === '1');
// =========================================================================
// 2. DATA LOADING & FILTERING ENGINE
// =========================================================================
async function fetchLibraryPage(offset, limit) {
const response = await fetch(`/api/library_items?offset=${offset}&limit=${limit}`);
if (!response.ok) {
@@ -590,7 +592,6 @@
return response.json();
}
// Load items progressively: first page immediately, rest in background
async function loadLibraryItems() {
if (pagingState.loading) return;
pagingState.loading = true;
@@ -629,7 +630,6 @@
pagingState.total = page.total || pagingState.total;
pagingState.hasMore = Boolean(page.has_more);
// Keep the current view reactive while avoiding a full rerender burst.
applyFiltersAndSearch(false);
if ('requestIdleCallback' in window) {
@@ -645,18 +645,16 @@
}
function filterItems(items) {
const author = activeFilters.author;
const isbn = activeFilters.isbn;
const type = activeFilters.type;
const status = activeFilters.status;
return items.filter(item => {
const authorMatch = !author || (item.Autor || item.Author || '').toLowerCase().includes(author);
const isbnMatch = !isbn || (item.ISBN || item.Code_4 || item.Code4 || '').toLowerCase().includes(isbn);
const typeMatch = !type || (item.ItemType || 'book') === type;
const statusKey = item.LibraryDisplayStatus || (item.Verfuegbar === false ? 'borrowed' : 'available');
const statusMatch = !status || statusKey === status;
return authorMatch && isbnMatch && typeMatch && statusMatch;
return isbnMatch && typeMatch && statusMatch;
});
}
@@ -664,7 +662,6 @@
if (!currentSearchTerm) return items;
return items.filter(item =>
(item.Name || '').toLowerCase().includes(currentSearchTerm) ||
(item.Autor || item.Author || '').toLowerCase().includes(currentSearchTerm) ||
(item.ISBN || item.Code_4 || item.Code4 || '').toLowerCase().includes(currentSearchTerm)
);
}
@@ -681,7 +678,6 @@
renderItems();
}
// Display current view with incremental rendering
function renderItems() {
const tbody = document.getElementById('itemsTableBody');
const emptyState = document.getElementById('emptyState');
@@ -699,37 +695,33 @@
emptyState.style.display = 'none';
const rowsToRender = visibleItems.slice(0, renderedCount);
tbody.innerHTML = rowsToRender.map(item => `
${(() => {
const statusKey = item.LibraryDisplayStatus || (item.Verfuegbar === false ? 'borrowed' : 'available');
const statusClass = statusKey === 'damaged' ? 'status-damaged' : (statusKey === 'borrowed' ? 'status-borrowed' : 'status-available');
const statusText = statusKey === 'damaged' ? 'Defekt/Zerstört' : (statusKey === 'borrowed' ? 'Ausgeliehen' : 'Verfügbar');
const actionLabel = statusKey === 'available' ? 'Ausleihen' : (statusKey === 'borrowed' ? 'Reservieren' : 'Nicht ausleihbar');
const actionDisabled = statusKey === 'damaged' ? 'disabled' : '';
return `
<tr>
<td class="table-title">${escapeHtml(item.Name || 'Untitled')}</td>
<td>${escapeHtml(item.Autor || item.Author || '-')}</td>
<td>${escapeHtml(item.ISBN || item.Code_4 || item.Code4 || '-')}</td>
<td>${getItemTypeLabel(item.ItemType || 'book')}</td>
<td style="font-weight:600; text-align:center;">${item.Quantity || item.GroupedDisplayCount || 1}</td>
<td>
<span class="table-status ${statusClass}">
${statusText}
</span>
</td>
<td class="table-actions">
<button class="button" onclick="showItemDetail('${item._id}')">Details</button>
<button class="button" style="background: #28a745; color: white;" onclick="borrowItem('${item._id}')" ${actionDisabled}>
${actionLabel}
</button>
${canEditLibraryItems ? `<button class="button" style="background:#0ea5e9;color:#fff;" onclick="openEditLibraryItem('${item._id}')">Bearbeiten</button>` : ''}
${canEditLibraryItems ? `<button class="button" style="background:#dc2626;color:#fff; margin-left:6px;" onclick="confirmDeleteLibraryItem('${item._id}')">Löschen</button>` : ''}
</td>
</tr>
`;
})()}
`).join('');
tbody.innerHTML = rowsToRender.map(item => {
const statusKey = item.LibraryDisplayStatus || (item.Verfuegbar === false ? 'borrowed' : 'available');
const statusClass = statusKey === 'damaged' ? 'status-damaged' : (statusKey === 'borrowed' ? 'status-borrowed' : 'status-available');
const statusText = statusKey === 'damaged' ? 'Defekt/Zerstört' : (statusKey === 'borrowed' ? 'Ausgeliehen' : 'Verfügbar');
const actionLabel = statusKey === 'available' ? 'Ausleihen' : (statusKey === 'borrowed' ? 'Reservieren' : 'Nicht ausleihbar');
const actionDisabled = statusKey === 'damaged' ? 'disabled' : '';
return `
<tr>
<td class="table-title">${escapeHtml(item.Name || 'Untitled')}</td>
<td>${escapeHtml(item.ISBN || item.Code_4 || item.Code4 || '-')}</td>
<td>${getItemTypeLabel(item.ItemType || 'book')}</td>
<td style="font-weight:600; text-align:center;">${item.Quantity || item.GroupedDisplayCount || 1}</td>
<td>
<span class="table-status ${statusClass}">
${statusText}
</span>
</td>
<td class="table-actions">
<button class="button" onclick="showItemDetail('${item._id}')">Details</button>
<button class="button" style="background: #28a745; color: white;" onclick="borrowItem('${item._id}')" ${actionDisabled}>
${actionLabel}
</button>
${canEditLibraryItems ? `<button class="button" style="background:#0ea5e9;color:#fff;" onclick="openEditLibraryItem('${item._id}')">Bearbeiten</button>` : ''}
${canEditLibraryItems ? `<button class="button" style="background:#dc2626;color:#fff; margin-left:6px;" onclick="confirmDeleteLibraryItem('${item._id}')">Löschen</button>` : ''}
</td>
</tr>`;
}).join('');
const canLoadMoreRows = renderedCount < visibleItems.length;
if (canLoadMoreRows) {
@@ -744,13 +736,6 @@
}
}
// Confirm and call library item delete endpoint
function confirmDeleteLibraryItem(itemId) {
if (!itemId) return;
if (!confirm('Dieses Bibliotheksmedium revisionssicher löschen? Diese Aktion deaktiviert alle zugehörigen Exemplare und archiviert Medien.')) return;
deleteLibraryItem(itemId);
}
async function deleteLibraryItem(itemId) {
if (!itemId) return;
document.body.style.cursor = 'wait';
@@ -774,7 +759,6 @@
}
loadLibraryItems();
} else {
// Server redirected to HTML (flash messages). Reload to show state.
location.reload();
}
} catch (e) {
@@ -785,46 +769,269 @@
}
}
// Filter toggle
document.getElementById('filterToggleBtn').addEventListener('click', () => {
filterPanelOpen = !filterPanelOpen;
const panel = document.getElementById('filterPanel');
const btn = document.getElementById('filterToggleBtn');
panel.classList.toggle('open');
btn.classList.toggle('active');
function confirmDeleteLibraryItem(itemId) {
if (!itemId) return;
if (!confirm('Dieses Bibliotheksmedium revisionssicher löschen? Diese Aktion deaktiviert alle zugehörigen Exemplare und archiviert Medien.')) return;
deleteLibraryItem(itemId);
}
// =========================================================================
// 3. CORE SCANNER ROUTING ENGINE (QUAGGA2)
// =========================================================================
function startScanner(targetCallback) {
const readerWrap = document.getElementById('scanReaderWrap');
const toggleBtn = document.getElementById('toggleScannerBtn');
activeScannerCallback = targetCallback;
if (readerWrap) readerWrap.style.display = 'block';
setScanStatus('Initializing camera...', 'warn');
Quagga.init({
inputStream: {
name: "Live",
type: "LiveStream",
target: document.querySelector('#library-scanner-container'),
constraints: {
width: 640,
height: 480,
facingMode: "environment"
},
},
decoder: {
readers: [
"code_128_reader",
"ean_reader",
"code_39_reader",
"upc_reader",
"codabar_reader",
"i2of5_reader"
]
}
}, function(err) {
if (err) {
console.error('Scanner start failed:', err);
if (readerWrap) readerWrap.style.display = 'none';
const detail = (err && (err.message || err.name)) ? ` (${err.message || err.name})` : '';
setScanStatus(`Scanner konnte nicht gestartet werden${detail}`, 'error');
return;
}
Quagga.start();
scannerRunning = true;
if (!targetCallback && toggleBtn) {
toggleBtn.textContent = 'Scanner stoppen';
}
setScanStatus('Scanner aktiv. Jetzt Code scannen.', 'warn');
});
}
function stopScanner() {
if (!scannerRunning) return;
const readerWrap = document.getElementById('scanReaderWrap');
const toggleBtn = document.getElementById('toggleScannerBtn');
Quagga.stop();
scannerRunning = false;
activeScannerCallback = null;
if (readerWrap) readerWrap.style.display = 'none';
if (toggleBtn) toggleBtn.textContent = 'Scanner starten';
setScanStatus('Scanner gestoppt.', 'warn');
}
Quagga.onDetected(function(data) {
if (!data || !data.codeResult || !data.codeResult.code) return;
const barcode = String(data.codeResult.code || '').trim();
console.log("Barcode detected:", barcode);
const currentCallback = activeScannerCallback;
stopScanner();
if (typeof currentCallback === "function") {
currentCallback(barcode);
} else {
handleScanSuccess(barcode);
}
});
// Apply filters
document.getElementById('applyFilterBtn').addEventListener('click', () => {
activeFilters.author = document.getElementById('filterAuthor').value.toLowerCase();
activeFilters.isbn = document.getElementById('filterISBN').value.toLowerCase();
activeFilters.type = document.getElementById('filterType').value;
activeFilters.status = document.getElementById('filterStatus').value;
applyFiltersAndSearch(true);
});
function handleScanSuccess(decodedText) {
const scannedCode = normalizeScannedCode(decodedText);
if (!scannedCode) return;
const now = Date.now();
if (scannedCode === lastScanValue && (now - lastScanAt) < 1500) {
return;
}
lastScanValue = scannedCode;
lastScanAt = now;
const mode = (document.getElementById('scanModeSelect') || {}).value || 'card_only';
if (mode === 'card_only') {
setActiveStudentCard(scannedCode);
setScanStatus(`Ausweis gesetzt: ${activeStudentCardId}`, 'ok');
return;
}
processQuickToggleScan(scannedCode);
}
// Clear filters
document.getElementById('clearFilterBtn').addEventListener('click', () => {
document.getElementById('filterAuthor').value = '';
document.getElementById('filterISBN').value = '';
document.getElementById('filterType').value = '';
document.getElementById('filterStatus').value = '';
activeFilters = { author: '', isbn: '', type: '', status: '' };
applyFiltersAndSearch(true);
});
async function processQuickToggleScan(scannedCode) {
if (!activeStudentCardId) {
setActiveStudentCard(scannedCode);
setScanStatus(`Ausweis gesetzt: ${activeStudentCardId}`, 'ok');
return;
}
try {
setScanStatus('Verarbeite Mediencode...', 'warn');
const response = await fetch('/api/library_scan_action', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
student_card_id: activeStudentCardId,
item_code: scannedCode
})
});
const result = await response.json();
if (!response.ok || !result.ok) {
setScanStatus(result.message || 'Scan-Aktion fehlgeschlagen.', 'error');
return;
}
if (result.action === 'borrowed') {
setScanStatus(`Ausgeliehen: ${result.item_name}`, 'ok');
} else if (result.action === 'returned') {
setScanStatus(`Zurückgegeben: ${result.item_name}`, 'ok');
} else {
setScanStatus(result.message || 'Aktion durchgeführt.', 'ok');
}
await loadLibraryItems();
} catch (err) {
console.error('Quick scan action failed:', err);
setScanStatus('Fehler beim Verarbeiten des Scans.', 'error');
}
}
// Search input
document.getElementById('librarySearch').addEventListener('input', (e) => {
currentSearchTerm = (e.target.value || '').toLowerCase();
applyFiltersAndSearch(true);
});
function scanIntoEditCode() {
const scanReaderWrap = document.getElementById('scanReaderWrap');
const editCodeInput = document.getElementById('edit-code4');
const scanEditBtn = document.getElementById('scan-edit-code-btn');
if (!scanReaderWrap || !editCodeInput || !scanEditBtn) {
return;
}
if (scannerRunning && scanReaderWrap.style.display !== 'none') {
stopScanner();
scanEditBtn.textContent = 'Barcode scannen';
return;
}
scanEditBtn.textContent = 'Scanner schließen';
startScanner(function(decodedText) {
editCodeInput.value = decodedText;
if(typeof validateCodeField === "function") {
validateCodeField(editCodeInput, document.getElementById('edit-item-id')?.value || null);
}
scanEditBtn.textContent = 'Barcode scannen';
});
}
document.getElementById('loadMoreBtn').addEventListener('click', () => {
renderedCount = Math.min(renderedCount + RENDER_BATCH_COUNT, visibleItems.length);
renderItems();
});
// =========================================================================
// 4. UI INTERACTIONS & UTILITIES
// =========================================================================
function borrowItem(itemId) {
const selectedItem = (libraryItems || []).find(item => item._id === itemId);
if (selectedItem && selectedItem.LibraryDisplayStatus === 'damaged') {
alert('Dieses Medium ist als defekt/zerstört markiert und kann nicht ausgeliehen werden.');
return;
}
const defaultCardId = activeStudentCardId || '';
const cardId = (window.prompt('Bitte Schülerausweis-ID eingeben:', defaultCardId) || '').trim().toUpperCase();
if (!cardId) {
alert('Ausleihe abgebrochen: Für Bibliotheksmedien ist eine gültige Schülerausweis-ID erforderlich.');
return;
}
setActiveStudentCard(cardId);
const durationInput = (window.prompt('Ausleihdauer in Tagen (optional):') || '').trim();
const maxAvailable = Math.max(1, parseInt(selectedItem?.AvailableGroupedCount || selectedItem?.Quantity || 1, 10) || 1);
const countPrompt = (window.prompt(`Anzahl ausleihen? (Standard: 1, verfügbar: ${maxAvailable})`, '1') || '').trim();
let borrowCount = parseInt(countPrompt || '1', 10);
if (!Number.isFinite(borrowCount) || borrowCount < 1) {
borrowCount = 1;
}
if (borrowCount > maxAvailable) {
alert(`Es sind nur ${maxAvailable} Exemplar(e) verfügbar.`);
return;
}
const form = document.createElement('form');
form.method = 'POST';
form.action = `/ausleihen/${itemId}`;
const csrfField = document.createElement('input');
csrfField.type = 'hidden';
csrfField.name = 'csrf_token';
csrfField.value = '{{ csrf_token }}';
form.appendChild(csrfField);
const cardField = document.createElement('input');
cardField.type = 'hidden';
cardField.name = 'borrower_card_id';
cardField.value = cardId;
form.appendChild(cardField);
const returnTargetField = document.createElement('input');
returnTargetField.type = 'hidden';
returnTargetField.name = 'return_to';
returnTargetField.value = 'library';
form.appendChild(returnTargetField);
if (durationInput) {
const durationField = document.createElement('input');
durationField.type = 'hidden';
durationField.name = 'borrow_duration_days';
durationField.value = durationInput;
form.appendChild(durationField);
}
const countField = document.createElement('input');
countField.type = 'hidden';
countField.name = 'exemplare_count';
countField.value = String(borrowCount || 1);
form.appendChild(countField);
document.body.appendChild(form);
form.submit();
}
function setScanStatus(message, kind = '') {
const el = document.getElementById('scanStatus');
if (!el) return;
el.textContent = message;
el.classList.remove('ok', 'warn', 'error');
if (kind) el.classList.add(kind);
}
function setActiveStudentCard(cardId) {
activeStudentCardId = (cardId || '').trim().toUpperCase();
const input = document.getElementById('activeStudentCard');
if (input) input.value = activeStudentCardId;
}
function normalizeScannedCode(code) {
return (code || '').trim();
}
// Helper functions
function getItemTypeLabel(type) {
const labels = { 'book': 'Buch', 'cd': 'CD', 'dvd': 'DVD', 'other': 'Sonstige' };
return labels[type] || type;
@@ -837,7 +1044,6 @@
}
function showItemDetail(itemId) {
// Load and show detail modal
fetch(`/api/item_detail/${itemId}`)
.then(r => r.text())
.then(html => {
@@ -851,322 +1057,165 @@
document.getElementById('detailModal').style.display = 'none';
}
function borrowItem(itemId) {
const selectedItem = (libraryItems || []).find(item => item._id === itemId);
if (selectedItem && selectedItem.LibraryDisplayStatus === 'damaged') {
alert('Dieses Medium ist als defekt/zerstört markiert und kann nicht ausgeliehen werden.');
return;
}
const defaultCardId = activeStudentCardId || '';
const cardId = (window.prompt('Bitte Schülerausweis-ID eingeben:', defaultCardId) || '').trim().toUpperCase();
if (!cardId) {
alert('Ausleihe abgebrochen: Für Bibliotheksmedien ist eine gültige Schülerausweis-ID erforderlich.');
return;
}
setActiveStudentCard(cardId);
const durationInput = (window.prompt('Ausleihdauer in Tagen (optional):') || '').trim();
const maxAvailable = Math.max(1, parseInt(selectedItem?.AvailableGroupedCount || selectedItem?.Quantity || 1, 10) || 1);
const countPrompt = (window.prompt(`Anzahl ausleihen? (Standard: 1, verfügbar: ${maxAvailable})`, '1') || '').trim();
let borrowCount = parseInt(countPrompt || '1', 10);
if (!Number.isFinite(borrowCount) || borrowCount < 1) {
borrowCount = 1;
}
if (borrowCount > maxAvailable) {
alert(`Es sind nur ${maxAvailable} Exemplar(e) verfügbar.`);
return;
}
const form = document.createElement('form');
form.method = 'POST';
form.action = `/ausleihen/${itemId}`;
const csrfField = document.createElement('input');
csrfField.type = 'hidden';
csrfField.name = 'csrf_token';
csrfField.value = '{{ csrf_token }}';
form.appendChild(csrfField);
const cardField = document.createElement('input');
cardField.type = 'hidden';
cardField.name = 'borrower_card_id';
cardField.value = cardId;
form.appendChild(cardField);
const returnTargetField = document.createElement('input');
returnTargetField.type = 'hidden';
returnTargetField.name = 'return_to';
returnTargetField.value = 'library';
form.appendChild(returnTargetField);
if (durationInput) {
const durationField = document.createElement('input');
durationField.type = 'hidden';
durationField.name = 'borrow_duration_days';
durationField.value = durationInput;
form.appendChild(durationField);
}
const countField = document.createElement('input');
countField.type = 'hidden';
countField.name = 'exemplare_count';
countField.value = String(borrowCount || 1);
form.appendChild(countField);
document.body.appendChild(form);
form.submit();
}
function setScanStatus(message, kind = '') {
const el = document.getElementById('scanStatus');
if (!el) return;
el.textContent = message;
el.classList.remove('ok', 'warn', 'error');
if (kind) {
el.classList.add(kind);
}
}
function setActiveStudentCard(cardId) {
activeStudentCardId = (cardId || '').trim().toUpperCase();
const input = document.getElementById('activeStudentCard');
if (input) {
input.value = activeStudentCardId;
}
}
function normalizeScannedCode(code) {
return (code || '').trim();
}
async function processQuickToggleScan(scannedCode) {
if (!activeStudentCardId) {
setActiveStudentCard(scannedCode);
setScanStatus(`Ausweis gesetzt: ${activeStudentCardId}`, 'ok');
return;
}
try {
setScanStatus('Verarbeite Mediencode...', 'warn');
const response = await fetch('/api/library_scan_action', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
student_card_id: activeStudentCardId,
item_code: scannedCode
})
});
const result = await response.json();
if (!response.ok || !result.ok) {
setScanStatus(result.message || 'Scan-Aktion fehlgeschlagen.', 'error');
return;
}
if (result.action === 'borrowed') {
setScanStatus(`Ausgeliehen: ${result.item_name}`, 'ok');
} else if (result.action === 'returned') {
setScanStatus(`Zurückgegeben: ${result.item_name}`, 'ok');
} else {
setScanStatus(result.message || 'Aktion durchgeführt.', 'ok');
}
await loadLibraryItems();
} catch (err) {
console.error('Quick scan action failed:', err);
setScanStatus('Fehler beim Verarbeiten des Scans.', 'error');
}
}
function handleScanSuccess(decodedText) {
const scannedCode = normalizeScannedCode(decodedText);
if (!scannedCode) return;
const now = Date.now();
if (scannedCode === lastScanValue && (now - lastScanAt) < 1500) {
return;
}
lastScanValue = scannedCode;
lastScanAt = now;
const mode = (document.getElementById('scanModeSelect') || {}).value || 'card_only';
if (mode === 'card_only') {
setActiveStudentCard(scannedCode);
setScanStatus(`Ausweis gesetzt: ${activeStudentCardId}`, 'ok');
return;
}
processQuickToggleScan(scannedCode);
}
function handleScanError() {
// Intentionally silent to avoid UI spam while camera searches codes.
}
async function ensureScannerLibraryLoaded() {
// HybridScanner is loaded globally via script tag
return typeof HybridScanner !== 'undefined';
}
async function startScanner() {
if (scannerRunning) return;
const scannerLoaded = await ensureScannerLibraryLoaded();
if (!scannerLoaded) {
setScanStatus('Scanner-Bibliothek konnte nicht geladen werden.', 'error');
return;
}
const readerWrap = document.getElementById('scanReaderWrap');
const toggleBtn = document.getElementById('toggleScannerBtn');
readerWrap.style.display = 'block';
try {
scannerInstance = scannerInstance || new HybridScanner({
videoId: 'libraryQrReaderVideo',
canvasId: 'libraryQrReaderCanvas',
formats: ['QR_CODE', 'EAN_13', 'EAN_8', 'CODE_128', 'CODE_39', 'UPC_A', 'UPC_E', 'ITF', 'CODABAR'],
fps: 10,
facingMode: 'environment',
onSuccess: handleScanSuccess,
onError: handleScanError
});
scannerInstance.start();
scannerRunning = true;
toggleBtn.textContent = 'Scanner stoppen';
setScanStatus('Scanner aktiv. Jetzt Code scannen.', 'warn');
} catch (err) {
console.error('Scanner start failed:', err);
readerWrap.style.display = 'none';
const detail = (err && (err.message || err.name)) ? ` (${err.message || err.name})` : '';
setScanStatus(`Scanner konnte nicht gestartet werden${detail}`, 'error');
}
}
async function stopScanner() {
if (!scannerRunning || !scannerInstance) return;
const readerWrap = document.getElementById('scanReaderWrap');
const toggleBtn = document.getElementById('toggleScannerBtn');
try {
scannerInstance.stop();
} catch (err) {
console.error('Scanner stop failed:', err);
}
scannerRunning = false;
scannerInstance = null;
readerWrap.style.display = 'none';
toggleBtn.textContent = 'Scanner starten';
setScanStatus('Scanner gestoppt.', 'warn');
}
// =========================================================================
// 5. EVENT LISTENERS INITIALIZATION & ON-LOAD INITIALIZER
// =========================================================================
function wireScannerUi() {
const toggleBtn = document.getElementById('toggleScannerBtn');
const resetBtn = document.getElementById('resetCardBtn');
const modeSelect = document.getElementById('scanModeSelect');
if (toggleBtn) {
toggleBtn.addEventListener('click', async () => {
if (scannerRunning) {
await stopScanner();
} else {
await startScanner();
await startScanner(null);
}
});
}
if (resetBtn) {
resetBtn.addEventListener('click', () => {
setActiveStudentCard('');
setScanStatus('Ausweis zurückgesetzt. Bitte neu scannen.', 'warn');
});
}
if (modeSelect) {
modeSelect.addEventListener('change', () => {
if (modeSelect.value === 'quick_toggle' && !activeStudentCardId) {
setScanStatus('Schnellmodus: zuerst Schülerausweis scannen.', 'warn');
} else if (modeSelect.value === 'card_only') {
setScanStatus('Modus aktiv: Nur Ausweis erfassen.', 'warn');
} else {
setScanStatus('Schnellmodus aktiv: jetzt Mediencode scannen.', 'warn');
setScanStatus('Nur Ausweis-Modus aktiv.', 'warn');
}
});
}
}
function getLibraryItemById(itemId) {
return libraryItems.find(i => String(i._id) === String(itemId));
}
// Run when DOM structure is entirely ready
document.addEventListener('DOMContentLoaded', () => {
wireScannerUi(); // Setup scanner control buttons
loadLibraryItems(); // Fetch your database items right away!
// Safely connect standard Filters and Search inputs inside DOMContentLoaded
const filterToggleBtn = document.getElementById('filterToggleBtn');
if (filterToggleBtn) {
filterToggleBtn.addEventListener('click', () => {
filterPanelOpen = !filterPanelOpen;
document.getElementById('filterPanel').classList.toggle('open');
filterToggleBtn.classList.toggle('active');
});
}
const applyFilterBtn = document.getElementById('applyFilterBtn');
if (applyFilterBtn) {
applyFilterBtn.addEventListener('click', () => {
activeFilters.isbn = document.getElementById('filterISBN').value.toLowerCase();
activeFilters.type = document.getElementById('filterType').value;
activeFilters.status = document.getElementById('filterStatus').value;
applyFiltersAndSearch(true);
});
}
const clearFilterBtn = document.getElementById('clearFilterBtn');
if (clearFilterBtn) {
clearFilterBtn.addEventListener('click', () => {
document.getElementById('filterISBN').value = '';
document.getElementById('filterType').value = '';
document.getElementById('filterStatus').value = '';
activeFilters = { isbn: '', type: '', status: '' };
applyFiltersAndSearch(true);
});
}
const librarySearch = document.getElementById('librarySearch');
if (librarySearch) {
librarySearch.addEventListener('input', (e) => {
currentSearchTerm = (e.target.value || '').toLowerCase();
applyFiltersAndSearch(true);
});
}
const loadMoreBtn = document.getElementById('loadMoreBtn');
if (loadMoreBtn) {
loadMoreBtn.addEventListener('click', () => {
renderedCount = Math.min(renderedCount + RENDER_BATCH_COUNT, visibleItems.length);
renderItems();
});
}
// Edit Modal Form processing
const editForm = document.getElementById('editLibraryForm');
if (editForm) {
editForm.addEventListener('submit', async function(e) {
e.preventDefault();
const itemId = document.getElementById('editLibraryItemId').value;
const updatedData = {
name: document.getElementById('editLibraryName').value,
item_type: document.getElementById('editLibraryType').value,
isbn: document.getElementById('editLibraryIsbn').value,
code_4: document.getElementById('editLibraryCode4').value,
ort: document.getElementById('editLibraryLocation').value,
beschreibung: document.getElementById('editLibraryDescription').value
};
try {
const response = await fetch(`/api/library_item/${itemId}/update`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': '{{ csrf_token }}',
'X-CSRF-Token': '{{ csrf_token }}'
},
body: JSON.stringify(updatedData)
});
const result = await response.json();
if (response.ok && result.ok) {
alert(result.message || 'Medium erfolgreich aktualisiert!');
closeEditLibraryModal();
pagingState.loading = false;
loadLibraryItems();
} else {
alert(result.message || 'Fehler beim Speichern der Änderungen.');
}
} catch (error) {
console.error('Update failed:', error);
alert('Netzwerkfehler beim Aktualisieren des Mediums.');
}
});
}
});
// Modal Control Functions (accessible globally from table buttons)
function openEditLibraryItem(itemId) {
const item = getLibraryItemById(itemId);
const item = libraryItems.find(i => i._id === itemId);
if (!item) {
alert('Element nicht gefunden.');
alert('Fehler: Das Medium konnte nicht gefunden werden.');
return;
}
const modal = document.getElementById('editLibraryModal');
// Populate the form fields - Using correct German properties from MongoDB fields
document.getElementById('editLibraryItemId').value = item._id || '';
document.getElementById('editLibraryName').value = item.Name || '';
document.getElementById('editLibraryAuthor').value = item.Autor || item.Author || '';
document.getElementById('editLibraryDescription').value = item.Beschreibung || '';
document.getElementById('editLibraryLocation').value = item.Ort || '';
document.getElementById('editLibraryType').value = item.ItemType || 'book';
document.getElementById('editLibraryIsbn').value = item.ISBN || '';
document.getElementById('editLibraryCode4').value = item.Code_4 || '';
document.getElementById('editLibraryType').value = (item.ItemType || 'book');
modal.style.display = 'flex';
document.getElementById('editLibraryCode4').value = item.Code_4 || item.Code4 || '';
document.getElementById('editLibraryLocation').value = item.Ort || '';
document.getElementById('editLibraryDescription').value = item.Beschreibung || '';
document.getElementById('editLibraryModal').style.display = 'block';
}
function closeEditLibraryModal() {
const modal = document.getElementById('editLibraryModal');
modal.style.display = 'none';
document.getElementById('editLibraryModal').style.display = 'none';
}
async function submitEditLibraryItem(event) {
event.preventDefault();
const itemId = document.getElementById('editLibraryItemId').value;
const payload = {
name: document.getElementById('editLibraryName').value,
autor: document.getElementById('editLibraryAuthor').value,
beschreibung: document.getElementById('editLibraryDescription').value,
ort: document.getElementById('editLibraryLocation').value,
isbn: document.getElementById('editLibraryIsbn').value,
code_4: document.getElementById('editLibraryCode4').value,
item_type: document.getElementById('editLibraryType').value
};
try {
const response = await fetch(`/api/library_item/${itemId}/update`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(payload)
});
const result = await response.json();
if (!response.ok || !result.ok) {
alert(result.message || 'Bearbeiten fehlgeschlagen.');
return;
}
closeEditLibraryModal();
await loadLibraryItems();
setScanStatus('Bibliotheksmedium aktualisiert.', 'ok');
} catch (err) {
console.error('Library edit failed:', err);
alert('Fehler beim Bearbeiten des Bibliotheksmediums.');
}
}
// Load on page init
document.addEventListener('DOMContentLoaded', () => {
loadLibraryItems();
wireScannerUi();
const editForm = document.getElementById('editLibraryForm');
if (editForm) {
editForm.addEventListener('submit', submitEditLibraryItem);
}
});
</script>
<div id="editLibraryModal" class="modal" style="display:none;">
@@ -1180,10 +1229,6 @@
<label for="editLibraryName">Titel</label>
<input id="editLibraryName" required>
</div>
<div>
<label for="editLibraryAuthor">Autor/Künstler</label>
<input id="editLibraryAuthor">
</div>
<div>
<label for="editLibraryType">Medientyp</label>
<select id="editLibraryType">
@@ -1218,4 +1263,4 @@
</form>
</div>
</div>
{% endblock %}
{% endblock %}
+104 -54
View File
@@ -413,10 +413,7 @@
</div>
<div class="qr-container">
<button id="scanButton" class="scan-button" aria-controls="qr-reader" aria-expanded="false">Barcode scannen</button>
<div id="qr-reader" style="display: none;">
<video id="qr-reader-video" playsinline style="width: 100%; max-width: 500px; border-radius: 8px;"></video>
<canvas id="qr-reader-canvas" style="display: none;"></canvas>
</div>
<div id="qr-reader"></div>
</div>
<div id="table-view-header" class="table-view-header" aria-hidden="true">
<span>Name</span>
@@ -528,9 +525,7 @@
window.isDebug = false; // Set to true only for development environment
</script>
<script src="https://unpkg.com/@zxing/library@latest/umd/index.min.js"></script>
<script src="/static/js/scanner.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@ericblade/quagga2/dist/quagga.js"></script>
<script>
// Global state
const highlightItemId = (window.serverVars && window.serverVars.highlightItemId && window.serverVars.highlightItemId !== 'null')
@@ -539,59 +534,114 @@
let codeSearchTerm = '';
let descSearchIds = null; // Set of matching IDs or null when disabled
// QR scanner toggle setup
document.addEventListener('DOMContentLoaded', function() {
const scanButton = document.getElementById('scanButton');
// Keep track of the scanner state globally/outer scope
let isScanning = false;
let activeScannerCallback = null; // Tracks which function currently owns the scanner output
function startScanner(targetCallback) {
const qrReader = document.getElementById('qr-reader');
let hybridScanner = null;
const statusText = document.getElementById('status');
if (!scanButton || !qrReader) return;
// Store the custom action that should happen when a barcode is found
activeScannerCallback = targetCallback;
const setScannerUi = (isOpen) => {
scanButton.classList.toggle('is-active', isOpen);
scanButton.setAttribute('aria-expanded', isOpen ? 'true' : 'false');
scanButton.textContent = isOpen ? 'Scanner schliessen' : 'Barcode scannen';
};
// Show the container
if (qrReader) qrReader.style.display = 'block';
if (statusText) statusText.innerText = "Initializing camera...";
scanButton.addEventListener('click', function() {
if (qrReader.style.display !== 'block') {
qrReader.style.display = 'block';
hybridScanner = new HybridScanner({
videoId: 'qr-reader-video',
canvasId: 'qr-reader-canvas',
formats: ['QR_CODE', 'CODE_128', 'EAN_13', 'EAN_8', 'CODE_39'],
fps: 10,
facingMode: 'environment',
onSuccess: function(decodedText) {
hybridScanner.stop();
qrReader.style.display = 'none';
// Put scanned code into the search box and trigger search
const searchInput = document.getElementById('code-search');
if (searchInput) {
searchInput.value = decodedText;
searchByCode();
}
setScannerUi(false);
},
onError: function(error) {
console.error('Scanner error:', error);
}
});
hybridScanner.start();
setScannerUi(true);
} else {
if (hybridScanner) {
hybridScanner.stop();
hybridScanner = null;
}
qrReader.style.display = 'none';
setScannerUi(false);
Quagga.init({
inputStream: {
name: "Live",
type: "LiveStream",
// Make sure this targets your correct HTML container element
target: document.querySelector('#scanner-container'),
constraints: {
width: 640,
height: 480,
facingMode: "environment" // Force rear camera
},
},
decoder: {
// Your required formats mapped to Quagga2 reader strings
readers: ["code_128_reader", "ean_reader", "code_39_reader", "upc_reader"]
}
}, function(err) {
if (err) {
console.error("Initialization error:", err);
if (statusText) statusText.innerText = "Camera Error";
return;
}
Quagga.start();
isScanning = true;
// If we are using the main search button, manage its UI state
if (!targetCallback) {
setScannerUi(true);
}
if (statusText) statusText.innerText = "Scanning...";
});
}
function stopScanner() {
Quagga.stop();
isScanning = false;
activeScannerCallback = null; // Reset the callback routing
const qrReader = document.getElementById('qr-reader');
if (qrReader) qrReader.style.display = 'none';
setScannerUi(false);
}
function setScannerUi(isOpen) {
const scanBtn = document.getElementById('scanButton');
if (!scanBtn) return;
scanBtn.classList.toggle('is-active', isOpen);
scanBtn.setAttribute('aria-expanded', isOpen ? 'true' : 'false');
scanBtn.textContent = isOpen ? 'Scanner schliessen' : 'Barcode scannen';
}
// Unified global reader callback
Quagga.onDetected(function(data) {
const barcode = String(data.codeResult.code || '').trim();
console.log("Barcode detected:", barcode);
// Keep a local reference to the active callback before shutting down the engine
const currentCallback = activeScannerCallback;
// Critical: Stop scanning immediately to prevent multiple triggers
stopScanner();
// Route data based on who opened the scanner
if (typeof currentCallback === "function") {
// Send data directly to the specific edit field's logic
currentCallback(barcode);
} else {
// Fallback Default: Main search box logic
const resultText = document.getElementById('barcode-result');
if (resultText) resultText.innerText = barcode;
const searchInput = document.getElementById('code-search');
if (searchInput) {
searchInput.value = barcode;
if (typeof searchByCode === "function") {
searchByCode();
}
}
}
});
// Event Listener for the main standalone search button
document.getElementById('scanButton')?.addEventListener('click', function() {
if (!isScanning) {
// Pass null so it defaults to filling the standard search box
startScanner(null);
} else {
stopScanner();
}
});
// Return preferred primary and fallback image URLs based on extension
+125 -100
View File
@@ -2436,8 +2436,8 @@ document.addEventListener('DOMContentLoaded', ()=>{
<div class="qr-container">
<button id="scanButton" class="scan-button" aria-controls="qr-reader" aria-expanded="false">Barcode scannen</button>
<div id="qr-reader" style="display: none;">
<video id="qr-reader-video" playsinline style="width: 100%; max-width: 100%; height: auto; aspect-ratio: 4/3; object-fit: cover; border-radius: 8px;"></video>
<canvas id="qr-reader-canvas" style="display: none;"></canvas>
<h3>Status: <span id="status">Bereit</span></h3>
<div id="scanner-container"></div>
</div>
</div>
@@ -2732,8 +2732,7 @@ document.addEventListener('DOMContentLoaded', ()=>{
window.isDebug = false; // Set to true only for development environment
</script>
<script src="https://unpkg.com/@zxing/library@latest/umd/index.min.js"></script>
<script src="/static/js/scanner.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@ericblade/quagga2/dist/quagga.js"></script>
<script>
// Function to check if a file is a video
function isVideoFile(filename) {
@@ -2743,7 +2742,6 @@ document.addEventListener('DOMContentLoaded', ()=>{
}
// Initialize QR Code scanner and global variables
let hybridScanner = null;
let codeSearchTerm = '';
let descSearchIds = null; // Set of matching IDs or null when disabled
let currentUsername = '';
@@ -2772,55 +2770,114 @@ document.addEventListener('DOMContentLoaded', ()=>{
// No-op in production
return;
}
document.getElementById('scanButton').addEventListener('click', function() {
// Keep track of the scanner state globally/outer scope
let isScanning = false;
let activeScannerCallback = null; // Tracks which function currently owns the scanner output
function startScanner(targetCallback) {
const qrReader = document.getElementById('qr-reader');
const scanBtn = document.getElementById('scanButton');
const statusText = document.getElementById('status');
const setScannerUi = (isOpen) => {
if (!scanBtn) return;
scanBtn.classList.toggle('is-active', isOpen);
scanBtn.setAttribute('aria-expanded', isOpen ? 'true' : 'false');
scanBtn.textContent = isOpen ? 'Scanner schliessen' : 'Barcode scannen';
};
if (qrReader.style.display === 'none') {
qrReader.style.display = 'block';
hybridScanner = new HybridScanner({
videoId: 'qr-reader-video',
canvasId: 'qr-reader-canvas',
formats: ['QR_CODE', 'CODE_128', 'EAN_13', 'EAN_8', 'CODE_39', 'UPC_A'],
fps: 10,
facingMode: 'environment',
onSuccess: function(decodedText) {
hybridScanner.stop();
qrReader.style.display = 'none';
// Put the scanned code in the search box
const searchInput = document.getElementById('code-search');
if (searchInput) {
searchInput.value = decodedText;
// Trigger search automatically
searchByCode();
}
// Store the custom action that should happen when a barcode is found
activeScannerCallback = targetCallback;
setScannerUi(false);
// Show the container
if (qrReader) qrReader.style.display = 'block';
if (statusText) statusText.innerText = "Initializing camera...";
Quagga.init({
inputStream: {
name: "Live",
type: "LiveStream",
// Make sure this targets your correct HTML container element
target: document.querySelector('#scanner-container'),
constraints: {
width: 640,
height: 480,
facingMode: "environment" // Force rear camera
},
onError: function(error) {
console.error('Scanner error:', error);
}
});
hybridScanner.start();
setScannerUi(true);
} else {
if (hybridScanner) {
hybridScanner.stop();
hybridScanner = null;
},
decoder: {
// Your required formats mapped to Quagga2 reader strings
readers: ["code_128_reader", "ean_reader", "code_39_reader", "upc_reader"]
}
}, function(err) {
if (err) {
console.error("Initialization error:", err);
if (statusText) statusText.innerText = "Camera Error";
return;
}
qrReader.style.display = 'none';
setScannerUi(false);
Quagga.start();
isScanning = true;
// If we are using the main search button, manage its UI state
if (!targetCallback) {
setScannerUi(true);
}
if (statusText) statusText.innerText = "Scanning...";
});
}
function stopScanner() {
Quagga.stop();
isScanning = false;
activeScannerCallback = null; // Reset the callback routing
const qrReader = document.getElementById('qr-reader');
if (qrReader) qrReader.style.display = 'none';
setScannerUi(false);
}
function setScannerUi(isOpen) {
const scanBtn = document.getElementById('scanButton');
if (!scanBtn) return;
scanBtn.classList.toggle('is-active', isOpen);
scanBtn.setAttribute('aria-expanded', isOpen ? 'true' : 'false');
scanBtn.textContent = isOpen ? 'Scanner schliessen' : 'Barcode scannen';
}
// Unified global reader callback
Quagga.onDetected(function(data) {
const barcode = String(data.codeResult.code || '').trim();
console.log("Barcode detected:", barcode);
// Keep a local reference to the active callback before shutting down the engine
const currentCallback = activeScannerCallback;
// Critical: Stop scanning immediately to prevent multiple triggers
stopScanner();
// Route data based on who opened the scanner
if (typeof currentCallback === "function") {
// Send data directly to the specific edit field's logic
currentCallback(barcode);
} else {
// Fallback Default: Main search box logic
const resultText = document.getElementById('barcode-result');
if (resultText) resultText.innerText = barcode;
const searchInput = document.getElementById('code-search');
if (searchInput) {
searchInput.value = barcode;
if (typeof searchByCode === "function") {
searchByCode();
}
}
}
});
// Event Listener for the main standalone search button
document.getElementById('scanButton')?.addEventListener('click', function() {
if (!isScanning) {
// Pass null so it defaults to filling the standard search box
startScanner(null);
} else {
stopScanner();
}
});
@@ -2832,37 +2889,21 @@ document.addEventListener('DOMContentLoaded', ()=>{
return;
}
if (qrReader.style.display !== 'none') {
if (hybridScanner) {
hybridScanner.stop();
hybridScanner = null;
}
qrReader.style.display = 'none';
// Toggle close if it's already running
if (isScanning && qrReader.style.display !== 'none') {
stopScanner();
scanEditBtn.textContent = 'Barcode scannen';
return;
}
qrReader.style.display = 'block';
scanEditBtn.textContent = 'Scanner schließen';
hybridScanner = new HybridScanner({
videoId: 'qr-reader-video',
canvasId: 'qr-reader-canvas',
formats: ['CODE_128', 'CODE_39', 'QR_CODE'],
fps: 10,
facingMode: 'environment',
onSuccess: function(decodedText) {
hybridScanner.stop();
qrReader.style.display = 'none';
editCodeInput.value = String(decodedText || '').trim();
validateCodeField(editCodeInput, document.getElementById('edit-item-id')?.value || null);
scanEditBtn.textContent = 'Barcode scannen';
},
onError: function(error) {
console.error('Scanner error:', error);
}
// Start scanner with custom logic mapping to the Code input field
startScanner(function(decodedText) {
editCodeInput.value = decodedText;
validateCodeField(editCodeInput, document.getElementById('edit-item-id')?.value || null);
scanEditBtn.textContent = 'Barcode scannen';
});
hybridScanner.start();
}
function scanIntoEditIsbn() {
@@ -2873,39 +2914,23 @@ document.addEventListener('DOMContentLoaded', ()=>{
return;
}
if (qrReader.style.display !== 'none') {
if (hybridScanner) {
hybridScanner.stop();
hybridScanner = null;
}
qrReader.style.display = 'none';
// Toggle close if it's already running
if (isScanning && qrReader.style.display !== 'none') {
stopScanner();
scanIsbnBtn.textContent = 'ISBN scannen';
return;
}
qrReader.style.display = 'block';
scanIsbnBtn.textContent = 'Scanner schließen';
hybridScanner = new HybridScanner({
videoId: 'qr-reader-video',
canvasId: 'qr-reader-canvas',
formats: ['ISBN', 'EAN_13', 'EAN_8', 'QR_CODE'],
fps: 10,
facingMode: 'environment',
onSuccess: function(decodedText) {
hybridScanner.stop();
qrReader.style.display = 'none';
editIsbnInput.value = String(decodedText || '').trim();
scanIsbnBtn.textContent = 'ISBN scannen';
if (typeof fetchBookInfo === 'function') {
fetchBookInfo('edit');
}
},
onError: function(error) {
console.error('Scanner error:', error);
// Start scanner with custom logic mapping to the ISBN input field
startScanner(function(decodedText) {
editIsbnInput.value = decodedText;
scanIsbnBtn.textContent = 'ISBN scannen';
if (typeof fetchBookInfo === 'function') {
fetchBookInfo('edit');
}
});
hybridScanner.start();
}
function rebuildFilter3Options() {
+113 -277
View File
@@ -876,10 +876,7 @@
<button type="button" id="scan-code4-btn" class="fetch-isbn-button">Barcode scannen</button>
</div>
<small style="display:block; color:#666;">Einzelcode manuell setzen oder per Scanner erfassen.</small>
<div id="code4-scanner" style="width:100%; max-width:520px; display:none; margin-top:10px;">
<video id="code4-scanner-video" playsinline style="width: 100%; border-radius: 8px;"></video>
<canvas id="code4-scanner-canvas" style="display: none;"></canvas>
</div>
<div id="code4-scanner" style="width:100%; max-width:520px; display:none; margin-top:10px;"></div>
<small id="code4-scan-status" style="display:block; color:#666; margin-top:6px;"></small>
</div>
<div class="form-group">
@@ -910,10 +907,7 @@
<button type="button" id="scan-isbn-btn" class="fetch-isbn-button">Barcode scannen</button>
<button type="button" class="fetch-isbn-button" onclick="fetchBookInfo('upload')">Bild abrufen</button>
</div>
<div id="isbn-scanner" style="width:100%; max-width:520px; display:none; margin-top:10px;">
<video id="isbn-scanner-video" playsinline style="width: 100%; border-radius: 8px;"></video>
<canvas id="isbn-scanner-canvas" style="display: none;"></canvas>
</div>
<div id="isbn-scanner" style="width:100%; max-width:520px; display:none; margin-top:10px;"></div>
<small id="isbn-scan-status" style="display:block; color:#666; margin-top:6px;">Scannen oder manuell eingeben. Gültige ISBNs helfen beim Abruf von Buchdaten, andere Codes werden trotzdem akzeptiert.</small>
<div id="book-info-container" class="book-info-container"></div>
</div>
@@ -976,9 +970,7 @@
document.getElementById('csv-help-modal').style.display = 'none';
}
</script>
<script src="https://unpkg.com/@zxing/library@latest/umd/index.min.js"></script>
<script src="/static/js/scanner.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@ericblade/quagga2/dist/quagga.js"></script>
<script>
const libraryModuleEnabled = {{ 'true' if library_module_enabled else 'false' }};
@@ -991,12 +983,12 @@
// Global variable to store fetched book data
let currentBookData = null;
let code4ScannerInstance = null;
let code4ScannerRunning = false;
// Quagga2 engine state management properties
let scannerRunning = false;
let activeScannerCallback = null; // Dynamically handles routing data to the active caller field
let code4LastScanned = '';
let code4LastScannedAt = 0;
let isbnScannerInstance = null;
let isbnScannerRunning = false;
function setCode4ScanStatus(message, isError = false) {
const statusEl = document.getElementById('code4-scan-status');
@@ -1049,66 +1041,109 @@
statusEl.style.color = '#b00020';
}
// Centered Quagga2 Engine Initialization Pattern
function runEngineInitialization(targetSelector, activeCallback, completionMsg, errorStatusSetter) {
if (scannerRunning) {
Quagga.stop();
scannerRunning = false;
}
activeScannerCallback = activeCallback;
Quagga.init({
inputStream: {
name: "Live",
type: "LiveStream",
target: document.querySelector(targetSelector),
constraints: {
width: 640,
height: 480,
facingMode: "environment" // Force rear camera lenses
},
},
decoder: {
readers: ["code_128_reader", "ean_reader", "code_39_reader", "upc_reader"]
}
}, function(err) {
if (err) {
console.error("Initialization error:", err);
errorStatusSetter("Kamera konnte nicht gestartet werden.", true);
return;
}
Quagga.start();
scannerRunning = true;
errorStatusSetter(completionMsg, false);
});
}
function killScannerHardware() {
if (!scannerRunning) return;
Quagga.stop();
scannerRunning = false;
activeScannerCallback = null;
}
// Unified tracking capture listener
Quagga.onDetected(function(data) {
if (!data || !data.codeResult || !data.codeResult.code) return;
const rawBarcode = String(data.codeResult.code || '').trim();
const currentTargetCallback = activeScannerCallback;
if (typeof currentTargetCallback === "function") {
currentTargetCallback(rawBarcode);
}
});
function startCode4Scanner() {
const scannerBox = document.getElementById('code4-scanner');
const scanButton = document.getElementById('scan-code4-btn');
const codeField = document.getElementById('code_4');
if (!scannerBox || !scanButton || !codeField) return;
// Stop ISBN scanner if currently running to avoid camera conflicts.
if (isbnScannerRunning && isbnScannerInstance) {
isbnScannerInstance.stop();
isbnScannerInstance = null;
const isbnScannerBox = document.getElementById('isbn-scanner');
const isbnScanButton = document.getElementById('scan-isbn-btn');
// Automatically clean up running ISBN scanner to prevent track multi-binding
const isbnScannerBox = document.getElementById('isbn-scanner');
const isbnScanButton = document.getElementById('scan-isbn-btn');
if (isbnScannerBox && isbnScannerBox.style.display !== 'none') {
killScannerHardware();
if (isbnScannerBox) isbnScannerBox.style.display = 'none';
if (isbnScanButton) isbnScanButton.textContent = 'ISBN scannen';
isbnScannerRunning = false;
setIsbnScanStatus('Scanner gestoppt.');
}
if (code4ScannerRunning && code4ScannerInstance) {
code4ScannerInstance.stop();
code4ScannerInstance = null;
if (scannerBox.style.display !== 'none') {
killScannerHardware();
scannerBox.style.display = 'none';
scanButton.textContent = 'Barcode scannen';
code4ScannerRunning = false;
setCode4ScanStatus('Scanner gestoppt.');
return;
}
scannerBox.style.display = 'block';
scanButton.textContent = 'Scanner stoppen';
code4ScannerRunning = true;
setCode4ScanStatus('Scanner läuft. Der Scan wird im Basis-Codefeld übernommen.');
code4ScannerInstance = new HybridScanner({
videoId: 'code4-scanner-video',
canvasId: 'code4-scanner-canvas',
formats: ['CODE_128', 'CODE_39', 'QR_CODE'],
fps: 10,
facingMode: 'environment',
onSuccess: function(decodedText) {
const scannedCode = String(decodedText || '').trim();
if (!scannedCode) return;
runEngineInitialization(
'#code4-scanner',
function(decodedText) {
const now = Date.now();
if (scannedCode === code4LastScanned && (now - code4LastScannedAt) < 1500) {
if (decodedText === code4LastScanned && (now - code4LastScannedAt) < 1500) {
return;
}
code4LastScanned = scannedCode;
code4LastScanned = decodedText;
code4LastScannedAt = now;
codeField.value = scannedCode;
validateCodeField(codeField);
setCode4ScanStatus(`Code_4 gesetzt: ${scannedCode}`);
},
onError: function(error) {
setCode4ScanStatus(`Fehler: ${error}`, true);
}
});
killScannerHardware();
scannerBox.style.display = 'none';
scanButton.textContent = 'Barcode scannen';
code4ScannerInstance.start();
codeField.value = decodedText;
validateCodeField(codeField);
setCode4ScanStatus(`Code_4 gesetzt: ${decodedText}`);
},
'Scanner läuft. Der Scan wird im Basis-Codefeld übernommen.',
setCode4ScanStatus
);
}
function startIsbnScanner() {
@@ -1121,72 +1156,49 @@
const isbnField = document.getElementById('isbn');
if (!scannerBox || !scanButton || !isbnField) return;
// Stop Code_4 scanner if currently running to avoid camera conflicts.
if (code4ScannerRunning && code4ScannerInstance) {
code4ScannerInstance.stop();
code4ScannerInstance = null;
const codeScannerBox = document.getElementById('code4-scanner');
const codeScanButton = document.getElementById('scan-code4-btn');
// Automatically clean up running Base Code_4 scanner to prevent track multi-binding
const codeScannerBox = document.getElementById('code4-scanner');
const codeScanButton = document.getElementById('scan-code4-btn');
if (codeScannerBox && codeScannerBox.style.display !== 'none') {
killScannerHardware();
if (codeScannerBox) codeScannerBox.style.display = 'none';
if (codeScanButton) codeScanButton.textContent = 'Barcode scannen';
code4ScannerRunning = false;
setCode4ScanStatus('Scanner gestoppt.');
}
if (isbnScannerRunning && isbnScannerInstance) {
isbnScannerInstance.stop();
isbnScannerInstance = null;
if (scannerBox.style.display !== 'none') {
killScannerHardware();
scannerBox.style.display = 'none';
scanButton.textContent = 'ISBN scannen';
isbnScannerRunning = false;
setIsbnScanStatus('Scanner gestoppt.');
return;
}
scannerBox.style.display = 'block';
scanButton.textContent = 'Scanner stoppen';
isbnScannerRunning = true;
setIsbnScanStatus('Scanner läuft. Bitte ISBN-Code erfassen.');
isbnScannerInstance = new HybridScanner({
videoId: 'isbn-scanner-video',
canvasId: 'isbn-scanner-canvas',
formats: ['ISBN', 'EAN_13', 'EAN_8', 'QR_CODE'],
fps: 10,
facingMode: 'environment',
onSuccess: function(decodedText) {
const scannedCode = String(decodedText || '').trim();
if (!scannedCode) return;
runEngineInitialization(
'#isbn-scanner',
function(decodedText) {
killScannerHardware();
scannerBox.style.display = 'none';
scanButton.textContent = 'ISBN scannen';
const normalizedIsbn = normalizeIsbnClient(scannedCode);
isbnField.value = normalizedIsbn || scannedCode;
updateIsbnLiveValidation();
if (normalizedIsbn) {
setIsbnScanStatus(`ISBN gesetzt: ${normalizedIsbn}`);
} else {
setIsbnScanStatus('Kein gültiges ISBN-Format erkannt, der gescannte Wert bleibt aber im Feld.', true);
}
const normalizedIsbn = normalizeIsbnClient(decodedText);
isbnField.value = normalizedIsbn || decodedText;
updateIsbnLiveValidation();
// Stop scanner after successful scan
if (isbnScannerInstance) {
isbnScannerInstance.stop();
isbnScannerInstance = null;
}
scannerBox.style.display = 'none';
scanButton.textContent = 'ISBN scannen';
isbnScannerRunning = false;
if (normalizedIsbn) {
// Automatically load book metadata after a valid ISBN scan.
fetchBookInfo('upload');
}
if (normalizedIsbn) {
setIsbnScanStatus(`ISBN gesetzt: ${normalizedIsbn}`);
// Automatically load book metadata after a valid ISBN scan.
fetchBookInfo('upload');
} else {
setIsbnScanStatus('Kein gültiges ISBN-Format erkannt, der gescannte Wert bleibt aber im Feld.', true);
}
},
onError: function(error) {
setIsbnScanStatus(`Fehler: ${error}`, true);
}
});
isbnScannerInstance.start();
'Scanner läuft. Bitte ISBN-Code erfassen.',
setIsbnScanStatus
);
}
// Load predefined filter values for dropdowns
@@ -1588,6 +1600,7 @@
}, 10000);
}
// Exported function for use within parent layout fields
function validateCodeField(input, excludeId = null) {
const code = input.value.trim();
@@ -1762,188 +1775,11 @@
if (typeof duplicateData.images === 'string') {
try {
duplicateData.images = JSON.parse(duplicateData.images);
console.log("Parsed images from string:", duplicateData.images);
} catch (e) {
console.warn("Failed to parse images string, using as single item:", duplicateData.images);
duplicateData.images = [duplicateData.images];
console.error("Error parsing duplicate images array:", e);
}
}
console.log(`Final duplicateData.images (${Array.isArray(duplicateData.images) ? 'array' : typeof duplicateData.images}):`, duplicateData.images);
}
}
// Then check for sessionStorage data (takes priority)
const sessionData = sessionStorage.getItem('duplicateItemData');
if (sessionData) {
try {
duplicateData = JSON.parse(sessionData);
sessionStorage.removeItem('duplicateItemData'); // Clean up
console.log("Session storage duplicate data:", duplicateData);
} catch (e) {
console.error('Error parsing session duplicate data:', e);
}
}
// Function to prefill form with duplicate data
function prefillFormWithDuplicateData() {
if (!duplicateData) return;
// Fill basic fields
const nameField = document.getElementById('name');
if (nameField && duplicateData.name) nameField.value = duplicateData.name;
const descField = document.getElementById('beschreibung');
if (descField && duplicateData.description) descField.value = duplicateData.description;
const yearField = document.getElementById('anschaffungsjahr');
if (yearField && duplicateData.year) yearField.value = duplicateData.year;
const costField = document.getElementById('anschaffungskosten');
if (costField && duplicateData.cost) costField.value = duplicateData.cost;
// Fill location dropdown
const locationField = document.getElementById('ort');
if (locationField && duplicateData.location) {
// Wait for locations to load, then set value
setTimeout(() => {
locationField.value = duplicateData.location;
}, 100);
}
// Fill filter arrays - handle up to 4 values per filter type
setTimeout(() => {
// Filter 1 (Unterrichtsfach)
if (duplicateData.filter1 && Array.isArray(duplicateData.filter1)) {
for (let j = 1; j <= Math.min(4, duplicateData.filter1.length); j++) {
const filterField = document.getElementById(`filter1-${j}`);
if (filterField && duplicateData.filter1[j-1]) {
filterField.value = duplicateData.filter1[j-1];
}
}
}
// Filter 2 (Jahrgangsstufe)
if (duplicateData.filter2 && Array.isArray(duplicateData.filter2)) {
for (let j = 1; j <= Math.min(4, duplicateData.filter2.length); j++) {
const filterField = document.getElementById(`filter2-${j}`);
if (filterField && duplicateData.filter2[j-1]) {
filterField.value = duplicateData.filter2[j-1];
}
}
}
// Filter 3 (Thema) - text inputs
if (duplicateData.filter3 && Array.isArray(duplicateData.filter3)) {
for (let j = 1; j <= Math.min(4, duplicateData.filter3.length); j++) {
const filterField = document.getElementById(`filter3-${j}`);
if (filterField && duplicateData.filter3[j-1]) {
filterField.value = duplicateData.filter3[j-1];
}
}
}
}, 200); // Wait for filter options to load
// Handle images - add hidden inputs for duplicate images
if (duplicateData.images && duplicateData.images.length > 0) {
console.log("Duplicate images data:", duplicateData.images);
const form = document.querySelector('form[action*="upload_item"]');
if (form) {
// Add hidden field to indicate duplication
const isDuplicatingField = document.createElement('input');
isDuplicatingField.type = 'hidden';
isDuplicatingField.name = 'is_duplicating';
isDuplicatingField.value = 'true';
form.appendChild(isDuplicatingField);
// Add each image as a duplicate image
const images = duplicateData.images || [];
console.log(`Adding ${images.length} images as hidden fields`);
if (Array.isArray(images)) {
images.forEach((imageName, index) => {
if (imageName) {
console.log(`Adding duplicate image ${index}: ${imageName}`);
const imageField = document.createElement('input');
imageField.type = 'hidden';
imageField.name = 'duplicate_images';
imageField.value = imageName;
form.appendChild(imageField);
}
});
} else {
console.warn("Images is not an array:", images);
// Try to handle as single item if it's a string
if (typeof images === 'string' && images) {
console.log("Adding single image:", images);
const imageField = document.createElement('input');
imageField.type = 'hidden';
imageField.name = 'duplicate_images';
imageField.value = images;
form.appendChild(imageField);
}
}
// Show image preview
showDuplicateImagePreview(duplicateData.images);
}
}
}
// Function to show preview of duplicate images
function showDuplicateImagePreview(images) {
const previewContainer = document.getElementById('image-preview-container');
if (!previewContainer) return;
console.log("showDuplicateImagePreview called with:", images);
// Make sure images is treated as an array
if (!Array.isArray(images)) {
if (typeof images === 'string') {
// Try to parse if it's a JSON string
try {
images = JSON.parse(images);
} catch (e) {
// If parsing fails, wrap the single string in an array
images = [images];
}
} else {
console.error("Invalid images data:", images);
return;
}
}
previewContainer.innerHTML = '';
console.log(`Processing ${images.length} images for preview`);
images.forEach((imageName, index) => {
console.log(`Processing image ${index}:`, imageName);
const preview = document.createElement('div');
preview.className = 'image-preview-item';
preview.innerHTML = `
<img src="/uploads/${imageName}" alt="Duplicate Image ${index + 1}" class="duplicate-image-preview">
<div class="image-controls">
<span>Von Original übernommen</span>
<button type="button" onclick="removeDuplicateImage('${imageName}', this)" class="remove-duplicate-image-button">Entfernen</button>
</div>
`;
previewContainer.appendChild(preview);
});
}
// Function to remove a duplicate image
function removeDuplicateImage(imageName, button) {
// Remove the hidden input
const hiddenInputs = document.querySelectorAll('input[name="duplicate_images"]');
hiddenInputs.forEach(input => {
if (input.value === imageName) {
input.remove();
}
});
// Remove the preview
const previewItem = button.closest('.image-preview-item');
if (previewItem) {
previewItem.remove();
console.log("Parsed duplicate images successfully.");
}
}
</script>
-54
View File
@@ -1,54 +0,0 @@
#!/usr/bin/env python3
"""
Small debug script to check tenant-aware lookup for an appointment id.
Usage: ./tools/debug_get_appointment.py <appointment_id> [tenant]
"""
import sys
from bson.objectid import ObjectId
from Web.modules.database.settings import MongoClient, MONGODB_HOST, MONGODB_PORT, MONGODB_DB
import Web.modules.database.termine as termine
def main():
if len(sys.argv) < 2:
print("Usage: debug_get_appointment.py <appointment_id> [tenant]")
sys.exit(2)
aid = sys.argv[1]
tenant = sys.argv[2] if len(sys.argv) > 2 else None
if tenant:
print(f"Looking up appointment {aid} for tenant {tenant}")
else:
print(f"Looking up appointment {aid} for default tenant")
try:
# Use the module helper directly
item = termine.get_item(aid)
if item:
print('Found with termini.get_item:')
print(item)
else:
print('termin.get_item returned None')
# Try explicit MongoClient + tenant DB resolution
client = MongoClient(MONGODB_HOST, MONGODB_PORT)
try:
from Web.tenant import TenantContext, get_tenant_db
if tenant:
ctx = TenantContext()
ctx.tenant_id = tenant
db = ctx.get_database(client)
else:
db = get_tenant_db(client) if 'get_tenant_db' in dir() else client[MONGODB_DB]
doc = db['appointments'].find_one({'_id': ObjectId(aid)})
print('Direct DB find_one returned:')
print(doc)
finally:
client.close()
except Exception as e:
print('Error during debug lookup:', e)
if __name__ == '__main__':
main()