Files
shop-old/tba-search.php
T

1217 lines
64 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* Krempl PostgreSQL Search Test Interface
* Search for devices and show matching ersatzteile
*/
require_once __DIR__ . '/core/postgres/KremplDB.php';
require_once __DIR__ . '/core/postgres/tba_render.php';
/**
* AJAX: Shop-Artikel eines Geraets nachladen.
*
* Muss vor jeder HTML-Ausgabe stehen. Hintergrund: vorher lud die Seite die Artikel fuer
* ALLE 20 Treffer vor, obwohl 19 davon hinter "Nicht mein Geraet" eingeklappt sind --
* das waren 40 MySQL-Queries auf Vorrat und der Grund fuer ~16 s Laufzeit. Jetzt laedt nur
* der Top-Treffer vor, die uebrigen Kacheln holen ihre Liste beim Aufklappen hier ab.
* An der Legacy-Shop-DB wird dabei nichts geaendert, sie wird nur seltener gefragt.
*/
if (isset($_GET['ajax']) && $_GET['ajax'] === 'items') {
header('Content-Type: text/html; charset=UTF-8');
header('Cache-Control: no-store');
$gid = isset($_GET['geraet_id']) ? (int)$_GET['geraet_id'] : 0;
if ($gid <= 0) {
http_response_code(400);
echo '<div class="v2-lazy-msg">Ungültige Geräte-ID.</div>';
exit;
}
try {
$db = new KremplDB(true);
$buy = $db->getAvailableItemsForGeraet($gid, 200);
if ($buy) {
echo '<h4>Verfügbare Artikel für dieses Gerät (' . count($buy) . '):</h4>';
echo v2_artikel_liste($buy, 12, 12);
} else {
// Zwei sehr unterschiedliche Faelle, die man nicht in einen Satz packen darf:
// a) der Hersteller fuehrt zu diesem Geraet gar keine Ersatzteile (z.B. Beko
// CN332100 BEKO_RUSSIA, 0 Eintraege in ersatzteil_mapping) -- da ist nichts zu holen.
// b) es gibt Ersatzteile, aber keines davon ist ein aktiver Shop-Artikel -- das
// lohnt eine Rueckfrage beim Grosshandel.
// Der Zusatz-Query laeuft nur im Leerfall.
$parts = $db->getErsatzteileForGeraet($gid, 1);
if (!$parts) {
echo '<div class="v2-lazy-msg">Zu diesem Gerät sind in den Herstellerdaten '
. 'keine Ersatzteile hinterlegt.</div>';
} else {
echo '<div class="v2-lazy-msg">Für dieses Gerät sind Ersatzteile bekannt, aber '
. 'derzeit keines davon im Shop verfügbar.</div>';
}
}
} catch (Exception $e) {
http_response_code(500);
echo '<div class="v2-lazy-msg">Artikel konnten nicht geladen werden.</div>';
}
exit;
}
$search_term = isset($_GET['q']) ? trim($_GET['q']) : '';
$results = [];
$item_results = [];
$search_time = 0;
$item_search_time = 0;
$error_message = '';
// "Nur verfuegbare Shop-Artikel" ist jetzt der IST-Zustand (Default AN): das verkleinert die
// Trefferliste auf real bestellbare Teile, und getAvailableItemsForGeraet() liefert dabei
// items.id mit -- also sind die Chips direkt in den Shop verlinkbar. Abschaltbar mit
// ?shop_items=0 (die Checkbox sendet das ueber ein vorgeschaltetes hidden-Feld).
$show_shop_items = !isset($_GET['shop_items']) || $_GET['shop_items'] !== '0';
$v2_guided = false; // gefuehrte Ansicht aktiv (ein klarer Top-Treffer)
$v2_top_detail = null; // Volldaten des Top-Geraets aus geraete
$v2_brandpick = []; // marke => anzahl, wenn erst der Hersteller geklaert werden muss
$v2_marke = isset($_GET['marke']) ? trim($_GET['marke']) : ''; // gewaehlter Hersteller
$v2_variants = []; // BSH-Bauzustaende /01 /02 ..., wenn nur der Zeitraum unterscheidet
$v2_variant_base = ''; // gemeinsame Typnummer ohne Suffix, z.B. "WT46W560BY"
$v2_shopmap = []; // navision_id => Shop-Artikel, fuer die Links an den Chips
$v2_partcounts = []; // geraet_id => Anzahl Ersatzteile (fuer die eingeklappten Kacheln)
try {
// MySQL wird in beiden Fällen gebraucht (rechte Spalte = Artikel-Suche)
$db = new KremplDB(true);
if ($search_term !== '') {
// Links: Geräte-Suche (PostgreSQL)
$start_time = microtime(true);
// Ersatzteile je Treffer nur im Debug-Modus (?shop_items=0) mitladen -- dort zeigen die
// Kacheln Ersatzteil-Chips statt des Lazy-Aufklappers. Im Normalfall kostet das 20
// ueberfluessige PG-Queries, deren Ergebnis nur der Top-Treffer braucht.
$results = $db->searchGeraete($search_term, 20, !$show_shop_items);
$search_time = (microtime(true) - $start_time) * 1000;
// Rechts: Artikel-/Ersatzteil-Suche (MySQL)
$start_time2 = microtime(true);
$item_results = $db->searchItems($search_term, 20);
$item_search_time = (microtime(true) - $start_time2) * 1000;
// --- Hersteller-Filter (kommt aus der Hersteller-Rueckfrage) ---
if ($v2_marke !== '') {
$results = array_values(array_filter($results, function ($d) use ($v2_marke) {
return isset($d['marke']) && trim($d['marke']) === $v2_marke;
}));
}
// --- Stufe 1: muss erst der Hersteller geklaert werden? ---
// Dasselbe Modell laeuft oft unter mehreren Konzern-/Versandhausmarken. Dann haben
// alle Top-Treffer denselben Score und denselben Modellnamen -- eine Bestaetigung
// waere geraten. Der Hersteller steht dem Kunden aber auf dem Typenschild zur
// Verfuegung, also fragen wir zuerst danach. Beispiel MUFF21VLQS: 10x Score 100
// ueber AEG, Electrolux, Frigidaire, Gibson.
if (count($results) > 1) {
$top_score = (int)$results[0]['score'];
$topgroup = array_filter($results, function ($d) use ($top_score) {
return (int)$d['score'] === $top_score;
});
$brands = [];
$models = [];
foreach ($topgroup as $d) {
$m = trim($d['marke'] ?? '');
if ($m !== '') { $brands[$m] = ($brands[$m] ?? 0) + 1; }
$models[mb_strtolower(trim($d['modell_bezeichnung'] ?? ''))] = true;
}
// Nur fragen, wenn es wirklich EIN Modell bei MEHREREN Marken ist.
if (count($brands) > 1 && count($models) === 1) {
arsort($brands);
$v2_brandpick = $brands;
}
// --- Stufe 1b: Varianten-Auswahl (BSH-Bauzustaende) ---
// SIEMENS/BOSCH haengen "/01", "/02" ... an die Typnummer in nr. Laesst der Kunde
// das Suffix weg ("WT46W560BY"), kommen alle Bauzustaende mit gleichem Score
// zurueck und unterscheiden sich NUR im Produktionszeitraum. Dann fragen wir
// danach, statt zehn optisch identische Zeilen zu zeigen.
if (!$v2_brandpick) {
$basis = [];
foreach ($topgroup as $d) {
$n = trim($d['nr'] ?? '');
$basis[($p = strpos($n, '/')) !== false ? substr($n, 0, $p) : $n] = true;
}
if (count($basis) === 1 && count($topgroup) > 1 && count($brands) <= 1) {
$v2_variant_base = (string)array_key_first($basis);
// Nur wenn die Treffer wirklich Suffix-Varianten sind, nicht zufaellig
// gleichnamige Geraete: mindestens einer muss ein "/" tragen.
$has_suffix = false;
foreach ($topgroup as $d) { if (strpos((string)($d['nr'] ?? ''), '/') !== false) { $has_suffix = true; break; } }
if ($has_suffix) {
$det_batch = $db->getGeraetDetailsBatch(array_column($topgroup, 'geraet_id'));
foreach ($topgroup as $d) {
$det = $det_batch[$d['geraet_id']] ?? [];
$v2_variants[] = [
'nr' => (string)($d['nr'] ?? ''),
'id' => $d['geraet_id'],
'von' => $det['produktionsstart'] ?? '',
'bis' => $det['produktionsende'] ?? '',
'typ_de'=> $det['typ_de'] ?? '',
'marke' => $d['marke'] ?? '',
];
}
usort($v2_variants, function ($a, $b) { return strcmp($a['nr'], $b['nr']); });
}
}
}
}
// --- Stufe 2: gibt es EINEN klaren Top-Treffer? ---
// Nur dann darf man "Das ist dein Geraet" behaupten. Bedingung: Top-Score >= 95
// UND mindestens 10 Punkte Abstand zum Zweiten. Beispiel "Lavatherm 400": 100 vs. 75.
// Entfaellt, solange die Hersteller-Frage offen ist.
if (!$v2_brandpick && !$v2_variants && count($results) > 0) {
$v2_top_score_raw = (int)$results[0]['score'];
$v2_second_raw = isset($results[1]) ? (int)$results[1]['score'] : 0;
if ($v2_top_score_raw >= 95 && ($v2_top_score_raw - $v2_second_raw) >= 10) {
$v2_guided = true;
$v2_top_detail = $db->getGeraetDetails($results[0]['geraet_id']);
}
}
// --- Ersatzteile + Shop-Artikel NUR fuer den Top-Treffer vorladen ---
// Bewusst erst hier, nach Hersteller-Filter und Stufen-Ermittlung: vorher stand an
// Position 0 evtl. ein anderes Geraet. Die uebrigen Kacheln sind eingeklappt und
// laden per AJAX nach (siehe Endpoint am Dateianfang) -- sie brauchen bis dahin nur
// die Anzahl, die ein einzelner Batch-Query liefert statt 20 Einzelabfragen.
if (isset($results[0])) {
$results[0]['ersatzteile'] = $db->getErsatzteileForGeraet($results[0]['geraet_id']);
if ($show_shop_items) {
$results[0]['shop_items'] = $db->getAvailableItemsForGeraet($results[0]['geraet_id'], 200);
}
}
$v2_partcounts = count($results) > 1
? $db->getErsatzteilCountsBatch(array_column($results, 'geraet_id'))
: [];
// --- Shop-Links fuer die Ersatzteil-Chips ---
// Die Chips kommen aus PostgreSQL und kennen nur navision_id. Damit sie -- wie die
// Artikel weiter unten -- in den Shop verlinken, wird einmalig gesammelt aufgeloest.
// Nur die tatsaechlich gerenderten (max. 10 je Geraet), sonst waeren es Tausende.
// Nur fuer den Top-Treffer: dessen Ersatzteil-Chips sind die einzigen, die ohne
// Lazy-Load gerendert werden. Vorher sammelte das die navision_ids ALLER 20 Geraete
// (bis 200 Stueck) fuer einen IN-Query, dessen Ergebnis niemand brauchte.
if (isset($results[0])) {
$nav_ids = [];
foreach (array_slice($results[0]['ersatzteile'] ?? [], 0, 10) as $p) {
if (!empty($p['navision_id'])) { $nav_ids[] = $p['navision_id']; }
}
if ($nav_ids) { $v2_shopmap = $db->getShopItemsByNavisionIds($nav_ids); }
}
}
} catch (Exception $e) {
$error_message = $e->getMessage();
}
?>
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Krempl PostgreSQL Search Test</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background: #f5f5f5;
padding: 20px;
}
.container {
max-width: 1600px;
margin: 0 auto;
background: white;
padding: 30px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.split-results {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 24px;
margin-top: 20px;
}
.split-results > div {
min-width: 0;
}
.split-results h2 {
font-size: 16px;
color: #444;
background: #f4f4f4;
padding: 10px 14px;
border-left: 4px solid #4CAF50;
margin-bottom: 14px;
}
.split-results .right-pane h2 {
border-left-color: #1976D2;
}
.item-card {
border: 1px solid #e0e0e0;
border-radius: 4px;
margin-bottom: 8px;
padding: 10px 12px;
background: #fafafa;
font-size: 13px;
}
.item-card .item-name {
font-weight: 600;
color: #333;
margin-bottom: 4px;
}
.item-card .item-meta {
color: #666;
font-size: 12px;
}
.item-card .item-meta strong {color:#1976D2;}
.inventory-positive {color:#2e7d32;font-weight:bold;}
.inventory-zero {color:#aaa;}
.inventory-null {color:#aaa;font-style:italic;}
@media (max-width: 900px) {
.split-results { grid-template-columns: 1fr; }
}
h1 {
color: #333;
margin-bottom: 20px;
}
.search-box {
margin-bottom: 30px;
}
.search-input {
width: 100%;
padding: 12px 15px;
font-size: 16px;
border: 2px solid #ddd;
border-radius: 4px;
transition: border-color 0.3s;
}
.search-input:focus {
outline: none;
border-color: #4CAF50;
}
.search-btn {
padding: 10px 20px;
background: #4CAF50;
color: white;
border: none;
border-radius: 4px;
font-size: 16px;
cursor: pointer;
transition: background 0.3s;
white-space: nowrap;
}
.search-btn:hover {
background: #45a049;
}
#shopItemsToggle {
cursor: pointer;
width: 18px;
height: 18px;
}
.search-info {
color: #666;
margin-bottom: 20px;
font-size: 14px;
}
.device-card {
border: 1px solid #e0e0e0;
border-radius: 4px;
margin-bottom: 20px;
background: #fafafa;
overflow: hidden;
}
.device-card.collapsed .ersatzteile-section {
display: none;
}
.device-header {
display: flex;
justify-content: space-between;
align-items: start;
padding: 15px;
margin-bottom: 0;
border-bottom: 2px solid #e0e0e0;
}
.score-badge {
cursor: pointer;
transition: all 0.2s;
user-select: none;
}
.score-badge:hover {
opacity: 0.9;
transform: scale(1.02);
}
.toggle-arrow {
display: inline-block;
margin-left: 8px;
transition: transform 0.3s;
font-size: 10px;
}
.device-card.collapsed .toggle-arrow {
transform: rotate(-90deg);
}
.device-info {
flex: 1;
}
.device-title {
font-size: 18px;
font-weight: bold;
color: #333;
margin-bottom: 5px;
}
.device-title a {
color: #333;
text-decoration: none;
transition: color 0.2s;
}
.device-title a:hover {
color: #4CAF50;
text-decoration: underline;
}
.device-details {
color: #666;
font-size: 14px;
}
.device-details span {
display: inline-block;
margin-right: 15px;
}
.score-badge {
display: inline-block;
padding: 5px 12px;
border-radius: 20px;
font-weight: bold;
font-size: 14px;
}
.score-100 { background: #4CAF50; color: white; }
.score-95 { background: #66BB6A; color: white; }
.score-90 { background: #81C784; color: white; }
.score-80 { background: #FFC107; color: white; }
.score-75 { background: #FFD54F; color: #333; }
.score-60 { background: #FFE082; color: #333; }
.score-default { background: #BDBDBD; color: white; }
.ersatzteile-section {
padding: 15px;
}
.ersatzteile-title {
font-weight: bold;
color: #333;
margin-bottom: 10px;
margin-top: 10px;
font-size: 14px;
}
.ersatzteile-table {
width: 100%;
border-collapse: collapse;
background: white;
border-radius: 4px;
overflow: hidden;
}
.ersatzteile-table th {
background: #f0f0f0;
padding: 8px;
text-align: left;
font-size: 13px;
font-weight: 600;
color: #555;
}
.ersatzteile-table td {
padding: 8px;
border-top: 1px solid #e0e0e0;
font-size: 13px;
}
.ersatzteile-table tr:hover {
background: #f9f9f9;
}
.navision-id {
font-weight: bold;
color: #1976D2;
}
.no-results {
text-align: center;
padding: 40px;
color: #999;
}
.match-type {
display: inline-block;
font-size: 11px;
padding: 2px 6px;
background: #e0e0e0;
border-radius: 3px;
color: #666;
margin-left: 8px;
}
.error-message {
background: #ffebee;
border: 1px solid #ef5350;
color: #c62828;
padding: 15px;
border-radius: 4px;
margin-bottom: 20px;
}
.error-message strong {
display: block;
margin-bottom: 5px;
}
</style>
</head>
<body>
<div class="container">
<h1>Krempl PostgreSQL Search Test</h1>
<div class="search-box">
<form method="GET" action="" id="searchForm">
<input
type="text"
name="q"
class="search-input"
placeholder="Gerätesuche (z.B. AWB 921 PH, ZCS 2100B, 2100B)"
value="<?php echo htmlspecialchars($search_term); ?>"
autofocus
/>
<div style="margin-top: 10px; display: flex; align-items: center; gap: 15px;">
<button type="submit" class="search-btn">Suchen</button>
</div>
<?php // "Nur verfuegbare Shop-Artikel" ist der Ist-Zustand und nicht mehr
// umschaltbar in der UI. Zum Debuggen weiterhin per ?shop_items=0. ?>
</form>
</div>
<?php if ($error_message): ?>
<div class="error-message">
<strong>Fehler bei der Datenbankverbindung:</strong>
<?php echo htmlspecialchars($error_message); ?>
</div>
<?php endif; ?>
<?php if ($search_term !== '' && !$error_message): ?>
<?php
// ==================================================================
// GEFUEHRTE ERGEBNIS-ANSICHT
// Dreistufiger Trichter: Hersteller -> Bauzustand -> Bestaetigung.
// ==================================================================
// Treffer in "stark" (>=75) und "schwach" (<75) teilen
$v2_strong = []; $v2_weak = [];
foreach ($results as $d) { ((int)$d['score'] >= 75) ? $v2_strong[] = $d : $v2_weak[] = $d; }
// Artikel klassifizieren: kaufbar / ausgelaufen-mit-Nachfolger / tot
$g_buy = []; $g_succ = []; $g_dead = [];
foreach ($item_results as $it) {
$it['_nml'] = v2_nml_full($it);
// Ob ein Ersatz existiert, entscheidet primaer die im Backend gepflegte Zuordnung
// (item_item_assign type=3, angehaengt als $it['alt'] von KremplDB). Sie erreicht
// 4.150 aktive Artikel gegenueber 71 beim Namens-Regex, und ihr Ziel ist nachweislich
// kaufbar. Der Namenshinweis bleibt nur als Rueckfallebene.
if (!$it['_nml']['is_nml']) $g_buy[] = $it; // regulär
elseif (!empty($it['alt']) || $it['_nml']['nachfolger']) $g_succ[] = $it; // ausgelaufen, Ersatz da
else $g_dead[] = $it; // ausgelaufen, kein Ersatz
}
$v2_top_score = isset($results[0]) ? (int)$results[0]['score'] : 0;
// Intent-Einordnung
// Der Leerfall MUSS zuerst geprueft werden: sonst greift der else-Zweig und verspricht
// "Mehrere moegliche Treffer", obwohl 0 Geraete und 0 Artikel gefunden wurden
// (z.B. bei einem Tippfehler wie "spannrolle boisch").
$v2_leer = (count($results) === 0 && count($item_results) === 0);
if ($v2_leer) $v2_headline = 'Dazu haben wir nichts gefunden.';
elseif ($v2_top_score >= 90) $v2_headline = 'Wir haben dein Gerät gefunden.';
elseif (count($v2_strong) > 0) $v2_headline = 'Das könnte dein Gerät sein:';
elseif (count($item_results)) $v2_headline = 'Passende Artikel gefunden:';
else $v2_headline = 'Mehrere mögliche Treffer – grenz gern ein.';
?>
<style>
.v2{margin-top:24px;font-size:15px}
.v2 .v2-flag{display:inline-block;background:#111;color:#fff;font-size:11px;font-weight:700;
letter-spacing:.5px;padding:3px 10px;border-radius:4px;text-transform:uppercase}
.v2 .v2-head{font-size:22px;font-weight:700;color:#222;margin:14px 0 4px}
.v2 .v2-sub{color:#777;font-size:13px;margin-bottom:22px}
.v2-dev{border:1px solid #e3e3e3;border-radius:10px;margin-bottom:16px;overflow:hidden;
box-shadow:0 1px 3px rgba(0,0,0,.05)}
.v2-dev-top{display:flex;justify-content:space-between;align-items:flex-start;gap:16px;
padding:16px 18px;background:#fff}
.v2-dev-name{font-size:18px;font-weight:700;color:#1a1a1a;line-height:1.3}
.v2-dev-meta{color:#666;font-size:13px;margin-top:5px}
.v2-dev-meta b{color:#444}
.v2-conf{white-space:nowrap;font-size:12px;font-weight:700;padding:6px 12px;border-radius:20px}
.v2-parts{padding:14px 18px;border-top:1px solid #f0f0f0;background:#fcfcfc}
.v2-parts h4{font-size:13px;color:#555;margin-bottom:9px;font-weight:600}
.v2-chip{display:inline-block;background:#fff;border:1px solid #e0e0e0;border-radius:16px;
padding:4px 11px;margin:0 5px 6px 0;font-size:12px;color:#444}
.v2-chip b{color:#1565c0}
.v2-buy{background:#e8f5e9;border-color:#c8e6c9}
/* Warengruppen-Ueberschrift in der Artikel-Liste */
.v2-wg{font-size:12px;font-weight:700;color:#37474f;text-transform:uppercase;letter-spacing:.5px;
margin:16px 0 8px;padding-bottom:5px;border-bottom:1px solid #e0e6e9}
.v2-wg:first-of-type{margin-top:12px}
.v2-wg span{display:inline-block;background:#eceff1;color:#546e7a;border-radius:10px;
padding:1px 7px;font-size:11px;margin-left:6px;letter-spacing:0}
.v2-wg-nml{color:#a1887f;border-bottom-color:#efebe9}
.v2-wg-nml span{background:#efebe9;color:#8d6e63}
/* Artikel-ZEILEN (ersetzen die Chip-Blasen bei den kaufbaren Teilen) */
.v2-rows{margin-top:14px;display:flex;flex-direction:column;gap:6px}
.v2-row{display:flex;align-items:center;gap:12px;padding:9px 12px;border:1px solid #e8e8e8;
border-radius:8px;background:#fff;text-decoration:none;color:inherit}
a.v2-row{transition:border-color .12s,box-shadow .12s}
a.v2-row:hover{border-color:#2e7d32;box-shadow:0 1px 5px rgba(46,125,50,.15)}
.v2-row-nolink{background:#fbfbfb}
.v2-row-img{width:42px;height:42px;flex:0 0 42px;object-fit:contain;background:#fafafa;
border:1px solid #eee;border-radius:6px}
.v2-row-noimg{display:flex;align-items:center;justify-content:center;color:#ccc;font-size:15px}
.v2-row-txt{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px}
.v2-row-name{font-size:14px;font-weight:600;color:#222;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.v2-row-nr{font-size:12px;color:#888}
.v2-row-price{flex:0 0 auto;text-align:right;white-space:nowrap;font-size:14px}
.v2-row-price b{display:block;color:#1a1a1a}
.v2-row-price s{font-size:11px;color:#aaa}
.v2-row-noprice{font-size:12px;color:#aaa}
/* Ausgelaufener Artikel + sein Ersatz gehoeren optisch zusammen, deshalb ein eigener
Container mit kleinerem Abstand als die 6px zwischen zwei Artikelzeilen. */
.v2-rowgrp{display:flex;flex-direction:column;gap:3px}
.v2-altrow{display:flex;align-items:center;gap:10px;padding:7px 12px 7px 30px;
border:1px solid #c8e6c9;border-left:3px solid #2e7d32;border-radius:8px;
background:#f3faf4;text-decoration:none;color:inherit;
transition:border-color .12s,box-shadow .12s}
.v2-altrow:hover{border-color:#2e7d32;box-shadow:0 1px 5px rgba(46,125,50,.15)}
.v2-altrow-tag{flex:0 0 auto;font-size:11px;font-weight:700;color:#2e7d32;
text-transform:uppercase;letter-spacing:.03em}
.v2-altrow-txt{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px}
.v2-altrow-name{font-size:13px;font-weight:600;color:#1b5e20;overflow:hidden;
text-overflow:ellipsis;white-space:nowrap}
.v2-altrow-nr{font-size:12px;color:#6d8a70}
.v2-altrow-price{flex:0 0 auto;text-align:right;white-space:nowrap;font-size:13px}
.v2-altrow-price b{display:block;color:#1b5e20}
.v2-altrow-price s{font-size:11px;color:#a5bfa7}
/* Lazy-Nachladen der Artikel einer eingeklappten Geraete-Kachel */
.v2-lazy>summary{cursor:pointer;color:#1565c0;font-size:13px;font-weight:600;padding:5px 2px;
list-style:none}
.v2-lazy>summary::-webkit-details-marker{display:none}
.v2-lazy>summary::before{content:"▸ ";color:#90a4ae}
.v2-lazy[open]>summary::before{content:"▾ "}
.v2-lazy>summary:hover{text-decoration:underline}
.v2-lazy-hint{font-size:12px;color:#9e9e9e;padding:2px 2px 0}
.v2-lazy-msg{font-size:13px;color:#78909c;padding:10px 2px}
.v2-spin{display:inline-block;width:13px;height:13px;border:2px solid #cfd8dc;
border-top-color:#1565c0;border-radius:50%;animation:v2spin .7s linear infinite;
vertical-align:-2px;margin-right:8px}
@keyframes v2spin{to{transform:rotate(360deg)}}
.v2-rowmore{margin-top:8px}
.v2-rowmore>summary{cursor:pointer;color:#1565c0;font-size:13px;font-weight:600;padding:6px 2px;
list-style:none}
.v2-rowmore>summary::-webkit-details-marker{display:none}
.v2-rowmore>summary::before{content:"▸ ";color:#90a4ae}
.v2-rowmore[open]>summary::before{content:"▾ "}
.v2-rowmore>summary:hover{text-decoration:underline}
/* verlinkte Chips: nur die mit Shop-Artikel sind klickbar */
a.v2-chip-link{text-decoration:none;transition:border-color .12s,box-shadow .12s}
a.v2-chip-link:hover{border-color:#1565c0;box-shadow:0 1px 4px rgba(21,101,192,.18)}
a.v2-chip-link.v2-buy:hover{border-color:#2e7d32;box-shadow:0 1px 4px rgba(46,125,50,.18)}
.v2-more{margin:6px 0 22px}
.v2-more summary{cursor:pointer;color:#1565c0;font-size:14px;padding:8px 0}
.v2-shop{margin-top:30px}
.v2-shop h3{font-size:16px;color:#333;margin-bottom:12px;border-left:4px solid #1976D2;padding-left:10px}
.v2-item{display:flex;justify-content:space-between;align-items:center;gap:14px;
border:1px solid #eee;border-radius:8px;padding:12px 15px;margin-bottom:8px;background:#fff}
.v2-item-name{font-weight:600;color:#222}
.v2-item-meta{color:#888;font-size:12px;margin-top:3px}
.v2-stock{white-space:nowrap;font-size:13px;font-weight:700}
.v2-in{color:#2e7d32}.v2-out{color:#c62828}.v2-na{color:#aaa;font-style:italic;font-weight:400}
.v2-cart{background:#4CAF50;color:#fff;border:none;border-radius:6px;padding:8px 14px;
font-size:13px;font-weight:600;cursor:pointer;white-space:nowrap}
.v2-cart:disabled{background:#ccc;cursor:not-allowed}
.v2-hint{background:#fffdf3;border:1px dashed #e6d9a8;border-radius:8px;padding:10px 14px;
font-size:12px;color:#8a7a3a;margin-top:20px}
/* --- Gefuehrte Bestaetigung: EIN klarer Top-Treffer --- */
.v2-cf{border:2px solid #2e7d32;border-radius:12px;padding:22px 24px;margin:4px 0 18px;
background:linear-gradient(180deg,#f4fbf4 0%,#fff 60%);box-shadow:0 2px 8px rgba(46,125,50,.08)}
.v2-cf-q{font-size:13px;font-weight:700;color:#2e7d32;text-transform:uppercase;letter-spacing:.6px}
.v2-cf-name{font-size:26px;font-weight:700;color:#1a1a1a;line-height:1.25;margin:8px 0 2px}
.v2-cf-marke{font-size:15px;color:#555;font-weight:600}
.v2-cf-facts{display:flex;flex-wrap:wrap;gap:8px;margin:16px 0 4px}
.v2-cf-fact{background:#fff;border:1px solid #dfe8df;border-radius:8px;padding:8px 13px;font-size:13px;color:#33691e}
.v2-cf-fact b{color:#1b5e20;display:block;font-size:11px;text-transform:uppercase;
letter-spacing:.4px;color:#7a8f7a;font-weight:700;margin-bottom:2px}
.v2-cf-act{display:flex;flex-wrap:wrap;align-items:center;gap:14px;margin-top:20px;
padding-top:16px;border-top:1px solid #e4efe4}
.v2-cf-yes{background:#2e7d32;color:#fff;border:none;border-radius:8px;padding:12px 22px;
font-size:15px;font-weight:700;text-decoration:none;display:inline-block}
.v2-cf-no{color:#1565c0;font-size:14px;text-decoration:underline;cursor:pointer;background:none;border:none;padding:0}
.v2-alt{margin:0 0 22px}
.v2-alt>summary{cursor:pointer;color:#1565c0;font-size:14px;padding:10px 0;font-weight:600}
.v2-alt[open]>summary{margin-bottom:10px}
/* Hersteller-Buendelung innerhalb der Alternativen */
.v2-mgroup{font-size:13px;font-weight:700;color:#455a64;text-transform:uppercase;letter-spacing:.6px;
margin:18px 0 10px;padding-bottom:6px;border-bottom:2px solid #eceff1}
.v2-mgroup:first-of-type{margin-top:6px}
.v2-mgroup span{display:inline-block;background:#eceff1;color:#546e7a;border-radius:10px;
padding:1px 8px;font-size:11px;margin-left:6px;letter-spacing:0}
/* Stufe 1: Hersteller-Rueckfrage bei Multi-Marken-Modellen */
.v2-cf-brand{border-color:#1565c0;background:linear-gradient(180deg,#f2f7fd 0%,#fff 60%);
box-shadow:0 2px 8px rgba(21,101,192,.08)}
.v2-cf-brand .v2-cf-q{color:#1565c0}
.v2-brands{display:flex;flex-wrap:wrap;gap:10px;margin:18px 0 4px}
.v2-brand{display:inline-flex;align-items:center;gap:8px;background:#fff;border:2px solid #cfe0f5;
border-radius:10px;padding:12px 18px;font-size:15px;font-weight:700;color:#0d47a1;
text-decoration:none;transition:border-color .12s,background .12s}
.v2-brand:hover{border-color:#1565c0;background:#f5faff}
.v2-brand span{background:#e3eefb;color:#1565c0;border-radius:10px;padding:1px 8px;
font-size:12px;font-weight:600}
.v2-filter{background:#eef4fb;border:1px solid #cfe0f5;border-radius:8px;padding:9px 14px;
font-size:13px;color:#0d47a1;margin:0 0 16px;display:flex;gap:12px;align-items:center;flex-wrap:wrap}
.v2-filter a{color:#1565c0}
/* Stufe 1b: BSH-Bauzustaende /01 /02 ... nur durch den Zeitraum unterscheidbar */
.v2-cf-var{border-color:#6a1b9a;background:linear-gradient(180deg,#f9f4fc 0%,#fff 60%);
box-shadow:0 2px 8px rgba(106,27,154,.08)}
.v2-cf-var .v2-cf-q{color:#6a1b9a}
.v2-vars{display:grid;grid-template-columns:repeat(auto-fill,minmax(210px,1fr));gap:10px;margin-top:18px}
.v2-var{display:flex;align-items:center;gap:10px;background:#fff;border:2px solid #e6d9f0;
border-radius:10px;padding:11px 15px;text-decoration:none;transition:border-color .12s,background .12s}
.v2-var:hover{border-color:#6a1b9a;background:#fcf9fe}
.v2-var b{font-size:16px;font-weight:700;color:#4a148c;min-width:38px}
.v2-var span{font-size:13px;color:#666}
</style>
<div class="v2">
<?php
// Kaufbare Artikel richtig zaehlen: count($g_buy) allein war falsch -- $g_buy kommt
// aus der Artikel-TEXTsuche, und bei einer Geraetenummer wie "WT46W560BY/01" findet
// die in items.name naturgemaess nichts (zeigte 0 statt 8). Was real erreichbar ist,
// sind die Shop-Artikel des Top-Geraets aus der Geraetezuordnung PLUS die
// Textsuche-Treffer -- ueber items.id zusammengefuehrt, damit nichts doppelt zaehlt.
$v2_aids = [];
foreach (($results[0]['shop_items'] ?? []) as $si) { if (!empty($si['id'])) { $v2_aids[$si['id']] = true; } }
foreach ($g_buy as $it) { if (!empty($it['id'])) { $v2_aids[$it['id']] = true; } }
$v2_artikel_n = count($v2_aids);
?>
<div class="v2-head"><?php echo htmlspecialchars($v2_headline); ?></div>
<?php if ($v2_leer): ?>
<?php // Keine Zaehlzeile mit "0 · 0" -- die sagt nichts und liest sich wie ein Fehler. ?>
<div class="v2-sub">
Prüf bitte die Schreibweise, oder gib die Typnummer vom Typenschild ein
(z.B. <b>WT46W560BY</b> oder <b>Lavatherm 400</b>).
</div>
<?php else: ?>
<div class="v2-sub">
<?php echo count($v2_strong); ?> <?php echo count($v2_strong) === 1 ? 'passendes Gerät' : 'passende Geräte'; ?>
· <?php echo $v2_artikel_n; ?> kaufbare Artikel
<?php if (count($g_succ)) echo ' · '.count($g_succ).' mit Nachfolger'; ?>
<?php if (count($g_dead)) echo ' · '.count($g_dead).' ausgelaufen'; ?>
<?php if (count($v2_weak)) echo ' · '.count($v2_weak).' weitere mögliche Geräte'; ?>
</div>
<?php endif; ?>
<?php
// Query-String-Bausteine fuer die Hersteller-Links
$v2_qs_shop = $show_shop_items ? '' : '&amp;shop_items=0';
$v2_qs_base = '?q=' . urlencode($search_term) . $v2_qs_shop;
?>
<?php if ($v2_marke !== ''): ?>
<div class="v2-filter">
<span>Hersteller: <b><?php echo htmlspecialchars($v2_marke); ?></b></span>
<a href="<?php echo $v2_qs_base; ?>">↺ alle Hersteller zeigen</a>
</div>
<?php endif; ?>
<?php if ($v2_brandpick): ?>
<div class="v2-cf v2-cf-brand">
<div class="v2-cf-q">Mehrere Hersteller führen dieses Modell</div>
<div class="v2-cf-name"><?php echo htmlspecialchars(v2_gname($results[0])); ?></div>
<div class="v2-cf-marke">Welcher Hersteller steht auf deinem Gerät?</div>
<div class="v2-brands">
<?php foreach ($v2_brandpick as $m => $n): ?>
<a class="v2-brand" href="<?php echo $v2_qs_base; ?>&amp;marke=<?php echo urlencode($m); ?>">
<?php echo htmlspecialchars($m); ?><span><?php echo (int)$n; ?></span>
</a>
<?php endforeach; ?>
</div>
<div style="font-size:12px;color:#78909c;margin-top:14px">
Der Hersteller steht meist auf dem Typenschild oder als Logo auf der Blende.
</div>
</div>
<?php endif; ?>
<?php if ($v2_variants): ?>
<div class="v2-cf v2-cf-var">
<div class="v2-cf-q">Dieses Modell gibt es in mehreren Bauzuständen</div>
<div class="v2-cf-name"><?php echo htmlspecialchars($v2_variant_base); ?></div>
<div class="v2-cf-marke">
<?php
$vm = trim($v2_variants[0]['marke'] ?? '');
$vt = trim($v2_variants[0]['typ_de'] ?? '');
echo htmlspecialchars(trim($vm.' '.$vt));
?>
· <?php echo count($v2_variants); ?> Varianten
</div>
<div class="v2-cf-marke" style="margin-top:14px;font-weight:400;color:#666">
Wann wurde dein Gerät gebaut? Die Nummer auf dem Typenschild endet auf
<b>/01</b>, <b>/02</b> … – wenn du sie findest, ist das der schnellste Weg.
</div>
<div class="v2-vars">
<?php foreach ($v2_variants as $v):
$von = v2_monat($v['von']); $bis = v2_monat($v['bis']);
if ($von && $bis && $von !== $bis) $zr = $von.' – '.$bis;
elseif ($von) $zr = 'ab '.$von;
elseif ($bis) $zr = 'bis '.$bis;
else $zr = 'Zeitraum unbekannt';
$suf = (($p = strpos($v['nr'], '/')) !== false) ? substr($v['nr'], $p) : '–';
?>
<a class="v2-var" href="?q=<?php echo urlencode($v['nr']); ?><?php echo $show_shop_items ? '' : '&amp;shop_items=0'; ?>">
<b><?php echo htmlspecialchars($suf); ?></b>
<span><?php echo htmlspecialchars($zr); ?></span>
</a>
<?php endforeach; ?>
</div>
</div>
<?php endif; ?>
<?php if ($v2_guided && $v2_top_detail):
$det = $v2_top_detail;
// produktionsstart/-ende liegen als YYYYMMDD-String vor (z.B. "19790101")
$js = v2_jahr4($det['produktionsstart'] ?? '');
$je = v2_jahr4($det['produktionsende'] ?? '');
if ($js && $je && $js !== $je) $v2_zeit = $js.'–'.$je;
elseif ($js) $v2_zeit = 'ab '.$js;
elseif ($je) $v2_zeit = 'bis '.$je;
else $v2_zeit = '';
// Bauart aus zusatz + Warengruppentexten. zusatz zuerst, weil es die
// aussagekraeftigste Angabe ist ("Kondenstrockner") und bei 923.555 von
// 1.675.828 Geraeten gefuellt ist -- deutlich oefter als wgtext2.
// "-" ist in diesen Feldern ein Platzhalter und KEIN Wert: ohne diese Pruefung
// stand bei WT46W560BY/01 wortwoertlich "- · -" als Bauart in der Karte.
$v2_wg = [];
foreach (['zusatz', 'wgtext1', 'wgtext2'] as $v2_wgk) {
$v2_wgv = trim((string)($det[$v2_wgk] ?? ''));
if ($v2_wgv !== '' && $v2_wgv !== '-') { $v2_wg[] = $v2_wgv; }
}
$v2_wg = array_values(array_unique($v2_wg));
$v2_nparts = count($results[0]['ersatzteile'] ?? []);
?>
<div class="v2-cf">
<div class="v2-cf-q">Ist das dein Gerät?</div>
<div class="v2-cf-name"><?php echo htmlspecialchars(v2_gname($det)); ?></div>
<div class="v2-cf-marke"><?php echo htmlspecialchars($det['marke']); ?></div>
<div class="v2-cf-facts">
<?php if (!empty($det['typ_de'])): ?>
<div class="v2-cf-fact"><b>Gerätetyp</b><?php echo htmlspecialchars($det['typ_de']); ?></div>
<?php endif; ?>
<?php if ($v2_wg): ?>
<div class="v2-cf-fact"><b>Bauart</b><?php echo htmlspecialchars(implode(' · ', $v2_wg)); ?></div>
<?php endif; ?>
<?php if ($v2_zeit): ?>
<div class="v2-cf-fact"><b>Baujahr</b><?php echo htmlspecialchars($v2_zeit); ?></div>
<?php endif; ?>
</div>
<?php
// Ersatzteile des Top-Geraets direkt hier -- der alte v2-dev-Block fuer
// denselben Treffer entfaellt dadurch (war doppelt).
$v2_top_parts = $results[0]['ersatzteile'] ?? [];
$v2_top_buy = $results[0]['shop_items'] ?? null;
?>
<?php if ($v2_top_buy && count($v2_top_buy)): ?>
<div class="v2-parts" style="background:transparent;border-top:1px solid #e4efe4;margin-top:16px;padding:14px 0 0">
<h4>Verfügbare Artikel für dieses Gerät (<?php echo count($v2_top_buy); ?>):</h4>
<?php echo v2_artikel_liste($v2_top_buy); ?>
</div>
<?php elseif (count($v2_top_parts)): ?>
<div class="v2-parts" style="background:transparent;border-top:1px solid #e4efe4;margin-top:16px;padding:14px 0 0">
<h4><?php echo count($v2_top_parts); ?> passende Ersatzteile — Auswahl:</h4>
<?php foreach (array_slice($v2_top_parts,0,10) as $p): ?>
<?php $sm = $v2_shopmap[trim($p['navision_id'] ?? '')] ?? null; ?>
<?php echo v2_chip($p['originalnummer'], trim($p['marke'] ?? ''),
$sm['id'] ?? null, false, $sm['short_uri'] ?? null); ?>
<?php endforeach; ?>
<?php if (count($v2_top_parts) > 10): ?>
<span class="v2-chip" style="border-style:dashed;color:#999;">+<?php echo count($v2_top_parts)-10; ?> weitere</span>
<?php endif; ?>
</div>
<?php endif; ?>
<div class="v2-cf-act">
<?php if (count($g_buy)): ?>
<a href="#v2-artikel" class="v2-cf-yes">Ja – Artikel im Shop zeigen (<?php echo count($g_buy); ?>)</a>
<?php endif; ?>
<?php // "Nicht mein Gerät" stand doppelt: hier UND als <summary> des
// Alternativen-Blocks direkt darunter. Das <summary> genuegt. ?>
</div>
</div>
<?php endif; ?>
<?php
// Gefuehrter Modus: der Top-Treffer steckt komplett in der Bestaetigungskarte
// (inkl. Ersatzteile), erscheint hier also NICHT mehr -- sonst stand er doppelt.
// Alle weiteren Treffer (starke ab Index 1 + schwache) liegen hinter einem Klick
// und werden dort nach Hersteller gebuendelt: dasselbe Modell laeuft oft unter
// mehreren Konzern- bzw. Versandhausmarken (Lavatherm: AEG, Quelle, Neckermann,
// Asko, Electrolux, ...). Marken mit den meisten Treffern zuerst.
$v2_alt_open = false;
$v2_last_mgroup = null;
$v2_alt_total = 0;
$v2_list = $v2_strong;
$v2_mcount = [];
if ($v2_guided && $v2_top_detail) {
$v2_rest = array_merge(array_slice($v2_strong, 1), $v2_weak);
$v2_alt_total = count($v2_rest);
$by_marke = [];
foreach ($v2_rest as $d) {
$m = (isset($d['marke']) && trim($d['marke']) !== '') ? trim($d['marke']) : 'ohne Herstellerangabe';
$by_marke[$m][] = $d;
}
uasort($by_marke, function ($a, $b) { return count($b) <=> count($a); });
$v2_list = [];
foreach ($by_marke as $m => $ds) {
$v2_mcount[$m] = count($ds);
foreach ($ds as $d) { $d['_mgroup'] = $m; $v2_list[] = $d; }
}
}
?>
<?php foreach ($v2_list as $i => $d):
list($clabel,$ccol,$cbg,$cicon) = v2_confidence($d['score']);
$parts = $d['ersatzteile'] ?? [];
$buy = $d['shop_items'] ?? null; // nur wenn shop_items-Toggle an
if (!$v2_alt_open && ($v2_guided && $v2_top_detail)) {
echo '<details class="v2-alt" id="v2-alternativen"><summary>Nicht mein Gerät – '
. $v2_alt_total . ' weitere Treffer anzeigen</summary>';
$v2_alt_open = true;
}
// Bei der Varianten-Karte stehen dieselben Geraete schon als Buttons oben --
// die ausfuehrliche Liste (mit Ersatzteilen) deshalb ebenfalls einklappen.
if (!$v2_alt_open && $v2_variants) {
echo '<details class="v2-alt" id="v2-alternativen"><summary>Alle '
. count($v2_variants) . ' Bauzustände mit Ersatzteilen anzeigen</summary>';
$v2_alt_open = true;
}
// Dito bei der Hersteller-Rueckfrage: erst die Marke klaeren, die Rohliste
// (oft 20 optisch identische Kacheln) gehoert dahinter.
if (!$v2_alt_open && $v2_brandpick) {
echo '<details class="v2-alt" id="v2-alternativen"><summary>Alle '
. count($results) . ' Treffer ungefiltert anzeigen</summary>';
$v2_alt_open = true;
}
if (isset($d['_mgroup']) && $d['_mgroup'] !== $v2_last_mgroup) {
$v2_last_mgroup = $d['_mgroup'];
echo '<h4 class="v2-mgroup">'.htmlspecialchars($v2_last_mgroup)
. ' <span>'.(int)($v2_mcount[$v2_last_mgroup] ?? 0).'</span></h4>';
}
?>
<div class="v2-dev">
<div class="v2-dev-top">
<div>
<div class="v2-dev-name"><?php echo htmlspecialchars(trim($d['marke'].' '.v2_gname($d))); ?></div>
<div class="v2-dev-meta">
<b>Typ:</b> <?php echo htmlspecialchars($d['typ']); ?>
· <b>Nr:</b> <?php echo htmlspecialchars($d['nr']); ?>
<?php if (!empty($d['produktionsstart']) || !empty($d['produktionsende'])): ?>
· <b>Baujahr:</b> <?php echo htmlspecialchars(trim(($d['produktionsstart']??'').'–'.($d['produktionsende']??''),'–')); ?>
<?php endif; ?>
</div>
</div>
<span class="v2-conf" style="color:<?php echo $ccol; ?>;background:<?php echo $cbg; ?>;">
<?php echo $cicon.' '.htmlspecialchars($clabel); ?>
</span>
</div>
<?php if ($buy && count($buy)): ?>
<div class="v2-parts">
<h4>Verfügbare Artikel für dieses Gerät (<?php echo count($buy); ?>):</h4>
<?php // in den Kacheln begrenzt, die aktive Karte zeigt alles ?>
<?php echo v2_artikel_liste($buy, 12, 12); ?>
</div>
<?php elseif ($show_shop_items): ?>
<?php // Diese Kachel ist eingeklappt -- ihre Shop-Artikel werden erst
// beim Aufklappen per AJAX geholt. Ein Vorladen fuer alle 20
// Treffer war die Ursache der langen Laufzeit.
// Anzahl aus dem Batch-COUNT -- die Liste selbst liegt hier noch
// nicht vor, deshalb kein $parts.
$pc = $v2_partcounts[$d['geraet_id']] ?? count($parts); ?>
<div class="v2-parts">
<?php if ($pc): ?>
<details class="v2-lazy" data-geraet="<?php echo (int)$d['geraet_id']; ?>">
<summary>Verfügbare Artikel im Shop anzeigen</summary>
<div class="v2-lazy-target"></div>
</details>
<?php // Der Hinweis "N Ersatzteile laut Herstellerdaten hinterlegt"
// ist am 05.08.2026 entfallen (Tom): Er nennt die Zahl aus dem
// Krempl-Katalog, nicht die der kaufbaren Shop-Artikel. Die
// Auswahl ist bewusst kuratiert, also steht die Herstellerzahl
// fast immer ueber einer kuerzeren Liste -- an gid 20001645889
// etwa "10 Ersatzteile" ueber genau einem Artikel. Das liest
// sich wie ein Fehler und weckt eine Erwartung, die der Shop
// absichtlich nicht bedient.
// $pc bleibt in Gebrauch: es entscheidet oben, ob der
// Aufklapper ueberhaupt gerendert wird. ?>
<?php else: ?>
<?php // Kein Aufklapper, wenn zu diesem Geraet ueberhaupt keine
// Ersatzteile gemappt sind: "Verfügbare Artikel im Shop
// anzeigen" verspricht dann etwas, das es nicht gibt, und
// der Kunde klickt ins Leere. Die 0 steht durch den
// Batch-COUNT bereits hier fest, nicht erst nach dem AJAX. ?>
<div class="v2-lazy-hint">Zu diesem Gerät sind in den Herstellerdaten keine Ersatzteile hinterlegt.</div>
<?php endif; ?>
</div>
<?php elseif (count($parts)): ?>
<div class="v2-parts">
<h4><?php echo count($parts); ?> passende Ersatzteile — Auswahl:</h4>
<?php foreach (array_slice($parts,0,10) as $p): ?>
<?php $sm = $v2_shopmap[trim($p['navision_id'] ?? '')] ?? null; ?>
<?php echo v2_chip($p['originalnummer'], trim($p['marke'] ?? ''),
$sm['id'] ?? null, false, $sm['short_uri'] ?? null); ?>
<?php endforeach; ?>
<?php if (count($parts) > 10): ?>
<span class="v2-chip" style="border-style:dashed;color:#999;">+<?php echo count($parts)-10; ?> weitere</span>
<?php endif; ?>
</div>
<?php endif; ?>
</div>
<?php endforeach; ?>
<?php
// Im gefuehrten Modus stecken die schwachen Treffer bereits in $v2_list (nach
// Hersteller gebuendelt), der separate Schwach-Block entfaellt dort komplett.
?>
<?php if (!$v2_guided && count($v2_weak)): ?>
<details class="v2-more">
<summary>Weitere mögliche Geräte anzeigen (<?php echo count($v2_weak); ?>) – schwächere Treffer</summary>
<?php foreach ($v2_weak as $d): list($clabel,$ccol,$cbg,$cicon)=v2_confidence($d['score']); ?>
<div class="v2-dev" style="opacity:.75">
<div class="v2-dev-top">
<div>
<div class="v2-dev-name" style="font-size:15px"><?php echo htmlspecialchars(trim($d['marke'].' '.v2_gname($d))); ?></div>
<div class="v2-dev-meta"><b>Typ:</b> <?php echo htmlspecialchars($d['typ']); ?> · <b>Nr:</b> <?php echo htmlspecialchars($d['nr']); ?></div>
</div>
<span class="v2-conf" style="color:<?php echo $ccol; ?>;background:<?php echo $cbg; ?>;"><?php echo $cicon.' '.htmlspecialchars($clabel); ?></span>
</div>
</div>
<?php endforeach; ?>
</details>
<?php endif; ?>
<?php
// Alternativen-Block schliessen (nur wenn er ueberhaupt geoeffnet wurde).
if ($v2_alt_open) { echo '</details>'; }
?>
<?php $keep_shop = $show_shop_items ? '' : '&shop_items=0'; ?>
<?php if (count($g_buy)): ?>
<div class="v2-shop" id="v2-artikel">
<h3>🛒 Diese Artikel kannst du direkt kaufen</h3>
<?php foreach ($g_buy as $it):
// KEIN Lager-Kriterium mehr: die Kundin fuehrt im Shop kein Lager
// (kein einziger Artikel hat inventory > 0 -- 10.160x 0, 8.724x NULL).
// Bestellbar steuert sie ueber items.active, und searchItems() filtert
// bereits mit WHERE i.active = 1 -> alles hier ist bestellbar.
// Shop-Artikel und Bilder liegen nur auf LIVE -> absolute URLs
$url = v2_shopurl($it['id'] ?? null, $it['short_uri'] ?? null);
$img = !empty($it['default_image_file_name'])
? tba_url('web/intelectra_shop/images/article/'.rawurlencode($it['default_image_file_name']))
: '';
$pb = isset($it['price_brutto']) ? (float)$it['price_brutto'] : 0;
$bb = !empty($it['bargain_brutto']) ? (float)$it['bargain_brutto'] : 0;
?>
<div class="v2-item">
<a href="<?php echo $url; ?>" target="_blank" rel="noopener"
style="display:flex;align-items:center;gap:14px;min-width:0;text-decoration:none;color:inherit">
<?php if ($img): ?>
<img src="<?php echo $img; ?>" alt="" loading="lazy"
style="width:64px;height:64px;object-fit:contain;background:#fff;border:1px solid #eee;border-radius:6px;flex:0 0 64px"
onerror="this.style.display='none'">
<?php else: ?>
<div style="width:64px;height:64px;flex:0 0 64px;background:#f5f5f5;border:1px dashed #ddd;border-radius:6px;
display:flex;align-items:center;justify-content:center;color:#bbb;font-size:11px">kein Bild</div>
<?php endif; ?>
<div style="min-width:0">
<div class="v2-item-name" style="text-decoration:underline;text-decoration-color:#cfd8dc"><?php echo htmlspecialchars($it['_nml']['clean']); ?></div>
<div class="v2-item-meta">Art-Nr <?php echo htmlspecialchars($it['number']); ?></div>
</div>
</a>
<div style="text-align:right;flex:0 0 auto">
<div style="font-size:17px;font-weight:700;color:#1a1a1a;margin-bottom:2px">
<?php if ($bb > 0 && $pb > 0 && $bb < $pb): ?>
<span style="color:#c62828"><?php echo number_format($bb, 2, ',', '.'); ?>&nbsp;€</span>
<span style="font-size:12px;color:#999;text-decoration:line-through;font-weight:400"><?php echo number_format($pb, 2, ',', '.'); ?>&nbsp;€</span>
<?php elseif ($pb > 0): ?>
<?php echo number_format($pb, 2, ',', '.'); ?>&nbsp;€
<?php else: ?>
<span style="font-size:12px;color:#999;font-weight:400">Preis auf Anfrage</span>
<?php endif; ?>
</div>
<?php if ($pb > 0): ?><div style="font-size:10px;color:#aaa;margin-bottom:4px">inkl. MwSt.</div><?php endif; ?>
<a href="<?php echo $url; ?>" target="_blank" rel="noopener"
class="v2-cart" style="margin-top:6px;text-decoration:none;display:inline-block">
Zum Artikel
</a>
</div>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
<?php if (count($g_succ)): ?>
<div class="v2-shop">
<h3 style="border-left-color:#fb8c00">🔄 Ausgelaufen – aber es gibt einen Nachfolger</h3>
<?php foreach ($g_succ as $it):
$nf = $it['_nml']['nachfolger'];
$alt = $it['alt'] ?? null;
// (nmle) = ausgelaufener Artikel MIT Ersatz. Der alte Artikel
// existiert im Shop weiterhin und darf deshalb anklickbar sein.
$url_alt = v2_shopurl($it['id'] ?? null, $it['short_uri'] ?? null);
?>
<div class="v2-item" style="background:#fff8f0;border-color:#ffe0b2">
<div>
<a href="<?php echo $url_alt; ?>" target="_blank" rel="noopener" style="text-decoration:none">
<div class="v2-item-name" style="color:#8a6d3b;text-decoration:line-through;text-decoration-color:#d9b38c">
<?php echo htmlspecialchars($it['_nml']['clean']); ?>
</div>
</a>
<div class="v2-item-meta">Art-Nr <?php echo htmlspecialchars($it['number']); ?> · nicht mehr lieferbar</div>
</div>
<?php if ($alt): ?>
<?php
// Gepflegte Zuordnung: der Ersatz ist namentlich bekannt und
// nachweislich kaufbar, also direkt darauf verlinken statt eine
// neue Suche anzustossen. Der Umweg ueber ?q=<Nummer> landete in
// 18 von 58 Faellen wieder bei einem ausgelaufenen Artikel.
$ap = isset($alt['price_brutto']) ? (float)$alt['price_brutto'] : 0;
$ab = !empty($alt['bargain_brutto']) ? (float)$alt['bargain_brutto'] : 0;
?>
<div style="text-align:right;max-width:52%">
<div style="font-size:12px;color:#2e7d32;font-weight:700;margin-bottom:2px">→ Ersatz verfügbar</div>
<div style="font-size:12px;color:#555;margin-bottom:2px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">
<?php echo htmlspecialchars($alt['name']); ?>
</div>
<div style="font-size:11px;color:#888;margin-bottom:6px">
Art-Nr <?php echo htmlspecialchars($alt['number']); ?>
<?php if ($ab > 0 && $ap > 0 && $ab < $ap): ?>
· <b style="color:#c62828"><?php echo number_format($ab, 2, ',', '.'); ?> €</b>
<?php elseif ($ap > 0): ?>
· <b style="color:#1b5e20"><?php echo number_format($ap, 2, ',', '.'); ?> €</b>
<?php endif; ?>
<?php if (!empty($alt['tiefe']) && (int)$alt['tiefe'] > 1): ?>
· über <?php echo (int)$alt['tiefe']; ?> Nachfolger ermittelt
<?php endif; ?>
</div>
<a href="<?php echo v2_shopurl($alt['id'] ?? null, $alt['short_uri'] ?? null); ?>"
target="_blank" rel="noopener"
class="v2-cart" style="background:#2e7d32;text-decoration:none;display:inline-block">
→ Zum Ersatzartikel
</a>
</div>
<?php else: ?>
<div style="text-align:right">
<div style="font-size:12px;color:#e65100;font-weight:600;margin-bottom:6px">Nachfolger: <?php echo htmlspecialchars($nf); ?></div>
<a href="?q=<?php echo urlencode($nf).$keep_shop; ?>"
class="v2-cart" style="background:#fb8c00;text-decoration:none;display:inline-block">
→ Nachfolger suchen
</a>
</div>
<?php endif; ?>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
<?php if (count($g_dead)): ?>
<details class="v2-more" style="margin-top:20px">
<summary>Nicht mehr lieferbar, kein Ersatz (<?php echo count($g_dead); ?>) – ausgeblendet</summary>
<?php foreach ($g_dead as $it): ?>
<div class="v2-item" style="opacity:.55">
<div>
<div class="v2-item-name" style="text-decoration:line-through"><?php echo htmlspecialchars($it['_nml']['clean']); ?></div>
<div class="v2-item-meta">Art-Nr <?php echo htmlspecialchars($it['number']); ?></div>
</div>
<div class="v2-stock v2-out" style="white-space:nowrap">✕ entfällt</div>
</div>
<?php endforeach; ?>
</details>
<?php endif; ?>
<?php if (!count($g_buy) && !count($g_succ) && !count($g_dead)): ?>
<!-- keine Artikel -->
<?php endif; ?>
</div>
<?php else: ?>
<div class="no-results">
Gib einen Suchbegriff ein (z.B. "AWB 921 PH", "ZCS 2100B", "Kohlefilter Bauknecht")
</div>
<?php endif; ?>
</div>
<script>
/**
* Laedt die Shop-Artikel einer Geraete-Kachel erst beim Aufklappen nach.
*
* Vorher lud die Seite die Artikel fuer alle 20 Treffer vor, obwohl 19 eingeklappt sind.
* Das toggle-Event bubbelt nicht, deshalb wird es in der Capture-Phase abgefangen -- so
* greift es auch fuer Kacheln, die in einem uebergeordneten <details> stecken.
*/
document.addEventListener('toggle', function (e) {
var d = e.target;
if (!d.classList || !d.classList.contains('v2-lazy') || !d.open) return;
if (d.dataset.loaded === '1') return;
d.dataset.loaded = '1';
var box = d.querySelector('.v2-lazy-target');
if (!box) return;
box.innerHTML = '<div class="v2-lazy-msg"><span class="v2-spin"></span>Artikel werden geladen …</div>';
fetch('?ajax=items&geraet_id=' + encodeURIComponent(d.dataset.geraet), {
credentials: 'same-origin'
})
.then(function (r) {
if (!r.ok) throw new Error('HTTP ' + r.status);
return r.text();
})
.then(function (html) { box.innerHTML = html; })
.catch(function () {
// Flag zuruecksetzen, damit ein zweiter Versuch moeglich bleibt.
d.dataset.loaded = '';
box.innerHTML = '<div class="v2-lazy-msg">Artikel konnten nicht geladen werden – '
+ 'bitte zuklappen und erneut öffnen.</div>';
});
}, true);
</script>
</body>
</html>