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 '
'; + print ''; + print ''; + print ''; + print ''; + print ''; + print '
'; + } + } + print ''; print ''; print ''; @@ -391,35 +466,31 @@ if (in_array($action, array('create', 'edit'))) { foreach ($fields as $field) { // Check if we're editing this field - if ($editFieldId == $field->id) { - // Edit row - print ''; - print ''; - print ''; - print ''; - print ''; - print ''; - print ''; + if ($editFieldId == $field->rowid) { + // Edit row - inputs linked to form outside table via form attribute + $formId = 'editfield_'.$field->rowid; - print ''; - print ''; - print ''; + print ''; + print ''; + print ''; - print ''; - print ''; - print ''; - print ''; - print ''; + print ''; + print ''; + print ''; + print ''; + print ''; print ''; print ''; - print ''; print ''; } else { // Display row @@ -434,14 +505,16 @@ if (in_array($action, array('create', 'edit'))) { print ''; print ''; print ''; print ''; } @@ -556,10 +629,13 @@ if (in_array($action, array('create', 'edit'))) { print ''; print ''; print ''; diff --git a/admin/setup.php b/admin/setup.php index e5d012c..c68ffc1 100755 --- a/admin/setup.php +++ b/admin/setup.php @@ -48,10 +48,56 @@ if (!$user->admin && !$user->hasRight('kundenkarte', 'admin')) { // Parameters $action = GETPOST('action', 'aZ09'); +// Directory for PDF templates +$uploadDir = $conf->kundenkarte->dir_output.'/templates'; + /* * Actions */ +// Handle PDF template upload +if ($action == 'upload_template') { + $error = 0; + + if (!empty($_FILES['pdf_template']['name'])) { + // Check file type + $fileInfo = pathinfo($_FILES['pdf_template']['name']); + if (strtolower($fileInfo['extension']) !== 'pdf') { + setEventMessages($langs->trans("ErrorOnlyPDFAllowed"), null, 'errors'); + $error++; + } + + if (!$error) { + // Create directory if not exists + if (!is_dir($uploadDir)) { + dol_mkdir($uploadDir); + } + + // Save template as fixed name + $targetFile = $uploadDir.'/export_template.pdf'; + + if (move_uploaded_file($_FILES['pdf_template']['tmp_name'], $targetFile)) { + dolibarr_set_const($db, 'KUNDENKARTE_PDF_TEMPLATE', 'export_template.pdf', 'chaine', 0, '', $conf->entity); + setEventMessages($langs->trans("TemplateUploadSuccess"), null, 'mesgs'); + } else { + setEventMessages($langs->trans("ErrorUploadFailed"), null, 'errors'); + } + } + } else { + setEventMessages($langs->trans("ErrorNoFileSelected"), null, 'errors'); + } +} + +// Handle template deletion +if ($action == 'delete_template') { + $templateFile = $uploadDir.'/export_template.pdf'; + if (file_exists($templateFile)) { + unlink($templateFile); + dolibarr_set_const($db, 'KUNDENKARTE_PDF_TEMPLATE', '', 'chaine', 0, '', $conf->entity); + setEventMessages($langs->trans("TemplateDeleted"), null, 'mesgs'); + } +} + if ($action == 'update') { $error = 0; @@ -71,6 +117,11 @@ if ($action == 'update') { $error++; } + // PDF font size settings + dolibarr_set_const($db, 'KUNDENKARTE_PDF_FONT_HEADER', GETPOSTINT('KUNDENKARTE_PDF_FONT_HEADER'), 'chaine', 0, '', $conf->entity); + dolibarr_set_const($db, 'KUNDENKARTE_PDF_FONT_CONTENT', GETPOSTINT('KUNDENKARTE_PDF_FONT_CONTENT'), 'chaine', 0, '', $conf->entity); + dolibarr_set_const($db, 'KUNDENKARTE_PDF_FONT_FIELDS', GETPOSTINT('KUNDENKARTE_PDF_FONT_FIELDS'), 'chaine', 0, '', $conf->entity); + if (!$error) { setEventMessages($langs->trans("SetupSaved"), null, 'mesgs'); } else { @@ -139,6 +190,62 @@ print ''; print '
'.$langs->trans('FieldCode').'
show_in_tree ? ' checked' : '').'>show_in_hover ? ' checked' : '').'>required ? ' checked' : '').'>show_in_tree ? ' checked' : '').'>show_in_hover ? ' checked' : '').'>required ? ' checked' : '').'>'; - print ''; - print ''; + print '
'; + print ''; + print ''; + print '
'; print '
'.$field->position.''; if ($field->active) { - print ''.img_picto($langs->trans('Enabled'), 'switch_on').''; + print ''.img_picto($langs->trans('Enabled'), 'switch_on').''; } else { - print ''.img_picto($langs->trans('Disabled'), 'switch_off').''; + print ''.img_picto($langs->trans('Disabled'), 'switch_off').''; } print ''; - print ''.img_edit().''; - print ' '.img_delete().''; + print '
'; + print ''; + print ''; + print '
'; print '
'; - print ''.img_edit().''; + print '
'; + print ''; + print ''; if (!$type->is_system) { - print ' '.img_delete().''; + print ''; } + print '
'; print '
'; +// PDF Font Size Settings +print '

'; +print '
'.$langs->trans("PDFFontSettings").'
'; +print '

'; + +print ''; +print ''; +print ''; +print ''; +print ''; +print ''; + +// Header font size +print ''; +print ''; +print ''; +print ''; +print ''; + +// Content font size +print ''; +print ''; +print ''; +print ''; +print ''; + +// Field label font size +print ''; +print ''; +print ''; +print ''; +print ''; + +print '
'.$langs->trans("Parameter").''.$langs->trans("Value").''.$langs->trans("Description").'
'.$langs->trans("PDFFontHeader").''; +print ''; +print ''.$langs->trans("PDFFontHeaderHelp").'
'.$langs->trans("PDFFontContent").''; +print ''; +print ''.$langs->trans("PDFFontContentHelp").'
'.$langs->trans("PDFFontFields").''; +print ''; +print ''.$langs->trans("PDFFontFieldsHelp").'
'; + print '
'; print '
'; print ''; @@ -146,6 +253,52 @@ print '
'; print ''; +// PDF Template Section +print '

'; +print '
'.$langs->trans("PDFExportTemplate").'
'; +print '

'; + +$templateFile = $uploadDir.'/export_template.pdf'; +$hasTemplate = file_exists($templateFile); + +print ''; +print ''; +print ''; +print ''; + +print ''; +print ''; +print ''; +print ''; + +print ''; +print ''; +print ''; +print ''; + +print '
'.$langs->trans("PDFTemplate").'
'.$langs->trans("CurrentTemplate").''; +if ($hasTemplate) { + print ''; + print ' export_template.pdf'; + print ''; + print ' ('.dol_print_size(filesize($templateFile)).')'; + print '

'; + print ''; + print ' '.$langs->trans("DeleteTemplate"); + print ''; +} else { + print ''.$langs->trans("NoTemplateUploaded").''; +} +print '
'.$langs->trans("UploadNewTemplate").''; +print '
'; +print ''; +print ''; +print ''; +print ' '; +print '
'; +print '
'.$langs->trans("PDFTemplateHelp").''; +print '
'; + // Info section print '
'; 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 += '' + this.escapeHtml(field.label) + ''; + } 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 = ''; + return html; + + case 'date': + var inputId = 'date_' + name.replace(/[^a-zA-Z0-9]/g, '_'); + return '' + + ''; + + 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 '
'; print ''; + if ($systemId > 0) { + $exportUrl = dol_buildpath('/kundenkarte/ajax/export_tree_pdf.php', 1).'?socid='.$id.'&system='.$systemId; + print ''; + print ' PDF Export'; + print ''; + } print '
'; } @@ -428,8 +424,8 @@ if (empty($customerSystems)) { print ''; } elseif ($systemId > 0) { // Show form or tree for selected system - if (in_array($action, array('create', 'edit', 'view'))) { - // Load element for edit/view + if (in_array($action, array('create', 'edit', 'view', 'copy'))) { + // Load element for edit/view/copy if ($action != 'create' && $anlageId > 0) { $anlage->fetch($anlageId); $type = new AnlageType($db); @@ -451,45 +447,26 @@ if (empty($customerSystems)) { print ''.$langs->trans('Type').''; print ''.dol_escape_htmltag($anlage->type_label).''; - if ($anlage->manufacturer) { - print ''.$langs->trans('FieldManufacturer').''; - print ''.dol_escape_htmltag($anlage->manufacturer).''; - } - if ($anlage->model) { - print ''.$langs->trans('FieldModel').''; - print ''.dol_escape_htmltag($anlage->model).''; - } - if ($anlage->serial_number) { - print ''.$langs->trans('FieldSerialNumber').''; - print ''.dol_escape_htmltag($anlage->serial_number).''; - } - if ($anlage->power_rating) { - print ''.$langs->trans('FieldPowerRating').''; - print ''.dol_escape_htmltag($anlage->power_rating).''; - } - if ($anlage->location) { - print ''.$langs->trans('FieldLocation').''; - print ''.dol_escape_htmltag($anlage->location).''; - } - if ($anlage->installation_date) { - print ''.$langs->trans('FieldInstallationDate').''; - print ''.dol_print_date($anlage->installation_date, 'day').''; - } - - // Dynamic fields + // Dynamic fields - all fields come from type definition $fieldValues = $anlage->getFieldValues(); - foreach ($type->fields as $field) { - if (isset($fieldValues[$field->field_code]) && $fieldValues[$field->field_code] !== '') { - print ''.dol_escape_htmltag($field->field_label).''; - $value = $fieldValues[$field->field_code]; - // For select fields, show the label - if ($field->field_type == 'select' && $field->field_options) { - $options = json_decode($field->field_options, true); - if (isset($options['options'][$value])) { - $value = $options['options'][$value]; + $typeFieldsList = $type->fetchFields(); + foreach ($typeFieldsList as $field) { + if ($field->field_type === 'header') { + // Section header + print ''.dol_escape_htmltag($field->field_label).''; + } else { + $value = isset($fieldValues[$field->field_code]) ? $fieldValues[$field->field_code] : ''; + if ($value !== '') { + print ''.dol_escape_htmltag($field->field_label).''; + // Format date fields + if ($field->field_type === 'date' && $value) { + print ''.dol_print_date(strtotime($value), 'day').''; + } elseif ($field->field_type === 'checkbox') { + print ''.($value ? $langs->trans('Yes') : $langs->trans('No')).''; + } else { + print ''.dol_escape_htmltag($value).''; } } - print ''.dol_escape_htmltag($value).''; } } @@ -498,6 +475,18 @@ if (empty($customerSystems)) { print ''.dol_htmlentitiesbr($anlage->note_private).''; } + // Creation date + if ($anlage->date_creation) { + print ''.$langs->trans('DateCreation').''; + print ''.dol_print_date($anlage->date_creation, 'dayhour').''; + } + + // Last modification date + if ($anlage->tms && $anlage->tms != $anlage->date_creation) { + print ''.$langs->trans('DateLastModification').''; + print ''.dol_print_date($anlage->tms, 'dayhour').''; + } + print ''; // Files section @@ -556,6 +545,7 @@ if (empty($customerSystems)) { print '
'; if ($permissiontoadd) { print ''.$langs->trans('Modify').''; + print ''.$langs->trans('Copy').''; } if ($permissiontodelete) { print ''.$langs->trans('Delete').''; @@ -564,27 +554,41 @@ if (empty($customerSystems)) { print '
'; } else { - // Create/Edit form - $formAction = ($action == 'create') ? 'add' : 'update'; + // Create/Edit/Copy form + $isEdit = ($action == 'edit'); + $isCopy = ($action == 'copy'); + $formAction = $isEdit ? 'update' : 'add'; + print '
'; print ''; print ''; - if ($action == 'edit') { + if ($isEdit) { print ''; } + if ($isCopy) { + print ''; + } - print ''; + print '
'; // Label + $labelValue = ''; + if ($isEdit) { + $labelValue = $anlage->label; + } elseif ($isCopy) { + $labelValue = $anlage->label.' (Kopie)'; + } else { + $labelValue = GETPOST('label'); + } print ''; - print ''; + print ''; // Type print ''; print ''; - // Parent + // Parent - for copy, use same parent as original $tree = $anlage->fetchTree($id, $systemId); + $selectedParent = $isEdit || $isCopy ? $anlage->fk_parent : $parentId; + $excludeId = $isEdit ? $anlageId : 0; print ''; print ''; - // Manufacturer - print ''; - print ''; + // Dynamic fields will be inserted here via JavaScript + print ''; - // Model - print ''; - print ''; - - // Serial number - print ''; - print ''; - - // Power rating - print ''; - print ''; - - // Location - print ''; - print ''; - - // Installation date - print ''; - print ''; - - // Notes - print ''; - print ''; + // Notes (always at the end) + print ''; + $noteValue = ($isEdit || $isCopy) ? $anlage->note_private : (isset($_POST['note_private']) ? $_POST['note_private'] : ''); + print ''; print '
'.$langs->trans('Label').'
'.$langs->trans('Type').''; @@ -593,47 +597,26 @@ if (empty($customerSystems)) { } print '
'.$langs->trans('SelectParent').'
'.$langs->trans('FieldManufacturer').'
'.$langs->trans('FieldModel').'
'.$langs->trans('FieldSerialNumber').'
'.$langs->trans('FieldPowerRating').'
'.$langs->trans('FieldLocation').'
'.$langs->trans('FieldInstallationDate').''.$form->selectDate($action == 'edit' ? $anlage->installation_date : '', 'installation_date', 0, 0, 1, '', 1, 1).'
'.$langs->trans('FieldNotes').'
'.$langs->trans('FieldNotes').'
'; - // Dynamic fields will be loaded via JavaScript based on type selection - print '
'; - print '
'; print ''; print ' '.$langs->trans('Cancel').''; @@ -657,9 +640,23 @@ if (empty($customerSystems)) { // Load tree $tree = $anlage->fetchTree($id, $systemId); + // Pre-load all type fields for tooltip and tree display + $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; + } + $db->free($resql); + } + if (!empty($tree)) { print '
'; - printTree($tree, $id, $systemId, $permissiontoadd, $permissiontodelete, $langs); + printTree($tree, $id, $systemId, $permissiontoadd, $permissiontodelete, $langs, 0, $typeFieldsMap); print '
'; } else { print '
'.$langs->trans('NoInstallations').'
'; @@ -680,24 +677,65 @@ $db->close(); /** * Print tree recursively */ -function printTree($nodes, $socid, $systemId, $canEdit, $canDelete, $langs, $level = 0) +function printTree($nodes, $socid, $systemId, $canEdit, $canDelete, $langs, $level = 0, $typeFieldsMap = array()) { foreach ($nodes as $node) { $hasChildren = !empty($node->children); + $fieldValues = $node->getFieldValues(); - // Build tooltip data for hover on icon + // Build tooltip data - only label, type and dynamic fields $tooltipData = array( 'label' => $node->label, 'type' => $node->type_label, - 'location' => $node->location, - 'manufacturer' => $node->manufacturer, - 'model' => $node->model, - 'serial_number' => $node->serial_number, - 'power_rating' => $node->power_rating, - 'installation_date' => $node->installation_date ? dol_print_date($node->installation_date, 'day') : '', - 'note' => $node->note_private, + 'note_html' => $node->note_private ? nl2br(htmlspecialchars($node->note_private, ENT_QUOTES, 'UTF-8')) : '', + 'fields' => array() ); + // Collect fields for tooltip (show_in_hover) and tree label (show_in_tree) + $treeInfo = array(); + + if (!empty($typeFieldsMap[$node->fk_anlage_type])) { + foreach ($typeFieldsMap[$node->fk_anlage_type] as $fieldDef) { + // Handle header fields + if ($fieldDef->field_type === 'header') { + if ($fieldDef->show_in_hover) { + $tooltipData['fields'][$fieldDef->field_code] = array( + 'label' => $fieldDef->field_label, + 'value' => '', + 'type' => 'header' + ); + } + continue; + } + + $value = isset($fieldValues[$fieldDef->field_code]) ? $fieldValues[$fieldDef->field_code] : ''; + + // Add to tooltip if show_in_hover + if ($fieldDef->show_in_hover && $value !== '') { + // Format date values + $displayValue = $value; + if ($fieldDef->field_type === 'date' && $value) { + $displayValue = dol_print_date(strtotime($value), 'day'); + } + $tooltipData['fields'][$fieldDef->field_code] = array( + 'label' => $fieldDef->field_label, + 'value' => $displayValue, + 'type' => $fieldDef->field_type + ); + } + + // Add to tree label info if show_in_tree + if ($fieldDef->show_in_tree && $value !== '') { + // Format date for tree info too + if ($fieldDef->field_type === 'date' && $value) { + $treeInfo[] = dol_print_date(strtotime($value), 'day'); + } else { + $treeInfo[] = $value; + } + } + } + } + print '
'; print '
'; @@ -712,18 +750,11 @@ function printTree($nodes, $socid, $systemId, $canEdit, $canDelete, $langs, $lev $picto = $node->type_picto ? $node->type_picto : 'fa-cube'; print ''.kundenkarte_render_icon($picto).''; - // Label with manufacturer/power in parentheses + file indicators + // Label with tree info in parentheses + file indicators $viewUrl = $_SERVER['PHP_SELF'].'?id='.$socid.'&system='.$systemId.'&action=view&anlage_id='.$node->id; print ''.dol_escape_htmltag($node->label); - $labelInfo = array(); - if ($node->manufacturer) { - $labelInfo[] = $node->manufacturer; - } - if ($node->power_rating) { - $labelInfo[] = $node->power_rating; - } - if (!empty($labelInfo)) { - print ' ('.dol_escape_htmltag(implode(', ', $labelInfo)).')'; + if (!empty($treeInfo)) { + print ' ('.dol_escape_htmltag(implode(', ', $treeInfo)).')'; } // File indicators - directly after parentheses if ($node->image_count > 0 || $node->doc_count > 0) { @@ -754,17 +785,13 @@ function printTree($nodes, $socid, $systemId, $canEdit, $canDelete, $langs, $lev print ''.dol_escape_htmltag($typeDisplay).''; } - // Location with icon - if ($node->location) { - print ' '.dol_escape_htmltag($node->location).''; - } - // Actions print ''; print ''; if ($canEdit) { print ''; print ''; + print ''; } if ($canDelete) { print ''; @@ -776,7 +803,7 @@ function printTree($nodes, $socid, $systemId, $canEdit, $canDelete, $langs, $lev // Children if ($hasChildren) { print '
'; - printTree($node->children, $socid, $systemId, $canEdit, $canDelete, $langs, $level + 1); + printTree($node->children, $socid, $systemId, $canEdit, $canDelete, $langs, $level + 1, $typeFieldsMap); print '
'; } diff --git a/tabs/contact_anlagen.php b/tabs/contact_anlagen.php index 4cddb91..e8ce4b5 100644 --- a/tabs/contact_anlagen.php +++ b/tabs/contact_anlagen.php @@ -157,7 +157,7 @@ if ($action == 'add' && $permissiontoadd) { $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 @@ -199,7 +199,7 @@ if ($action == 'update' && $permissiontoadd) { $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); @@ -382,8 +382,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 '
'; print ''; + if ($systemId > 0) { + $exportUrl = dol_buildpath('/kundenkarte/ajax/export_tree_pdf.php', 1).'?socid='.$object->socid.'&contactid='.$id.'&system='.$systemId; + print ''; + print ' PDF Export'; + print ''; + } print '
'; } @@ -429,8 +435,8 @@ if (empty($customerSystems)) { print '
'; } elseif ($systemId > 0) { // Show form or tree for selected system - if (in_array($action, array('create', 'edit', 'view'))) { - // Load element for edit/view + if (in_array($action, array('create', 'edit', 'view', 'copy'))) { + // Load element for edit/view/copy if ($action != 'create' && $anlageId > 0) { $anlage->fetch($anlageId); $type = new AnlageType($db); @@ -483,13 +489,6 @@ if (empty($customerSystems)) { if (isset($fieldValues[$field->field_code]) && $fieldValues[$field->field_code] !== '') { print ''.dol_escape_htmltag($field->field_label).''; $value = $fieldValues[$field->field_code]; - // For select fields, show the label - if ($field->field_type == 'select' && $field->field_options) { - $options = json_decode($field->field_options, true); - if (isset($options['options'][$value])) { - $value = $options['options'][$value]; - } - } print ''.dol_escape_htmltag($value).''; } } @@ -499,6 +498,18 @@ if (empty($customerSystems)) { print ''.dol_htmlentitiesbr($anlage->note_private).''; } + // Creation date + if ($anlage->date_creation) { + print ''.$langs->trans('DateCreation').''; + print ''.dol_print_date($anlage->date_creation, 'dayhour').''; + } + + // Last modification date + if ($anlage->tms && $anlage->tms != $anlage->date_creation) { + print ''.$langs->trans('DateLastModification').''; + print ''.dol_print_date($anlage->tms, 'dayhour').''; + } + print ''; // Files section @@ -557,6 +568,7 @@ if (empty($customerSystems)) { print '
'; if ($permissiontoadd) { print ''.$langs->trans('Modify').''; + print ''.$langs->trans('Copy').''; } if ($permissiontodelete) { print ''.$langs->trans('Delete').''; @@ -565,27 +577,41 @@ if (empty($customerSystems)) { print '
'; } else { - // Create/Edit form - $formAction = ($action == 'create') ? 'add' : 'update'; + // Create/Edit/Copy form + $isEdit = ($action == 'edit'); + $isCopy = ($action == 'copy'); + $formAction = $isEdit ? 'update' : 'add'; + print ''; print ''; print ''; - if ($action == 'edit') { + if ($isEdit) { print ''; } + if ($isCopy) { + print ''; + } - print ''; + print '
'; // Label + $labelValue = ''; + if ($isEdit) { + $labelValue = $anlage->label; + } elseif ($isCopy) { + $labelValue = $anlage->label.' (Kopie)'; + } else { + $labelValue = GETPOST('label'); + } print ''; - print ''; + print ''; // Type print ''; print ''; print ''; - // Manufacturer - print ''; - print ''; + // Dynamic fields will be inserted here via JavaScript + print ''; - // Model - print ''; - print ''; - - // Serial number - print ''; - print ''; - - // Power rating - print ''; - print ''; - - // Location - print ''; - print ''; - - // Installation date - print ''; - print ''; - - // Notes - print ''; - print ''; + // Notes (always at the end) + print ''; + $noteValue = ($isEdit || $isCopy) ? $anlage->note_private : (isset($_POST['note_private']) ? $_POST['note_private'] : ''); + print ''; print '
'.$langs->trans('Label').'
'.$langs->trans('Type').''; @@ -596,45 +622,24 @@ if (empty($customerSystems)) { // Parent (uses contact-specific tree) $tree = $anlage->fetchTreeByContact($object->socid, $id, $systemId); + $selectedParent = ($isEdit || $isCopy) ? $anlage->fk_parent : $parentId; + $excludeId = $isEdit ? $anlageId : 0; print '
'.$langs->trans('SelectParent').'
'.$langs->trans('FieldManufacturer').'
'.$langs->trans('FieldModel').'
'.$langs->trans('FieldSerialNumber').'
'.$langs->trans('FieldPowerRating').'
'.$langs->trans('FieldLocation').'
'.$langs->trans('FieldInstallationDate').''.$form->selectDate($action == 'edit' ? $anlage->installation_date : '', 'installation_date', 0, 0, 1, '', 1, 1).'
'.$langs->trans('FieldNotes').'
'.$langs->trans('FieldNotes').'
'; - // Dynamic fields will be loaded via JavaScript based on type selection - print '
'; - print '
'; print ''; print ' '.$langs->trans('Cancel').''; @@ -658,9 +663,23 @@ if (empty($customerSystems)) { // Load tree for this contact $tree = $anlage->fetchTreeByContact($object->socid, $id, $systemId); + // Pre-load all type fields for tooltip and tree display + $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; + } + $db->free($resql); + } + if (!empty($tree)) { print '
'; - printTree($tree, $id, $systemId, $permissiontoadd, $permissiontodelete, $langs); + printTree($tree, $id, $systemId, $permissiontoadd, $permissiontodelete, $langs, 0, $typeFieldsMap); print '
'; } else { print '
'.$langs->trans('NoInstallations').'
'; @@ -681,24 +700,64 @@ $db->close(); /** * Print tree recursively */ -function printTree($nodes, $contactid, $systemId, $canEdit, $canDelete, $langs, $level = 0) +function printTree($nodes, $contactid, $systemId, $canEdit, $canDelete, $langs, $level = 0, $typeFieldsMap = array()) { foreach ($nodes as $node) { $hasChildren = !empty($node->children); - // Build tooltip data for hover on icon + // Build tooltip data for hover on icon (only dynamic fields now) $tooltipData = array( 'label' => $node->label, 'type' => $node->type_label, - 'location' => $node->location, - 'manufacturer' => $node->manufacturer, - 'model' => $node->model, - 'serial_number' => $node->serial_number, - 'power_rating' => $node->power_rating, - 'installation_date' => $node->installation_date ? dol_print_date($node->installation_date, 'day') : '', - 'note' => $node->note_private, + 'note_html' => $node->note_private ? nl2br(htmlspecialchars($node->note_private, ENT_QUOTES, 'UTF-8')) : '', + 'fields' => array() ); + // Collect dynamic fields for tooltip (show_in_hover) and tree label (show_in_tree) + $treeInfoParts = array(); + if (!empty($typeFieldsMap[$node->fk_anlage_type])) { + $fieldValues = $node->getFieldValues(); + foreach ($typeFieldsMap[$node->fk_anlage_type] as $fieldDef) { + // Handle header fields + if ($fieldDef->field_type === 'header') { + if ($fieldDef->show_in_hover) { + $tooltipData['fields'][$fieldDef->field_code] = array( + 'label' => $fieldDef->field_label, + 'value' => '', + 'type' => 'header' + ); + } + continue; + } + + $value = isset($fieldValues[$fieldDef->field_code]) ? $fieldValues[$fieldDef->field_code] : ''; + + // Add to tooltip if show_in_hover + if ($fieldDef->show_in_hover && $value !== '') { + // Format date values + $displayValue = $value; + if ($fieldDef->field_type === 'date' && $value) { + $displayValue = dol_print_date(strtotime($value), 'day'); + } + $tooltipData['fields'][$fieldDef->field_code] = array( + 'label' => $fieldDef->field_label, + 'value' => $displayValue, + 'type' => $fieldDef->field_type + ); + } + + // Add to tree label info if show_in_tree + if ($fieldDef->show_in_tree && $value !== '') { + // Format date for tree info too + if ($fieldDef->field_type === 'date' && $value) { + $treeInfoParts[] = dol_print_date(strtotime($value), 'day'); + } else { + $treeInfoParts[] = $value; + } + } + } + } + print '
'; print '
'; @@ -716,15 +775,8 @@ function printTree($nodes, $contactid, $systemId, $canEdit, $canDelete, $langs, // Label with manufacturer/power in parentheses + file indicators $viewUrl = $_SERVER['PHP_SELF'].'?id='.$contactid.'&system='.$systemId.'&action=view&anlage_id='.$node->id; print ''.dol_escape_htmltag($node->label); - $labelInfo = array(); - if ($node->manufacturer) { - $labelInfo[] = $node->manufacturer; - } - if ($node->power_rating) { - $labelInfo[] = $node->power_rating; - } - if (!empty($labelInfo)) { - print ' ('.dol_escape_htmltag(implode(', ', $labelInfo)).')'; + if (!empty($treeInfoParts)) { + print ' ('.dol_escape_htmltag(implode(', ', $treeInfoParts)).')'; } // File indicators - directly after parentheses if ($node->image_count > 0 || $node->doc_count > 0) { @@ -755,17 +807,13 @@ function printTree($nodes, $contactid, $systemId, $canEdit, $canDelete, $langs, print ''.dol_escape_htmltag($typeDisplay).''; } - // Location with icon - if ($node->location) { - print ' '.dol_escape_htmltag($node->location).''; - } - // Actions print ''; print ''; if ($canEdit) { print ''; print ''; + print ''; } if ($canDelete) { print ''; @@ -777,7 +825,7 @@ function printTree($nodes, $contactid, $systemId, $canEdit, $canDelete, $langs, // Children if ($hasChildren) { print '
'; - printTree($node->children, $contactid, $systemId, $canEdit, $canDelete, $langs, $level + 1); + printTree($node->children, $contactid, $systemId, $canEdit, $canDelete, $langs, $level + 1, $typeFieldsMap); print '
'; }