- Menü aus Header entfernt, neuer Eintrag unter Produkte > Scanner - Barcode-Erkennung: patchSize medium, grösserer Scan-Bereich, höhere Frequenz - Timeout-Hinweis nach 8s wenn kein Barcode erkannt wird - Tab-Wechsel (Order/Shop/Inventur) ohne Seitenreload, Kamera bleibt aktiv - PWA: gleiche Tab-Logik, Buttons statt Links - Changelog und README aktualisiert Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
372 lines
10 KiB
PHP
Executable file
372 lines
10 KiB
PHP
Executable file
<?php
|
|
/* Copyright (C) 2026 Eduard Wisch <data@data-it-solution.de>
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
/**
|
|
* \file class/actions_handybarcodescanner.class.php
|
|
* \ingroup handybarcodescanner
|
|
* \brief Hook class for HandyBarcodeScanner module
|
|
*/
|
|
|
|
/**
|
|
* Class ActionsHandyBarcodeScanner
|
|
*/
|
|
class ActionsHandyBarcodeScanner
|
|
{
|
|
/**
|
|
* @var DoliDB Database handler.
|
|
*/
|
|
public $db;
|
|
|
|
/**
|
|
* @var string Error code (or message)
|
|
*/
|
|
public $error = '';
|
|
|
|
/**
|
|
* @var array Errors
|
|
*/
|
|
public $errors = array();
|
|
|
|
/**
|
|
* @var array Hook results. Propagated to $hookmanager->resArray for later reuse
|
|
*/
|
|
public $results = array();
|
|
|
|
/**
|
|
* @var string String displayed by executeHook() immediately after return
|
|
*/
|
|
public $resprints;
|
|
|
|
/**
|
|
* Constructor
|
|
*
|
|
* @param DoliDB $db Database handler
|
|
*/
|
|
public function __construct($db)
|
|
{
|
|
$this->db = $db;
|
|
}
|
|
|
|
/**
|
|
* Execute action - Hook on product card view
|
|
*
|
|
* @param array $parameters Array of parameters
|
|
* @param CommonObject $object The object to process
|
|
* @param string $action Action code
|
|
* @param HookManager $hookmanager Hook manager propagated to allow calling another hook
|
|
* @return int 0=OK, <0 on error, >0 to replace standard code
|
|
*/
|
|
public function tabContentViewProduct($parameters, &$object, &$action, $hookmanager)
|
|
{
|
|
global $conf, $langs;
|
|
|
|
// Only if module is enabled
|
|
if (!isModEnabled('handybarcodescanner')) {
|
|
return 0;
|
|
}
|
|
|
|
// Inject JavaScript and CSS for barcode zoom feature
|
|
$this->resprints = $this->getBarcodeZoomAssets();
|
|
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* Get JavaScript and CSS for barcode zoom modal
|
|
*
|
|
* @return string HTML with script and style tags
|
|
*/
|
|
private function getBarcodeZoomAssets()
|
|
{
|
|
$html = '';
|
|
|
|
// CSS for the modal
|
|
$html .= '
|
|
<style>
|
|
/* Barcode Zoom Modal */
|
|
.barcode-zoom-overlay {
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
background: rgba(0, 0, 0, 0.85);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
z-index: 10000;
|
|
cursor: pointer;
|
|
animation: barcodeZoomFadeIn 0.2s ease;
|
|
}
|
|
|
|
@keyframes barcodeZoomFadeIn {
|
|
from { opacity: 0; }
|
|
to { opacity: 1; }
|
|
}
|
|
|
|
.barcode-zoom-content {
|
|
background: #fff;
|
|
padding: 30px 40px;
|
|
border-radius: 12px;
|
|
text-align: center;
|
|
max-width: 90vw;
|
|
max-height: 90vh;
|
|
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.4);
|
|
animation: barcodeZoomScaleIn 0.25s ease;
|
|
}
|
|
|
|
@keyframes barcodeZoomScaleIn {
|
|
from { transform: scale(0.8); opacity: 0; }
|
|
to { transform: scale(1); opacity: 1; }
|
|
}
|
|
|
|
.barcode-zoom-text {
|
|
font-family: monospace;
|
|
font-size: 28px;
|
|
color: #333;
|
|
letter-spacing: 3px;
|
|
margin-top: 15px;
|
|
font-weight: bold;
|
|
}
|
|
|
|
.barcode-zoom-hint {
|
|
font-size: 12px;
|
|
color: #999;
|
|
margin-top: 15px;
|
|
}
|
|
|
|
/* Make original barcode clickable */
|
|
.barcode-clickable {
|
|
cursor: pointer !important;
|
|
transition: all 0.2s ease;
|
|
display: inline-block;
|
|
padding: 4px 8px;
|
|
border-radius: 4px;
|
|
border: 1px dashed transparent;
|
|
}
|
|
|
|
.barcode-clickable:hover {
|
|
background: rgba(0, 119, 179, 0.1);
|
|
border-color: #0077b3;
|
|
}
|
|
|
|
/* SVG Barcode Styles */
|
|
.barcode-zoom-svg {
|
|
max-width: 100%;
|
|
height: auto;
|
|
}
|
|
</style>
|
|
';
|
|
|
|
// JavaScript for barcode zoom functionality
|
|
$html .= '
|
|
<script>
|
|
(function() {
|
|
"use strict";
|
|
|
|
// Wait for DOM to be fully loaded
|
|
function initBarcodeZoom() {
|
|
console.log("HandyBarcodeScanner: initBarcodeZoom started");
|
|
|
|
// Find all rows in the product info table
|
|
var rows = document.querySelectorAll("table.border tr, table.tableforfield tr");
|
|
console.log("HandyBarcodeScanner: Found " + rows.length + " rows");
|
|
|
|
rows.forEach(function(row) {
|
|
var cells = row.querySelectorAll("td");
|
|
if (cells.length < 2) return;
|
|
|
|
var labelCell = cells[0];
|
|
var valueCell = cells[1];
|
|
|
|
// Check if this is the barcode value row
|
|
var labelText = labelCell.textContent || labelCell.innerText;
|
|
|
|
// Match "BarcodeValue" or "Barcode" but not "BarcodeType"
|
|
var isBarcode = (labelText.indexOf("Barcode") !== -1 || labelText.indexOf("barcode") !== -1);
|
|
var isType = (labelText.indexOf("Type") !== -1 || labelText.indexOf("type") !== -1 || labelText.indexOf("Typ") !== -1);
|
|
|
|
if (!isBarcode || isType) return;
|
|
|
|
console.log("HandyBarcodeScanner: Found barcode row with label: " + labelText);
|
|
|
|
// Find the barcode value
|
|
var clipboardSpan = valueCell.querySelector(".clipboardCPValue");
|
|
if (!clipboardSpan) {
|
|
console.log("HandyBarcodeScanner: No clipboardCPValue found in this row");
|
|
return;
|
|
}
|
|
|
|
var barcodeValue = clipboardSpan.textContent.trim();
|
|
console.log("HandyBarcodeScanner: Barcode value: " + barcodeValue);
|
|
|
|
if (!barcodeValue || barcodeValue === "-" || barcodeValue === "") return;
|
|
|
|
// Make the entire value cell clickable
|
|
valueCell.classList.add("barcode-clickable");
|
|
valueCell.title = "Klicken zum Vergroessern";
|
|
valueCell.style.cursor = "pointer";
|
|
|
|
valueCell.addEventListener("click", function(e) {
|
|
// Dont interfere with clipboard button clicks
|
|
if (e.target.closest(".clipboardCPButton")) return;
|
|
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
showBarcodeZoom(barcodeValue);
|
|
});
|
|
|
|
console.log("HandyBarcodeScanner: Click handler attached");
|
|
});
|
|
}
|
|
|
|
function showBarcodeZoom(barcodeValue) {
|
|
console.log("HandyBarcodeScanner: showBarcodeZoom for " + barcodeValue);
|
|
|
|
// Create overlay
|
|
var overlay = document.createElement("div");
|
|
overlay.className = "barcode-zoom-overlay";
|
|
overlay.onclick = function(e) {
|
|
if (e.target === overlay) {
|
|
overlay.remove();
|
|
}
|
|
};
|
|
|
|
// Create content
|
|
var content = document.createElement("div");
|
|
content.className = "barcode-zoom-content";
|
|
content.onclick = function(e) {
|
|
e.stopPropagation();
|
|
};
|
|
|
|
// Generate SVG barcode
|
|
var barcodeSvg = generateBarcodeSVG(barcodeValue);
|
|
|
|
// Add barcode SVG
|
|
var svgContainer = document.createElement("div");
|
|
svgContainer.innerHTML = barcodeSvg;
|
|
content.appendChild(svgContainer);
|
|
|
|
// Add text below barcode
|
|
var text = document.createElement("div");
|
|
text.className = "barcode-zoom-text";
|
|
text.textContent = barcodeValue;
|
|
content.appendChild(text);
|
|
|
|
// Add hint
|
|
var hint = document.createElement("div");
|
|
hint.className = "barcode-zoom-hint";
|
|
hint.textContent = "Klicken Sie ausserhalb oder druecken Sie ESC zum Schliessen";
|
|
content.appendChild(hint);
|
|
|
|
overlay.appendChild(content);
|
|
document.body.appendChild(overlay);
|
|
|
|
// Close on Escape key
|
|
var closeHandler = function(e) {
|
|
if (e.key === "Escape") {
|
|
overlay.remove();
|
|
document.removeEventListener("keydown", closeHandler);
|
|
}
|
|
};
|
|
document.addEventListener("keydown", closeHandler);
|
|
}
|
|
|
|
// Generate Code 128 B SVG barcode
|
|
function generateBarcodeSVG(text) {
|
|
// Code 128 B patterns (value 0-106)
|
|
var CODE128B = [
|
|
"11011001100", "11001101100", "11001100110", "10010011000", "10010001100",
|
|
"10001001100", "10011001000", "10011000100", "10001100100", "11001001000",
|
|
"11001000100", "11000100100", "10110011100", "10011011100", "10011001110",
|
|
"10111001100", "10011101100", "10011100110", "11001110010", "11001011100",
|
|
"11001001110", "11011100100", "11001110100", "11101101110", "11101001100",
|
|
"11100101100", "11100100110", "11101100100", "11100110100", "11100110010",
|
|
"11011011000", "11011000110", "11000110110", "10100011000", "10001011000",
|
|
"10001000110", "10110001000", "10001101000", "10001100010", "11010001000",
|
|
"11000101000", "11000100010", "10110111000", "10110001110", "10001101110",
|
|
"10111011000", "10111000110", "10001110110", "11101110110", "11010001110",
|
|
"11000101110", "11011101000", "11011100010", "11011101110", "11101011000",
|
|
"11101000110", "11100010110", "11101101000", "11101100010", "11100011010",
|
|
"11101111010", "11001000010", "11110001010", "10100110000", "10100001100",
|
|
"10010110000", "10010000110", "10000101100", "10000100110", "10110010000",
|
|
"10110000100", "10011010000", "10011000010", "10000110100", "10000110010",
|
|
"11000010010", "11001010000", "11110111010", "11000010100", "10001111010",
|
|
"10100111100", "10010111100", "10010011110", "10111100100", "10011110100",
|
|
"10011110010", "11110100100", "11110010100", "11110010010", "11011011110",
|
|
"11011110110", "11110110110", "10101111000", "10100011110", "10001011110",
|
|
"10111101000", "10111100010", "11110101000", "11110100010", "10111011110",
|
|
"10111101110", "11101011110", "11110101110", "11010000100", "11010010000",
|
|
"11010011100", "1100011101011"
|
|
];
|
|
|
|
var START_B = 104;
|
|
var STOP = 106;
|
|
|
|
// Calculate checksum
|
|
var checksum = START_B;
|
|
for (var i = 0; i < text.length; i++) {
|
|
var charCode = text.charCodeAt(i) - 32;
|
|
if (charCode < 0 || charCode > 95) charCode = 0;
|
|
checksum += charCode * (i + 1);
|
|
}
|
|
checksum = checksum % 103;
|
|
|
|
// Build pattern
|
|
var pattern = CODE128B[START_B];
|
|
for (var i = 0; i < text.length; i++) {
|
|
var charCode = text.charCodeAt(i) - 32;
|
|
if (charCode < 0 || charCode > 95) charCode = 0;
|
|
pattern += CODE128B[charCode];
|
|
}
|
|
pattern += CODE128B[checksum];
|
|
pattern += CODE128B[STOP];
|
|
|
|
// Generate SVG
|
|
var barWidth = 3;
|
|
var height = 120;
|
|
var quietZone = 30;
|
|
var width = (pattern.length * barWidth) + (quietZone * 2);
|
|
|
|
var svg = \'<svg class="barcode-zoom-svg" width="\' + width + \'" height="\' + height + \'" xmlns="http://www.w3.org/2000/svg">\';
|
|
svg += \'<rect width="100%" height="100%" fill="white"/>\';
|
|
|
|
var x = quietZone;
|
|
for (var i = 0; i < pattern.length; i++) {
|
|
if (pattern[i] === "1") {
|
|
svg += \'<rect x="\' + x + \'" y="10" width="\' + barWidth + \'" height="\' + (height - 20) + \'" fill="black"/>\';
|
|
}
|
|
x += barWidth;
|
|
}
|
|
|
|
svg += \'</svg>\';
|
|
return svg;
|
|
}
|
|
|
|
// Initialize when DOM is ready
|
|
if (document.readyState === "loading") {
|
|
document.addEventListener("DOMContentLoaded", initBarcodeZoom);
|
|
} else {
|
|
initBarcodeZoom();
|
|
}
|
|
})();
|
|
</script>
|
|
';
|
|
|
|
return $html;
|
|
}
|
|
}
|