dolibarr.netdiag/lib/netdiag_pdf.lib.php
Eduard Wisch 9b69438d17 WLAN-Kanal-Momentaufnahme darstellen (HTML + PDF)
Die App legt eine WLAN-Momentaufnahme jetzt als Messung ab (tool='wifikanal'),
damit sie beim Kunden aufgenommen auch im Protokoll auf dem Server ankommt —
bisher lag sie nur lokal auf dem Handy.

- netdiagFormatWifiKanal(): Zusammenfassung (Anzahl Netze, eigenes Netz mit
  Kanal/Pegel/Bewertung, störungsärmster 2,4-GHz-Kanal), Hinweisliste und
  vollständige Netztabelle. Eigener Zweig wie beim Dauertest, weil die
  generische Darstellung das Netz-Array per dol_trunc(...,200) mitten im Satz
  abschneiden würde.
- netdiagPdfWifiKanal(): dieselbe Aufbereitung als PDF-Tabelle.
- Nebenbei: PDF zeigte bei Status 0 doppelt "OK OK" (Zeichen + Label).

Gegen das Test-Dolibarr geprüft: Kartenansicht rendert Zusammenfassung, beide
Warnhinweise und alle 12 Netze; PDF-Erzeugung ebenfalls verifiziert.
2026-08-15 17:25:13 +02:00

373 lines
13 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
/* Copyright (C) 2026 Eduard Wisch <data@data-it-solution.de>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
/**
* \file netdiag/lib/netdiag_pdf.lib.php
* \ingroup netdiag
* \brief PDF-Generator für Diagnose-Protokolle
*/
require_once DOL_DOCUMENT_ROOT.'/core/lib/pdf.lib.php';
require_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php';
require_once DOL_DOCUMENT_ROOT.'/societe/class/societe.class.php';
require_once DOL_DOCUMENT_ROOT.'/commande/class/commande.class.php';
require_once __DIR__.'/netdiag.lib.php';
require_once __DIR__.'/../class/netdiagdevice.class.php';
require_once __DIR__.'/../class/netdiagmeasurement.class.php';
/**
* Ein Diagnose-Protokoll als PDF erzeugen und im Dokumentenverzeichnis ablegen.
*
* @param DoliDB $db Datenbank-Handler
* @param NetDiagProtocol $protocol Protokoll-Objekt (geladen)
* @param Translate $outputlangs Sprache der Ausgabe
* @return int >0 wenn OK, <=0 bei Fehler
*/
function netdiagGeneratePdf($db, $protocol, $outputlangs)
{
global $conf, $mysoc, $langs;
$outputlangs->loadLangs(array("netdiag@netdiag", "main", "companies", "orders"));
$dir = netdiagGetOutputDir().'/'.dol_sanitizeFileName($protocol->ref);
if (!dol_is_dir($dir)) {
if (dol_mkdir($dir) < 0) {
$protocol->error = 'Konnte Verzeichnis nicht anlegen: '.$dir;
return -1;
}
}
$file = $dir.'/'.dol_sanitizeFileName($protocol->ref).'.pdf';
// Daten laden
$soc = new Societe($db);
if ($protocol->fk_soc > 0) {
$soc->fetch($protocol->fk_soc);
}
$devObj = new NetDiagDevice($db);
$devices = $devObj->fetchAllByProtocol($protocol->id);
$measObj = new NetDiagMeasurement($db);
$measurements = $measObj->fetchAllByProtocol($protocol->id);
// PDF aufbauen
$pdf = pdf_getInstance('A4', 'mm', 'P');
$default_font_size = pdf_getPDFFontSize($outputlangs);
$pdf->SetAutoPageBreak(true, 20);
$pdf->SetMargins(15, 15, 15);
$pdf->SetTitle($protocol->ref);
$pdf->SetSubject($outputlangs->transnoentities("NetDiagProtocol"));
$pdf->SetCreator("Dolibarr ".DOL_VERSION." - NetDiag");
$pdf->SetAuthor($mysoc->name);
$pdf->AddPage();
// Kopf
$pdf->SetFont('', 'B', 16);
$pdf->SetTextColor(0, 70, 130);
$pdf->Cell(0, 8, $outputlangs->transnoentities("NetDiagProtocol").' '.$protocol->ref, 0, 1, 'L');
$pdf->SetTextColor(0, 0, 0);
$pdf->SetFont('', '', 9);
$pdf->Cell(0, 5, $mysoc->name, 0, 1, 'L');
$pdf->Ln(3);
// Stammdaten
$pdf->SetFont('', '', 10);
$infos = array(
$outputlangs->transnoentities("ThirdParty") => ($soc->id > 0 ? $soc->name : '-'),
$outputlangs->transnoentities("DateDiag") => dol_print_date($protocol->date_diag, 'dayhour', false, $outputlangs),
$outputlangs->transnoentities("Location") => ($protocol->standort ? $protocol->standort : '-'),
$outputlangs->transnoentities("Subnet") => ($protocol->subnet ? $protocol->subnet : '-'),
);
foreach ($infos as $k => $v) {
$pdf->SetFont('', 'B', 10);
$pdf->Cell(40, 6, $k.':', 0, 0, 'L');
$pdf->SetFont('', '', 10);
$pdf->Cell(0, 6, $v, 0, 1, 'L');
}
if (!empty($protocol->note)) {
$pdf->Ln(1);
$pdf->SetFont('', 'I', 9);
$pdf->MultiCell(0, 5, $outputlangs->transnoentities("Note").': '.$protocol->note, 0, 'L');
}
$pdf->Ln(4);
// Geräteliste
$pdf->SetFont('', 'B', 12);
$pdf->Cell(0, 7, $outputlangs->transnoentities("NetDiagDevices").' ('.count($devices).')', 0, 1, 'L');
$pdf->SetFillColor(0, 70, 130);
$pdf->SetTextColor(255, 255, 255);
$pdf->SetFont('', 'B', 8);
$cols = array(35, 40, 50, 40, 15);
$heads = array("IpAddress", "MacAddress", "Hostname", "Vendor", "DeviceType");
foreach ($heads as $idx => $h) {
$pdf->Cell($cols[$idx], 6, $outputlangs->transnoentities($h), 1, 0, 'L', true);
}
$pdf->Ln();
$pdf->SetTextColor(0, 0, 0);
$pdf->SetFont('', '', 8);
foreach ($devices as $dev) {
$pdf->Cell($cols[0], 5, dol_trunc($dev->ip, 22), 1, 0, 'L');
$pdf->Cell($cols[1], 5, dol_trunc($dev->mac, 24), 1, 0, 'L');
$pdf->Cell($cols[2], 5, dol_trunc($dev->hostname, 32), 1, 0, 'L');
$pdf->Cell($cols[3], 5, dol_trunc($dev->vendor, 26), 1, 0, 'L');
$pdf->Cell($cols[4], 5, dol_trunc($dev->devicetype, 10), 1, 1, 'L');
}
if (empty($devices)) {
$pdf->Cell(array_sum($cols), 5, '-', 1, 1, 'C');
}
$pdf->Ln(4);
// Messungen
$pdf->SetFont('', 'B', 12);
$pdf->Cell(0, 7, $outputlangs->transnoentities("NetDiagMeasurements").' ('.count($measurements).')', 0, 1, 'L');
// Status 3 = "nicht messbar": der Test konnte nicht durchgeführt werden.
// Bewusst grau und mit eigenem Zeichen — weder grün (wäre gelogen) noch rot
// (wäre eine Aussage über das Kundennetz, die die Messung nicht hergibt).
$statuslabels = array(
0 => $outputlangs->transnoentities("NetDiagMeasureOk"),
1 => $outputlangs->transnoentities("NetDiagMeasureWarn"),
2 => $outputlangs->transnoentities("NetDiagMeasureFail"),
3 => $outputlangs->transnoentities("NetDiagMeasureUnmeasurable"),
);
// Zusätzlich ein Zeichen, damit die Ampel auch im Schwarz-Weiß-Ausdruck lesbar
// bleibt. Bei Status 0 bewusst leer: das Label heißt dort bereits "OK", sonst
// stünde im PDF doppelt "OK OK".
$statusmarks = array(0 => '', 1 => '! ', 2 => 'X ', 3 => '? ');
foreach ($measurements as $m) {
$st = (int) $m->measure_status;
if ($st < 0 || $st > 3) {
$st = 1; // unbekannter Wert -> als Warnung behandeln, nie als OK
}
if ($st == 2) {
$pdf->SetFillColor(230, 130, 130);
} elseif ($st == 1) {
$pdf->SetFillColor(245, 215, 130);
} elseif ($st == 3) {
$pdf->SetFillColor(210, 210, 210);
} else {
$pdf->SetFillColor(170, 215, 170);
}
$pdf->SetFont('', 'B', 9);
$title = ($m->category ? '['.$m->category.'] ' : '').$m->tool.($m->label ? ' — '.$m->label : '');
$pdf->Cell(150, 6, dol_trunc($title, 80), 1, 0, 'L');
$pdf->Cell(30, 6, $statusmarks[$st].$statuslabels[$st], 1, 1, 'C', true);
$pdf->SetFont('', '', 8);
if ($m->tool === 'stresstest') {
netdiagPdfStressTest($pdf, $m->result);
} elseif ($m->tool === 'wifikanal') {
netdiagPdfWifiKanal($pdf, $m->result);
} else {
$pdf->MultiCell(180, 5, netdiagPdfFlattenResult($m->result), 1, 'L');
}
}
if (empty($measurements)) {
$pdf->SetFont('', '', 9);
$pdf->Cell(180, 5, '-', 1, 1, 'C');
}
// Fuß
$pdf->Ln(6);
$pdf->SetFont('', 'I', 8);
$pdf->SetTextColor(120, 120, 120);
$pdf->Cell(0, 5, $outputlangs->transnoentities("NetDiagProtocol").' '.$protocol->ref.' — '.dol_print_date(dol_now(), 'dayhour', false, $outputlangs), 0, 1, 'L');
// Speichern
$pdf->Output($file, 'F');
dolChmod($file);
return 1;
}
/**
* Mess-Ergebnis (JSON) als einzeiligen Text für PDF-Ausgabe.
*
* @param string $json JSON-String
* @return string Lesbarer Text
*/
function netdiagPdfFlattenResult($json)
{
if (empty($json)) {
return '-';
}
$data = json_decode($json, true);
if (!is_array($data)) {
return (string) $json;
}
$parts = array();
foreach ($data as $key => $val) {
if (is_array($val)) {
$flat = array();
foreach ($val as $item) {
$flat[] = is_array($item) ? json_encode($item, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) : (string) $item;
}
$val = implode(', ', $flat);
} elseif (is_bool($val)) {
$val = $val ? 'ja' : 'nein';
}
$parts[] = ucfirst((string) $key).': '.$val;
}
return implode(' | ', $parts);
}
/**
* Dauer-/Stresstest strukturiert ins PDF schreiben statt als Fließtext.
*
* netdiagPdfFlattenResult() würde Kennzahlen, Ausfallliste und Zeitreihe zu
* einem einzigen langen Absatz zusammenkleben — bei einer Stunde Laufzeit mit
* 60 Minuten-Buckets kaum lesbar. Hier stattdessen eine kompakte
* Kennzahlenzeile plus eine echte Ausfalltabelle. Die Zeitreihe (`verlauf`)
* wird bewusst NICHT gedruckt — sie ist als Diagramm in der App nützlich,
* als reiner Text auf Papier nicht.
*
* @param TCPDF $pdf PDF-Objekt (Cursor steht direkt hinter der Titelzeile)
* @param string $json result-JSON der Messung
* @return void
*/
function netdiagPdfStressTest($pdf, $json)
{
$data = json_decode((string) $json, true);
if (!is_array($data)) {
$pdf->MultiCell(180, 5, (string) $json, 1, 'L');
return;
}
if (!empty($data['hinweis']) || !empty($data['fehler'])) {
$pdf->MultiCell(180, 5, (string) ($data['hinweis'] ?? $data['fehler']), 1, 'L');
return;
}
$sum = array();
if (isset($data['host'])) {
$sum[] = 'Ziel: '.$data['host'];
}
if (isset($data['dauerSekunden'])) {
$sum[] = 'Dauer: '.round($data['dauerSekunden'] / 60).' min';
}
if (isset($data['intervallSek'])) {
$sum[] = 'Takt: '.$data['intervallSek'].' s';
}
if (isset($data['gesendet']) && isset($data['empfangen'])) {
$sum[] = 'Proben: '.$data['empfangen'].'/'.$data['gesendet'];
}
if (isset($data['verlustProzent'])) {
$sum[] = 'Verlust: '.$data['verlustProzent'].' %';
}
if (isset($data['avgMs']) && $data['avgMs'] !== null) {
$sum[] = 'Ø '.$data['avgMs'].' ms';
}
if (isset($data['minMs']) && isset($data['maxMs']) && $data['minMs'] !== null) {
$sum[] = 'Min/Max '.$data['minMs'].'/'.$data['maxMs'].' ms';
}
if (isset($data['p95Ms']) && $data['p95Ms'] !== null) {
$sum[] = 'p95 '.$data['p95Ms'].' ms';
}
if (isset($data['laengsterAusfallSek']) && $data['laengsterAusfallSek'] !== null) {
$sum[] = 'längster Ausfall '.$data['laengsterAusfallSek'].' s';
}
$pdf->MultiCell(180, 5, implode(' | ', $sum), 1, 'L');
// Ausfalltabelle — je Zeile "HH:MM:SS HH:MM:SS (Xs)", vorformatiert von
// der App (dieselben Zeilen, die auch in der Messungen-Liste in der App
// stehen — eine Quelle der Wahrheit statt zwei Formatierungen).
$ausfaelle = (isset($data['ausfaelle']) && is_array($data['ausfaelle'])) ? $data['ausfaelle'] : array();
if (empty($ausfaelle)) {
$pdf->SetFont('', 'I', 8);
$pdf->Cell(180, 5, 'Kein Ausfall während des Laufs.', 1, 1, 'L');
$pdf->SetFont('', '', 8);
return;
}
$pdf->SetFont('', 'B', 8);
$pdf->Cell(180, 5, 'Ausfälle ('.count($ausfaelle).')', 1, 1, 'L');
$pdf->SetFont('', '', 8);
foreach ($ausfaelle as $line) {
$pdf->Cell(180, 5, dol_trunc((string) $line, 120), 1, 1, 'L');
}
}
/**
* WLAN-Kanal-Momentaufnahme im PDF als Tabelle ausgeben.
*
* Wie netdiagPdfStressTest ein eigener Zweig, weil die generische
* Flach-Darstellung die Netzliste (Array von Objekten) unlesbar macht.
*
* @param TCPDF $pdf PDF-Objekt
* @param string $json JSON-String des Ergebnisses
* @return void
*/
function netdiagPdfWifiKanal($pdf, $json)
{
$data = json_decode($json, true);
if (!is_array($data)) {
$pdf->MultiCell(180, 5, netdiagPdfFlattenResult($json), 1, 'L');
return;
}
// Kopfzeile: Anzahl, eigenes Netz, Empfehlung
$sum = array();
if (isset($data['anzahlNetze'])) {
$sum[] = $data['anzahlNetze'].' Netze sichtbar';
}
if (!empty($data['eigenesNetz']) && is_array($data['eigenesNetz'])) {
$e = $data['eigenesNetz'];
$teil = 'Eigenes Netz: '.($e['ssid'] ?? '?').' Kanal '.($e['kanal'] ?? '?');
if (isset($e['rssi'])) {
$teil .= ', '.$e['rssi'].' dBm';
}
if (!empty($e['bewertung'])) {
$teil .= ' ('.$e['bewertung'].')';
}
$sum[] = $teil;
}
if (!empty($data['empfehlung24GHz']) && is_array($data['empfehlung24GHz'])) {
$best = reset($data['empfehlung24GHz']);
if (is_array($best) && isset($best['kanal'])) {
$sum[] = 'Störungsärmster 2,4-GHz-Kanal: '.$best['kanal'];
}
}
if (!empty($sum)) {
$pdf->MultiCell(180, 5, implode(' | ', $sum), 1, 'L');
}
if (!empty($data['warnungen']) && is_array($data['warnungen'])) {
foreach ($data['warnungen'] as $w) {
$pdf->MultiCell(180, 5, '! '.dol_trunc((string) $w, 160), 1, 'L');
}
}
$netze = (isset($data['netze']) && is_array($data['netze'])) ? $data['netze'] : array();
if (empty($netze)) {
return;
}
// Tabellenkopf
$pdf->SetFont('', 'B', 7);
$pdf->Cell(60, 5, 'SSID', 1, 0, 'L');
$pdf->Cell(18, 5, 'Kanal', 1, 0, 'C');
$pdf->Cell(22, 5, 'Band', 1, 0, 'L');
$pdf->Cell(20, 5, 'Breite', 1, 0, 'C');
$pdf->Cell(35, 5, 'Standard', 1, 0, 'L');
$pdf->Cell(25, 5, 'Pegel', 1, 1, 'R');
$pdf->SetFont('', '', 7);
foreach ($netze as $n) {
if (!is_array($n)) {
continue;
}
$pdf->Cell(60, 5, dol_trunc((string) ($n['ssid'] ?? ''), 34), 1, 0, 'L');
$pdf->Cell(18, 5, (string) ($n['kanal'] ?? ''), 1, 0, 'C');
$pdf->Cell(22, 5, (string) ($n['band'] ?? ''), 1, 0, 'L');
$pdf->Cell(20, 5, (isset($n['breiteMhz']) && $n['breiteMhz'] !== null) ? $n['breiteMhz'].' MHz' : '-', 1, 0, 'C');
$pdf->Cell(35, 5, dol_trunc((string) ($n['standard'] ?? '-'), 20), 1, 0, 'L');
$pdf->Cell(25, 5, (string) ($n['rssi'] ?? '').' dBm', 1, 1, 'R');
}
}