Compare commits

...

17 Commits

Author SHA1 Message Date
Aiirondev_dev 3aa19b10d1 Reverted Faulti changes that have proven not worth implementing 2026-06-06 15:03:43 +02:00
Aiirondev_dev ff742d018e removed obsolet file 2026-06-01 14:34:05 +02:00
Aiirondev_dev 6fa40f26b0 Generall fixes, to manage the unused overhead 2026-06-01 13:23:52 +02:00
Aiirondev_dev 777fc81064 fix for the latest Tag 2026-06-01 13:20:10 +02:00
Aiirondev_dev cf5e38e319 removed development release version 2026-06-01 13:15:54 +02:00
Aiirondev_dev b76941d5fe Fix in the release versioning 2026-06-01 13:07:52 +02:00
Aiirondev_dev f46f2674b0 Library style adjustments for mobile 2026-06-01 12:51:28 +02:00
github-actions[bot] 8ebbdcfd54 chore: bump version to v0.8.31.1 2026-06-01 10:35:56 +00:00
Aiirondev_dev 92b5235035 feat: Improve mobile video playback for barcode scanning with autoplay and mute settings 2026-06-01 12:32:42 +02:00
github-actions[bot] 4bda2e044b chore: bump version to v0.8.30 2026-06-01 10:23:51 +00:00
Aiirondev_dev b83b6c0ba2 feat: Enhance barcode scanner with improved mobile compatibility and error handling 2026-06-01 12:22:54 +02:00
github-actions[bot] ab718d6ac2 chore: bump version to v0.8.29 2026-06-01 10:12:29 +00:00
Aiirondev_dev 112d9b6aa0 feat: Make video elements visible for barcode scanning in various templates 2026-06-01 12:10:31 +02:00
github-actions[bot] 5139a50a43 chore: bump version to v0.8.28 2026-06-01 09:56:08 +00:00
Aiirondev_dev 997c7b42d5 feat: Implement hybrid barcode scanner with real-time camera support and image upload fallback 2026-06-01 11:55:28 +02:00
github-actions[bot] 20e5d3bb9a chore: bump version to v0.8.27 2026-06-01 07:01:33 +00:00
Aiirondev_dev f62b22c2f7 feat: Refactor appointment handling to support multi-day events and improve calendar visibility 2026-06-01 09:00:35 +02:00
8 changed files with 193 additions and 392 deletions
+14 -89
View File
@@ -15,15 +15,6 @@ on:
- patch
- minor
- major
- development
push_dev:
description: "If true, push the :dev image to GHCR for development releases"
required: false
default: "false"
type: choice
options:
- "true"
- "false"
permissions:
contents: write
@@ -48,13 +39,12 @@ jobs:
EVENT_NAME: ${{ github.event_name }}
REF_NAME: ${{ github.ref_name }}
BUMP_TYPE: ${{ github.event.inputs.bump || 'patch' }}
PUSH_DEV: ${{ github.event.inputs.push_dev || 'false' }}
run: |
if [ "$EVENT_NAME" = "push" ] && [ -n "$REF_NAME" ]; then
TAG="$REF_NAME"
else
# Fetch latest release tag via GitHub API (fall back to v3.0.0)
latest_tag="v3.0.0"
# Fetch latest release tag via GitHub API (fall back to v0.8.31)
latest_tag="v0.8.31"
if meta_json=$(curl -fsSL -H "Authorization: Bearer $GH_TOKEN" -H "Accept: application/vnd.github+json" "https://api.github.com/repos/$REPO/releases/latest" 2>/dev/null); then
tag_name=$(printf "%s" "$meta_json" | sed -n 's/.*"tag_name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1)
if [ -n "$tag_name" ]; then
@@ -67,7 +57,7 @@ jobs:
minor=${BASH_REMATCH[2]}
patch=${BASH_REMATCH[3]}
else
major=3; minor=0; patch=0
major=0; minor=8; patch=31
fi
# Bump strategy: major / minor / patch
@@ -78,30 +68,28 @@ jobs:
else
patch=$((patch + 1))
fi
if [ "${BUMP_TYPE:-}" = "development" ]; then
TAG="v${major}.${minor}.${patch}-dev"
else
TAG="v${major}.${minor}.${patch}"
fi
# Zusammenbau des Tags für den manuellen Run
TAG="v${major}.${minor}.${patch}"
fi
# Validierung des erzeugten oder übergebenen Tags
if ! echo "$TAG" | grep -Eq '^v[0-9]+\.[0-9]+\.[0-9]+(-dev(\.[0-9]+)?)?$'; then
echo "Error: tag '$TAG' is not valid semver (vX.Y.Z or vX.Y.Z-dev)"
echo "Error: tag '$TAG' is not valid semver (vX.Y.Z)"
exit 1
fi
git fetch --tags --force
LATEST_TAG="$(git tag -l 'v*' | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | sort -V | tail -n1)"
if [ -z "$LATEST_TAG" ]; then
LATEST_TAG="v3.0.0"
LATEST_TAG="v0.8.31"
fi
TAG_MAJOR="${TAG#v}"
TAG_MAJOR="${TAG_MAJOR%%.*}"
LATEST_MAJOR="$(echo "$LATEST_TAG" | grep -Eo '^v[0-9]+' | tr -d 'v')"
if [ -z "$LATEST_MAJOR" ]; then
LATEST_MAJOR="3"
LATEST_MAJOR="0"
fi
# If not explicitly bumping major, disallow changing major version
@@ -123,33 +111,6 @@ jobs:
IMAGE="ghcr.io/aiirondev/legendary-octo-garbanzo:${TAG}"
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
echo "image=$IMAGE" >> "$GITHUB_OUTPUT"
if [ "${BUMP_TYPE:-}" = "development" ]; then
echo "is_development=true" >> "$GITHUB_OUTPUT"
else
echo "is_development=false" >> "$GITHUB_OUTPUT"
fi
if [ "${BUMP_TYPE:-}" = "development" ] && [ "${PUSH_DEV:-}" = "true" ]; then
echo "push_dev=true" >> "$GITHUB_OUTPUT"
else
echo "push_dev=false" >> "$GITHUB_OUTPUT"
fi
- name: Update .release-version file
run: |
echo "${{ steps.meta.outputs.tag }}" > .release-version
cat .release-version
- name: Create and push tag for manual releases
if: github.event_name == 'workflow_dispatch'
run: |
TAG="${{ steps.meta.outputs.tag }}"
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add .release-version
git commit -m "chore: bump version to $TAG" || true
git tag "$TAG"
git push origin "$TAG"
git push origin HEAD:${{ github.ref_name }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
@@ -162,26 +123,16 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push release image
if: steps.meta.outputs.is_development != 'true'
uses: docker/build-push-action@v6
with:
context: .
file: ./Dockerfile
push: true
load: true # Lädt das Image in den lokalen Docker-Daemon für den 'docker save' Schritt
tags: |
${{ steps.meta.outputs.image }}
ghcr.io/aiirondev/legendary-octo-garbanzo:latest
- name: Build and push development image (:dev)
if: steps.meta.outputs.is_development == 'true' && steps.meta.outputs.push_dev == 'true'
uses: docker/build-push-action@v6
with:
context: .
file: ./Dockerfile
push: true
tags: |
ghcr.io/aiirondev/legendary-octo-garbanzo:dev
- name: Build local image tar for offline deploy
run: |
set -euo pipefail
@@ -204,30 +155,17 @@ jobs:
docker build -t "$IMG" .
docker save "$IMG" | gzip > "inventarsystem-image-${TAG}.tar.gz"
# development tar omitted: dev releases will be versioned (vX.Y.Z-dev) and handled by update.sh using the tag
- name: Commit .release-version for tag pushes
if: github.event_name == 'push'
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
if ! git diff --quiet .release-version; then
git add .release-version
git commit -m "chore: update version to ${{ steps.meta.outputs.tag }}"
git push origin HEAD:${{ github.ref_name }}
fi
- name: Create release-only docker bundle
run: |
mkdir -p release-bundle
cat > release-bundle/docker-compose.yml <<EOF
services:
app:
image: ${INVENTAR_APP_IMAGE:-ghcr.io/aiirondev/legendary-octo-garbanzo:${{ steps.meta.outputs.tag }}}
image: \${INVENTAR_APP_IMAGE:-ghcr.io/aiirondev/legendary-octo-garbanzo:${{ steps.meta.outputs.tag }}}
container_name: inventarsystem-app
restart: unless-stopped
ports:
- "${INVENTAR_HTTP_PORT:-10000}:8000"
- "\${INVENTAR_HTTP_PORT:-10000}:8000"
depends_on:
- mongodb
- redis
@@ -300,17 +238,6 @@ jobs:
fi
done
cat > release-bundle/DEVELOPMENT.md <<EOF
This is a development prerelease bundle for tag: ${{ steps.meta.outputs.tag }}
This prerelease is intentionally separate from normal releases and will not be used by default.
To install this prerelease on a target host run:
./update.sh ${{ steps.meta.outputs.tag }}
EOF
# Make any shipped scripts executable
find release-bundle -maxdepth 1 -type f -name '*.sh' -exec chmod +x {} \; || true
tar -czf inventarsystem-docker-bundle.tar.gz -C release-bundle .
@@ -319,10 +246,8 @@ jobs:
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ steps.meta.outputs.tag }}
prerelease: ${{ steps.meta.outputs.is_development }}
files: |
inventarsystem-docker-bundle.tar.gz
inventarsystem-image-${{ steps.meta.outputs.tag }}.tar.gz
inventarsystem-image-dev.tar.gz
fail_on_unmatched_files: false
generate_release_notes: true
generate_release_notes: true
+1 -1
View File
@@ -1 +1 @@
v0.8.26
v0.8.31.1
+53 -65
View File
@@ -4823,6 +4823,22 @@ def get_user_appointments():
result = []
import re as _re
def _date_iter(start_value: str, end_value: str):
try:
start_date = datetime.datetime.strptime(start_value, '%Y-%m-%d').date()
end_date = datetime.datetime.strptime(end_value, '%Y-%m-%d').date()
except Exception:
return []
if end_date < start_date:
end_date = start_date
cursor = start_date
days = []
while cursor <= end_date:
days.append(cursor.strftime('%Y-%m-%d'))
cursor += datetime.timedelta(days=1)
return days
for appt in appointments:
appt_id = str(appt.get('_id') or '')
if not appt_id:
@@ -4831,72 +4847,44 @@ def get_user_appointments():
date_start = str(appt.get('date_start') or '')
date_end = str(appt.get('date_end') or date_start)
time_span = appt.get('time_span', []) or []
# Determine a sensible start and end datetime for the event.
# Prefer a time from the time_span for the first day and a time for the last day when multi-day.
start_dt_str = date_start
end_dt_str = date_end
first_time = None
last_time = None
generic_first = None
generic_last = None
try:
# collect times from spans
for entry in time_span:
s = str(entry or '').strip()
if not s:
continue
m_date = _re.match(r"^(\d{4}-\d{2}-\d{2})\s+(\d{2}:\d{2})-(\d{2}:\d{2})$", s)
if m_date:
d = m_date.group(1)
if d == date_start and not first_time:
first_time = m_date.group(2)
if d == date_end:
last_time = m_date.group(3)
continue
m = _re.match(r"^(\d{2}:\d{2})-(\d{2}:\d{2})$", s)
if m:
if generic_first is None:
generic_first = m.group(1)
generic_last = m.group(2)
if not first_time and generic_first:
first_time = generic_first
if not last_time and generic_last:
last_time = generic_last
if first_time:
start_dt_str = f"{date_start}T{first_time}"
# If appointment spans multiple days and we have a last_time for the final day, use it
if date_end != date_start:
if last_time:
end_dt_str = f"{date_end}T{last_time}"
else:
# span until end of final day
end_dt_str = f"{date_end}T23:59:59"
else:
if last_time:
end_dt_str = f"{date_start}T{last_time}"
except Exception:
# fallback: use whole-day span
if date_end != date_start:
end_dt_str = f"{date_end}T23:59:59"
title = appt.get('note') or f"Termin von {appt.get('user') or ''}"
result.append({
'id': appt_id,
'title': title,
'start': start_dt_str,
'end': end_dt_str,
'status': 'planned',
'itemId': appt_id,
'userName': str(appt.get('user') or ''),
'notes': str(appt.get('note') or ''),
'period': None,
'isCurrentUser': True,
'itemBorrower': '',
})
days_in_range = _date_iter(date_start, date_end) or ([date_start] if date_start else [])
span_entries = []
for entry in time_span:
s = str(entry or '').strip()
if not s:
continue
m_date = _re.match(r"^(\d{4}-\d{2}-\d{2})\s+(\d{2}:\d{2})-(\d{2}:\d{2})$", s)
if m_date:
span_entries.append((m_date.group(1), m_date.group(2), m_date.group(3)))
continue
m = _re.match(r"^(\d{2}:\d{2})-(\d{2}:\d{2})$", s)
if m:
for day in days_in_range:
span_entries.append((day, m.group(1), m.group(2)))
# If the stored span is already day-specific, use it as-is.
# If it was generic, we duplicated it to each day in the range above.
if not span_entries and days_in_range:
# Fallback: create a simple all-day marker for each date so the appointment is visible.
for day in days_in_range:
span_entries.append((day, '08:00', '16:45'))
for day, start_time, end_time in span_entries:
result.append({
'id': f"{appt_id}-{day}-{start_time}",
'title': title,
'start': f"{day}T{start_time}",
'end': f"{day}T{end_time}",
'status': 'planned',
'itemId': appt_id,
'userName': str(appt.get('user') or ''),
'notes': str(appt.get('note') or ''),
'period': None,
'isCurrentUser': True,
'itemBorrower': '',
})
return jsonify({'ok': True, 'bookings': result})
except Exception as e:
+18 -80
View File
@@ -459,7 +459,10 @@
Hinweis: Im Schnellmodus zuerst den Schülerausweis scannen, danach den Buch-/Mediencode.
</div>
<div id="scanReaderWrap" class="library-scan-reader-wrap">
<div id="libraryQrReader" class="library-scan-reader"></div>
<div id="libraryQrReader" class="library-scan-reader">
<video id="libraryQrReaderVideo" playsinline style="width: 100%; border-radius: 8px;"></video>
<canvas id="libraryQrReaderCanvas" style="display: none;"></canvas>
</div>
</div>
</div>
<div id="filterPanel" class="library-filter-panel">
@@ -547,7 +550,8 @@
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/html5-qrcode/minified/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>
// State
let libraryItems = [];
@@ -1002,54 +1006,8 @@
}
async function ensureScannerLibraryLoaded() {
if (typeof Html5QrcodeScanner !== 'undefined') {
return true;
}
const sources = [
'https://cdn.jsdelivr.net/npm/html5-qrcode/minified/html5-qrcode.min.js'
];
for (const src of sources) {
try {
await new Promise((resolve, reject) => {
const existing = document.querySelector(`script[data-scanner-src="${src}"]`);
if (existing) {
const onLoad = () => resolve();
const onError = () => reject(new Error('Script load failed'));
existing.addEventListener('load', onLoad, { once: true });
existing.addEventListener('error', onError, { once: true });
setTimeout(() => {
existing.removeEventListener('load', onLoad);
existing.removeEventListener('error', onError);
if (typeof Html5QrcodeScanner !== 'undefined') {
resolve();
} else {
reject(new Error('Script not available'));
}
}, 1200);
return;
}
const script = document.createElement('script');
script.src = src;
script.async = true;
script.defer = true;
script.dataset.scannerSrc = src;
script.onload = () => resolve();
script.onerror = () => reject(new Error('Script load failed'));
document.head.appendChild(script);
});
if (typeof Html5QrcodeScanner !== 'undefined') {
return true;
}
} catch (err) {
console.warn('Scanner library load failed from', src, err);
}
}
return false;
// HybridScanner is loaded globally via script tag
return typeof HybridScanner !== 'undefined';
}
async function startScanner() {
@@ -1066,37 +1024,17 @@
readerWrap.style.display = 'block';
try {
const formats = [];
if (typeof Html5QrcodeSupportedFormats !== 'undefined') {
formats.push(
Html5QrcodeSupportedFormats.QR_CODE,
Html5QrcodeSupportedFormats.EAN_13,
Html5QrcodeSupportedFormats.EAN_8,
Html5QrcodeSupportedFormats.CODE_128,
Html5QrcodeSupportedFormats.CODE_39,
Html5QrcodeSupportedFormats.UPC_A,
Html5QrcodeSupportedFormats.UPC_E,
Html5QrcodeSupportedFormats.ITF,
Html5QrcodeSupportedFormats.CODABAR
);
}
const scannerConfig = {
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,
rememberLastUsedCamera: true,
aspectRatio: 1.333334
};
if (formats.length > 0) {
scannerConfig.formatsToSupport = formats;
}
facingMode: 'environment',
onSuccess: handleScanSuccess,
onError: handleScanError
});
scannerInstance = scannerInstance || new Html5QrcodeScanner(
'libraryQrReader',
scannerConfig,
false
);
scannerInstance.render(handleScanSuccess, handleScanError);
scannerInstance.start();
scannerRunning = true;
toggleBtn.textContent = 'Scanner stoppen';
setScanStatus('Scanner aktiv. Jetzt Code scannen.', 'warn');
@@ -1113,7 +1051,7 @@
const readerWrap = document.getElementById('scanReaderWrap');
const toggleBtn = document.getElementById('toggleScannerBtn');
try {
await scannerInstance.clear();
scannerInstance.stop();
} catch (err) {
console.error('Scanner stop failed:', err);
}
+32 -20
View File
@@ -413,7 +413,10 @@
</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: 500px; 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">
<span>Name</span>
@@ -526,7 +529,8 @@
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>
// Global state
const highlightItemId = (window.serverVars && window.serverVars.highlightItemId && window.serverVars.highlightItemId !== 'null')
@@ -539,7 +543,7 @@
document.addEventListener('DOMContentLoaded', function() {
const scanButton = document.getElementById('scanButton');
const qrReader = document.getElementById('qr-reader');
let html5QrcodeScanner = null;
let hybridScanner = null;
if (!scanButton || !qrReader) return;
@@ -553,28 +557,36 @@
if (qrReader.style.display !== 'block') {
qrReader.style.display = 'block';
html5QrcodeScanner = new Html5QrcodeScanner(
'qr-reader', { fps: 10, qrbox: 250 }
);
html5QrcodeScanner.render((decodedText) => {
html5QrcodeScanner.clear();
qrReader.style.display = 'none';
// Put scanned code into the search box and trigger search
const searchInput = document.getElementById('code-search');
if (searchInput) {
searchInput.value = decodedText;
searchByCode();
}
hybridScanner = new HybridScanner({
videoId: 'qr-reader-video',
canvasId: 'qr-reader-canvas',
formats: ['QR_CODE', 'CODE_128', 'EAN_13', 'EAN_8', 'CODE_39'],
fps: 10,
facingMode: 'environment',
onSuccess: function(decodedText) {
hybridScanner.stop();
qrReader.style.display = 'none';
// Put scanned code into the search box and trigger search
const searchInput = document.getElementById('code-search');
if (searchInput) {
searchInput.value = decodedText;
searchByCode();
}
setScannerUi(false);
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);
+75 -51
View File
@@ -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() {
-32
View File
@@ -288,22 +288,6 @@ document.addEventListener('DOMContentLoaded', function() {
? processedEvents
: processedEvents.filter(event => event.extendedProps.status !== 'completed');
// If any event spans multiple days, switch to week view for better visibility
try {
const multiDay = processedEvents.some(ev => {
try {
const s = new Date(ev.start);
const e = new Date(ev.end);
return s && e && s.toDateString() !== e.toDateString();
} catch (e) { return false; }
});
if (multiDay && calendar && calendar.view && calendar.view.type === 'timeGridDay') {
calendar.changeView('timeGridWeek');
}
} catch (e) {
// ignore
}
// Provide events to calendar
successCallback(filteredEvents);
})
@@ -330,22 +314,6 @@ document.addEventListener('DOMContentLoaded', function() {
periodDiv.style.display = 'inline-block';
periodDiv.style.fontSize = '10px';
info.el.appendChild(periodDiv);
} else {
// Check if it is a multi-day event
const start = info.event.start;
const end = info.event.end;
if (start && end && start.toDateString() !== end.toDateString()) {
const periodDiv = document.createElement('div');
periodDiv.className = 'fc-event-period-marker';
periodDiv.textContent = 'Mehrtägig';
periodDiv.style.backgroundColor = 'rgba(255,255,255,0.3)';
periodDiv.style.borderRadius = '3px';
periodDiv.style.padding = '1px 3px';
periodDiv.style.marginTop = '2px';
periodDiv.style.display = 'inline-block';
periodDiv.style.fontSize = '10px';
info.el.appendChild(periodDiv);
}
}
// Add borrowed indicator if relevant
-54
View File
@@ -1,54 +0,0 @@
#!/usr/bin/env python3
"""
Small debug script to check tenant-aware lookup for an appointment id.
Usage: ./tools/debug_get_appointment.py <appointment_id> [tenant]
"""
import sys
from bson.objectid import ObjectId
from Web.modules.database.settings import MongoClient, MONGODB_HOST, MONGODB_PORT, MONGODB_DB
import Web.modules.database.termine as termine
def main():
if len(sys.argv) < 2:
print("Usage: debug_get_appointment.py <appointment_id> [tenant]")
sys.exit(2)
aid = sys.argv[1]
tenant = sys.argv[2] if len(sys.argv) > 2 else None
if tenant:
print(f"Looking up appointment {aid} for tenant {tenant}")
else:
print(f"Looking up appointment {aid} for default tenant")
try:
# Use the module helper directly
item = termine.get_item(aid)
if item:
print('Found with termini.get_item:')
print(item)
else:
print('termin.get_item returned None')
# Try explicit MongoClient + tenant DB resolution
client = MongoClient(MONGODB_HOST, MONGODB_PORT)
try:
from Web.tenant import TenantContext, get_tenant_db
if tenant:
ctx = TenantContext()
ctx.tenant_id = tenant
db = ctx.get_database(client)
else:
db = get_tenant_db(client) if 'get_tenant_db' in dir() else client[MONGODB_DB]
doc = db['appointments'].find_one({'_id': ObjectId(aid)})
print('Direct DB find_one returned:')
print(doc)
finally:
client.close()
except Exception as e:
print('Error during debug lookup:', e)
if __name__ == '__main__':
main()