Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1b38d1f0c8 | |||
| e7e24b3fae | |||
| fc80857fbe | |||
| f763ad064d | |||
| bc94ffdc18 | |||
| 76e9670cee | |||
| 577c6c4bed | |||
| 3aa19b10d1 | |||
| ff742d018e | |||
| 6fa40f26b0 | |||
| 777fc81064 | |||
| cf5e38e319 | |||
| b76941d5fe | |||
| f46f2674b0 | |||
| 8ebbdcfd54 | |||
| 92b5235035 | |||
| 4bda2e044b | |||
| b83b6c0ba2 |
@@ -15,15 +15,6 @@ on:
|
|||||||
- patch
|
- patch
|
||||||
- minor
|
- minor
|
||||||
- major
|
- 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:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
@@ -48,13 +39,12 @@ jobs:
|
|||||||
EVENT_NAME: ${{ github.event_name }}
|
EVENT_NAME: ${{ github.event_name }}
|
||||||
REF_NAME: ${{ github.ref_name }}
|
REF_NAME: ${{ github.ref_name }}
|
||||||
BUMP_TYPE: ${{ github.event.inputs.bump || 'patch' }}
|
BUMP_TYPE: ${{ github.event.inputs.bump || 'patch' }}
|
||||||
PUSH_DEV: ${{ github.event.inputs.push_dev || 'false' }}
|
|
||||||
run: |
|
run: |
|
||||||
if [ "$EVENT_NAME" = "push" ] && [ -n "$REF_NAME" ]; then
|
if [ "$EVENT_NAME" = "push" ] && [ -n "$REF_NAME" ]; then
|
||||||
TAG="$REF_NAME"
|
TAG="$REF_NAME"
|
||||||
else
|
else
|
||||||
# Fetch latest release tag via GitHub API (fall back to v3.0.0)
|
# Fetch latest release tag via GitHub API (fall back to v0.8.31)
|
||||||
latest_tag="v3.0.0"
|
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
|
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)
|
tag_name=$(printf "%s" "$meta_json" | sed -n 's/.*"tag_name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1)
|
||||||
if [ -n "$tag_name" ]; then
|
if [ -n "$tag_name" ]; then
|
||||||
@@ -67,7 +57,7 @@ jobs:
|
|||||||
minor=${BASH_REMATCH[2]}
|
minor=${BASH_REMATCH[2]}
|
||||||
patch=${BASH_REMATCH[3]}
|
patch=${BASH_REMATCH[3]}
|
||||||
else
|
else
|
||||||
major=3; minor=0; patch=0
|
major=0; minor=8; patch=31
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Bump strategy: major / minor / patch
|
# Bump strategy: major / minor / patch
|
||||||
@@ -78,30 +68,28 @@ jobs:
|
|||||||
else
|
else
|
||||||
patch=$((patch + 1))
|
patch=$((patch + 1))
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ "${BUMP_TYPE:-}" = "development" ]; then
|
# Zusammenbau des Tags für den manuellen Run
|
||||||
TAG="v${major}.${minor}.${patch}-dev"
|
TAG="v${major}.${minor}.${patch}"
|
||||||
else
|
|
||||||
TAG="v${major}.${minor}.${patch}"
|
|
||||||
fi
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Validierung des erzeugten oder übergebenen Tags
|
||||||
if ! echo "$TAG" | grep -Eq '^v[0-9]+\.[0-9]+\.[0-9]+(-dev(\.[0-9]+)?)?$'; then
|
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
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
git fetch --tags --force
|
git fetch --tags --force
|
||||||
LATEST_TAG="$(git tag -l 'v*' | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | sort -V | tail -n1)"
|
LATEST_TAG="$(git tag -l 'v*' | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | sort -V | tail -n1)"
|
||||||
if [ -z "$LATEST_TAG" ]; then
|
if [ -z "$LATEST_TAG" ]; then
|
||||||
LATEST_TAG="v3.0.0"
|
LATEST_TAG="v0.8.31"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
TAG_MAJOR="${TAG#v}"
|
TAG_MAJOR="${TAG#v}"
|
||||||
TAG_MAJOR="${TAG_MAJOR%%.*}"
|
TAG_MAJOR="${TAG_MAJOR%%.*}"
|
||||||
LATEST_MAJOR="$(echo "$LATEST_TAG" | grep -Eo '^v[0-9]+' | tr -d 'v')"
|
LATEST_MAJOR="$(echo "$LATEST_TAG" | grep -Eo '^v[0-9]+' | tr -d 'v')"
|
||||||
if [ -z "$LATEST_MAJOR" ]; then
|
if [ -z "$LATEST_MAJOR" ]; then
|
||||||
LATEST_MAJOR="3"
|
LATEST_MAJOR="0"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# If not explicitly bumping major, disallow changing major version
|
# If not explicitly bumping major, disallow changing major version
|
||||||
@@ -123,33 +111,6 @@ jobs:
|
|||||||
IMAGE="ghcr.io/aiirondev/legendary-octo-garbanzo:${TAG}"
|
IMAGE="ghcr.io/aiirondev/legendary-octo-garbanzo:${TAG}"
|
||||||
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
|
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
|
||||||
echo "image=$IMAGE" >> "$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
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v3
|
||||||
@@ -162,26 +123,16 @@ jobs:
|
|||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Build and push release image
|
- name: Build and push release image
|
||||||
if: steps.meta.outputs.is_development != 'true'
|
|
||||||
uses: docker/build-push-action@v6
|
uses: docker/build-push-action@v6
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: ./Dockerfile
|
file: ./Dockerfile
|
||||||
push: true
|
push: true
|
||||||
|
load: true # Lädt das Image in den lokalen Docker-Daemon für den 'docker save' Schritt
|
||||||
tags: |
|
tags: |
|
||||||
${{ steps.meta.outputs.image }}
|
${{ steps.meta.outputs.image }}
|
||||||
ghcr.io/aiirondev/legendary-octo-garbanzo:latest
|
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
|
- name: Build local image tar for offline deploy
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
@@ -204,30 +155,17 @@ jobs:
|
|||||||
docker build -t "$IMG" .
|
docker build -t "$IMG" .
|
||||||
docker save "$IMG" | gzip > "inventarsystem-image-${TAG}.tar.gz"
|
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
|
- name: Create release-only docker bundle
|
||||||
run: |
|
run: |
|
||||||
mkdir -p release-bundle
|
mkdir -p release-bundle
|
||||||
cat > release-bundle/docker-compose.yml <<EOF
|
cat > release-bundle/docker-compose.yml <<EOF
|
||||||
services:
|
services:
|
||||||
app:
|
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
|
container_name: inventarsystem-app
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
ports:
|
ports:
|
||||||
- "${INVENTAR_HTTP_PORT:-10000}:8000"
|
- "\${INVENTAR_HTTP_PORT:-10000}:8000"
|
||||||
depends_on:
|
depends_on:
|
||||||
- mongodb
|
- mongodb
|
||||||
- redis
|
- redis
|
||||||
@@ -300,17 +238,6 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
done
|
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
|
# Make any shipped scripts executable
|
||||||
find release-bundle -maxdepth 1 -type f -name '*.sh' -exec chmod +x {} \; || true
|
find release-bundle -maxdepth 1 -type f -name '*.sh' -exec chmod +x {} \; || true
|
||||||
tar -czf inventarsystem-docker-bundle.tar.gz -C release-bundle .
|
tar -czf inventarsystem-docker-bundle.tar.gz -C release-bundle .
|
||||||
@@ -319,10 +246,8 @@ jobs:
|
|||||||
uses: softprops/action-gh-release@v2
|
uses: softprops/action-gh-release@v2
|
||||||
with:
|
with:
|
||||||
tag_name: ${{ steps.meta.outputs.tag }}
|
tag_name: ${{ steps.meta.outputs.tag }}
|
||||||
prerelease: ${{ steps.meta.outputs.is_development }}
|
|
||||||
files: |
|
files: |
|
||||||
inventarsystem-docker-bundle.tar.gz
|
inventarsystem-docker-bundle.tar.gz
|
||||||
inventarsystem-image-${{ steps.meta.outputs.tag }}.tar.gz
|
inventarsystem-image-${{ steps.meta.outputs.tag }}.tar.gz
|
||||||
inventarsystem-image-dev.tar.gz
|
|
||||||
fail_on_unmatched_files: false
|
fail_on_unmatched_files: false
|
||||||
generate_release_notes: true
|
generate_release_notes: true
|
||||||
+1
-1
@@ -1 +1 @@
|
|||||||
v0.8.29
|
v0.8.31.1
|
||||||
|
|||||||
-60
@@ -3484,66 +3484,6 @@ def api_library_scan_action():
|
|||||||
client.close()
|
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>')
|
@app.route('/api/item_detail/<item_id>')
|
||||||
def api_item_detail(item_id):
|
def api_item_detail(item_id):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -15,4 +15,3 @@ openpyxl
|
|||||||
cryptography>=42.0.0
|
cryptography>=42.0.0
|
||||||
pywebpush
|
pywebpush
|
||||||
py-vapid>=1.9.0
|
py-vapid>=1.9.0
|
||||||
pyzbar
|
|
||||||
|
|||||||
@@ -1,296 +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;
|
|
||||||
}
|
|
||||||
|
|
||||||
const script = document.createElement('script');
|
|
||||||
script.src = 'https://unpkg.com/@zxing/library@latest/umd/index.min.js';
|
|
||||||
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.readyState === video.HAVE_ENOUGH_DATA) {
|
|
||||||
canvas.width = video.videoWidth;
|
|
||||||
canvas.height = video.videoHeight;
|
|
||||||
const context = canvas.getContext('2d');
|
|
||||||
|
|
||||||
if (context) {
|
|
||||||
context.drawImage(video, 0, 0);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const imageData = context.getImageData(0, 0, canvas.width, canvas.height);
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Throttle scan loop
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
+59
-197
@@ -10,24 +10,6 @@
|
|||||||
<h1>📚 Bibliothek</h1>
|
<h1>📚 Bibliothek</h1>
|
||||||
<p class="subtitle">Bücher, CDs und weitere Medien</p>
|
<p class="subtitle">Bücher, CDs und weitere Medien</p>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
|
|
||||||
<!-- Search & Filter Section -->
|
<!-- Search & Filter Section -->
|
||||||
@@ -67,22 +49,8 @@
|
|||||||
|
|
||||||
<!-- Scanner Panel -->
|
<!-- Scanner Panel -->
|
||||||
<div id="qrContainer" class="qr-container" style="display: none;">
|
<div id="qrContainer" class="qr-container" style="display: none;">
|
||||||
<div class="scanner-header">
|
<div id="qr-reader" style="width: auto; height: 300px;"></div>
|
||||||
<h3>Barcode/QR-Code Scanner</h3>
|
<span id="qr-result" style="display: none;"></span>
|
||||||
<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: 500px; 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.9em; color: #999; margin-top: 10px; text-align: center;">
|
|
||||||
💡 Hinweis: Scan-Ergebnis wird automatisch in das Feld oben übernommen. USB-Scanner werden als Tastatureingabe erkannt.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Table Header (only in table mode) -->
|
<!-- Table Header (only in table mode) -->
|
||||||
@@ -219,9 +187,7 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/@ericblade/quagga2/dist/quagga.js"></script>
|
||||||
<script src="https://unpkg.com/@zxing/library@latest/umd/index.min.js"></script>
|
|
||||||
<script src="/static/js/scanner.js"></script>
|
|
||||||
<script>
|
<script>
|
||||||
// View mode persistence
|
// View mode persistence
|
||||||
const LIBRARY_VIEW_MODE_KEY = 'inventarLibraryViewMode';
|
const LIBRARY_VIEW_MODE_KEY = 'inventarLibraryViewMode';
|
||||||
@@ -640,103 +606,80 @@ document.getElementById('clearFiltersBtn').addEventListener('click', function()
|
|||||||
closeAllFilters();
|
closeAllFilters();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Favorites toggle
|
// Scanner toggle
|
||||||
document.getElementById('favoriteToggle').addEventListener('click', function() {
|
// Scanner state tracking
|
||||||
this.classList.toggle('open');
|
let isScanning = false;
|
||||||
// TODO: Implement favorites filtering
|
|
||||||
});
|
|
||||||
|
|
||||||
// Scanner instance (hybrid: real-time + upload + keyboard)
|
|
||||||
let scanner = null;
|
|
||||||
|
|
||||||
document.getElementById('scannerBtn').addEventListener('click', function(e) {
|
document.getElementById('scannerBtn').addEventListener('click', function(e) {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
const container = document.getElementById('qrContainer');
|
const container = document.getElementById('qrContainer');
|
||||||
const isOpen = container.style.display !== 'none';
|
const isOpen = container.style.display !== 'none';
|
||||||
|
const btn = this;
|
||||||
|
|
||||||
if (isOpen) {
|
if (isOpen) {
|
||||||
|
// Close the scanner
|
||||||
container.style.display = 'none';
|
container.style.display = 'none';
|
||||||
if (scanner) {
|
Quagga.stop();
|
||||||
scanner.stop();
|
isScanning = false;
|
||||||
scanner = null;
|
|
||||||
}
|
btn.classList.remove('open');
|
||||||
this.classList.remove('open');
|
btn.setAttribute('aria-expanded', 'false');
|
||||||
this.setAttribute('aria-expanded', 'false');
|
|
||||||
} else {
|
} else {
|
||||||
|
// Open the scanner
|
||||||
container.style.display = 'block';
|
container.style.display = 'block';
|
||||||
this.classList.add('open');
|
btn.classList.add('open');
|
||||||
this.setAttribute('aria-expanded', 'true');
|
btn.setAttribute('aria-expanded', 'true');
|
||||||
|
|
||||||
if (!scanner) {
|
Quagga.init({
|
||||||
initScanner();
|
inputStream: {
|
||||||
}
|
name: "Live",
|
||||||
}
|
type: "LiveStream",
|
||||||
});
|
// Targets the element where the video stream will inject
|
||||||
|
target: document.querySelector('#qr-reader'),
|
||||||
function initScanner() {
|
constraints: {
|
||||||
scanner = new HybridScanner({
|
width: 640,
|
||||||
videoId: 'qr-reader',
|
height: 480,
|
||||||
canvasId: 'scanner-canvas',
|
facingMode: "environment" // Forces back camera
|
||||||
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
|
|
||||||
},
|
},
|
||||||
(error) => {
|
decoder: {
|
||||||
messageEl.textContent = '✗ ' + error;
|
// Optimized for 1D barcodes (e.g., student IDs, member cards)
|
||||||
messageEl.style.color = '#f44336';
|
readers: ["code_128_reader", "ean_reader", "code_39_reader", "upc_reader"]
|
||||||
e.target.value = '';
|
}
|
||||||
|
}, function(err) {
|
||||||
|
if (err) {
|
||||||
|
console.error("Initialization error:", err);
|
||||||
|
alert("Kamera konnte nicht gestartet werden.");
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
);
|
Quagga.start();
|
||||||
|
isScanning = true;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
function onScanSuccess(decodedText, decodedResult) {
|
// Single Quagga event listener for successful scans
|
||||||
const studentId = decodedText.trim();
|
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;
|
document.getElementById('studentIdInput').value = studentId;
|
||||||
alert('Ausweis gescannt: ' + studentId);
|
alert('Ausweis gescannt: ' + studentId);
|
||||||
}
|
});
|
||||||
|
|
||||||
function onScanError(error) {
|
|
||||||
// Silently ignore scan errors
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
@@ -805,86 +748,5 @@ function onScanError(error) {
|
|||||||
display: block; /* keep rows stacked but allow horizontal scroll */
|
display: block; /* keep rows stacked but allow horizontal scroll */
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Hybrid Scanner Styles */
|
|
||||||
.qr-container {
|
|
||||||
background: #f5f5f5;
|
|
||||||
border: 1px solid #ddd;
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 16px;
|
|
||||||
margin: 16px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.scanner-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 12px;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.scanner-header h3 {
|
|
||||||
margin: 0;
|
|
||||||
color: #333;
|
|
||||||
font-size: 1.1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.scanner-controls {
|
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.scanner-button {
|
|
||||||
background: #2196F3;
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
padding: 8px 12px;
|
|
||||||
border-radius: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 0.9em;
|
|
||||||
transition: background-color 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.scanner-button:hover {
|
|
||||||
background: #1976D2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.scanner-button:active {
|
|
||||||
background: #1565C0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.scanner-video-wrapper {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
margin: 12px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
#qr-reader {
|
|
||||||
border: 2px solid #2196F3;
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.scanner-message {
|
|
||||||
min-height: 20px;
|
|
||||||
font-size: 0.95em;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 600px) {
|
|
||||||
.scanner-header {
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: stretch;
|
|
||||||
}
|
|
||||||
|
|
||||||
.scanner-controls {
|
|
||||||
width: 100%;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
.scanner-button {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -445,7 +445,7 @@
|
|||||||
<!-- Customizable Filter Panel -->
|
<!-- Customizable Filter Panel -->
|
||||||
|
|
||||||
<!-- Student card / quick scan workflow -->
|
<!-- Student card / quick scan workflow -->
|
||||||
<div class="library-scan-panel">
|
<div class="library-scan-panel">
|
||||||
<div class="library-scan-controls">
|
<div class="library-scan-controls">
|
||||||
<select id="scanModeSelect" aria-label="Scan-Modus">
|
<select id="scanModeSelect" aria-label="Scan-Modus">
|
||||||
<option value="card_only">Nur Ausweis erfassen</option>
|
<option value="card_only">Nur Ausweis erfassen</option>
|
||||||
@@ -458,10 +458,9 @@
|
|||||||
<div id="scanStatus" class="library-scan-status">
|
<div id="scanStatus" class="library-scan-status">
|
||||||
Hinweis: Im Schnellmodus zuerst den Schülerausweis scannen, danach den Buch-/Mediencode.
|
Hinweis: Im Schnellmodus zuerst den Schülerausweis scannen, danach den Buch-/Mediencode.
|
||||||
</div>
|
</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">
|
<div id="libraryQrReader" class="library-scan-reader">
|
||||||
<video id="libraryQrReaderVideo" playsinline style="width: 100%; border-radius: 8px;"></video>
|
<div id="library-scanner-container"></div>
|
||||||
<canvas id="libraryQrReaderCanvas" style="display: none;"></canvas>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -550,8 +549,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="https://unpkg.com/@zxing/library@latest/umd/index.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/@ericblade/quagga2/dist/quagga.js"></script>
|
||||||
<script src="/static/js/scanner.js"></script>
|
|
||||||
<script>
|
<script>
|
||||||
// State
|
// State
|
||||||
let libraryItems = [];
|
let libraryItems = [];
|
||||||
@@ -851,10 +849,16 @@
|
|||||||
document.getElementById('detailModal').style.display = 'none';
|
document.getElementById('detailModal').style.display = 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Keep track of the scanner state globally/outer scope
|
||||||
|
let scannerRunning = false;
|
||||||
|
let lastScanValue = null;
|
||||||
|
let lastScanAt = 0;
|
||||||
|
let activeStudentCardId = ''; // Managed globally by your system
|
||||||
|
|
||||||
function borrowItem(itemId) {
|
function borrowItem(itemId) {
|
||||||
const selectedItem = (libraryItems || []).find(item => item._id === itemId);
|
const selectedItem = (libraryItems || []).find(item => item._id === itemId);
|
||||||
if (selectedItem && selectedItem.LibraryDisplayStatus === 'damaged') {
|
if (selectedItem && selectedItem.LibraryDisplayStatus === 'damaged') {
|
||||||
alert('Dieses Medium ist als defekt/zerstört markiert und kann nicht ausgeliehen werden.');
|
alert('Dieses Medium ist als defekt/zerstör markiert und kann nicht ausgeliehen werden.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1001,64 +1005,71 @@
|
|||||||
processQuickToggleScan(scannedCode);
|
processQuickToggleScan(scannedCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleScanError() {
|
// Global Quagga reader hook
|
||||||
// Intentionally silent to avoid UI spam while camera searches codes.
|
Quagga.onDetected(function(data) {
|
||||||
}
|
if (data && data.codeResult && data.codeResult.code) {
|
||||||
|
handleScanSuccess(data.codeResult.code);
|
||||||
async function ensureScannerLibraryLoaded() {
|
}
|
||||||
// HybridScanner is loaded globally via script tag
|
});
|
||||||
return typeof HybridScanner !== 'undefined';
|
|
||||||
}
|
|
||||||
|
|
||||||
async function startScanner() {
|
async function startScanner() {
|
||||||
if (scannerRunning) return;
|
if (scannerRunning) return;
|
||||||
|
|
||||||
const scannerLoaded = await ensureScannerLibraryLoaded();
|
|
||||||
if (!scannerLoaded) {
|
|
||||||
setScanStatus('Scanner-Bibliothek konnte nicht geladen werden.', 'error');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const readerWrap = document.getElementById('scanReaderWrap');
|
const readerWrap = document.getElementById('scanReaderWrap');
|
||||||
const toggleBtn = document.getElementById('toggleScannerBtn');
|
const toggleBtn = document.getElementById('toggleScannerBtn');
|
||||||
readerWrap.style.display = 'block';
|
if (readerWrap) readerWrap.style.display = 'block';
|
||||||
|
|
||||||
try {
|
setScanStatus('Initializing camera...', 'warn');
|
||||||
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();
|
Quagga.init({
|
||||||
|
inputStream: {
|
||||||
|
name: "Live",
|
||||||
|
type: "LiveStream",
|
||||||
|
// Points to the interior viewport layout box
|
||||||
|
target: document.querySelector('#libraryQrReader'),
|
||||||
|
constraints: {
|
||||||
|
width: 640,
|
||||||
|
height: 480,
|
||||||
|
facingMode: "environment" // Force rear mobile lenses
|
||||||
|
},
|
||||||
|
},
|
||||||
|
decoder: {
|
||||||
|
// Limited down precisely to standard library/ID configurations
|
||||||
|
readers: [
|
||||||
|
"code_128_reader",
|
||||||
|
"ean_reader",
|
||||||
|
"code_39_reader",
|
||||||
|
"upc_reader",
|
||||||
|
"codabar_reader",
|
||||||
|
"i2of5_reader" // Matches original 'ITF' format mapping
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}, 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;
|
scannerRunning = true;
|
||||||
toggleBtn.textContent = 'Scanner stoppen';
|
if (toggleBtn) toggleBtn.textContent = 'Scanner stoppen';
|
||||||
setScanStatus('Scanner aktiv. Jetzt Code scannen.', 'warn');
|
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() {
|
async function stopScanner() {
|
||||||
if (!scannerRunning || !scannerInstance) return;
|
if (!scannerRunning) return;
|
||||||
const readerWrap = document.getElementById('scanReaderWrap');
|
const readerWrap = document.getElementById('scanReaderWrap');
|
||||||
const toggleBtn = document.getElementById('toggleScannerBtn');
|
const toggleBtn = document.getElementById('toggleScannerBtn');
|
||||||
try {
|
|
||||||
scannerInstance.stop();
|
Quagga.stop();
|
||||||
} catch (err) {
|
|
||||||
console.error('Scanner stop failed:', err);
|
|
||||||
}
|
|
||||||
scannerRunning = false;
|
scannerRunning = false;
|
||||||
scannerInstance = null;
|
if (readerWrap) readerWrap.style.display = 'none';
|
||||||
readerWrap.style.display = 'none';
|
if (toggleBtn) toggleBtn.textContent = 'Scanner starten';
|
||||||
toggleBtn.textContent = 'Scanner starten';
|
|
||||||
setScanStatus('Scanner gestoppt.', 'warn');
|
setScanStatus('Scanner gestoppt.', 'warn');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+104
-54
@@ -413,10 +413,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="qr-container">
|
<div class="qr-container">
|
||||||
<button id="scanButton" class="scan-button" aria-controls="qr-reader" aria-expanded="false">Barcode scannen</button>
|
<button id="scanButton" class="scan-button" aria-controls="qr-reader" aria-expanded="false">Barcode scannen</button>
|
||||||
<div id="qr-reader" style="display: none;">
|
<div id="qr-reader"></div>
|
||||||
<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>
|
</div>
|
||||||
<div id="table-view-header" class="table-view-header" aria-hidden="true">
|
<div id="table-view-header" class="table-view-header" aria-hidden="true">
|
||||||
<span>Name</span>
|
<span>Name</span>
|
||||||
@@ -528,9 +525,7 @@
|
|||||||
|
|
||||||
window.isDebug = false; // Set to true only for development environment
|
window.isDebug = false; // Set to true only for development environment
|
||||||
</script>
|
</script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/@ericblade/quagga2/dist/quagga.js"></script>
|
||||||
<script src="https://unpkg.com/@zxing/library@latest/umd/index.min.js"></script>
|
|
||||||
<script src="/static/js/scanner.js"></script>
|
|
||||||
<script>
|
<script>
|
||||||
// Global state
|
// Global state
|
||||||
const highlightItemId = (window.serverVars && window.serverVars.highlightItemId && window.serverVars.highlightItemId !== 'null')
|
const highlightItemId = (window.serverVars && window.serverVars.highlightItemId && window.serverVars.highlightItemId !== 'null')
|
||||||
@@ -539,59 +534,114 @@
|
|||||||
let codeSearchTerm = '';
|
let codeSearchTerm = '';
|
||||||
let descSearchIds = null; // Set of matching IDs or null when disabled
|
let descSearchIds = null; // Set of matching IDs or null when disabled
|
||||||
|
|
||||||
// QR scanner toggle setup
|
// Keep track of the scanner state globally/outer scope
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
let isScanning = false;
|
||||||
const scanButton = document.getElementById('scanButton');
|
let activeScannerCallback = null; // Tracks which function currently owns the scanner output
|
||||||
|
|
||||||
|
function startScanner(targetCallback) {
|
||||||
const qrReader = document.getElementById('qr-reader');
|
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) => {
|
// Show the container
|
||||||
scanButton.classList.toggle('is-active', isOpen);
|
if (qrReader) qrReader.style.display = 'block';
|
||||||
scanButton.setAttribute('aria-expanded', isOpen ? 'true' : 'false');
|
if (statusText) statusText.innerText = "Initializing camera...";
|
||||||
scanButton.textContent = isOpen ? 'Scanner schliessen' : 'Barcode scannen';
|
|
||||||
};
|
|
||||||
|
|
||||||
scanButton.addEventListener('click', function() {
|
Quagga.init({
|
||||||
if (qrReader.style.display !== 'block') {
|
inputStream: {
|
||||||
qrReader.style.display = 'block';
|
name: "Live",
|
||||||
|
type: "LiveStream",
|
||||||
hybridScanner = new HybridScanner({
|
// Make sure this targets your correct HTML container element
|
||||||
videoId: 'qr-reader-video',
|
target: document.querySelector('#scanner-container'),
|
||||||
canvasId: 'qr-reader-canvas',
|
constraints: {
|
||||||
formats: ['QR_CODE', 'CODE_128', 'EAN_13', 'EAN_8', 'CODE_39'],
|
width: 640,
|
||||||
fps: 10,
|
height: 480,
|
||||||
facingMode: 'environment',
|
facingMode: "environment" // Force rear camera
|
||||||
onSuccess: function(decodedText) {
|
},
|
||||||
hybridScanner.stop();
|
},
|
||||||
qrReader.style.display = 'none';
|
decoder: {
|
||||||
|
// Your required formats mapped to Quagga2 reader strings
|
||||||
// Put scanned code into the search box and trigger search
|
readers: ["code_128_reader", "ean_reader", "code_39_reader", "upc_reader"]
|
||||||
const searchInput = document.getElementById('code-search');
|
}
|
||||||
if (searchInput) {
|
}, function(err) {
|
||||||
searchInput.value = decodedText;
|
if (err) {
|
||||||
searchByCode();
|
console.error("Initialization error:", err);
|
||||||
}
|
if (statusText) statusText.innerText = "Camera Error";
|
||||||
|
return;
|
||||||
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.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
|
// Return preferred primary and fallback image URLs based on extension
|
||||||
|
|||||||
+123
-74
@@ -2435,7 +2435,10 @@ document.addEventListener('DOMContentLoaded', ()=>{
|
|||||||
</div>
|
</div>
|
||||||
<div class="qr-container">
|
<div class="qr-container">
|
||||||
<button id="scanButton" class="scan-button" aria-controls="qr-reader" aria-expanded="false">Barcode scannen</button>
|
<button id="scanButton" class="scan-button" aria-controls="qr-reader" aria-expanded="false">Barcode scannen</button>
|
||||||
<div id="qr-reader"></div>
|
<div id="qr-reader" style="display: none;">
|
||||||
|
<h3>Status: <span id="status">Bereit</span></h3>
|
||||||
|
<div id="scanner-container"></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="table-view-header" class="table-view-header" aria-hidden="true">
|
<div id="table-view-header" class="table-view-header" aria-hidden="true">
|
||||||
@@ -2729,7 +2732,7 @@ document.addEventListener('DOMContentLoaded', ()=>{
|
|||||||
|
|
||||||
window.isDebug = false; // Set to true only for development environment
|
window.isDebug = false; // Set to true only for development environment
|
||||||
</script>
|
</script>
|
||||||
<script src="https://unpkg.com/html5-qrcode@2.0.9/dist/html5-qrcode.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/@ericblade/quagga2/dist/quagga.js"></script>
|
||||||
<script>
|
<script>
|
||||||
// Function to check if a file is a video
|
// Function to check if a file is a video
|
||||||
function isVideoFile(filename) {
|
function isVideoFile(filename) {
|
||||||
@@ -2739,7 +2742,6 @@ document.addEventListener('DOMContentLoaded', ()=>{
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Initialize QR Code scanner and global variables
|
// Initialize QR Code scanner and global variables
|
||||||
let html5QrcodeScanner = null;
|
|
||||||
let codeSearchTerm = '';
|
let codeSearchTerm = '';
|
||||||
let descSearchIds = null; // Set of matching IDs or null when disabled
|
let descSearchIds = null; // Set of matching IDs or null when disabled
|
||||||
let currentUsername = '';
|
let currentUsername = '';
|
||||||
@@ -2768,51 +2770,114 @@ document.addEventListener('DOMContentLoaded', ()=>{
|
|||||||
// No-op in production
|
// No-op in production
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
document.getElementById('scanButton').addEventListener('click', function() {
|
|
||||||
const qrReader = document.getElementById('qr-reader');
|
|
||||||
const scanBtn = document.getElementById('scanButton');
|
|
||||||
|
|
||||||
const setScannerUi = (isOpen) => {
|
// Keep track of the scanner state globally/outer scope
|
||||||
if (!scanBtn) return;
|
let isScanning = false;
|
||||||
scanBtn.classList.toggle('is-active', isOpen);
|
let activeScannerCallback = null; // Tracks which function currently owns the scanner output
|
||||||
scanBtn.setAttribute('aria-expanded', isOpen ? 'true' : 'false');
|
|
||||||
scanBtn.textContent = isOpen ? 'Scanner schliessen' : 'Barcode scannen';
|
function startScanner(targetCallback) {
|
||||||
};
|
const qrReader = document.getElementById('qr-reader');
|
||||||
|
const statusText = document.getElementById('status');
|
||||||
if (qrReader.style.display === 'none') {
|
|
||||||
qrReader.style.display = 'block';
|
// Store the custom action that should happen when a barcode is found
|
||||||
|
activeScannerCallback = targetCallback;
|
||||||
html5QrcodeScanner = new Html5QrcodeScanner(
|
|
||||||
"qr-reader", {
|
// Show the container
|
||||||
fps: 10,
|
if (qrReader) qrReader.style.display = 'block';
|
||||||
qrbox: 250,
|
if (statusText) statusText.innerText = "Initializing camera...";
|
||||||
rememberLastUsedCamera: true
|
|
||||||
}
|
Quagga.init({
|
||||||
);
|
inputStream: {
|
||||||
|
name: "Live",
|
||||||
html5QrcodeScanner.render((decodedText) => {
|
type: "LiveStream",
|
||||||
html5QrcodeScanner.clear();
|
// Make sure this targets your correct HTML container element
|
||||||
qrReader.style.display = 'none';
|
target: document.querySelector('#scanner-container'),
|
||||||
|
constraints: {
|
||||||
// Instead of navigating to the URL, put the scanned code in the search box
|
width: 640,
|
||||||
const searchInput = document.getElementById('code-search');
|
height: 480,
|
||||||
if (searchInput) {
|
facingMode: "environment" // Force rear camera
|
||||||
searchInput.value = decodedText;
|
},
|
||||||
// Trigger search automatically
|
},
|
||||||
|
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();
|
searchByCode();
|
||||||
}
|
}
|
||||||
|
|
||||||
setScannerUi(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
setScannerUi(true);
|
|
||||||
} else {
|
|
||||||
if (html5QrcodeScanner) {
|
|
||||||
html5QrcodeScanner.clear();
|
|
||||||
}
|
}
|
||||||
qrReader.style.display = 'none';
|
}
|
||||||
setScannerUi(false);
|
});
|
||||||
|
|
||||||
|
// 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();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -2824,26 +2889,18 @@ document.addEventListener('DOMContentLoaded', ()=>{
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (qrReader.style.display !== 'none') {
|
// Toggle close if it's already running
|
||||||
if (html5QrcodeScanner) {
|
if (isScanning && qrReader.style.display !== 'none') {
|
||||||
html5QrcodeScanner.clear();
|
stopScanner();
|
||||||
}
|
|
||||||
qrReader.style.display = 'none';
|
|
||||||
scanEditBtn.textContent = 'Barcode scannen';
|
scanEditBtn.textContent = 'Barcode scannen';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
qrReader.style.display = 'block';
|
|
||||||
scanEditBtn.textContent = 'Scanner schließen';
|
scanEditBtn.textContent = 'Scanner schließen';
|
||||||
html5QrcodeScanner = new Html5QrcodeScanner('qr-reader', {
|
|
||||||
fps: 10,
|
// Start scanner with custom logic mapping to the Code input field
|
||||||
qrbox: 250,
|
startScanner(function(decodedText) {
|
||||||
rememberLastUsedCamera: true
|
editCodeInput.value = decodedText;
|
||||||
});
|
|
||||||
html5QrcodeScanner.render((decodedText) => {
|
|
||||||
html5QrcodeScanner.clear();
|
|
||||||
qrReader.style.display = 'none';
|
|
||||||
editCodeInput.value = String(decodedText || '').trim();
|
|
||||||
validateCodeField(editCodeInput, document.getElementById('edit-item-id')?.value || null);
|
validateCodeField(editCodeInput, document.getElementById('edit-item-id')?.value || null);
|
||||||
scanEditBtn.textContent = 'Barcode scannen';
|
scanEditBtn.textContent = 'Barcode scannen';
|
||||||
});
|
});
|
||||||
@@ -2857,26 +2914,18 @@ document.addEventListener('DOMContentLoaded', ()=>{
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (qrReader.style.display !== 'none') {
|
// Toggle close if it's already running
|
||||||
if (html5QrcodeScanner) {
|
if (isScanning && qrReader.style.display !== 'none') {
|
||||||
html5QrcodeScanner.clear();
|
stopScanner();
|
||||||
}
|
|
||||||
qrReader.style.display = 'none';
|
|
||||||
scanIsbnBtn.textContent = 'ISBN scannen';
|
scanIsbnBtn.textContent = 'ISBN scannen';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
qrReader.style.display = 'block';
|
|
||||||
scanIsbnBtn.textContent = 'Scanner schließen';
|
scanIsbnBtn.textContent = 'Scanner schließen';
|
||||||
html5QrcodeScanner = new Html5QrcodeScanner('qr-reader', {
|
|
||||||
fps: 10,
|
// Start scanner with custom logic mapping to the ISBN input field
|
||||||
qrbox: 250,
|
startScanner(function(decodedText) {
|
||||||
rememberLastUsedCamera: true
|
editIsbnInput.value = decodedText;
|
||||||
});
|
|
||||||
html5QrcodeScanner.render((decodedText) => {
|
|
||||||
html5QrcodeScanner.clear();
|
|
||||||
qrReader.style.display = 'none';
|
|
||||||
editIsbnInput.value = String(decodedText || '').trim();
|
|
||||||
scanIsbnBtn.textContent = 'ISBN scannen';
|
scanIsbnBtn.textContent = 'ISBN scannen';
|
||||||
if (typeof fetchBookInfo === 'function') {
|
if (typeof fetchBookInfo === 'function') {
|
||||||
fetchBookInfo('edit');
|
fetchBookInfo('edit');
|
||||||
|
|||||||
@@ -876,10 +876,7 @@
|
|||||||
<button type="button" id="scan-code4-btn" class="fetch-isbn-button">Barcode scannen</button>
|
<button type="button" id="scan-code4-btn" class="fetch-isbn-button">Barcode scannen</button>
|
||||||
</div>
|
</div>
|
||||||
<small style="display:block; color:#666;">Einzelcode manuell setzen oder per Scanner erfassen.</small>
|
<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;">
|
<div id="code4-scanner" style="width:100%; max-width:520px; display:none; margin-top:10px;"></div>
|
||||||
<video id="code4-scanner-video" playsinline style="width: 100%; border-radius: 8px;"></video>
|
|
||||||
<canvas id="code4-scanner-canvas" style="display: none;"></canvas>
|
|
||||||
</div>
|
|
||||||
<small id="code4-scan-status" style="display:block; color:#666; margin-top:6px;"></small>
|
<small id="code4-scan-status" style="display:block; color:#666; margin-top:6px;"></small>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<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" id="scan-isbn-btn" class="fetch-isbn-button">Barcode scannen</button>
|
||||||
<button type="button" class="fetch-isbn-button" onclick="fetchBookInfo('upload')">Bild abrufen</button>
|
<button type="button" class="fetch-isbn-button" onclick="fetchBookInfo('upload')">Bild abrufen</button>
|
||||||
</div>
|
</div>
|
||||||
<div id="isbn-scanner" style="width:100%; max-width:520px; display:none; margin-top:10px;">
|
<div id="isbn-scanner" style="width:100%; max-width:520px; display:none; margin-top:10px;"></div>
|
||||||
<video id="isbn-scanner-video" playsinline style="width: 100%; border-radius: 8px;"></video>
|
|
||||||
<canvas id="isbn-scanner-canvas" style="display: none;"></canvas>
|
|
||||||
</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>
|
<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 id="book-info-container" class="book-info-container"></div>
|
||||||
</div>
|
</div>
|
||||||
@@ -977,8 +971,7 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script src="https://unpkg.com/@zxing/library@latest/umd/index.min.js"></script>
|
<script src="https://unpkg.com/html5-qrcode@2.0.9/dist/html5-qrcode.min.js"></script>
|
||||||
<script src="/static/js/scanner.js"></script>
|
|
||||||
<script>
|
<script>
|
||||||
const libraryModuleEnabled = {{ 'true' if library_module_enabled else 'false' }};
|
const libraryModuleEnabled = {{ 'true' if library_module_enabled else 'false' }};
|
||||||
|
|
||||||
@@ -1057,19 +1050,16 @@
|
|||||||
|
|
||||||
// Stop ISBN scanner if currently running to avoid camera conflicts.
|
// Stop ISBN scanner if currently running to avoid camera conflicts.
|
||||||
if (isbnScannerRunning && isbnScannerInstance) {
|
if (isbnScannerRunning && isbnScannerInstance) {
|
||||||
isbnScannerInstance.stop();
|
isbnScannerInstance.clear().catch(() => {});
|
||||||
isbnScannerInstance = null;
|
|
||||||
const isbnScannerBox = document.getElementById('isbn-scanner');
|
const isbnScannerBox = document.getElementById('isbn-scanner');
|
||||||
const isbnScanButton = document.getElementById('scan-isbn-btn');
|
const isbnScanButton = document.getElementById('scan-isbn-btn');
|
||||||
if (isbnScannerBox) isbnScannerBox.style.display = 'none';
|
if (isbnScannerBox) isbnScannerBox.style.display = 'none';
|
||||||
if (isbnScanButton) isbnScanButton.textContent = 'ISBN scannen';
|
if (isbnScanButton) isbnScanButton.textContent = 'ISBN scannen';
|
||||||
isbnScannerRunning = false;
|
isbnScannerRunning = false;
|
||||||
setIsbnScanStatus('Scanner gestoppt.');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (code4ScannerRunning && code4ScannerInstance) {
|
if (code4ScannerRunning && code4ScannerInstance) {
|
||||||
code4ScannerInstance.stop();
|
code4ScannerInstance.clear().catch(() => {});
|
||||||
code4ScannerInstance = null;
|
|
||||||
scannerBox.style.display = 'none';
|
scannerBox.style.display = 'none';
|
||||||
scanButton.textContent = 'Barcode scannen';
|
scanButton.textContent = 'Barcode scannen';
|
||||||
code4ScannerRunning = false;
|
code4ScannerRunning = false;
|
||||||
@@ -1079,36 +1069,29 @@
|
|||||||
|
|
||||||
scannerBox.style.display = 'block';
|
scannerBox.style.display = 'block';
|
||||||
scanButton.textContent = 'Scanner stoppen';
|
scanButton.textContent = 'Scanner stoppen';
|
||||||
|
code4ScannerInstance = new Html5QrcodeScanner('code4-scanner', {
|
||||||
|
fps: 10,
|
||||||
|
qrbox: 250,
|
||||||
|
rememberLastUsedCamera: true
|
||||||
|
});
|
||||||
code4ScannerRunning = true;
|
code4ScannerRunning = true;
|
||||||
setCode4ScanStatus('Scanner läuft. Der Scan wird im Basis-Codefeld übernommen.');
|
setCode4ScanStatus('Scanner läuft. Der Scan wird im Basis-Codefeld übernommen.');
|
||||||
|
|
||||||
code4ScannerInstance = new HybridScanner({
|
code4ScannerInstance.render((decodedText) => {
|
||||||
videoId: 'code4-scanner-video',
|
const scannedCode = String(decodedText || '').trim();
|
||||||
canvasId: 'code4-scanner-canvas',
|
if (!scannedCode) return;
|
||||||
formats: ['CODE_128', 'CODE_39', 'QR_CODE'],
|
|
||||||
fps: 10,
|
|
||||||
facingMode: 'environment',
|
|
||||||
onSuccess: function(decodedText) {
|
|
||||||
const scannedCode = String(decodedText || '').trim();
|
|
||||||
if (!scannedCode) return;
|
|
||||||
|
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
if (scannedCode === code4LastScanned && (now - code4LastScannedAt) < 1500) {
|
if (scannedCode === code4LastScanned && (now - code4LastScannedAt) < 1500) {
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
code4LastScanned = scannedCode;
|
|
||||||
code4LastScannedAt = now;
|
|
||||||
|
|
||||||
codeField.value = scannedCode;
|
|
||||||
validateCodeField(codeField);
|
|
||||||
setCode4ScanStatus(`Code_4 gesetzt: ${scannedCode}`);
|
|
||||||
},
|
|
||||||
onError: function(error) {
|
|
||||||
setCode4ScanStatus(`Fehler: ${error}`, true);
|
|
||||||
}
|
}
|
||||||
});
|
code4LastScanned = scannedCode;
|
||||||
|
code4LastScannedAt = now;
|
||||||
|
|
||||||
code4ScannerInstance.start();
|
codeField.value = scannedCode;
|
||||||
|
validateCodeField(codeField);
|
||||||
|
setCode4ScanStatus(`Code_4 gesetzt: ${scannedCode}`);
|
||||||
|
}, () => {});
|
||||||
}
|
}
|
||||||
|
|
||||||
function startIsbnScanner() {
|
function startIsbnScanner() {
|
||||||
@@ -1123,8 +1106,7 @@
|
|||||||
|
|
||||||
// Stop Code_4 scanner if currently running to avoid camera conflicts.
|
// Stop Code_4 scanner if currently running to avoid camera conflicts.
|
||||||
if (code4ScannerRunning && code4ScannerInstance) {
|
if (code4ScannerRunning && code4ScannerInstance) {
|
||||||
code4ScannerInstance.stop();
|
code4ScannerInstance.clear().catch(() => {});
|
||||||
code4ScannerInstance = null;
|
|
||||||
const codeScannerBox = document.getElementById('code4-scanner');
|
const codeScannerBox = document.getElementById('code4-scanner');
|
||||||
const codeScanButton = document.getElementById('scan-code4-btn');
|
const codeScanButton = document.getElementById('scan-code4-btn');
|
||||||
if (codeScannerBox) codeScannerBox.style.display = 'none';
|
if (codeScannerBox) codeScannerBox.style.display = 'none';
|
||||||
@@ -1134,8 +1116,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isbnScannerRunning && isbnScannerInstance) {
|
if (isbnScannerRunning && isbnScannerInstance) {
|
||||||
isbnScannerInstance.stop();
|
isbnScannerInstance.clear().catch(() => {});
|
||||||
isbnScannerInstance = null;
|
|
||||||
scannerBox.style.display = 'none';
|
scannerBox.style.display = 'none';
|
||||||
scanButton.textContent = 'ISBN scannen';
|
scanButton.textContent = 'ISBN scannen';
|
||||||
isbnScannerRunning = false;
|
isbnScannerRunning = false;
|
||||||
@@ -1145,16 +1126,15 @@
|
|||||||
|
|
||||||
scannerBox.style.display = 'block';
|
scannerBox.style.display = 'block';
|
||||||
scanButton.textContent = 'Scanner stoppen';
|
scanButton.textContent = 'Scanner stoppen';
|
||||||
|
isbnScannerInstance = new Html5QrcodeScanner('isbn-scanner', {
|
||||||
|
fps: 10,
|
||||||
|
qrbox: 250,
|
||||||
|
rememberLastUsedCamera: true
|
||||||
|
});
|
||||||
isbnScannerRunning = true;
|
isbnScannerRunning = true;
|
||||||
setIsbnScanStatus('Scanner läuft. Bitte ISBN-Code erfassen.');
|
setIsbnScanStatus('Scanner läuft. Bitte ISBN-Code erfassen.');
|
||||||
|
|
||||||
isbnScannerInstance = new HybridScanner({
|
isbnScannerInstance.render((decodedText) => {
|
||||||
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();
|
const scannedCode = String(decodedText || '').trim();
|
||||||
if (!scannedCode) return;
|
if (!scannedCode) return;
|
||||||
|
|
||||||
@@ -1167,11 +1147,7 @@
|
|||||||
setIsbnScanStatus('Kein gültiges ISBN-Format erkannt, der gescannte Wert bleibt aber im Feld.', true);
|
setIsbnScanStatus('Kein gültiges ISBN-Format erkannt, der gescannte Wert bleibt aber im Feld.', true);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stop scanner after successful scan
|
isbnScannerInstance.clear().catch(() => {});
|
||||||
if (isbnScannerInstance) {
|
|
||||||
isbnScannerInstance.stop();
|
|
||||||
isbnScannerInstance = null;
|
|
||||||
}
|
|
||||||
scannerBox.style.display = 'none';
|
scannerBox.style.display = 'none';
|
||||||
scanButton.textContent = 'ISBN scannen';
|
scanButton.textContent = 'ISBN scannen';
|
||||||
isbnScannerRunning = false;
|
isbnScannerRunning = false;
|
||||||
@@ -1180,13 +1156,7 @@
|
|||||||
// Automatically load book metadata after a valid ISBN scan.
|
// Automatically load book metadata after a valid ISBN scan.
|
||||||
fetchBookInfo('upload');
|
fetchBookInfo('upload');
|
||||||
}
|
}
|
||||||
},
|
}, () => {});
|
||||||
onError: function(error) {
|
|
||||||
setIsbnScanStatus(`Fehler: ${error}`, true);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
isbnScannerInstance.start();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load predefined filter values for dropdowns
|
// Load predefined filter values for dropdowns
|
||||||
|
|||||||
@@ -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()
|
|
||||||
Reference in New Issue
Block a user