Compare commits
24 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f763ad064d | |||
| bc94ffdc18 | |||
| 76e9670cee | |||
| 577c6c4bed | |||
| 3aa19b10d1 | |||
| ff742d018e | |||
| 6fa40f26b0 | |||
| 777fc81064 | |||
| cf5e38e319 | |||
| b76941d5fe | |||
| f46f2674b0 | |||
| 8ebbdcfd54 | |||
| 92b5235035 | |||
| 4bda2e044b | |||
| b83b6c0ba2 | |||
| ab718d6ac2 | |||
| 112d9b6aa0 | |||
| 5139a50a43 | |||
| 997c7b42d5 | |||
| 20e5d3bb9a | |||
| f62b22c2f7 | |||
| e76d525e4b | |||
| 7117dac67c | |||
| 5313c507ed |
@@ -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.25
|
||||
v0.8.31.1
|
||||
|
||||
+54
-28
@@ -4822,6 +4822,23 @@ def get_user_appointments():
|
||||
appointments = termin.get_upcoming_for_user(username, limit=250)
|
||||
|
||||
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:
|
||||
@@ -4830,35 +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 []
|
||||
|
||||
# Try to extract a time range from the first time_span entry
|
||||
start_dt_str = date_start
|
||||
end_dt_str = date_end
|
||||
try:
|
||||
import re as _re
|
||||
if time_span:
|
||||
first = str(time_span[0])
|
||||
m = _re.search(r"(\d{2}:\d{2})-(\d{2}:\d{2})", first)
|
||||
if m:
|
||||
start_dt_str = f"{date_start}T{m.group(1)}"
|
||||
end_dt_str = f"{date_start}T{m.group(2)}"
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
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:
|
||||
|
||||
+61
-26
@@ -205,8 +205,7 @@
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/html5-qrcode/minified/html5-qrcode.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/@ericblade/quagga2/dist/quagga.js"></script>
|
||||
<script>
|
||||
// View mode persistence
|
||||
const LIBRARY_VIEW_MODE_KEY = 'inventarLibraryViewMode';
|
||||
@@ -632,43 +631,79 @@ document.getElementById('favoriteToggle').addEventListener('click', function() {
|
||||
});
|
||||
|
||||
// Scanner toggle
|
||||
let scanner = null;
|
||||
l// Scanner state tracking
|
||||
let isScanning = false;
|
||||
|
||||
document.getElementById('scannerBtn').addEventListener('click', function(e) {
|
||||
e.stopPropagation();
|
||||
const container = document.getElementById('qrContainer');
|
||||
const isOpen = container.style.display !== 'none';
|
||||
const btn = this;
|
||||
|
||||
if (isOpen) {
|
||||
// Close the scanner
|
||||
container.style.display = 'none';
|
||||
if (scanner) scanner.clear();
|
||||
this.classList.remove('open');
|
||||
this.setAttribute('aria-expanded', 'false');
|
||||
} else {
|
||||
container.style.display = 'block';
|
||||
this.classList.add('open');
|
||||
this.setAttribute('aria-expanded', 'true');
|
||||
Quagga.stop();
|
||||
isScanning = false;
|
||||
|
||||
if (!scanner) {
|
||||
scanner = new Html5Qrcode('qr-reader');
|
||||
scanner.start(
|
||||
{ facingMode: "environment" },
|
||||
{ fps: 10, qrbox: 250 },
|
||||
onScanSuccess,
|
||||
onScanError
|
||||
);
|
||||
}
|
||||
btn.classList.remove('open');
|
||||
btn.setAttribute('aria-expanded', 'false');
|
||||
} else {
|
||||
// Open the scanner
|
||||
container.style.display = 'block';
|
||||
btn.classList.add('open');
|
||||
btn.setAttribute('aria-expanded', 'true');
|
||||
|
||||
Quagga.init({
|
||||
inputStream: {
|
||||
name: "Live",
|
||||
type: "LiveStream",
|
||||
// Targets the element where the video stream will inject
|
||||
target: document.querySelector('#qr-reader'),
|
||||
constraints: {
|
||||
width: 640,
|
||||
height: 480,
|
||||
facingMode: "environment" // Forces back camera
|
||||
},
|
||||
},
|
||||
decoder: {
|
||||
// Optimized for 1D barcodes (e.g., student IDs, member cards)
|
||||
readers: ["code_128_reader", "ean_reader", "code_39_reader", "upc_reader"]
|
||||
}
|
||||
}, function(err) {
|
||||
if (err) {
|
||||
console.error("Initialization error:", err);
|
||||
alert("Kamera konnte nicht gestartet werden.");
|
||||
return;
|
||||
}
|
||||
Quagga.start();
|
||||
isScanning = true;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
function onScanSuccess(decodedText, decodedResult) {
|
||||
const studentId = decodedText.trim();
|
||||
// Single Quagga event listener for successful scans
|
||||
Quagga.onDetected(function(data) {
|
||||
const decodedText = data.codeResult.code;
|
||||
const studentId = String(decodedText || '').trim();
|
||||
|
||||
// Stop scanning immediately to prevent multiple triggers
|
||||
Quagga.stop();
|
||||
isScanning = false;
|
||||
|
||||
// Reset UI states to closed
|
||||
const container = document.getElementById('qrContainer');
|
||||
const btn = document.getElementById('scannerBtn');
|
||||
if (container) container.style.display = 'none';
|
||||
if (btn) {
|
||||
btn.classList.remove('open');
|
||||
btn.setAttribute('aria-expanded', 'false');
|
||||
}
|
||||
|
||||
// Route the scanned data to your input
|
||||
document.getElementById('studentIdInput').value = studentId;
|
||||
alert('Ausweis gescannt: ' + studentId);
|
||||
}
|
||||
|
||||
function onScanError(error) {
|
||||
// Silently ignore scan errors
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
+103
-41
@@ -525,8 +525,7 @@
|
||||
|
||||
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://cdn.jsdelivr.net/npm/@ericblade/quagga2/dist/quagga.js"></script>
|
||||
<script>
|
||||
// Global state
|
||||
const highlightItemId = (window.serverVars && window.serverVars.highlightItemId && window.serverVars.highlightItemId !== 'null')
|
||||
@@ -535,51 +534,114 @@
|
||||
let codeSearchTerm = '';
|
||||
let descSearchIds = null; // Set of matching IDs or null when disabled
|
||||
|
||||
// QR scanner toggle setup
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const scanButton = document.getElementById('scanButton');
|
||||
// Keep track of the scanner state globally/outer scope
|
||||
let isScanning = false;
|
||||
let activeScannerCallback = null; // Tracks which function currently owns the scanner output
|
||||
|
||||
function startScanner(targetCallback) {
|
||||
const qrReader = document.getElementById('qr-reader');
|
||||
let html5QrcodeScanner = null;
|
||||
const statusText = document.getElementById('status');
|
||||
|
||||
if (!scanButton || !qrReader) return;
|
||||
// Store the custom action that should happen when a barcode is found
|
||||
activeScannerCallback = targetCallback;
|
||||
|
||||
const setScannerUi = (isOpen) => {
|
||||
scanButton.classList.toggle('is-active', isOpen);
|
||||
scanButton.setAttribute('aria-expanded', isOpen ? 'true' : 'false');
|
||||
scanButton.textContent = isOpen ? 'Scanner schliessen' : 'Barcode scannen';
|
||||
};
|
||||
// Show the container
|
||||
if (qrReader) qrReader.style.display = 'block';
|
||||
if (statusText) statusText.innerText = "Initializing camera...";
|
||||
|
||||
scanButton.addEventListener('click', function() {
|
||||
if (qrReader.style.display !== 'block') {
|
||||
qrReader.style.display = 'block';
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
setScannerUi(false);
|
||||
});
|
||||
|
||||
setScannerUi(true);
|
||||
} else {
|
||||
if (html5QrcodeScanner) {
|
||||
html5QrcodeScanner.clear();
|
||||
}
|
||||
qrReader.style.display = 'none';
|
||||
setScannerUi(false);
|
||||
Quagga.init({
|
||||
inputStream: {
|
||||
name: "Live",
|
||||
type: "LiveStream",
|
||||
// Make sure this targets your correct HTML container element
|
||||
target: document.querySelector('#scanner-container'),
|
||||
constraints: {
|
||||
width: 640,
|
||||
height: 480,
|
||||
facingMode: "environment" // Force rear camera
|
||||
},
|
||||
},
|
||||
decoder: {
|
||||
// Your required formats mapped to Quagga2 reader strings
|
||||
readers: ["code_128_reader", "ean_reader", "code_39_reader", "upc_reader"]
|
||||
}
|
||||
}, function(err) {
|
||||
if (err) {
|
||||
console.error("Initialization error:", err);
|
||||
if (statusText) statusText.innerText = "Camera Error";
|
||||
return;
|
||||
}
|
||||
|
||||
Quagga.start();
|
||||
isScanning = true;
|
||||
|
||||
// If we are using the main search button, manage its UI state
|
||||
if (!targetCallback) {
|
||||
setScannerUi(true);
|
||||
}
|
||||
|
||||
if (statusText) statusText.innerText = "Scanning...";
|
||||
});
|
||||
}
|
||||
|
||||
function stopScanner() {
|
||||
Quagga.stop();
|
||||
isScanning = false;
|
||||
activeScannerCallback = null; // Reset the callback routing
|
||||
|
||||
const qrReader = document.getElementById('qr-reader');
|
||||
if (qrReader) qrReader.style.display = 'none';
|
||||
|
||||
setScannerUi(false);
|
||||
}
|
||||
|
||||
function setScannerUi(isOpen) {
|
||||
const scanBtn = document.getElementById('scanButton');
|
||||
if (!scanBtn) return;
|
||||
|
||||
scanBtn.classList.toggle('is-active', isOpen);
|
||||
scanBtn.setAttribute('aria-expanded', isOpen ? 'true' : 'false');
|
||||
scanBtn.textContent = isOpen ? 'Scanner schliessen' : 'Barcode scannen';
|
||||
}
|
||||
|
||||
// Unified global reader callback
|
||||
Quagga.onDetected(function(data) {
|
||||
const barcode = String(data.codeResult.code || '').trim();
|
||||
console.log("Barcode detected:", barcode);
|
||||
|
||||
// Keep a local reference to the active callback before shutting down the engine
|
||||
const currentCallback = activeScannerCallback;
|
||||
|
||||
// Critical: Stop scanning immediately to prevent multiple triggers
|
||||
stopScanner();
|
||||
|
||||
// Route data based on who opened the scanner
|
||||
if (typeof currentCallback === "function") {
|
||||
// Send data directly to the specific edit field's logic
|
||||
currentCallback(barcode);
|
||||
} else {
|
||||
// Fallback Default: Main search box logic
|
||||
const resultText = document.getElementById('barcode-result');
|
||||
if (resultText) resultText.innerText = barcode;
|
||||
|
||||
const searchInput = document.getElementById('code-search');
|
||||
if (searchInput) {
|
||||
searchInput.value = barcode;
|
||||
if (typeof searchByCode === "function") {
|
||||
searchByCode();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Event Listener for the main standalone search button
|
||||
document.getElementById('scanButton')?.addEventListener('click', function() {
|
||||
if (!isScanning) {
|
||||
// Pass null so it defaults to filling the standard search box
|
||||
startScanner(null);
|
||||
} else {
|
||||
stopScanner();
|
||||
}
|
||||
});
|
||||
|
||||
// Return preferred primary and fallback image URLs based on extension
|
||||
|
||||
+123
-74
@@ -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;">
|
||||
<h3>Status: <span id="status">Bereit</span></h3>
|
||||
<div id="scanner-container"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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
|
||||
</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>
|
||||
// Function to check if a file is a video
|
||||
function isVideoFile(filename) {
|
||||
@@ -2739,7 +2742,6 @@ document.addEventListener('DOMContentLoaded', ()=>{
|
||||
}
|
||||
|
||||
// Initialize QR Code scanner and global variables
|
||||
let html5QrcodeScanner = null;
|
||||
let codeSearchTerm = '';
|
||||
let descSearchIds = null; // Set of matching IDs or null when disabled
|
||||
let currentUsername = '';
|
||||
@@ -2768,51 +2770,114 @@ document.addEventListener('DOMContentLoaded', ()=>{
|
||||
// No-op in production
|
||||
return;
|
||||
}
|
||||
|
||||
document.getElementById('scanButton').addEventListener('click', function() {
|
||||
const qrReader = document.getElementById('qr-reader');
|
||||
const scanBtn = document.getElementById('scanButton');
|
||||
|
||||
const setScannerUi = (isOpen) => {
|
||||
if (!scanBtn) return;
|
||||
scanBtn.classList.toggle('is-active', isOpen);
|
||||
scanBtn.setAttribute('aria-expanded', isOpen ? 'true' : 'false');
|
||||
scanBtn.textContent = isOpen ? 'Scanner schliessen' : 'Barcode scannen';
|
||||
};
|
||||
|
||||
if (qrReader.style.display === 'none') {
|
||||
qrReader.style.display = 'block';
|
||||
|
||||
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
|
||||
// Keep track of the scanner state globally/outer scope
|
||||
let isScanning = false;
|
||||
let activeScannerCallback = null; // Tracks which function currently owns the scanner output
|
||||
|
||||
function startScanner(targetCallback) {
|
||||
const qrReader = document.getElementById('qr-reader');
|
||||
const statusText = document.getElementById('status');
|
||||
|
||||
// Store the custom action that should happen when a barcode is found
|
||||
activeScannerCallback = targetCallback;
|
||||
|
||||
// Show the container
|
||||
if (qrReader) qrReader.style.display = 'block';
|
||||
if (statusText) statusText.innerText = "Initializing camera...";
|
||||
|
||||
Quagga.init({
|
||||
inputStream: {
|
||||
name: "Live",
|
||||
type: "LiveStream",
|
||||
// Make sure this targets your correct HTML container element
|
||||
target: document.querySelector('#scanner-container'),
|
||||
constraints: {
|
||||
width: 640,
|
||||
height: 480,
|
||||
facingMode: "environment" // Force rear camera
|
||||
},
|
||||
},
|
||||
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();
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
if (qrReader.style.display !== 'none') {
|
||||
if (html5QrcodeScanner) {
|
||||
html5QrcodeScanner.clear();
|
||||
}
|
||||
qrReader.style.display = 'none';
|
||||
// Toggle close if it's already running
|
||||
if (isScanning && qrReader.style.display !== 'none') {
|
||||
stopScanner();
|
||||
scanEditBtn.textContent = 'Barcode scannen';
|
||||
return;
|
||||
}
|
||||
|
||||
qrReader.style.display = 'block';
|
||||
scanEditBtn.textContent = 'Scanner schließen';
|
||||
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();
|
||||
|
||||
// Start scanner with custom logic mapping to the Code input field
|
||||
startScanner(function(decodedText) {
|
||||
editCodeInput.value = decodedText;
|
||||
validateCodeField(editCodeInput, document.getElementById('edit-item-id')?.value || null);
|
||||
scanEditBtn.textContent = 'Barcode scannen';
|
||||
});
|
||||
@@ -2857,26 +2914,18 @@ document.addEventListener('DOMContentLoaded', ()=>{
|
||||
return;
|
||||
}
|
||||
|
||||
if (qrReader.style.display !== 'none') {
|
||||
if (html5QrcodeScanner) {
|
||||
html5QrcodeScanner.clear();
|
||||
}
|
||||
qrReader.style.display = 'none';
|
||||
// Toggle close if it's already running
|
||||
if (isScanning && qrReader.style.display !== 'none') {
|
||||
stopScanner();
|
||||
scanIsbnBtn.textContent = 'ISBN scannen';
|
||||
return;
|
||||
}
|
||||
|
||||
qrReader.style.display = 'block';
|
||||
scanIsbnBtn.textContent = 'Scanner schließen';
|
||||
html5QrcodeScanner = new Html5QrcodeScanner('qr-reader', {
|
||||
fps: 10,
|
||||
qrbox: 250,
|
||||
rememberLastUsedCamera: true
|
||||
});
|
||||
html5QrcodeScanner.render((decodedText) => {
|
||||
html5QrcodeScanner.clear();
|
||||
qrReader.style.display = 'none';
|
||||
editIsbnInput.value = String(decodedText || '').trim();
|
||||
|
||||
// Start scanner with custom logic mapping to the ISBN input field
|
||||
startScanner(function(decodedText) {
|
||||
editIsbnInput.value = decodedText;
|
||||
scanIsbnBtn.textContent = 'ISBN scannen';
|
||||
if (typeof fetchBookInfo === 'function') {
|
||||
fetchBookInfo('edit');
|
||||
|
||||
@@ -357,63 +357,7 @@
|
||||
}
|
||||
});
|
||||
|
||||
// Build background gap events to grey-out times that are not available
|
||||
function buildBackgroundGaps() {
|
||||
const bgEvents = [];
|
||||
const uiMin = slotMinTime; // UI visible window start
|
||||
const uiMax = slotMaxTime; // UI visible window end
|
||||
|
||||
// Helper to collect allowed intervals per day
|
||||
const spans = Array.isArray(available.time_span) ? available.time_span : [];
|
||||
|
||||
allDays.forEach(function(day) {
|
||||
// collect allowed intervals for this day
|
||||
const allowed = [];
|
||||
spans.forEach(function(entry) {
|
||||
const p = parseTimeSpanEntry(entry);
|
||||
if (!p) return;
|
||||
if (p.date && p.date !== day) return;
|
||||
// for generic spans (no date) include for all days
|
||||
allowed.push({from: p.from + ':00', to: p.to + ':00'});
|
||||
});
|
||||
|
||||
// sort allowed intervals
|
||||
allowed.sort(function(a,b){ return a.from < b.from ? -1 : (a.from > b.from ? 1 : 0); });
|
||||
|
||||
// if no allowed intervals -> grey the full ui window
|
||||
if (allowed.length === 0) {
|
||||
bgEvents.push({
|
||||
start: new Date(day + 'T' + uiMin),
|
||||
end: new Date(day + 'T' + uiMax),
|
||||
display: 'background',
|
||||
backgroundColor: '#f1f3f5'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// gap before first allowed
|
||||
if (allowed[0].from > uiMin) {
|
||||
bgEvents.push({ start: new Date(day + 'T' + uiMin), end: new Date(day + 'T' + allowed[0].from), display: 'background', backgroundColor: '#f1f3f5' });
|
||||
}
|
||||
|
||||
// gaps between allowed intervals
|
||||
for (let i = 0; i < allowed.length - 1; i++) {
|
||||
const endA = allowed[i].to;
|
||||
const startB = allowed[i+1].from;
|
||||
if (startB > endA) {
|
||||
bgEvents.push({ start: new Date(day + 'T' + endA), end: new Date(day + 'T' + startB), display: 'background', backgroundColor: '#f1f3f5' });
|
||||
}
|
||||
}
|
||||
|
||||
// gap after last allowed
|
||||
const last = allowed[allowed.length - 1];
|
||||
if (last.to < uiMax) {
|
||||
bgEvents.push({ start: new Date(day + 'T' + last.to), end: new Date(day + 'T' + uiMax), display: 'background', backgroundColor: '#f1f3f5' });
|
||||
}
|
||||
});
|
||||
|
||||
return bgEvents;
|
||||
}
|
||||
// No background greying: show full calendar skeleton and only mark possible slots
|
||||
|
||||
function updateSliderLabel() {
|
||||
const dateList = dateRangeInclusive(String(available.date_start || ''), String(available.date_end || ''));
|
||||
@@ -471,11 +415,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Add background gap events first so they appear behind slot events
|
||||
const bgGaps = buildBackgroundGaps();
|
||||
bgGaps.forEach(function(be) {
|
||||
calendar.addEvent(be);
|
||||
});
|
||||
// No background gaps: keep full skeleton/grid visible
|
||||
|
||||
// Add candidate free slots (blue)
|
||||
candidateSlots.forEach(function (slot) {
|
||||
|
||||
@@ -287,7 +287,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
const filteredEvents = showCompletedBookings
|
||||
? processedEvents
|
||||
: processedEvents.filter(event => event.extendedProps.status !== 'completed');
|
||||
|
||||
|
||||
// Provide events to calendar
|
||||
successCallback(filteredEvents);
|
||||
})
|
||||
@@ -314,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
|
||||
|
||||
@@ -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