Version 4.0.1 - Mobile Navigation, Badge-Farben, Datei-Vorschau
Neue Features: - Badge-Farben pro Feld konfigurierbar (Admin > Element-Typen) - Datei-Vorschau Tooltip beim Hover über Datei-Badge - Mobile/Kompakte Ansicht mit einheitlichen Button-Größen - Autocomplete für Textfelder - Backup/Restore für Konfiguration Bugfixes: - Dolibarr App Navigation: Vor/Zurück-Pfeile funktionieren jetzt (Module akzeptieren id UND socid/contactid Parameter) - Datei-Badge zeigt jetzt Büroklammer-Icon Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
411a48d577
commit
07e0e2365b
23 changed files with 3288 additions and 135 deletions
126
CLAUDE.md
126
CLAUDE.md
|
|
@ -1 +1,125 @@
|
|||
CLAUDE_CODE_DISABLE_AUTO_MEMORY=0
|
||||
CLAUDE_CODE_DISABLE_AUTO_MEMORY=0
|
||||
|
||||
# KundenKarte Module - Entwicklungshinweise
|
||||
|
||||
## Dolibarr App Navigation (Vor/Zurück Pfeile)
|
||||
|
||||
### Problem
|
||||
Die Dolibarr Mobile App hat eigene Navigations-Pfeile (vorheriger/nächster Kunde), die zwischen Datensätzen navigieren. Diese verwenden andere Parameter als unsere Module erwarten:
|
||||
|
||||
- **Kunden-Navigation**: Dolibarr verwendet `socid`, Module erwarten oft `id`
|
||||
- **Kontakt-Navigation**: Dolibarr verwendet `contactid`, Module erwarten oft `id`
|
||||
|
||||
Wenn man diese Pfeile auf einem Modul-Tab verwendet, verliert das Modul die ID und zeigt einen Fehler oder leere Seite.
|
||||
|
||||
### Lösung: Beide Parameter akzeptieren
|
||||
|
||||
In **JEDEM Tab-PHP-File** muss am Anfang beide Parameter akzeptiert werden:
|
||||
|
||||
**Für Kunden-Tabs (thirdparty):**
|
||||
```php
|
||||
// Get parameters
|
||||
// Support both 'id' and 'socid' for compatibility with Dolibarr's customer navigation arrows
|
||||
$id = GETPOSTINT('id');
|
||||
if ($id <= 0) {
|
||||
$id = GETPOSTINT('socid');
|
||||
}
|
||||
```
|
||||
|
||||
**Für Kontakt-Tabs (contact):**
|
||||
```php
|
||||
// Get parameters
|
||||
// Support both 'id' and 'contactid' for compatibility with Dolibarr's contact navigation arrows
|
||||
$id = GETPOSTINT('id');
|
||||
if ($id <= 0) {
|
||||
$id = GETPOSTINT('contactid');
|
||||
}
|
||||
```
|
||||
|
||||
### Betroffene Dateien in diesem Modul
|
||||
- `tabs/anlagen.php` - Kunden-Anlagen (socid)
|
||||
- `tabs/favoriteproducts.php` - Kunden-Favoriten (socid)
|
||||
- `tabs/contact_anlagen.php` - Kontakt-Anlagen (contactid)
|
||||
- `tabs/contact_favoriteproducts.php` - Kontakt-Favoriten (contactid)
|
||||
|
||||
### Best Practices für zukünftige Module
|
||||
|
||||
1. **IMMER beide Parameter akzeptieren** - `id` UND `socid`/`contactid`
|
||||
2. **Tab-Definition in modXxx.class.php** verwendet `?id=__ID__` - das ist korrekt
|
||||
3. **Dolibarr's `dol_banner_tab()`** generiert die Navigationspfeile mit `socid`/`contactid`
|
||||
4. **Fallback bei fehlender ID** - zur Liste weiterleiten, nicht Fehler zeigen:
|
||||
```php
|
||||
if ($id <= 0) {
|
||||
header('Location: '.DOL_URL_ROOT.'/societe/list.php');
|
||||
exit;
|
||||
}
|
||||
```
|
||||
|
||||
## Mobile Ansicht - Einheitliche Button-Größen
|
||||
|
||||
### Problem
|
||||
Auf mobilen Geräten haben die Buttons (Kompakt, Aufklappen, Einklappen, PDF Export) unterschiedliche Größen.
|
||||
|
||||
### Lösung
|
||||
CSS Media Queries für einheitliche Button-Größen:
|
||||
|
||||
```css
|
||||
@media (max-width: 768px) {
|
||||
.kundenkarte-tree-controls {
|
||||
justify-content: center !important;
|
||||
flex-wrap: wrap !important;
|
||||
gap: 8px !important;
|
||||
}
|
||||
|
||||
.kundenkarte-tree-controls .button {
|
||||
flex: 1 1 auto !important;
|
||||
min-width: 80px !important;
|
||||
max-width: 150px !important;
|
||||
padding: 10px 8px !important;
|
||||
font-size: 12px !important;
|
||||
text-align: center !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
/* 2x2 Grid auf sehr kleinen Bildschirmen */
|
||||
.kundenkarte-tree-controls {
|
||||
display: grid !important;
|
||||
grid-template-columns: 1fr 1fr !important;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Migrationen
|
||||
|
||||
Alle Datenbankänderungen werden als idempotente Migrationen in `modKundenKarte.class.php` implementiert:
|
||||
- `runMigrations()` wird bei jeder Modulaktivierung aufgerufen
|
||||
- Jede Migration prüft zuerst, ob die Änderung bereits existiert
|
||||
- Später werden Migrationen entfernt und Tabellen direkt korrekt erstellt
|
||||
|
||||
## Dateistruktur
|
||||
|
||||
- `tabs/anlagen.php` - Hauptansicht für Anlagen auf Kundenebene
|
||||
- `tabs/contact_anlagen.php` - Anlagen für Kontakte
|
||||
- `tabs/favoriteproducts.php` - Lieblingsprodukte auf Kundenebene
|
||||
- `tabs/contact_favoriteproducts.php` - Lieblingsprodukte für Kontakte
|
||||
- `admin/anlage_types.php` - Verwaltung der Element-Typen
|
||||
- `ajax/` - AJAX-Endpunkte für dynamische Funktionen
|
||||
- `js/kundenkarte.js` - Alle JavaScript-Komponenten
|
||||
- `css/kundenkarte.css` - Alle Styles (Dark Mode)
|
||||
|
||||
## Wichtige Hinweise
|
||||
|
||||
### FontAwesome Icons
|
||||
- Dolibarr verwendet FontAwesome 4.x Format: `fa fa-icon-name`
|
||||
- NICHT: `fas fa-icon-name` oder `far fa-icon-name`
|
||||
|
||||
### Badge-Farben
|
||||
- Können pro Feld in Admin > Element-Typen konfiguriert werden
|
||||
- Spalte `badge_color` in `llx_kundenkarte_anlage_type_field`
|
||||
- Hex-Format: `#RRGGBB`
|
||||
|
||||
### Datei-Vorschau Tooltip
|
||||
- AJAX-Endpoint: `ajax/file_preview.php`
|
||||
- Zeigt Thumbnails für Bilder, Icons für Dokumente
|
||||
- Hover über Datei-Badge im Baum
|
||||
|
|
|
|||
32
ChangeLog.md
32
ChangeLog.md
|
|
@ -1,5 +1,37 @@
|
|||
# CHANGELOG MODULE KUNDENKARTE FOR [DOLIBARR ERP CRM](https://www.dolibarr.org)
|
||||
|
||||
## 4.0.1 (2026-02)
|
||||
|
||||
### Neue Features
|
||||
- **Badge-Farben pro Feld**: Individuelle Farben fuer Badges im Baum konfigurierbar
|
||||
- Neue Spalte in Admin > Element-Typen > Felder
|
||||
- Color-Picker fuer einfache Farbauswahl
|
||||
- Hex-Format (#RRGGBB)
|
||||
|
||||
- **Datei-Vorschau Tooltip**: Hover ueber Datei-Badge zeigt Vorschau
|
||||
- Thumbnails fuer Bilder
|
||||
- Icons fuer Dokumente (PDF, Word, Excel, etc.)
|
||||
- Neuer AJAX-Endpoint `ajax/file_preview.php`
|
||||
|
||||
- **Mobile/Kompakte Ansicht**: Optimiert fuer mobile Geraete
|
||||
- Kompakt-Modus Toggle-Button
|
||||
- Einheitliche Button-Groessen auf mobilen Geraeten
|
||||
- 2x2 Grid-Layout auf sehr kleinen Bildschirmen
|
||||
- Touch-freundliche Bedienelemente
|
||||
|
||||
### Bugfixes
|
||||
- **Dolibarr App Navigation**: Vor/Zurueck-Pfeile funktionieren jetzt korrekt
|
||||
- Module akzeptieren nun `id` UND `socid`/`contactid` Parameter
|
||||
- Kunden-Kontext bleibt beim Navigieren erhalten
|
||||
- Betroffene Dateien: alle Tab-PHP-Files
|
||||
|
||||
- **Datei-Badge Icon**: Zeigt jetzt Bueroklammer-Icon statt nur Zahl
|
||||
|
||||
### Datenbank-Aenderungen
|
||||
- Neue Spalte `badge_color` in `llx_kundenkarte_anlage_type_field`
|
||||
|
||||
---
|
||||
|
||||
## 3.5.0 (2026-02)
|
||||
|
||||
### Neue Features
|
||||
|
|
|
|||
|
|
@ -203,7 +203,14 @@ if ($action == 'add_field') {
|
|||
$fieldType = GETPOST('field_type', 'aZ09');
|
||||
$fieldOptions = GETPOST('field_options', 'nohtml');
|
||||
$showInTree = GETPOSTINT('show_in_tree');
|
||||
$treeDisplayMode = GETPOST('tree_display_mode', 'aZ09');
|
||||
if (empty($treeDisplayMode)) $treeDisplayMode = 'badge';
|
||||
$badgeColor = GETPOST('badge_color', 'alphanohtml');
|
||||
if ($badgeColor && !preg_match('/^#[0-9A-Fa-f]{6}$/', $badgeColor)) {
|
||||
$badgeColor = '';
|
||||
}
|
||||
$showInHover = GETPOSTINT('show_in_hover');
|
||||
$enableAutocomplete = GETPOSTINT('enable_autocomplete');
|
||||
$isRequired = GETPOSTINT('is_required');
|
||||
$fieldPosition = GETPOSTINT('field_position');
|
||||
|
||||
|
|
@ -211,10 +218,10 @@ if ($action == 'add_field') {
|
|||
setEventMessages($langs->trans('ErrorFieldRequired'), null, 'errors');
|
||||
} else {
|
||||
$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 .= " (fk_anlage_type, field_code, field_label, field_type, field_options, show_in_tree, tree_display_mode, badge_color, show_in_hover, enable_autocomplete, required, position, active)";
|
||||
$sql .= " VALUES (".((int) $typeId).", '".$db->escape($fieldCode)."', '".$db->escape($fieldLabel)."',";
|
||||
$sql .= " '".$db->escape($fieldType)."', '".$db->escape($fieldOptions)."',";
|
||||
$sql .= " ".((int) $showInTree).", ".((int) $showInHover).", ".((int) $isRequired).", ".((int) $fieldPosition).", 1)";
|
||||
$sql .= " ".((int) $showInTree).", '".$db->escape($treeDisplayMode)."', ".($badgeColor ? "'".$db->escape($badgeColor)."'" : "NULL").", ".((int) $showInHover).", ".((int) $enableAutocomplete).", ".((int) $isRequired).", ".((int) $fieldPosition).", 1)";
|
||||
|
||||
if ($db->query($sql)) {
|
||||
setEventMessages($langs->trans('RecordSaved'), null, 'mesgs');
|
||||
|
|
@ -231,7 +238,15 @@ if ($action == 'update_field') {
|
|||
$fieldType = GETPOST('field_type', 'aZ09');
|
||||
$fieldOptions = GETPOST('field_options', 'nohtml');
|
||||
$showInTree = GETPOSTINT('show_in_tree');
|
||||
$treeDisplayMode = GETPOST('tree_display_mode', 'aZ09');
|
||||
if (empty($treeDisplayMode)) $treeDisplayMode = 'badge';
|
||||
$badgeColor = GETPOST('badge_color', 'alphanohtml');
|
||||
// Validate color format (#RRGGBB)
|
||||
if ($badgeColor && !preg_match('/^#[0-9A-Fa-f]{6}$/', $badgeColor)) {
|
||||
$badgeColor = '';
|
||||
}
|
||||
$showInHover = GETPOSTINT('show_in_hover');
|
||||
$enableAutocomplete = GETPOSTINT('enable_autocomplete');
|
||||
$isRequired = GETPOSTINT('is_required');
|
||||
$fieldPosition = GETPOSTINT('field_position');
|
||||
|
||||
|
|
@ -241,7 +256,10 @@ if ($action == 'update_field') {
|
|||
$sql .= " field_type = '".$db->escape($fieldType)."',";
|
||||
$sql .= " field_options = '".$db->escape($fieldOptions)."',";
|
||||
$sql .= " show_in_tree = ".((int) $showInTree).",";
|
||||
$sql .= " tree_display_mode = '".$db->escape($treeDisplayMode)."',";
|
||||
$sql .= " badge_color = ".($badgeColor ? "'".$db->escape($badgeColor)."'" : "NULL").",";
|
||||
$sql .= " show_in_hover = ".((int) $showInHover).",";
|
||||
$sql .= " enable_autocomplete = ".((int) $enableAutocomplete).",";
|
||||
$sql .= " required = ".((int) $isRequired).",";
|
||||
$sql .= " position = ".((int) $fieldPosition);
|
||||
$sql .= " WHERE rowid = ".((int) $fieldId);
|
||||
|
|
@ -513,7 +531,10 @@ if (in_array($action, array('create', 'edit'))) {
|
|||
print '<th>'.$langs->trans('FieldType').'</th>';
|
||||
print '<th>'.$langs->trans('FieldOptions').'</th>';
|
||||
print '<th class="center">'.$langs->trans('ShowInTree').'</th>';
|
||||
print '<th class="center" title="Badge (rechts) oder Klammer (nach Bezeichnung)">'.$langs->trans('TreeDisplayMode').'</th>';
|
||||
print '<th class="center" title="Farbe des Badges im Baum"><i class="fa fa-paint-brush"></i></th>';
|
||||
print '<th class="center">'.$langs->trans('ShowInHover').'</th>';
|
||||
print '<th class="center" title="Autocomplete aus gespeicherten Werten"><i class="fa fa-magic"></i></th>';
|
||||
print '<th class="center">'.$langs->trans('IsRequired').'</th>';
|
||||
print '<th class="center">'.$langs->trans('Position').'</th>';
|
||||
print '<th class="center">'.$langs->trans('Status').'</th>';
|
||||
|
|
@ -537,7 +558,15 @@ if (in_array($action, array('create', 'edit'))) {
|
|||
print '</select></td>';
|
||||
print '<td><input type="text" name="field_options" form="'.$formId.'" class="flat minwidth100" value="'.dol_escape_htmltag($field->field_options).'" placeholder="opt1|opt2|opt3"></td>';
|
||||
print '<td class="center"><input type="checkbox" name="show_in_tree" form="'.$formId.'" value="1"'.($field->show_in_tree ? ' checked' : '').'></td>';
|
||||
$treeMode = $field->tree_display_mode ?? 'badge';
|
||||
print '<td class="center"><select name="tree_display_mode" form="'.$formId.'" class="flat" style="width:90px;">';
|
||||
print '<option value="badge"'.($treeMode == 'badge' ? ' selected' : '').'>'.$langs->trans('Badge').'</option>';
|
||||
print '<option value="parentheses"'.($treeMode == 'parentheses' ? ' selected' : '').'>'.$langs->trans('Parentheses').'</option>';
|
||||
print '</select></td>';
|
||||
$badgeColor = $field->badge_color ?? '';
|
||||
print '<td class="center"><input type="color" name="badge_color" form="'.$formId.'" value="'.($badgeColor ?: '#2a4a5e').'" style="width:30px;height:24px;padding:0;border:1px solid #ccc;cursor:pointer;" title="Badge-Farbe"></td>';
|
||||
print '<td class="center"><input type="checkbox" name="show_in_hover" form="'.$formId.'" value="1"'.($field->show_in_hover ? ' checked' : '').'></td>';
|
||||
print '<td class="center"><input type="checkbox" name="enable_autocomplete" form="'.$formId.'" value="1"'.($field->enable_autocomplete ? ' checked' : '').' title="Autocomplete aktivieren"></td>';
|
||||
print '<td class="center"><input type="checkbox" name="is_required" form="'.$formId.'" value="1"'.($field->required ? ' checked' : '').'></td>';
|
||||
print '<td class="center"><input type="number" name="field_position" form="'.$formId.'" class="flat" style="width:50px;" value="'.$field->position.'" min="0"></td>';
|
||||
print '<td></td>';
|
||||
|
|
@ -556,7 +585,17 @@ if (in_array($action, array('create', 'edit'))) {
|
|||
print '<td>'.dol_escape_htmltag($fieldTypes[$field->field_type] ?? $field->field_type).'</td>';
|
||||
print '<td class="small opacitymedium">'.dol_escape_htmltag(dol_trunc($field->field_options, 20)).'</td>';
|
||||
print '<td class="center">'.($field->show_in_tree ? img_picto('', 'tick') : '').'</td>';
|
||||
$treeMode = $field->tree_display_mode ?? 'badge';
|
||||
$treeModeIcon = ($treeMode == 'badge') ? '<span class="badge badge-secondary" style="font-size:9px;">B</span>' : '<span style="color:#666;">(K)</span>';
|
||||
print '<td class="center" title="'.($treeMode == 'badge' ? $langs->trans('Badge') : $langs->trans('Parentheses')).'">'.$treeModeIcon.'</td>';
|
||||
$badgeColor = $field->badge_color ?? '';
|
||||
if ($badgeColor) {
|
||||
print '<td class="center"><span style="display:inline-block;width:20px;height:20px;background:'.$badgeColor.';border-radius:3px;border:1px solid #555;"></span></td>';
|
||||
} else {
|
||||
print '<td class="center"><span style="color:#888;">-</span></td>';
|
||||
}
|
||||
print '<td class="center">'.($field->show_in_hover ? img_picto('', 'tick') : '').'</td>';
|
||||
print '<td class="center">'.($field->enable_autocomplete ? '<i class="fa fa-magic" style="color:#3498db;" title="Autocomplete aktiv"></i>' : '').'</td>';
|
||||
print '<td class="center">'.($field->required ? img_picto('', 'tick') : '').'</td>';
|
||||
print '<td class="center">'.$field->position.'</td>';
|
||||
print '<td class="center">';
|
||||
|
|
@ -577,7 +616,7 @@ if (in_array($action, array('create', 'edit'))) {
|
|||
}
|
||||
|
||||
if (empty($fields)) {
|
||||
print '<tr class="oddeven"><td colspan="10" class="opacitymedium">'.$langs->trans('NoFieldsDefined').'</td></tr>';
|
||||
print '<tr class="oddeven"><td colspan="12" class="opacitymedium">'.$langs->trans('NoFieldsDefined').'</td></tr>';
|
||||
}
|
||||
|
||||
print '</table>';
|
||||
|
|
@ -591,7 +630,7 @@ if (in_array($action, array('create', 'edit'))) {
|
|||
print '<input type="hidden" name="system" value="'.$systemFilter.'">';
|
||||
print '<table class="noborder centpercent">';
|
||||
print '<tr class="liste_titre">';
|
||||
print '<th colspan="10">'.$langs->trans('Add').' '.$langs->trans('Field').'</th>';
|
||||
print '<th colspan="12">'.$langs->trans('Add').' '.$langs->trans('Field').'</th>';
|
||||
print '</tr>';
|
||||
print '<tr class="oddeven">';
|
||||
print '<td><input type="text" name="field_code" class="flat minwidth100" placeholder="CODE" required></td>';
|
||||
|
|
@ -604,7 +643,13 @@ if (in_array($action, array('create', 'edit'))) {
|
|||
print '</select></td>';
|
||||
print '<td><input type="text" name="field_options" class="flat minwidth100" placeholder="opt1|opt2"></td>';
|
||||
print '<td class="center"><input type="checkbox" name="show_in_tree" value="1"></td>';
|
||||
print '<td class="center"><select name="tree_display_mode" class="flat" style="width:90px;">';
|
||||
print '<option value="badge">'.$langs->trans('Badge').'</option>';
|
||||
print '<option value="parentheses">'.$langs->trans('Parentheses').'</option>';
|
||||
print '</select></td>';
|
||||
print '<td class="center"><input type="color" name="badge_color" value="#2a4a5e" style="width:30px;height:24px;padding:0;border:1px solid #ccc;cursor:pointer;" title="Badge-Farbe"></td>';
|
||||
print '<td class="center"><input type="checkbox" name="show_in_hover" value="1" checked></td>';
|
||||
print '<td class="center"><input type="checkbox" name="enable_autocomplete" value="1" title="Autocomplete aktivieren"></td>';
|
||||
print '<td class="center"><input type="checkbox" name="is_required" value="1"></td>';
|
||||
print '<td class="center"><input type="number" name="field_position" class="flat" style="width:50px;" value="0" min="0"></td>';
|
||||
print '<td></td>';
|
||||
|
|
|
|||
364
admin/backup.php
Normal file
364
admin/backup.php
Normal file
|
|
@ -0,0 +1,364 @@
|
|||
<?php
|
||||
/* Copyright (C) 2026 Alles Watt lauft
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*/
|
||||
|
||||
/**
|
||||
* \file kundenkarte/admin/backup.php
|
||||
* \ingroup kundenkarte
|
||||
* \brief Backup and restore page for KundenKarte data
|
||||
*/
|
||||
|
||||
// Load Dolibarr environment
|
||||
$res = 0;
|
||||
if (!$res && file_exists("../../main.inc.php")) {
|
||||
$res = @include "../../main.inc.php";
|
||||
}
|
||||
if (!$res && file_exists("../../../main.inc.php")) {
|
||||
$res = @include "../../../main.inc.php";
|
||||
}
|
||||
if (!$res) {
|
||||
die("Include of main fails");
|
||||
}
|
||||
|
||||
// Libraries
|
||||
require_once DOL_DOCUMENT_ROOT."/core/lib/admin.lib.php";
|
||||
require_once DOL_DOCUMENT_ROOT."/core/class/html.form.class.php";
|
||||
require_once '../lib/kundenkarte.lib.php';
|
||||
dol_include_once('/kundenkarte/class/anlagebackup.class.php');
|
||||
|
||||
// Translations
|
||||
$langs->loadLangs(array("admin", "kundenkarte@kundenkarte"));
|
||||
|
||||
// Access control
|
||||
if (!$user->admin && !$user->hasRight('kundenkarte', 'admin')) {
|
||||
accessforbidden();
|
||||
}
|
||||
|
||||
// Parameters
|
||||
$action = GETPOST('action', 'aZ09');
|
||||
$confirm = GETPOST('confirm', 'alpha');
|
||||
|
||||
$form = new Form($db);
|
||||
$backup = new AnlageBackup($db);
|
||||
|
||||
/*
|
||||
* Actions
|
||||
*/
|
||||
|
||||
// Create backup
|
||||
if ($action == 'create_backup') {
|
||||
$includeFiles = GETPOSTINT('include_files');
|
||||
|
||||
$result = $backup->createBackup($includeFiles);
|
||||
if ($result) {
|
||||
setEventMessages($langs->trans('BackupCreatedSuccess', basename($result)), null, 'mesgs');
|
||||
} else {
|
||||
setEventMessages($langs->trans('BackupCreatedError').': '.$backup->error, null, 'errors');
|
||||
}
|
||||
|
||||
header('Location: '.$_SERVER['PHP_SELF']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Download backup
|
||||
if ($action == 'download') {
|
||||
$filename = GETPOST('file', 'alpha');
|
||||
$filepath = $conf->kundenkarte->dir_output.'/backups/'.basename($filename);
|
||||
|
||||
if (file_exists($filepath) && strpos($filename, 'kundenkarte_backup_') === 0) {
|
||||
header('Content-Type: application/zip');
|
||||
header('Content-Disposition: attachment; filename="'.basename($filepath).'"');
|
||||
header('Content-Length: '.filesize($filepath));
|
||||
readfile($filepath);
|
||||
exit;
|
||||
} else {
|
||||
setEventMessages($langs->trans('FileNotFound'), null, 'errors');
|
||||
}
|
||||
}
|
||||
|
||||
// Delete backup
|
||||
if ($action == 'confirm_delete' && $confirm == 'yes') {
|
||||
$filename = GETPOST('file', 'alpha');
|
||||
if ($backup->deleteBackup($filename)) {
|
||||
setEventMessages($langs->trans('BackupDeleted'), null, 'mesgs');
|
||||
} else {
|
||||
setEventMessages($langs->trans('Error'), null, 'errors');
|
||||
}
|
||||
|
||||
header('Location: '.$_SERVER['PHP_SELF']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Restore backup
|
||||
if ($action == 'confirm_restore' && $confirm == 'yes') {
|
||||
$filename = GETPOST('file', 'alpha');
|
||||
$clearExisting = GETPOSTINT('clear_existing');
|
||||
|
||||
$filepath = $conf->kundenkarte->dir_output.'/backups/'.basename($filename);
|
||||
|
||||
if ($backup->restoreBackup($filepath, $clearExisting)) {
|
||||
setEventMessages($langs->trans('RestoreSuccess'), null, 'mesgs');
|
||||
} else {
|
||||
setEventMessages($langs->trans('RestoreError').': '.$backup->error, null, 'errors');
|
||||
}
|
||||
|
||||
header('Location: '.$_SERVER['PHP_SELF']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Upload backup
|
||||
if ($action == 'upload_backup' && !empty($_FILES['backupfile']['name'])) {
|
||||
$backupDir = $conf->kundenkarte->dir_output.'/backups';
|
||||
if (!is_dir($backupDir)) {
|
||||
dol_mkdir($backupDir);
|
||||
}
|
||||
|
||||
$filename = $_FILES['backupfile']['name'];
|
||||
|
||||
// Validate filename format
|
||||
if (!preg_match('/^kundenkarte_backup_\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}\.zip$/', $filename)) {
|
||||
setEventMessages($langs->trans('InvalidBackupFile'), null, 'errors');
|
||||
} elseif ($_FILES['backupfile']['error'] !== UPLOAD_ERR_OK) {
|
||||
setEventMessages($langs->trans('ErrorUploadFailed'), null, 'errors');
|
||||
} else {
|
||||
$targetPath = $backupDir.'/'.$filename;
|
||||
if (move_uploaded_file($_FILES['backupfile']['tmp_name'], $targetPath)) {
|
||||
setEventMessages($langs->trans('BackupUploaded'), null, 'mesgs');
|
||||
} else {
|
||||
setEventMessages($langs->trans('ErrorUploadFailed'), null, 'errors');
|
||||
}
|
||||
}
|
||||
|
||||
header('Location: '.$_SERVER['PHP_SELF']);
|
||||
exit;
|
||||
}
|
||||
|
||||
/*
|
||||
* View
|
||||
*/
|
||||
|
||||
$title = $langs->trans("BackupRestore");
|
||||
llxHeader('', $title);
|
||||
|
||||
// Subheader
|
||||
$linkback = '<a href="'.DOL_URL_ROOT.'/admin/modules.php?restore_lastsearch_values=1">'.$langs->trans("BackToModuleList").'</a>';
|
||||
print load_fiche_titre($title, $linkback, 'title_setup');
|
||||
|
||||
// Configuration header
|
||||
$head = kundenkarteAdminPrepareHead();
|
||||
print dol_get_fiche_head($head, 'backup', $langs->trans('ModuleKundenKarteName'), -1, "fa-address-card");
|
||||
|
||||
// Confirmation dialogs
|
||||
if ($action == 'delete') {
|
||||
$filename = GETPOST('file', 'alpha');
|
||||
print $form->formconfirm(
|
||||
$_SERVER['PHP_SELF'].'?file='.urlencode($filename),
|
||||
$langs->trans('DeleteBackup'),
|
||||
$langs->trans('ConfirmDeleteBackup', $filename),
|
||||
'confirm_delete',
|
||||
'',
|
||||
'yes',
|
||||
1
|
||||
);
|
||||
}
|
||||
|
||||
if ($action == 'restore') {
|
||||
$filename = GETPOST('file', 'alpha');
|
||||
|
||||
$formquestion = array(
|
||||
array(
|
||||
'type' => 'checkbox',
|
||||
'name' => 'clear_existing',
|
||||
'label' => $langs->trans('ClearExistingData'),
|
||||
'value' => 0
|
||||
)
|
||||
);
|
||||
|
||||
print $form->formconfirm(
|
||||
$_SERVER['PHP_SELF'].'?file='.urlencode($filename),
|
||||
$langs->trans('RestoreBackup'),
|
||||
$langs->trans('ConfirmRestoreBackup', $filename),
|
||||
'confirm_restore',
|
||||
$formquestion,
|
||||
'yes',
|
||||
1
|
||||
);
|
||||
}
|
||||
|
||||
// Statistics
|
||||
$stats = $backup->getStatistics();
|
||||
|
||||
print '<div class="fichecenter">';
|
||||
|
||||
// Stats cards
|
||||
print '<div style="display:flex;gap:20px;flex-wrap:wrap;margin-bottom:30px;">';
|
||||
|
||||
print '<div class="info-box" style="min-width:200px;">';
|
||||
print '<span class="info-box-icon bg-primary"><i class="fa fa-sitemap"></i></span>';
|
||||
print '<div class="info-box-content">';
|
||||
print '<span class="info-box-text">'.$langs->trans('TotalElements').'</span>';
|
||||
print '<span class="info-box-number">'.$stats['total_anlagen'].'</span>';
|
||||
print '</div>';
|
||||
print '</div>';
|
||||
|
||||
print '<div class="info-box" style="min-width:200px;">';
|
||||
print '<span class="info-box-icon bg-success"><i class="fa fa-file"></i></span>';
|
||||
print '<div class="info-box-content">';
|
||||
print '<span class="info-box-text">'.$langs->trans('TotalFiles').'</span>';
|
||||
print '<span class="info-box-number">'.$stats['total_files'].'</span>';
|
||||
print '</div>';
|
||||
print '</div>';
|
||||
|
||||
print '<div class="info-box" style="min-width:200px;">';
|
||||
print '<span class="info-box-icon bg-warning"><i class="fa fa-plug"></i></span>';
|
||||
print '<div class="info-box-content">';
|
||||
print '<span class="info-box-text">'.$langs->trans('TotalConnections').'</span>';
|
||||
print '<span class="info-box-number">'.$stats['total_connections'].'</span>';
|
||||
print '</div>';
|
||||
print '</div>';
|
||||
|
||||
print '<div class="info-box" style="min-width:200px;">';
|
||||
print '<span class="info-box-icon bg-info"><i class="fa fa-building"></i></span>';
|
||||
print '<div class="info-box-content">';
|
||||
print '<span class="info-box-text">'.$langs->trans('CustomersWithAnlagen').'</span>';
|
||||
print '<span class="info-box-number">'.$stats['total_customers'].'</span>';
|
||||
print '</div>';
|
||||
print '</div>';
|
||||
|
||||
print '<div class="info-box" style="min-width:200px;">';
|
||||
print '<span class="info-box-icon bg-secondary"><i class="fa fa-hdd-o"></i></span>';
|
||||
print '<div class="info-box-content">';
|
||||
print '<span class="info-box-text">'.$langs->trans('FilesStorageSize').'</span>';
|
||||
print '<span class="info-box-number">'.dol_print_size($stats['files_size']).'</span>';
|
||||
print '</div>';
|
||||
print '</div>';
|
||||
|
||||
print '</div>';
|
||||
|
||||
// Create Backup Section
|
||||
print '<div class="titre inline-block">'.$langs->trans("CreateBackup").'</div>';
|
||||
print '<br><br>';
|
||||
|
||||
print '<form method="POST" action="'.$_SERVER['PHP_SELF'].'">';
|
||||
print '<input type="hidden" name="token" value="'.newToken().'">';
|
||||
print '<input type="hidden" name="action" value="create_backup">';
|
||||
|
||||
print '<table class="noborder centpercent">';
|
||||
print '<tr class="liste_titre">';
|
||||
print '<td colspan="2">'.$langs->trans("BackupOptions").'</td>';
|
||||
print '</tr>';
|
||||
|
||||
print '<tr class="oddeven">';
|
||||
print '<td style="width:300px;">'.$langs->trans("IncludeUploadedFiles").'</td>';
|
||||
print '<td>';
|
||||
print '<input type="checkbox" name="include_files" value="1" checked> ';
|
||||
print '<span class="opacitymedium">'.$langs->trans("IncludeFilesHelp").'</span>';
|
||||
print '</td>';
|
||||
print '</tr>';
|
||||
|
||||
print '</table>';
|
||||
|
||||
print '<br>';
|
||||
print '<div class="center">';
|
||||
print '<button type="submit" class="button button-primary">';
|
||||
print '<i class="fa fa-download"></i> '.$langs->trans("CreateBackupNow");
|
||||
print '</button>';
|
||||
print '</div>';
|
||||
|
||||
print '</form>';
|
||||
|
||||
// Upload Backup Section
|
||||
print '<br><br>';
|
||||
print '<div class="titre inline-block">'.$langs->trans("UploadBackup").'</div>';
|
||||
print '<br><br>';
|
||||
|
||||
print '<form method="POST" action="'.$_SERVER['PHP_SELF'].'" enctype="multipart/form-data">';
|
||||
print '<input type="hidden" name="token" value="'.newToken().'">';
|
||||
print '<input type="hidden" name="action" value="upload_backup">';
|
||||
|
||||
print '<table class="noborder centpercent">';
|
||||
print '<tr class="liste_titre">';
|
||||
print '<td colspan="2">'.$langs->trans("UploadBackupFile").'</td>';
|
||||
print '</tr>';
|
||||
|
||||
print '<tr class="oddeven">';
|
||||
print '<td style="width:300px;">'.$langs->trans("SelectBackupFile").'</td>';
|
||||
print '<td>';
|
||||
print '<input type="file" name="backupfile" accept=".zip" class="flat">';
|
||||
print ' <button type="submit" class="button buttongen smallpaddingimp">'.$langs->trans("Upload").'</button>';
|
||||
print '</td>';
|
||||
print '</tr>';
|
||||
|
||||
print '</table>';
|
||||
|
||||
print '</form>';
|
||||
|
||||
// Existing Backups Section
|
||||
print '<br><br>';
|
||||
print '<div class="titre inline-block">'.$langs->trans("ExistingBackups").'</div>';
|
||||
print '<br><br>';
|
||||
|
||||
$backups = $backup->getBackupList();
|
||||
|
||||
print '<table class="noborder centpercent">';
|
||||
print '<tr class="liste_titre">';
|
||||
print '<td>'.$langs->trans("Filename").'</td>';
|
||||
print '<td>'.$langs->trans("Date").'</td>';
|
||||
print '<td class="right">'.$langs->trans("Size").'</td>';
|
||||
print '<td class="center">'.$langs->trans("Actions").'</td>';
|
||||
print '</tr>';
|
||||
|
||||
if (empty($backups)) {
|
||||
print '<tr class="oddeven">';
|
||||
print '<td colspan="4" class="opacitymedium center">'.$langs->trans("NoBackupsFound").'</td>';
|
||||
print '</tr>';
|
||||
} else {
|
||||
foreach ($backups as $bk) {
|
||||
print '<tr class="oddeven">';
|
||||
print '<td><i class="fa fa-file-archive-o"></i> '.dol_escape_htmltag($bk['filename']).'</td>';
|
||||
print '<td>'.dol_print_date(strtotime($bk['date']), 'dayhour').'</td>';
|
||||
print '<td class="right">'.dol_print_size($bk['size']).'</td>';
|
||||
print '<td class="center nowraponall">';
|
||||
|
||||
// Download button
|
||||
print '<a href="'.$_SERVER['PHP_SELF'].'?action=download&file='.urlencode($bk['filename']).'&token='.newToken().'" class="button buttongen smallpaddingimp" title="'.$langs->trans('Download').'">';
|
||||
print '<i class="fa fa-download"></i>';
|
||||
print '</a> ';
|
||||
|
||||
// Restore button
|
||||
print '<a href="'.$_SERVER['PHP_SELF'].'?action=restore&file='.urlencode($bk['filename']).'&token='.newToken().'" class="button buttongen smallpaddingimp" title="'.$langs->trans('Restore').'">';
|
||||
print '<i class="fa fa-undo"></i>';
|
||||
print '</a> ';
|
||||
|
||||
// Delete button
|
||||
print '<a href="'.$_SERVER['PHP_SELF'].'?action=delete&file='.urlencode($bk['filename']).'&token='.newToken().'" class="button buttongen smallpaddingimp" title="'.$langs->trans('Delete').'">';
|
||||
print '<i class="fa fa-trash"></i>';
|
||||
print '</a>';
|
||||
|
||||
print '</td>';
|
||||
print '</tr>';
|
||||
}
|
||||
}
|
||||
|
||||
print '</table>';
|
||||
|
||||
// Info section
|
||||
print '<br>';
|
||||
print '<div class="info">';
|
||||
print '<strong><i class="fa fa-info-circle"></i> '.$langs->trans("BackupInfo").':</strong><br>';
|
||||
print '• '.$langs->trans("BackupInfoContent").'<br>';
|
||||
print '• '.$langs->trans("BackupInfoFiles").'<br>';
|
||||
print '• '.$langs->trans("BackupInfoRestore").'<br>';
|
||||
print '</div>';
|
||||
|
||||
print '</div>';
|
||||
|
||||
print dol_get_fiche_end();
|
||||
|
||||
llxFooter();
|
||||
$db->close();
|
||||
|
|
@ -122,6 +122,10 @@ if ($action == 'update') {
|
|||
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);
|
||||
|
||||
// Tree display settings
|
||||
dolibarr_set_const($db, 'KUNDENKARTE_TREE_INFO_DISPLAY', GETPOST('KUNDENKARTE_TREE_INFO_DISPLAY', 'aZ09'), 'chaine', 0, '', $conf->entity);
|
||||
dolibarr_set_const($db, 'KUNDENKARTE_TREE_BADGE_COLOR', GETPOST('KUNDENKARTE_TREE_BADGE_COLOR', 'alphanohtml'), 'chaine', 0, '', $conf->entity);
|
||||
|
||||
if (!$error) {
|
||||
setEventMessages($langs->trans("SetupSaved"), null, 'mesgs');
|
||||
} else {
|
||||
|
|
@ -190,6 +194,86 @@ print '</tr>';
|
|||
|
||||
print '</table>';
|
||||
|
||||
// Tree Display Settings
|
||||
print '<br><br>';
|
||||
print '<div class="titre inline-block">'.$langs->trans("TreeDisplaySettings").'</div>';
|
||||
print '<br><br>';
|
||||
|
||||
print '<table class="noborder centpercent">';
|
||||
print '<tr class="liste_titre">';
|
||||
print '<td>'.$langs->trans("Parameter").'</td>';
|
||||
print '<td>'.$langs->trans("Value").'</td>';
|
||||
print '<td>'.$langs->trans("Preview").'</td>';
|
||||
print '</tr>';
|
||||
|
||||
// Tree info display mode
|
||||
$currentDisplay = getDolGlobalString('KUNDENKARTE_TREE_INFO_DISPLAY', 'badge');
|
||||
print '<tr class="oddeven">';
|
||||
print '<td>'.$langs->trans("TreeInfoDisplayMode").'</td>';
|
||||
print '<td>';
|
||||
print '<select name="KUNDENKARTE_TREE_INFO_DISPLAY" class="flat" id="tree_info_display">';
|
||||
print '<option value="badge"'.($currentDisplay == 'badge' ? ' selected' : '').'>'.$langs->trans("DisplayAsBadge").'</option>';
|
||||
print '<option value="parentheses"'.($currentDisplay == 'parentheses' ? ' selected' : '').'>'.$langs->trans("DisplayInParentheses").'</option>';
|
||||
print '<option value="none"'.($currentDisplay == 'none' ? ' selected' : '').'>'.$langs->trans("DisplayNone").'</option>';
|
||||
print '</select>';
|
||||
print '</td>';
|
||||
print '<td>';
|
||||
print '<span id="preview_badge" class="kundenkarte-tree-badge-preview" style="display:'.($currentDisplay == 'badge' ? 'inline-flex' : 'none').';align-items:center;gap:4px;padding:2px 8px;background:linear-gradient(135deg,#2a4a5e 0%,#1e3a4a 100%);border:1px solid #3a6a8e;border-radius:12px;font-size:11px;color:#8cc4e8;"><i class="fa fa-map-marker"></i> Serverraum</span>';
|
||||
print '<span id="preview_parentheses" style="display:'.($currentDisplay == 'parentheses' ? 'inline' : 'none').';color:#999;font-size:0.9em;">(Standort: Serverraum)</span>';
|
||||
print '<span id="preview_none" style="display:'.($currentDisplay == 'none' ? 'inline' : 'none').';color:#999;font-style:italic;">'.$langs->trans("Hidden").'</span>';
|
||||
print '</td>';
|
||||
print '</tr>';
|
||||
|
||||
// Badge color
|
||||
$currentColor = getDolGlobalString('KUNDENKARTE_TREE_BADGE_COLOR', '#2a4a5e');
|
||||
print '<tr class="oddeven" id="row_badge_color"'.($currentDisplay != 'badge' ? ' style="display:none;"' : '').'>';
|
||||
print '<td>'.$langs->trans("TreeBadgeColor").'</td>';
|
||||
print '<td>';
|
||||
print '<input type="color" name="KUNDENKARTE_TREE_BADGE_COLOR" id="badge_color" value="'.dol_escape_htmltag($currentColor).'" style="width:60px;height:30px;border:1px solid #ccc;border-radius:4px;cursor:pointer;">';
|
||||
print ' <input type="text" id="badge_color_hex" value="'.dol_escape_htmltag($currentColor).'" style="width:80px;" class="flat" readonly>';
|
||||
print '</td>';
|
||||
print '<td class="opacitymedium small">'.$langs->trans("TreeBadgeColorHelp").'</td>';
|
||||
print '</tr>';
|
||||
|
||||
print '</table>';
|
||||
|
||||
print '<script>
|
||||
$(document).ready(function() {
|
||||
// Update preview and show/hide color row on display mode change
|
||||
$("#tree_info_display").on("change", function() {
|
||||
var mode = $(this).val();
|
||||
$("#preview_badge, #preview_parentheses, #preview_none").hide();
|
||||
if (mode === "badge") {
|
||||
$("#preview_badge").show();
|
||||
$("#row_badge_color").show();
|
||||
} else if (mode === "parentheses") {
|
||||
$("#preview_parentheses").show();
|
||||
$("#row_badge_color").hide();
|
||||
} else {
|
||||
$("#preview_none").show();
|
||||
$("#row_badge_color").hide();
|
||||
}
|
||||
});
|
||||
|
||||
// Update color preview and hex input
|
||||
$("#badge_color").on("input", function() {
|
||||
var color = $(this).val();
|
||||
$("#badge_color_hex").val(color);
|
||||
// Update preview badge background
|
||||
var lighterColor = color;
|
||||
$("#preview_badge").css("background", "linear-gradient(135deg, " + color + " 0%, " + adjustColor(color, -20) + " 100%)");
|
||||
});
|
||||
|
||||
function adjustColor(hex, percent) {
|
||||
var num = parseInt(hex.slice(1), 16);
|
||||
var r = Math.min(255, Math.max(0, (num >> 16) + percent));
|
||||
var g = Math.min(255, Math.max(0, ((num >> 8) & 0x00FF) + percent));
|
||||
var b = Math.min(255, Math.max(0, (num & 0x0000FF) + percent));
|
||||
return "#" + (0x1000000 + (r << 16) + (g << 8) + b).toString(16).slice(1);
|
||||
}
|
||||
});
|
||||
</script>';
|
||||
|
||||
// PDF Font Size Settings
|
||||
print '<br><br>';
|
||||
print '<div class="titre inline-block">'.$langs->trans("PDFFontSettings").'</div>';
|
||||
|
|
|
|||
106
ajax/field_autocomplete.php
Normal file
106
ajax/field_autocomplete.php
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
<?php
|
||||
/* Copyright (C) 2026 Alles Watt lauft
|
||||
*
|
||||
* AJAX endpoint for field autocomplete suggestions
|
||||
* Returns unique values from saved anlage field values
|
||||
*/
|
||||
|
||||
// Load Dolibarr environment
|
||||
$res = 0;
|
||||
if (!$res && file_exists("../main.inc.php")) {
|
||||
$res = @include "../main.inc.php";
|
||||
}
|
||||
if (!$res && file_exists("../../main.inc.php")) {
|
||||
$res = @include "../../main.inc.php";
|
||||
}
|
||||
if (!$res && file_exists("../../../main.inc.php")) {
|
||||
$res = @include "../../../main.inc.php";
|
||||
}
|
||||
if (!$res) {
|
||||
die("Include of main fails");
|
||||
}
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
// Check permissions
|
||||
if (!$user->hasRight('kundenkarte', 'read')) {
|
||||
echo json_encode(array('error' => 'Access denied'));
|
||||
exit;
|
||||
}
|
||||
|
||||
$action = GETPOST('action', 'aZ09');
|
||||
$fieldCode = GETPOST('field_code', 'aZ09');
|
||||
$query = GETPOST('query', 'alphanohtml');
|
||||
$typeId = GETPOSTINT('type_id');
|
||||
|
||||
if ($action == 'suggest') {
|
||||
$suggestions = array();
|
||||
|
||||
if (empty($fieldCode)) {
|
||||
echo json_encode(array('suggestions' => $suggestions));
|
||||
exit;
|
||||
}
|
||||
|
||||
// Get unique saved values for this field code
|
||||
// Search in JSON field_values column
|
||||
$sql = "SELECT DISTINCT ";
|
||||
$sql .= "JSON_UNQUOTE(JSON_EXTRACT(field_values, '$.\"".($db->escape($fieldCode))."\"')) as field_value ";
|
||||
$sql .= "FROM ".MAIN_DB_PREFIX."kundenkarte_anlage ";
|
||||
$sql .= "WHERE field_values IS NOT NULL ";
|
||||
$sql .= "AND JSON_EXTRACT(field_values, '$.\"".($db->escape($fieldCode))."\"') IS NOT NULL ";
|
||||
|
||||
// Filter by query if provided
|
||||
if (!empty($query)) {
|
||||
$sql .= "AND JSON_UNQUOTE(JSON_EXTRACT(field_values, '$.\"".($db->escape($fieldCode))."\"')) LIKE '%".$db->escape($query)."%' ";
|
||||
}
|
||||
|
||||
// Optionally filter by type
|
||||
if ($typeId > 0) {
|
||||
$sql .= "AND fk_anlage_type = ".((int) $typeId)." ";
|
||||
}
|
||||
|
||||
$sql .= "ORDER BY field_value ASC ";
|
||||
$sql .= "LIMIT 20";
|
||||
|
||||
$resql = $db->query($sql);
|
||||
if ($resql) {
|
||||
while ($obj = $db->fetch_object($resql)) {
|
||||
if (!empty($obj->field_value) && $obj->field_value !== 'null') {
|
||||
$suggestions[] = $obj->field_value;
|
||||
}
|
||||
}
|
||||
$db->free($resql);
|
||||
}
|
||||
|
||||
echo json_encode(array('suggestions' => $suggestions));
|
||||
exit;
|
||||
}
|
||||
|
||||
// Get all autocomplete-enabled fields for a type
|
||||
if ($action == 'get_autocomplete_fields') {
|
||||
$fields = array();
|
||||
|
||||
if ($typeId > 0) {
|
||||
$sql = "SELECT field_code, field_label FROM ".MAIN_DB_PREFIX."kundenkarte_anlage_type_field ";
|
||||
$sql .= "WHERE fk_anlage_type = ".((int) $typeId)." ";
|
||||
$sql .= "AND enable_autocomplete = 1 ";
|
||||
$sql .= "AND active = 1 ";
|
||||
$sql .= "AND field_type IN ('text', 'textarea')";
|
||||
|
||||
$resql = $db->query($sql);
|
||||
if ($resql) {
|
||||
while ($obj = $db->fetch_object($resql)) {
|
||||
$fields[] = array(
|
||||
'code' => $obj->field_code,
|
||||
'label' => $obj->field_label
|
||||
);
|
||||
}
|
||||
$db->free($resql);
|
||||
}
|
||||
}
|
||||
|
||||
echo json_encode(array('fields' => $fields));
|
||||
exit;
|
||||
}
|
||||
|
||||
echo json_encode(array('error' => 'Unknown action'));
|
||||
101
ajax/file_preview.php
Normal file
101
ajax/file_preview.php
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
<?php
|
||||
/* Copyright (C) 2026 Alles Watt lauft
|
||||
*
|
||||
* AJAX endpoint to get file preview data for an installation element
|
||||
*/
|
||||
|
||||
if (!defined('NOTOKENRENEWAL')) define('NOTOKENRENEWAL', '1');
|
||||
if (!defined('NOREQUIREHTML')) define('NOREQUIREHTML', '1');
|
||||
if (!defined('NOREQUIREAJAX')) define('NOREQUIREAJAX', '1');
|
||||
if (!defined('NOREQUIRESOC')) define('NOREQUIRESOC', '1');
|
||||
|
||||
$res = 0;
|
||||
if (!$res && file_exists("../../../main.inc.php")) $res = @include "../../../main.inc.php";
|
||||
if (!$res && file_exists("../../../../main.inc.php")) $res = @include "../../../../main.inc.php";
|
||||
if (!$res) die("Include of main fails");
|
||||
|
||||
dol_include_once('/kundenkarte/class/anlagefile.class.php');
|
||||
dol_include_once('/kundenkarte/class/anlage.class.php');
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
// Permission check
|
||||
if (!$user->hasRight('kundenkarte', 'read')) {
|
||||
http_response_code(403);
|
||||
echo json_encode(['error' => 'Permission denied']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$anlageId = GETPOSTINT('anlage_id');
|
||||
|
||||
if ($anlageId <= 0) {
|
||||
echo json_encode(array('error' => 'Invalid ID', 'files' => array()));
|
||||
exit;
|
||||
}
|
||||
|
||||
// Get the anlage
|
||||
$anlage = new Anlage($db);
|
||||
if ($anlage->fetch($anlageId) <= 0) {
|
||||
echo json_encode(array('error' => 'Element not found', 'files' => array()));
|
||||
exit;
|
||||
}
|
||||
|
||||
// Get all files for this element
|
||||
$anlagefile = new AnlageFile($db);
|
||||
$files = $anlagefile->fetchAllByAnlage($anlageId);
|
||||
|
||||
$images = array();
|
||||
$documents = array();
|
||||
|
||||
foreach ($files as $file) {
|
||||
$fileData = array(
|
||||
'id' => $file->id,
|
||||
'name' => $file->filename,
|
||||
'url' => $file->getUrl(),
|
||||
'type' => $file->file_type,
|
||||
'is_pinned' => (int)$file->is_pinned,
|
||||
'is_cover' => (int)$file->is_cover,
|
||||
);
|
||||
|
||||
if ($file->file_type == 'image') {
|
||||
// Add thumbnail URL for images
|
||||
$fileData['thumb_url'] = $file->getThumbnailUrl();
|
||||
$images[] = $fileData;
|
||||
} else {
|
||||
// Determine icon based on file type
|
||||
switch ($file->file_type) {
|
||||
case 'pdf':
|
||||
$fileData['icon'] = 'fa-file-pdf-o';
|
||||
$fileData['color'] = '#e74c3c';
|
||||
break;
|
||||
case 'archive':
|
||||
$fileData['icon'] = 'fa-file-archive-o';
|
||||
$fileData['color'] = '#9b59b6';
|
||||
break;
|
||||
default:
|
||||
// Check extension for more specific icons
|
||||
$ext = strtolower(pathinfo($file->filename, PATHINFO_EXTENSION));
|
||||
if (in_array($ext, array('doc', 'docx'))) {
|
||||
$fileData['icon'] = 'fa-file-word-o';
|
||||
$fileData['color'] = '#2980b9';
|
||||
} elseif (in_array($ext, array('xls', 'xlsx'))) {
|
||||
$fileData['icon'] = 'fa-file-excel-o';
|
||||
$fileData['color'] = '#27ae60';
|
||||
} elseif (in_array($ext, array('txt', 'rtf'))) {
|
||||
$fileData['icon'] = 'fa-file-text-o';
|
||||
$fileData['color'] = '#f39c12';
|
||||
} else {
|
||||
$fileData['icon'] = 'fa-file-o';
|
||||
$fileData['color'] = '#7f8c8d';
|
||||
}
|
||||
}
|
||||
$documents[] = $fileData;
|
||||
}
|
||||
}
|
||||
|
||||
echo json_encode(array(
|
||||
'images' => $images,
|
||||
'documents' => $documents,
|
||||
'total_images' => count($images),
|
||||
'total_documents' => count($documents),
|
||||
));
|
||||
|
|
@ -49,9 +49,13 @@ foreach ($fields as $field) {
|
|||
'type' => $field->field_type,
|
||||
'options' => $field->field_options,
|
||||
'required' => (int)$field->required === 1,
|
||||
'autocomplete' => (int)$field->enable_autocomplete === 1,
|
||||
'value' => isset($existingValues[$field->field_code]) ? $existingValues[$field->field_code] : ''
|
||||
);
|
||||
$result['fields'][] = $fieldData;
|
||||
}
|
||||
|
||||
// Also include type_id for autocomplete
|
||||
$result['type_id'] = $typeId;
|
||||
|
||||
echo json_encode($result);
|
||||
|
|
|
|||
558
class/anlagebackup.class.php
Normal file
558
class/anlagebackup.class.php
Normal file
|
|
@ -0,0 +1,558 @@
|
|||
<?php
|
||||
/* Copyright (C) 2026 Alles Watt lauft
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Class AnlageBackup
|
||||
* Handles backup and restore of all installation data
|
||||
*/
|
||||
class AnlageBackup
|
||||
{
|
||||
public $db;
|
||||
public $error;
|
||||
public $errors = array();
|
||||
|
||||
// Tables to backup (in order for foreign key constraints)
|
||||
private $tables = array(
|
||||
'kundenkarte_anlage_system',
|
||||
'kundenkarte_anlage_type',
|
||||
'kundenkarte_anlage_type_field',
|
||||
'kundenkarte_customer_system',
|
||||
'kundenkarte_anlage',
|
||||
'kundenkarte_anlage_files',
|
||||
'kundenkarte_anlage_connection',
|
||||
'kundenkarte_equipment_panel',
|
||||
'kundenkarte_equipment_carrier',
|
||||
'kundenkarte_equipment',
|
||||
'kundenkarte_medium_type',
|
||||
'kundenkarte_busbar_type',
|
||||
'kundenkarte_building_type',
|
||||
);
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param DoliDB $db Database handler
|
||||
*/
|
||||
public function __construct($db)
|
||||
{
|
||||
$this->db = $db;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a full backup
|
||||
*
|
||||
* @param bool $includeFiles Include uploaded files in backup
|
||||
* @return string|false Path to backup file or false on error
|
||||
*/
|
||||
public function createBackup($includeFiles = true)
|
||||
{
|
||||
global $conf;
|
||||
|
||||
$backupDir = $conf->kundenkarte->dir_output.'/backups';
|
||||
if (!is_dir($backupDir)) {
|
||||
dol_mkdir($backupDir);
|
||||
}
|
||||
|
||||
$timestamp = date('Y-m-d_H-i-s');
|
||||
$backupName = 'kundenkarte_backup_'.$timestamp;
|
||||
$tempDir = $backupDir.'/'.$backupName;
|
||||
|
||||
if (!dol_mkdir($tempDir)) {
|
||||
$this->error = 'Cannot create backup directory';
|
||||
return false;
|
||||
}
|
||||
|
||||
// Export database tables
|
||||
$dbData = $this->exportDatabaseTables();
|
||||
if ($dbData === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Save database export as JSON
|
||||
$dbFile = $tempDir.'/database.json';
|
||||
if (file_put_contents($dbFile, json_encode($dbData, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)) === false) {
|
||||
$this->error = 'Cannot write database file';
|
||||
return false;
|
||||
}
|
||||
|
||||
// Create metadata file
|
||||
$metadata = array(
|
||||
'version' => '3.6.0',
|
||||
'created' => date('Y-m-d H:i:s'),
|
||||
'tables' => array_keys($dbData),
|
||||
'record_counts' => array(),
|
||||
'includes_files' => $includeFiles,
|
||||
);
|
||||
foreach ($dbData as $table => $records) {
|
||||
$metadata['record_counts'][$table] = count($records);
|
||||
}
|
||||
file_put_contents($tempDir.'/metadata.json', json_encode($metadata, JSON_PRETTY_PRINT));
|
||||
|
||||
// Copy uploaded files if requested
|
||||
if ($includeFiles) {
|
||||
$filesDir = $conf->kundenkarte->dir_output.'/anlagen';
|
||||
if (is_dir($filesDir)) {
|
||||
$this->copyDirectory($filesDir, $tempDir.'/files');
|
||||
}
|
||||
}
|
||||
|
||||
// Create ZIP archive
|
||||
$zipFile = $backupDir.'/'.$backupName.'.zip';
|
||||
if (!$this->createZipArchive($tempDir, $zipFile)) {
|
||||
$this->error = 'Cannot create ZIP archive';
|
||||
return false;
|
||||
}
|
||||
|
||||
// Clean up temp directory
|
||||
$this->deleteDirectory($tempDir);
|
||||
|
||||
return $zipFile;
|
||||
}
|
||||
|
||||
/**
|
||||
* Export all database tables
|
||||
*
|
||||
* @return array|false Array of table data or false on error
|
||||
*/
|
||||
private function exportDatabaseTables()
|
||||
{
|
||||
global $conf;
|
||||
|
||||
$data = array();
|
||||
|
||||
foreach ($this->tables as $table) {
|
||||
$fullTable = MAIN_DB_PREFIX.$table;
|
||||
|
||||
// Check if table exists
|
||||
$sql = "SHOW TABLES LIKE '".$this->db->escape($fullTable)."'";
|
||||
$resql = $this->db->query($sql);
|
||||
if (!$resql || $this->db->num_rows($resql) == 0) {
|
||||
continue; // Skip non-existent tables
|
||||
}
|
||||
|
||||
$records = array();
|
||||
$sql = "SELECT * FROM ".$fullTable;
|
||||
$sql .= " WHERE entity = ".((int) $conf->entity);
|
||||
|
||||
$resql = $this->db->query($sql);
|
||||
if ($resql) {
|
||||
while ($obj = $this->db->fetch_array($resql)) {
|
||||
$records[] = $obj;
|
||||
}
|
||||
$this->db->free($resql);
|
||||
}
|
||||
|
||||
$data[$table] = $records;
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore from a backup file
|
||||
*
|
||||
* @param string $backupFile Path to backup ZIP file
|
||||
* @param bool $clearExisting Clear existing data before restore
|
||||
* @return bool True on success, false on error
|
||||
*/
|
||||
public function restoreBackup($backupFile, $clearExisting = false)
|
||||
{
|
||||
global $conf, $user;
|
||||
|
||||
if (!file_exists($backupFile)) {
|
||||
$this->error = 'Backup file not found';
|
||||
return false;
|
||||
}
|
||||
|
||||
// Create temp directory for extraction
|
||||
$tempDir = $conf->kundenkarte->dir_output.'/backups/restore_'.uniqid();
|
||||
if (!dol_mkdir($tempDir)) {
|
||||
$this->error = 'Cannot create temp directory';
|
||||
return false;
|
||||
}
|
||||
|
||||
// Extract ZIP
|
||||
$zip = new ZipArchive();
|
||||
if ($zip->open($backupFile) !== true) {
|
||||
$this->error = 'Cannot open backup file';
|
||||
return false;
|
||||
}
|
||||
$zip->extractTo($tempDir);
|
||||
$zip->close();
|
||||
|
||||
// Read metadata
|
||||
$metadataFile = $tempDir.'/metadata.json';
|
||||
if (!file_exists($metadataFile)) {
|
||||
$this->error = 'Invalid backup: metadata.json not found';
|
||||
$this->deleteDirectory($tempDir);
|
||||
return false;
|
||||
}
|
||||
$metadata = json_decode(file_get_contents($metadataFile), true);
|
||||
|
||||
// Read database data
|
||||
$dbFile = $tempDir.'/database.json';
|
||||
if (!file_exists($dbFile)) {
|
||||
$this->error = 'Invalid backup: database.json not found';
|
||||
$this->deleteDirectory($tempDir);
|
||||
return false;
|
||||
}
|
||||
$dbData = json_decode(file_get_contents($dbFile), true);
|
||||
|
||||
$this->db->begin();
|
||||
|
||||
try {
|
||||
// Clear existing data if requested
|
||||
if ($clearExisting) {
|
||||
$this->clearExistingData();
|
||||
}
|
||||
|
||||
// Import database tables (in correct order)
|
||||
foreach ($this->tables as $table) {
|
||||
if (isset($dbData[$table])) {
|
||||
$this->importTable($table, $dbData[$table]);
|
||||
}
|
||||
}
|
||||
|
||||
// Restore files if included
|
||||
if (!empty($metadata['includes_files']) && is_dir($tempDir.'/files')) {
|
||||
$filesDir = $conf->kundenkarte->dir_output.'/anlagen';
|
||||
if (!is_dir($filesDir)) {
|
||||
dol_mkdir($filesDir);
|
||||
}
|
||||
$this->copyDirectory($tempDir.'/files', $filesDir);
|
||||
}
|
||||
|
||||
$this->db->commit();
|
||||
} catch (Exception $e) {
|
||||
$this->db->rollback();
|
||||
$this->error = $e->getMessage();
|
||||
$this->deleteDirectory($tempDir);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Clean up
|
||||
$this->deleteDirectory($tempDir);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear existing data for this entity
|
||||
*/
|
||||
private function clearExistingData()
|
||||
{
|
||||
global $conf;
|
||||
|
||||
// Delete in reverse order to respect foreign keys
|
||||
$reverseTables = array_reverse($this->tables);
|
||||
|
||||
foreach ($reverseTables as $table) {
|
||||
$fullTable = MAIN_DB_PREFIX.$table;
|
||||
|
||||
// Check if table exists
|
||||
$sql = "SHOW TABLES LIKE '".$this->db->escape($fullTable)."'";
|
||||
$resql = $this->db->query($sql);
|
||||
if (!$resql || $this->db->num_rows($resql) == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$sql = "DELETE FROM ".$fullTable." WHERE entity = ".((int) $conf->entity);
|
||||
$this->db->query($sql);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Import data into a table
|
||||
*
|
||||
* @param string $table Table name (without prefix)
|
||||
* @param array $records Array of records
|
||||
*/
|
||||
private function importTable($table, $records)
|
||||
{
|
||||
global $conf;
|
||||
|
||||
if (empty($records)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$fullTable = MAIN_DB_PREFIX.$table;
|
||||
|
||||
// Check if table exists
|
||||
$sql = "SHOW TABLES LIKE '".$this->db->escape($fullTable)."'";
|
||||
$resql = $this->db->query($sql);
|
||||
if (!$resql || $this->db->num_rows($resql) == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get column info
|
||||
$columns = array();
|
||||
$sql = "SHOW COLUMNS FROM ".$fullTable;
|
||||
$resql = $this->db->query($sql);
|
||||
while ($obj = $this->db->fetch_object($resql)) {
|
||||
$columns[] = $obj->Field;
|
||||
}
|
||||
|
||||
foreach ($records as $record) {
|
||||
// Build insert statement
|
||||
$fields = array();
|
||||
$values = array();
|
||||
|
||||
foreach ($record as $field => $value) {
|
||||
if (!in_array($field, $columns)) {
|
||||
continue; // Skip unknown columns
|
||||
}
|
||||
|
||||
$fields[] = $field;
|
||||
|
||||
if ($value === null) {
|
||||
$values[] = 'NULL';
|
||||
} elseif (is_numeric($value)) {
|
||||
$values[] = $value;
|
||||
} else {
|
||||
$values[] = "'".$this->db->escape($value)."'";
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($fields)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$sql = "INSERT INTO ".$fullTable." (".implode(', ', $fields).") VALUES (".implode(', ', $values).")";
|
||||
$sql .= " ON DUPLICATE KEY UPDATE ";
|
||||
|
||||
$updates = array();
|
||||
foreach ($fields as $i => $field) {
|
||||
if ($field != 'rowid') {
|
||||
$updates[] = $field." = ".$values[$i];
|
||||
}
|
||||
}
|
||||
$sql .= implode(', ', $updates);
|
||||
|
||||
if (!$this->db->query($sql)) {
|
||||
throw new Exception('Error importing '.$table.': '.$this->db->lasterror());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get list of available backups
|
||||
*
|
||||
* @return array Array of backup info
|
||||
*/
|
||||
public function getBackupList()
|
||||
{
|
||||
global $conf;
|
||||
|
||||
$backups = array();
|
||||
$backupDir = $conf->kundenkarte->dir_output.'/backups';
|
||||
|
||||
if (!is_dir($backupDir)) {
|
||||
return $backups;
|
||||
}
|
||||
|
||||
$files = glob($backupDir.'/kundenkarte_backup_*.zip');
|
||||
if ($files) {
|
||||
foreach ($files as $file) {
|
||||
$filename = basename($file);
|
||||
// Extract date from filename
|
||||
if (preg_match('/kundenkarte_backup_(\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2})\.zip/', $filename, $matches)) {
|
||||
$date = str_replace('_', ' ', $matches[1]);
|
||||
$date = str_replace('-', ':', substr($date, 11));
|
||||
$date = substr($matches[1], 0, 10).' '.$date;
|
||||
|
||||
$backups[] = array(
|
||||
'file' => $file,
|
||||
'filename' => $filename,
|
||||
'date' => $date,
|
||||
'size' => filesize($file),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by date descending
|
||||
usort($backups, function ($a, $b) {
|
||||
return strcmp($b['date'], $a['date']);
|
||||
});
|
||||
|
||||
return $backups;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a backup file
|
||||
*
|
||||
* @param string $filename Backup filename
|
||||
* @return bool
|
||||
*/
|
||||
public function deleteBackup($filename)
|
||||
{
|
||||
global $conf;
|
||||
|
||||
$file = $conf->kundenkarte->dir_output.'/backups/'.basename($filename);
|
||||
if (file_exists($file) && strpos($filename, 'kundenkarte_backup_') === 0) {
|
||||
return unlink($file);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy directory recursively
|
||||
*
|
||||
* @param string $src Source directory
|
||||
* @param string $dst Destination directory
|
||||
*/
|
||||
private function copyDirectory($src, $dst)
|
||||
{
|
||||
if (!is_dir($dst)) {
|
||||
dol_mkdir($dst);
|
||||
}
|
||||
|
||||
$dir = opendir($src);
|
||||
while (($file = readdir($dir)) !== false) {
|
||||
if ($file == '.' || $file == '..') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$srcFile = $src.'/'.$file;
|
||||
$dstFile = $dst.'/'.$file;
|
||||
|
||||
if (is_dir($srcFile)) {
|
||||
$this->copyDirectory($srcFile, $dstFile);
|
||||
} else {
|
||||
copy($srcFile, $dstFile);
|
||||
}
|
||||
}
|
||||
closedir($dir);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete directory recursively
|
||||
*
|
||||
* @param string $dir Directory path
|
||||
*/
|
||||
private function deleteDirectory($dir)
|
||||
{
|
||||
if (!is_dir($dir)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$files = array_diff(scandir($dir), array('.', '..'));
|
||||
foreach ($files as $file) {
|
||||
$path = $dir.'/'.$file;
|
||||
if (is_dir($path)) {
|
||||
$this->deleteDirectory($path);
|
||||
} else {
|
||||
unlink($path);
|
||||
}
|
||||
}
|
||||
rmdir($dir);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create ZIP archive from directory
|
||||
*
|
||||
* @param string $sourceDir Source directory
|
||||
* @param string $zipFile Target ZIP file
|
||||
* @return bool
|
||||
*/
|
||||
private function createZipArchive($sourceDir, $zipFile)
|
||||
{
|
||||
$zip = new ZipArchive();
|
||||
if ($zip->open($zipFile, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== true) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$sourceDir = realpath($sourceDir);
|
||||
$files = new RecursiveIteratorIterator(
|
||||
new RecursiveDirectoryIterator($sourceDir),
|
||||
RecursiveIteratorIterator::LEAVES_ONLY
|
||||
);
|
||||
|
||||
foreach ($files as $file) {
|
||||
if (!$file->isDir()) {
|
||||
$filePath = $file->getRealPath();
|
||||
$relativePath = substr($filePath, strlen($sourceDir) + 1);
|
||||
$zip->addFile($filePath, $relativePath);
|
||||
}
|
||||
}
|
||||
|
||||
return $zip->close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get backup statistics
|
||||
*
|
||||
* @return array Statistics array
|
||||
*/
|
||||
public function getStatistics()
|
||||
{
|
||||
global $conf;
|
||||
|
||||
$stats = array(
|
||||
'total_anlagen' => 0,
|
||||
'total_files' => 0,
|
||||
'total_connections' => 0,
|
||||
'total_customers' => 0,
|
||||
'files_size' => 0,
|
||||
);
|
||||
|
||||
// Count anlagen
|
||||
$sql = "SELECT COUNT(*) as cnt FROM ".MAIN_DB_PREFIX."kundenkarte_anlage WHERE entity = ".((int) $conf->entity);
|
||||
$resql = $this->db->query($sql);
|
||||
if ($resql && $obj = $this->db->fetch_object($resql)) {
|
||||
$stats['total_anlagen'] = $obj->cnt;
|
||||
}
|
||||
|
||||
// Count files
|
||||
$sql = "SELECT COUNT(*) as cnt FROM ".MAIN_DB_PREFIX."kundenkarte_anlage_files WHERE entity = ".((int) $conf->entity);
|
||||
$resql = $this->db->query($sql);
|
||||
if ($resql && $obj = $this->db->fetch_object($resql)) {
|
||||
$stats['total_files'] = $obj->cnt;
|
||||
}
|
||||
|
||||
// Count connections
|
||||
$sql = "SELECT COUNT(*) as cnt FROM ".MAIN_DB_PREFIX."kundenkarte_anlage_connection WHERE entity = ".((int) $conf->entity);
|
||||
$resql = $this->db->query($sql);
|
||||
if ($resql && $obj = $this->db->fetch_object($resql)) {
|
||||
$stats['total_connections'] = $obj->cnt;
|
||||
}
|
||||
|
||||
// Count customers with anlagen
|
||||
$sql = "SELECT COUNT(DISTINCT fk_soc) as cnt FROM ".MAIN_DB_PREFIX."kundenkarte_anlage WHERE entity = ".((int) $conf->entity);
|
||||
$resql = $this->db->query($sql);
|
||||
if ($resql && $obj = $this->db->fetch_object($resql)) {
|
||||
$stats['total_customers'] = $obj->cnt;
|
||||
}
|
||||
|
||||
// Calculate files size
|
||||
$filesDir = $conf->kundenkarte->dir_output.'/anlagen';
|
||||
if (is_dir($filesDir)) {
|
||||
$stats['files_size'] = $this->getDirectorySize($filesDir);
|
||||
}
|
||||
|
||||
return $stats;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get directory size recursively
|
||||
*
|
||||
* @param string $dir Directory path
|
||||
* @return int Size in bytes
|
||||
*/
|
||||
private function getDirectorySize($dir)
|
||||
{
|
||||
$size = 0;
|
||||
foreach (new RecursiveIteratorIterator(new RecursiveDirectoryIterator($dir)) as $file) {
|
||||
if ($file->isFile()) {
|
||||
$size += $file->getSize();
|
||||
}
|
||||
}
|
||||
return $size;
|
||||
}
|
||||
}
|
||||
|
|
@ -25,6 +25,7 @@ class AnlageFile extends CommonObject
|
|||
public $label;
|
||||
public $description;
|
||||
public $is_cover;
|
||||
public $is_pinned;
|
||||
public $position;
|
||||
public $share;
|
||||
|
||||
|
|
@ -69,7 +70,7 @@ class AnlageFile extends CommonObject
|
|||
|
||||
$sql = "INSERT INTO ".MAIN_DB_PREFIX.$this->table_element." (";
|
||||
$sql .= "entity, fk_anlage, filename, filepath, filesize, mimetype,";
|
||||
$sql .= " file_type, label, description, is_cover, position, share,";
|
||||
$sql .= " file_type, label, description, is_cover, is_pinned, position, share,";
|
||||
$sql .= " date_creation, fk_user_creat";
|
||||
$sql .= ") VALUES (";
|
||||
$sql .= ((int) $conf->entity);
|
||||
|
|
@ -82,6 +83,7 @@ class AnlageFile extends CommonObject
|
|||
$sql .= ", ".($this->label ? "'".$this->db->escape($this->label)."'" : "NULL");
|
||||
$sql .= ", ".($this->description ? "'".$this->db->escape($this->description)."'" : "NULL");
|
||||
$sql .= ", ".((int) $this->is_cover);
|
||||
$sql .= ", ".((int) $this->is_pinned);
|
||||
$sql .= ", ".((int) $this->position);
|
||||
$sql .= ", ".($this->share ? "'".$this->db->escape($this->share)."'" : "NULL");
|
||||
$sql .= ", '".$this->db->idate($now)."'";
|
||||
|
|
@ -137,6 +139,7 @@ class AnlageFile extends CommonObject
|
|||
$this->label = $obj->label;
|
||||
$this->description = $obj->description;
|
||||
$this->is_cover = $obj->is_cover;
|
||||
$this->is_pinned = $obj->is_pinned;
|
||||
$this->position = $obj->position;
|
||||
$this->share = $obj->share;
|
||||
$this->date_creation = $this->db->jdate($obj->date_creation);
|
||||
|
|
@ -216,7 +219,7 @@ class AnlageFile extends CommonObject
|
|||
if ($fileType) {
|
||||
$sql .= " AND file_type = '".$this->db->escape($fileType)."'";
|
||||
}
|
||||
$sql .= " ORDER BY is_cover DESC, position ASC, filename ASC";
|
||||
$sql .= " ORDER BY is_pinned DESC, is_cover DESC, position ASC, filename ASC";
|
||||
|
||||
$resql = $this->db->query($sql);
|
||||
if ($resql) {
|
||||
|
|
@ -232,6 +235,7 @@ class AnlageFile extends CommonObject
|
|||
$file->label = $obj->label;
|
||||
$file->description = $obj->description;
|
||||
$file->is_cover = $obj->is_cover;
|
||||
$file->is_pinned = $obj->is_pinned;
|
||||
$file->position = $obj->position;
|
||||
$file->share = $obj->share;
|
||||
$file->date_creation = $this->db->jdate($obj->date_creation);
|
||||
|
|
@ -264,6 +268,9 @@ class AnlageFile extends CommonObject
|
|||
if (in_array($ext, array('doc', 'docx', 'xls', 'xlsx', 'odt', 'ods', 'txt', 'rtf'))) {
|
||||
return 'document';
|
||||
}
|
||||
if (in_array($ext, array('zip', 'rar', '7z', 'tar', 'gz', 'tgz'))) {
|
||||
return 'archive';
|
||||
}
|
||||
|
||||
return 'other';
|
||||
}
|
||||
|
|
@ -384,6 +391,29 @@ class AnlageFile extends CommonObject
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle pinned status of file
|
||||
*
|
||||
* @param User $user User making the change
|
||||
* @return int <0 if KO, >0 if OK
|
||||
*/
|
||||
public function togglePin($user)
|
||||
{
|
||||
$newStatus = $this->is_pinned ? 0 : 1;
|
||||
|
||||
$sql = "UPDATE ".MAIN_DB_PREFIX.$this->table_element;
|
||||
$sql .= " SET is_pinned = ".((int) $newStatus);
|
||||
$sql .= ", fk_user_modif = ".((int) $user->id);
|
||||
$sql .= " WHERE rowid = ".((int) $this->id);
|
||||
|
||||
$resql = $this->db->query($sql);
|
||||
if ($resql) {
|
||||
$this->is_pinned = $newStatus;
|
||||
return 1;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate thumbnail for image
|
||||
*
|
||||
|
|
|
|||
|
|
@ -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 = '3.5.0';
|
||||
$this->version = '4.0.1';
|
||||
// Url to the file with your last numberversion of this module
|
||||
//$this->url_last_version = 'http://www.example.com/versionmodule.txt';
|
||||
|
||||
|
|
@ -515,6 +515,9 @@ class modKundenKarte extends DolibarrModules
|
|||
// Migrationen: UNIQUE KEY uk_kundenkarte_societe_system um fk_contact erweitern
|
||||
$this->_migrateSocieteSystemUniqueKey();
|
||||
|
||||
// Run all database migrations
|
||||
$this->runMigrations();
|
||||
|
||||
// Permissions
|
||||
$this->remove($options);
|
||||
|
||||
|
|
@ -589,6 +592,117 @@ class modKundenKarte extends DolibarrModules
|
|||
$this->db->query("ALTER TABLE ".$table." ADD UNIQUE INDEX uk_kundenkarte_societe_system (fk_soc, fk_contact, fk_system)");
|
||||
}
|
||||
|
||||
/**
|
||||
* Run all database migrations.
|
||||
* Each migration is idempotent - safe to run multiple times.
|
||||
*/
|
||||
private function runMigrations()
|
||||
{
|
||||
// v3.6.0: Add autocomplete field
|
||||
$this->migrate_v360_autocomplete();
|
||||
|
||||
// v3.6.0: Add file pinning
|
||||
$this->migrate_v360_file_pinning();
|
||||
|
||||
// v3.6.0: Add tree display mode for fields
|
||||
$this->migrate_v360_tree_display_mode();
|
||||
|
||||
// v3.7.0: Add badge color for fields
|
||||
$this->migrate_v370_badge_color();
|
||||
}
|
||||
|
||||
/**
|
||||
* Migration v3.6.0: Add enable_autocomplete column to type fields
|
||||
*/
|
||||
private function migrate_v360_autocomplete()
|
||||
{
|
||||
$table = MAIN_DB_PREFIX."kundenkarte_anlage_type_field";
|
||||
|
||||
// Check if table exists
|
||||
$resql = $this->db->query("SHOW TABLES LIKE '".$this->db->escape($table)."'");
|
||||
if (!$resql || $this->db->num_rows($resql) == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if column exists
|
||||
$resql = $this->db->query("SHOW COLUMNS FROM ".$table." LIKE 'enable_autocomplete'");
|
||||
if ($resql && $this->db->num_rows($resql) > 0) {
|
||||
return; // Already migrated
|
||||
}
|
||||
|
||||
// Add column
|
||||
$this->db->query("ALTER TABLE ".$table." ADD COLUMN enable_autocomplete tinyint DEFAULT 0 NOT NULL AFTER show_in_hover");
|
||||
}
|
||||
|
||||
/**
|
||||
* Migration v3.6.0: Add is_pinned column to files
|
||||
*/
|
||||
private function migrate_v360_file_pinning()
|
||||
{
|
||||
$table = MAIN_DB_PREFIX."kundenkarte_anlage_files";
|
||||
|
||||
// Check if table exists
|
||||
$resql = $this->db->query("SHOW TABLES LIKE '".$this->db->escape($table)."'");
|
||||
if (!$resql || $this->db->num_rows($resql) == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if column exists
|
||||
$resql = $this->db->query("SHOW COLUMNS FROM ".$table." LIKE 'is_pinned'");
|
||||
if ($resql && $this->db->num_rows($resql) > 0) {
|
||||
return; // Already migrated
|
||||
}
|
||||
|
||||
// Add column
|
||||
$this->db->query("ALTER TABLE ".$table." ADD COLUMN is_pinned tinyint DEFAULT 0 NOT NULL AFTER is_cover");
|
||||
}
|
||||
|
||||
/**
|
||||
* Migration v3.6.0: Add tree_display_mode column to type fields
|
||||
*/
|
||||
private function migrate_v360_tree_display_mode()
|
||||
{
|
||||
$table = MAIN_DB_PREFIX."kundenkarte_anlage_type_field";
|
||||
|
||||
// Check if table exists
|
||||
$resql = $this->db->query("SHOW TABLES LIKE '".$this->db->escape($table)."'");
|
||||
if (!$resql || $this->db->num_rows($resql) == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if column exists
|
||||
$resql = $this->db->query("SHOW COLUMNS FROM ".$table." LIKE 'tree_display_mode'");
|
||||
if ($resql && $this->db->num_rows($resql) > 0) {
|
||||
return; // Already migrated
|
||||
}
|
||||
|
||||
// Add column
|
||||
$this->db->query("ALTER TABLE ".$table." ADD COLUMN tree_display_mode varchar(20) DEFAULT 'badge' AFTER show_in_tree");
|
||||
}
|
||||
|
||||
/**
|
||||
* Migration v3.7.0: Add badge_color column to type fields
|
||||
*/
|
||||
private function migrate_v370_badge_color()
|
||||
{
|
||||
$table = MAIN_DB_PREFIX."kundenkarte_anlage_type_field";
|
||||
|
||||
// Check if table exists
|
||||
$resql = $this->db->query("SHOW TABLES LIKE '".$this->db->escape($table)."'");
|
||||
if (!$resql || $this->db->num_rows($resql) == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if column exists
|
||||
$resql = $this->db->query("SHOW COLUMNS FROM ".$table." LIKE 'badge_color'");
|
||||
if ($resql && $this->db->num_rows($resql) > 0) {
|
||||
return; // Already migrated
|
||||
}
|
||||
|
||||
// Add column
|
||||
$this->db->query("ALTER TABLE ".$table." ADD COLUMN badge_color varchar(7) AFTER tree_display_mode");
|
||||
}
|
||||
|
||||
/**
|
||||
* Function called when module is disabled.
|
||||
* Remove from database constants, boxes and permissions from Dolibarr database.
|
||||
|
|
|
|||
|
|
@ -222,8 +222,49 @@
|
|||
|
||||
.kundenkarte-tree-label-info {
|
||||
font-weight: normal !important;
|
||||
color: #999 !important;
|
||||
font-size: 0.9em !important;
|
||||
color: #888 !important;
|
||||
font-size: 0.85em !important;
|
||||
margin-left: 4px !important;
|
||||
}
|
||||
|
||||
/* Spacer to push badges to the right */
|
||||
.kundenkarte-tree-spacer {
|
||||
flex: 1 !important;
|
||||
min-width: 10px !important;
|
||||
}
|
||||
|
||||
/* Tree info badges */
|
||||
.kundenkarte-tree-badges {
|
||||
display: inline-flex !important;
|
||||
gap: 6px !important;
|
||||
margin-left: 0 !important;
|
||||
margin-right: 8px !important;
|
||||
flex-wrap: wrap !important;
|
||||
}
|
||||
|
||||
.kundenkarte-tree-badge {
|
||||
display: inline-flex !important;
|
||||
align-items: center !important;
|
||||
vertical-align: middle !important;
|
||||
gap: 4px !important;
|
||||
padding: 2px 8px !important;
|
||||
background: linear-gradient(135deg, #2a4a5e 0%, #1e3a4a 100%) !important;
|
||||
border: 1px solid #3a6a8e !important;
|
||||
border-radius: 12px !important;
|
||||
font-size: 11px !important;
|
||||
color: #8cc4e8 !important;
|
||||
font-weight: 500 !important;
|
||||
white-space: nowrap !important;
|
||||
}
|
||||
|
||||
.kundenkarte-tree-badge i {
|
||||
font-size: 10px !important;
|
||||
color: #5aa8d4 !important;
|
||||
}
|
||||
|
||||
.kundenkarte-tree-badge:hover {
|
||||
background: linear-gradient(135deg, #3a5a6e 0%, #2a4a5a 100%) !important;
|
||||
border-color: #4a7a9e !important;
|
||||
}
|
||||
|
||||
.kundenkarte-tree-actions {
|
||||
|
|
@ -298,6 +339,7 @@ body.kundenkarte-drag-active * {
|
|||
/* Tree - File Indicators */
|
||||
.kundenkarte-tree-files {
|
||||
display: inline-flex !important;
|
||||
vertical-align: middle !important;
|
||||
gap: 3px !important;
|
||||
margin-left: 6px !important;
|
||||
}
|
||||
|
|
@ -306,8 +348,9 @@ body.kundenkarte-drag-active * {
|
|||
font-size: 0.8em !important;
|
||||
padding: 2px 6px !important;
|
||||
border-radius: 4px !important;
|
||||
display: flex !important;
|
||||
display: inline-flex !important;
|
||||
align-items: center !important;
|
||||
vertical-align: middle !important;
|
||||
gap: 3px !important;
|
||||
text-decoration: none !important;
|
||||
}
|
||||
|
|
@ -322,6 +365,11 @@ body.kundenkarte-drag-active * {
|
|||
color: #ddd !important;
|
||||
}
|
||||
|
||||
.kundenkarte-tree-file-all {
|
||||
background: #4a5a6a !important;
|
||||
color: #ddd !important;
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
TOOLTIP
|
||||
======================================== */
|
||||
|
|
@ -393,6 +441,144 @@ body.kundenkarte-drag-active * {
|
|||
color: #aaa !important;
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
FILE PREVIEW TOOLTIP
|
||||
======================================== */
|
||||
|
||||
.kundenkarte-file-preview {
|
||||
min-width: 280px !important;
|
||||
max-width: 400px !important;
|
||||
}
|
||||
|
||||
.kundenkarte-file-preview-section {
|
||||
margin-bottom: 12px !important;
|
||||
}
|
||||
|
||||
.kundenkarte-file-preview-section:last-child {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
||||
.kundenkarte-file-preview-title {
|
||||
font-weight: bold !important;
|
||||
color: #8cc4e8 !important;
|
||||
margin-bottom: 8px !important;
|
||||
font-size: 12px !important;
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
gap: 6px !important;
|
||||
}
|
||||
|
||||
.kundenkarte-file-preview-thumbs {
|
||||
display: flex !important;
|
||||
flex-wrap: wrap !important;
|
||||
gap: 6px !important;
|
||||
}
|
||||
|
||||
.kundenkarte-file-preview-thumb {
|
||||
position: relative !important;
|
||||
width: 60px !important;
|
||||
height: 60px !important;
|
||||
border-radius: 4px !important;
|
||||
overflow: hidden !important;
|
||||
border: 2px solid #444 !important;
|
||||
transition: border-color 0.2s !important;
|
||||
}
|
||||
|
||||
.kundenkarte-file-preview-thumb:hover {
|
||||
border-color: #8cc4e8 !important;
|
||||
}
|
||||
|
||||
.kundenkarte-file-preview-thumb.is-cover {
|
||||
border-color: #f39c12 !important;
|
||||
}
|
||||
|
||||
.kundenkarte-file-preview-thumb.is-pinned {
|
||||
border-color: #27ae60 !important;
|
||||
}
|
||||
|
||||
.kundenkarte-file-preview-thumb img {
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
object-fit: cover !important;
|
||||
}
|
||||
|
||||
.kundenkarte-file-preview-pin {
|
||||
position: absolute !important;
|
||||
top: 2px !important;
|
||||
right: 2px !important;
|
||||
background: rgba(39, 174, 96, 0.9) !important;
|
||||
color: #fff !important;
|
||||
font-size: 9px !important;
|
||||
padding: 2px 4px !important;
|
||||
border-radius: 3px !important;
|
||||
}
|
||||
|
||||
.kundenkarte-file-preview-more {
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
justify-content: center !important;
|
||||
width: 60px !important;
|
||||
height: 60px !important;
|
||||
background: #333 !important;
|
||||
border-radius: 4px !important;
|
||||
color: #888 !important;
|
||||
font-size: 14px !important;
|
||||
font-weight: bold !important;
|
||||
}
|
||||
|
||||
.kundenkarte-file-preview-docs {
|
||||
display: flex !important;
|
||||
flex-direction: column !important;
|
||||
gap: 4px !important;
|
||||
}
|
||||
|
||||
.kundenkarte-file-preview-doc {
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
gap: 8px !important;
|
||||
padding: 6px 8px !important;
|
||||
background: #2a2a2a !important;
|
||||
border-radius: 4px !important;
|
||||
text-decoration: none !important;
|
||||
color: #ddd !important;
|
||||
transition: background-color 0.2s !important;
|
||||
}
|
||||
|
||||
.kundenkarte-file-preview-doc:hover {
|
||||
background: #3a3a3a !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
.kundenkarte-file-preview-doc.is-pinned {
|
||||
background: #2a3a2a !important;
|
||||
}
|
||||
|
||||
.kundenkarte-file-preview-doc i:first-child {
|
||||
font-size: 16px !important;
|
||||
width: 20px !important;
|
||||
text-align: center !important;
|
||||
}
|
||||
|
||||
.kundenkarte-file-preview-doc-name {
|
||||
flex: 1 !important;
|
||||
overflow: hidden !important;
|
||||
text-overflow: ellipsis !important;
|
||||
white-space: nowrap !important;
|
||||
font-size: 12px !important;
|
||||
}
|
||||
|
||||
.kundenkarte-file-preview-doc-pin {
|
||||
color: #27ae60 !important;
|
||||
font-size: 10px !important;
|
||||
}
|
||||
|
||||
.kundenkarte-file-preview-more-docs {
|
||||
padding: 4px 8px !important;
|
||||
color: #888 !important;
|
||||
font-size: 11px !important;
|
||||
text-align: center !important;
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
SYSTEM TABS
|
||||
======================================== */
|
||||
|
|
@ -569,38 +755,142 @@ body.kundenkarte-drag-active * {
|
|||
FILE GALLERY
|
||||
======================================== */
|
||||
|
||||
/* ========================================
|
||||
FILE DROPZONE
|
||||
======================================== */
|
||||
|
||||
.kundenkarte-dropzone {
|
||||
border: 2px dashed #555 !important;
|
||||
border-radius: 8px !important;
|
||||
padding: 30px !important;
|
||||
text-align: center !important;
|
||||
background: #1e1e1e !important;
|
||||
transition: all 0.3s ease !important;
|
||||
cursor: pointer !important;
|
||||
}
|
||||
|
||||
.kundenkarte-dropzone:hover,
|
||||
.kundenkarte-dropzone.dragover {
|
||||
border-color: #3498db !important;
|
||||
background: #252530 !important;
|
||||
}
|
||||
|
||||
.kundenkarte-dropzone.dragover {
|
||||
transform: scale(1.02) !important;
|
||||
}
|
||||
|
||||
.kundenkarte-dropzone-content {
|
||||
pointer-events: none !important;
|
||||
}
|
||||
|
||||
.kundenkarte-dropzone-files {
|
||||
display: flex !important;
|
||||
flex-wrap: wrap !important;
|
||||
gap: 10px !important;
|
||||
margin-top: 15px !important;
|
||||
justify-content: center !important;
|
||||
}
|
||||
|
||||
.kundenkarte-dropzone-file {
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
gap: 8px !important;
|
||||
padding: 8px 12px !important;
|
||||
background: #333 !important;
|
||||
border-radius: 4px !important;
|
||||
font-size: 12px !important;
|
||||
color: #e0e0e0 !important;
|
||||
}
|
||||
|
||||
.kundenkarte-dropzone-file i {
|
||||
color: #3498db !important;
|
||||
}
|
||||
|
||||
.kundenkarte-dropzone-file .remove-file {
|
||||
color: #e74c3c !important;
|
||||
cursor: pointer !important;
|
||||
margin-left: 5px !important;
|
||||
}
|
||||
|
||||
.kundenkarte-dropzone-file .remove-file:hover {
|
||||
color: #ff6b6b !important;
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
FILE GRID - LARGER BOXES
|
||||
======================================== */
|
||||
|
||||
.kundenkarte-files-grid {
|
||||
display: grid !important;
|
||||
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)) !important;
|
||||
gap: 15px !important;
|
||||
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)) !important;
|
||||
gap: 20px !important;
|
||||
margin-top: 15px !important;
|
||||
}
|
||||
|
||||
.kundenkarte-file-item {
|
||||
border: 1px solid #444 !important;
|
||||
border-radius: 6px !important;
|
||||
border-radius: 8px !important;
|
||||
overflow: hidden !important;
|
||||
background: #2d2d2d !important;
|
||||
transition: transform 0.2s, box-shadow 0.2s !important;
|
||||
position: relative !important;
|
||||
}
|
||||
|
||||
.kundenkarte-file-item:hover {
|
||||
transform: translateY(-3px) !important;
|
||||
box-shadow: 0 6px 20px rgba(0,0,0,0.3) !important;
|
||||
}
|
||||
|
||||
/* Pinned files */
|
||||
.kundenkarte-file-pinned {
|
||||
border-color: #e6a500 !important;
|
||||
box-shadow: 0 0 0 1px #e6a500 inset !important;
|
||||
}
|
||||
|
||||
.kundenkarte-pin-indicator {
|
||||
position: absolute !important;
|
||||
top: 8px !important;
|
||||
right: 8px !important;
|
||||
background: #e6a500 !important;
|
||||
color: #000 !important;
|
||||
width: 24px !important;
|
||||
height: 24px !important;
|
||||
border-radius: 50% !important;
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
justify-content: center !important;
|
||||
font-size: 12px !important;
|
||||
z-index: 10 !important;
|
||||
}
|
||||
|
||||
.kundenkarte-file-btn-pinned {
|
||||
color: #e6a500 !important;
|
||||
}
|
||||
|
||||
.kundenkarte-file-preview {
|
||||
height: 140px !important;
|
||||
height: 180px !important;
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
justify-content: center !important;
|
||||
background: #1e1e1e !important;
|
||||
overflow: hidden !important;
|
||||
cursor: pointer !important;
|
||||
}
|
||||
|
||||
.kundenkarte-file-preview img {
|
||||
max-width: 100% !important;
|
||||
max-height: 100% !important;
|
||||
object-fit: contain !important;
|
||||
transition: transform 0.3s !important;
|
||||
}
|
||||
|
||||
.kundenkarte-file-item:hover .kundenkarte-file-preview img {
|
||||
transform: scale(1.05) !important;
|
||||
}
|
||||
|
||||
.kundenkarte-file-info {
|
||||
padding: 10px !important;
|
||||
font-size: 0.9em !important;
|
||||
padding: 12px 15px !important;
|
||||
font-size: 0.95em !important;
|
||||
}
|
||||
|
||||
.kundenkarte-file-name {
|
||||
|
|
@ -609,17 +899,19 @@ body.kundenkarte-drag-active * {
|
|||
text-overflow: ellipsis !important;
|
||||
white-space: nowrap !important;
|
||||
color: #e0e0e0 !important;
|
||||
font-size: 13px !important;
|
||||
margin-bottom: 4px !important;
|
||||
}
|
||||
|
||||
.kundenkarte-file-size {
|
||||
color: #999 !important;
|
||||
font-size: 0.85em !important;
|
||||
color: #888 !important;
|
||||
font-size: 11px !important;
|
||||
}
|
||||
|
||||
.kundenkarte-file-actions {
|
||||
display: flex !important;
|
||||
gap: 5px !important;
|
||||
margin-top: 8px !important;
|
||||
gap: 8px !important;
|
||||
margin-top: 10px !important;
|
||||
justify-content: center !important;
|
||||
}
|
||||
|
||||
|
|
@ -627,21 +919,23 @@ body.kundenkarte-drag-active * {
|
|||
display: inline-flex !important;
|
||||
align-items: center !important;
|
||||
justify-content: center !important;
|
||||
width: 28px !important;
|
||||
height: 28px !important;
|
||||
border-radius: 4px !important;
|
||||
width: 32px !important;
|
||||
height: 32px !important;
|
||||
border-radius: 6px !important;
|
||||
background: #444 !important;
|
||||
color: #e0e0e0 !important;
|
||||
text-decoration: none !important;
|
||||
transition: all 0.2s !important;
|
||||
}
|
||||
|
||||
.kundenkarte-file-btn:hover {
|
||||
background: #555 !important;
|
||||
color: #fff !important;
|
||||
transform: scale(1.1) !important;
|
||||
}
|
||||
|
||||
.kundenkarte-file-btn-delete:hover {
|
||||
background: #a33 !important;
|
||||
background: #c0392b !important;
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
|
|
@ -2165,3 +2459,261 @@ body.kundenkarte-drag-active * {
|
|||
.kundenkarte-conn-tooltip-hint i {
|
||||
margin-right: 5px !important;
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
MOBILE / COMPACT VIEW
|
||||
======================================== */
|
||||
|
||||
/* Mobile view toggle button */
|
||||
.kundenkarte-view-toggle {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.kundenkarte-view-toggle {
|
||||
display: inline-flex !important;
|
||||
align-items: center !important;
|
||||
gap: 5px !important;
|
||||
padding: 6px 12px !important;
|
||||
background: #333 !important;
|
||||
border: 1px solid #555 !important;
|
||||
border-radius: 4px !important;
|
||||
color: #ddd !important;
|
||||
cursor: pointer !important;
|
||||
font-size: 12px !important;
|
||||
}
|
||||
|
||||
.kundenkarte-view-toggle.active {
|
||||
background: #2a4a5e !important;
|
||||
border-color: #3a6a8e !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Compact mode styles */
|
||||
.kundenkarte-tree.compact-mode .kundenkarte-tree-row {
|
||||
min-height: 32px !important;
|
||||
}
|
||||
|
||||
.kundenkarte-tree.compact-mode .kundenkarte-tree-item {
|
||||
padding: 4px 8px !important;
|
||||
font-size: 13px !important;
|
||||
}
|
||||
|
||||
.kundenkarte-tree.compact-mode .kundenkarte-tree-badges,
|
||||
.kundenkarte-tree.compact-mode .kundenkarte-tree-label-info,
|
||||
.kundenkarte-tree.compact-mode .kundenkarte-tree-type {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.kundenkarte-tree.compact-mode .kundenkarte-tree-actions {
|
||||
opacity: 0 !important;
|
||||
transition: opacity 0.2s !important;
|
||||
}
|
||||
|
||||
.kundenkarte-tree.compact-mode .kundenkarte-tree-item:hover .kundenkarte-tree-actions,
|
||||
.kundenkarte-tree.compact-mode .kundenkarte-tree-item.expanded .kundenkarte-tree-actions {
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
.kundenkarte-tree.compact-mode .kundenkarte-tree-files {
|
||||
margin-left: auto !important;
|
||||
}
|
||||
|
||||
/* Mobile-specific overrides */
|
||||
@media (max-width: 768px) {
|
||||
/* Tree container */
|
||||
.kundenkarte-tree {
|
||||
padding: 5px 0 !important;
|
||||
}
|
||||
|
||||
/* Tree row */
|
||||
.kundenkarte-tree-row {
|
||||
min-height: 40px !important;
|
||||
}
|
||||
|
||||
/* Tree item - touch-friendly */
|
||||
.kundenkarte-tree-item {
|
||||
padding: 8px 10px !important;
|
||||
font-size: 14px !important;
|
||||
min-height: 40px !important;
|
||||
}
|
||||
|
||||
/* Hide badges and extra info on mobile by default */
|
||||
.kundenkarte-tree-badges,
|
||||
.kundenkarte-tree-label-info {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Smaller type badge */
|
||||
.kundenkarte-tree-type {
|
||||
font-size: 10px !important;
|
||||
padding: 2px 4px !important;
|
||||
}
|
||||
|
||||
/* Touch-friendly actions */
|
||||
.kundenkarte-tree-actions {
|
||||
gap: 12px !important;
|
||||
}
|
||||
|
||||
.kundenkarte-tree-actions a {
|
||||
padding: 8px !important;
|
||||
}
|
||||
|
||||
/* Cable lines - thinner on mobile */
|
||||
.cable-line {
|
||||
width: 12px !important;
|
||||
min-width: 12px !important;
|
||||
}
|
||||
|
||||
/* File badge */
|
||||
.kundenkarte-tree-file-badge {
|
||||
padding: 4px 8px !important;
|
||||
font-size: 12px !important;
|
||||
}
|
||||
|
||||
/* System tabs - scrollable */
|
||||
.kundenkarte-system-tabs-wrapper {
|
||||
flex-direction: column !important;
|
||||
align-items: stretch !important;
|
||||
}
|
||||
|
||||
.kundenkarte-system-tabs {
|
||||
overflow-x: auto !important;
|
||||
-webkit-overflow-scrolling: touch !important;
|
||||
padding-bottom: 5px !important;
|
||||
}
|
||||
|
||||
.kundenkarte-system-tabs::-webkit-scrollbar {
|
||||
height: 4px !important;
|
||||
}
|
||||
|
||||
.kundenkarte-tree-controls {
|
||||
justify-content: center !important;
|
||||
margin-top: 8px !important;
|
||||
flex-wrap: wrap !important;
|
||||
gap: 8px !important;
|
||||
}
|
||||
|
||||
/* Uniform button sizes on mobile */
|
||||
.kundenkarte-tree-controls .button,
|
||||
.kundenkarte-tree-controls .button.small,
|
||||
.kundenkarte-tree-controls a.button,
|
||||
.kundenkarte-tree-controls a.button.small,
|
||||
.kundenkarte-tree-controls button.button,
|
||||
.kundenkarte-view-toggle {
|
||||
flex: 1 1 auto !important;
|
||||
min-width: 80px !important;
|
||||
max-width: 150px !important;
|
||||
padding: 10px 8px !important;
|
||||
font-size: 12px !important;
|
||||
text-align: center !important;
|
||||
justify-content: center !important;
|
||||
white-space: nowrap !important;
|
||||
}
|
||||
|
||||
/* Hide button text on very small screens, show only icons */
|
||||
.kundenkarte-tree-controls .button span,
|
||||
.kundenkarte-tree-controls a.button span {
|
||||
display: inline !important;
|
||||
}
|
||||
|
||||
.kundenkarte-tree-controls .button i,
|
||||
.kundenkarte-tree-controls a.button i {
|
||||
margin-right: 4px !important;
|
||||
}
|
||||
|
||||
/* Compact toggle indicator */
|
||||
.kundenkarte-tree-toggle {
|
||||
padding: 10px !important;
|
||||
}
|
||||
|
||||
/* Hide spacer on mobile */
|
||||
.kundenkarte-tree-spacer {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Tooltip adjustments for mobile */
|
||||
.kundenkarte-tooltip {
|
||||
max-width: 90vw !important;
|
||||
min-width: 200px !important;
|
||||
}
|
||||
|
||||
.kundenkarte-file-preview {
|
||||
max-width: 85vw !important;
|
||||
}
|
||||
|
||||
.kundenkarte-file-preview-thumbs {
|
||||
justify-content: flex-start !important;
|
||||
}
|
||||
|
||||
.kundenkarte-file-preview-thumb {
|
||||
width: 50px !important;
|
||||
height: 50px !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Very small screens */
|
||||
@media (max-width: 480px) {
|
||||
.kundenkarte-tree-item {
|
||||
padding: 6px 8px !important;
|
||||
}
|
||||
|
||||
.kundenkarte-tree-type {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.kundenkarte-tree-actions a {
|
||||
font-size: 14px !important;
|
||||
}
|
||||
|
||||
/* Only show most important actions */
|
||||
.kundenkarte-tree-actions a:not(:first-child):not(:nth-child(2)) {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.cable-line {
|
||||
width: 10px !important;
|
||||
min-width: 10px !important;
|
||||
}
|
||||
|
||||
/* Buttons on very small screens - 2x2 grid */
|
||||
.kundenkarte-tree-controls {
|
||||
display: grid !important;
|
||||
grid-template-columns: 1fr 1fr !important;
|
||||
gap: 6px !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.kundenkarte-tree-controls .button,
|
||||
.kundenkarte-tree-controls .button.small,
|
||||
.kundenkarte-tree-controls a.button,
|
||||
.kundenkarte-tree-controls a.button.small,
|
||||
.kundenkarte-tree-controls button.button,
|
||||
.kundenkarte-view-toggle {
|
||||
min-width: unset !important;
|
||||
max-width: unset !important;
|
||||
width: 100% !important;
|
||||
padding: 10px 6px !important;
|
||||
font-size: 11px !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Expanded item in compact mode - show details */
|
||||
.kundenkarte-tree.compact-mode .kundenkarte-tree-item.expanded {
|
||||
background: #2a2a2a !important;
|
||||
flex-wrap: wrap !important;
|
||||
}
|
||||
|
||||
.kundenkarte-tree.compact-mode .kundenkarte-tree-item.expanded .kundenkarte-tree-badges,
|
||||
.kundenkarte-tree.compact-mode .kundenkarte-tree-item.expanded .kundenkarte-tree-label-info {
|
||||
display: flex !important;
|
||||
width: 100% !important;
|
||||
margin-top: 8px !important;
|
||||
padding-top: 8px !important;
|
||||
border-top: 1px solid #444 !important;
|
||||
}
|
||||
|
||||
.kundenkarte-tree.compact-mode .kundenkarte-tree-item.expanded .kundenkarte-tree-badges {
|
||||
flex-wrap: wrap !important;
|
||||
gap: 6px !important;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -178,6 +178,7 @@
|
|||
init: function() {
|
||||
this.bindEvents();
|
||||
this.initDragDrop();
|
||||
this.initCompactMode();
|
||||
},
|
||||
|
||||
bindEvents: function() {
|
||||
|
|
@ -209,6 +210,21 @@
|
|||
self.collapseAll();
|
||||
});
|
||||
|
||||
// Compact mode toggle
|
||||
$(document).on('click', '#btn-compact-mode', function(e) {
|
||||
e.preventDefault();
|
||||
self.toggleCompactMode();
|
||||
});
|
||||
|
||||
// In compact mode, click on item to expand/show details
|
||||
$(document).on('click', '.kundenkarte-tree.compact-mode .kundenkarte-tree-item', function(e) {
|
||||
// Don't trigger on action buttons or toggle
|
||||
if ($(e.target).closest('.kundenkarte-tree-actions, .kundenkarte-tree-toggle, .kundenkarte-tree-files').length) {
|
||||
return;
|
||||
}
|
||||
$(this).toggleClass('expanded');
|
||||
});
|
||||
|
||||
// Hover tooltip on ICON only - show after delay
|
||||
$(document).on('mouseenter', '.kundenkarte-tooltip-trigger', function(e) {
|
||||
var $trigger = $(this);
|
||||
|
|
@ -287,6 +303,30 @@
|
|||
}, 100);
|
||||
});
|
||||
|
||||
// File badge tooltip on hover (combined images + documents)
|
||||
$(document).on('mouseenter', '.kundenkarte-tree-file-badge', function(e) {
|
||||
var $trigger = $(this);
|
||||
var anlageId = $trigger.data('anlage-id');
|
||||
|
||||
if (!anlageId) return;
|
||||
|
||||
clearTimeout(self.hideTimeout);
|
||||
self.hideTimeout = null;
|
||||
|
||||
self.tooltipTimeout = setTimeout(function() {
|
||||
self.showFilePreview($trigger, anlageId);
|
||||
}, 300);
|
||||
});
|
||||
|
||||
$(document).on('mouseleave', '.kundenkarte-tree-file-badge', function() {
|
||||
clearTimeout(self.tooltipTimeout);
|
||||
self.tooltipTimeout = null;
|
||||
|
||||
self.hideTimeout = setTimeout(function() {
|
||||
self.hideTooltip();
|
||||
}, 100);
|
||||
});
|
||||
|
||||
// Keep tooltip visible when hovering over it
|
||||
$(document).on('mouseenter', '#kundenkarte-tooltip', function() {
|
||||
clearTimeout(self.hideTimeout);
|
||||
|
|
@ -510,6 +550,105 @@
|
|||
});
|
||||
},
|
||||
|
||||
showFilePreview: function($trigger, anlageId) {
|
||||
var self = this;
|
||||
|
||||
// Load all files via AJAX
|
||||
$.ajax({
|
||||
url: baseUrl + '/custom/kundenkarte/ajax/file_preview.php',
|
||||
data: { anlage_id: anlageId },
|
||||
dataType: 'json',
|
||||
success: function(response) {
|
||||
if ((!response.images || response.images.length === 0) &&
|
||||
(!response.documents || response.documents.length === 0)) {
|
||||
return;
|
||||
}
|
||||
|
||||
var html = '<div class="kundenkarte-file-preview">';
|
||||
|
||||
// Images section with thumbnails
|
||||
if (response.images && response.images.length > 0) {
|
||||
html += '<div class="kundenkarte-file-preview-section">';
|
||||
html += '<div class="kundenkarte-file-preview-title"><i class="fa fa-image"></i> Bilder (' + response.images.length + ')</div>';
|
||||
html += '<div class="kundenkarte-file-preview-thumbs">';
|
||||
for (var i = 0; i < response.images.length && i < 6; i++) {
|
||||
var img = response.images[i];
|
||||
var pinnedClass = img.is_pinned ? ' is-pinned' : '';
|
||||
var coverClass = img.is_cover ? ' is-cover' : '';
|
||||
html += '<a href="' + img.url + '" target="_blank" class="kundenkarte-file-preview-thumb' + pinnedClass + coverClass + '">';
|
||||
html += '<img src="' + img.thumb_url + '" alt="' + self.escapeHtml(img.name) + '">';
|
||||
if (img.is_pinned) {
|
||||
html += '<span class="kundenkarte-file-preview-pin"><i class="fa fa-thumb-tack"></i></span>';
|
||||
}
|
||||
html += '</a>';
|
||||
}
|
||||
if (response.images.length > 6) {
|
||||
html += '<span class="kundenkarte-file-preview-more">+' + (response.images.length - 6) + '</span>';
|
||||
}
|
||||
html += '</div>';
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
// Documents section with icons
|
||||
if (response.documents && response.documents.length > 0) {
|
||||
html += '<div class="kundenkarte-file-preview-section">';
|
||||
html += '<div class="kundenkarte-file-preview-title"><i class="fa fa-file-text-o"></i> Dokumente (' + response.documents.length + ')</div>';
|
||||
html += '<div class="kundenkarte-file-preview-docs">';
|
||||
for (var j = 0; j < response.documents.length && j < 5; j++) {
|
||||
var doc = response.documents[j];
|
||||
var docPinnedClass = doc.is_pinned ? ' is-pinned' : '';
|
||||
html += '<a href="' + doc.url + '" target="_blank" class="kundenkarte-file-preview-doc' + docPinnedClass + '">';
|
||||
html += '<i class="fa ' + doc.icon + '" style="color:' + doc.color + '"></i>';
|
||||
html += '<span class="kundenkarte-file-preview-doc-name">' + self.escapeHtml(doc.name) + '</span>';
|
||||
if (doc.is_pinned) {
|
||||
html += '<i class="fa fa-thumb-tack kundenkarte-file-preview-doc-pin"></i>';
|
||||
}
|
||||
html += '</a>';
|
||||
}
|
||||
if (response.documents.length > 5) {
|
||||
html += '<div class="kundenkarte-file-preview-more-docs">+' + (response.documents.length - 5) + ' weitere</div>';
|
||||
}
|
||||
html += '</div>';
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
html += '</div>';
|
||||
|
||||
var $tooltip = $('#kundenkarte-tooltip');
|
||||
if (!$tooltip.length) {
|
||||
$tooltip = $('<div id="kundenkarte-tooltip" class="kundenkarte-tooltip"></div>');
|
||||
$('body').append($tooltip);
|
||||
}
|
||||
|
||||
$tooltip.html(html);
|
||||
|
||||
// Position tooltip
|
||||
var offset = $trigger.offset();
|
||||
var windowWidth = $(window).width();
|
||||
var scrollTop = $(window).scrollTop();
|
||||
|
||||
$tooltip.css({ visibility: 'hidden', display: 'block' });
|
||||
var tooltipWidth = $tooltip.outerWidth();
|
||||
var tooltipHeight = $tooltip.outerHeight();
|
||||
$tooltip.css({ visibility: '', display: '' });
|
||||
|
||||
var left = offset.left + $trigger.outerWidth() + 10;
|
||||
if (left + tooltipWidth > windowWidth - 20) {
|
||||
left = offset.left - tooltipWidth - 10;
|
||||
}
|
||||
if (left < 10) left = 10;
|
||||
|
||||
var top = offset.top;
|
||||
if (top + tooltipHeight > scrollTop + $(window).height() - 20) {
|
||||
top = scrollTop + $(window).height() - tooltipHeight - 20;
|
||||
}
|
||||
|
||||
$tooltip.css({ top: top, left: left }).addClass('visible').show();
|
||||
self.currentTooltip = $tooltip;
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
buildTooltipHtml: function(data) {
|
||||
var html = '<div class="kundenkarte-tooltip-header">';
|
||||
html += '<span class="kundenkarte-tooltip-icon"><i class="fa ' + (data.picto || 'fa-cube') + '"></i></span>';
|
||||
|
|
@ -704,6 +843,41 @@
|
|||
collapseAll: function() {
|
||||
$('.kundenkarte-tree-toggle').addClass('collapsed');
|
||||
$('.kundenkarte-tree-children').addClass('collapsed');
|
||||
},
|
||||
|
||||
toggleCompactMode: function() {
|
||||
var $tree = $('.kundenkarte-tree');
|
||||
var $btn = $('#btn-compact-mode');
|
||||
|
||||
$tree.toggleClass('compact-mode');
|
||||
$btn.toggleClass('active');
|
||||
|
||||
if ($tree.hasClass('compact-mode')) {
|
||||
$btn.find('span').text('Normal');
|
||||
$btn.find('i').removeClass('fa-compress').addClass('fa-expand');
|
||||
// Remove any expanded items
|
||||
$('.kundenkarte-tree-item.expanded').removeClass('expanded');
|
||||
// Store preference
|
||||
localStorage.setItem('kundenkarte_compact_mode', '1');
|
||||
} else {
|
||||
$btn.find('span').text('Kompakt');
|
||||
$btn.find('i').removeClass('fa-expand').addClass('fa-compress');
|
||||
localStorage.removeItem('kundenkarte_compact_mode');
|
||||
}
|
||||
},
|
||||
|
||||
initCompactMode: function() {
|
||||
// Check localStorage for saved preference
|
||||
if (localStorage.getItem('kundenkarte_compact_mode') === '1') {
|
||||
this.toggleCompactMode();
|
||||
}
|
||||
|
||||
// Auto-enable on mobile
|
||||
if (window.innerWidth <= 768 && !localStorage.getItem('kundenkarte_compact_mode_manual')) {
|
||||
if (!$('.kundenkarte-tree').hasClass('compact-mode')) {
|
||||
this.toggleCompactMode();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -1202,12 +1376,16 @@
|
|||
},
|
||||
|
||||
loadFields: function(typeId) {
|
||||
var self = this;
|
||||
var $container = $('#dynamic_fields');
|
||||
if (!typeId) {
|
||||
$container.html('');
|
||||
return;
|
||||
}
|
||||
|
||||
// Store current type ID for autocomplete
|
||||
this.currentTypeId = typeId;
|
||||
|
||||
// Get anlage_id if editing or copying
|
||||
var anlageId = $('input[name="anlage_id"]').val() || $('input[name="copy_from"]').val() || 0;
|
||||
|
||||
|
|
@ -1240,17 +1418,21 @@
|
|||
});
|
||||
},
|
||||
|
||||
currentTypeId: 0,
|
||||
|
||||
renderField: function(field) {
|
||||
var name = 'field_' + field.code;
|
||||
var value = field.value || '';
|
||||
var required = field.required ? ' required' : '';
|
||||
var autocompleteClass = field.autocomplete ? ' kk-autocomplete' : '';
|
||||
var autocompleteAttrs = field.autocomplete ? ' data-field-code="' + field.code + '" data-type-id="' + this.currentTypeId + '"' : '';
|
||||
|
||||
switch (field.type) {
|
||||
case 'text':
|
||||
return '<input type="text" name="' + name + '" class="flat minwidth300" value="' + this.escapeHtml(value) + '"' + required + '>';
|
||||
return '<input type="text" name="' + name + '" class="flat minwidth300' + autocompleteClass + '" value="' + this.escapeHtml(value) + '"' + autocompleteAttrs + required + ' autocomplete="off">';
|
||||
|
||||
case 'textarea':
|
||||
return '<textarea name="' + name + '" class="flat minwidth300" rows="3"' + required + '>' + this.escapeHtml(value) + '</textarea>';
|
||||
return '<textarea name="' + name + '" class="flat minwidth300' + autocompleteClass + '" rows="3"' + autocompleteAttrs + required + ' autocomplete="off">' + this.escapeHtml(value) + '</textarea>';
|
||||
|
||||
case 'number':
|
||||
var attrs = '';
|
||||
|
|
@ -10502,4 +10684,353 @@
|
|||
}
|
||||
};
|
||||
|
||||
// ===========================================
|
||||
// File Upload Dropzone
|
||||
// ===========================================
|
||||
|
||||
KundenKarte.initFileDropzone = function() {
|
||||
var dropzone = document.getElementById('fileDropzone');
|
||||
var fileInput = document.getElementById('fileInput');
|
||||
var selectedFilesDiv = document.getElementById('selectedFiles');
|
||||
var uploadBtn = document.getElementById('uploadBtn');
|
||||
var fileCountSpan = document.getElementById('fileCount');
|
||||
var form = document.getElementById('fileUploadForm');
|
||||
|
||||
if (!dropzone || !fileInput) return;
|
||||
|
||||
var selectedFiles = [];
|
||||
|
||||
// Click to open file dialog
|
||||
dropzone.addEventListener('click', function(e) {
|
||||
if (e.target.tagName !== 'A') {
|
||||
fileInput.click();
|
||||
}
|
||||
});
|
||||
|
||||
// Drag & Drop handlers
|
||||
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(function(eventName) {
|
||||
dropzone.addEventListener(eventName, function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
});
|
||||
});
|
||||
|
||||
['dragenter', 'dragover'].forEach(function(eventName) {
|
||||
dropzone.addEventListener(eventName, function() {
|
||||
dropzone.classList.add('dragover');
|
||||
});
|
||||
});
|
||||
|
||||
['dragleave', 'drop'].forEach(function(eventName) {
|
||||
dropzone.addEventListener(eventName, function() {
|
||||
dropzone.classList.remove('dragover');
|
||||
});
|
||||
});
|
||||
|
||||
dropzone.addEventListener('drop', function(e) {
|
||||
var files = e.dataTransfer.files;
|
||||
handleFiles(files);
|
||||
});
|
||||
|
||||
fileInput.addEventListener('change', function() {
|
||||
handleFiles(this.files);
|
||||
});
|
||||
|
||||
function handleFiles(files) {
|
||||
for (var i = 0; i < files.length; i++) {
|
||||
var file = files[i];
|
||||
// Check for duplicates
|
||||
var isDuplicate = selectedFiles.some(function(f) {
|
||||
return f.name === file.name && f.size === file.size;
|
||||
});
|
||||
if (!isDuplicate) {
|
||||
selectedFiles.push(file);
|
||||
}
|
||||
}
|
||||
updateFileList();
|
||||
}
|
||||
|
||||
function updateFileList() {
|
||||
selectedFilesDiv.innerHTML = '';
|
||||
|
||||
if (selectedFiles.length === 0) {
|
||||
selectedFilesDiv.style.display = 'none';
|
||||
uploadBtn.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
|
||||
selectedFilesDiv.style.display = 'flex';
|
||||
uploadBtn.style.display = 'inline-block';
|
||||
fileCountSpan.textContent = selectedFiles.length;
|
||||
|
||||
selectedFiles.forEach(function(file, index) {
|
||||
var fileDiv = document.createElement('div');
|
||||
fileDiv.className = 'kundenkarte-dropzone-file';
|
||||
|
||||
var icon = getFileIcon(file.name);
|
||||
fileDiv.innerHTML = '<i class="fa ' + icon + '"></i> ' +
|
||||
'<span>' + KundenKarte.AnlageConnection.escapeHtml(file.name) + '</span>' +
|
||||
'<span class="remove-file" data-index="' + index + '"><i class="fa fa-times"></i></span>';
|
||||
|
||||
selectedFilesDiv.appendChild(fileDiv);
|
||||
});
|
||||
|
||||
// Add remove handlers
|
||||
selectedFilesDiv.querySelectorAll('.remove-file').forEach(function(btn) {
|
||||
btn.addEventListener('click', function(e) {
|
||||
e.stopPropagation();
|
||||
var idx = parseInt(this.getAttribute('data-index'));
|
||||
selectedFiles.splice(idx, 1);
|
||||
updateFileList();
|
||||
});
|
||||
});
|
||||
|
||||
// Update file input with DataTransfer
|
||||
updateFileInput();
|
||||
}
|
||||
|
||||
function updateFileInput() {
|
||||
var dt = new DataTransfer();
|
||||
selectedFiles.forEach(function(file) {
|
||||
dt.items.add(file);
|
||||
});
|
||||
fileInput.files = dt.files;
|
||||
}
|
||||
|
||||
function getFileIcon(filename) {
|
||||
var ext = filename.split('.').pop().toLowerCase();
|
||||
if (['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp'].indexOf(ext) !== -1) {
|
||||
return 'fa-image';
|
||||
} else if (ext === 'pdf') {
|
||||
return 'fa-file-pdf-o';
|
||||
} else if (['doc', 'docx'].indexOf(ext) !== -1) {
|
||||
return 'fa-file-word-o';
|
||||
} else if (['xls', 'xlsx'].indexOf(ext) !== -1) {
|
||||
return 'fa-file-excel-o';
|
||||
}
|
||||
return 'fa-file-o';
|
||||
}
|
||||
};
|
||||
|
||||
// Auto-init on DOM ready
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', KundenKarte.initFileDropzone);
|
||||
} else {
|
||||
KundenKarte.initFileDropzone();
|
||||
}
|
||||
|
||||
// ===========================================
|
||||
// Field Autocomplete
|
||||
// ===========================================
|
||||
|
||||
KundenKarte.FieldAutocomplete = {
|
||||
baseUrl: '',
|
||||
activeInput: null,
|
||||
dropdown: null,
|
||||
debounceTimer: null,
|
||||
|
||||
init: function(baseUrl) {
|
||||
this.baseUrl = baseUrl || '';
|
||||
this.createDropdown();
|
||||
this.bindEvents();
|
||||
},
|
||||
|
||||
createDropdown: function() {
|
||||
// Create dropdown element if not exists
|
||||
if (!document.getElementById('kk-autocomplete-dropdown')) {
|
||||
var dropdown = document.createElement('div');
|
||||
dropdown.id = 'kk-autocomplete-dropdown';
|
||||
dropdown.className = 'kk-autocomplete-dropdown';
|
||||
dropdown.style.cssText = 'display:none;position:absolute;z-index:10000;background:#2d2d2d;border:1px solid #555;border-radius:4px;max-height:200px;overflow-y:auto;box-shadow:0 4px 12px rgba(0,0,0,0.3);';
|
||||
document.body.appendChild(dropdown);
|
||||
this.dropdown = dropdown;
|
||||
} else {
|
||||
this.dropdown = document.getElementById('kk-autocomplete-dropdown');
|
||||
}
|
||||
},
|
||||
|
||||
bindEvents: function() {
|
||||
var self = this;
|
||||
|
||||
// Delegate events for autocomplete inputs
|
||||
document.addEventListener('input', function(e) {
|
||||
if (e.target.classList.contains('kk-autocomplete')) {
|
||||
self.onInput(e.target);
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('focus', function(e) {
|
||||
if (e.target.classList.contains('kk-autocomplete')) {
|
||||
self.onFocus(e.target);
|
||||
}
|
||||
}, true);
|
||||
|
||||
document.addEventListener('blur', function(e) {
|
||||
if (e.target.classList.contains('kk-autocomplete')) {
|
||||
// Delay to allow click on dropdown item
|
||||
setTimeout(function() {
|
||||
self.hideDropdown();
|
||||
}, 200);
|
||||
}
|
||||
}, true);
|
||||
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (e.target.classList.contains('kk-autocomplete') && self.dropdown.style.display !== 'none') {
|
||||
self.onKeydown(e);
|
||||
}
|
||||
});
|
||||
|
||||
// Click on dropdown item
|
||||
this.dropdown.addEventListener('click', function(e) {
|
||||
var item = e.target.closest('.kk-autocomplete-item');
|
||||
if (item) {
|
||||
self.selectItem(item);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
onInput: function(input) {
|
||||
var self = this;
|
||||
this.activeInput = input;
|
||||
|
||||
clearTimeout(this.debounceTimer);
|
||||
this.debounceTimer = setTimeout(function() {
|
||||
self.fetchSuggestions(input);
|
||||
}, 150);
|
||||
},
|
||||
|
||||
onFocus: function(input) {
|
||||
this.activeInput = input;
|
||||
if (input.value.length >= 1) {
|
||||
this.fetchSuggestions(input);
|
||||
}
|
||||
},
|
||||
|
||||
onKeydown: function(e) {
|
||||
var items = this.dropdown.querySelectorAll('.kk-autocomplete-item');
|
||||
var activeItem = this.dropdown.querySelector('.kk-autocomplete-item.active');
|
||||
var activeIndex = -1;
|
||||
|
||||
items.forEach(function(item, idx) {
|
||||
if (item.classList.contains('active')) activeIndex = idx;
|
||||
});
|
||||
|
||||
if (e.key === 'ArrowDown') {
|
||||
e.preventDefault();
|
||||
if (activeIndex < items.length - 1) {
|
||||
if (activeItem) activeItem.classList.remove('active');
|
||||
items[activeIndex + 1].classList.add('active');
|
||||
items[activeIndex + 1].scrollIntoView({ block: 'nearest' });
|
||||
}
|
||||
} else if (e.key === 'ArrowUp') {
|
||||
e.preventDefault();
|
||||
if (activeIndex > 0) {
|
||||
if (activeItem) activeItem.classList.remove('active');
|
||||
items[activeIndex - 1].classList.add('active');
|
||||
items[activeIndex - 1].scrollIntoView({ block: 'nearest' });
|
||||
}
|
||||
} else if (e.key === 'Enter' && activeItem) {
|
||||
e.preventDefault();
|
||||
this.selectItem(activeItem);
|
||||
} else if (e.key === 'Escape') {
|
||||
this.hideDropdown();
|
||||
}
|
||||
},
|
||||
|
||||
fetchSuggestions: function(input) {
|
||||
var self = this;
|
||||
var fieldCode = input.getAttribute('data-field-code');
|
||||
var typeId = input.getAttribute('data-type-id') || 0;
|
||||
var query = input.value;
|
||||
|
||||
if (!fieldCode) return;
|
||||
|
||||
$.ajax({
|
||||
url: this.baseUrl + '/custom/kundenkarte/ajax/field_autocomplete.php',
|
||||
method: 'GET',
|
||||
data: {
|
||||
action: 'suggest',
|
||||
field_code: fieldCode,
|
||||
type_id: typeId,
|
||||
query: query
|
||||
},
|
||||
dataType: 'json',
|
||||
success: function(response) {
|
||||
if (response.suggestions && response.suggestions.length > 0) {
|
||||
self.showSuggestions(input, response.suggestions, query);
|
||||
} else {
|
||||
self.hideDropdown();
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
showSuggestions: function(input, suggestions, query) {
|
||||
var self = this;
|
||||
var rect = input.getBoundingClientRect();
|
||||
|
||||
this.dropdown.innerHTML = '';
|
||||
this.dropdown.style.top = (rect.bottom + window.scrollY) + 'px';
|
||||
this.dropdown.style.left = rect.left + 'px';
|
||||
this.dropdown.style.width = rect.width + 'px';
|
||||
this.dropdown.style.display = 'block';
|
||||
|
||||
suggestions.forEach(function(suggestion, idx) {
|
||||
var item = document.createElement('div');
|
||||
item.className = 'kk-autocomplete-item';
|
||||
if (idx === 0) item.classList.add('active');
|
||||
item.style.cssText = 'padding:8px 12px;cursor:pointer;color:#e0e0e0;border-bottom:1px solid #444;';
|
||||
item.setAttribute('data-value', suggestion);
|
||||
|
||||
// Highlight matching text
|
||||
var html = self.highlightMatch(suggestion, query);
|
||||
item.innerHTML = html;
|
||||
|
||||
self.dropdown.appendChild(item);
|
||||
});
|
||||
|
||||
// Hover effect
|
||||
this.dropdown.querySelectorAll('.kk-autocomplete-item').forEach(function(item) {
|
||||
item.addEventListener('mouseenter', function() {
|
||||
self.dropdown.querySelectorAll('.kk-autocomplete-item').forEach(function(i) {
|
||||
i.classList.remove('active');
|
||||
});
|
||||
item.classList.add('active');
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
highlightMatch: function(text, query) {
|
||||
if (!query) return text;
|
||||
var regex = new RegExp('(' + query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + ')', 'gi');
|
||||
return text.replace(regex, '<strong style="color:#3498db;">$1</strong>');
|
||||
},
|
||||
|
||||
selectItem: function(item) {
|
||||
var value = item.getAttribute('data-value');
|
||||
if (this.activeInput) {
|
||||
this.activeInput.value = value;
|
||||
// Trigger change event
|
||||
var event = new Event('change', { bubbles: true });
|
||||
this.activeInput.dispatchEvent(event);
|
||||
}
|
||||
this.hideDropdown();
|
||||
},
|
||||
|
||||
hideDropdown: function() {
|
||||
if (this.dropdown) {
|
||||
this.dropdown.style.display = 'none';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Auto-init autocomplete
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
KundenKarte.FieldAutocomplete.init('');
|
||||
});
|
||||
} else {
|
||||
KundenKarte.FieldAutocomplete.init('');
|
||||
}
|
||||
|
||||
})();
|
||||
|
|
|
|||
|
|
@ -322,6 +322,60 @@ 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
|
||||
|
||||
# Tree Display Settings
|
||||
TreeDisplaySettings = Baum-Anzeige Einstellungen
|
||||
TreeInfoDisplayMode = Zusatzinfos-Anzeige
|
||||
DisplayAsBadge = Als Badge (mit Icon)
|
||||
DisplayInParentheses = In Klammern
|
||||
DisplayNone = Ausblenden
|
||||
TreeBadgeColor = Badge-Farbe
|
||||
TreeBadgeColorHelp = Waehlen Sie die Hintergrundfarbe fuer die Info-Badges
|
||||
Hidden = Ausgeblendet
|
||||
TreeDisplayMode = Anzeige-Modus
|
||||
Badge = Badge
|
||||
Parentheses = Klammer
|
||||
|
||||
# File Pinning
|
||||
Pin = Anheften
|
||||
Unpin = Loslassen
|
||||
Pinned = Angeheftet
|
||||
FilePinned = Datei wurde angeheftet
|
||||
FileUnpinned = Datei wurde losgelassen
|
||||
|
||||
# Backup & Restore
|
||||
BackupRestore = Backup & Wiederherstellung
|
||||
CreateBackup = Backup erstellen
|
||||
BackupOptions = Backup-Optionen
|
||||
IncludeUploadedFiles = Hochgeladene Dateien einschliessen
|
||||
IncludeFilesHelp = Alle Bilder und Dokumente der Anlagen werden im Backup gespeichert
|
||||
CreateBackupNow = Backup jetzt erstellen
|
||||
UploadBackup = Backup hochladen
|
||||
UploadBackupFile = Backup-Datei hochladen
|
||||
SelectBackupFile = Backup-Datei auswaehlen
|
||||
ExistingBackups = Vorhandene Backups
|
||||
NoBackupsFound = Keine Backups gefunden
|
||||
BackupCreatedSuccess = Backup wurde erstellt: %s
|
||||
BackupCreatedError = Backup konnte nicht erstellt werden
|
||||
BackupDeleted = Backup wurde geloescht
|
||||
BackupUploaded = Backup wurde hochgeladen
|
||||
InvalidBackupFile = Ungueltige Backup-Datei
|
||||
DeleteBackup = Backup loeschen
|
||||
ConfirmDeleteBackup = Moechten Sie das Backup "%s" wirklich loeschen?
|
||||
RestoreBackup = Backup wiederherstellen
|
||||
ConfirmRestoreBackup = Moechten Sie das Backup "%s" wirklich wiederherstellen? Dies kann bestehende Daten ueberschreiben.
|
||||
ClearExistingData = Bestehende Daten vorher loeschen
|
||||
RestoreSuccess = Wiederherstellung erfolgreich
|
||||
RestoreError = Wiederherstellung fehlgeschlagen
|
||||
TotalElements = Gesamt Elemente
|
||||
TotalFiles = Gesamt Dateien
|
||||
TotalConnections = Gesamt Verbindungen
|
||||
CustomersWithAnlagen = Kunden mit Anlagen
|
||||
FilesStorageSize = Dateispeicher-Groesse
|
||||
BackupInfo = Backup-Informationen
|
||||
BackupInfoContent = Das Backup enthaelt alle Anlagen, Typen, Systeme, Felder und Verbindungen
|
||||
BackupInfoFiles = Wenn aktiviert, werden auch alle hochgeladenen Dateien (Bilder, PDFs, Dokumente) gesichert
|
||||
BackupInfoRestore = Bei der Wiederherstellung koennen bestehende Daten ueberschrieben oder ergaenzt werden
|
||||
|
||||
# Standard Dolibarr
|
||||
Save = Speichern
|
||||
Cancel = Abbrechen
|
||||
|
|
|
|||
|
|
@ -187,6 +187,60 @@ 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
|
||||
|
||||
# Tree Display Settings
|
||||
TreeDisplaySettings = Tree Display Settings
|
||||
TreeInfoDisplayMode = Info Display Mode
|
||||
DisplayAsBadge = As Badge (with icon)
|
||||
DisplayInParentheses = In Parentheses
|
||||
DisplayNone = Hidden
|
||||
TreeBadgeColor = Badge Color
|
||||
TreeBadgeColorHelp = Select the background color for the info badges
|
||||
Hidden = Hidden
|
||||
TreeDisplayMode = Display Mode
|
||||
Badge = Badge
|
||||
Parentheses = Parentheses
|
||||
|
||||
# File Pinning
|
||||
Pin = Pin
|
||||
Unpin = Unpin
|
||||
Pinned = Pinned
|
||||
FilePinned = File has been pinned
|
||||
FileUnpinned = File has been unpinned
|
||||
|
||||
# Backup & Restore
|
||||
BackupRestore = Backup & Restore
|
||||
CreateBackup = Create Backup
|
||||
BackupOptions = Backup Options
|
||||
IncludeUploadedFiles = Include uploaded files
|
||||
IncludeFilesHelp = All images and documents from installations will be saved in the backup
|
||||
CreateBackupNow = Create Backup Now
|
||||
UploadBackup = Upload Backup
|
||||
UploadBackupFile = Upload Backup File
|
||||
SelectBackupFile = Select backup file
|
||||
ExistingBackups = Existing Backups
|
||||
NoBackupsFound = No backups found
|
||||
BackupCreatedSuccess = Backup created: %s
|
||||
BackupCreatedError = Backup could not be created
|
||||
BackupDeleted = Backup deleted
|
||||
BackupUploaded = Backup uploaded
|
||||
InvalidBackupFile = Invalid backup file
|
||||
DeleteBackup = Delete Backup
|
||||
ConfirmDeleteBackup = Do you really want to delete the backup "%s"?
|
||||
RestoreBackup = Restore Backup
|
||||
ConfirmRestoreBackup = Do you really want to restore the backup "%s"? This may overwrite existing data.
|
||||
ClearExistingData = Clear existing data first
|
||||
RestoreSuccess = Restore successful
|
||||
RestoreError = Restore failed
|
||||
TotalElements = Total Elements
|
||||
TotalFiles = Total Files
|
||||
TotalConnections = Total Connections
|
||||
CustomersWithAnlagen = Customers with Installations
|
||||
FilesStorageSize = Files Storage Size
|
||||
BackupInfo = Backup Information
|
||||
BackupInfoContent = The backup contains all installations, types, systems, fields and connections
|
||||
BackupInfoFiles = If enabled, all uploaded files (images, PDFs, documents) will also be backed up
|
||||
BackupInfoRestore = When restoring, existing data can be overwritten or supplemented
|
||||
|
||||
# Standard Dolibarr
|
||||
Save = Save
|
||||
Cancel = Cancel
|
||||
|
|
|
|||
|
|
@ -74,6 +74,11 @@ function kundenkarteAdminPrepareHead()
|
|||
$head[$h][2] = 'building_types';
|
||||
$h++;
|
||||
|
||||
$head[$h][0] = dol_buildpath("/kundenkarte/admin/backup.php", 1);
|
||||
$head[$h][1] = $langs->trans("BackupRestore");
|
||||
$head[$h][2] = 'backup';
|
||||
$h++;
|
||||
|
||||
/*
|
||||
$head[$h][0] = dol_buildpath("/kundenkarte/admin/myobject_extrafields.php", 1);
|
||||
$head[$h][1] = $langs->trans("ExtraFields");
|
||||
|
|
@ -138,3 +143,106 @@ function kundenkarte_render_icon($picto, $alt = '', $style = '')
|
|||
// Font Awesome icon
|
||||
return '<i class="fa '.dol_escape_htmltag($picto).'"'.($style ? ' style="'.$style.'"' : '').'></i>';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get icon for a field based on field code or type
|
||||
*
|
||||
* @param string $fieldCode Field code
|
||||
* @param string $fieldType Field type
|
||||
* @return string Font Awesome icon class
|
||||
*/
|
||||
function kundenkarte_get_field_icon($fieldCode, $fieldType = '')
|
||||
{
|
||||
// Map common field codes to icons
|
||||
$codeIcons = array(
|
||||
'standort' => 'fa-map-marker',
|
||||
'location' => 'fa-map-marker',
|
||||
'ort' => 'fa-map-marker',
|
||||
'raum' => 'fa-door-open',
|
||||
'room' => 'fa-door-open',
|
||||
'zimmer' => 'fa-door-open',
|
||||
'etage' => 'fa-layer-group',
|
||||
'floor' => 'fa-layer-group',
|
||||
'stockwerk' => 'fa-layer-group',
|
||||
'gebaeude' => 'fa-building',
|
||||
'building' => 'fa-building',
|
||||
'adresse' => 'fa-home',
|
||||
'address' => 'fa-home',
|
||||
'hersteller' => 'fa-industry',
|
||||
'manufacturer' => 'fa-industry',
|
||||
'modell' => 'fa-tag',
|
||||
'model' => 'fa-tag',
|
||||
'seriennummer' => 'fa-barcode',
|
||||
'serial' => 'fa-barcode',
|
||||
'leistung' => 'fa-bolt',
|
||||
'power' => 'fa-bolt',
|
||||
'spannung' => 'fa-plug',
|
||||
'voltage' => 'fa-plug',
|
||||
'strom' => 'fa-bolt',
|
||||
'current' => 'fa-bolt',
|
||||
'datum' => 'fa-calendar',
|
||||
'date' => 'fa-calendar',
|
||||
'installation' => 'fa-wrench',
|
||||
'wartung' => 'fa-tools',
|
||||
'maintenance' => 'fa-tools',
|
||||
'ip' => 'fa-network-wired',
|
||||
'ip_adresse' => 'fa-network-wired',
|
||||
'mac' => 'fa-ethernet',
|
||||
'telefon' => 'fa-phone',
|
||||
'phone' => 'fa-phone',
|
||||
'notiz' => 'fa-sticky-note',
|
||||
'note' => 'fa-sticky-note',
|
||||
'bemerkung' => 'fa-comment',
|
||||
'comment' => 'fa-comment',
|
||||
);
|
||||
|
||||
// Check field code (lowercase)
|
||||
$codeLower = strtolower($fieldCode);
|
||||
foreach ($codeIcons as $key => $icon) {
|
||||
if (strpos($codeLower, $key) !== false) {
|
||||
return $icon;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback based on field type
|
||||
$typeIcons = array(
|
||||
'date' => 'fa-calendar',
|
||||
'number' => 'fa-hashtag',
|
||||
'textarea' => 'fa-align-left',
|
||||
'select' => 'fa-list',
|
||||
'checkbox' => 'fa-check-square',
|
||||
);
|
||||
|
||||
if (isset($typeIcons[$fieldType])) {
|
||||
return $typeIcons[$fieldType];
|
||||
}
|
||||
|
||||
// Default icon
|
||||
return 'fa-info-circle';
|
||||
}
|
||||
|
||||
/**
|
||||
* Adjust a hex color by a percentage
|
||||
*
|
||||
* @param string $hex Hex color (e.g., #2a4a5e)
|
||||
* @param int $percent Percentage to adjust (-100 to 100, negative = darker)
|
||||
* @return string Adjusted hex color
|
||||
*/
|
||||
function kundenkarte_adjust_color($hex, $percent)
|
||||
{
|
||||
// Remove # if present
|
||||
$hex = ltrim($hex, '#');
|
||||
|
||||
// Parse RGB values
|
||||
$r = hexdec(substr($hex, 0, 2));
|
||||
$g = hexdec(substr($hex, 2, 2));
|
||||
$b = hexdec(substr($hex, 4, 2));
|
||||
|
||||
// Adjust each component
|
||||
$r = max(0, min(255, $r + $percent));
|
||||
$g = max(0, min(255, $g + $percent));
|
||||
$b = max(0, min(255, $b + $percent));
|
||||
|
||||
// Return as hex
|
||||
return sprintf('#%02x%02x%02x', $r, $g, $b);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ CREATE TABLE llx_kundenkarte_anlage_files
|
|||
description text,
|
||||
|
||||
is_cover tinyint DEFAULT 0 NOT NULL,
|
||||
is_pinned tinyint DEFAULT 0 NOT NULL,
|
||||
position integer DEFAULT 0,
|
||||
|
||||
share varchar(128),
|
||||
|
|
|
|||
|
|
@ -21,7 +21,10 @@ CREATE TABLE llx_kundenkarte_anlage_type_field
|
|||
|
||||
required tinyint DEFAULT 0 NOT NULL,
|
||||
show_in_tree tinyint DEFAULT 0 NOT NULL,
|
||||
tree_display_mode varchar(20) DEFAULT 'badge',
|
||||
badge_color varchar(7),
|
||||
show_in_hover tinyint DEFAULT 1 NOT NULL,
|
||||
enable_autocomplete tinyint DEFAULT 0 NOT NULL,
|
||||
|
||||
position integer DEFAULT 0,
|
||||
active tinyint DEFAULT 1 NOT NULL,
|
||||
|
|
|
|||
16
sql/update_3.6.0.sql
Normal file
16
sql/update_3.6.0.sql
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
-- ============================================================================
|
||||
-- KundenKarte Module - Update 3.6.0
|
||||
-- Autocomplete for text fields + File pinning
|
||||
-- ============================================================================
|
||||
|
||||
-- Add autocomplete option to field definitions
|
||||
ALTER TABLE llx_kundenkarte_anlage_type_field
|
||||
ADD COLUMN enable_autocomplete tinyint DEFAULT 0 NOT NULL AFTER show_in_hover;
|
||||
|
||||
-- Add pinned flag for files (pinned files appear first)
|
||||
ALTER TABLE llx_kundenkarte_anlage_files
|
||||
ADD COLUMN is_pinned tinyint DEFAULT 0 NOT NULL AFTER is_cover;
|
||||
|
||||
-- Add tree display mode for fields (badge = right side badge, parentheses = after label in parentheses)
|
||||
ALTER TABLE llx_kundenkarte_anlage_type_field
|
||||
ADD COLUMN tree_display_mode varchar(20) DEFAULT 'badge' AFTER show_in_tree;
|
||||
327
tabs/anlagen.php
327
tabs/anlagen.php
|
|
@ -35,7 +35,11 @@ dol_include_once('/kundenkarte/lib/kundenkarte.lib.php');
|
|||
$langs->loadLangs(array('companies', 'kundenkarte@kundenkarte'));
|
||||
|
||||
// Get parameters
|
||||
// Support both 'id' and 'socid' for compatibility with Dolibarr's customer navigation arrows
|
||||
$id = GETPOSTINT('id');
|
||||
if ($id <= 0) {
|
||||
$id = GETPOSTINT('socid');
|
||||
}
|
||||
$action = GETPOST('action', 'aZ09');
|
||||
$confirm = GETPOST('confirm', 'alpha');
|
||||
$systemId = GETPOSTINT('system');
|
||||
|
|
@ -53,13 +57,32 @@ $form = new Form($db);
|
|||
$anlage = new Anlage($db);
|
||||
$anlageType = new AnlageType($db);
|
||||
|
||||
// Load thirdparty
|
||||
if ($id > 0) {
|
||||
$result = $object->fetch($id);
|
||||
if ($result <= 0) {
|
||||
dol_print_error($db, $object->error);
|
||||
exit;
|
||||
// Load thirdparty - 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 && $tmpAnlage->fk_soc > 0) {
|
||||
$id = $tmpAnlage->fk_soc;
|
||||
// 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 - redirect to thirdparty list
|
||||
header('Location: '.DOL_URL_ROOT.'/societe/list.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
$result = $object->fetch($id);
|
||||
if ($result <= 0) {
|
||||
// Thirdparty not found - redirect to list
|
||||
header('Location: '.DOL_URL_ROOT.'/societe/list.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
$permissiontoread = $user->hasRight('kundenkarte', 'read');
|
||||
|
|
@ -244,7 +267,7 @@ if ($action == 'confirm_delete' && $confirm == 'yes' && $permissiontodelete) {
|
|||
exit;
|
||||
}
|
||||
|
||||
// File upload
|
||||
// File upload (multi-file support)
|
||||
if ($action == 'uploadfile' && $permissiontoadd) {
|
||||
$anlage->fetch($anlageId);
|
||||
$upload_dir = $anlage->getFileDirectory();
|
||||
|
|
@ -254,23 +277,47 @@ if ($action == 'uploadfile' && $permissiontoadd) {
|
|||
dol_mkdir($upload_dir);
|
||||
}
|
||||
|
||||
if (!empty($_FILES['userfile']['name'])) {
|
||||
$result = dol_add_file_process($upload_dir, 0, 1, 'userfile', '', null, '', 1);
|
||||
if ($result > 0) {
|
||||
// Add to database
|
||||
$anlagefile = new AnlageFile($db);
|
||||
$anlagefile->fk_anlage = $anlageId;
|
||||
$anlagefile->filename = dol_sanitizeFileName($_FILES['userfile']['name']);
|
||||
// IMPORTANT: Store ONLY relative path (anlagen/socid/anlageid/filename) - never full path!
|
||||
$anlagefile->filepath = 'anlagen/'.$anlage->fk_soc.'/'.$anlage->id.'/'.$anlagefile->filename;
|
||||
$anlagefile->filesize = $_FILES['userfile']['size'];
|
||||
$anlagefile->mimetype = dol_mimetype($anlagefile->filename);
|
||||
$anlagefile->create($user);
|
||||
$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;
|
||||
|
||||
// Generate thumbnail
|
||||
$anlagefile->generateThumbnail();
|
||||
// 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++;
|
||||
}
|
||||
|
||||
setEventMessages($langs->trans('FileUploaded'), null, 'mesgs');
|
||||
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);
|
||||
|
|
@ -295,6 +342,22 @@ if ($action == 'confirm_deletefile' && $confirm == 'yes' && $permissiontodelete)
|
|||
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
|
||||
*/
|
||||
|
|
@ -390,6 +453,10 @@ print '</div>';
|
|||
$isTreeView = !in_array($action, array('create', 'edit', 'view', 'copy'));
|
||||
if ($isTreeView) {
|
||||
print '<div class="kundenkarte-tree-controls">';
|
||||
// Compact mode toggle (visible on mobile)
|
||||
print '<button type="button" class="kundenkarte-view-toggle" id="btn-compact-mode" title="Kompakte Ansicht">';
|
||||
print '<i class="fa fa-compress"></i> <span>Kompakt</span>';
|
||||
print '</button>';
|
||||
print '<button type="button" class="button small" id="btn-expand-all" title="'.$langs->trans('ExpandAll').'">';
|
||||
print '<i class="fa fa-expand"></i> '.$langs->trans('ExpandAll');
|
||||
print '</button>';
|
||||
|
|
@ -511,17 +578,29 @@ if (empty($customerSystems)) {
|
|||
print '<br><h4>'.$langs->trans('AttachedFiles').'</h4>';
|
||||
|
||||
if ($permissiontoadd) {
|
||||
print '<form method="POST" enctype="multipart/form-data" action="'.$_SERVER['PHP_SELF'].'?id='.$id.'&system='.$systemId.'&action=uploadfile&anlage_id='.$anlageId.'">';
|
||||
print '<form method="POST" enctype="multipart/form-data" action="'.$_SERVER['PHP_SELF'].'?id='.$id.'&system='.$systemId.'&action=uploadfile&anlage_id='.$anlageId.'" id="fileUploadForm">';
|
||||
print '<input type="hidden" name="token" value="'.newToken().'">';
|
||||
print '<input type="file" name="userfile" accept="image/*,.pdf,.doc,.docx">';
|
||||
print ' <button type="submit" class="button">'.$langs->trans('Upload').'</button>';
|
||||
print '<div class="kundenkarte-dropzone" id="fileDropzone">';
|
||||
print '<input type="file" name="userfiles[]" id="fileInput" multiple accept="image/*,.pdf,.doc,.docx,.xls,.xlsx,.odt,.ods,.txt,.rtf,.zip,.rar,.7z,.tar,.gz" style="display:none;">';
|
||||
print '<div class="kundenkarte-dropzone-content">';
|
||||
print '<i class="fa fa-cloud-upload" style="font-size:32px;color:#3498db;margin-bottom:10px;"></i>';
|
||||
print '<p style="margin:0;color:#888;">Dateien hierher ziehen oder <a href="#" onclick="document.getElementById(\'fileInput\').click();return false;" style="color:#3498db;">durchsuchen</a></p>';
|
||||
print '<p style="margin:5px 0 0;font-size:11px;color:#666;">Bilder, PDF, Office-Dokumente, ZIP-Archive</p>';
|
||||
print '</div>';
|
||||
print '<div class="kundenkarte-dropzone-files" id="selectedFiles" style="display:none;"></div>';
|
||||
print '</div>';
|
||||
print '<button type="submit" class="button" id="uploadBtn" style="display:none;margin-top:10px;">'.$langs->trans('Upload').' (<span id="fileCount">0</span> Dateien)</button>';
|
||||
print '</form><br>';
|
||||
}
|
||||
|
||||
if (!empty($files)) {
|
||||
print '<div class="kundenkarte-files-grid">';
|
||||
foreach ($files as $file) {
|
||||
print '<div class="kundenkarte-file-item">';
|
||||
$pinnedClass = $file->is_pinned ? ' kundenkarte-file-pinned' : '';
|
||||
print '<div class="kundenkarte-file-item'.$pinnedClass.'">';
|
||||
if ($file->is_pinned) {
|
||||
print '<div class="kundenkarte-pin-indicator" title="'.$langs->trans('Pinned').'"><i class="fa fa-thumb-tack"></i></div>';
|
||||
}
|
||||
print '<div class="kundenkarte-file-preview">';
|
||||
if ($file->file_type == 'image') {
|
||||
$thumbUrl = $file->getThumbUrl();
|
||||
|
|
@ -536,14 +615,41 @@ if (empty($customerSystems)) {
|
|||
print '<iframe src="'.$file->getUrl().'#page=1&toolbar=0&navpanes=0&statusbar=0&view=FitH" class="kundenkarte-pdf-preview-frame"></iframe>';
|
||||
print '</div>';
|
||||
} else {
|
||||
print '<i class="fa fa-file-o" style="font-size:48px;color:#999;"></i>';
|
||||
// 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 '<i class="fa '.$fileIcon.'" style="font-size:48px;color:'.$iconColor.';"></i>';
|
||||
}
|
||||
print '</div>';
|
||||
print '<div class="kundenkarte-file-info">';
|
||||
print '<div class="kundenkarte-file-name" title="'.dol_escape_htmltag($file->filename).'">'.dol_escape_htmltag(dol_trunc($file->filename, 20)).'</div>';
|
||||
print '<div class="kundenkarte-file-name" title="'.dol_escape_htmltag($file->filename).'">'.dol_escape_htmltag(dol_trunc($file->filename, 35)).'</div>';
|
||||
print '<div class="kundenkarte-file-size">'.dol_print_size($file->filesize).'</div>';
|
||||
print '<div class="kundenkarte-file-actions">';
|
||||
print '<a href="'.$file->getUrl().'" target="_blank" class="kundenkarte-file-btn" title="'.$langs->trans('View').'"><i class="fa fa-eye"></i></a>';
|
||||
if ($permissiontoadd) {
|
||||
$pinClass = $file->is_pinned ? ' kundenkarte-file-btn-pinned' : '';
|
||||
$pinTitle = $file->is_pinned ? $langs->trans('Unpin') : $langs->trans('Pin');
|
||||
print '<a href="'.$_SERVER['PHP_SELF'].'?id='.$id.'&system='.$systemId.'&action=togglepin&anlage_id='.$anlageId.'&fileid='.$file->id.'" class="kundenkarte-file-btn'.$pinClass.'" title="'.$pinTitle.'"><i class="fa fa-thumb-tack"></i></a>';
|
||||
}
|
||||
if ($permissiontodelete) {
|
||||
print '<a href="'.$_SERVER['PHP_SELF'].'?id='.$id.'&system='.$systemId.'&action=askdeletefile&anlage_id='.$anlageId.'&fileid='.$file->id.'" class="kundenkarte-file-btn kundenkarte-file-btn-delete" title="'.$langs->trans('Delete').'"><i class="fa fa-trash"></i></a>';
|
||||
}
|
||||
|
|
@ -865,6 +971,19 @@ if (empty($customerSystems)) {
|
|||
$typeSelect.prop("disabled", true);
|
||||
$("#row_type").hide();
|
||||
}
|
||||
|
||||
// Select2 für übergeordnetes Element mit Icons
|
||||
var $parentSelect = $("select[name=\'fk_parent\']");
|
||||
if ($parentSelect.length) {
|
||||
$parentSelect.select2({
|
||||
templateResult: formatTypeOption,
|
||||
templateSelection: formatTypeOption,
|
||||
placeholder: "'.dol_escape_js($langs->trans('SelectParent')).'",
|
||||
allowClear: true,
|
||||
width: "300px",
|
||||
dropdownAutoWidth: true
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>';
|
||||
}
|
||||
|
|
@ -949,7 +1068,8 @@ function printTree($nodes, $socid, $systemId, $canEdit, $canDelete, $langs, $lev
|
|||
);
|
||||
|
||||
// Collect fields for tooltip (show_in_hover) and tree label (show_in_tree)
|
||||
$treeInfo = array();
|
||||
$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) {
|
||||
|
|
@ -984,10 +1104,24 @@ function printTree($nodes, $socid, $systemId, $canEdit, $canDelete, $langs, $lev
|
|||
// 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) {
|
||||
$treeInfo[] = dol_print_date(strtotime($value), 'day');
|
||||
$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 {
|
||||
$treeInfo[] = $value;
|
||||
$treeInfoBadges[] = $fieldInfo;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1045,34 +1179,47 @@ function printTree($nodes, $socid, $systemId, $canEdit, $canDelete, $langs, $lev
|
|||
$picto = $node->type_picto ? $node->type_picto : 'fa-cube';
|
||||
print '<span class="kundenkarte-tree-icon kundenkarte-tooltip-trigger" data-tooltip="'.htmlspecialchars(json_encode($tooltipData), ENT_QUOTES, 'UTF-8').'" data-anlage-id="'.$node->id.'">'.kundenkarte_render_icon($picto).'</span>';
|
||||
|
||||
// Label with tree info in parentheses + file indicators
|
||||
// Label with parentheses info (directly after label, only values)
|
||||
$viewUrl = $_SERVER['PHP_SELF'].'?id='.$socid.'&system='.$systemId.'&action=view&anlage_id='.$node->id;
|
||||
print '<span class="kundenkarte-tree-label">'.dol_escape_htmltag($node->label);
|
||||
if (!empty($treeInfo)) {
|
||||
print ' <span class="kundenkarte-tree-label-info">('.dol_escape_htmltag(implode(', ', $treeInfo)).')</span>';
|
||||
}
|
||||
// File indicators - directly after parentheses
|
||||
if ($node->image_count > 0 || $node->doc_count > 0) {
|
||||
print ' <span class="kundenkarte-tree-files">';
|
||||
if ($node->image_count > 0) {
|
||||
print '<a href="'.$viewUrl.'#files" class="kundenkarte-tree-file-badge kundenkarte-tree-file-images kundenkarte-images-trigger" data-anlage-id="'.$node->id.'" title="'.$node->image_count.' '.($node->image_count == 1 ? 'Bild' : 'Bilder').'">';
|
||||
print '<i class="fa fa-image"></i>';
|
||||
if ($node->image_count > 1) {
|
||||
print ' '.$node->image_count;
|
||||
}
|
||||
print '</a>';
|
||||
|
||||
// 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']);
|
||||
}
|
||||
if ($node->doc_count > 0) {
|
||||
print '<a href="'.$viewUrl.'#files" class="kundenkarte-tree-file-badge kundenkarte-tree-file-docs kundenkarte-docs-trigger" data-anlage-id="'.$node->id.'" title="'.$node->doc_count.' '.($node->doc_count == 1 ? 'Dokument' : 'Dokumente').'">';
|
||||
print '<i class="fa fa-file-pdf-o"></i>';
|
||||
if ($node->doc_count > 1) {
|
||||
print ' '.$node->doc_count;
|
||||
}
|
||||
print '</a>';
|
||||
print ' <span class="kundenkarte-tree-label-info">('.implode(', ', $infoValues).')</span>';
|
||||
}
|
||||
print '</span>';
|
||||
|
||||
// Spacer to push badges to the right
|
||||
print '<span class="kundenkarte-tree-spacer"></span>';
|
||||
|
||||
// Badges (far right, before actions)
|
||||
$defaultBadgeColor = getDolGlobalString('KUNDENKARTE_TREE_BADGE_COLOR', '#2a4a5e');
|
||||
if (!empty($treeInfoBadges)) {
|
||||
print '<span class="kundenkarte-tree-badges">';
|
||||
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 '<span class="kundenkarte-tree-badge" title="'.dol_escape_htmltag($info['label']).'" style="background:linear-gradient(135deg, '.$fieldBadgeColor.' 0%, '.kundenkarte_adjust_color($fieldBadgeColor, -20).' 100%);">';
|
||||
print '<i class="fa '.$badgeIcon.'"></i> '.dol_escape_htmltag($info['value']);
|
||||
print '</span>';
|
||||
}
|
||||
print '</span>';
|
||||
}
|
||||
print '</span>';
|
||||
|
||||
// File indicators
|
||||
if ($node->image_count > 0 || $node->doc_count > 0) {
|
||||
$totalFiles = $node->image_count + $node->doc_count;
|
||||
print '<span class="kundenkarte-tree-files">';
|
||||
print '<a href="'.$viewUrl.'#files" class="kundenkarte-tree-file-badge kundenkarte-tree-file-all" data-anlage-id="'.$node->id.'" title="'.$totalFiles.' '.($totalFiles == 1 ? 'Datei' : 'Dateien').'">';
|
||||
print '<i class="fa fa-paperclip"></i> '.$totalFiles;
|
||||
print '</a>';
|
||||
print '</span>';
|
||||
}
|
||||
|
||||
// Type badge
|
||||
if ($node->type_short || $node->type_label) {
|
||||
|
|
@ -1156,7 +1303,8 @@ function printTreeWithCableLines($nodes, $socid, $systemId, $canEdit, $canDelete
|
|||
'fields' => array()
|
||||
);
|
||||
|
||||
$treeInfo = 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;
|
||||
|
|
@ -1173,7 +1321,20 @@ function printTreeWithCableLines($nodes, $socid, $systemId, $canEdit, $canDelete
|
|||
);
|
||||
}
|
||||
if ($fieldDef->show_in_tree && $value !== '') {
|
||||
$treeInfo[] = ($fieldDef->field_type === 'date' && $value) ? dol_print_date(strtotime($value), 'day') : $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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1279,27 +1440,43 @@ function printTreeWithCableLines($nodes, $socid, $systemId, $canEdit, $canDelete
|
|||
print '<span class="kundenkarte-tree-icon kundenkarte-tooltip-trigger" data-tooltip="'.htmlspecialchars(json_encode($tooltipData), ENT_QUOTES, 'UTF-8').'" data-anlage-id="'.$node->id.'">'.kundenkarte_render_icon($picto).'</span>';
|
||||
|
||||
$viewUrl = $_SERVER['PHP_SELF'].'?id='.$socid.'&system='.$systemId.'&action=view&anlage_id='.$node->id;
|
||||
// Label with parentheses info (only values, no field names)
|
||||
print '<span class="kundenkarte-tree-label">'.dol_escape_htmltag($node->label);
|
||||
if (!empty($treeInfo)) {
|
||||
print ' <span class="kundenkarte-tree-label-info">('.dol_escape_htmltag(implode(', ', $treeInfo)).')</span>';
|
||||
}
|
||||
if ($node->image_count > 0 || $node->doc_count > 0) {
|
||||
print ' <span class="kundenkarte-tree-files">';
|
||||
if ($node->image_count > 0) {
|
||||
print '<a href="'.$viewUrl.'#files" class="kundenkarte-tree-file-badge kundenkarte-tree-file-images" data-anlage-id="'.$node->id.'">';
|
||||
print '<i class="fa fa-image"></i>';
|
||||
if ($node->image_count > 1) print ' '.$node->image_count;
|
||||
print '</a>';
|
||||
if (!empty($treeInfoParentheses)) {
|
||||
$infoValues = array();
|
||||
foreach ($treeInfoParentheses as $info) {
|
||||
$infoValues[] = dol_escape_htmltag($info['value']);
|
||||
}
|
||||
if ($node->doc_count > 0) {
|
||||
print '<a href="'.$viewUrl.'#files" class="kundenkarte-tree-file-badge kundenkarte-tree-file-docs" data-anlage-id="'.$node->id.'">';
|
||||
print '<i class="fa fa-file-pdf-o"></i>';
|
||||
if ($node->doc_count > 1) print ' '.$node->doc_count;
|
||||
print '</a>';
|
||||
print ' <span class="kundenkarte-tree-label-info">('.implode(', ', $infoValues).')</span>';
|
||||
}
|
||||
print '</span>';
|
||||
|
||||
// Spacer to push badges to the right
|
||||
print '<span class="kundenkarte-tree-spacer"></span>';
|
||||
|
||||
// Badges (far right)
|
||||
if (!empty($treeInfoBadges)) {
|
||||
$defaultBadgeColor = getDolGlobalString('KUNDENKARTE_TREE_BADGE_COLOR', '#2a4a5e');
|
||||
print '<span class="kundenkarte-tree-badges">';
|
||||
foreach ($treeInfoBadges as $info) {
|
||||
$badgeIcon = kundenkarte_get_field_icon($info['code'], $info['type']);
|
||||
$fieldBadgeColor = !empty($info['color']) ? $info['color'] : $defaultBadgeColor;
|
||||
print '<span class="kundenkarte-tree-badge" title="'.dol_escape_htmltag($info['label']).'" style="background:linear-gradient(135deg, '.$fieldBadgeColor.' 0%, '.kundenkarte_adjust_color($fieldBadgeColor, -20).' 100%);">';
|
||||
print '<i class="fa '.$badgeIcon.'"></i> '.dol_escape_htmltag($info['value']);
|
||||
print '</span>';
|
||||
}
|
||||
print '</span>';
|
||||
}
|
||||
print '</span>';
|
||||
|
||||
// File indicators
|
||||
if ($node->image_count > 0 || $node->doc_count > 0) {
|
||||
$totalFiles = $node->image_count + $node->doc_count;
|
||||
print '<span class="kundenkarte-tree-files">';
|
||||
print '<a href="'.$viewUrl.'#files" class="kundenkarte-tree-file-badge kundenkarte-tree-file-all" data-anlage-id="'.$node->id.'" title="'.$totalFiles.' '.($totalFiles == 1 ? 'Datei' : 'Dateien').'">';
|
||||
print '<i class="fa fa-paperclip"></i> '.$totalFiles;
|
||||
print '</a>';
|
||||
print '</span>';
|
||||
}
|
||||
|
||||
if ($node->type_short || $node->type_label) {
|
||||
$typeDisplay = $node->type_short ? $node->type_short : $node->type_label;
|
||||
|
|
@ -1405,16 +1582,18 @@ function printTreeNode($node, $socid, $systemId, $canEdit, $canDelete, $langs, $
|
|||
/**
|
||||
* Print tree options for select
|
||||
*/
|
||||
function printTreeOptions($nodes, $selected = 0, $excludeId = 0, $prefix = '')
|
||||
function printTreeOptions($nodes, $selected = 0, $excludeId = 0, $prefix = '', $level = 0)
|
||||
{
|
||||
foreach ($nodes as $node) {
|
||||
if ($node->id == $excludeId) continue;
|
||||
|
||||
$sel = ($node->id == $selected) ? ' selected' : '';
|
||||
print '<option value="'.$node->id.'"'.$sel.'>'.$prefix.dol_escape_htmltag($node->label).'</option>';
|
||||
$icon = !empty($node->type_picto) ? $node->type_picto : 'fa-cube';
|
||||
$color = !empty($node->type_color) ? $node->type_color : '#888';
|
||||
print '<option value="'.$node->id.'"'.$sel.' data-icon="'.$icon.'" data-color="'.$color.'">'.$prefix.dol_escape_htmltag($node->label).'</option>';
|
||||
|
||||
if (!empty($node->children)) {
|
||||
printTreeOptions($node->children, $selected, $excludeId, $prefix.' ');
|
||||
printTreeOptions($node->children, $selected, $excludeId, $prefix.'── ', $level + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,7 +32,11 @@ dol_include_once('/kundenkarte/lib/kundenkarte.lib.php');
|
|||
$langs->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');
|
||||
|
|
@ -50,13 +54,28 @@ $form = new Form($db);
|
|||
$anlage = new Anlage($db);
|
||||
$anlageType = new AnlageType($db);
|
||||
|
||||
// Load contact
|
||||
if ($id > 0) {
|
||||
$result = $object->fetch($id);
|
||||
if ($result <= 0) {
|
||||
dol_print_error($db, $object->error);
|
||||
exit;
|
||||
// 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
|
||||
header('Location: '.$_SERVER['PHP_SELF'].'?id='.$id.'&system='.$systemId.'&action='.$action.'&anlage_id='.$anlageId);
|
||||
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');
|
||||
|
|
@ -623,7 +642,9 @@ if (empty($customerSystems)) {
|
|||
print '<option value="">'.$langs->trans('SelectType').'</option>';
|
||||
foreach ($types as $t) {
|
||||
$selected = (($isEdit || $isCopy) && $anlage->fk_anlage_type == $t->id) ? ' selected' : '';
|
||||
print '<option value="'.$t->id.'"'.$selected.'>'.dol_escape_htmltag($t->label).'</option>';
|
||||
$icon = !empty($t->picto) ? $t->picto : 'fa-cube';
|
||||
$color = !empty($t->color) ? $t->color : '#888';
|
||||
print '<option value="'.$t->id.'"'.$selected.' data-icon="'.$icon.'" data-color="'.$color.'">'.dol_escape_htmltag($t->label).'</option>';
|
||||
}
|
||||
print '</select>';
|
||||
if (empty($types)) {
|
||||
|
|
@ -657,6 +678,49 @@ if (empty($customerSystems)) {
|
|||
print '</div>';
|
||||
|
||||
print '</form>';
|
||||
|
||||
// JavaScript: Select2 mit Icons für Type und Parent
|
||||
print '<script>
|
||||
$(document).ready(function() {
|
||||
// Select2 Template-Funktion mit Icons
|
||||
function formatTypeOption(option) {
|
||||
if (!option.id) return option.text;
|
||||
var $opt = $(option.element);
|
||||
var icon = $opt.data("icon");
|
||||
var color = $opt.data("color") || "#666";
|
||||
if (icon) {
|
||||
return $("<span><i class=\"fa " + icon + "\" style=\"color:" + color + ";width:20px;margin-right:8px;text-align:center;\"></i>" + option.text + "</span>");
|
||||
}
|
||||
return option.text;
|
||||
}
|
||||
|
||||
// Select2 für Type
|
||||
var $typeSelect = $("#select_type");
|
||||
if ($typeSelect.length) {
|
||||
$typeSelect.select2({
|
||||
templateResult: formatTypeOption,
|
||||
templateSelection: formatTypeOption,
|
||||
placeholder: "'.dol_escape_js($langs->trans('SelectType')).'",
|
||||
allowClear: true,
|
||||
width: "300px",
|
||||
dropdownAutoWidth: true
|
||||
});
|
||||
}
|
||||
|
||||
// Select2 für übergeordnetes Element
|
||||
var $parentSelect = $("select[name=\'fk_parent\']");
|
||||
if ($parentSelect.length) {
|
||||
$parentSelect.select2({
|
||||
templateResult: formatTypeOption,
|
||||
templateSelection: formatTypeOption,
|
||||
placeholder: "'.dol_escape_js($langs->trans('SelectParent')).'",
|
||||
allowClear: true,
|
||||
width: "300px",
|
||||
dropdownAutoWidth: true
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>';
|
||||
}
|
||||
|
||||
print '</div>';
|
||||
|
|
@ -760,11 +824,16 @@ function printTree($nodes, $contactid, $systemId, $canEdit, $canDelete, $langs,
|
|||
// 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) {
|
||||
$treeInfoParts[] = dol_print_date(strtotime($value), 'day');
|
||||
} else {
|
||||
$treeInfoParts[] = $value;
|
||||
$displayVal = dol_print_date(strtotime($value), 'day');
|
||||
}
|
||||
$treeInfoParts[] = array(
|
||||
'label' => $fieldDef->field_label,
|
||||
'value' => $displayVal,
|
||||
'code' => $fieldDef->field_code,
|
||||
'type' => $fieldDef->field_type
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -783,31 +852,45 @@ function printTree($nodes, $contactid, $systemId, $canEdit, $canDelete, $langs,
|
|||
$picto = $node->type_picto ? $node->type_picto : 'fa-cube';
|
||||
print '<span class="kundenkarte-tree-icon kundenkarte-tooltip-trigger" data-tooltip="'.htmlspecialchars(json_encode($tooltipData), ENT_QUOTES, 'UTF-8').'" data-anlage-id="'.$node->id.'">'.kundenkarte_render_icon($picto).'</span>';
|
||||
|
||||
// Label with manufacturer/power in parentheses + file indicators
|
||||
// Label with tree info badges + file indicators
|
||||
$viewUrl = $_SERVER['PHP_SELF'].'?id='.$contactid.'&system='.$systemId.'&action=view&anlage_id='.$node->id;
|
||||
print '<span class="kundenkarte-tree-label">'.dol_escape_htmltag($node->label);
|
||||
if (!empty($treeInfoParts)) {
|
||||
print ' <span class="kundenkarte-tree-label-info">('.dol_escape_htmltag(implode(', ', $treeInfoParts)).')</span>';
|
||||
print '<span class="kundenkarte-tree-label">'.dol_escape_htmltag($node->label).'</span>';
|
||||
|
||||
// Tree info display based on settings
|
||||
$treeInfoDisplay = getDolGlobalString('KUNDENKARTE_TREE_INFO_DISPLAY', 'badge');
|
||||
$badgeColor = getDolGlobalString('KUNDENKARTE_TREE_BADGE_COLOR', '#2a4a5e');
|
||||
|
||||
if (!empty($treeInfoParts) && $treeInfoDisplay !== 'none') {
|
||||
if ($treeInfoDisplay === 'badge') {
|
||||
// Display as badges with icons
|
||||
print '<span class="kundenkarte-tree-badges">';
|
||||
foreach ($treeInfoParts as $info) {
|
||||
$badgeIcon = kundenkarte_get_field_icon($info['code'], $info['type']);
|
||||
print '<span class="kundenkarte-tree-badge" title="'.dol_escape_htmltag($info['label']).'" style="background:linear-gradient(135deg, '.$badgeColor.' 0%, '.kundenkarte_adjust_color($badgeColor, -20).' 100%);">';
|
||||
print '<i class="fa '.$badgeIcon.'"></i> '.dol_escape_htmltag($info['value']);
|
||||
print '</span>';
|
||||
}
|
||||
print '</span>';
|
||||
} else {
|
||||
// Display in parentheses (old style)
|
||||
print '<span class="kundenkarte-tree-label-info"> (';
|
||||
$infoTexts = array();
|
||||
foreach ($treeInfoParts as $info) {
|
||||
$infoTexts[] = dol_escape_htmltag($info['label']).': '.dol_escape_htmltag($info['value']);
|
||||
}
|
||||
print implode(', ', $infoTexts);
|
||||
print ')</span>';
|
||||
}
|
||||
}
|
||||
// File indicators - directly after parentheses
|
||||
|
||||
// File indicators
|
||||
if ($node->image_count > 0 || $node->doc_count > 0) {
|
||||
$totalFiles = $node->image_count + $node->doc_count;
|
||||
print ' <span class="kundenkarte-tree-files">';
|
||||
if ($node->image_count > 0) {
|
||||
print '<a href="'.$viewUrl.'#files" class="kundenkarte-tree-file-badge kundenkarte-tree-file-images kundenkarte-images-trigger" data-anlage-id="'.$node->id.'" title="'.$node->image_count.' '.($node->image_count == 1 ? 'Bild' : 'Bilder').'">';
|
||||
print '<i class="fa fa-image"></i>';
|
||||
if ($node->image_count > 1) {
|
||||
print ' '.$node->image_count;
|
||||
}
|
||||
print '</a>';
|
||||
}
|
||||
if ($node->doc_count > 0) {
|
||||
print '<a href="'.$viewUrl.'#files" class="kundenkarte-tree-file-badge kundenkarte-tree-file-docs kundenkarte-docs-trigger" data-anlage-id="'.$node->id.'" title="'.$node->doc_count.' '.($node->doc_count == 1 ? 'Dokument' : 'Dokumente').'">';
|
||||
print '<i class="fa fa-file-pdf-o"></i>';
|
||||
if ($node->doc_count > 1) {
|
||||
print ' '.$node->doc_count;
|
||||
}
|
||||
print '</a>';
|
||||
}
|
||||
// Combined file badge with file icon
|
||||
print '<a href="'.$viewUrl.'#files" class="kundenkarte-tree-file-badge kundenkarte-tree-file-all" data-anlage-id="'.$node->id.'" title="'.$totalFiles.' '.($totalFiles == 1 ? 'Datei' : 'Dateien').'">';
|
||||
print '<i class="fa fa-paperclip"></i> '.$totalFiles;
|
||||
print '</a>';
|
||||
print '</span>';
|
||||
}
|
||||
print '</span>';
|
||||
|
|
@ -847,16 +930,18 @@ function printTree($nodes, $contactid, $systemId, $canEdit, $canDelete, $langs,
|
|||
/**
|
||||
* Print tree options for select
|
||||
*/
|
||||
function printTreeOptions($nodes, $selected = 0, $excludeId = 0, $prefix = '')
|
||||
function printTreeOptions($nodes, $selected = 0, $excludeId = 0, $prefix = '', $level = 0)
|
||||
{
|
||||
foreach ($nodes as $node) {
|
||||
if ($node->id == $excludeId) continue;
|
||||
|
||||
$sel = ($node->id == $selected) ? ' selected' : '';
|
||||
print '<option value="'.$node->id.'"'.$sel.'>'.$prefix.dol_escape_htmltag($node->label).'</option>';
|
||||
$icon = !empty($node->type_picto) ? $node->type_picto : 'fa-cube';
|
||||
$color = !empty($node->type_color) ? $node->type_color : '#888';
|
||||
print '<option value="'.$node->id.'"'.$sel.' data-icon="'.$icon.'" data-color="'.$color.'">'.$prefix.dol_escape_htmltag($node->label).'</option>';
|
||||
|
||||
if (!empty($node->children)) {
|
||||
printTreeOptions($node->children, $selected, $excludeId, $prefix.' ');
|
||||
printTreeOptions($node->children, $selected, $excludeId, $prefix.'── ', $level + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,7 +30,11 @@ dol_include_once('/kundenkarte/class/favoriteproduct.class.php');
|
|||
$langs->loadLangs(array('companies', 'products', 'orders', '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');
|
||||
|
||||
|
|
|
|||
|
|
@ -30,7 +30,11 @@ dol_include_once('/kundenkarte/class/favoriteproduct.class.php');
|
|||
$langs->loadLangs(array('companies', 'products', 'orders', 'kundenkarte@kundenkarte'));
|
||||
|
||||
// Get parameters
|
||||
// Support both 'id' and 'socid' for compatibility with Dolibarr's customer navigation arrows
|
||||
$id = GETPOSTINT('id');
|
||||
if ($id <= 0) {
|
||||
$id = GETPOSTINT('socid');
|
||||
}
|
||||
$action = GETPOST('action', 'aZ09');
|
||||
$confirm = GETPOST('confirm', 'alpha');
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue