von Punkt6 » Di 30. Jun 2026, 19:08
Hallo,
ich habe jetzt noch die Admin Artikeverwaltung mit den Farben erweitert.
Damit man auch bei der Artikelanlage/Verwaltung sieht, ob eine Farbe und welche Farbe zugewiesen wurde.
Code: Alles auswählen
<?php
/**
* OrderSprinter – Erweiterung "Artikelfarben"
* ------------------------------------------------------------
* Idempotenter Patcher. Prueft fuer JEDE Einzelaenderung, ob sie bereits
* (ganz oder teilweise) vorhanden ist, und ergaenzt nur Fehlendes.
* Mehrfaches Ausfuehren ist gefahrlos.
*
* ABLEGEN im webapp-Rootverzeichnis (dort wo waiter.html / waiterdesktop.php liegen).
*
* AUFRUF
* Vorschau (nichts wird geaendert):
* php patch_article_colors.php
* Anwenden:
* php patch_article_colors.php --apply
* Im Browser: patch_article_colors.php (Vorschau)
* patch_article_colors.php?apply=1 (Anwenden)
*
* Vor jeder ersten Aenderung einer Datei wird eine Sicherung <datei>.colorbak angelegt.
*/
error_reporting(E_ALL);
$APPLY = (PHP_SAPI === 'cli' && in_array('--apply', $argv, true)) || isset($_GET['apply']);
$isCli = (PHP_SAPI === 'cli');
if (!$isCli) { header('Content-Type: text/plain; charset=utf-8'); }
$BASE = __DIR__;
function out($s) { echo $s . "\n"; }
out("OrderSprinter – Patcher 'Artikelfarben' [" . ($APPLY ? "ANWENDEN" : "VORSCHAU") . "]");
out(str_repeat('=', 64));
$counters = ['already' => 0, 'applied' => 0, 'todo' => 0, 'missing' => 0];
/* ----------------------------------------------------------------
* Schritt 1: Datenbankspalte products.color
* ---------------------------------------------------------------- */
out("\n[DB] Spalte `color` in der products-Tabelle");
$cfg = $BASE . '/php/config.php';
if (!is_file($cfg)) {
out(" ! php/config.php nicht gefunden – DB-Schritt uebersprungen.");
} else {
require_once $cfg;
$pdo = null;
$port = defined('MYSQL_PORT') ? MYSQL_PORT : 3306;
// Verbindung versuchen; bei 'localhost' (Socket) zusaetzlich TCP-Fallback 127.0.0.1,
// weil CLI- und Webserver-PHP oft unterschiedliche Sockets nutzen.
$hosts = [MYSQL_HOST];
if (strtolower(MYSQL_HOST) === 'localhost') { $hosts[] = '127.0.0.1'; }
$connErr = '';
foreach ($hosts as $h) {
try {
$dsn = 'mysql:host=' . $h . ';port=' . $port . ';dbname=' . MYSQL_DB;
$pdo = new PDO($dsn, MYSQL_USER, MYSQL_PASSWORD);
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
break;
} catch (Throwable $e) { $connErr = $e->getMessage(); $pdo = null; }
}
try {
if ($pdo === null) { throw new RuntimeException($connErr); }
$table = (defined('TAB_PREFIX') ? TAB_PREFIX : '') . 'products';
$has = $pdo->query("SHOW COLUMNS FROM `$table` LIKE 'color'")->fetch();
if ($has) {
out(" = Spalte `color` in `$table` bereits vorhanden.");
$counters['already']++;
} elseif ($APPLY) {
$pdo->exec("ALTER TABLE `$table` ADD `color` VARCHAR(7) NULL");
out(" + Spalte `color` in `$table` hinzugefuegt.");
$counters['applied']++;
} else {
out(" ~ wuerde hinzugefuegt: ALTER TABLE `$table` ADD `color` VARCHAR(7) NULL");
$counters['todo']++;
}
} catch (Throwable $e) {
out(" ! DB nicht erreichbar – Schritt uebersprungen (" . $e->getMessage() . ").");
out(" Manuell (phpMyAdmin): ALTER TABLE " . (defined('TAB_PREFIX') ? TAB_PREFIX : '<prefix>') . "products ADD color VARCHAR(7) NULL;");
out(" Tipp: Patcher im BROWSER aufrufen (?apply=1) – dann nutzt er den MySQL-Zugang des Webservers.");
}
}
/* ----------------------------------------------------------------
* Schritt 2: Quelltext-Aenderungen
* Jede Aenderung: present=Marker (schon erledigt?), search=Anker, replace=Ersatz.
* Alle Ersetzungen sind einzeilig -> keine Einrueckungsprobleme.
* ---------------------------------------------------------------- */
$edits = [];
// --- php/dbutils.php : $prodCols (Editor-Feld + Persistenz) ---
$edits[] = ['file' => 'php/dbutils.php', 'label' => '$prodCols: Farb-Definition',
'present' => <<<'T'
"property" => "color"
T,
'search' => <<<'T'
array("col" => 'longname',
T,
'replace' => <<<'T'
array("col" => 'color', "hist" => 0, "property" => "color", "default" => null, "type" => "color", "menu" => "Farbe", "guiname" => ["Farbe","Color","Color"]),
array("col" => 'longname',
T];
// --- php/dbutils.php : Farbe NICHT historisieren (hist=0) ---
// Verhindert "Unknown column 'color'" beim Schreiben in die Historie-Tabelle histprod.
// Wirkt auch als Upgrade fuer bereits mit hist=1 gepatchte Installationen.
$edits[] = ['file' => 'php/dbutils.php', 'label' => '$prodCols: color aus Historie ausnehmen (hist=0)',
'present' => <<<'T'
"col" => 'color', "hist" => 0,
T,
'search' => <<<'T'
"col" => 'color', "hist" => 1,
T,
'replace' => <<<'T'
"col" => 'color', "hist" => 0,
T];
// --- php/products.php : $proddefs (Wert beim Bearbeiten laden) ---
$edits[] = ['file' => 'php/products.php', 'label' => '$proddefs: color "get"-Spalte',
'present' => <<<'T'
"id" => "color"
T,
'search' => <<<'T'
array("id" => "longname", "get" => "longname",
T,
'replace' => <<<'T'
array("id" => "color", "get" => "COALESCE(color,'') as color","histid" => "color","histget" => "color","histexportname" => "Farbe","isnumber" => "0"),
array("id" => "longname", "get" => "longname",
T];
// --- php/products.php : Bestelllisten-SELECT (Zweig 1) ---
$edits[] = ['file' => 'php/products.php', 'label' => 'Bestellliste SELECT (Standard)',
'present' => <<<'T'
COALESCE(color,'') as color,COALESCE(prodimageid,0) as prodimageid,sorting
T,
'search' => <<<'T'
COALESCE(prodimageid,0) as prodimageid,sorting
T,
'replace' => <<<'T'
COALESCE(color,'') as color,COALESCE(prodimageid,0) as prodimageid,sorting
T];
// --- php/products.php : Bestelllisten-SELECT (Zweig 2, meistverkaufte) ---
$edits[] = ['file' => 'php/products.php', 'label' => 'Bestellliste SELECT (Favoriten/UNION)',
'present' => <<<'T'
COALESCE(P.color,'') as color,COALESCE(P.prodimageid,0) as prodimageid,P.sorting as sorting
T,
'search' => <<<'T'
COALESCE(P.prodimageid,0) as prodimageid,P.sorting as sorting
T,
'replace' => <<<'T'
COALESCE(P.color,'') as color,COALESCE(P.prodimageid,0) as prodimageid,P.sorting as sorting
T];
// --- php/products.php : color ins JSON ---
$edits[] = ['file' => 'php/products.php', 'label' => 'JSON: color ans Frontend',
'present' => <<<'T'
"color" => $row['color']
T,
'search' => <<<'T'
"prodimageid" => $row['prodimageid'],
T,
'replace' => <<<'T'
"color" => $row['color'],"prodimageid" => $row['prodimageid'],
T];
// --- elements/productgui.js : Colorpicker im Editor (Render-Switch) ---
// Robust ggue. jQuery Mobile: alle Controls mit data-role="none" (kein Enhancement),
// IMMER sichtbare Swatch-Palette + Vorschaufeld + nativer Picker (umrandet) + Hex-Feld
// (= gespeichertes Feld, leer = keine Farbe) + Clear. Ohne Fremdbibliothek.
// mode 'block': ersetzt einen evtl. vorhandenen (aelteren) case "color" KOMPLETT,
// sonst Einfuegen vor case "yesno". So heilt es alte Patch-Staende selbst.
$edits[] = ['file' => 'elements/productgui.js', 'label' => 'Editor: Colorpicker',
'mode' => 'block',
'present' => <<<'T'
pv_'+idOfField
T,
'blockstart' => <<<'T'
case "color":
T,
'lookahead' => <<<'T'
case "yesno":
T,
'replace' => <<<'T'
case "color": { var cv = (value && value !== 'null') ? value : ''; var pal=['#e74c3c','#e67e22','#f1c40f','#2ecc71','#1abc9c','#3498db','#9b59b6','#34495e','#e84393','#a29bfe','#00cec9','#fab1a0','#ffeaa7','#55efc4','#000000','#7f8c8d','#bdc3c7','#ffffff']; var sw=''; pal.forEach(function(col){ sw += '<span title="'+col+'" onclick="document.getElementById(\''+idOfField+'\').value=\''+col+'\';var p=document.getElementById(\'cp_'+idOfField+'\');if(p){p.value=\''+col+'\';}var v=document.getElementById(\'pv_'+idOfField+'\');if(v){v.style.background=\''+col+'\';}" style="display:inline-block;width:24px;height:24px;border:1px solid #555;background:'+col+';cursor:pointer;margin:2px;"></span>'; }); txt += '<p><div data-role="fieldcontain"><label for="'+idOfField+'">'+guiname+'</label><div style="margin:4px 0;"><input type="color" data-role="none" id="cp_'+idOfField+'" value="'+(cv||'#ffffff')+'" onchange="document.getElementById(\''+idOfField+'\').value=this.value;var v=document.getElementById(\'pv_'+idOfField+'\');if(v){v.style.background=this.value;}" style="width:60px;height:34px;border:1px solid #999;padding:0;cursor:pointer;vertical-align:middle;" /> <input type="text" data-role="none" name="'+idOfField+'" id="'+idOfField+'" value="'+cv+'" placeholder="#rrggbb" oninput="var v=document.getElementById(\'pv_'+idOfField+'\');if(v){v.style.background=this.value||\'transparent\';}" style="width:90px;height:30px;border:1px solid #999;background:white;color:black;vertical-align:middle;" /> <span id="pv_'+idOfField+'" style="display:inline-block;width:30px;height:30px;border:1px solid #999;vertical-align:middle;background:'+(cv||'transparent')+';"></span> <button type="button" data-role="none" onclick="document.getElementById(\''+idOfField+'\').value=\'\';var v=document.getElementById(\'pv_'+idOfField+'\');if(v){v.style.background=\'transparent\';}" style="height:32px;cursor:pointer;">✕</button><br>'+sw+'</div></div></p>'; break; }
T];
// --- elements/productgui.js : jQuery-Mobile-Fix ---
// jQM (1.4) "enhanced" <input type="color"> und versteckt den nativen Picker.
// data-role="none" laesst das Element als natives Control stehen.
// Wirkt auch als Upgrade auf bereits gepatchte Installationen.
$edits[] = ['file' => 'elements/productgui.js', 'label' => 'Editor: Colorpicker jQM-Fix (data-role=none)',
'present' => <<<'T'
<input type="color" data-role="none"
T,
'search' => <<<'T'
<input type="color"
T,
'replace' => <<<'T'
<input type="color" data-role="none"
T];
// --- elements/productgui.js : Admin-Artikelliste – color-Parameter in Signatur ---
$edits[] = ['file' => 'elements/productgui.js', 'label' => 'Admin-Liste: color-Parameter',
'present' => <<<'T'
createOneEntryOfProdInCat(divIdPrefix,prodid,longname,isNewProdEntry,isAvailable,color)
T,
'search' => <<<'T'
createOneEntryOfProdInCat(divIdPrefix,prodid,longname,isNewProdEntry,isAvailable) {
T,
'replace' => <<<'T'
createOneEntryOfProdInCat(divIdPrefix,prodid,longname,isNewProdEntry,isAvailable,color) {
T];
// --- elements/productgui.js : Admin-Artikelliste – Farb-Punkt im Listenkopf ---
// Zeigt einen kleinen farbigen Kreis links vom Artikelnamen, sofern eine Farbe gesetzt ist.
$edits[] = ['file' => 'elements/productgui.js', 'label' => 'Admin-Liste: Farb-Punkt im Header',
'present' => <<<'T'
+(color?'<span style="display:inline-block;width:14px;
T,
'search' => <<<'T'
+'<h3><span id=prodheader_' + prodid + ' class="' + isAvailableClass + '">' + toHtml(longname) + '</span>';
T,
'replace' => <<<'T'
+'<h3>'+(color?'<span style="display:inline-block;width:14px;height:14px;border-radius:3px;border:1px solid #555;background:'+color+';vertical-align:middle;margin-right:5px;flex-shrink:0;"></span>':'')+'<span id=prodheader_' + prodid + ' class="' + isAvailableClass + '">' + toHtml(longname) + '</span>';
T];
// --- elements/productgui.js : Admin-Artikelliste – color an Zeilen-Builder uebergeben ---
$edits[] = ['file' => 'elements/productgui.js', 'label' => 'Admin-Liste: color durchreichen (Aufruf)',
'present' => <<<'T'
aProd.color||''
T,
'search' => <<<'T'
txt += this.createOneEntryOfProdInCat("cont_",aProd.id,aProd.longname,false,aProd.available);
T,
'replace' => <<<'T'
txt += this.createOneEntryOfProdInCat("cont_",aProd.id,aProd.longname,false,aProd.available,aProd.color||'');
T];
// --- waiterdesktop.php : Produktbutton ohne Bild ---
$edits[] = ['file' => 'waiterdesktop.php', 'label' => 'Desktop-Button (ohne Bild)',
'present' => <<<'T'
var cscol=f.color
T,
'search' => <<<'T'
if(0==preferimgdesk)b+='<div id="prod_'+f.id+'" class="productitem '+a+'">'+f.name+e;
T,
'replace' => <<<'T'
if(0==preferimgdesk){var cscol=f.color?' style="background:'+f.color+' !important;"':'';b+='<div id="prod_'+f.id+'" class="productitem '+a+'"'+cscol+'>'+f.name+e;}
T];
// --- waiterdesktop.php : Produktbutton mit Bild (Farbe als Rahmen) ---
$edits[] = ['file' => 'waiterdesktop.php', 'label' => 'Desktop-Button (mit Bild, Rahmen)',
'present' => <<<'T'
border:4px solid '+f.color
T,
'search' => <<<'T'
createImageStyle("php/contenthandler.php?module=products&command=getprodimage&prodid="+f.id+"&size=m")
T,
'replace' => <<<'T'
(f.color?createImageStyle("php/contenthandler.php?module=products&command=getprodimage&prodid="+f.id+"&size=m").replace('style="','style="border:4px solid '+f.color+'; '):createImageStyle("php/contenthandler.php?module=products&command=getprodimage&prodid="+f.id+"&size=m"))
T];
// --- waiterdesktop.php : color durchreichen (getAllProds baut Objekte mit Whitelist neu) ---
// Ohne dies ist f.color im Desktop-Button undefined -> keine Farbe. Deckt alle 3 Objekt-Stellen.
$edits[] = ['file' => 'waiterdesktop.php', 'label' => 'Desktop: color durchreichen (getAllProds)',
'present' => <<<'T'
amount:f.amount,price:f.price,color:f.color}
T,
'search' => <<<'T'
amount:f.amount,price:f.price}
T,
'replace' => <<<'T'
amount:f.amount,price:f.price,color:f.color}
T];
// --- waiter.html : zentraler Produkt-Renderer ---
$edits[] = ['file' => 'waiter.html', 'label' => 'Handheld _renderProdEntry',
'present' => <<<'T'
var cscol=b.color
T,
'search' => <<<'T'
c='<li data-theme="c" data-icon="false"><a href="#" id="'+e+b.id+'" class="'+g+c+'">';
T,
'replace' => <<<'T'
var cscol=b.color?' style="background:'+b.color+' !important;"':'';c='<li data-theme="c" data-icon="false"><a href="#" id="'+e+b.id+'" class="'+g+c+'"'+cscol+'>';
T];
// --- waiter.html : color in der Produktobjekt-Kopie behalten ---
$edits[] = ['file' => 'waiter.html', 'label' => 'Handheld: color durchreichen (Liste)',
'present' => <<<'T'
,phase:f.phase,amount:f.amount,color:f.color}
T,
'search' => <<<'T'
,phase:f.phase,amount:f.amount}
T,
'replace' => <<<'T'
,phase:f.phase,amount:f.amount,color:f.color}
T];
// --- waiter.html : color in der Favoriten-Kopie behalten ---
$edits[] = ['file' => 'waiter.html', 'label' => 'Handheld: color durchreichen (Favoriten)',
'present' => <<<'T'
,amount:g.amount,color:g.color}
T,
'search' => <<<'T'
{id:g.id,longname:g.longname,unit:g.unit,amount:g.amount}
T,
'replace' => <<<'T'
{id:g.id,longname:g.longname,unit:g.unit,amount:g.amount,color:g.color}
T];
/* ----------------------------------------------------------------
* Verarbeitung pro Datei
* ---------------------------------------------------------------- */
$byFile = [];
foreach ($edits as $e) { $byFile[$e['file']][] = $e; }
foreach ($byFile as $relFile => $fileEdits) {
out("\n[" . $relFile . "]");
$path = $BASE . '/' . $relFile;
if (!is_file($path)) {
out(" ! Datei nicht gefunden – uebersprungen.");
foreach ($fileEdits as $e) { $counters['missing']++; }
continue;
}
$content = file_get_contents($path);
$orig = $content;
foreach ($fileEdits as $e) {
// Block-Modus: vorhandenen Code-Block (z.B. alter case "color") komplett ersetzen,
// sonst vor 'lookahead' einfuegen. Heilt aeltere Patch-Staende selbst.
if (isset($e['mode']) && $e['mode'] === 'block') {
if (strpos($content, $e['present']) !== false) {
out(" = {$e['label']}: bereits aktuell.");
$counters['already']++;
continue;
}
$insert = $e['replace'] . "\n ";
$pat = '/' . preg_quote($e['blockstart'], '/') . '.*?(?=' . preg_quote($e['lookahead'], '/') . ')/s';
if (preg_match($pat, $content)) {
if ($APPLY) {
$content = preg_replace_callback($pat, function () use ($insert) { return $insert; }, $content, 1);
out(" + {$e['label']}: aeltere Variante ersetzt (Upgrade).");
$counters['applied']++;
} else {
out(" ~ {$e['label']}: aeltere Variante wuerde ersetzt.");
$counters['todo']++;
}
} elseif (($pos = strpos($content, $e['lookahead'])) !== false) {
if ($APPLY) {
$content = substr($content, 0, $pos) . $insert . substr($content, $pos);
out(" + {$e['label']}: eingefuegt.");
$counters['applied']++;
} else {
out(" ~ {$e['label']}: wuerde eingefuegt.");
$counters['todo']++;
}
} else {
out(" ! {$e['label']}: Anker NICHT gefunden (andere Version?) – manuell pruefen.");
$counters['missing']++;
}
continue;
}
if (strpos($content, $e['present']) !== false) {
out(" = {$e['label']}: bereits vorhanden.");
$counters['already']++;
} elseif (isset($e['conflict']) && strpos($content, $e['conflict']) !== false) {
out(" ! {$e['label']}: AELTERE Variante erkannt. Bitte '$relFile' aus der Sicherung");
out(" '$relFile.colorbak' wiederherstellen und Patcher erneut ausfuehren.");
$counters['missing']++;
} elseif (strpos($content, $e['search']) !== false) {
if ($APPLY) {
$content = str_replace($e['search'], $e['replace'], $content);
out(" + {$e['label']}: ergaenzt.");
$counters['applied']++;
} else {
out(" ~ {$e['label']}: wuerde ergaenzt.");
$counters['todo']++;
}
} else {
out(" ! {$e['label']}: Anker NICHT gefunden (andere Version?) – manuell pruefen.");
$counters['missing']++;
}
}
if ($APPLY && $content !== $orig) {
$bak = $path . '.colorbak';
if (!is_file($bak)) { copy($path, $bak); out(" (Sicherung: " . basename($bak) . ")"); }
file_put_contents($path, $content);
}
}
/* ----------------------------------------------------------------
* Zusammenfassung
* ---------------------------------------------------------------- */
out("\n" . str_repeat('=', 64));
out(sprintf("Bereits vorhanden: %d Ergaenzt: %d Offen: %d Anker fehlt: %d",
$counters['already'], $counters['applied'], $counters['todo'], $counters['missing']));
if (!$APPLY && ($counters['todo'] > 0)) {
out("Vorschau-Modus. Zum Anwenden: php " . basename(__FILE__) . " --apply (bzw. ?apply=1)");
}
if ($counters['missing'] > 0) {
out("Hinweis: 'Anker fehlt' = diese Stelle weicht von der erwarteten Version ab; bitte manuell ergaenzen.");
}
if ($APPLY && $counters['applied'] > 0) {
out("Fertig. Bitte den Browser-Cache leeren (JS/HTML werden gecacht).");
}
Gruß Thomas
- Dateianhänge
-

- Screenshot 2026-06-30 190630.png (18.86 KiB) 729 mal betrachtet
Hallo,
ich habe jetzt noch die Admin Artikeverwaltung mit den Farben erweitert.
Damit man auch bei der Artikelanlage/Verwaltung sieht, ob eine Farbe und welche Farbe zugewiesen wurde.
[code]<?php
/**
* OrderSprinter – Erweiterung "Artikelfarben"
* ------------------------------------------------------------
* Idempotenter Patcher. Prueft fuer JEDE Einzelaenderung, ob sie bereits
* (ganz oder teilweise) vorhanden ist, und ergaenzt nur Fehlendes.
* Mehrfaches Ausfuehren ist gefahrlos.
*
* ABLEGEN im webapp-Rootverzeichnis (dort wo waiter.html / waiterdesktop.php liegen).
*
* AUFRUF
* Vorschau (nichts wird geaendert):
* php patch_article_colors.php
* Anwenden:
* php patch_article_colors.php --apply
* Im Browser: patch_article_colors.php (Vorschau)
* patch_article_colors.php?apply=1 (Anwenden)
*
* Vor jeder ersten Aenderung einer Datei wird eine Sicherung <datei>.colorbak angelegt.
*/
error_reporting(E_ALL);
$APPLY = (PHP_SAPI === 'cli' && in_array('--apply', $argv, true)) || isset($_GET['apply']);
$isCli = (PHP_SAPI === 'cli');
if (!$isCli) { header('Content-Type: text/plain; charset=utf-8'); }
$BASE = __DIR__;
function out($s) { echo $s . "\n"; }
out("OrderSprinter – Patcher 'Artikelfarben' [" . ($APPLY ? "ANWENDEN" : "VORSCHAU") . "]");
out(str_repeat('=', 64));
$counters = ['already' => 0, 'applied' => 0, 'todo' => 0, 'missing' => 0];
/* ----------------------------------------------------------------
* Schritt 1: Datenbankspalte products.color
* ---------------------------------------------------------------- */
out("\n[DB] Spalte `color` in der products-Tabelle");
$cfg = $BASE . '/php/config.php';
if (!is_file($cfg)) {
out(" ! php/config.php nicht gefunden – DB-Schritt uebersprungen.");
} else {
require_once $cfg;
$pdo = null;
$port = defined('MYSQL_PORT') ? MYSQL_PORT : 3306;
// Verbindung versuchen; bei 'localhost' (Socket) zusaetzlich TCP-Fallback 127.0.0.1,
// weil CLI- und Webserver-PHP oft unterschiedliche Sockets nutzen.
$hosts = [MYSQL_HOST];
if (strtolower(MYSQL_HOST) === 'localhost') { $hosts[] = '127.0.0.1'; }
$connErr = '';
foreach ($hosts as $h) {
try {
$dsn = 'mysql:host=' . $h . ';port=' . $port . ';dbname=' . MYSQL_DB;
$pdo = new PDO($dsn, MYSQL_USER, MYSQL_PASSWORD);
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
break;
} catch (Throwable $e) { $connErr = $e->getMessage(); $pdo = null; }
}
try {
if ($pdo === null) { throw new RuntimeException($connErr); }
$table = (defined('TAB_PREFIX') ? TAB_PREFIX : '') . 'products';
$has = $pdo->query("SHOW COLUMNS FROM `$table` LIKE 'color'")->fetch();
if ($has) {
out(" = Spalte `color` in `$table` bereits vorhanden.");
$counters['already']++;
} elseif ($APPLY) {
$pdo->exec("ALTER TABLE `$table` ADD `color` VARCHAR(7) NULL");
out(" + Spalte `color` in `$table` hinzugefuegt.");
$counters['applied']++;
} else {
out(" ~ wuerde hinzugefuegt: ALTER TABLE `$table` ADD `color` VARCHAR(7) NULL");
$counters['todo']++;
}
} catch (Throwable $e) {
out(" ! DB nicht erreichbar – Schritt uebersprungen (" . $e->getMessage() . ").");
out(" Manuell (phpMyAdmin): ALTER TABLE " . (defined('TAB_PREFIX') ? TAB_PREFIX : '<prefix>') . "products ADD color VARCHAR(7) NULL;");
out(" Tipp: Patcher im BROWSER aufrufen (?apply=1) – dann nutzt er den MySQL-Zugang des Webservers.");
}
}
/* ----------------------------------------------------------------
* Schritt 2: Quelltext-Aenderungen
* Jede Aenderung: present=Marker (schon erledigt?), search=Anker, replace=Ersatz.
* Alle Ersetzungen sind einzeilig -> keine Einrueckungsprobleme.
* ---------------------------------------------------------------- */
$edits = [];
// --- php/dbutils.php : $prodCols (Editor-Feld + Persistenz) ---
$edits[] = ['file' => 'php/dbutils.php', 'label' => '$prodCols: Farb-Definition',
'present' => <<<'T'
"property" => "color"
T,
'search' => <<<'T'
array("col" => 'longname',
T,
'replace' => <<<'T'
array("col" => 'color', "hist" => 0, "property" => "color", "default" => null, "type" => "color", "menu" => "Farbe", "guiname" => ["Farbe","Color","Color"]),
array("col" => 'longname',
T];
// --- php/dbutils.php : Farbe NICHT historisieren (hist=0) ---
// Verhindert "Unknown column 'color'" beim Schreiben in die Historie-Tabelle histprod.
// Wirkt auch als Upgrade fuer bereits mit hist=1 gepatchte Installationen.
$edits[] = ['file' => 'php/dbutils.php', 'label' => '$prodCols: color aus Historie ausnehmen (hist=0)',
'present' => <<<'T'
"col" => 'color', "hist" => 0,
T,
'search' => <<<'T'
"col" => 'color', "hist" => 1,
T,
'replace' => <<<'T'
"col" => 'color', "hist" => 0,
T];
// --- php/products.php : $proddefs (Wert beim Bearbeiten laden) ---
$edits[] = ['file' => 'php/products.php', 'label' => '$proddefs: color "get"-Spalte',
'present' => <<<'T'
"id" => "color"
T,
'search' => <<<'T'
array("id" => "longname", "get" => "longname",
T,
'replace' => <<<'T'
array("id" => "color", "get" => "COALESCE(color,'') as color","histid" => "color","histget" => "color","histexportname" => "Farbe","isnumber" => "0"),
array("id" => "longname", "get" => "longname",
T];
// --- php/products.php : Bestelllisten-SELECT (Zweig 1) ---
$edits[] = ['file' => 'php/products.php', 'label' => 'Bestellliste SELECT (Standard)',
'present' => <<<'T'
COALESCE(color,'') as color,COALESCE(prodimageid,0) as prodimageid,sorting
T,
'search' => <<<'T'
COALESCE(prodimageid,0) as prodimageid,sorting
T,
'replace' => <<<'T'
COALESCE(color,'') as color,COALESCE(prodimageid,0) as prodimageid,sorting
T];
// --- php/products.php : Bestelllisten-SELECT (Zweig 2, meistverkaufte) ---
$edits[] = ['file' => 'php/products.php', 'label' => 'Bestellliste SELECT (Favoriten/UNION)',
'present' => <<<'T'
COALESCE(P.color,'') as color,COALESCE(P.prodimageid,0) as prodimageid,P.sorting as sorting
T,
'search' => <<<'T'
COALESCE(P.prodimageid,0) as prodimageid,P.sorting as sorting
T,
'replace' => <<<'T'
COALESCE(P.color,'') as color,COALESCE(P.prodimageid,0) as prodimageid,P.sorting as sorting
T];
// --- php/products.php : color ins JSON ---
$edits[] = ['file' => 'php/products.php', 'label' => 'JSON: color ans Frontend',
'present' => <<<'T'
"color" => $row['color']
T,
'search' => <<<'T'
"prodimageid" => $row['prodimageid'],
T,
'replace' => <<<'T'
"color" => $row['color'],"prodimageid" => $row['prodimageid'],
T];
// --- elements/productgui.js : Colorpicker im Editor (Render-Switch) ---
// Robust ggue. jQuery Mobile: alle Controls mit data-role="none" (kein Enhancement),
// IMMER sichtbare Swatch-Palette + Vorschaufeld + nativer Picker (umrandet) + Hex-Feld
// (= gespeichertes Feld, leer = keine Farbe) + Clear. Ohne Fremdbibliothek.
// mode 'block': ersetzt einen evtl. vorhandenen (aelteren) case "color" KOMPLETT,
// sonst Einfuegen vor case "yesno". So heilt es alte Patch-Staende selbst.
$edits[] = ['file' => 'elements/productgui.js', 'label' => 'Editor: Colorpicker',
'mode' => 'block',
'present' => <<<'T'
pv_'+idOfField
T,
'blockstart' => <<<'T'
case "color":
T,
'lookahead' => <<<'T'
case "yesno":
T,
'replace' => <<<'T'
case "color": { var cv = (value && value !== 'null') ? value : ''; var pal=['#e74c3c','#e67e22','#f1c40f','#2ecc71','#1abc9c','#3498db','#9b59b6','#34495e','#e84393','#a29bfe','#00cec9','#fab1a0','#ffeaa7','#55efc4','#000000','#7f8c8d','#bdc3c7','#ffffff']; var sw=''; pal.forEach(function(col){ sw += '<span title="'+col+'" onclick="document.getElementById(\''+idOfField+'\').value=\''+col+'\';var p=document.getElementById(\'cp_'+idOfField+'\');if(p){p.value=\''+col+'\';}var v=document.getElementById(\'pv_'+idOfField+'\');if(v){v.style.background=\''+col+'\';}" style="display:inline-block;width:24px;height:24px;border:1px solid #555;background:'+col+';cursor:pointer;margin:2px;"></span>'; }); txt += '<p><div data-role="fieldcontain"><label for="'+idOfField+'">'+guiname+'</label><div style="margin:4px 0;"><input type="color" data-role="none" id="cp_'+idOfField+'" value="'+(cv||'#ffffff')+'" onchange="document.getElementById(\''+idOfField+'\').value=this.value;var v=document.getElementById(\'pv_'+idOfField+'\');if(v){v.style.background=this.value;}" style="width:60px;height:34px;border:1px solid #999;padding:0;cursor:pointer;vertical-align:middle;" /> <input type="text" data-role="none" name="'+idOfField+'" id="'+idOfField+'" value="'+cv+'" placeholder="#rrggbb" oninput="var v=document.getElementById(\'pv_'+idOfField+'\');if(v){v.style.background=this.value||\'transparent\';}" style="width:90px;height:30px;border:1px solid #999;background:white;color:black;vertical-align:middle;" /> <span id="pv_'+idOfField+'" style="display:inline-block;width:30px;height:30px;border:1px solid #999;vertical-align:middle;background:'+(cv||'transparent')+';"></span> <button type="button" data-role="none" onclick="document.getElementById(\''+idOfField+'\').value=\'\';var v=document.getElementById(\'pv_'+idOfField+'\');if(v){v.style.background=\'transparent\';}" style="height:32px;cursor:pointer;">✕</button><br>'+sw+'</div></div></p>'; break; }
T];
// --- elements/productgui.js : jQuery-Mobile-Fix ---
// jQM (1.4) "enhanced" <input type="color"> und versteckt den nativen Picker.
// data-role="none" laesst das Element als natives Control stehen.
// Wirkt auch als Upgrade auf bereits gepatchte Installationen.
$edits[] = ['file' => 'elements/productgui.js', 'label' => 'Editor: Colorpicker jQM-Fix (data-role=none)',
'present' => <<<'T'
<input type="color" data-role="none"
T,
'search' => <<<'T'
<input type="color"
T,
'replace' => <<<'T'
<input type="color" data-role="none"
T];
// --- elements/productgui.js : Admin-Artikelliste – color-Parameter in Signatur ---
$edits[] = ['file' => 'elements/productgui.js', 'label' => 'Admin-Liste: color-Parameter',
'present' => <<<'T'
createOneEntryOfProdInCat(divIdPrefix,prodid,longname,isNewProdEntry,isAvailable,color)
T,
'search' => <<<'T'
createOneEntryOfProdInCat(divIdPrefix,prodid,longname,isNewProdEntry,isAvailable) {
T,
'replace' => <<<'T'
createOneEntryOfProdInCat(divIdPrefix,prodid,longname,isNewProdEntry,isAvailable,color) {
T];
// --- elements/productgui.js : Admin-Artikelliste – Farb-Punkt im Listenkopf ---
// Zeigt einen kleinen farbigen Kreis links vom Artikelnamen, sofern eine Farbe gesetzt ist.
$edits[] = ['file' => 'elements/productgui.js', 'label' => 'Admin-Liste: Farb-Punkt im Header',
'present' => <<<'T'
+(color?'<span style="display:inline-block;width:14px;
T,
'search' => <<<'T'
+'<h3><span id=prodheader_' + prodid + ' class="' + isAvailableClass + '">' + toHtml(longname) + '</span>';
T,
'replace' => <<<'T'
+'<h3>'+(color?'<span style="display:inline-block;width:14px;height:14px;border-radius:3px;border:1px solid #555;background:'+color+';vertical-align:middle;margin-right:5px;flex-shrink:0;"></span>':'')+'<span id=prodheader_' + prodid + ' class="' + isAvailableClass + '">' + toHtml(longname) + '</span>';
T];
// --- elements/productgui.js : Admin-Artikelliste – color an Zeilen-Builder uebergeben ---
$edits[] = ['file' => 'elements/productgui.js', 'label' => 'Admin-Liste: color durchreichen (Aufruf)',
'present' => <<<'T'
aProd.color||''
T,
'search' => <<<'T'
txt += this.createOneEntryOfProdInCat("cont_",aProd.id,aProd.longname,false,aProd.available);
T,
'replace' => <<<'T'
txt += this.createOneEntryOfProdInCat("cont_",aProd.id,aProd.longname,false,aProd.available,aProd.color||'');
T];
// --- waiterdesktop.php : Produktbutton ohne Bild ---
$edits[] = ['file' => 'waiterdesktop.php', 'label' => 'Desktop-Button (ohne Bild)',
'present' => <<<'T'
var cscol=f.color
T,
'search' => <<<'T'
if(0==preferimgdesk)b+='<div id="prod_'+f.id+'" class="productitem '+a+'">'+f.name+e;
T,
'replace' => <<<'T'
if(0==preferimgdesk){var cscol=f.color?' style="background:'+f.color+' !important;"':'';b+='<div id="prod_'+f.id+'" class="productitem '+a+'"'+cscol+'>'+f.name+e;}
T];
// --- waiterdesktop.php : Produktbutton mit Bild (Farbe als Rahmen) ---
$edits[] = ['file' => 'waiterdesktop.php', 'label' => 'Desktop-Button (mit Bild, Rahmen)',
'present' => <<<'T'
border:4px solid '+f.color
T,
'search' => <<<'T'
createImageStyle("php/contenthandler.php?module=products&command=getprodimage&prodid="+f.id+"&size=m")
T,
'replace' => <<<'T'
(f.color?createImageStyle("php/contenthandler.php?module=products&command=getprodimage&prodid="+f.id+"&size=m").replace('style="','style="border:4px solid '+f.color+'; '):createImageStyle("php/contenthandler.php?module=products&command=getprodimage&prodid="+f.id+"&size=m"))
T];
// --- waiterdesktop.php : color durchreichen (getAllProds baut Objekte mit Whitelist neu) ---
// Ohne dies ist f.color im Desktop-Button undefined -> keine Farbe. Deckt alle 3 Objekt-Stellen.
$edits[] = ['file' => 'waiterdesktop.php', 'label' => 'Desktop: color durchreichen (getAllProds)',
'present' => <<<'T'
amount:f.amount,price:f.price,color:f.color}
T,
'search' => <<<'T'
amount:f.amount,price:f.price}
T,
'replace' => <<<'T'
amount:f.amount,price:f.price,color:f.color}
T];
// --- waiter.html : zentraler Produkt-Renderer ---
$edits[] = ['file' => 'waiter.html', 'label' => 'Handheld _renderProdEntry',
'present' => <<<'T'
var cscol=b.color
T,
'search' => <<<'T'
c='<li data-theme="c" data-icon="false"><a href="#" id="'+e+b.id+'" class="'+g+c+'">';
T,
'replace' => <<<'T'
var cscol=b.color?' style="background:'+b.color+' !important;"':'';c='<li data-theme="c" data-icon="false"><a href="#" id="'+e+b.id+'" class="'+g+c+'"'+cscol+'>';
T];
// --- waiter.html : color in der Produktobjekt-Kopie behalten ---
$edits[] = ['file' => 'waiter.html', 'label' => 'Handheld: color durchreichen (Liste)',
'present' => <<<'T'
,phase:f.phase,amount:f.amount,color:f.color}
T,
'search' => <<<'T'
,phase:f.phase,amount:f.amount}
T,
'replace' => <<<'T'
,phase:f.phase,amount:f.amount,color:f.color}
T];
// --- waiter.html : color in der Favoriten-Kopie behalten ---
$edits[] = ['file' => 'waiter.html', 'label' => 'Handheld: color durchreichen (Favoriten)',
'present' => <<<'T'
,amount:g.amount,color:g.color}
T,
'search' => <<<'T'
{id:g.id,longname:g.longname,unit:g.unit,amount:g.amount}
T,
'replace' => <<<'T'
{id:g.id,longname:g.longname,unit:g.unit,amount:g.amount,color:g.color}
T];
/* ----------------------------------------------------------------
* Verarbeitung pro Datei
* ---------------------------------------------------------------- */
$byFile = [];
foreach ($edits as $e) { $byFile[$e['file']][] = $e; }
foreach ($byFile as $relFile => $fileEdits) {
out("\n[" . $relFile . "]");
$path = $BASE . '/' . $relFile;
if (!is_file($path)) {
out(" ! Datei nicht gefunden – uebersprungen.");
foreach ($fileEdits as $e) { $counters['missing']++; }
continue;
}
$content = file_get_contents($path);
$orig = $content;
foreach ($fileEdits as $e) {
// Block-Modus: vorhandenen Code-Block (z.B. alter case "color") komplett ersetzen,
// sonst vor 'lookahead' einfuegen. Heilt aeltere Patch-Staende selbst.
if (isset($e['mode']) && $e['mode'] === 'block') {
if (strpos($content, $e['present']) !== false) {
out(" = {$e['label']}: bereits aktuell.");
$counters['already']++;
continue;
}
$insert = $e['replace'] . "\n ";
$pat = '/' . preg_quote($e['blockstart'], '/') . '.*?(?=' . preg_quote($e['lookahead'], '/') . ')/s';
if (preg_match($pat, $content)) {
if ($APPLY) {
$content = preg_replace_callback($pat, function () use ($insert) { return $insert; }, $content, 1);
out(" + {$e['label']}: aeltere Variante ersetzt (Upgrade).");
$counters['applied']++;
} else {
out(" ~ {$e['label']}: aeltere Variante wuerde ersetzt.");
$counters['todo']++;
}
} elseif (($pos = strpos($content, $e['lookahead'])) !== false) {
if ($APPLY) {
$content = substr($content, 0, $pos) . $insert . substr($content, $pos);
out(" + {$e['label']}: eingefuegt.");
$counters['applied']++;
} else {
out(" ~ {$e['label']}: wuerde eingefuegt.");
$counters['todo']++;
}
} else {
out(" ! {$e['label']}: Anker NICHT gefunden (andere Version?) – manuell pruefen.");
$counters['missing']++;
}
continue;
}
if (strpos($content, $e['present']) !== false) {
out(" = {$e['label']}: bereits vorhanden.");
$counters['already']++;
} elseif (isset($e['conflict']) && strpos($content, $e['conflict']) !== false) {
out(" ! {$e['label']}: AELTERE Variante erkannt. Bitte '$relFile' aus der Sicherung");
out(" '$relFile.colorbak' wiederherstellen und Patcher erneut ausfuehren.");
$counters['missing']++;
} elseif (strpos($content, $e['search']) !== false) {
if ($APPLY) {
$content = str_replace($e['search'], $e['replace'], $content);
out(" + {$e['label']}: ergaenzt.");
$counters['applied']++;
} else {
out(" ~ {$e['label']}: wuerde ergaenzt.");
$counters['todo']++;
}
} else {
out(" ! {$e['label']}: Anker NICHT gefunden (andere Version?) – manuell pruefen.");
$counters['missing']++;
}
}
if ($APPLY && $content !== $orig) {
$bak = $path . '.colorbak';
if (!is_file($bak)) { copy($path, $bak); out(" (Sicherung: " . basename($bak) . ")"); }
file_put_contents($path, $content);
}
}
/* ----------------------------------------------------------------
* Zusammenfassung
* ---------------------------------------------------------------- */
out("\n" . str_repeat('=', 64));
out(sprintf("Bereits vorhanden: %d Ergaenzt: %d Offen: %d Anker fehlt: %d",
$counters['already'], $counters['applied'], $counters['todo'], $counters['missing']));
if (!$APPLY && ($counters['todo'] > 0)) {
out("Vorschau-Modus. Zum Anwenden: php " . basename(__FILE__) . " --apply (bzw. ?apply=1)");
}
if ($counters['missing'] > 0) {
out("Hinweis: 'Anker fehlt' = diese Stelle weicht von der erwarteten Version ab; bitte manuell ergaenzen.");
}
if ($APPLY && $counters['applied'] > 0) {
out("Fertig. Bitte den Browser-Cache leeren (JS/HTML werden gecacht).");
}
[/code]
Gruß Thomas