Jahr. In produktionsstart/-ende stecken ZWEI Formate: * YYYYMMDD z.B. "19790101" (AEG Lavatherm 400) * YYMM z.B. "8810" (SIEMENS WT46W560BY/01 = Oktober 1988) * Bei YYMM gilt: >= 70 -> 19YY, sonst 20YY. */ function v2_jahr4($v) { $s = preg_replace('/\D/', '', (string)$v); if ($s === '') return ''; if (strlen($s) >= 6) return substr($s, 0, 4); // YYYYMM(DD) if (strlen($s) === 4) { // YYMM $yy = (int)substr($s, 0, 2); return (string)($yy >= 70 ? 1900 + $yy : 2000 + $yy); } if (strlen($s) === 2) { // nur YY $yy = (int)$s; return (string)($yy >= 70 ? 1900 + $yy : 2000 + $yy); } return ''; } /** Monat/Jahr fuer die Variantenliste, z.B. "10/1988". Leer, wenn nicht ableitbar. */ function v2_monat($v) { $s = preg_replace('/\D/', '', (string)$v); $j = v2_jahr4($s); if ($j === '') return ''; if (strlen($s) >= 6) $m = substr($s, 4, 2); // YYYYMM(DD) elseif (strlen($s) === 4) $m = substr($s, 2, 2); // YYMM else return $j; $m = (int)$m; return ($m >= 1 && $m <= 12) ? sprintf('%02d/%s', $m, $j) : $j; } /** * Shop-URL zu einem Artikel -- leer, wenn kein Shop-Artikel bekannt ist. * * Bevorzugt die sprechende SEO-URL "/.html". Das Feld `items.short_uri` * ist bei allen 16.927 aktiven Artikeln gefuellt (Format "-") * und wird vom Shop selbst so verlinkt (item_box.tpl). Aufgeloest wird sie in der * .htaccess ueber "RewriteRule ^([aA-zZ0-9-_,]+).(html)$ index.php?item_suri=$1". * * Diese Regel akzeptiert nur die Zeichen ihrer Klasse. Deshalb wird hier geprueft, * BEVOR verlinkt wird -- eine short_uri mit abweichenden Zeichen wuerde nicht * greifen und den Kunden auf einen 404 schicken. In dem Fall bleibt es bei der * ID-URL, die immer funktioniert. * * Bewusst enger als die .htaccess: deren "aA-zZ" ist ein alter Tippfehler und deckt * als ASCII-Bereich A(65)-z(122) auch ` [ \ ] ^ _ ab. Fuenf Artikel haben deshalb * einen Backtick in der short_uri und funktionieren nur zufaellig; die bekommen * hier die ID-URL. Auf den Tippfehler zu bauen waere fahrlaessig. */ function v2_shopurl($item_id, $short_uri = null) { if (!$item_id) return ''; $uri = trim((string)$short_uri); if ($uri !== '' && preg_match('/^[A-Za-z0-9_,-]+$/', $uri)) { return tba_url($uri . '.html'); } return tba_url('index.php?item_id=' . (int)$item_id); } /** * Chip fuer ein Ersatzteil/Artikel. Gibt einen aus, wenn ein Shop-Artikel * bekannt ist, sonst ein -- damit nichts ins Leere klickt. */ function v2_chip($label, $sub, $item_id, $buyclass = false, $short_uri = null) { $cls = 'v2-chip' . ($buyclass ? ' v2-buy' : ''); $inner = '' . htmlspecialchars($label) . '' . ($sub !== '' ? ' · ' . htmlspecialchars($sub) : ''); $url = v2_shopurl($item_id, $short_uri); if ($url === '') { return '' . $inner . ''; } return '' . $inner . ''; } /** * Artikel-ZEILE statt der alten Chip-"Blase": Bild, Name, Art-Nr, Preis. * Wird in der Bestaetigungskarte und in den Geraete-Kacheln verwendet, damit * die Teile-Liste dieselbe Form hat wie die Artikelliste weiter unten. * Ohne Shop-Artikel-ID wird ein div statt eines Links ausgegeben. */ function v2_artikel_row($it) { $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; $nml = v2_nml_info((string)($it['name'] ?? '')); // nml zusaetzlich ueber die Warengruppe erkennen: die ist verlaesslicher als // der Namensmarker (erkennt 2.207 Faelle, die der Regex uebersieht). $is_nml = $nml['is_nml'] || v2_is_nml_wg($it['wg_name'] ?? ''); $h = $url !== '' ? '' : ''); // Der Ersatzartikel bekommt eine EIGENE Zeile: ein Link darf nicht in einem // Link stehen, und der Kunde soll ihn direkt anklicken koennen statt erst den // ausgelaufenen Artikel zu oeffnen. if ($is_nml && !empty($it['alt'])) { $h = '
' . $h . v2_altrow($it['alt']) . '
'; } return $h; } /** * Zeile "Ersatz verfuegbar" unter einem nicht mehr lieferbaren Artikel. * Datenquelle ist die im Backend gepflegte Zuordnung (item_item_assign type=3), * aufgeloest von KremplDB::getAlternativesBatch(). Angezeigt werden nur kaufbare * Ziele, deshalb ist die Zusage hier belastbar. */ function v2_altrow($alt) { $url = v2_shopurl($alt['id'] ?? null, $alt['short_uri'] ?? null); if ($url === '') return ''; $pb = isset($alt['price_brutto']) ? (float)$alt['price_brutto'] : 0; $bb = !empty($alt['bargain_brutto']) ? (float)$alt['bargain_brutto'] : 0; $h = '' . '→ Ersatz' . '' . '' . htmlspecialchars((string)($alt['name'] ?? '')) . '' . 'Art-Nr ' . htmlspecialchars((string)($alt['number'] ?? '')); // Ueber mehr als eine Station gelaufen: der direkte Nachfolger war selbst // ausgelaufen. Das ist eine ehrliche Angabe, keine Fussnote. if (!empty($alt['tiefe']) && (int)$alt['tiefe'] > 1) { $h .= ' · über ' . (int)$alt['tiefe'] . ' Nachfolger ermittelt'; } if (!empty($alt['weitere'])) { $h .= ' · ' . (int)$alt['weitere'] . ' weitere Alternative' . ((int)$alt['weitere'] > 1 ? 'n' : ''); } $h .= ''; if ($bb > 0 && $pb > 0 && $bb < $pb) { $h .= '' . number_format($bb, 2, ',', '.') . ' €' . '' . number_format($pb, 2, ',', '.') . ' €'; } elseif ($pb > 0) { $h .= '' . number_format($pb, 2, ',', '.') . ' €'; } else { $h .= 'Preis auf Anfrage'; } return $h . ''; } /** Warengruppen-Name deutet auf "nicht mehr lieferbar" hin? */ function v2_is_nml_wg($wg) { return (bool)preg_match('/lieferbar|NMLE|Nachfolge|kein Ersatz/i', (string)$wg); } /** * nml-Status eines Shop-Artikels aus BEIDEN Quellen: Warengruppe und Namensmarker. * * Die Warengruppe ist der verlaesslichere Marker -- gemessen am 30.07.2026: * ueber WG erkannt 9.848 Artikel, ueber (nml)-Regex 7.694, davon erkennt nur die * WG 2.207 Faelle (Klartext "Nicht mehr lieferbar +++ kein Ersatz" ohne Suffix) * und nur der Name 51. Zusammen also ~9.899 von 16.923 aktiven. * Die nml-Warengruppen sind 9929, 10649, 10746, 10750; ihr Name "weitere Treffer: * Nachfolge-Artikel" (10649) ist irrefuehrend -- dort liegen ausgelaufene Artikel * (1.241 von 1.274 tragen selbst einen nml-Marker), nicht die Nachfolger. * Ein Nachfolger laesst sich weiterhin nur aus dem NAMEN lesen, daher bleibt die * Unterscheidung g_succ/g_dead am Namenstext haengen. */ function v2_nml_full($it) { $info = v2_nml_info((string)($it['name'] ?? '')); if (!$info['is_nml'] && v2_is_nml_wg($it['wg_name'] ?? '')) { $info['is_nml'] = true; } return $info; } /** * Artikel-Liste als Zeilen. Ab $threshold Treffern wird nach Warengruppe * gruppiert -- genau wie die alte Shop-Suche, weil eine flache Liste mit 30+ * Positionen unbrauchbar ist. nml-Warengruppen ("Nicht mehr Lieferbar +++ ...", * "NMLE-ohne WG", "weitere Treffer: Nachfolge-Artikel") wandern ans Ende. */ function v2_artikel_liste(array $items, $threshold = 12, $max = null) { if (!$items) return ''; // nml-Artikel ans Ende: sie sind nicht bestellbar und sollen die Liste nicht // anfuehren. Muss VOR dem Abschneiden passieren, sonst landen kaufbare Teile // im versteckten Rest, waehrend nml-Positionen die sichtbaren Plaetze belegen. $nml_flag = function ($it) { return (v2_is_nml_wg($it['wg_name'] ?? '') || v2_nml_info((string)($it['name'] ?? ''))['is_nml']) ? 1 : 0; }; usort($items, function ($a, $b) use ($nml_flag) { return $nml_flag($a) - $nml_flag($b); }); // In den eingeklappten Geraete-Kacheln wird begrenzt ($max), sonst rendert eine // Suche wie "WT46W560BY" 10 Kacheln x bis 200 Artikel = mehrere Tausend Zeilen. // Die aktive Bestaetigungskarte zeigt alles ($max = null). Der Rest wird NICHT // verworfen, sondern aufklappbar angehaengt -- vorher war "+ N weitere" ein // toter Text und der Kunde erfuhr nie, was er noch kaufen koennte. $restitems = []; if ($max !== null && count($items) > $max) { $restitems = array_slice($items, $max); $items = array_slice($items, 0, $max); } $more = ''; if ($restitems) { $more = '
+ ' . count($restitems) . ' weitere Artikel für dieses Gerät anzeigen
'; foreach ($restitems as $it) { $more .= v2_artikel_row($it); } $more .= '
'; } if (count($items) <= $threshold) { $h = '
'; foreach ($items as $it) { $h .= v2_artikel_row($it); } return $h . '
' . $more; } $groups = []; foreach ($items as $it) { $wg = trim((string)($it['wg_name'] ?? '')) ?: 'Ohne Warengruppe'; $groups[$wg][] = $it; } // Echte Warengruppen zuerst (die groesste oben), nml-Gruppen ans Ende. uksort($groups, function ($a, $b) use (&$groups) { $na = v2_is_nml_wg($a) ? 1 : 0; $nb = v2_is_nml_wg($b) ? 1 : 0; if ($na !== $nb) return $na - $nb; return count($groups[$b]) <=> count($groups[$a]); }); $h = ''; foreach ($groups as $wg => $its) { $cls = v2_is_nml_wg($wg) ? ' v2-wg-nml' : ''; $h .= '

' . htmlspecialchars($wg) . ' ' . count($its) . '

'; foreach ($its as $it) { $h .= v2_artikel_row($it); } $h .= '
'; } return $h . $more; } function v2_confidence($score) { $s = (int)$score; if ($s >= 100) return ['Exakter Treffer', '#2e7d32', '#e8f5e9', '✓']; if ($s >= 90) return ['Sehr wahrscheinlich dein Gerät','#388e3c', '#edf7ee', '✓']; if ($s >= 75) return ['Könnte passen', '#f9a825', '#fff8e1', '≈']; return ['Ähnlich – bitte prüfen', '#9e9e9e', '#f5f5f5', '?']; } // nml/Nachfolger steckt im Artikelnamen: "(nml)"/"(nmle)" + "Nachfolger " function v2_nml_info($name) { $is_nml = (bool)preg_match('/\(nml[e]?\)/i', $name); $nachf = null; // Nachfolger-Nummer muss mind. eine Ziffer enthalten (schließt "Nachfolger Original" aus) if (preg_match('/Nachfolger\s+([A-Za-z0-9\-\/\.]*\d[A-Za-z0-9\-\/\.]*)/i', $name, $m)) { $nachf = $m[1]; } $kein_ersatz = (stripos($name, 'kein Ersatz') !== false); // Anzeigename säubern: (nml)-Marker + "Nachfolger ... Original(teil)" raus $clean = preg_replace('/\s*\(nml[e]?\)\s*/i', ' ', $name); $clean = preg_replace('/\s*Nachfolger\s+[A-Za-z0-9\-\/\.]*\d[A-Za-z0-9\-\/\.]*\s*/i', ' ', $clean); $clean = preg_replace('/\s*(Originalteil|Original)\s*$/i', '', $clean); $clean = trim(preg_replace('/\s{2,}/', ' ', $clean)); return ['is_nml'=>$is_nml, 'nachfolger'=>$nachf, 'kein_ersatz'=>$kein_ersatz, 'clean'=>($clean ?: $name)]; } }