dolibarr.handybarcodescanner/class/actions_handybarcodescanner.class.php
data ad180db510 v4.6: Menü unter Produkte, bessere Barcode-Erkennung, Tab-Wechsel ohne Reload
- 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>
2026-02-17 17:54:13 +01:00

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;
}
}