diff --git a/ChangeLog.md b/ChangeLog.md
index 35e1770..0ce2004 100755
--- a/ChangeLog.md
+++ b/ChangeLog.md
@@ -1,5 +1,28 @@
# CHANGELOG MODULE KUNDENKARTE FOR [DOLIBARR ERP CRM](https://www.dolibarr.org)
+## 2.0 (2026-01)
+
+### Neue Features
+- **PDF Export mit Vorlage**: Briefpapier/Hintergrund-PDF kann als Vorlage hochgeladen werden
+ - Upload im Admin-Bereich unter Einstellungen
+ - Vorlage wird als Hintergrund auf allen Seiten verwendet
+- **PDF Schriftgroessen konfigurierbar**: Anpassbare Schriftgroessen fuer den PDF-Export
+ - Ueberschriften (7-14pt)
+ - Inhalte (6-12pt)
+ - Feldbezeichnungen (5-10pt)
+- **Verbesserte PDF-Baumdarstellung**: Professionelle Darstellung der Anlagenstruktur
+ - Farbcodierte Header pro Hierarchie-Ebene (dezente Grauabstufungen)
+ - Abgerundete Rahmen um Elemente
+ - Visuelle Verbindungslinien zwischen Elementen
+ - Bessere Einrueckung und Lesbarkeit
+
+### Verbesserungen
+- Logo aus PDF-Export entfernt (ersetzt durch Vorlagen-System)
+- Dynamische Felder fuer Element-Typen (Ueberschrift als neuer Feldtyp)
+- Kopierfunktion fuer Elemente und Typen
+
+---
+
## 1.1 (2026-01)
### Neue Features
diff --git a/README.md b/README.md
index f58c355..e9ca190 100755
--- a/README.md
+++ b/README.md
@@ -17,6 +17,12 @@ Das KundenKarte-Modul erweitert Dolibarr um zwei wichtige Funktionen fuer Kunden
- Datei-Upload mit Bild-Vorschau und PDF-Anzeige
- Separate Verwaltung pro Kunde oder pro Kontakt/Adresse (z.B. verschiedene Gebaeude)
+### PDF Export
+- Export der Anlagenstruktur als PDF
+- Upload einer PDF-Vorlage als Briefpapier/Hintergrund
+- Konfigurierbare Schriftgroessen (Ueberschriften, Inhalte, Felder)
+- Professionelle Baumdarstellung mit farbcodierten Ebenen und Rahmen
+
### Kontakt/Adressen-Unterstuetzung
- Beide Funktionen (Favoriten + Anlagen) sind sowohl auf Kundenebene als auch auf Kontakt-/Adressebene verfuegbar
- Ideal fuer Kunden mit mehreren Standorten/Gebaeuden
diff --git a/admin/anlage_types.php b/admin/anlage_types.php
index 2027f67..308a555 100755
--- a/admin/anlage_types.php
+++ b/admin/anlage_types.php
@@ -51,7 +51,7 @@ if ($action == 'add') {
$anlageType->fk_system = GETPOSTINT('fk_system');
$anlageType->can_have_children = GETPOSTINT('can_have_children');
$anlageType->can_be_nested = GETPOSTINT('can_be_nested');
- $anlageType->allowed_parent_types = GETPOST('allowed_parent_types', 'alphanohtml');
+ $anlageType->allowed_parent_types = preg_replace('/[^A-Z0-9_,]/i', '', GETPOST('allowed_parent_types', 'nohtml'));
$anlageType->picto = GETPOST('picto', 'alphanohtml');
$anlageType->color = GETPOST('color', 'alphanohtml');
$anlageType->position = GETPOSTINT('position');
@@ -63,8 +63,26 @@ if ($action == 'add') {
} else {
$result = $anlageType->create($user);
if ($result > 0) {
+ // Create default fields for the new type
+ $defaultFields = array(
+ array('code' => 'manufacturer', 'label' => 'Hersteller', 'type' => 'text', 'position' => 10, 'show_in_hover' => 1, 'show_in_tree' => 1),
+ array('code' => 'model', 'label' => 'Modell', 'type' => 'text', 'position' => 20, 'show_in_hover' => 1, 'show_in_tree' => 0),
+ array('code' => 'serial_number', 'label' => 'Seriennummer', 'type' => 'text', 'position' => 30, 'show_in_hover' => 1, 'show_in_tree' => 0),
+ array('code' => 'power_rating', 'label' => 'Leistung', 'type' => 'text', 'position' => 40, 'show_in_hover' => 1, 'show_in_tree' => 1),
+ array('code' => 'location', 'label' => 'Standort', 'type' => 'text', 'position' => 50, 'show_in_hover' => 1, 'show_in_tree' => 0),
+ array('code' => 'installation_date', 'label' => 'Installationsdatum', 'type' => 'date', 'position' => 60, 'show_in_hover' => 1, 'show_in_tree' => 0),
+ );
+
+ foreach ($defaultFields as $field) {
+ $sql = "INSERT INTO ".MAIN_DB_PREFIX."kundenkarte_anlage_type_field";
+ $sql .= " (fk_anlage_type, field_code, field_label, field_type, field_options, show_in_tree, show_in_hover, required, position, active)";
+ $sql .= " VALUES (".((int) $anlageType->id).", '".$db->escape($field['code'])."', '".$db->escape($field['label'])."',";
+ $sql .= " '".$db->escape($field['type'])."', '', ".((int) $field['show_in_tree']).", ".((int) $field['show_in_hover']).", 0, ".((int) $field['position']).", 1)";
+ $db->query($sql);
+ }
+
setEventMessages($langs->trans('RecordSaved'), null, 'mesgs');
- header('Location: '.$_SERVER['PHP_SELF'].'?system='.$anlageType->fk_system);
+ header('Location: '.$_SERVER['PHP_SELF'].'?action=edit&typeid='.$anlageType->id.'&system='.$anlageType->fk_system);
exit;
} else {
setEventMessages($anlageType->error, $anlageType->errors, 'errors');
@@ -82,7 +100,7 @@ if ($action == 'update') {
$anlageType->fk_system = GETPOSTINT('fk_system');
$anlageType->can_have_children = GETPOSTINT('can_have_children');
$anlageType->can_be_nested = GETPOSTINT('can_be_nested');
- $anlageType->allowed_parent_types = GETPOST('allowed_parent_types', 'alphanohtml');
+ $anlageType->allowed_parent_types = preg_replace('/[^A-Z0-9_,]/i', '', GETPOST('allowed_parent_types', 'nohtml'));
$anlageType->picto = GETPOST('picto', 'alphanohtml');
$anlageType->color = GETPOST('color', 'alphanohtml');
$anlageType->position = GETPOSTINT('position');
@@ -121,6 +139,48 @@ if ($action == 'deactivate') {
$action = '';
}
+// Copy type with all fields
+if ($action == 'copy' && $typeId > 0) {
+ $sourceType = new AnlageType($db);
+ if ($sourceType->fetch($typeId) > 0) {
+ // Create new type with copied data
+ $newType = new AnlageType($db);
+ $newType->ref = $sourceType->ref.'_COPY';
+ $newType->label = $sourceType->label.' (Kopie)';
+ $newType->label_short = $sourceType->label_short;
+ $newType->description = $sourceType->description;
+ $newType->fk_system = $sourceType->fk_system;
+ $newType->can_have_children = $sourceType->can_have_children;
+ $newType->can_be_nested = $sourceType->can_be_nested;
+ $newType->allowed_parent_types = $sourceType->allowed_parent_types;
+ $newType->picto = $sourceType->picto;
+ $newType->color = $sourceType->color;
+ $newType->position = $sourceType->position + 1;
+ $newType->active = 1;
+
+ $result = $newType->create($user);
+ if ($result > 0) {
+ // Copy all fields from source type
+ $sourceFields = $sourceType->fetchFields(0); // Get all fields including inactive
+ foreach ($sourceFields as $field) {
+ $sql = "INSERT INTO ".MAIN_DB_PREFIX."kundenkarte_anlage_type_field";
+ $sql .= " (fk_anlage_type, field_code, field_label, field_type, field_options, show_in_tree, show_in_hover, required, position, active)";
+ $sql .= " VALUES (".((int) $newType->id).", '".$db->escape($field->field_code)."', '".$db->escape($field->field_label)."',";
+ $sql .= " '".$db->escape($field->field_type)."', '".$db->escape($field->field_options)."', ".((int) $field->show_in_tree).",";
+ $sql .= " ".((int) $field->show_in_hover).", ".((int) $field->required).", ".((int) $field->position).", ".((int) $field->active).")";
+ $db->query($sql);
+ }
+
+ setEventMessages($langs->trans('RecordSaved'), null, 'mesgs');
+ header('Location: '.$_SERVER['PHP_SELF'].'?action=edit&typeid='.$newType->id.'&system='.$newType->fk_system);
+ exit;
+ } else {
+ setEventMessages($newType->error, $newType->errors, 'errors');
+ }
+ }
+ $action = '';
+}
+
// Field actions
$fieldId = GETPOSTINT('fieldid');
@@ -128,7 +188,7 @@ if ($action == 'add_field') {
$fieldCode = GETPOST('field_code', 'aZ09');
$fieldLabel = GETPOST('field_label', 'alphanohtml');
$fieldType = GETPOST('field_type', 'aZ09');
- $fieldOptions = GETPOST('field_options', 'restricthtml');
+ $fieldOptions = GETPOST('field_options', 'nohtml');
$showInTree = GETPOSTINT('show_in_tree');
$showInHover = GETPOSTINT('show_in_hover');
$isRequired = GETPOSTINT('is_required');
@@ -156,7 +216,7 @@ if ($action == 'update_field') {
$fieldCode = GETPOST('field_code', 'aZ09');
$fieldLabel = GETPOST('field_label', 'alphanohtml');
$fieldType = GETPOST('field_type', 'aZ09');
- $fieldOptions = GETPOST('field_options', 'restricthtml');
+ $fieldOptions = GETPOST('field_options', 'nohtml');
$showInTree = GETPOSTINT('show_in_tree');
$showInHover = GETPOSTINT('show_in_hover');
$isRequired = GETPOSTINT('is_required');
@@ -367,6 +427,7 @@ if (in_array($action, array('create', 'edit'))) {
// Field types available
$fieldTypes = array(
+ 'header' => '── Überschrift ──',
'text' => 'Textfeld (einzeilig)',
'textarea' => 'Textfeld (mehrzeilig)',
'number' => 'Zahlenfeld',
@@ -375,6 +436,20 @@ if (in_array($action, array('create', 'edit'))) {
'checkbox' => 'Checkbox (Ja/Nein)',
);
+ // Output edit forms BEFORE the table (forms cannot be inside tables)
+ foreach ($fields as $field) {
+ if ($editFieldId == $field->rowid) {
+ $formId = 'editfield_'.$field->rowid;
+ print '
';
diff --git a/ajax/anlage_tooltip.php b/ajax/anlage_tooltip.php
index 665323a..55f0bef 100755
--- a/ajax/anlage_tooltip.php
+++ b/ajax/anlage_tooltip.php
@@ -53,20 +53,28 @@ $fieldsData = array();
foreach ($typeFields as $field) {
if ($field->show_in_hover) {
+ // Handle header fields
+ if ($field->field_type === 'header') {
+ $fieldsData[$field->field_code] = array(
+ 'label' => $field->field_label,
+ 'value' => '',
+ 'type' => 'header'
+ );
+ continue;
+ }
+
$value = isset($fieldValues[$field->field_code]) ? $fieldValues[$field->field_code] : '';
- // For select fields, get label
- if ($field->field_type == 'select' && $value && $field->field_options) {
- $options = json_decode($field->field_options, true);
- if (isset($options['options'][$value])) {
- $value = $options['options'][$value];
- }
+ // Format date values
+ $displayValue = $value;
+ if ($field->field_type === 'date' && $value) {
+ $displayValue = dol_print_date(strtotime($value), 'day');
}
$fieldsData[$field->field_code] = array(
'label' => $field->field_label,
- 'value' => $value,
- 'show_in_hover' => true
+ 'value' => $displayValue,
+ 'type' => $field->field_type
);
}
}
@@ -90,11 +98,7 @@ $data = array(
'label' => $anlage->label,
'type_label' => $anlage->type_label,
'picto' => $anlage->type_picto,
- 'manufacturer' => $anlage->manufacturer,
- 'model' => $anlage->model,
- 'serial_number' => $anlage->serial_number,
- 'power_rating' => $anlage->power_rating,
- 'location' => $anlage->location,
+ 'note_html' => $anlage->note_private ? nl2br(htmlspecialchars($anlage->note_private, ENT_QUOTES, 'UTF-8')) : '',
'fields' => $fieldsData,
'images' => $images
);
diff --git a/ajax/export_tree_pdf.php b/ajax/export_tree_pdf.php
new file mode 100644
index 0000000..082b05f
--- /dev/null
+++ b/ajax/export_tree_pdf.php
@@ -0,0 +1,432 @@
+loadLangs(array('companies', 'kundenkarte@kundenkarte'));
+
+// Get parameters
+$socId = GETPOSTINT('socid');
+$contactId = GETPOSTINT('contactid');
+$systemId = GETPOSTINT('system');
+
+// Security check
+if (!$user->hasRight('kundenkarte', 'read')) {
+ accessforbidden();
+}
+
+// Load company
+$societe = new Societe($db);
+$societe->fetch($socId);
+
+// Load contact if specified
+$contact = null;
+if ($contactId > 0) {
+ $contact = new Contact($db);
+ $contact->fetch($contactId);
+}
+
+// Load system info
+$systemLabel = '';
+$sql = "SELECT label FROM ".MAIN_DB_PREFIX."c_kundenkarte_anlage_system WHERE rowid = ".((int) $systemId);
+$resql = $db->query($sql);
+if ($resql && $obj = $db->fetch_object($resql)) {
+ $systemLabel = $obj->label;
+}
+
+// Load tree
+$anlage = new Anlage($db);
+if ($contactId > 0) {
+ $tree = $anlage->fetchTreeByContact($socId, $contactId, $systemId);
+} else {
+ $tree = $anlage->fetchTree($socId, $systemId);
+}
+
+// Load all type fields for display (including headers)
+$typeFieldsMap = array();
+$sql = "SELECT f.*, f.fk_anlage_type FROM ".MAIN_DB_PREFIX."kundenkarte_anlage_type_field f WHERE f.active = 1 ORDER BY f.position ASC";
+$resql = $db->query($sql);
+if ($resql) {
+ while ($obj = $db->fetch_object($resql)) {
+ if (!isset($typeFieldsMap[$obj->fk_anlage_type])) {
+ $typeFieldsMap[$obj->fk_anlage_type] = array();
+ }
+ $typeFieldsMap[$obj->fk_anlage_type][] = $obj;
+ }
+}
+
+// Create PDF
+$pdf = pdf_getInstance();
+$pdf->SetCreator('Dolibarr - Kundenkarte');
+$pdf->SetAuthor($user->getFullName($langs));
+
+$title = $systemLabel.' - '.$societe->name;
+if ($contact) {
+ $title .= ' - '.$contact->getFullName($langs);
+}
+$pdf->SetTitle($title);
+
+$pdf->SetMargins(15, 15, 15);
+$pdf->SetAutoPageBreak(true, 15);
+$pdf->SetFont('dejavusans', '', 9);
+
+// Check for PDF template
+$tplidx = null;
+$templateFile = $conf->kundenkarte->dir_output.'/templates/export_template.pdf';
+if (file_exists($templateFile) && is_readable($templateFile)) {
+ try {
+ $pagecount = $pdf->setSourceFile($templateFile);
+ $tplidx = $pdf->importPage(1);
+ } catch (Exception $e) {
+ // Template could not be loaded, continue without
+ $tplidx = null;
+ }
+}
+
+$pdf->AddPage();
+if (!empty($tplidx)) {
+ $pdf->useTemplate($tplidx);
+}
+
+// Compact header - left aligned
+$pdf->SetFont('dejavusans', 'B', 14);
+$pdf->Cell(120, 6, $systemLabel, 0, 1, 'L');
+
+$pdf->SetFont('dejavusans', '', 9);
+$pdf->SetTextColor(80, 80, 80);
+
+// Customer info in one compact block
+$customerInfo = $societe->name;
+if ($contact) {
+ $customerInfo .= ' | '.$contact->getFullName($langs);
+}
+$pdf->Cell(120, 4, $customerInfo, 0, 1, 'L');
+
+// Address
+$address = array();
+if ($societe->address) $address[] = $societe->address;
+if ($societe->zip || $societe->town) $address[] = trim($societe->zip.' '.$societe->town);
+if (!empty($address)) {
+ $pdf->Cell(120, 4, implode(', ', $address), 0, 1, 'L');
+}
+
+// Date and count
+$totalElements = countTreeElements($tree);
+$pdf->Cell(120, 4, dol_print_date(dol_now(), 'day').' | '.$totalElements.' Elemente', 0, 1, 'L');
+
+$pdf->SetTextColor(0, 0, 0);
+$pdf->Ln(2);
+
+// Separator line
+$pdf->SetDrawColor(200, 200, 200);
+$pdf->Line(15, $pdf->GetY(), $pdf->getPageWidth() - 15, $pdf->GetY());
+$pdf->Ln(4);
+
+// Get font size settings
+$fontSettings = array(
+ 'header' => getDolGlobalInt('KUNDENKARTE_PDF_FONT_HEADER', 9),
+ 'content' => getDolGlobalInt('KUNDENKARTE_PDF_FONT_CONTENT', 7),
+ 'fields' => getDolGlobalInt('KUNDENKARTE_PDF_FONT_FIELDS', 7)
+);
+
+// Draw tree
+if (!empty($tree)) {
+ drawTreePdf($pdf, $tree, $typeFieldsMap, $langs, 0, $tplidx, $fontSettings);
+} else {
+ $pdf->SetFont('dejavusans', 'I', 10);
+ $pdf->Cell(0, 10, $langs->trans('NoInstallations'), 0, 1);
+}
+
+/**
+ * Count total elements in tree
+ */
+function countTreeElements($nodes) {
+ $count = 0;
+ if (!empty($nodes)) {
+ foreach ($nodes as $node) {
+ $count++;
+ if (!empty($node->children)) {
+ $count += countTreeElements($node->children);
+ }
+ }
+ }
+ return $count;
+}
+
+// Output PDF
+$filename = 'Export_'.$systemLabel.'_'.dol_sanitizeFileName($societe->name);
+if ($contact) {
+ $filename .= '_'.dol_sanitizeFileName($contact->lastname);
+}
+$filename .= '_'.date('Y-m-d').'.pdf';
+
+$pdf->Output($filename, 'D');
+
+/**
+ * Draw tree recursively in PDF with visual hierarchy
+ */
+function drawTreePdf(&$pdf, $nodes, $typeFieldsMap, $langs, $level = 0, $tplidx = null, $fontSettings = null)
+{
+ // Default font settings if not provided
+ if ($fontSettings === null) {
+ $fontSettings = array('header' => 9, 'content' => 7, 'fields' => 7);
+ }
+
+ $indent = $level * 12;
+ $leftMargin = 15;
+ $pageWidth = $pdf->getPageWidth() - 30;
+ $contentWidth = $pageWidth - $indent;
+
+ // Subtle gray tones - darker for higher levels, lighter for deeper levels
+ $levelColors = array(
+ 0 => array('bg' => array(70, 70, 70), 'border' => array(50, 50, 50)), // Dark gray
+ 1 => array('bg' => array(100, 100, 100), 'border' => array(80, 80, 80)), // Medium dark
+ 2 => array('bg' => array(130, 130, 130), 'border' => array(110, 110, 110)), // Medium
+ 3 => array('bg' => array(150, 150, 150), 'border' => array(130, 130, 130)), // Medium light
+ 4 => array('bg' => array(170, 170, 170), 'border' => array(150, 150, 150)), // Light gray
+ );
+ $colorIndex = min($level, count($levelColors) - 1);
+ $colors = $levelColors[$colorIndex];
+
+ $nodeCount = count($nodes);
+ $nodeIndex = 0;
+
+ foreach ($nodes as $node) {
+ $nodeIndex++;
+ $isLast = ($nodeIndex == $nodeCount);
+
+ // Calculate content height to check page break
+ $estimatedHeight = 12; // Header height
+ $fieldValues = $node->getFieldValues();
+ if (!empty($typeFieldsMap[$node->fk_anlage_type])) {
+ foreach ($typeFieldsMap[$node->fk_anlage_type] as $fieldDef) {
+ if ($fieldDef->field_type === 'header') {
+ $estimatedHeight += 5;
+ } else {
+ $value = isset($fieldValues[$fieldDef->field_code]) ? $fieldValues[$fieldDef->field_code] : '';
+ if ($value !== '') $estimatedHeight += 4;
+ }
+ }
+ }
+ if ($node->note_private) $estimatedHeight += 8;
+ if ($node->image_count > 0 || $node->doc_count > 0) $estimatedHeight += 4;
+
+ // Check if we need a new page
+ if ($pdf->GetY() + $estimatedHeight > 265) {
+ $pdf->AddPage();
+ if (!empty($tplidx)) {
+ $pdf->useTemplate($tplidx);
+ }
+ }
+
+ $startY = $pdf->GetY();
+ $boxX = $leftMargin + $indent;
+
+ // Draw tree connector lines
+ if ($level > 0) {
+ $pdf->SetDrawColor(180, 180, 180);
+ $pdf->SetLineWidth(0.3);
+
+ // Horizontal line to element
+ $lineStartX = $boxX - 8;
+ $lineEndX = $boxX - 2;
+ $lineY = $startY + 4;
+ $pdf->Line($lineStartX, $lineY, $lineEndX, $lineY);
+
+ // Vertical line segment
+ $pdf->Line($lineStartX, $startY - 2, $lineStartX, $lineY);
+ }
+
+ // Draw element box with rounded corners
+ $pdf->SetDrawColor($colors['border'][0], $colors['border'][1], $colors['border'][2]);
+ $pdf->SetLineWidth(0.4);
+
+ // Header bar with color
+ $pdf->SetFillColor($colors['bg'][0], $colors['bg'][1], $colors['bg'][2]);
+ $pdf->RoundedRect($boxX, $startY, $contentWidth, 8, 1.5, '1100', 'DF');
+
+ // Content area with light background
+ $pdf->SetFillColor(250, 250, 250);
+ $pdf->SetDrawColor(220, 220, 220);
+
+ // Element header text (white on colored background)
+ $pdf->SetXY($boxX + 3, $startY + 1.5);
+ $pdf->SetFont('dejavusans', 'B', $fontSettings['header']);
+ $pdf->SetTextColor(255, 255, 255);
+
+ $headerText = $node->label;
+ if ($node->type_label) {
+ $headerText .= ' · '.$node->type_label;
+ }
+ $pdf->Cell($contentWidth - 6, 5, $headerText, 0, 1, 'L');
+
+ $pdf->SetTextColor(0, 0, 0);
+ $contentStartY = $startY + 8;
+ $pdf->SetY($contentStartY);
+
+ // Collect content to measure height
+ $hasContent = false;
+ $contentY = $contentStartY + 2;
+
+ // Draw fields
+ if (!empty($typeFieldsMap[$node->fk_anlage_type])) {
+ foreach ($typeFieldsMap[$node->fk_anlage_type] as $fieldDef) {
+ // Handle header fields as section titles
+ if ($fieldDef->field_type === 'header') {
+ $pdf->SetXY($boxX + 4, $contentY);
+ $pdf->SetFont('dejavusans', 'B', $fontSettings['fields']);
+ $pdf->SetTextColor(100, 100, 100);
+ $pdf->Cell($contentWidth - 8, 4, strtoupper($fieldDef->field_label), 0, 1);
+ $pdf->SetTextColor(0, 0, 0);
+ $contentY += 4;
+ $hasContent = true;
+ continue;
+ }
+
+ $value = isset($fieldValues[$fieldDef->field_code]) ? $fieldValues[$fieldDef->field_code] : '';
+
+ if ($value !== '') {
+ // Format date values
+ if ($fieldDef->field_type === 'date' && $value) {
+ $value = dol_print_date(strtotime($value), 'day');
+ }
+
+ $pdf->SetXY($boxX + 4, $contentY);
+ $pdf->SetFont('dejavusans', '', $fontSettings['fields']);
+ $pdf->SetTextColor(120, 120, 120);
+ $pdf->Cell(30, 3.5, $fieldDef->field_label.':', 0, 0);
+ $pdf->SetFont('dejavusans', '', $fontSettings['content']);
+ $pdf->SetTextColor(50, 50, 50);
+ $pdf->Cell($contentWidth - 38, 3.5, $value, 0, 1);
+ $contentY += 3.5;
+ $hasContent = true;
+ }
+ }
+ }
+
+ // Notes
+ if ($node->note_private) {
+ $pdf->SetXY($boxX + 4, $contentY);
+ $pdf->SetFont('dejavusans', 'I', $fontSettings['content']);
+ $pdf->SetTextColor(100, 100, 100);
+ $noteText = strip_tags($node->note_private);
+ if (strlen($noteText) > 80) {
+ $noteText = substr($noteText, 0, 77).'...';
+ }
+ $pdf->Cell($contentWidth - 8, 3.5, $noteText, 0, 1);
+ $contentY += 3.5;
+ $hasContent = true;
+ }
+
+ // File counts
+ if ($node->image_count > 0 || $node->doc_count > 0) {
+ $pdf->SetXY($boxX + 4, $contentY);
+ $pdf->SetFont('dejavusans', '', $fontSettings['content']);
+ $pdf->SetTextColor(150, 150, 150);
+ $fileInfo = array();
+ if ($node->image_count > 0) {
+ $fileInfo[] = $node->image_count.' '.($node->image_count == 1 ? 'Bild' : 'Bilder');
+ }
+ if ($node->doc_count > 0) {
+ $fileInfo[] = $node->doc_count.' '.($node->doc_count == 1 ? 'Dok.' : 'Dok.');
+ }
+ $pdf->Cell($contentWidth - 8, 3.5, implode(' | ', $fileInfo), 0, 1);
+ $contentY += 3.5;
+ $hasContent = true;
+ }
+
+ // Draw content box if there's content
+ if ($hasContent) {
+ $contentHeight = $contentY - $contentStartY + 2;
+ $pdf->SetDrawColor(220, 220, 220);
+ $pdf->SetFillColor(252, 252, 252);
+ $pdf->RoundedRect($boxX, $contentStartY, $contentWidth, $contentHeight, 1.5, '0011', 'DF');
+
+ // Redraw content on top of box
+ $contentY = $contentStartY + 2;
+
+ if (!empty($typeFieldsMap[$node->fk_anlage_type])) {
+ foreach ($typeFieldsMap[$node->fk_anlage_type] as $fieldDef) {
+ if ($fieldDef->field_type === 'header') {
+ $pdf->SetXY($boxX + 4, $contentY);
+ $pdf->SetFont('dejavusans', 'B', $fontSettings['fields']);
+ $pdf->SetTextColor(100, 100, 100);
+ $pdf->Cell($contentWidth - 8, 4, strtoupper($fieldDef->field_label), 0, 1);
+ $pdf->SetTextColor(0, 0, 0);
+ $contentY += 4;
+ continue;
+ }
+
+ $value = isset($fieldValues[$fieldDef->field_code]) ? $fieldValues[$fieldDef->field_code] : '';
+
+ if ($value !== '') {
+ if ($fieldDef->field_type === 'date' && $value) {
+ $value = dol_print_date(strtotime($value), 'day');
+ }
+
+ $pdf->SetXY($boxX + 4, $contentY);
+ $pdf->SetFont('dejavusans', '', $fontSettings['fields']);
+ $pdf->SetTextColor(120, 120, 120);
+ $pdf->Cell(30, 3.5, $fieldDef->field_label.':', 0, 0);
+ $pdf->SetFont('dejavusans', '', $fontSettings['content']);
+ $pdf->SetTextColor(50, 50, 50);
+ $pdf->Cell($contentWidth - 38, 3.5, $value, 0, 1);
+ $contentY += 3.5;
+ }
+ }
+ }
+
+ if ($node->note_private) {
+ $pdf->SetXY($boxX + 4, $contentY);
+ $pdf->SetFont('dejavusans', 'I', $fontSettings['content']);
+ $pdf->SetTextColor(100, 100, 100);
+ $noteText = strip_tags($node->note_private);
+ if (strlen($noteText) > 80) {
+ $noteText = substr($noteText, 0, 77).'...';
+ }
+ $pdf->Cell($contentWidth - 8, 3.5, $noteText, 0, 1);
+ $contentY += 3.5;
+ }
+
+ if ($node->image_count > 0 || $node->doc_count > 0) {
+ $pdf->SetXY($boxX + 4, $contentY);
+ $pdf->SetFont('dejavusans', '', $fontSettings['content']);
+ $pdf->SetTextColor(150, 150, 150);
+ $fileInfo = array();
+ if ($node->image_count > 0) {
+ $fileInfo[] = $node->image_count.' '.($node->image_count == 1 ? 'Bild' : 'Bilder');
+ }
+ if ($node->doc_count > 0) {
+ $fileInfo[] = $node->doc_count.' '.($node->doc_count == 1 ? 'Dok.' : 'Dok.');
+ }
+ $pdf->Cell($contentWidth - 8, 3.5, implode(' | ', $fileInfo), 0, 1);
+ $contentY += 3.5;
+ }
+
+ $pdf->SetY($contentStartY + $contentHeight + 3);
+ } else {
+ $pdf->SetY($startY + 11);
+ }
+
+ $pdf->SetTextColor(0, 0, 0);
+
+ // Children
+ if (!empty($node->children)) {
+ drawTreePdf($pdf, $node->children, $typeFieldsMap, $langs, $level + 1, $tplidx, $fontSettings);
+ }
+ }
+}
diff --git a/ajax/type_fields.php b/ajax/type_fields.php
new file mode 100644
index 0000000..9c2a6be
--- /dev/null
+++ b/ajax/type_fields.php
@@ -0,0 +1,57 @@
+ array()));
+ exit;
+}
+
+$type = new AnlageType($db);
+if ($type->fetch($typeId) <= 0) {
+ echo json_encode(array('fields' => array()));
+ exit;
+}
+
+$fields = $type->fetchFields();
+
+// Get existing values if editing
+$existingValues = array();
+if ($anlageId > 0) {
+ $anlage = new Anlage($db);
+ if ($anlage->fetch($anlageId) > 0) {
+ $existingValues = $anlage->getFieldValues();
+ }
+}
+
+$result = array('fields' => array());
+
+foreach ($fields as $field) {
+ $fieldData = array(
+ 'code' => $field->field_code,
+ 'label' => $field->field_label,
+ 'type' => $field->field_type,
+ 'options' => $field->field_options,
+ 'required' => (int)$field->required === 1,
+ 'value' => isset($existingValues[$field->field_code]) ? $existingValues[$field->field_code] : ''
+ );
+ $result['fields'][] = $fieldData;
+}
+
+echo json_encode($result);
diff --git a/core/modules/modKundenKarte.class.php b/core/modules/modKundenKarte.class.php
index 6b2e583..b3a6ff8 100755
--- a/core/modules/modKundenKarte.class.php
+++ b/core/modules/modKundenKarte.class.php
@@ -76,7 +76,7 @@ class modKundenKarte extends DolibarrModules
$this->editor_squarred_logo = ''; // Must be image filename into the module/img directory followed with @modulename. Example: 'myimage.png@kundenkarte'
// Possible values for version are: 'development', 'experimental', 'dolibarr', 'dolibarr_deprecated', 'experimental_deprecated' or a version string like 'x.y.z'
- $this->version = '1.2';
+ $this->version = '2.0';
// Url to the file with your last numberversion of this module
//$this->url_last_version = 'http://www.example.com/versionmodule.txt';
diff --git a/js/kundenkarte.js b/js/kundenkarte.js
index fc61913..98d821d 100755
--- a/js/kundenkarte.js
+++ b/js/kundenkarte.js
@@ -376,52 +376,28 @@
html += '
';
- if (data.location) {
- html += ' Standort: ';
- html += '' + this.escapeHtml(data.location) + ' ';
- }
-
- if (data.manufacturer) {
- html += 'Hersteller: ';
- html += '' + this.escapeHtml(data.manufacturer) + ' ';
- }
-
- if (data.model) {
- html += 'Modell: ';
- html += '' + this.escapeHtml(data.model) + ' ';
- }
-
- if (data.serial_number) {
- html += 'Seriennummer: ';
- html += '' + this.escapeHtml(data.serial_number) + ' ';
- }
-
- if (data.power_rating) {
- html += 'Leistung: ';
- html += '' + this.escapeHtml(data.power_rating) + ' ';
- }
-
- if (data.installation_date) {
- html += 'Installiert: ';
- html += '' + this.escapeHtml(data.installation_date) + ' ';
- }
-
- // Dynamic fields (from AJAX)
+ // Dynamic fields only (from PHP data-tooltip attribute)
if (data.fields) {
for (var key in data.fields) {
- if (data.fields.hasOwnProperty(key) && data.fields[key].show_in_hover) {
- html += '' + this.escapeHtml(data.fields[key].label) + ': ';
- html += '' + this.escapeHtml(data.fields[key].value) + ' ';
+ if (data.fields.hasOwnProperty(key)) {
+ var field = data.fields[key];
+ // Handle header fields as section titles
+ if (field.type === 'header') {
+ html += '';
+ } else if (field.value) {
+ html += '' + this.escapeHtml(field.label) + ': ';
+ html += '' + this.escapeHtml(field.value) + ' ';
+ }
}
}
}
html += '
';
- // Notes
- if (data.note) {
+ // Notes (note_html is already sanitized and formatted with
by PHP)
+ if (data.note_html) {
html += '
';
- html += ' ' + this.escapeHtml(data.note);
+ html += ' ' + data.note_html;
html += '
';
}
@@ -444,6 +420,13 @@
return div.innerHTML;
},
+ escapeHtmlPreservingBreaks: function(text) {
+ if (!text) return '';
+ // Convert
to newlines first (for old data with
tags)
+ text = text.replace(/
/gi, '\n');
+ return this.escapeHtml(text);
+ },
+
refresh: function(socId, systemId) {
var $container = $('.kundenkarte-tree[data-system="' + systemId + '"]');
if (!$container.length) return;
@@ -939,12 +922,135 @@
}
};
+ /**
+ * Dynamic Fields Component
+ * Loads and renders type-specific fields when creating/editing anlagen
+ */
+ KundenKarte.DynamicFields = {
+ init: function() {
+ var self = this;
+ var $typeSelect = $('select[name="fk_anlage_type"]');
+ var $container = $('#dynamic_fields');
+
+ if (!$typeSelect.length || !$container.length) return;
+
+ // Load fields when type changes
+ $typeSelect.on('change', function() {
+ self.loadFields($(this).val());
+ });
+
+ // Load initial fields if type is already selected
+ if ($typeSelect.val()) {
+ self.loadFields($typeSelect.val());
+ }
+ },
+
+ loadFields: function(typeId) {
+ var $container = $('#dynamic_fields');
+ if (!typeId) {
+ $container.html('');
+ return;
+ }
+
+ // Get anlage_id if editing or copying
+ var anlageId = $('input[name="anlage_id"]').val() || $('input[name="copy_from"]').val() || 0;
+
+ $.ajax({
+ url: baseUrl + '/custom/kundenkarte/ajax/type_fields.php',
+ data: { type_id: typeId, anlage_id: anlageId },
+ dataType: 'json',
+ success: function(data) {
+ if (data.fields && data.fields.length > 0) {
+ var html = '';
+
+ data.fields.forEach(function(field) {
+ if (field.type === 'header') {
+ // Header row spans both columns with styling
+ html += '
' + KundenKarte.DynamicFields.escapeHtml(field.label) + ' ';
+ } else {
+ html += '
' + KundenKarte.DynamicFields.escapeHtml(field.label);
+ if (field.required) html += ' * ';
+ html += ' ';
+ html += KundenKarte.DynamicFields.renderField(field);
+ html += ' ';
+ }
+ });
+
+ $container.html(html);
+ } else {
+ $container.html('');
+ }
+ }
+ });
+ },
+
+ renderField: function(field) {
+ var name = 'field_' + field.code;
+ var value = field.value || '';
+ var required = field.required ? ' required' : '';
+
+ switch (field.type) {
+ case 'text':
+ return '
';
+
+ case 'textarea':
+ return '
';
+
+ case 'number':
+ var attrs = '';
+ if (field.options) {
+ var opts = field.options.split('|');
+ opts.forEach(function(opt) {
+ var parts = opt.split(':');
+ if (parts.length === 2) {
+ attrs += ' ' + parts[0] + '="' + parts[1] + '"';
+ }
+ });
+ }
+ return '
';
+
+ case 'select':
+ var html = '
';
+ html += '-- Auswählen -- ';
+ if (field.options) {
+ var options = field.options.split('|');
+ options.forEach(function(opt) {
+ var selected = (opt === value) ? ' selected' : '';
+ html += '' + KundenKarte.DynamicFields.escapeHtml(opt) + ' ';
+ });
+ }
+ html += ' ';
+ return html;
+
+ case 'date':
+ var inputId = 'date_' + name.replace(/[^a-zA-Z0-9]/g, '_');
+ return '
' +
+ '
Heute ';
+
+ case 'checkbox':
+ var checked = (value === '1' || value === 'true' || value === 'yes') ? ' checked' : '';
+ return '
';
+
+ default:
+ return '
';
+ }
+ },
+
+ escapeHtml: function(text) {
+ if (!text) return '';
+ var div = document.createElement('div');
+ div.textContent = text;
+ return div.innerHTML;
+ }
+ };
+
// Initialize on DOM ready
$(document).ready(function() {
KundenKarte.Tree.init();
KundenKarte.Favorites.init();
KundenKarte.SystemTabs.init();
KundenKarte.IconPicker.init();
+ KundenKarte.DynamicFields.init();
});
})();
diff --git a/langs/de_DE/kundenkarte.lang b/langs/de_DE/kundenkarte.lang
index f7c77ec..fbd7a01 100755
--- a/langs/de_DE/kundenkarte.lang
+++ b/langs/de_DE/kundenkarte.lang
@@ -178,6 +178,29 @@ ConfigHelpSystems = Systeme verwalten: Gehen Sie zum Tab "Anlagen-Systeme" um ei
ConfigHelpTypes = Element-Typen verwalten: Gehen Sie zum Tab "Element-Typen" um Geraetetypen und Felder zu definieren
SetupSaved = Einstellungen gespeichert
+# PDF Export
+PDFExportTemplate = PDF Export Vorlage
+PDFFontSettings = PDF Schriftgroessen
+PDFFontHeader = Ueberschrift Schriftgroesse
+PDFFontContent = Inhalt Schriftgroesse
+PDFFontFields = Felder Schriftgroesse
+PDFFontHeaderHelp = Schriftgroesse fuer Element-Namen (Standard: 9pt)
+PDFFontContentHelp = Schriftgroesse fuer Feldwerte (Standard: 7pt)
+PDFFontFieldsHelp = Schriftgroesse fuer Feld-Labels (Standard: 7pt)
+PDFTemplate = PDF Vorlage
+CurrentTemplate = Aktuelle Vorlage
+NoTemplateUploaded = Keine Vorlage hochgeladen
+UploadNewTemplate = Neue Vorlage hochladen
+DeleteTemplate = Vorlage loeschen
+ConfirmDeleteTemplate = Moechten Sie die Vorlage wirklich loeschen?
+TemplateUploadSuccess = Vorlage wurde erfolgreich hochgeladen
+TemplateDeleted = Vorlage wurde geloescht
+ErrorOnlyPDFAllowed = Nur PDF-Dateien sind erlaubt
+ErrorUploadFailed = Hochladen fehlgeschlagen
+ErrorNoFileSelected = Keine Datei ausgewaehlt
+PDFTemplateHelp = Laden Sie eine PDF-Datei als Hintergrund/Briefpapier fuer den Export hoch. Die erste Seite wird als Vorlage verwendet.
+ExportTreeAsPDF = Als PDF exportieren
+
# Standard Dolibarr
Save = Speichern
Cancel = Abbrechen
diff --git a/langs/en_US/kundenkarte.lang b/langs/en_US/kundenkarte.lang
index a16a624..a0b2f21 100755
--- a/langs/en_US/kundenkarte.lang
+++ b/langs/en_US/kundenkarte.lang
@@ -161,6 +161,29 @@ ErrorParentNotAllowed = This element cannot be placed under the selected parent
ErrorCannotDeleteSystemType = System types cannot be deleted
ErrorFieldRequired = Required field not filled
+# PDF Export
+PDFExportTemplate = PDF Export Template
+PDFFontSettings = PDF Font Sizes
+PDFFontHeader = Header Font Size
+PDFFontContent = Content Font Size
+PDFFontFields = Field Label Font Size
+PDFFontHeaderHelp = Font size for element names (Default: 9pt)
+PDFFontContentHelp = Font size for field values (Default: 7pt)
+PDFFontFieldsHelp = Font size for field labels (Default: 7pt)
+PDFTemplate = PDF Template
+CurrentTemplate = Current Template
+NoTemplateUploaded = No template uploaded
+UploadNewTemplate = Upload new template
+DeleteTemplate = Delete template
+ConfirmDeleteTemplate = Are you sure you want to delete this template?
+TemplateUploadSuccess = Template has been uploaded successfully
+TemplateDeleted = Template has been deleted
+ErrorOnlyPDFAllowed = Only PDF files are allowed
+ErrorUploadFailed = Upload failed
+ErrorNoFileSelected = No file selected
+PDFTemplateHelp = Upload a PDF file as background/letterhead for export. The first page will be used as template.
+ExportTreeAsPDF = Export as PDF
+
# Standard Dolibarr
Save = Save
Cancel = Cancel
diff --git a/tabs/anlagen.php b/tabs/anlagen.php
index 1298d00..c273ac6 100755
--- a/tabs/anlagen.php
+++ b/tabs/anlagen.php
@@ -150,13 +150,7 @@ if ($action == 'add' && $permissiontoadd) {
$anlage->fk_anlage_type = GETPOSTINT('fk_anlage_type');
$anlage->fk_parent = GETPOSTINT('fk_parent');
$anlage->fk_system = $systemId;
- $anlage->manufacturer = GETPOST('manufacturer', 'alphanohtml');
- $anlage->model = GETPOST('model', 'alphanohtml');
- $anlage->serial_number = GETPOST('serial_number', 'alphanohtml');
- $anlage->power_rating = GETPOST('power_rating', 'alphanohtml');
- $anlage->location = GETPOST('location', 'alphanohtml');
- $anlage->installation_date = dol_mktime(0, 0, 0, GETPOSTINT('installation_datemonth'), GETPOSTINT('installation_dateday'), GETPOSTINT('installation_dateyear'));
- $anlage->note_private = GETPOST('note_private', 'restricthtml');
+ $anlage->note_private = isset($_POST['note_private']) ? $_POST['note_private'] : '';
$anlage->status = 1;
// Get type for system ID
@@ -165,10 +159,11 @@ if ($action == 'add' && $permissiontoadd) {
$anlage->fk_system = $type->fk_system;
}
- // Dynamic fields
+ // All fields come from dynamic fields now
$fieldValues = array();
$fields = $type->fetchFields();
foreach ($fields as $field) {
+ if ($field->field_type === 'header') continue; // Skip headers
$value = GETPOST('field_'.$field->field_code, 'alphanohtml');
if ($value !== '') {
$fieldValues[$field->field_code] = $value;
@@ -192,13 +187,7 @@ if ($action == 'update' && $permissiontoadd) {
$anlage->label = GETPOST('label', 'alphanohtml');
$anlage->fk_anlage_type = GETPOSTINT('fk_anlage_type');
$anlage->fk_parent = GETPOSTINT('fk_parent');
- $anlage->manufacturer = GETPOST('manufacturer', 'alphanohtml');
- $anlage->model = GETPOST('model', 'alphanohtml');
- $anlage->serial_number = GETPOST('serial_number', 'alphanohtml');
- $anlage->power_rating = GETPOST('power_rating', 'alphanohtml');
- $anlage->location = GETPOST('location', 'alphanohtml');
- $anlage->installation_date = dol_mktime(0, 0, 0, GETPOSTINT('installation_datemonth'), GETPOSTINT('installation_dateday'), GETPOSTINT('installation_dateyear'));
- $anlage->note_private = GETPOST('note_private', 'restricthtml');
+ $anlage->note_private = isset($_POST['note_private']) ? $_POST['note_private'] : '';
// Get type for system ID
$type = new AnlageType($db);
@@ -206,10 +195,11 @@ if ($action == 'update' && $permissiontoadd) {
$anlage->fk_system = $type->fk_system;
}
- // Dynamic fields
+ // All fields come from dynamic fields now
$fieldValues = array();
$fields = $type->fetchFields();
foreach ($fields as $field) {
+ if ($field->field_type === 'header') continue; // Skip headers
$value = GETPOST('field_'.$field->field_code, 'alphanohtml');
if ($value !== '') {
$fieldValues[$field->field_code] = $value;
@@ -381,8 +371,8 @@ if ($permissiontoadd) {
}
print '
';
-// Expand/Collapse buttons (only in tree view, not in create/edit/view)
-$isTreeView = !in_array($action, array('create', 'edit', 'view'));
+// Expand/Collapse buttons (only in tree view, not in create/edit/view/copy)
+$isTreeView = !in_array($action, array('create', 'edit', 'view', 'copy'));
if ($isTreeView) {
print '