loadLangs(array('companies', 'kundenkarte@kundenkarte'));
// Get parameters
// Support both 'id' and 'contactid' for compatibility with Dolibarr's contact navigation arrows
$id = GETPOSTINT('id');
if ($id <= 0) {
$id = GETPOSTINT('contactid');
}
$action = GETPOST('action', 'aZ09');
$confirm = GETPOST('confirm', 'alpha');
$systemId = GETPOSTINT('system');
$anlageId = GETPOSTINT('anlage_id');
$parentId = GETPOSTINT('parent_id');
// Security check
if (!$user->hasRight('kundenkarte', 'read')) {
accessforbidden();
}
// Initialize objects
$object = new Contact($db);
$form = new Form($db);
$anlage = new Anlage($db);
$anlageType = new AnlageType($db);
// Load contact - id is required
if ($id <= 0) {
// If no id, try to get it from anlage_id
if ($anlageId > 0) {
$tmpAnlage = new Anlage($db);
if ($tmpAnlage->fetch($anlageId) > 0) {
$id = $tmpAnlage->fk_contact;
if ($id > 0) {
// Redirect to include id in URL for proper navigation
$redirectUrl = $_SERVER['PHP_SELF'].'?id='.$id;
if ($systemId > 0) $redirectUrl .= '&system='.$systemId;
if ($action) $redirectUrl .= '&action='.urlencode($action);
if ($anlageId > 0) $redirectUrl .= '&anlage_id='.$anlageId;
header('Location: '.$redirectUrl);
exit;
}
}
}
// Still no id - show error
accessforbidden($langs->trans('ErrorRecordNotFound'));
}
$result = $object->fetch($id);
if ($result <= 0) {
dol_print_error($db, $object->error);
exit;
}
$permissiontoread = $user->hasRight('kundenkarte', 'read');
$permissiontoadd = $user->hasRight('kundenkarte', 'write');
$permissiontodelete = $user->hasRight('kundenkarte', 'delete');
// Load ALL available systems (from dictionary)
$allSystems = array();
$sql = "SELECT rowid, code, label, picto, color FROM ".MAIN_DB_PREFIX."c_kundenkarte_anlage_system WHERE active = 1 ORDER BY position ASC";
$resql = $db->query($sql);
if ($resql) {
while ($obj = $db->fetch_object($resql)) {
$allSystems[$obj->rowid] = $obj;
}
}
// Load systems ENABLED for this contact
$customerSystems = array();
$sql = "SELECT ss.rowid, ss.fk_system, s.code, s.label, s.picto, s.color
FROM ".MAIN_DB_PREFIX."kundenkarte_societe_system ss
JOIN ".MAIN_DB_PREFIX."c_kundenkarte_anlage_system s ON s.rowid = ss.fk_system
WHERE ss.fk_soc = ".((int) $object->socid)." AND ss.fk_contact = ".((int) $id)." AND ss.active = 1 AND s.active = 1
AND s.code != 'GLOBAL'
ORDER BY s.position ASC";
$resql = $db->query($sql);
if ($resql) {
while ($obj = $db->fetch_object($resql)) {
$customerSystems[$obj->fk_system] = $obj;
}
}
// Default to first enabled system if not specified
if (empty($systemId) && !empty($customerSystems)) {
$systemId = array_key_first($customerSystems);
}
/*
* Actions
*/
// Add system to contact
if ($action == 'add_system' && $permissiontoadd) {
$newSystemId = GETPOSTINT('new_system_id');
if ($newSystemId > 0 && !isset($customerSystems[$newSystemId])) {
$sql = "INSERT INTO ".MAIN_DB_PREFIX."kundenkarte_societe_system";
$sql .= " (entity, fk_soc, fk_contact, fk_system, date_creation, fk_user_creat, active)";
$sql .= " VALUES (".$conf->entity.", ".((int) $object->socid).", ".((int) $id).", ".((int) $newSystemId).", NOW(), ".((int) $user->id).", 1)";
$result = $db->query($sql);
if ($result) {
setEventMessages($langs->trans('SystemAdded'), null, 'mesgs');
header('Location: '.$_SERVER['PHP_SELF'].'?id='.$id.'&system='.$newSystemId);
exit;
} else {
setEventMessages($db->lasterror(), null, 'errors');
}
}
$action = '';
}
// Remove system from contact
if ($action == 'confirm_remove_system' && $confirm == 'yes' && $permissiontodelete) {
$removeSystemId = GETPOSTINT('remove_system_id');
if ($removeSystemId > 0) {
// Check if system has any elements for this contact
$sql = "SELECT COUNT(*) as cnt FROM ".MAIN_DB_PREFIX."kundenkarte_anlage WHERE fk_soc = ".((int) $object->socid)." AND fk_contact = ".((int) $id)." AND fk_system = ".((int) $removeSystemId);
$resql = $db->query($sql);
$obj = $db->fetch_object($resql);
if ($obj->cnt > 0) {
setEventMessages($langs->trans('ErrorSystemHasElements'), null, 'errors');
} else {
$sql = "DELETE FROM ".MAIN_DB_PREFIX."kundenkarte_societe_system WHERE fk_soc = ".((int) $object->socid)." AND fk_contact = ".((int) $id)." AND fk_system = ".((int) $removeSystemId);
$db->query($sql);
setEventMessages($langs->trans('SystemRemoved'), null, 'mesgs');
// Switch to another system or none
unset($customerSystems[$removeSystemId]);
if (!empty($customerSystems)) {
$systemId = array_key_first($customerSystems);
} else {
$systemId = 0;
}
}
}
header('Location: '.$_SERVER['PHP_SELF'].'?id='.$id.($systemId ? '&system='.$systemId : ''));
exit;
}
if ($action == 'add' && $permissiontoadd) {
$anlage->label = GETPOST('label', 'alphanohtml');
$anlage->fk_soc = $object->socid;
$anlage->fk_contact = $id;
$anlage->fk_anlage_type = GETPOSTINT('fk_anlage_type');
$anlage->fk_parent = GETPOSTINT('fk_parent');
$anlage->fk_system = $systemId;
$anlage->fk_product = GETPOSTINT('fk_product') > 0 ? GETPOSTINT('fk_product') : null;
$anlage->note_private = isset($_POST['note_private']) ? $_POST['note_private'] : '';
$anlage->status = 1;
// Get type - but keep current system for GLOBAL types (buildings)
$type = new AnlageType($db);
if ($type->fetch($anlage->fk_anlage_type) > 0) {
// Only change system if type is NOT a global type
// Global types (buildings) should stay in the current system
if ($type->system_code !== 'GLOBAL') {
$anlage->fk_system = $type->fk_system;
}
}
// 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;
}
}
$anlage->setFieldValues($fieldValues);
$result = $anlage->create($user);
if ($result > 0) {
setEventMessages($langs->trans('RecordSaved'), null, 'mesgs');
header('Location: '.$_SERVER['PHP_SELF'].'?id='.$id.'&system='.$systemId);
exit;
} else {
setEventMessages($anlage->error, $anlage->errors, 'errors');
$action = 'create';
}
}
if ($action == 'update' && $permissiontoadd) {
$anlage->fetch($anlageId);
$systemIdBefore = $anlage->fk_system; // Remember original system
$anlage->label = GETPOST('label', 'alphanohtml');
$anlage->fk_anlage_type = GETPOSTINT('fk_anlage_type');
$anlage->fk_parent = GETPOSTINT('fk_parent');
$anlage->fk_product = GETPOSTINT('fk_product') > 0 ? GETPOSTINT('fk_product') : null;
$anlage->note_private = isset($_POST['note_private']) ? $_POST['note_private'] : '';
// Get type - but keep current system for GLOBAL types (buildings)
$type = new AnlageType($db);
if ($type->fetch($anlage->fk_anlage_type) > 0) {
// Only change system if type is NOT a global type
// Global types (buildings) should stay in their current system
if ($type->system_code !== 'GLOBAL') {
$anlage->fk_system = $type->fk_system;
}
}
// 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;
}
}
$anlage->setFieldValues($fieldValues);
$result = $anlage->update($user);
if ($result > 0) {
setEventMessages($langs->trans('RecordSaved'), null, 'mesgs');
header('Location: '.$_SERVER['PHP_SELF'].'?id='.$id.'&system='.$systemId);
exit;
} else {
setEventMessages($anlage->error, $anlage->errors, 'errors');
$action = 'edit';
}
}
if ($action == 'confirm_delete' && $confirm == 'yes' && $permissiontodelete) {
$anlage->fetch($anlageId);
$systemIdBefore = $anlage->fk_system;
$result = $anlage->delete($user);
if ($result > 0) {
setEventMessages($langs->trans('RecordDeleted'), null, 'mesgs');
} else {
setEventMessages($anlage->error, $anlage->errors, 'errors');
}
header('Location: '.$_SERVER['PHP_SELF'].'?id='.$id.'&system='.$systemIdBefore);
exit;
}
// File upload (multi-file support)
if ($action == 'uploadfile' && $permissiontoadd) {
$anlage->fetch($anlageId);
$upload_dir = $anlage->getFileDirectory();
// Create directory if not exists
if (!is_dir($upload_dir)) {
dol_mkdir($upload_dir);
}
$uploadCount = 0;
if (!empty($_FILES['userfiles']['name'][0])) {
// Multi-file upload
$fileCount = count($_FILES['userfiles']['name']);
for ($i = 0; $i < $fileCount; $i++) {
if ($_FILES['userfiles']['error'][$i] === UPLOAD_ERR_OK && !empty($_FILES['userfiles']['name'][$i])) {
$tmpName = $_FILES['userfiles']['tmp_name'][$i];
$fileName = dol_sanitizeFileName($_FILES['userfiles']['name'][$i]);
$destPath = $upload_dir.'/'.$fileName;
// Handle duplicate filenames
$counter = 1;
$baseName = pathinfo($fileName, PATHINFO_FILENAME);
$extension = pathinfo($fileName, PATHINFO_EXTENSION);
while (file_exists($destPath)) {
$fileName = $baseName.'_'.$counter.'.'.$extension;
$destPath = $upload_dir.'/'.$fileName;
$counter++;
}
if (move_uploaded_file($tmpName, $destPath)) {
// Add to database
$anlagefile = new AnlageFile($db);
$anlagefile->fk_anlage = $anlageId;
$anlagefile->filename = $fileName;
// IMPORTANT: Store ONLY relative path (anlagen/socid/anlageid/filename) - never full path!
$anlagefile->filepath = 'anlagen/'.$anlage->fk_soc.'/'.$anlage->id.'/'.$fileName;
$anlagefile->filesize = $_FILES['userfiles']['size'][$i];
$anlagefile->mimetype = dol_mimetype($fileName);
$anlagefile->create($user);
// Generate thumbnail
$anlagefile->generateThumbnail();
$uploadCount++;
}
}
}
if ($uploadCount > 0) {
$msg = $uploadCount > 1 ? $uploadCount.' Dateien hochgeladen' : 'Datei hochgeladen';
setEventMessages($msg, null, 'mesgs');
}
}
header('Location: '.$_SERVER['PHP_SELF'].'?id='.$id.'&system='.$systemId.'&action=view&anlage_id='.$anlageId);
exit;
}
// File delete (after confirmation)
if ($action == 'confirm_deletefile' && $confirm == 'yes' && $permissiontodelete) {
$fileId = GETPOSTINT('fileid');
if ($fileId > 0) {
$anlagefile = new AnlageFile($db);
if ($anlagefile->fetch($fileId) > 0) {
// Delete method handles physical file and database
if ($anlagefile->delete($user) > 0) {
setEventMessages($langs->trans('FileDeleted'), null, 'mesgs');
} else {
setEventMessages($langs->trans('Error'), null, 'errors');
}
}
}
header('Location: '.$_SERVER['PHP_SELF'].'?id='.$id.'&system='.$systemId.'&action=view&anlage_id='.$anlageId);
exit;
}
// Toggle file pinned status
if ($action == 'togglepin' && $permissiontoadd) {
$fileId = GETPOSTINT('fileid');
if ($fileId > 0) {
$anlagefile = new AnlageFile($db);
if ($anlagefile->fetch($fileId) > 0) {
if ($anlagefile->togglePin($user) > 0) {
$msg = $anlagefile->is_pinned ? $langs->trans('FilePinned') : $langs->trans('FileUnpinned');
setEventMessages($msg, null, 'mesgs');
}
}
}
header('Location: '.$_SERVER['PHP_SELF'].'?id='.$id.'&system='.$systemId.'&action=view&anlage_id='.$anlageId.'#files');
exit;
}
/*
* View
*/
$title = $langs->trans('TechnicalInstallations').' - '.$object->getFullName($langs);
// Ansichtsmodus: pro System konfigurierbar (tree/graph/both)
dol_include_once('/kundenkarte/lib/graph_view.lib.php');
$allowedViews = 'both';
if ($systemId > 0 && isset($customerSystems[$systemId]) && !empty($customerSystems[$systemId]->view_modes)) {
$allowedViews = $customerSystems[$systemId]->view_modes;
}
$defaultView = getDolGlobalString('KUNDENKARTE_DEFAULT_VIEW', 'tree');
$viewMode = GETPOST('view', 'aZ09');
if (!in_array($viewMode, array('tree', 'graph'))) {
$viewMode = $defaultView;
}
// Erzwinge erlaubten Modus wenn nur einer verfügbar
if ($allowedViews === 'tree') {
$viewMode = 'tree';
} elseif ($allowedViews === 'graph') {
$viewMode = 'graph';
}
$jsFiles = array('/kundenkarte/js/kundenkarte.js?v='.time());
$cssFiles = array('/kundenkarte/css/kundenkarte.css?v='.time());
kundenkarte_graph_add_assets($jsFiles, $cssFiles, $viewMode);
llxHeader('', $title, '', '', 0, 0, $jsFiles, $cssFiles);
// Prepare tabs
$head = contact_prepare_head($object);
print dol_get_fiche_head($head, 'anlagen', $langs->trans("ContactAddress"), -1, 'contact');
// Contact card
$linkback = ''.$langs->trans("BackToList").' ';
dol_banner_tab($object, 'id', $linkback, 1, 'rowid', 'nom');
// Admin-Schnellzugriff (nur für Modul-Admins)
if ($user->admin || $user->hasRight('kundenkarte', 'admin')) {
$adminUrl = dol_buildpath('/kundenkarte/admin/setup.php', 1);
print '
';
}
print '';
// Confirmation dialogs
if ($action == 'delete') {
print $form->formconfirm(
$_SERVER['PHP_SELF'].'?id='.$id.'&anlage_id='.$anlageId.'&system='.$systemId,
$langs->trans('DeleteElement'),
$langs->trans('ConfirmDeleteElement'),
'confirm_delete',
'',
'yes',
1
);
}
if ($action == 'remove_system') {
$removeSystemId = GETPOSTINT('remove_system_id');
$sysLabel = isset($customerSystems[$removeSystemId]) ? $customerSystems[$removeSystemId]->label : '';
print $form->formconfirm(
$_SERVER['PHP_SELF'].'?id='.$id.'&remove_system_id='.$removeSystemId,
$langs->trans('RemoveSystem'),
$langs->trans('ConfirmRemoveSystem', $sysLabel),
'confirm_remove_system',
'',
'yes',
1
);
}
if ($action == 'askdeletefile') {
$fileId = GETPOSTINT('fileid');
print $form->formconfirm(
$_SERVER['PHP_SELF'].'?id='.$id.'&system='.$systemId.'&anlage_id='.$anlageId.'&fileid='.$fileId,
$langs->trans('Delete'),
$langs->trans('ConfirmDeleteFile'),
'confirm_deletefile',
'',
'yes',
1
);
}
// System tabs (only show enabled systems for this contact)
print '
';
print '
';
foreach ($customerSystems as $sysId => $sys) {
$activeClass = ($sysId == $systemId) ? ' active' : '';
print '
';
}
// Add system button (always on the right)
if ($permissiontoadd) {
// Get systems not yet enabled for this contact
$availableSystems = array_diff_key($allSystems, $customerSystems);
if (!empty($availableSystems)) {
print '
';
print ' '.$langs->trans('AddSystem');
print ' ';
}
}
print '
';
// Steuerungs-Buttons (nur wenn kein Formular aktiv)
$isTreeView = !in_array($action, array('create', 'edit', 'view', 'copy'));
if ($isTreeView) {
$toggleView = ($viewMode === 'graph') ? 'tree' : 'graph';
$toggleUrl = $_SERVER['PHP_SELF'].'?id='.$id.'&system='.$systemId.'&view='.$toggleView;
$toggleIcon = ($viewMode === 'graph') ? 'fa-list' : 'fa-sitemap';
$toggleLabel = ($viewMode === 'graph') ? $langs->trans('TreeView') : $langs->trans('GraphView');
if ($viewMode !== 'graph') {
// Baumansicht: Controls auf gleicher Zeile wie System-Tabs (im Wrapper)
print '
';
if ($allowedViews === 'both') {
print '
';
print ' '.$toggleLabel;
print ' ';
}
print '
';
print ' Kompakt ';
print ' ';
print '
';
print ' '.$langs->trans('ExpandAll');
print ' ';
print '
';
print ' '.$langs->trans('CollapseAll');
print ' ';
$showDecomm = getDolGlobalInt('KUNDENKARTE_SHOW_DECOMMISSIONED', 0);
print '
';
print ' '.$langs->trans('Decommissioned').' ';
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 '
';
}
}
print '
'; // End kundenkarte-system-tabs-wrapper
// Graph-Toolbar: UNTER der System-Tab-Borderlinie
if ($isTreeView && $viewMode === 'graph') {
kundenkarte_graph_print_toolbar(array(
'socid' => $object->socid,
'contactid' => $id,
'systemid' => $systemId,
'viewMode' => $viewMode,
'allowedViews' => $allowedViews,
'permissiontoadd' => $permissiontoadd,
'pageUrl' => $_SERVER['PHP_SELF'],
));
}
// Add system form (hidden by default)
if ($permissiontoadd && !empty($availableSystems)) {
print '
';
print '';
print '
';
}
// Check if contact has any systems
if (empty($customerSystems)) {
print '
';
print ' ';
print $langs->trans('NoSystemsConfigured').' ';
if ($permissiontoadd && !empty($allSystems)) {
print $langs->trans('ClickAddSystemToStart');
} else {
print $langs->trans('ContactAdminToAddSystems');
}
print '
';
} elseif ($systemId > 0) {
// Show form or tree for selected system
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);
$type->fetch($anlage->fk_anlage_type);
$type->fetchFields();
}
// Load types for select
$types = $anlageType->fetchAllBySystem($systemId);
print '
';
if ($action == 'view') {
// View mode
print '
'.dol_escape_htmltag($anlage->label).' ';
print '
';
print ''.$langs->trans('Type').' ';
print ''.dol_escape_htmltag($anlage->type_label).' ';
// Zugeordnetes Produkt (nur wenn Typ es erlaubt)
if ($type->has_product && $anlage->fk_product > 0) {
require_once DOL_DOCUMENT_ROOT.'/product/class/product.class.php';
$product = new Product($db);
if ($product->fetch($anlage->fk_product) > 0) {
print ''.$langs->trans('Product').' ';
print ''.dol_escape_htmltag($product->ref).' ';
print ' - '.dol_escape_htmltag($product->label);
if ($product->price > 0) {
print ' ('.price($product->price).' €) ';
}
print ' ';
}
}
// Dynamic fields - all fields come from type definition
$fieldValues = $anlage->getFieldValues();
$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).' ';
}
}
}
}
if ($anlage->note_private) {
print ''.$langs->trans('FieldNotes').' ';
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').' ';
}
// Ausgebaut-Status
if (!empty($anlage->decommissioned)) {
print ''.$langs->trans('Decommissioned').' ';
print ' '.$langs->trans('Decommissioned').' ';
if (!empty($anlage->date_decommissioned)) {
print ' '.dol_print_date(strtotime($anlage->date_decommissioned), 'day');
}
print ' ';
}
print '
';
// Files section
$anlagefile = new AnlageFile($db);
$files = $anlagefile->fetchAllByAnlage($anlageId);
print '
'.$langs->trans('AttachedFiles').' ';
if ($permissiontoadd) {
print '
';
}
if (!empty($files)) {
print '
';
foreach ($files as $file) {
$pinnedClass = $file->is_pinned ? ' kundenkarte-file-pinned' : '';
print '
';
if ($file->is_pinned) {
print '
';
}
print '
';
if ($file->file_type == 'image') {
$thumbUrl = $file->getThumbUrl();
if ($thumbUrl) {
print '
';
} else {
print '
';
}
} elseif ($file->file_type == 'pdf') {
// PDF preview using iframe
print '
';
print '';
print '
';
} else {
// Show icon based on file type
$fileIcon = 'fa-file-o';
$iconColor = '#999';
if ($file->file_type == 'archive') {
$fileIcon = 'fa-file-archive-o';
$iconColor = '#f39c12';
} elseif ($file->file_type == 'document') {
$ext = strtolower(pathinfo($file->filename, PATHINFO_EXTENSION));
if (in_array($ext, array('doc', 'docx'))) {
$fileIcon = 'fa-file-word-o';
$iconColor = '#2b579a';
} elseif (in_array($ext, array('xls', 'xlsx'))) {
$fileIcon = 'fa-file-excel-o';
$iconColor = '#217346';
} elseif ($ext == 'txt') {
$fileIcon = 'fa-file-text-o';
$iconColor = '#666';
} else {
$fileIcon = 'fa-file-text-o';
$iconColor = '#3498db';
}
}
print '
';
}
print '
';
print '
';
print '
'.dol_escape_htmltag(dol_trunc($file->filename, 35)).'
';
print '
'.dol_print_size($file->filesize).'
';
print '
';
print '
';
if ($permissiontoadd) {
$pinClass = $file->is_pinned ? ' kundenkarte-file-btn-pinned' : '';
$pinTitle = $file->is_pinned ? $langs->trans('Unpin') : $langs->trans('Pin');
print '
';
}
if ($permissiontodelete) {
print '
';
}
print '
';
print '
';
print '
';
}
print '
';
} else {
print '
'.$langs->trans('NoFiles').'
';
}
// Equipment section (only if type can have equipment)
if ($type->can_have_equipment) {
print '
'.$langs->trans('Equipment').' - Schaltplan';
// Equipment container
print '
';
// Schematic Editor
print '
';
print '';
print '
Bereit
';
print '
';
print '
';
print '
';
print '
'; // .kundenkarte-equipment-container
// Initialize SchematicEditor JavaScript
print '';
}
// Zubehör-Bereich (nur wenn Typ has_accessories hat)
if (!empty($type->has_accessories)) {
$accessoryObj = new AnlageAccessory($db);
$accessories = $accessoryObj->fetchAllByAnlage($anlageId);
print '
'.$langs->trans('Accessories').'';
if (!empty($accessories)) {
print '
';
print '
';
print '';
print ''.$langs->trans('ProductRef').' ';
print ''.$langs->trans('Label').' ';
print ''.$langs->trans('Qty').' ';
print ''.$langs->trans('Note').' ';
if ($permissiontodelete) {
print ''.$langs->trans('Action').' ';
}
print ' ';
foreach ($accessories as $acc) {
print '';
print ''.dol_escape_htmltag($acc->product_ref).' ';
print ''.dol_escape_htmltag($acc->product_label).' ';
print ''.$acc->qty.' ';
print ''.dol_escape_htmltag($acc->note).' ';
if ($permissiontodelete) {
print ' ';
}
print ' ';
}
print '
';
print '
';
} else {
print '
'.$langs->trans('NoAccessories').'
';
}
if ($permissiontoadd) {
print '
';
}
if ($permissiontoadd && !empty($accessories)) {
print '
';
print '
'.$langs->trans('OrderAccessories').'';
print '
';
print '';
print ''.$langs->trans('SelectSupplier').' ';
$sqlSupp = "SELECT s.rowid, s.nom FROM ".MAIN_DB_PREFIX."societe s WHERE s.fournisseur = 1 AND s.status = 1 ORDER BY s.nom";
$resSupp = $db->query($sqlSupp);
if ($resSupp) {
while ($objSupp = $db->fetch_object($resSupp)) {
print ''.dol_escape_htmltag($objSupp->nom).' ';
}
}
print ' ';
print ''.$langs->trans('CreateSupplierOrder').' ';
print '
';
print '
';
}
}
// Action buttons
print '
';
} else {
// Create/Edit/Copy form
$isEdit = ($action == 'edit');
$isCopy = ($action == 'copy');
$formAction = $isEdit ? 'update' : 'add';
print '
';
// JavaScript: Kategorie-Filter + Select2 mit Icons
print '';
}
print '
';
} else {
// Listenansicht (Baum oder Graph)
if ($permissiontoadd && $viewMode !== 'graph') {
print '
';
}
if ($viewMode === 'graph' && $isTreeView) {
// Graph-Ansicht: Container rendern, Daten werden per AJAX geladen
kundenkarte_graph_print_container(array(
'socid' => $object->socid,
'contactid' => $id,
'systemid' => $systemId,
'permissiontoadd' => $permissiontoadd,
'permissiontodelete' => $permissiontodelete,
'pageUrl' => $_SERVER['PHP_SELF'],
));
} else {
// Baumansicht (klassisch)
// 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);
}
// Verbindungen werden nur im Graph angezeigt, nicht im Baum
$connectionsByTarget = array();
if (!empty($tree)) {
print '
';
printTree($tree, $id, $systemId, $permissiontoadd, $permissiontodelete, $langs, 0, $typeFieldsMap, $connectionsByTarget);
print '
';
} else {
print '
'.$langs->trans('NoInstallations').'
';
}
}
}
}
print '
';
print dol_get_fiche_end();
// Tooltip container
print '
';
// Produkt-Autocomplete + Zubehör-AJAX (nur wenn Formular oder Detailansicht aktiv)
if (in_array($action, array('create', 'edit', 'copy', 'view'))) {
print '';
print '';
}
llxFooter();
$db->close();
/**
* Print tree recursively (root level - handles cable line assignment)
*/
function printTree($nodes, $contactid, $systemId, $canEdit, $canDelete, $langs, $level = 0, $typeFieldsMap = array(), $connectionsByTarget = array())
{
foreach ($nodes as $node) {
$hasChildren = !empty($node->children);
$fieldValues = $node->getFieldValues();
// Build tooltip data - only label, type and dynamic fields
$tooltipData = array(
'label' => $node->label,
'type' => $node->type_label,
'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)
$treeInfoBadges = array(); // Fields to display as badges (right side)
$treeInfoParentheses = array(); // Fields to display in parentheses (after label)
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
$displayVal = $value;
if ($fieldDef->field_type === 'date' && $value) {
$displayVal = dol_print_date(strtotime($value), 'day');
}
// Store as array with field info
$fieldInfo = array(
'label' => $fieldDef->field_label,
'value' => $displayVal,
'code' => $fieldDef->field_code,
'type' => $fieldDef->field_type,
'color' => $fieldDef->badge_color ?? ''
);
// Sort into badge or parentheses based on field setting
$displayMode = $fieldDef->tree_display_mode ?? 'badge';
if ($displayMode === 'parentheses') {
$treeInfoParentheses[] = $fieldInfo;
} else {
$treeInfoBadges[] = $fieldInfo;
}
}
}
}
// Show cable connections TO this node (BEFORE the element - cable appears above verbraucher)
$hasConnection = !empty($connectionsByTarget[$node->id]);
if ($hasConnection) {
foreach ($connectionsByTarget[$node->id] as $conn) {
// Build cable info (type + spec + length)
$cableInfo = '';
if ($conn->medium_type_label || $conn->medium_type_text) {
$cableInfo = $conn->medium_type_label ?: $conn->medium_type_text;
}
if ($conn->medium_spec) {
$cableInfo .= ($cableInfo ? ' ' : '').$conn->medium_spec;
}
if ($conn->medium_length) {
$cableInfo .= ' ('.$conn->medium_length.')';
}
// If label exists, show cable info as badge. Otherwise show cable info as main text
$mainText = $conn->label ? $conn->label : $cableInfo;
$badgeText = $conn->label ? $cableInfo : '';
$connEditUrl = dol_buildpath('/kundenkarte/anlage_connection.php', 1).'?id='.$conn->id.'&socid='.$node->fk_soc.'&contactid='.$id.'&system_id='.$systemId;
print '';
print ' ';
if ($mainText) {
print ''.dol_escape_htmltag($mainText).' ';
}
if ($badgeText) {
print ' '.dol_escape_htmltag($badgeText).' ';
}
print ' ';
}
}
// CSS class basierend auf Kabel-Verbindung und Typ-Kategorie
$nodeClass = 'kundenkarte-tree-node';
if (!$hasConnection && $level > 0) {
$nodeClass .= ' no-cable'; // durchgeschleift - kein eigenes Kabel
}
if (!empty($node->decommissioned)) {
$nodeClass .= ' decommissioned';
}
if ($node->type_can_have_equipment) {
$nodeClass .= ' node-equipment'; // Geräte-Container (Schaltschrank, Verteiler)
} elseif ($node->type_can_have_children) {
$nodeClass .= ' node-structure'; // Gebäudeteil (Gebäude, Etage, Raum)
} else {
$nodeClass .= ' node-leaf'; // Endgerät
}
print '';
print '
';
// Toggle
if ($hasChildren) {
print '
';
} else {
print '
';
}
// Icon with tooltip data
$picto = $node->type_picto ? $node->type_picto : 'fa-cube';
print '
'.kundenkarte_render_icon($picto).' ';
// Label with parentheses info (directly after label, only values)
$viewUrl = $_SERVER['PHP_SELF'].'?id='.$contactid.'&system='.$systemId.'&action=view&anlage_id='.$node->id;
print '
'.dol_escape_htmltag($node->label);
// Parentheses info directly after label (only values, no field names)
if (!empty($treeInfoParentheses)) {
$infoValues = array();
foreach ($treeInfoParentheses as $info) {
$infoValues[] = dol_escape_htmltag($info['value']);
}
print ' ('.implode(', ', $infoValues).') ';
}
print ' ';
// Ausgebaut-Badge mit Datum
if (!empty($node->decommissioned)) {
$decommDate = !empty($node->date_decommissioned) ? dol_print_date(strtotime($node->date_decommissioned), 'day') : '';
$decommText = $langs->trans('Decommissioned');
if ($decommDate) $decommText .= ' '.$decommDate;
print '
'.$decommText.'';
}
// Spacer to push badges to the right
print '
';
// Badges (far right, before actions)
$defaultBadgeColor = getDolGlobalString('KUNDENKARTE_TREE_BADGE_COLOR', '#2a4a5e');
if (!empty($treeInfoBadges)) {
print '
';
foreach ($treeInfoBadges as $info) {
$badgeIcon = kundenkarte_get_field_icon($info['code'], $info['type']);
// Use field-specific color if set, otherwise global default
$fieldBadgeColor = !empty($info['color']) ? $info['color'] : $defaultBadgeColor;
print '';
print ' '.dol_escape_htmltag($info['value']);
print ' ';
}
print ' ';
}
// File indicators
if ($node->image_count > 0 || $node->doc_count > 0) {
$totalFiles = $node->image_count + $node->doc_count;
print '
';
print '';
print ' '.$totalFiles;
print ' ';
print ' ';
}
// Type badge
if ($node->type_short || $node->type_label) {
$typeDisplay = $node->type_short ? $node->type_short : $node->type_label;
print '
'.dol_escape_htmltag($typeDisplay).' ';
}
// Actions
print '
';
print ' ';
if ($canEdit) {
print ' ';
print ' ';
print ' ';
$decommLabel = $node->decommissioned ? $langs->trans('Recommission') : $langs->trans('Decommission');
$decommIcon = $node->decommissioned ? 'fa-plug' : 'fa-power-off';
print ' ';
}
if ($canDelete) {
print ' ';
}
print ' ';
print '
';
// Children - vertical tree layout with multiple parallel cable lines
if ($hasChildren) {
// First pass: assign cable index to each child with cable
$cableCount = 0;
$childCableIndex = array(); // child_id => cable_index
foreach ($node->children as $child) {
if (!empty($connectionsByTarget[$child->id])) {
$cableCount++;
$childCableIndex[$child->id] = $cableCount;
}
}
print '
';
// Render children - each row has cable line columns on the left
$prevHadCable = false;
$isFirst = true;
foreach ($node->children as $child) {
$myCableIdx = isset($childCableIndex[$child->id]) ? $childCableIndex[$child->id] : 0;
$hasOwnCable = !empty($connectionsByTarget[$child->id]);
// Add spacer row before elements with their own cable (except first)
if ($hasOwnCable && !$isFirst) {
// Spacer row with active cable lines passing through
$spacerActiveLines = array();
$foundCurrent = false;
foreach ($node->children as $c) {
if ($c->id == $child->id) {
$foundCurrent = true;
}
if ($foundCurrent && isset($childCableIndex[$c->id])) {
$spacerActiveLines[] = $childCableIndex[$c->id];
}
}
print '
';
for ($i = $cableCount; $i >= 1; $i--) {
$isActive = in_array($i, $spacerActiveLines);
$lineClass = 'cable-line';
if ($isActive) {
$lineClass .= ' active';
}
print '
';
}
print '
';
print '
';
}
// Find which cable lines are still active (run past this row to children below)
$childActiveLinesAfter = array();
$foundCurrent = false;
foreach ($node->children as $c) {
if ($c->id == $child->id) {
$foundCurrent = true;
continue;
}
if ($foundCurrent && isset($childCableIndex[$c->id])) {
$childActiveLinesAfter[] = $childCableIndex[$c->id];
}
}
printTreeWithCableLines(array($child), $contactid, $systemId, $canEdit, $canDelete, $langs, $level + 1, $typeFieldsMap, $connectionsByTarget, $myCableIdx, $cableCount, $childActiveLinesAfter);
$prevHadCable = $hasOwnCable;
$isFirst = false;
}
print '
';
}
print '
';
}
}
/**
* Print tree with multiple parallel cable lines
* Each child with its own cable gets a vertical line that runs from top past all siblings
*/
function printTreeWithCableLines($nodes, $contactid, $systemId, $canEdit, $canDelete, $langs, $level = 0, $typeFieldsMap = array(), $connectionsByTarget = array(), $myCableIndex = 0, $totalCables = 0, $activeLinesAfter = array())
{
foreach ($nodes as $node) {
$hasChildren = !empty($node->children);
$hasConnection = !empty($connectionsByTarget[$node->id]);
$fieldValues = $node->getFieldValues();
// Build tooltip data
$tooltipData = array(
'label' => $node->label,
'type' => $node->type_label,
'note_html' => $node->note_private ? nl2br(htmlspecialchars($node->note_private, ENT_QUOTES, 'UTF-8')) : '',
'fields' => array()
);
$treeInfoBadges = array();
$treeInfoParentheses = array();
if (!empty($typeFieldsMap[$node->fk_anlage_type])) {
foreach ($typeFieldsMap[$node->fk_anlage_type] as $fieldDef) {
if ($fieldDef->field_type === 'header') continue;
$value = isset($fieldValues[$fieldDef->field_code]) ? $fieldValues[$fieldDef->field_code] : '';
if ($fieldDef->show_in_hover && $value !== '') {
$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
);
}
if ($fieldDef->show_in_tree && $value !== '') {
$displayVal = ($fieldDef->field_type === 'date' && $value) ? dol_print_date(strtotime($value), 'day') : $value;
$fieldInfo = array(
'label' => $fieldDef->field_label,
'value' => $displayVal,
'code' => $fieldDef->field_code,
'type' => $fieldDef->field_type,
'color' => $fieldDef->badge_color ?? ''
);
$displayMode = $fieldDef->tree_display_mode ?? 'badge';
if ($displayMode === 'parentheses') {
$treeInfoParentheses[] = $fieldInfo;
} else {
$treeInfoBadges[] = $fieldInfo;
}
}
}
}
// All lines that need to be drawn for this row (active lines passing through + my line if I have cable)
$allLines = $activeLinesAfter;
if ($hasConnection && $myCableIndex > 0) {
$allLines[] = $myCableIndex;
}
sort($allLines);
// Cable connection row (BEFORE the element)
if ($hasConnection) {
foreach ($connectionsByTarget[$node->id] as $conn) {
$cableInfo = '';
if ($conn->medium_type_label || $conn->medium_type_text) {
$cableInfo = $conn->medium_type_label ?: $conn->medium_type_text;
}
if ($conn->medium_spec) {
$cableInfo .= ($cableInfo ? ' ' : '').$conn->medium_spec;
}
if ($conn->medium_length) {
$cableInfo .= ' ('.$conn->medium_length.')';
}
$mainText = $conn->label ? $conn->label : $cableInfo;
$badgeText = $conn->label ? $cableInfo : '';
$connEditUrl = dol_buildpath('/kundenkarte/anlage_connection.php', 1).'?id='.$conn->id.'&socid='.$node->fk_soc.'&contactid='.$id.'&system_id='.$systemId;
print '';
// Draw vertical line columns (for cables passing through)
// Reverse order: index 1 (first element) = rightmost, highest index = leftmost
for ($i = $totalCables; $i >= 1; $i--) {
$isActive = in_array($i, $activeLinesAfter);
$isMyLine = ($i == $myCableIndex);
$lineClass = 'cable-line';
$inlineStyle = '';
if ($isActive) {
$lineClass .= ' active'; // Line continues down
}
if ($isMyLine) {
$lineClass .= ' my-line conn-line'; // This is my cable line - ends here with horizontal
// Calculate width: columns to the right * 15px + 8px to reach element border
$columnsToRight = $i - 1;
$horizontalWidth = ($columnsToRight * 15) + 8;
$inlineStyle = ' style="--h-width: '.$horizontalWidth.'px;"';
}
print '
';
}
print '
';
print ' ';
if ($mainText) {
print ''.dol_escape_htmltag($mainText).' ';
}
if ($badgeText) {
print ' '.dol_escape_htmltag($badgeText).' ';
}
print ' ';
print '
';
}
}
// Node row
$nodeClass = 'kundenkarte-tree-row node-row';
if (!$hasConnection && $level > 0) {
$nodeClass .= ' no-cable';
}
if (!empty($node->decommissioned)) {
$nodeClass .= ' decommissioned';
}
print '';
// Draw vertical line columns (for cables passing through)
// Reverse order: index 1 (first element) = rightmost, highest index = leftmost
for ($i = $totalCables; $i >= 1; $i--) {
$isActive = in_array($i, $activeLinesAfter);
$isMyLine = ($i == $myCableIndex && $hasConnection);
$lineClass = 'cable-line';
$inlineStyle = '';
if ($isActive) {
$lineClass .= ' active';
}
if ($isMyLine) {
$lineClass .= ' my-line node-line'; // Horizontal connector to this node
// Calculate width: columns to the right * 15px + 8px to reach element border
$columnsToRight = $i - 1;
$horizontalWidth = ($columnsToRight * 15) + 8;
$inlineStyle = ' style="--h-width: '.$horizontalWidth.'px;"';
}
print '
';
}
// Typ-Kategorie als CSS-Klasse
$nodeContentClass = 'kundenkarte-tree-node-content';
if ($node->type_can_have_equipment) {
$nodeContentClass .= ' node-equipment';
} elseif ($node->type_can_have_children) {
$nodeContentClass .= ' node-structure';
} else {
$nodeContentClass .= ' node-leaf';
}
print '
';
print '
';
if ($hasChildren) {
print '
';
} else {
print '
';
}
$picto = $node->type_picto ? $node->type_picto : 'fa-cube';
print '
'.kundenkarte_render_icon($picto).' ';
$viewUrl = $_SERVER['PHP_SELF'].'?id='.$contactid.'&system='.$systemId.'&action=view&anlage_id='.$node->id;
// Label with parentheses info (only values, no field names)
print '
'.dol_escape_htmltag($node->label);
if (!empty($treeInfoParentheses)) {
$infoValues = array();
foreach ($treeInfoParentheses as $info) {
$infoValues[] = dol_escape_htmltag($info['value']);
}
print ' ('.implode(', ', $infoValues).') ';
}
print ' ';
// Ausgebaut-Badge mit Datum
if (!empty($node->decommissioned)) {
$decommDate = !empty($node->date_decommissioned) ? dol_print_date(strtotime($node->date_decommissioned), 'day') : '';
$decommText = $langs->trans('Decommissioned');
if ($decommDate) $decommText .= ' '.$decommDate;
print '
'.$decommText.'';
}
// Spacer to push badges to the right
print '
';
// Badges (far right)
if (!empty($treeInfoBadges)) {
$defaultBadgeColor = getDolGlobalString('KUNDENKARTE_TREE_BADGE_COLOR', '#2a4a5e');
print '
';
foreach ($treeInfoBadges as $info) {
$badgeIcon = kundenkarte_get_field_icon($info['code'], $info['type']);
$fieldBadgeColor = !empty($info['color']) ? $info['color'] : $defaultBadgeColor;
print '';
print ' '.dol_escape_htmltag($info['value']);
print ' ';
}
print ' ';
}
// File indicators
if ($node->image_count > 0 || $node->doc_count > 0) {
$totalFiles = $node->image_count + $node->doc_count;
print '
';
print '';
print ' '.$totalFiles;
print ' ';
print ' ';
}
if ($node->type_short || $node->type_label) {
$typeDisplay = $node->type_short ? $node->type_short : $node->type_label;
print '
'.dol_escape_htmltag($typeDisplay).' ';
}
print '
';
print ' ';
if ($canEdit) {
print ' ';
print ' ';
print ' ';
$decommLabel = $node->decommissioned ? $langs->trans('Recommission') : $langs->trans('Decommission');
$decommIcon = $node->decommissioned ? 'fa-plug' : 'fa-power-off';
print ' ';
}
if ($canDelete) {
print ' ';
}
print ' ';
print '
';
print '
'; // node-content
print '
'; // tree-row
// Children
if ($hasChildren) {
// First pass: assign cable index to each child with cable
$childCableCount = 0;
$childCableIndex = array(); // child_id => cable_index
foreach ($node->children as $child) {
if (!empty($connectionsByTarget[$child->id])) {
$childCableCount++;
$childCableIndex[$child->id] = $childCableCount;
}
}
print '';
$prevHadCable = false;
$isFirst = true;
foreach ($node->children as $child) {
$myCableIdx = isset($childCableIndex[$child->id]) ? $childCableIndex[$child->id] : 0;
$hasOwnCable = !empty($connectionsByTarget[$child->id]);
// Add spacer row before elements with their own cable (except first)
if ($hasOwnCable && !$isFirst) {
// Spacer row with active cable lines passing through
$spacerActiveLines = array();
$foundCurrent = false;
foreach ($node->children as $c) {
if ($c->id == $child->id) {
$foundCurrent = true;
}
if ($foundCurrent && isset($childCableIndex[$c->id])) {
$spacerActiveLines[] = $childCableIndex[$c->id];
}
}
print '
';
for ($i = $childCableCount; $i >= 1; $i--) {
$isActive = in_array($i, $spacerActiveLines);
$lineClass = 'cable-line';
if ($isActive) {
$lineClass .= ' active';
}
print '
';
}
print '
';
print '
';
}
// Find which cable lines are still active (run past this row to children below)
$childActiveLinesAfter = array();
$foundCurrent = false;
foreach ($node->children as $c) {
if ($c->id == $child->id) {
$foundCurrent = true;
continue;
}
if ($foundCurrent && isset($childCableIndex[$c->id])) {
$childActiveLinesAfter[] = $childCableIndex[$c->id];
}
}
printTreeWithCableLines(array($child), $contactid, $systemId, $canEdit, $canDelete, $langs, $level + 1, $typeFieldsMap, $connectionsByTarget, $myCableIdx, $childCableCount, $childActiveLinesAfter);
$prevHadCable = $hasOwnCable;
$isFirst = false;
}
print '
';
}
}
}
/**
* Print tree options for select
*/
function printTreeOptions($nodes, $selected = 0, $excludeId = 0, $prefix = '', $level = 0)
{
foreach ($nodes as $node) {
if ($node->id == $excludeId) continue;
$sel = ($node->id == $selected) ? ' selected' : '';
$icon = !empty($node->type_picto) ? $node->type_picto : 'fa-cube';
$color = !empty($node->type_color) ? $node->type_color : '#888';
print ''.$prefix.dol_escape_htmltag($node->label).' ';
if (!empty($node->children)) {
printTreeOptions($node->children, $selected, $excludeId, $prefix.'── ', $level + 1);
}
}
}