Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ff742d018e | |||
| 6fa40f26b0 | |||
| 777fc81064 | |||
| cf5e38e319 | |||
| b76941d5fe | |||
| f46f2674b0 | |||
| 8ebbdcfd54 | |||
| 92b5235035 | |||
| 4bda2e044b | |||
| b83b6c0ba2 |
@@ -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
@@ -1 +1 @@
|
||||
v0.8.29
|
||||
v0.8.31.1
|
||||
|
||||
+74
-17
@@ -47,8 +47,26 @@ class HybridScanner {
|
||||
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 = 'https://unpkg.com/@zxing/library@latest/umd/index.min.js';
|
||||
script.src = scriptUrl;
|
||||
script.async = true;
|
||||
script.onload = () => {
|
||||
this.state.zxingReady = typeof ZXing !== 'undefined';
|
||||
resolve(this.state.zxingReady);
|
||||
@@ -101,6 +119,21 @@ class HybridScanner {
|
||||
|
||||
// Make video element visible (ensure it's not hidden by display: none)
|
||||
this.state.videoElement.style.display = 'block';
|
||||
// Ensure autoplay works on mobile by muting and setting playsInline
|
||||
try {
|
||||
this.state.videoElement.muted = true;
|
||||
this.state.videoElement.autoplay = true;
|
||||
this.state.videoElement.playsInline = true;
|
||||
// Some browsers require explicit play() after setting srcObject
|
||||
const p = this.state.videoElement.play();
|
||||
if (p && p.then) {
|
||||
p.catch(() => {
|
||||
// ignore play promise rejection (user gesture required on some browsers)
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
|
||||
// Initialize ZXing reader
|
||||
const hints = new Map();
|
||||
@@ -140,6 +173,16 @@ class HybridScanner {
|
||||
this.state.videoElement.style.display = 'none';
|
||||
}
|
||||
|
||||
// Reset ZXing reader to stop any internal loops
|
||||
try {
|
||||
if (this.state.reader && typeof this.state.reader.reset === 'function') {
|
||||
this.state.reader.reset();
|
||||
}
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
this.state.reader = null;
|
||||
|
||||
this.removeKeyboardInput();
|
||||
}
|
||||
|
||||
@@ -154,32 +197,46 @@ class HybridScanner {
|
||||
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) {
|
||||
canvas.width = video.videoWidth;
|
||||
canvas.height = video.videoHeight;
|
||||
const context = canvas.getContext('2d');
|
||||
try {
|
||||
canvas.width = video.videoWidth;
|
||||
canvas.height = video.videoHeight;
|
||||
|
||||
if (canvas.width === 0 || canvas.height === 0) {
|
||||
setTimeout(() => this.scanLoop(), 100);
|
||||
return;
|
||||
}
|
||||
|
||||
if (context) {
|
||||
context.drawImage(video, 0, 0);
|
||||
const context = canvas.getContext('2d');
|
||||
|
||||
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));
|
||||
if (context) {
|
||||
context.drawImage(video, 0, 0);
|
||||
|
||||
try {
|
||||
const result = this.state.reader.decodeFromBitmap(binaryBitmap);
|
||||
this.handleScanResult(result.text);
|
||||
} catch (e) {
|
||||
// No barcode found in frame; continue scanning
|
||||
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) {
|
||||
// Silently ignore canvas errors
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('Scan loop error:', err);
|
||||
}
|
||||
}
|
||||
|
||||
// Throttle scan loop
|
||||
// Throttle scan loop based on FPS setting
|
||||
setTimeout(() => this.scanLoop(), 1000 / this.options.fps);
|
||||
}
|
||||
|
||||
|
||||
+164
-13
@@ -76,11 +76,11 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="scanner-video-wrapper">
|
||||
<video id="qr-reader" playsinline style="width: 100%; max-width: 500px; border-radius: 8px;"></video>
|
||||
<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.9em; color: #999; margin-top: 10px; text-align: center;">
|
||||
<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>
|
||||
@@ -564,6 +564,29 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
});
|
||||
|
||||
initMobileWindowedLibraryLoading();
|
||||
// On mobile, make tapping an item open details for easier access (tap target larger)
|
||||
function enableMobileTapToDetails() {
|
||||
if (!isMobileViewport()) return;
|
||||
document.querySelectorAll('.library-item').forEach(item => {
|
||||
item.addEventListener('click', function(e) {
|
||||
// Ignore clicks on buttons/links inside the card
|
||||
const tag = e.target.tagName.toLowerCase();
|
||||
if (tag === 'button' || tag === 'a' || e.target.closest('.action-button')) return;
|
||||
const itemId = this.dataset.itemId;
|
||||
if (!itemId) return;
|
||||
fetch(`/get_item_details/${itemId}`)
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
displayLibraryItemDetail(data.item);
|
||||
document.getElementById('detailModal').style.display = 'flex';
|
||||
}
|
||||
});
|
||||
}, { passive: true });
|
||||
});
|
||||
}
|
||||
|
||||
enableMobileTapToDetails();
|
||||
});
|
||||
|
||||
function displayLibraryItemDetail(item) {
|
||||
@@ -574,7 +597,7 @@ function displayLibraryItemDetail(item) {
|
||||
modalBody.innerHTML = `
|
||||
<h2>${item.Name}</h2>
|
||||
<div class="detail-content">
|
||||
${item.Image ? `<img src="/uploads/${item.Image}" alt="${item.Name}" style="max-width: 200px; margin-bottom: 20px;">` : ''}
|
||||
${item.Image ? `<img src="/uploads/${item.Image}" alt="${item.Name}" class="detail-image">` : ''}
|
||||
<p><strong>Autor:</strong> ${item.Author || '—'}</p>
|
||||
<p><strong>ISBN:</strong> ${item.ISBN || '—'}</p>
|
||||
<p><strong>Typ:</strong> ${item.ItemType || 'Medium'}</p>
|
||||
@@ -791,12 +814,46 @@ function onScanError(error) {
|
||||
color: #b3261e;
|
||||
}
|
||||
|
||||
/* Mobile: simplify list entries - show only title */
|
||||
@media (max-width: 900px) {
|
||||
.library-item .item-author,
|
||||
.library-item .item-isbn,
|
||||
.table-cell.author-cell,
|
||||
.table-cell.isbn-cell {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Make title more prominent and full-width */
|
||||
.library-item .item-details {
|
||||
display: block;
|
||||
width: calc(100% - 140px);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.library-item .item-name {
|
||||
font-size: 1.05rem;
|
||||
margin: 0 0 6px 0;
|
||||
line-height: 1.2;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.library-item .item-image-wrapper {
|
||||
width: 100px;
|
||||
height: 130px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Mobile: allow horizontal scrolling for table-mode views */
|
||||
@media (max-width: 900px) {
|
||||
.library-table-wrapper {
|
||||
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 */
|
||||
@@ -811,38 +868,50 @@ function onScanError(error) {
|
||||
background: #f5f5f5;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
margin: 16px 0;
|
||||
padding: 12px;
|
||||
margin: 12px 0;
|
||||
overflow-x: hidden;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.scanner-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.scanner-header h3 {
|
||||
margin: 0;
|
||||
color: #333;
|
||||
font-size: 1.1em;
|
||||
font-size: 1em;
|
||||
flex: 0 0 auto;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.scanner-controls {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
gap: 6px;
|
||||
flex-wrap: wrap;
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.scanner-button {
|
||||
background: #2196F3;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 8px 12px;
|
||||
padding: 6px 10px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 0.9em;
|
||||
font-size: 0.85em;
|
||||
white-space: nowrap;
|
||||
flex: 0 1 auto;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
@@ -857,24 +926,44 @@ function onScanError(error) {
|
||||
.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.95em;
|
||||
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 {
|
||||
@@ -883,8 +972,70 @@ function onScanError(error) {
|
||||
}
|
||||
|
||||
.scanner-button {
|
||||
flex: 1;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/* Modal styles - responsive for mobile */
|
||||
.modal {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(0,0,0,0.5);
|
||||
z-index: 2000;
|
||||
padding: 16px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background: #fff;
|
||||
border-radius: 10px;
|
||||
width: 100%;
|
||||
max-width: 640px;
|
||||
max-height: 90vh;
|
||||
overflow-y: auto;
|
||||
box-sizing: border-box;
|
||||
padding: 16px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.modal-close {
|
||||
background: transparent;
|
||||
border: none;
|
||||
font-size: 1.1rem;
|
||||
position: absolute;
|
||||
right: 20px;
|
||||
top: 18px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.modal-body img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
display: block;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.modal-body p { margin: 6px 0; }
|
||||
|
||||
@media (max-width: 420px) {
|
||||
.modal-content { padding: 12px; }
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
@@ -2435,7 +2435,10 @@ document.addEventListener('DOMContentLoaded', ()=>{
|
||||
</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"></div>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="table-view-header" class="table-view-header" aria-hidden="true">
|
||||
@@ -2729,7 +2732,8 @@ document.addEventListener('DOMContentLoaded', ()=>{
|
||||
|
||||
window.isDebug = false; // Set to true only for development environment
|
||||
</script>
|
||||
<script src="https://unpkg.com/html5-qrcode@2.0.9/dist/html5-qrcode.min.js"></script>
|
||||
<script src="https://unpkg.com/@zxing/library@latest/umd/index.min.js"></script>
|
||||
<script src="/static/js/scanner.js"></script>
|
||||
<script>
|
||||
// Function to check if a file is a video
|
||||
function isVideoFile(filename) {
|
||||
@@ -2739,7 +2743,7 @@ document.addEventListener('DOMContentLoaded', ()=>{
|
||||
}
|
||||
|
||||
// Initialize QR Code scanner and global variables
|
||||
let html5QrcodeScanner = null;
|
||||
let hybridScanner = null;
|
||||
let codeSearchTerm = '';
|
||||
let descSearchIds = null; // Set of matching IDs or null when disabled
|
||||
let currentUsername = '';
|
||||
@@ -2783,33 +2787,37 @@ document.addEventListener('DOMContentLoaded', ()=>{
|
||||
if (qrReader.style.display === 'none') {
|
||||
qrReader.style.display = 'block';
|
||||
|
||||
html5QrcodeScanner = new Html5QrcodeScanner(
|
||||
"qr-reader", {
|
||||
fps: 10,
|
||||
qrbox: 250,
|
||||
rememberLastUsedCamera: true
|
||||
}
|
||||
);
|
||||
|
||||
html5QrcodeScanner.render((decodedText) => {
|
||||
html5QrcodeScanner.clear();
|
||||
qrReader.style.display = 'none';
|
||||
|
||||
// Instead of navigating to the URL, put the scanned code in the search box
|
||||
const searchInput = document.getElementById('code-search');
|
||||
if (searchInput) {
|
||||
searchInput.value = decodedText;
|
||||
// Trigger search automatically
|
||||
searchByCode();
|
||||
}
|
||||
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();
|
||||
}
|
||||
|
||||
setScannerUi(false);
|
||||
setScannerUi(false);
|
||||
},
|
||||
onError: function(error) {
|
||||
console.error('Scanner error:', error);
|
||||
}
|
||||
});
|
||||
|
||||
hybridScanner.start();
|
||||
setScannerUi(true);
|
||||
} else {
|
||||
if (html5QrcodeScanner) {
|
||||
html5QrcodeScanner.clear();
|
||||
if (hybridScanner) {
|
||||
hybridScanner.stop();
|
||||
hybridScanner = null;
|
||||
}
|
||||
qrReader.style.display = 'none';
|
||||
setScannerUi(false);
|
||||
@@ -2825,8 +2833,9 @@ document.addEventListener('DOMContentLoaded', ()=>{
|
||||
}
|
||||
|
||||
if (qrReader.style.display !== 'none') {
|
||||
if (html5QrcodeScanner) {
|
||||
html5QrcodeScanner.clear();
|
||||
if (hybridScanner) {
|
||||
hybridScanner.stop();
|
||||
hybridScanner = null;
|
||||
}
|
||||
qrReader.style.display = 'none';
|
||||
scanEditBtn.textContent = 'Barcode scannen';
|
||||
@@ -2835,18 +2844,25 @@ document.addEventListener('DOMContentLoaded', ()=>{
|
||||
|
||||
qrReader.style.display = 'block';
|
||||
scanEditBtn.textContent = 'Scanner schließen';
|
||||
html5QrcodeScanner = new Html5QrcodeScanner('qr-reader', {
|
||||
fps: 10,
|
||||
qrbox: 250,
|
||||
rememberLastUsedCamera: true
|
||||
});
|
||||
html5QrcodeScanner.render((decodedText) => {
|
||||
html5QrcodeScanner.clear();
|
||||
qrReader.style.display = 'none';
|
||||
editCodeInput.value = String(decodedText || '').trim();
|
||||
validateCodeField(editCodeInput, document.getElementById('edit-item-id')?.value || null);
|
||||
scanEditBtn.textContent = 'Barcode scannen';
|
||||
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);
|
||||
}
|
||||
});
|
||||
|
||||
hybridScanner.start();
|
||||
}
|
||||
|
||||
function scanIntoEditIsbn() {
|
||||
@@ -2858,8 +2874,9 @@ document.addEventListener('DOMContentLoaded', ()=>{
|
||||
}
|
||||
|
||||
if (qrReader.style.display !== 'none') {
|
||||
if (html5QrcodeScanner) {
|
||||
html5QrcodeScanner.clear();
|
||||
if (hybridScanner) {
|
||||
hybridScanner.stop();
|
||||
hybridScanner = null;
|
||||
}
|
||||
qrReader.style.display = 'none';
|
||||
scanIsbnBtn.textContent = 'ISBN scannen';
|
||||
@@ -2868,20 +2885,27 @@ document.addEventListener('DOMContentLoaded', ()=>{
|
||||
|
||||
qrReader.style.display = 'block';
|
||||
scanIsbnBtn.textContent = 'Scanner schließen';
|
||||
html5QrcodeScanner = new Html5QrcodeScanner('qr-reader', {
|
||||
hybridScanner = new HybridScanner({
|
||||
videoId: 'qr-reader-video',
|
||||
canvasId: 'qr-reader-canvas',
|
||||
formats: ['ISBN', 'EAN_13', 'EAN_8', 'QR_CODE'],
|
||||
fps: 10,
|
||||
qrbox: 250,
|
||||
rememberLastUsedCamera: true
|
||||
});
|
||||
html5QrcodeScanner.render((decodedText) => {
|
||||
html5QrcodeScanner.clear();
|
||||
qrReader.style.display = 'none';
|
||||
editIsbnInput.value = String(decodedText || '').trim();
|
||||
scanIsbnBtn.textContent = 'ISBN scannen';
|
||||
if (typeof fetchBookInfo === 'function') {
|
||||
fetchBookInfo('edit');
|
||||
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);
|
||||
}
|
||||
});
|
||||
|
||||
hybridScanner.start();
|
||||
}
|
||||
|
||||
function rebuildFilter3Options() {
|
||||
|
||||
@@ -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