feat(pwa): Offline-fähige Progressive Web App für Elektriker
PWA Mobile App für Schaltschrank-Dokumentation vor Ort: - Token-basierte Authentifizierung (15 Tage gültig) - Kundensuche mit Offline-Cache - Anlagen-Auswahl und Offline-Laden - Felder/Hutschienen/Automaten erfassen - Automatische Synchronisierung wenn wieder online - Installierbar auf dem Smartphone Home Screen - Touch-optimiertes Dark Mode Design - Quick-Select für Automaten-Werte (B16, C32, etc.) Schaltplan-Editor Verbesserungen: - Block Hover-Tooltip mit show_in_hover Feldern - Produktinfo mit Icon im Tooltip - Position und Breite in TE Neue Dateien: - pwa.php, pwa_auth.php - PWA Einstieg & Auth - ajax/pwa_api.php - PWA AJAX API - js/pwa.js, css/pwa.css - PWA App & Styles - sw.js, manifest.json - Service Worker & Manifest - img/pwa-icon-192.png, img/pwa-icon-512.png Version: 5.2.0 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
commit
844e6060c6
143 changed files with 59986 additions and 0 deletions
164
CLAUDE.md
Executable file
164
CLAUDE.md
Executable file
|
|
@ -0,0 +1,164 @@
|
||||||
|
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
|
||||||
|
|
||||||
|
## PWA Mobile App
|
||||||
|
|
||||||
|
### Übersicht
|
||||||
|
Offline-fähige Progressive Web App für Elektriker zur Schaltschrank-Dokumentation vor Ort.
|
||||||
|
|
||||||
|
### Dateien
|
||||||
|
- `pwa.php` - Haupteinstieg (HTML/CSS/JS Container)
|
||||||
|
- `pwa_auth.php` - Token-basierte Authentifizierung (15 Tage gültig)
|
||||||
|
- `ajax/pwa_api.php` - Alle AJAX-Endpoints für die PWA
|
||||||
|
- `js/pwa.js` - Komplette App-Logik (vanilla JS, kein jQuery)
|
||||||
|
- `css/pwa.css` - Mobile-First Dark Mode Design
|
||||||
|
- `sw.js` - Service Worker für Offline-Cache
|
||||||
|
- `manifest.json` - Web App Manifest für Installation
|
||||||
|
|
||||||
|
### Workflow
|
||||||
|
1. Login mit Dolibarr-Credentials → Token wird lokal gespeichert
|
||||||
|
2. Kunde suchen → Anlagen werden gecached
|
||||||
|
3. Anlage mit Schaltplan-Editor auswählen → Daten werden gecached
|
||||||
|
4. Offline arbeiten: Felder, Hutschienen, Automaten hinzufügen
|
||||||
|
5. Änderungen werden in lokaler Queue gespeichert
|
||||||
|
6. Bei Internetverbindung: Automatische Synchronisierung
|
||||||
|
|
||||||
|
### Token-Authentifizierung
|
||||||
|
- Tokens enthalten: user_id, login, created, expires, hash
|
||||||
|
- Hash = MD5(user_id + login + MAIN_SECURITY_SALT)
|
||||||
|
- Gültigkeit: 15 Tage
|
||||||
|
- Gespeichert in localStorage
|
||||||
|
|
||||||
|
### Offline-Sync
|
||||||
|
- Alle Änderungen werden in `offlineQueue` (localStorage) gespeichert
|
||||||
|
- Badge zeigt Anzahl ungesyncte Änderungen
|
||||||
|
- Sync-Button oder automatisch bei Online-Event
|
||||||
|
- Bei Sync werden Aktionen der Reihe nach ausgeführt
|
||||||
|
|
||||||
|
### Installation auf Smartphone
|
||||||
|
1. PWA im Browser öffnen: `https://domain/dolibarr/custom/kundenkarte/pwa.php`
|
||||||
|
2. Browser-Menü → "Zum Startbildschirm hinzufügen"
|
||||||
|
3. App öffnet sich als Standalone ohne Browser-UI
|
||||||
621
COPYING
Executable file
621
COPYING
Executable file
|
|
@ -0,0 +1,621 @@
|
||||||
|
GNU GENERAL PUBLIC LICENSE
|
||||||
|
Version 3, 29 June 2007
|
||||||
|
|
||||||
|
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
|
||||||
|
Everyone is permitted to copy and distribute verbatim copies
|
||||||
|
of this license document, but changing it is not allowed.
|
||||||
|
|
||||||
|
Preamble
|
||||||
|
|
||||||
|
The GNU General Public License is a free, copyleft license for
|
||||||
|
software and other kinds of works.
|
||||||
|
|
||||||
|
The licenses for most software and other practical works are designed
|
||||||
|
to take away your freedom to share and change the works. By contrast,
|
||||||
|
the GNU General Public License is intended to guarantee your freedom to
|
||||||
|
share and change all versions of a program--to make sure it remains free
|
||||||
|
software for all its users. We, the Free Software Foundation, use the
|
||||||
|
GNU General Public License for most of our software; it applies also to
|
||||||
|
any other work released this way by its authors. You can apply it to
|
||||||
|
your programs, too.
|
||||||
|
|
||||||
|
When we speak of free software, we are referring to freedom, not
|
||||||
|
price. Our General Public Licenses are designed to make sure that you
|
||||||
|
have the freedom to distribute copies of free software (and charge for
|
||||||
|
them if you wish), that you receive source code or can get it if you
|
||||||
|
want it, that you can change the software or use pieces of it in new
|
||||||
|
free programs, and that you know you can do these things.
|
||||||
|
|
||||||
|
To protect your rights, we need to prevent others from denying you
|
||||||
|
these rights or asking you to surrender the rights. Therefore, you have
|
||||||
|
certain responsibilities if you distribute copies of the software, or if
|
||||||
|
you modify it: responsibilities to respect the freedom of others.
|
||||||
|
|
||||||
|
For example, if you distribute copies of such a program, whether
|
||||||
|
gratis or for a fee, you must pass on to the recipients the same
|
||||||
|
freedoms that you received. You must make sure that they, too, receive
|
||||||
|
or can get the source code. And you must show them these terms so they
|
||||||
|
know their rights.
|
||||||
|
|
||||||
|
Developers that use the GNU GPL protect your rights with two steps:
|
||||||
|
(1) assert copyright on the software, and (2) offer you this License
|
||||||
|
giving you legal permission to copy, distribute and/or modify it.
|
||||||
|
|
||||||
|
For the developers' and authors' protection, the GPL clearly explains
|
||||||
|
that there is no warranty for this free software. For both users' and
|
||||||
|
authors' sake, the GPL requires that modified versions be marked as
|
||||||
|
changed, so that their problems will not be attributed erroneously to
|
||||||
|
authors of previous versions.
|
||||||
|
|
||||||
|
Some devices are designed to deny users access to install or run
|
||||||
|
modified versions of the software inside them, although the manufacturer
|
||||||
|
can do so. This is fundamentally incompatible with the aim of
|
||||||
|
protecting users' freedom to change the software. The systematic
|
||||||
|
pattern of such abuse occurs in the area of products for individuals to
|
||||||
|
use, which is precisely where it is most unacceptable. Therefore, we
|
||||||
|
have designed this version of the GPL to prohibit the practice for those
|
||||||
|
products. If such problems arise substantially in other domains, we
|
||||||
|
stand ready to extend this provision to those domains in future versions
|
||||||
|
of the GPL, as needed to protect the freedom of users.
|
||||||
|
|
||||||
|
Finally, every program is threatened constantly by software patents.
|
||||||
|
States should not allow patents to restrict development and use of
|
||||||
|
software on general-purpose computers, but in those that do, we wish to
|
||||||
|
avoid the special danger that patents applied to a free program could
|
||||||
|
make it effectively proprietary. To prevent this, the GPL assures that
|
||||||
|
patents cannot be used to render the program non-free.
|
||||||
|
|
||||||
|
The precise terms and conditions for copying, distribution and
|
||||||
|
modification follow.
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
0. Definitions.
|
||||||
|
|
||||||
|
"This License" refers to version 3 of the GNU General Public License.
|
||||||
|
|
||||||
|
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||||
|
works, such as semiconductor masks.
|
||||||
|
|
||||||
|
"The Program" refers to any copyrightable work licensed under this
|
||||||
|
License. Each licensee is addressed as "you". "Licensees" and
|
||||||
|
"recipients" may be individuals or organizations.
|
||||||
|
|
||||||
|
To "modify" a work means to copy from or adapt all or part of the work
|
||||||
|
in a fashion requiring copyright permission, other than the making of an
|
||||||
|
exact copy. The resulting work is called a "modified version" of the
|
||||||
|
earlier work or a work "based on" the earlier work.
|
||||||
|
|
||||||
|
A "covered work" means either the unmodified Program or a work based
|
||||||
|
on the Program.
|
||||||
|
|
||||||
|
To "propagate" a work means to do anything with it that, without
|
||||||
|
permission, would make you directly or secondarily liable for
|
||||||
|
infringement under applicable copyright law, except executing it on a
|
||||||
|
computer or modifying a private copy. Propagation includes copying,
|
||||||
|
distribution (with or without modification), making available to the
|
||||||
|
public, and in some countries other activities as well.
|
||||||
|
|
||||||
|
To "convey" a work means any kind of propagation that enables other
|
||||||
|
parties to make or receive copies. Mere interaction with a user through
|
||||||
|
a computer network, with no transfer of a copy, is not conveying.
|
||||||
|
|
||||||
|
An interactive user interface displays "Appropriate Legal Notices"
|
||||||
|
to the extent that it includes a convenient and prominently visible
|
||||||
|
feature that (1) displays an appropriate copyright notice, and (2)
|
||||||
|
tells the user that there is no warranty for the work (except to the
|
||||||
|
extent that warranties are provided), that licensees may convey the
|
||||||
|
work under this License, and how to view a copy of this License. If
|
||||||
|
the interface presents a list of user commands or options, such as a
|
||||||
|
menu, a prominent item in the list meets this criterion.
|
||||||
|
|
||||||
|
1. Source Code.
|
||||||
|
|
||||||
|
The "source code" for a work means the preferred form of the work
|
||||||
|
for making modifications to it. "Object code" means any non-source
|
||||||
|
form of a work.
|
||||||
|
|
||||||
|
A "Standard Interface" means an interface that either is an official
|
||||||
|
standard defined by a recognized standards body, or, in the case of
|
||||||
|
interfaces specified for a particular programming language, one that
|
||||||
|
is widely used among developers working in that language.
|
||||||
|
|
||||||
|
The "System Libraries" of an executable work include anything, other
|
||||||
|
than the work as a whole, that (a) is included in the normal form of
|
||||||
|
packaging a Major Component, but which is not part of that Major
|
||||||
|
Component, and (b) serves only to enable use of the work with that
|
||||||
|
Major Component, or to implement a Standard Interface for which an
|
||||||
|
implementation is available to the public in source code form. A
|
||||||
|
"Major Component", in this context, means a major essential component
|
||||||
|
(kernel, window system, and so on) of the specific operating system
|
||||||
|
(if any) on which the executable work runs, or a compiler used to
|
||||||
|
produce the work, or an object code interpreter used to run it.
|
||||||
|
|
||||||
|
The "Corresponding Source" for a work in object code form means all
|
||||||
|
the source code needed to generate, install, and (for an executable
|
||||||
|
work) run the object code and to modify the work, including scripts to
|
||||||
|
control those activities. However, it does not include the work's
|
||||||
|
System Libraries, or general-purpose tools or generally available free
|
||||||
|
programs which are used unmodified in performing those activities but
|
||||||
|
which are not part of the work. For example, Corresponding Source
|
||||||
|
includes interface definition files associated with source files for
|
||||||
|
the work, and the source code for shared libraries and dynamically
|
||||||
|
linked subprograms that the work is specifically designed to require,
|
||||||
|
such as by intimate data communication or control flow between those
|
||||||
|
subprograms and other parts of the work.
|
||||||
|
|
||||||
|
The Corresponding Source need not include anything that users
|
||||||
|
can regenerate automatically from other parts of the Corresponding
|
||||||
|
Source.
|
||||||
|
|
||||||
|
The Corresponding Source for a work in source code form is that
|
||||||
|
same work.
|
||||||
|
|
||||||
|
2. Basic Permissions.
|
||||||
|
|
||||||
|
All rights granted under this License are granted for the term of
|
||||||
|
copyright on the Program, and are irrevocable provided the stated
|
||||||
|
conditions are met. This License explicitly affirms your unlimited
|
||||||
|
permission to run the unmodified Program. The output from running a
|
||||||
|
covered work is covered by this License only if the output, given its
|
||||||
|
content, constitutes a covered work. This License acknowledges your
|
||||||
|
rights of fair use or other equivalent, as provided by copyright law.
|
||||||
|
|
||||||
|
You may make, run and propagate covered works that you do not
|
||||||
|
convey, without conditions so long as your license otherwise remains
|
||||||
|
in force. You may convey covered works to others for the sole purpose
|
||||||
|
of having them make modifications exclusively for you, or provide you
|
||||||
|
with facilities for running those works, provided that you comply with
|
||||||
|
the terms of this License in conveying all material for which you do
|
||||||
|
not control copyright. Those thus making or running the covered works
|
||||||
|
for you must do so exclusively on your behalf, under your direction
|
||||||
|
and control, on terms that prohibit them from making any copies of
|
||||||
|
your copyrighted material outside their relationship with you.
|
||||||
|
|
||||||
|
Conveying under any other circumstances is permitted solely under
|
||||||
|
the conditions stated below. Sublicensing is not allowed; section 10
|
||||||
|
makes it unnecessary.
|
||||||
|
|
||||||
|
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||||
|
|
||||||
|
No covered work shall be deemed part of an effective technological
|
||||||
|
measure under any applicable law fulfilling obligations under article
|
||||||
|
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||||
|
similar laws prohibiting or restricting circumvention of such
|
||||||
|
measures.
|
||||||
|
|
||||||
|
When you convey a covered work, you waive any legal power to forbid
|
||||||
|
circumvention of technological measures to the extent such circumvention
|
||||||
|
is effected by exercising rights under this License with respect to
|
||||||
|
the covered work, and you disclaim any intention to limit operation or
|
||||||
|
modification of the work as a means of enforcing, against the work's
|
||||||
|
users, your or third parties' legal rights to forbid circumvention of
|
||||||
|
technological measures.
|
||||||
|
|
||||||
|
4. Conveying Verbatim Copies.
|
||||||
|
|
||||||
|
You may convey verbatim copies of the Program's source code as you
|
||||||
|
receive it, in any medium, provided that you conspicuously and
|
||||||
|
appropriately publish on each copy an appropriate copyright notice;
|
||||||
|
keep intact all notices stating that this License and any
|
||||||
|
non-permissive terms added in accord with section 7 apply to the code;
|
||||||
|
keep intact all notices of the absence of any warranty; and give all
|
||||||
|
recipients a copy of this License along with the Program.
|
||||||
|
|
||||||
|
You may charge any price or no price for each copy that you convey,
|
||||||
|
and you may offer support or warranty protection for a fee.
|
||||||
|
|
||||||
|
5. Conveying Modified Source Versions.
|
||||||
|
|
||||||
|
You may convey a work based on the Program, or the modifications to
|
||||||
|
produce it from the Program, in the form of source code under the
|
||||||
|
terms of section 4, provided that you also meet all of these conditions:
|
||||||
|
|
||||||
|
a) The work must carry prominent notices stating that you modified
|
||||||
|
it, and giving a relevant date.
|
||||||
|
|
||||||
|
b) The work must carry prominent notices stating that it is
|
||||||
|
released under this License and any conditions added under section
|
||||||
|
7. This requirement modifies the requirement in section 4 to
|
||||||
|
"keep intact all notices".
|
||||||
|
|
||||||
|
c) You must license the entire work, as a whole, under this
|
||||||
|
License to anyone who comes into possession of a copy. This
|
||||||
|
License will therefore apply, along with any applicable section 7
|
||||||
|
additional terms, to the whole of the work, and all its parts,
|
||||||
|
regardless of how they are packaged. This License gives no
|
||||||
|
permission to license the work in any other way, but it does not
|
||||||
|
invalidate such permission if you have separately received it.
|
||||||
|
|
||||||
|
d) If the work has interactive user interfaces, each must display
|
||||||
|
Appropriate Legal Notices; however, if the Program has interactive
|
||||||
|
interfaces that do not display Appropriate Legal Notices, your
|
||||||
|
work need not make them do so.
|
||||||
|
|
||||||
|
A compilation of a covered work with other separate and independent
|
||||||
|
works, which are not by their nature extensions of the covered work,
|
||||||
|
and which are not combined with it such as to form a larger program,
|
||||||
|
in or on a volume of a storage or distribution medium, is called an
|
||||||
|
"aggregate" if the compilation and its resulting copyright are not
|
||||||
|
used to limit the access or legal rights of the compilation's users
|
||||||
|
beyond what the individual works permit. Inclusion of a covered work
|
||||||
|
in an aggregate does not cause this License to apply to the other
|
||||||
|
parts of the aggregate.
|
||||||
|
|
||||||
|
6. Conveying Non-Source Forms.
|
||||||
|
|
||||||
|
You may convey a covered work in object code form under the terms
|
||||||
|
of sections 4 and 5, provided that you also convey the
|
||||||
|
machine-readable Corresponding Source under the terms of this License,
|
||||||
|
in one of these ways:
|
||||||
|
|
||||||
|
a) Convey the object code in, or embodied in, a physical product
|
||||||
|
(including a physical distribution medium), accompanied by the
|
||||||
|
Corresponding Source fixed on a durable physical medium
|
||||||
|
customarily used for software interchange.
|
||||||
|
|
||||||
|
b) Convey the object code in, or embodied in, a physical product
|
||||||
|
(including a physical distribution medium), accompanied by a
|
||||||
|
written offer, valid for at least three years and valid for as
|
||||||
|
long as you offer spare parts or customer support for that product
|
||||||
|
model, to give anyone who possesses the object code either (1) a
|
||||||
|
copy of the Corresponding Source for all the software in the
|
||||||
|
product that is covered by this License, on a durable physical
|
||||||
|
medium customarily used for software interchange, for a price no
|
||||||
|
more than your reasonable cost of physically performing this
|
||||||
|
conveying of source, or (2) access to copy the
|
||||||
|
Corresponding Source from a network server at no charge.
|
||||||
|
|
||||||
|
c) Convey individual copies of the object code with a copy of the
|
||||||
|
written offer to provide the Corresponding Source. This
|
||||||
|
alternative is allowed only occasionally and noncommercially, and
|
||||||
|
only if you received the object code with such an offer, in accord
|
||||||
|
with subsection 6b.
|
||||||
|
|
||||||
|
d) Convey the object code by offering access from a designated
|
||||||
|
place (gratis or for a charge), and offer equivalent access to the
|
||||||
|
Corresponding Source in the same way through the same place at no
|
||||||
|
further charge. You need not require recipients to copy the
|
||||||
|
Corresponding Source along with the object code. If the place to
|
||||||
|
copy the object code is a network server, the Corresponding Source
|
||||||
|
may be on a different server (operated by you or a third party)
|
||||||
|
that supports equivalent copying facilities, provided you maintain
|
||||||
|
clear directions next to the object code saying where to find the
|
||||||
|
Corresponding Source. Regardless of what server hosts the
|
||||||
|
Corresponding Source, you remain obligated to ensure that it is
|
||||||
|
available for as long as needed to satisfy these requirements.
|
||||||
|
|
||||||
|
e) Convey the object code using peer-to-peer transmission, provided
|
||||||
|
you inform other peers where the object code and Corresponding
|
||||||
|
Source of the work are being offered to the general public at no
|
||||||
|
charge under subsection 6d.
|
||||||
|
|
||||||
|
A separable portion of the object code, whose source code is excluded
|
||||||
|
from the Corresponding Source as a System Library, need not be
|
||||||
|
included in conveying the object code work.
|
||||||
|
|
||||||
|
A "User Product" is either (1) a "consumer product", which means any
|
||||||
|
tangible personal property which is normally used for personal, family,
|
||||||
|
or household purposes, or (2) anything designed or sold for incorporation
|
||||||
|
into a dwelling. In determining whether a product is a consumer product,
|
||||||
|
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||||
|
product received by a particular user, "normally used" refers to a
|
||||||
|
typical or common use of that class of product, regardless of the status
|
||||||
|
of the particular user or of the way in which the particular user
|
||||||
|
actually uses, or expects or is expected to use, the product. A product
|
||||||
|
is a consumer product regardless of whether the product has substantial
|
||||||
|
commercial, industrial or non-consumer uses, unless such uses represent
|
||||||
|
the only significant mode of use of the product.
|
||||||
|
|
||||||
|
"Installation Information" for a User Product means any methods,
|
||||||
|
procedures, authorization keys, or other information required to install
|
||||||
|
and execute modified versions of a covered work in that User Product from
|
||||||
|
a modified version of its Corresponding Source. The information must
|
||||||
|
suffice to ensure that the continued functioning of the modified object
|
||||||
|
code is in no case prevented or interfered with solely because
|
||||||
|
modification has been made.
|
||||||
|
|
||||||
|
If you convey an object code work under this section in, or with, or
|
||||||
|
specifically for use in, a User Product, and the conveying occurs as
|
||||||
|
part of a transaction in which the right of possession and use of the
|
||||||
|
User Product is transferred to the recipient in perpetuity or for a
|
||||||
|
fixed term (regardless of how the transaction is characterized), the
|
||||||
|
Corresponding Source conveyed under this section must be accompanied
|
||||||
|
by the Installation Information. But this requirement does not apply
|
||||||
|
if neither you nor any third party retains the ability to install
|
||||||
|
modified object code on the User Product (for example, the work has
|
||||||
|
been installed in ROM).
|
||||||
|
|
||||||
|
The requirement to provide Installation Information does not include a
|
||||||
|
requirement to continue to provide support service, warranty, or updates
|
||||||
|
for a work that has been modified or installed by the recipient, or for
|
||||||
|
the User Product in which it has been modified or installed. Access to a
|
||||||
|
network may be denied when the modification itself materially and
|
||||||
|
adversely affects the operation of the network or violates the rules and
|
||||||
|
protocols for communication across the network.
|
||||||
|
|
||||||
|
Corresponding Source conveyed, and Installation Information provided,
|
||||||
|
in accord with this section must be in a format that is publicly
|
||||||
|
documented (and with an implementation available to the public in
|
||||||
|
source code form), and must require no special password or key for
|
||||||
|
unpacking, reading or copying.
|
||||||
|
|
||||||
|
7. Additional Terms.
|
||||||
|
|
||||||
|
"Additional permissions" are terms that supplement the terms of this
|
||||||
|
License by making exceptions from one or more of its conditions.
|
||||||
|
Additional permissions that are applicable to the entire Program shall
|
||||||
|
be treated as though they were included in this License, to the extent
|
||||||
|
that they are valid under applicable law. If additional permissions
|
||||||
|
apply only to part of the Program, that part may be used separately
|
||||||
|
under those permissions, but the entire Program remains governed by
|
||||||
|
this License without regard to the additional permissions.
|
||||||
|
|
||||||
|
When you convey a copy of a covered work, you may at your option
|
||||||
|
remove any additional permissions from that copy, or from any part of
|
||||||
|
it. (Additional permissions may be written to require their own
|
||||||
|
removal in certain cases when you modify the work.) You may place
|
||||||
|
additional permissions on material, added by you to a covered work,
|
||||||
|
for which you have or can give appropriate copyright permission.
|
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, for material you
|
||||||
|
add to a covered work, you may (if authorized by the copyright holders of
|
||||||
|
that material) supplement the terms of this License with terms:
|
||||||
|
|
||||||
|
a) Disclaiming warranty or limiting liability differently from the
|
||||||
|
terms of sections 15 and 16 of this License; or
|
||||||
|
|
||||||
|
b) Requiring preservation of specified reasonable legal notices or
|
||||||
|
author attributions in that material or in the Appropriate Legal
|
||||||
|
Notices displayed by works containing it; or
|
||||||
|
|
||||||
|
c) Prohibiting misrepresentation of the origin of that material, or
|
||||||
|
requiring that modified versions of such material be marked in
|
||||||
|
reasonable ways as different from the original version; or
|
||||||
|
|
||||||
|
d) Limiting the use for publicity purposes of names of licensors or
|
||||||
|
authors of the material; or
|
||||||
|
|
||||||
|
e) Declining to grant rights under trademark law for use of some
|
||||||
|
trade names, trademarks, or service marks; or
|
||||||
|
|
||||||
|
f) Requiring indemnification of licensors and authors of that
|
||||||
|
material by anyone who conveys the material (or modified versions of
|
||||||
|
it) with contractual assumptions of liability to the recipient, for
|
||||||
|
any liability that these contractual assumptions directly impose on
|
||||||
|
those licensors and authors.
|
||||||
|
|
||||||
|
All other non-permissive additional terms are considered "further
|
||||||
|
restrictions" within the meaning of section 10. If the Program as you
|
||||||
|
received it, or any part of it, contains a notice stating that it is
|
||||||
|
governed by this License along with a term that is a further
|
||||||
|
restriction, you may remove that term. If a license document contains
|
||||||
|
a further restriction but permits relicensing or conveying under this
|
||||||
|
License, you may add to a covered work material governed by the terms
|
||||||
|
of that license document, provided that the further restriction does
|
||||||
|
not survive such relicensing or conveying.
|
||||||
|
|
||||||
|
If you add terms to a covered work in accord with this section, you
|
||||||
|
must place, in the relevant source files, a statement of the
|
||||||
|
additional terms that apply to those files, or a notice indicating
|
||||||
|
where to find the applicable terms.
|
||||||
|
|
||||||
|
Additional terms, permissive or non-permissive, may be stated in the
|
||||||
|
form of a separately written license, or stated as exceptions;
|
||||||
|
the above requirements apply either way.
|
||||||
|
|
||||||
|
8. Termination.
|
||||||
|
|
||||||
|
You may not propagate or modify a covered work except as expressly
|
||||||
|
provided under this License. Any attempt otherwise to propagate or
|
||||||
|
modify it is void, and will automatically terminate your rights under
|
||||||
|
this License (including any patent licenses granted under the third
|
||||||
|
paragraph of section 11).
|
||||||
|
|
||||||
|
However, if you cease all violation of this License, then your
|
||||||
|
license from a particular copyright holder is reinstated (a)
|
||||||
|
provisionally, unless and until the copyright holder explicitly and
|
||||||
|
finally terminates your license, and (b) permanently, if the copyright
|
||||||
|
holder fails to notify you of the violation by some reasonable means
|
||||||
|
prior to 60 days after the cessation.
|
||||||
|
|
||||||
|
Moreover, your license from a particular copyright holder is
|
||||||
|
reinstated permanently if the copyright holder notifies you of the
|
||||||
|
violation by some reasonable means, this is the first time you have
|
||||||
|
received notice of violation of this License (for any work) from that
|
||||||
|
copyright holder, and you cure the violation prior to 30 days after
|
||||||
|
your receipt of the notice.
|
||||||
|
|
||||||
|
Termination of your rights under this section does not terminate the
|
||||||
|
licenses of parties who have received copies or rights from you under
|
||||||
|
this License. If your rights have been terminated and not permanently
|
||||||
|
reinstated, you do not qualify to receive new licenses for the same
|
||||||
|
material under section 10.
|
||||||
|
|
||||||
|
9. Acceptance Not Required for Having Copies.
|
||||||
|
|
||||||
|
You are not required to accept this License in order to receive or
|
||||||
|
run a copy of the Program. Ancillary propagation of a covered work
|
||||||
|
occurring solely as a consequence of using peer-to-peer transmission
|
||||||
|
to receive a copy likewise does not require acceptance. However,
|
||||||
|
nothing other than this License grants you permission to propagate or
|
||||||
|
modify any covered work. These actions infringe copyright if you do
|
||||||
|
not accept this License. Therefore, by modifying or propagating a
|
||||||
|
covered work, you indicate your acceptance of this License to do so.
|
||||||
|
|
||||||
|
10. Automatic Licensing of Downstream Recipients.
|
||||||
|
|
||||||
|
Each time you convey a covered work, the recipient automatically
|
||||||
|
receives a license from the original licensors, to run, modify and
|
||||||
|
propagate that work, subject to this License. You are not responsible
|
||||||
|
for enforcing compliance by third parties with this License.
|
||||||
|
|
||||||
|
An "entity transaction" is a transaction transferring control of an
|
||||||
|
organization, or substantially all assets of one, or subdividing an
|
||||||
|
organization, or merging organizations. If propagation of a covered
|
||||||
|
work results from an entity transaction, each party to that
|
||||||
|
transaction who receives a copy of the work also receives whatever
|
||||||
|
licenses to the work the party's predecessor in interest had or could
|
||||||
|
give under the previous paragraph, plus a right to possession of the
|
||||||
|
Corresponding Source of the work from the predecessor in interest, if
|
||||||
|
the predecessor has it or can get it with reasonable efforts.
|
||||||
|
|
||||||
|
You may not impose any further restrictions on the exercise of the
|
||||||
|
rights granted or affirmed under this License. For example, you may
|
||||||
|
not impose a license fee, royalty, or other charge for exercise of
|
||||||
|
rights granted under this License, and you may not initiate litigation
|
||||||
|
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||||
|
any patent claim is infringed by making, using, selling, offering for
|
||||||
|
sale, or importing the Program or any portion of it.
|
||||||
|
|
||||||
|
11. Patents.
|
||||||
|
|
||||||
|
A "contributor" is a copyright holder who authorizes use under this
|
||||||
|
License of the Program or a work on which the Program is based. The
|
||||||
|
work thus licensed is called the contributor's "contributor version".
|
||||||
|
|
||||||
|
A contributor's "essential patent claims" are all patent claims
|
||||||
|
owned or controlled by the contributor, whether already acquired or
|
||||||
|
hereafter acquired, that would be infringed by some manner, permitted
|
||||||
|
by this License, of making, using, or selling its contributor version,
|
||||||
|
but do not include claims that would be infringed only as a
|
||||||
|
consequence of further modification of the contributor version. For
|
||||||
|
purposes of this definition, "control" includes the right to grant
|
||||||
|
patent sublicenses in a manner consistent with the requirements of
|
||||||
|
this License.
|
||||||
|
|
||||||
|
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||||
|
patent license under the contributor's essential patent claims, to
|
||||||
|
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||||
|
propagate the contents of its contributor version.
|
||||||
|
|
||||||
|
In the following three paragraphs, a "patent license" is any express
|
||||||
|
agreement or commitment, however denominated, not to enforce a patent
|
||||||
|
(such as an express permission to practice a patent or covenant not to
|
||||||
|
sue for patent infringement). To "grant" such a patent license to a
|
||||||
|
party means to make such an agreement or commitment not to enforce a
|
||||||
|
patent against the party.
|
||||||
|
|
||||||
|
If you convey a covered work, knowingly relying on a patent license,
|
||||||
|
and the Corresponding Source of the work is not available for anyone
|
||||||
|
to copy, free of charge and under the terms of this License, through a
|
||||||
|
publicly available network server or other readily accessible means,
|
||||||
|
then you must either (1) cause the Corresponding Source to be so
|
||||||
|
available, or (2) arrange to deprive yourself of the benefit of the
|
||||||
|
patent license for this particular work, or (3) arrange, in a manner
|
||||||
|
consistent with the requirements of this License, to extend the patent
|
||||||
|
license to downstream recipients. "Knowingly relying" means you have
|
||||||
|
actual knowledge that, but for the patent license, your conveying the
|
||||||
|
covered work in a country, or your recipient's use of the covered work
|
||||||
|
in a country, would infringe one or more identifiable patents in that
|
||||||
|
country that you have reason to believe are valid.
|
||||||
|
|
||||||
|
If, pursuant to or in connection with a single transaction or
|
||||||
|
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||||
|
covered work, and grant a patent license to some of the parties
|
||||||
|
receiving the covered work authorizing them to use, propagate, modify
|
||||||
|
or convey a specific copy of the covered work, then the patent license
|
||||||
|
you grant is automatically extended to all recipients of the covered
|
||||||
|
work and works based on it.
|
||||||
|
|
||||||
|
A patent license is "discriminatory" if it does not include within
|
||||||
|
the scope of its coverage, prohibits the exercise of, or is
|
||||||
|
conditioned on the non-exercise of one or more of the rights that are
|
||||||
|
specifically granted under this License. You may not convey a covered
|
||||||
|
work if you are a party to an arrangement with a third party that is
|
||||||
|
in the business of distributing software, under which you make payment
|
||||||
|
to the third party based on the extent of your activity of conveying
|
||||||
|
the work, and under which the third party grants, to any of the
|
||||||
|
parties who would receive the covered work from you, a discriminatory
|
||||||
|
patent license (a) in connection with copies of the covered work
|
||||||
|
conveyed by you (or copies made from those copies), or (b) primarily
|
||||||
|
for and in connection with specific products or compilations that
|
||||||
|
contain the covered work, unless you entered into that arrangement,
|
||||||
|
or that patent license was granted, prior to 28 March 2007.
|
||||||
|
|
||||||
|
Nothing in this License shall be construed as excluding or limiting
|
||||||
|
any implied license or other defenses to infringement that may
|
||||||
|
otherwise be available to you under applicable patent law.
|
||||||
|
|
||||||
|
12. No Surrender of Others' Freedom.
|
||||||
|
|
||||||
|
If conditions are imposed on you (whether by court order, agreement or
|
||||||
|
otherwise) that contradict the conditions of this License, they do not
|
||||||
|
excuse you from the conditions of this License. If you cannot convey a
|
||||||
|
covered work so as to satisfy simultaneously your obligations under this
|
||||||
|
License and any other pertinent obligations, then as a consequence you may
|
||||||
|
not convey it at all. For example, if you agree to terms that obligate you
|
||||||
|
to collect a royalty for further conveying from those to whom you convey
|
||||||
|
the Program, the only way you could satisfy both those terms and this
|
||||||
|
License would be to refrain entirely from conveying the Program.
|
||||||
|
|
||||||
|
13. Use with the GNU Affero General Public License.
|
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, you have
|
||||||
|
permission to link or combine any covered work with a work licensed
|
||||||
|
under version 3 of the GNU Affero General Public License into a single
|
||||||
|
combined work, and to convey the resulting work. The terms of this
|
||||||
|
License will continue to apply to the part which is the covered work,
|
||||||
|
but the special requirements of the GNU Affero General Public License,
|
||||||
|
section 13, concerning interaction through a network will apply to the
|
||||||
|
combination as such.
|
||||||
|
|
||||||
|
14. Revised Versions of this License.
|
||||||
|
|
||||||
|
The Free Software Foundation may publish revised and/or new versions of
|
||||||
|
the GNU General Public License from time to time. Such new versions will
|
||||||
|
be similar in spirit to the present version, but may differ in detail to
|
||||||
|
address new problems or concerns.
|
||||||
|
|
||||||
|
Each version is given a distinguishing version number. If the
|
||||||
|
Program specifies that a certain numbered version of the GNU General
|
||||||
|
Public License "or any later version" applies to it, you have the
|
||||||
|
option of following the terms and conditions either of that numbered
|
||||||
|
version or of any later version published by the Free Software
|
||||||
|
Foundation. If the Program does not specify a version number of the
|
||||||
|
GNU General Public License, you may choose any version ever published
|
||||||
|
by the Free Software Foundation.
|
||||||
|
|
||||||
|
If the Program specifies that a proxy can decide which future
|
||||||
|
versions of the GNU General Public License can be used, that proxy's
|
||||||
|
public statement of acceptance of a version permanently authorizes you
|
||||||
|
to choose that version for the Program.
|
||||||
|
|
||||||
|
Later license versions may give you additional or different
|
||||||
|
permissions. However, no additional obligations are imposed on any
|
||||||
|
author or copyright holder as a result of your choosing to follow a
|
||||||
|
later version.
|
||||||
|
|
||||||
|
15. Disclaimer of Warranty.
|
||||||
|
|
||||||
|
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||||
|
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||||
|
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||||
|
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||||
|
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||||
|
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||||
|
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||||
|
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||||
|
|
||||||
|
16. Limitation of Liability.
|
||||||
|
|
||||||
|
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||||
|
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||||
|
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||||
|
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||||
|
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||||
|
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||||
|
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||||
|
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||||
|
SUCH DAMAGES.
|
||||||
|
|
||||||
|
17. Interpretation of Sections 15 and 16.
|
||||||
|
|
||||||
|
If the disclaimer of warranty and limitation of liability provided
|
||||||
|
above cannot be given local legal effect according to their terms,
|
||||||
|
reviewing courts shall apply local law that most closely approximates
|
||||||
|
an absolute waiver of all civil liability in connection with the
|
||||||
|
Program, unless a warranty or assumption of liability accompanies a
|
||||||
|
copy of the Program in return for a fee.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
244
ChangeLog.md
Executable file
244
ChangeLog.md
Executable file
|
|
@ -0,0 +1,244 @@
|
||||||
|
# CHANGELOG MODULE KUNDENKARTE FOR [DOLIBARR ERP CRM](https://www.dolibarr.org)
|
||||||
|
|
||||||
|
## 5.2.0 (2026-02)
|
||||||
|
|
||||||
|
### Neue Features
|
||||||
|
|
||||||
|
- **PWA Mobile App**: Offline-faehige Progressive Web App fuer Elektriker
|
||||||
|
- Schaltschrank-Dokumentation direkt vor Ort im Keller (ohne Internet)
|
||||||
|
- Kundensuche, Anlagen-Auswahl, Felder/Hutschienen/Automaten erfassen
|
||||||
|
- Token-basierte Authentifizierung (15 Tage gueltig)
|
||||||
|
- Automatische Synchronisierung wenn wieder online
|
||||||
|
- Installierbar auf dem Smartphone Home Screen
|
||||||
|
- Touch-optimiertes Dark Mode Design
|
||||||
|
- Quick-Select fuer Automaten-Werte (B16, C32, etc.)
|
||||||
|
|
||||||
|
- **Schaltplan-Editor: Block Hover-Tooltip**
|
||||||
|
- Zeigt alle Felder mit `show_in_hover=1` Einstellung
|
||||||
|
- Produktinfo mit Icon wenn Produkt zugewiesen
|
||||||
|
- Position und Breite in TE
|
||||||
|
- Schickes dunkles Design
|
||||||
|
|
||||||
|
- **Schaltplan-Editor: Produkt-Zuordnung**
|
||||||
|
- Autocomplete-Suche fuer Produkte beim Hinzufuegen/Bearbeiten
|
||||||
|
- Produktinfo im Klick-Popup angezeigt
|
||||||
|
|
||||||
|
### Verbesserungen
|
||||||
|
|
||||||
|
- Equipment-Popup zeigt jetzt Produktinfo mit Cube-Icon
|
||||||
|
- Loeschbestaetigung fuer Panels und Hutschienen mit Inhalt
|
||||||
|
|
||||||
|
### Neue Dateien
|
||||||
|
- `pwa.php` - PWA Haupteinstieg
|
||||||
|
- `pwa_auth.php` - Token-basierte Authentifizierung
|
||||||
|
- `ajax/pwa_api.php` - PWA AJAX API
|
||||||
|
- `js/pwa.js` - PWA JavaScript App
|
||||||
|
- `css/pwa.css` - PWA Mobile Styles
|
||||||
|
- `sw.js` - Service Worker (Offline-Cache)
|
||||||
|
- `manifest.json` - PWA Web App Manifest
|
||||||
|
- `img/pwa-icon-192.png`, `img/pwa-icon-512.png` - App Icons
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5.1.0 (2026-02)
|
||||||
|
|
||||||
|
### Verbesserungen
|
||||||
|
- **Graph-Ansicht: Intelligente Feldanzeige**
|
||||||
|
- Felder nach `position` sortiert (nicht mehr nach JSON-Reihenfolge)
|
||||||
|
- Nur Felder mit `show_in_tree=1` werden auf den Graph-Nodes angezeigt
|
||||||
|
- Nur Felder mit `show_in_hover=1` erscheinen im Tooltip
|
||||||
|
- Badge-Werte im Graph mit Feldbezeichnung (z.B. "Hersteller: ABB")
|
||||||
|
- Tooltip: Typ/System entfernt (redundant mit Graph-Node)
|
||||||
|
- Tooltip: Farbige Badge-Kaesten wie in der Baumansicht
|
||||||
|
- Shared Library `lib/graph_view.lib.php` fuer Toolbar/Container/Legende
|
||||||
|
|
||||||
|
### Neue Dateien
|
||||||
|
- `lib/graph_view.lib.php` - Gemeinsame Graph-Funktionen (Toolbar, Container, Legende)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5.0.0 (2026-02)
|
||||||
|
|
||||||
|
### Neue Features
|
||||||
|
- **Cytoscape.js Graph-Ansicht**: Neue interaktive Netzwerk-Visualisierung
|
||||||
|
- Raeume als Compound-Container, Geraete als Nodes darin
|
||||||
|
- Kabelverbindungen als sichtbare Edges (auch raumuebergreifend)
|
||||||
|
- Durchgeschleifte Leitungen als gestrichelte Linien
|
||||||
|
- Dagre-Layout: Hierarchischer Stromfluss top-down
|
||||||
|
- Zoom/Pan/Fit-Controls, Mausrad-Zoom Toggle
|
||||||
|
- Kabeltyp-Legende mit Farben
|
||||||
|
- **Bearbeitungsmodus**: Nodes nur per "Anordnen"-Button verschiebbar,
|
||||||
|
Positionen per "Speichern"-Button fest, "Abbrechen" setzt zurueck
|
||||||
|
- Viewport-Persistenz (Zoom/Pan bleibt beim Seitenwechsel)
|
||||||
|
- Klick auf Node/Edge oeffnet Detail-/Bearbeitungsseite
|
||||||
|
- Suche als Overlay im Graph-Container (Nodes hervorheben/abdunkeln)
|
||||||
|
- Kontextmenue (Rechtsklick): Ansehen, Bearbeiten, Kopieren, Loeschen
|
||||||
|
- PNG-Export des Graphen
|
||||||
|
- Admin-Setting: Ansichtsmodus (Baum/Graph) in Setup waehlbar
|
||||||
|
- Toolbar zweizeilig: Aktionen oben, Graph-Steuerung unten
|
||||||
|
|
||||||
|
- **Verbindungsformular verbessert**
|
||||||
|
- Select-Dropdowns zeigen nur Geraete (keine Gebaeude/Raeume)
|
||||||
|
- Icons (FontAwesome) in Select-Optionen via select2
|
||||||
|
- Gebaeude-Pfad als Kontext (z.B. "EG > Zahlerschrank")
|
||||||
|
- Systemuebergreifende Geraete-Auswahl (kein Systemfilter)
|
||||||
|
|
||||||
|
### Neue Dateien
|
||||||
|
- `js/kundenkarte_cytoscape.js` - Graph-Namespace (~750 Zeilen)
|
||||||
|
- `css/kundenkarte_cytoscape.css` - Graph-Styles mit Dark Mode
|
||||||
|
- `ajax/graph_data.php` - AJAX: Baum+Verbindungen → Cytoscape-Format
|
||||||
|
- `ajax/graph_save_positions.php` - AJAX: Node-Positionen speichern
|
||||||
|
- `js/cytoscape.min.js`, `js/dagre.min.js`, `js/cytoscape-dagre.js`, `js/cytoscape-cose-bilkent.js` - Bibliotheken
|
||||||
|
|
||||||
|
### Bugfixes
|
||||||
|
- **Contact-Filter im Graph**: Graph zeigte faelschlicherweise Kontakt-Elemente auf Kunden-Ebene
|
||||||
|
- Fix: `fk_contact` Filter in `graph_data.php` analog zum Baum
|
||||||
|
- **Verbindung hinzufuegen**: Formular zeigte "Feld erforderlich"-Fehler beim Oeffnen
|
||||||
|
- Ursache: `action=create` in URL triggerte Handler vor Formular-Anzeige
|
||||||
|
- Fix: Korrekte Dolibarr-Konvention (create=Formular, add=Verarbeitung)
|
||||||
|
- **Leere Dropdowns**: Quelle/Ziel-Auswahl war leer wenn System keine Elemente hatte
|
||||||
|
- Fix: Kein System-Filter mehr (Kabel koennen systemuebergreifend sein)
|
||||||
|
- **Kontakt-Redirect**: Nach Verbindung-Bearbeiten landete man auf Kundenansicht statt Kontaktansicht
|
||||||
|
- Fix: `contactid` wird jetzt in allen Edit-URLs mitgegeben
|
||||||
|
- **Kontakt-Anlagen**: Auf Stand von Kunden-Anlagen gebracht
|
||||||
|
- tree_display_mode, badge_color, Schaltplan-Editor, Drag&Drop Upload
|
||||||
|
|
||||||
|
### Datenbank-Aenderungen
|
||||||
|
- Neue Spalten `graph_x`, `graph_y` in `llx_kundenkarte_anlage` (Node-Positionen)
|
||||||
|
- Neue Spalte `fk_building_node` in `llx_kundenkarte_anlage` (vorbereitet fuer Phase 2)
|
||||||
|
|
||||||
|
### Admin-Settings
|
||||||
|
- `KUNDENKARTE_DEFAULT_VIEW`: `tree` (Standard) oder `graph`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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
|
||||||
|
|
||||||
|
- **Kontakt-Anlagen Kategorie-Filter**: Fehlende Kategorie-Auswahl hinzugefuegt
|
||||||
|
- Gebaeude/Standort vs. Technisches Element jetzt auch bei Kontakten waehlbar
|
||||||
|
- Strom-Typen sind jetzt auswaehlbar
|
||||||
|
|
||||||
|
- **Schematic Editor**: Hutschiene hinzufuegen Button bei leerem Panel
|
||||||
|
- Button erscheint jetzt auch wenn noch keine Hutschiene existiert
|
||||||
|
|
||||||
|
- **Panel-Hoehe**: Korrektur der Berechnung bei mehreren Hutschienen
|
||||||
|
- Unterer Rand wird nicht mehr abgeschnitten
|
||||||
|
|
||||||
|
### Datenbank-Aenderungen
|
||||||
|
- Neue Spalte `badge_color` in `llx_kundenkarte_anlage_type_field`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3.5.0 (2026-02)
|
||||||
|
|
||||||
|
### Neue Features
|
||||||
|
- **Drag & Drop Sortierung**: Elemente im Anlagenbaum per Drag & Drop umsortieren
|
||||||
|
- Geschwister-Elemente auf gleicher Ebene verschieben
|
||||||
|
- Visuelle Drop-Indikatoren (blaue Linie)
|
||||||
|
- Reihenfolge wird sofort per AJAX gespeichert (kein Seitenreload)
|
||||||
|
- Funktioniert in Kunden- und Kontakt-Anlagen
|
||||||
|
|
||||||
|
### Bugfixes
|
||||||
|
- **Duplicate-Key-Fehler behoben**: UNIQUE KEY `uk_kundenkarte_societe_system` um `fk_contact` erweitert
|
||||||
|
- Systeme koennen nun gleichzeitig auf Kunden- und Kontaktebene existieren
|
||||||
|
- Migration wird automatisch beim Modul-Aktivieren ausgefuehrt
|
||||||
|
|
||||||
|
### Verbesserungen
|
||||||
|
- Visueller Abstand zwischen Root-Elementen im Anlagenbaum
|
||||||
|
- INSERT fuer Kunden-Systeme setzt explizit `fk_contact = 0`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2.0 (2026-01)
|
||||||
|
|
||||||
|
### Neue Features
|
||||||
|
- **PDF Export mit Vorlage**: Briefpapier/Hintergrund-PDF kann als Vorlage hochgeladen werden
|
||||||
|
- Upload im Admin-Bereich unter Einstellungen
|
||||||
|
- Vorlage wird als Hintergrund auf allen Seiten verwendet
|
||||||
|
- **PDF Schriftgroessen konfigurierbar**: Anpassbare Schriftgroessen fuer den PDF-Export
|
||||||
|
- Ueberschriften (7-14pt)
|
||||||
|
- Inhalte (6-12pt)
|
||||||
|
- Feldbezeichnungen (5-10pt)
|
||||||
|
- **Verbesserte PDF-Baumdarstellung**: Professionelle Darstellung der Anlagenstruktur
|
||||||
|
- Farbcodierte Header pro Hierarchie-Ebene (dezente Grauabstufungen)
|
||||||
|
- Abgerundete Rahmen um Elemente
|
||||||
|
- Visuelle Verbindungslinien zwischen Elementen
|
||||||
|
- Bessere Einrueckung und Lesbarkeit
|
||||||
|
|
||||||
|
### Verbesserungen
|
||||||
|
- Logo aus PDF-Export entfernt (ersetzt durch Vorlagen-System)
|
||||||
|
- Dynamische Felder fuer Element-Typen (Ueberschrift als neuer Feldtyp)
|
||||||
|
- Kopierfunktion fuer Elemente und Typen
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1.1 (2026-01)
|
||||||
|
|
||||||
|
### Neue Features
|
||||||
|
- **Kontakt/Adressen-Unterstuetzung**: Favoriten und Anlagen koennen nun auch auf Kontakt-/Adressebene verwaltet werden
|
||||||
|
- Ideal fuer Kunden mit mehreren Gebaeuden/Standorten
|
||||||
|
- Neue Tabs "Favoriten" und "Anlagen" auf Kontaktkarten
|
||||||
|
- Vollstaendige Trennung der Daten zwischen Kunde und Kontakten
|
||||||
|
|
||||||
|
### Verbesserungen
|
||||||
|
- Mengen-Eingabe bei Favoriten vereinfacht (Textfeld + Speichern-Button)
|
||||||
|
- Modul-Icon geaendert zu fa-id-card
|
||||||
|
- Dokumentation aktualisiert
|
||||||
|
|
||||||
|
### Datenbank-Aenderungen
|
||||||
|
- Neue Spalte `fk_contact` in Tabelle `llx_kundenkarte_favorite_products`
|
||||||
|
- Neue Spalte `fk_contact` in Tabelle `llx_kundenkarte_anlage`
|
||||||
|
- Neue Spalte `fk_contact` in Tabelle `llx_kundenkarte_societe_system`
|
||||||
|
|
||||||
|
### Hinweis zum Upgrade
|
||||||
|
Nach dem Update bitte das Modul einmal deaktivieren und wieder aktivieren, damit die SQL-Aenderungen ausgefuehrt werden.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1.0
|
||||||
|
|
||||||
|
### Features
|
||||||
|
- Favoriten-Produkte fuer Kunden
|
||||||
|
- Produkte als Favoriten markieren
|
||||||
|
- Standardmengen festlegen
|
||||||
|
- Bestellungen aus Favoriten generieren
|
||||||
|
- Sortierbare Liste
|
||||||
|
|
||||||
|
- Technische Anlagen (Baumstruktur)
|
||||||
|
- Systemkategorien (Strom, Internet, Kabel, Sat)
|
||||||
|
- Konfigurierbare Element-Typen
|
||||||
|
- Individuelle Felder pro Typ
|
||||||
|
- Datei-Upload mit Vorschau
|
||||||
|
- Hierarchische Struktur
|
||||||
|
|
||||||
|
- Admin-Bereich
|
||||||
|
- Systeme verwalten
|
||||||
|
- Typen verwalten
|
||||||
|
- Felder konfigurieren
|
||||||
|
|
||||||
|
Initial version
|
||||||
117
README.md
Executable file
117
README.md
Executable file
|
|
@ -0,0 +1,117 @@
|
||||||
|
# KUNDENKARTE FOR [DOLIBARR ERP & CRM](https://www.dolibarr.org)
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
Das KundenKarte-Modul erweitert Dolibarr um zwei wichtige Funktionen fuer Kunden und deren Kontakte/Adressen:
|
||||||
|
|
||||||
|
### Favoriten-Produkte
|
||||||
|
- Verwalten von Lieblingsprodukten pro Kunde oder Kontakt/Adresse
|
||||||
|
- Schnelle Bestellgenerierung aus Favoriten
|
||||||
|
- Individuelle Standardmengen pro Produkt
|
||||||
|
- Sortierbare Liste mit Drag & Drop oder Pfeiltasten
|
||||||
|
|
||||||
|
### Technische Anlagen (Anlagen)
|
||||||
|
- Hierarchische Baumstruktur fuer technische Installationen
|
||||||
|
- Drag & Drop Sortierung der Elemente innerhalb einer Ebene
|
||||||
|
- Flexible Systemkategorien (z.B. Strom, Internet, Kabel, Sat)
|
||||||
|
- Kategorie-Auswahl beim Erstellen: Gebaeude/Standort oder Element/Geraet
|
||||||
|
- Typ-Select mit FontAwesome-Icons und Farbkodierung (Select2)
|
||||||
|
- Gebaeude-Typen gruppiert nach Ebene (Gebaeude, Etage, Fluegel, Raum, Bereich)
|
||||||
|
- Konfigurierbare Element-Typen mit individuellen Feldern
|
||||||
|
- Datei-Upload mit Bild-Vorschau und PDF-Anzeige
|
||||||
|
- Separate Verwaltung pro Kunde oder pro Kontakt/Adresse (z.B. verschiedene Gebaeude)
|
||||||
|
- Kabelverbindungen zwischen Anlagen-Elementen dokumentieren
|
||||||
|
- Visuelle Darstellung mit parallelen vertikalen Linien fuer jedes Kabel
|
||||||
|
- Automatische Gruppierung mit Abstaenden zwischen Kabel-Gruppen
|
||||||
|
|
||||||
|
### Verteilungsdokumentation (Schaltplan-Editor)
|
||||||
|
- Interaktiver SVG-basierter Schaltplan-Editor
|
||||||
|
- Felder (Panels) und Hutschienen visuell verwalten
|
||||||
|
- Equipment-Bloecke per Drag & Drop positionieren
|
||||||
|
- Sammelschienen (Busbars) fuer Phasenverteilung mit konfigurierbaren Typen
|
||||||
|
- Phasenschienen per Drag & Drop verschiebbar (auch zwischen Hutschienen)
|
||||||
|
- Verbindungen zwischen Geraeten zeichnen (automatisch oder manuell)
|
||||||
|
- Abgaenge und Anschlusspunkte dokumentieren
|
||||||
|
- Klickbare Hutschienen zum Bearbeiten
|
||||||
|
- Zoom und Pan fuer grosse Schaltplaene
|
||||||
|
- Block-Bilder fuer Equipment-Typen (individuelle Darstellung)
|
||||||
|
- Reihenklemmen mit gestapelten Terminals (Mehrstockklemmen)
|
||||||
|
- Bruecken zwischen Reihenklemmen
|
||||||
|
|
||||||
|
### PDF Export
|
||||||
|
- Export der Anlagenstruktur als PDF
|
||||||
|
- Upload einer PDF-Vorlage als Briefpapier/Hintergrund
|
||||||
|
- Konfigurierbare Schriftgroessen (Ueberschriften, Inhalte, Felder)
|
||||||
|
- Professionelle Baumdarstellung mit farbcodierten Ebenen und Rahmen
|
||||||
|
|
||||||
|
### Kontakt/Adressen-Unterstuetzung
|
||||||
|
- Beide Funktionen (Favoriten + Anlagen) sind sowohl auf Kundenebene als auch auf Kontakt-/Adressebene verfuegbar
|
||||||
|
- Ideal fuer Kunden mit mehreren Standorten/Gebaeuden
|
||||||
|
- Vollstaendige Trennung der Daten zwischen Kunde und Kontakten
|
||||||
|
|
||||||
|
## Tabs
|
||||||
|
|
||||||
|
Das Modul fuegt folgende Tabs hinzu:
|
||||||
|
|
||||||
|
| Tab | Objekt | Beschreibung |
|
||||||
|
|-----|--------|--------------|
|
||||||
|
| Favoriten | Kunde (Thirdparty) | Favoriten-Produkte fuer den Kunden |
|
||||||
|
| Favoriten | Kontakt/Adresse | Favoriten-Produkte fuer einen spezifischen Kontakt |
|
||||||
|
| Anlagen | Kunde (Thirdparty) | Technische Anlagen des Kunden |
|
||||||
|
| Anlagen | Kontakt/Adresse | Technische Anlagen eines spezifischen Kontakts/Gebaeudes |
|
||||||
|
|
||||||
|
## Admin-Bereich
|
||||||
|
|
||||||
|
Im Admin-Bereich (Home > Setup > Module > KundenKarte) koennen Sie:
|
||||||
|
|
||||||
|
- **Anlagen-Systeme**: System-Kategorien anlegen (z.B. Strom, Internet)
|
||||||
|
- **Element-Typen**: Geraetetypen definieren (z.B. Zaehler, Router, Wallbox)
|
||||||
|
- **Typ-Felder**: Individuelle Felder pro Geraetetyp konfigurieren
|
||||||
|
- **Gebaeudetypen**: Strukturtypen (Haus, Etage, Raum etc.) fuer die Gebaeude-Hierarchie
|
||||||
|
- **Kabeltypen**: Verbindungsmedien (NYM, NYY, CAT etc.) mit Spezifikationen
|
||||||
|
- **Equipment-Typen**: Schaltplan-Komponenten (z.B. Sicherungsautomaten, FI-Schalter) mit Breite (TE), Farbe und Terminal-Konfiguration
|
||||||
|
- **Phasenschienen-Typen**: Sammelschienen/Phasenschienen-Vorlagen (L1, L2, L3, N, PE, 3P+N etc.) mit Farben und Linien-Konfiguration
|
||||||
|
|
||||||
|
## Berechtigungen
|
||||||
|
|
||||||
|
| Berechtigung | Beschreibung |
|
||||||
|
|--------------|--------------|
|
||||||
|
| kundenkarte read | Favoriten und Anlagen ansehen |
|
||||||
|
| kundenkarte write | Favoriten und Anlagen bearbeiten |
|
||||||
|
| kundenkarte delete | Favoriten und Anlagen loeschen |
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
### Voraussetzungen
|
||||||
|
- Dolibarr ERP & CRM >= 19.0
|
||||||
|
- PHP >= 7.1
|
||||||
|
|
||||||
|
### Installation via ZIP
|
||||||
|
1. ZIP-Datei herunterladen
|
||||||
|
2. In Dolibarr: Home > Setup > Module > Externes Modul deployen
|
||||||
|
3. ZIP-Datei hochladen
|
||||||
|
4. Modul aktivieren unter Home > Setup > Module
|
||||||
|
|
||||||
|
### Manuelle Installation
|
||||||
|
1. Modul-Ordner in `/custom/kundenkarte` kopieren
|
||||||
|
2. In Dolibarr: Home > Setup > Module
|
||||||
|
3. Modul "KundenKarte" aktivieren
|
||||||
|
|
||||||
|
### Nach der Aktivierung
|
||||||
|
- Die SQL-Tabellen werden automatisch erstellt
|
||||||
|
- Systemkategorien und Typen im Admin-Bereich anlegen
|
||||||
|
- Fertig!
|
||||||
|
|
||||||
|
## Translations
|
||||||
|
|
||||||
|
Uebersetzungen befinden sich in:
|
||||||
|
- `langs/de_DE/kundenkarte.lang` (Deutsch)
|
||||||
|
- `langs/en_US/kundenkarte.lang` (Englisch)
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
GPLv3 or (at your option) any later version. See file COPYING for more information.
|
||||||
|
|
||||||
|
## Author
|
||||||
|
|
||||||
|
Alles Watt laeuft - Eduard Wisch
|
||||||
118
admin/about.php
Executable file
118
admin/about.php
Executable file
|
|
@ -0,0 +1,118 @@
|
||||||
|
<?php
|
||||||
|
/* Copyright (C) 2004-2017 Laurent Destailleur <eldy@users.sourceforge.net>
|
||||||
|
* Copyright (C) 2026 Eduard Wisch <data@data-it-solution.de>
|
||||||
|
* Copyright (C) 2024 Frédéric France <frederic.france@free.fr>
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* \file kundenkarte/admin/about.php
|
||||||
|
* \ingroup kundenkarte
|
||||||
|
* \brief About page of module KundenKarte.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Load Dolibarr environment
|
||||||
|
$res = 0;
|
||||||
|
// Try main.inc.php into web root known defined into CONTEXT_DOCUMENT_ROOT (not always defined)
|
||||||
|
if (!$res && !empty($_SERVER["CONTEXT_DOCUMENT_ROOT"])) {
|
||||||
|
$res = @include $_SERVER["CONTEXT_DOCUMENT_ROOT"]."/main.inc.php";
|
||||||
|
}
|
||||||
|
// Try main.inc.php into web root detected using web root calculated from SCRIPT_FILENAME
|
||||||
|
$tmp = empty($_SERVER['SCRIPT_FILENAME']) ? '' : $_SERVER['SCRIPT_FILENAME'];
|
||||||
|
$tmp2 = realpath(__FILE__);
|
||||||
|
$i = strlen($tmp) - 1;
|
||||||
|
$j = strlen($tmp2) - 1;
|
||||||
|
while ($i > 0 && $j > 0 && isset($tmp[$i]) && isset($tmp2[$j]) && $tmp[$i] == $tmp2[$j]) {
|
||||||
|
$i--;
|
||||||
|
$j--;
|
||||||
|
}
|
||||||
|
if (!$res && $i > 0 && file_exists(substr($tmp, 0, ($i + 1))."/main.inc.php")) {
|
||||||
|
$res = @include substr($tmp, 0, ($i + 1))."/main.inc.php";
|
||||||
|
}
|
||||||
|
if (!$res && $i > 0 && file_exists(dirname(substr($tmp, 0, ($i + 1)))."/main.inc.php")) {
|
||||||
|
$res = @include dirname(substr($tmp, 0, ($i + 1)))."/main.inc.php";
|
||||||
|
}
|
||||||
|
// Try main.inc.php using relative path
|
||||||
|
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/lib/functions2.lib.php';
|
||||||
|
require_once '../lib/kundenkarte.lib.php';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var Conf $conf
|
||||||
|
* @var DoliDB $db
|
||||||
|
* @var HookManager $hookmanager
|
||||||
|
* @var Translate $langs
|
||||||
|
* @var User $user
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Translations
|
||||||
|
$langs->loadLangs(array("errors", "admin", "kundenkarte@kundenkarte"));
|
||||||
|
|
||||||
|
// Access control
|
||||||
|
if (!$user->admin) {
|
||||||
|
accessforbidden();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parameters
|
||||||
|
$action = GETPOST('action', 'aZ09');
|
||||||
|
$backtopage = GETPOST('backtopage', 'alpha');
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Actions
|
||||||
|
*/
|
||||||
|
|
||||||
|
// None
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* View
|
||||||
|
*/
|
||||||
|
|
||||||
|
$form = new Form($db);
|
||||||
|
|
||||||
|
$help_url = '';
|
||||||
|
$title = "KundenKarteSetup";
|
||||||
|
|
||||||
|
llxHeader('', $langs->trans($title), $help_url, '', 0, 0, '', '', '', 'mod-kundenkarte page-admin_about');
|
||||||
|
|
||||||
|
// Subheader
|
||||||
|
$linkback = '<a href="'.($backtopage ? $backtopage : DOL_URL_ROOT.'/admin/modules.php?restore_lastsearch_values=1').'">'.$langs->trans("BackToModuleList").'</a>';
|
||||||
|
|
||||||
|
print load_fiche_titre($langs->trans($title), $linkback, 'title_setup');
|
||||||
|
|
||||||
|
// Configuration header
|
||||||
|
$head = kundenkarteAdminPrepareHead();
|
||||||
|
print dol_get_fiche_head($head, 'about', $langs->trans($title), 0, 'kundenkarte@kundenkarte');
|
||||||
|
|
||||||
|
dol_include_once('/kundenkarte/core/modules/modKundenKarte.class.php');
|
||||||
|
$tmpmodule = new modKundenKarte($db);
|
||||||
|
print $tmpmodule->getDescLong();
|
||||||
|
|
||||||
|
// Page end
|
||||||
|
print dol_get_fiche_end();
|
||||||
|
llxFooter();
|
||||||
|
$db->close();
|
||||||
349
admin/anlage_systems.php
Executable file
349
admin/anlage_systems.php
Executable file
|
|
@ -0,0 +1,349 @@
|
||||||
|
<?php
|
||||||
|
/* Copyright (C) 2026 Alles Watt lauft
|
||||||
|
*
|
||||||
|
* Admin page to manage installation system categories
|
||||||
|
*/
|
||||||
|
|
||||||
|
$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");
|
||||||
|
|
||||||
|
require_once DOL_DOCUMENT_ROOT.'/core/lib/admin.lib.php';
|
||||||
|
require_once DOL_DOCUMENT_ROOT.'/core/class/html.form.class.php';
|
||||||
|
dol_include_once('/kundenkarte/lib/kundenkarte.lib.php');
|
||||||
|
|
||||||
|
$langs->loadLangs(array('admin', 'kundenkarte@kundenkarte'));
|
||||||
|
|
||||||
|
// Security check
|
||||||
|
if (!$user->admin && !$user->hasRight('kundenkarte', 'admin')) {
|
||||||
|
accessforbidden();
|
||||||
|
}
|
||||||
|
|
||||||
|
$action = GETPOST('action', 'aZ09');
|
||||||
|
$confirm = GETPOST('confirm', 'alpha');
|
||||||
|
$systemId = GETPOSTINT('systemid');
|
||||||
|
|
||||||
|
$form = new Form($db);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Actions
|
||||||
|
*/
|
||||||
|
|
||||||
|
if ($action == 'add') {
|
||||||
|
$code = GETPOST('code', 'aZ09');
|
||||||
|
$label = GETPOST('label', 'alphanohtml');
|
||||||
|
$picto = GETPOST('picto', 'alphanohtml');
|
||||||
|
$color = GETPOST('color', 'alphanohtml');
|
||||||
|
$position = GETPOSTINT('position');
|
||||||
|
|
||||||
|
// Tree display config
|
||||||
|
$treeConfig = array(
|
||||||
|
'show_ref' => GETPOSTINT('show_ref'),
|
||||||
|
'show_label' => GETPOSTINT('show_label'),
|
||||||
|
'show_type' => GETPOSTINT('show_type'),
|
||||||
|
'show_icon' => GETPOSTINT('show_icon'),
|
||||||
|
'show_status' => GETPOSTINT('show_status'),
|
||||||
|
'show_fields' => GETPOSTINT('show_fields'),
|
||||||
|
'expand_default' => GETPOSTINT('expand_default'),
|
||||||
|
'indent_style' => GETPOST('indent_style', 'alpha') ?: 'lines'
|
||||||
|
);
|
||||||
|
$treeConfigJson = json_encode($treeConfig);
|
||||||
|
|
||||||
|
if (empty($code) || empty($label)) {
|
||||||
|
setEventMessages($langs->trans('ErrorFieldRequired'), null, 'errors');
|
||||||
|
} else {
|
||||||
|
$sql = "INSERT INTO ".MAIN_DB_PREFIX."c_kundenkarte_anlage_system";
|
||||||
|
$sql .= " (code, label, picto, color, position, active, entity, tree_display_config)";
|
||||||
|
$sql .= " VALUES ('".$db->escape(strtoupper($code))."', '".$db->escape($label)."',";
|
||||||
|
$sql .= " ".($picto ? "'".$db->escape($picto)."'" : "NULL").",";
|
||||||
|
$sql .= " ".($color ? "'".$db->escape($color)."'" : "NULL").",";
|
||||||
|
$sql .= " ".((int) $position).", 1, 0,";
|
||||||
|
$sql .= " '".$db->escape($treeConfigJson)."')";
|
||||||
|
|
||||||
|
$result = $db->query($sql);
|
||||||
|
if ($result) {
|
||||||
|
setEventMessages($langs->trans('RecordSaved'), null, 'mesgs');
|
||||||
|
} else {
|
||||||
|
setEventMessages($db->lasterror(), null, 'errors');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$action = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($action == 'update') {
|
||||||
|
$code = GETPOST('code', 'aZ09');
|
||||||
|
$label = GETPOST('label', 'alphanohtml');
|
||||||
|
$picto = GETPOST('picto', 'alphanohtml');
|
||||||
|
$color = GETPOST('color', 'alphanohtml');
|
||||||
|
$position = GETPOSTINT('position');
|
||||||
|
|
||||||
|
// Tree display config
|
||||||
|
$treeConfig = array(
|
||||||
|
'show_ref' => GETPOSTINT('show_ref'),
|
||||||
|
'show_label' => GETPOSTINT('show_label'),
|
||||||
|
'show_type' => GETPOSTINT('show_type'),
|
||||||
|
'show_icon' => GETPOSTINT('show_icon'),
|
||||||
|
'show_status' => GETPOSTINT('show_status'),
|
||||||
|
'show_fields' => GETPOSTINT('show_fields'),
|
||||||
|
'expand_default' => GETPOSTINT('expand_default'),
|
||||||
|
'indent_style' => GETPOST('indent_style', 'alpha') ?: 'lines'
|
||||||
|
);
|
||||||
|
$treeConfigJson = json_encode($treeConfig);
|
||||||
|
|
||||||
|
$sql = "UPDATE ".MAIN_DB_PREFIX."c_kundenkarte_anlage_system SET";
|
||||||
|
$sql .= " code = '".$db->escape(strtoupper($code))."'";
|
||||||
|
$sql .= ", label = '".$db->escape($label)."'";
|
||||||
|
$sql .= ", picto = ".($picto ? "'".$db->escape($picto)."'" : "NULL");
|
||||||
|
$sql .= ", color = ".($color ? "'".$db->escape($color)."'" : "NULL");
|
||||||
|
$sql .= ", position = ".((int) $position);
|
||||||
|
$sql .= ", tree_display_config = '".$db->escape($treeConfigJson)."'";
|
||||||
|
$sql .= " WHERE rowid = ".((int) $systemId);
|
||||||
|
|
||||||
|
$result = $db->query($sql);
|
||||||
|
if ($result) {
|
||||||
|
setEventMessages($langs->trans('RecordSaved'), null, 'mesgs');
|
||||||
|
} else {
|
||||||
|
setEventMessages($db->lasterror(), null, 'errors');
|
||||||
|
}
|
||||||
|
$action = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($action == 'confirm_delete' && $confirm == 'yes') {
|
||||||
|
// Check if in use
|
||||||
|
$sql = "SELECT COUNT(*) as cnt FROM ".MAIN_DB_PREFIX."kundenkarte_anlage WHERE fk_system = ".((int) $systemId);
|
||||||
|
$resql = $db->query($sql);
|
||||||
|
$obj = $db->fetch_object($resql);
|
||||||
|
|
||||||
|
if ($obj->cnt > 0) {
|
||||||
|
setEventMessages($langs->trans('ErrorSystemInUse'), null, 'errors');
|
||||||
|
} else {
|
||||||
|
// Also delete types for this system
|
||||||
|
$sql = "DELETE FROM ".MAIN_DB_PREFIX."kundenkarte_anlage_type WHERE fk_system = ".((int) $systemId);
|
||||||
|
$db->query($sql);
|
||||||
|
|
||||||
|
$sql = "DELETE FROM ".MAIN_DB_PREFIX."c_kundenkarte_anlage_system WHERE rowid = ".((int) $systemId);
|
||||||
|
$result = $db->query($sql);
|
||||||
|
if ($result) {
|
||||||
|
setEventMessages($langs->trans('RecordDeleted'), null, 'mesgs');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$action = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($action == 'activate') {
|
||||||
|
$sql = "UPDATE ".MAIN_DB_PREFIX."c_kundenkarte_anlage_system SET active = 1 WHERE rowid = ".((int) $systemId);
|
||||||
|
$db->query($sql);
|
||||||
|
$action = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($action == 'deactivate') {
|
||||||
|
$sql = "UPDATE ".MAIN_DB_PREFIX."c_kundenkarte_anlage_system SET active = 0 WHERE rowid = ".((int) $systemId);
|
||||||
|
$db->query($sql);
|
||||||
|
$action = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* View
|
||||||
|
*/
|
||||||
|
|
||||||
|
$title = $langs->trans('AnlagenSystems');
|
||||||
|
|
||||||
|
// Include CSS and JS
|
||||||
|
$morejs = array('/kundenkarte/js/kundenkarte.js?v=1769962608');
|
||||||
|
$morecss = array('/kundenkarte/css/kundenkarte.css?v=1769962608');
|
||||||
|
|
||||||
|
llxHeader('', $title, '', '', 0, 0, $morejs, $morecss);
|
||||||
|
|
||||||
|
$head = kundenkarteAdminPrepareHead();
|
||||||
|
print dol_get_fiche_head($head, 'systems', $langs->trans('ModuleKundenKarteName'), -1, 'fa-file');
|
||||||
|
|
||||||
|
// Confirmation
|
||||||
|
if ($action == 'delete') {
|
||||||
|
print $form->formconfirm(
|
||||||
|
$_SERVER['PHP_SELF'].'?systemid='.$systemId,
|
||||||
|
$langs->trans('Delete'),
|
||||||
|
$langs->trans('ConfirmDeleteSystem'),
|
||||||
|
'confirm_delete',
|
||||||
|
'',
|
||||||
|
'yes',
|
||||||
|
1
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add form
|
||||||
|
if ($action == 'create' || $action == 'edit') {
|
||||||
|
$system = null;
|
||||||
|
if ($action == 'edit' && $systemId > 0) {
|
||||||
|
$sql = "SELECT * FROM ".MAIN_DB_PREFIX."c_kundenkarte_anlage_system WHERE rowid = ".((int) $systemId);
|
||||||
|
$resql = $db->query($sql);
|
||||||
|
$system = $db->fetch_object($resql);
|
||||||
|
}
|
||||||
|
|
||||||
|
print '<form method="POST" action="'.$_SERVER['PHP_SELF'].'">';
|
||||||
|
print '<input type="hidden" name="token" value="'.newToken().'">';
|
||||||
|
print '<input type="hidden" name="action" value="'.($action == 'edit' ? 'update' : 'add').'">';
|
||||||
|
if ($action == 'edit') {
|
||||||
|
print '<input type="hidden" name="systemid" value="'.$systemId.'">';
|
||||||
|
}
|
||||||
|
|
||||||
|
print '<table class="border centpercent">';
|
||||||
|
|
||||||
|
print '<tr><td class="titlefield fieldrequired">'.$langs->trans('SystemCode').'</td>';
|
||||||
|
print '<td><input type="text" name="code" class="flat minwidth200" value="'.dol_escape_htmltag($system ? $system->code : '').'" maxlength="32" required></td></tr>';
|
||||||
|
|
||||||
|
print '<tr><td class="fieldrequired">'.$langs->trans('SystemLabel').'</td>';
|
||||||
|
print '<td><input type="text" name="label" class="flat minwidth300" value="'.dol_escape_htmltag($system ? $system->label : '').'" required></td></tr>';
|
||||||
|
|
||||||
|
print '<tr><td>'.$langs->trans('SystemPicto').'</td>';
|
||||||
|
print '<td><div class="kundenkarte-icon-picker-wrapper">';
|
||||||
|
print '<span class="kundenkarte-icon-preview">';
|
||||||
|
if ($system && $system->picto) {
|
||||||
|
print kundenkarte_render_icon($system->picto);
|
||||||
|
}
|
||||||
|
print '</span>';
|
||||||
|
print '<input type="text" name="picto" class="flat minwidth200" value="'.dol_escape_htmltag($system ? $system->picto : '').'" placeholder="fa-bolt">';
|
||||||
|
print '<button type="button" class="kundenkarte-icon-picker-btn" data-input="picto"><i class="fa fa-th"></i> '.$langs->trans('SelectIcon').'</button>';
|
||||||
|
print '</div></td></tr>';
|
||||||
|
|
||||||
|
print '<tr><td>'.$langs->trans('SystemColor').'</td>';
|
||||||
|
print '<td><input type="color" name="color" value="'.dol_escape_htmltag($system ? $system->color : '#3498db').'"></td></tr>';
|
||||||
|
|
||||||
|
print '<tr><td>'.$langs->trans('Position').'</td>';
|
||||||
|
print '<td><input type="number" name="position" class="flat" value="'.($system ? $system->position : 0).'" min="0"></td></tr>';
|
||||||
|
|
||||||
|
print '</table>';
|
||||||
|
|
||||||
|
// Tree display configuration
|
||||||
|
print '<br><h3>'.$langs->trans('TreeDisplayConfig').'</h3>';
|
||||||
|
|
||||||
|
// Parse existing config
|
||||||
|
$treeConfig = array(
|
||||||
|
'show_ref' => 1,
|
||||||
|
'show_label' => 1,
|
||||||
|
'show_type' => 1,
|
||||||
|
'show_icon' => 1,
|
||||||
|
'show_status' => 1,
|
||||||
|
'show_fields' => 0,
|
||||||
|
'expand_default' => 1,
|
||||||
|
'indent_style' => 'lines'
|
||||||
|
);
|
||||||
|
if ($system && !empty($system->tree_display_config)) {
|
||||||
|
$savedConfig = json_decode($system->tree_display_config, true);
|
||||||
|
if (is_array($savedConfig)) {
|
||||||
|
$treeConfig = array_merge($treeConfig, $savedConfig);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
print '<table class="border centpercent">';
|
||||||
|
|
||||||
|
print '<tr><td class="titlefield">'.$langs->trans('TreeShowRef').'</td>';
|
||||||
|
print '<td><input type="checkbox" name="show_ref" value="1"'.($treeConfig['show_ref'] ? ' checked' : '').'> ';
|
||||||
|
print '<span class="opacitymedium">'.$langs->trans('TreeShowRefHelp').'</span></td></tr>';
|
||||||
|
|
||||||
|
print '<tr><td>'.$langs->trans('TreeShowLabel').'</td>';
|
||||||
|
print '<td><input type="checkbox" name="show_label" value="1"'.($treeConfig['show_label'] ? ' checked' : '').'> ';
|
||||||
|
print '<span class="opacitymedium">'.$langs->trans('TreeShowLabelHelp').'</span></td></tr>';
|
||||||
|
|
||||||
|
print '<tr><td>'.$langs->trans('TreeShowType').'</td>';
|
||||||
|
print '<td><input type="checkbox" name="show_type" value="1"'.($treeConfig['show_type'] ? ' checked' : '').'> ';
|
||||||
|
print '<span class="opacitymedium">'.$langs->trans('TreeShowTypeHelp').'</span></td></tr>';
|
||||||
|
|
||||||
|
print '<tr><td>'.$langs->trans('TreeShowIcon').'</td>';
|
||||||
|
print '<td><input type="checkbox" name="show_icon" value="1"'.($treeConfig['show_icon'] ? ' checked' : '').'> ';
|
||||||
|
print '<span class="opacitymedium">'.$langs->trans('TreeShowIconHelp').'</span></td></tr>';
|
||||||
|
|
||||||
|
print '<tr><td>'.$langs->trans('TreeShowStatus').'</td>';
|
||||||
|
print '<td><input type="checkbox" name="show_status" value="1"'.($treeConfig['show_status'] ? ' checked' : '').'> ';
|
||||||
|
print '<span class="opacitymedium">'.$langs->trans('TreeShowStatusHelp').'</span></td></tr>';
|
||||||
|
|
||||||
|
print '<tr><td>'.$langs->trans('TreeShowFields').'</td>';
|
||||||
|
print '<td><input type="checkbox" name="show_fields" value="1"'.($treeConfig['show_fields'] ? ' checked' : '').'> ';
|
||||||
|
print '<span class="opacitymedium">'.$langs->trans('TreeShowFieldsHelp').'</span></td></tr>';
|
||||||
|
|
||||||
|
print '<tr><td>'.$langs->trans('TreeExpandDefault').'</td>';
|
||||||
|
print '<td><input type="checkbox" name="expand_default" value="1"'.($treeConfig['expand_default'] ? ' checked' : '').'> ';
|
||||||
|
print '<span class="opacitymedium">'.$langs->trans('TreeExpandDefaultHelp').'</span></td></tr>';
|
||||||
|
|
||||||
|
print '<tr><td>'.$langs->trans('TreeIndentStyle').'</td>';
|
||||||
|
print '<td><select name="indent_style" class="flat">';
|
||||||
|
$styles = array(
|
||||||
|
'lines' => $langs->trans('TreeIndentLines'),
|
||||||
|
'dots' => $langs->trans('TreeIndentDots'),
|
||||||
|
'arrows' => $langs->trans('TreeIndentArrows'),
|
||||||
|
'simple' => $langs->trans('TreeIndentSimple')
|
||||||
|
);
|
||||||
|
foreach ($styles as $code => $label) {
|
||||||
|
$selected = ($treeConfig['indent_style'] == $code) ? ' selected' : '';
|
||||||
|
print '<option value="'.$code.'"'.$selected.'>'.$label.'</option>';
|
||||||
|
}
|
||||||
|
print '</select></td></tr>';
|
||||||
|
|
||||||
|
print '</table>';
|
||||||
|
|
||||||
|
print '<div class="center" style="margin-top:20px;">';
|
||||||
|
print '<button type="submit" class="button">'.$langs->trans('Save').'</button>';
|
||||||
|
print ' <a class="button button-cancel" href="'.$_SERVER['PHP_SELF'].'">'.$langs->trans('Cancel').'</a>';
|
||||||
|
print '</div>';
|
||||||
|
|
||||||
|
print '</form>';
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// List
|
||||||
|
print '<div style="margin-bottom:15px;">';
|
||||||
|
print '<a class="button" href="'.$_SERVER['PHP_SELF'].'?action=create">';
|
||||||
|
print '<i class="fa fa-plus"></i> '.$langs->trans('AddSystem');
|
||||||
|
print '</a>';
|
||||||
|
print '</div>';
|
||||||
|
|
||||||
|
$sql = "SELECT * FROM ".MAIN_DB_PREFIX."c_kundenkarte_anlage_system ORDER BY position ASC, label ASC";
|
||||||
|
$resql = $db->query($sql);
|
||||||
|
|
||||||
|
print '<table class="noborder centpercent">';
|
||||||
|
print '<tr class="liste_titre">';
|
||||||
|
print '<th>'.$langs->trans('SystemCode').'</th>';
|
||||||
|
print '<th>'.$langs->trans('SystemLabel').'</th>';
|
||||||
|
print '<th>'.$langs->trans('SystemPicto').'</th>';
|
||||||
|
print '<th class="center">'.$langs->trans('Position').'</th>';
|
||||||
|
print '<th class="center">'.$langs->trans('Status').'</th>';
|
||||||
|
print '<th class="center">'.$langs->trans('Actions').'</th>';
|
||||||
|
print '</tr>';
|
||||||
|
|
||||||
|
if ($resql) {
|
||||||
|
while ($obj = $db->fetch_object($resql)) {
|
||||||
|
print '<tr class="oddeven">';
|
||||||
|
|
||||||
|
print '<td>'.dol_escape_htmltag($obj->code).'</td>';
|
||||||
|
print '<td>'.dol_escape_htmltag($obj->label).'</td>';
|
||||||
|
print '<td>';
|
||||||
|
if ($obj->picto) {
|
||||||
|
print kundenkarte_render_icon($obj->picto, '', 'color:'.$obj->color.';').' ';
|
||||||
|
print dol_escape_htmltag($obj->picto);
|
||||||
|
}
|
||||||
|
print '</td>';
|
||||||
|
print '<td class="center">'.$obj->position.'</td>';
|
||||||
|
|
||||||
|
print '<td class="center">';
|
||||||
|
if ($obj->active) {
|
||||||
|
print '<a href="'.$_SERVER['PHP_SELF'].'?action=deactivate&systemid='.$obj->rowid.'&token='.newToken().'">'.img_picto($langs->trans('Enabled'), 'switch_on').'</a>';
|
||||||
|
} else {
|
||||||
|
print '<a href="'.$_SERVER['PHP_SELF'].'?action=activate&systemid='.$obj->rowid.'&token='.newToken().'">'.img_picto($langs->trans('Disabled'), 'switch_off').'</a>';
|
||||||
|
}
|
||||||
|
print '</td>';
|
||||||
|
|
||||||
|
print '<td class="center nowraponall">';
|
||||||
|
print '<a href="'.$_SERVER['PHP_SELF'].'?action=edit&systemid='.$obj->rowid.'">'.img_edit().'</a>';
|
||||||
|
print ' <a href="'.$_SERVER['PHP_SELF'].'?action=delete&systemid='.$obj->rowid.'" class="deletelink">'.img_delete().'</a>';
|
||||||
|
print '</td>';
|
||||||
|
|
||||||
|
print '</tr>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
print '</table>';
|
||||||
|
}
|
||||||
|
|
||||||
|
print dol_get_fiche_end();
|
||||||
|
|
||||||
|
llxFooter();
|
||||||
|
$db->close();
|
||||||
873
admin/anlage_types.php
Executable file
873
admin/anlage_types.php
Executable file
|
|
@ -0,0 +1,873 @@
|
||||||
|
<?php
|
||||||
|
/* Copyright (C) 2026 Alles Watt lauft
|
||||||
|
*
|
||||||
|
* Admin page to manage installation element types
|
||||||
|
*/
|
||||||
|
|
||||||
|
$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");
|
||||||
|
|
||||||
|
require_once DOL_DOCUMENT_ROOT.'/core/lib/admin.lib.php';
|
||||||
|
require_once DOL_DOCUMENT_ROOT.'/core/class/html.form.class.php';
|
||||||
|
dol_include_once('/kundenkarte/lib/kundenkarte.lib.php');
|
||||||
|
dol_include_once('/kundenkarte/class/anlagetype.class.php');
|
||||||
|
|
||||||
|
$langs->loadLangs(array('admin', 'kundenkarte@kundenkarte'));
|
||||||
|
|
||||||
|
// Security check
|
||||||
|
if (!$user->admin && !$user->hasRight('kundenkarte', 'admin')) {
|
||||||
|
accessforbidden();
|
||||||
|
}
|
||||||
|
|
||||||
|
$action = GETPOST('action', 'aZ09');
|
||||||
|
$confirm = GETPOST('confirm', 'alpha');
|
||||||
|
$typeId = GETPOSTINT('typeid');
|
||||||
|
|
||||||
|
// System filter - save in session for persistence
|
||||||
|
$sessionKey = 'kundenkarte_anlage_types_system_filter';
|
||||||
|
if (GETPOSTISSET('system')) {
|
||||||
|
$systemFilter = GETPOSTINT('system');
|
||||||
|
$_SESSION[$sessionKey] = $systemFilter;
|
||||||
|
} elseif (isset($_SESSION[$sessionKey])) {
|
||||||
|
$systemFilter = $_SESSION[$sessionKey];
|
||||||
|
} else {
|
||||||
|
$systemFilter = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$form = new Form($db);
|
||||||
|
$anlageType = new AnlageType($db);
|
||||||
|
|
||||||
|
// Load systems
|
||||||
|
$systems = array();
|
||||||
|
$sql = "SELECT rowid, code, label FROM ".MAIN_DB_PREFIX."c_kundenkarte_anlage_system WHERE active = 1 ORDER BY position ASC";
|
||||||
|
$resql = $db->query($sql);
|
||||||
|
if ($resql) {
|
||||||
|
while ($obj = $db->fetch_object($resql)) {
|
||||||
|
$systems[$obj->rowid] = $obj;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Actions
|
||||||
|
*/
|
||||||
|
|
||||||
|
if ($action == 'add') {
|
||||||
|
$anlageType->ref = GETPOST('ref', 'aZ09');
|
||||||
|
$anlageType->label = GETPOST('label', 'alphanohtml');
|
||||||
|
$anlageType->label_short = GETPOST('label_short', 'alphanohtml');
|
||||||
|
$anlageType->description = GETPOST('description', 'restricthtml');
|
||||||
|
$anlageType->fk_system = GETPOSTINT('fk_system');
|
||||||
|
$anlageType->can_have_children = GETPOSTINT('can_have_children');
|
||||||
|
$anlageType->can_be_nested = GETPOSTINT('can_be_nested');
|
||||||
|
$anlageType->allowed_parent_types = preg_replace('/[^A-Z0-9_,]/i', '', GETPOST('allowed_parent_types', 'nohtml'));
|
||||||
|
$anlageType->can_have_equipment = GETPOSTINT('can_have_equipment');
|
||||||
|
$anlageType->picto = GETPOST('picto', 'alphanohtml');
|
||||||
|
$anlageType->color = GETPOST('color', 'alphanohtml');
|
||||||
|
$anlageType->position = GETPOSTINT('position');
|
||||||
|
$anlageType->active = 1;
|
||||||
|
|
||||||
|
if (empty($anlageType->ref) || empty($anlageType->label)) {
|
||||||
|
setEventMessages($langs->trans('ErrorFieldRequired'), null, 'errors');
|
||||||
|
$action = 'create';
|
||||||
|
} else {
|
||||||
|
$result = $anlageType->create($user);
|
||||||
|
if ($result > 0) {
|
||||||
|
// Create default fields for the new type
|
||||||
|
$defaultFields = array(
|
||||||
|
array('code' => 'manufacturer', 'label' => 'Hersteller', 'type' => 'text', 'position' => 10, 'show_in_hover' => 1, 'show_in_tree' => 1),
|
||||||
|
array('code' => 'model', 'label' => 'Modell', 'type' => 'text', 'position' => 20, 'show_in_hover' => 1, 'show_in_tree' => 0),
|
||||||
|
array('code' => 'serial_number', 'label' => 'Seriennummer', 'type' => 'text', 'position' => 30, 'show_in_hover' => 1, 'show_in_tree' => 0),
|
||||||
|
array('code' => 'power_rating', 'label' => 'Leistung', 'type' => 'text', 'position' => 40, 'show_in_hover' => 1, 'show_in_tree' => 1),
|
||||||
|
array('code' => 'location', 'label' => 'Standort', 'type' => 'text', 'position' => 50, 'show_in_hover' => 1, 'show_in_tree' => 0),
|
||||||
|
array('code' => 'installation_date', 'label' => 'Installationsdatum', 'type' => 'date', 'position' => 60, 'show_in_hover' => 1, 'show_in_tree' => 0),
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach ($defaultFields as $field) {
|
||||||
|
$sql = "INSERT INTO ".MAIN_DB_PREFIX."kundenkarte_anlage_type_field";
|
||||||
|
$sql .= " (fk_anlage_type, field_code, field_label, field_type, field_options, show_in_tree, show_in_hover, required, position, active)";
|
||||||
|
$sql .= " VALUES (".((int) $anlageType->id).", '".$db->escape($field['code'])."', '".$db->escape($field['label'])."',";
|
||||||
|
$sql .= " '".$db->escape($field['type'])."', '', ".((int) $field['show_in_tree']).", ".((int) $field['show_in_hover']).", 0, ".((int) $field['position']).", 1)";
|
||||||
|
$db->query($sql);
|
||||||
|
}
|
||||||
|
|
||||||
|
setEventMessages($langs->trans('RecordSaved'), null, 'mesgs');
|
||||||
|
header('Location: '.$_SERVER['PHP_SELF'].'?action=edit&typeid='.$anlageType->id.'&system='.$anlageType->fk_system);
|
||||||
|
exit;
|
||||||
|
} else {
|
||||||
|
setEventMessages($anlageType->error, $anlageType->errors, 'errors');
|
||||||
|
$action = 'create';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($action == 'update') {
|
||||||
|
$anlageType->fetch($typeId);
|
||||||
|
$anlageType->ref = GETPOST('ref', 'aZ09');
|
||||||
|
$anlageType->label = GETPOST('label', 'alphanohtml');
|
||||||
|
$anlageType->label_short = GETPOST('label_short', 'alphanohtml');
|
||||||
|
$anlageType->description = GETPOST('description', 'restricthtml');
|
||||||
|
$anlageType->fk_system = GETPOSTINT('fk_system');
|
||||||
|
$anlageType->can_have_children = GETPOSTINT('can_have_children');
|
||||||
|
$anlageType->can_be_nested = GETPOSTINT('can_be_nested');
|
||||||
|
$anlageType->allowed_parent_types = preg_replace('/[^A-Z0-9_,]/i', '', GETPOST('allowed_parent_types', 'nohtml'));
|
||||||
|
$anlageType->can_have_equipment = GETPOSTINT('can_have_equipment');
|
||||||
|
$anlageType->picto = GETPOST('picto', 'alphanohtml');
|
||||||
|
$anlageType->color = GETPOST('color', 'alphanohtml');
|
||||||
|
$anlageType->position = GETPOSTINT('position');
|
||||||
|
|
||||||
|
$result = $anlageType->update($user);
|
||||||
|
if ($result > 0) {
|
||||||
|
setEventMessages($langs->trans('RecordSaved'), null, 'mesgs');
|
||||||
|
header('Location: '.$_SERVER['PHP_SELF'].'?system='.$anlageType->fk_system);
|
||||||
|
exit;
|
||||||
|
} else {
|
||||||
|
setEventMessages($anlageType->error, $anlageType->errors, 'errors');
|
||||||
|
$action = 'edit';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($action == 'confirm_delete' && $confirm == 'yes') {
|
||||||
|
$anlageType->fetch($typeId);
|
||||||
|
$result = $anlageType->delete($user);
|
||||||
|
if ($result > 0) {
|
||||||
|
setEventMessages($langs->trans('RecordDeleted'), null, 'mesgs');
|
||||||
|
} else {
|
||||||
|
setEventMessages($anlageType->error, $anlageType->errors, 'errors');
|
||||||
|
}
|
||||||
|
$action = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($action == 'activate') {
|
||||||
|
$sql = "UPDATE ".MAIN_DB_PREFIX."kundenkarte_anlage_type SET active = 1 WHERE rowid = ".((int) $typeId);
|
||||||
|
$db->query($sql);
|
||||||
|
$action = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($action == 'deactivate') {
|
||||||
|
$sql = "UPDATE ".MAIN_DB_PREFIX."kundenkarte_anlage_type SET active = 0 WHERE rowid = ".((int) $typeId);
|
||||||
|
$db->query($sql);
|
||||||
|
$action = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy type with all fields
|
||||||
|
if ($action == 'copy' && $typeId > 0) {
|
||||||
|
$sourceType = new AnlageType($db);
|
||||||
|
if ($sourceType->fetch($typeId) > 0) {
|
||||||
|
// Create new type with copied data
|
||||||
|
$newType = new AnlageType($db);
|
||||||
|
$newType->ref = $sourceType->ref.'_COPY';
|
||||||
|
$newType->label = $sourceType->label.' (Kopie)';
|
||||||
|
$newType->label_short = $sourceType->label_short;
|
||||||
|
$newType->description = $sourceType->description;
|
||||||
|
$newType->fk_system = $sourceType->fk_system;
|
||||||
|
$newType->can_have_children = $sourceType->can_have_children;
|
||||||
|
$newType->can_be_nested = $sourceType->can_be_nested;
|
||||||
|
$newType->allowed_parent_types = $sourceType->allowed_parent_types;
|
||||||
|
$newType->can_have_equipment = $sourceType->can_have_equipment;
|
||||||
|
$newType->picto = $sourceType->picto;
|
||||||
|
$newType->color = $sourceType->color;
|
||||||
|
$newType->position = $sourceType->position + 1;
|
||||||
|
$newType->active = 1;
|
||||||
|
|
||||||
|
$result = $newType->create($user);
|
||||||
|
if ($result > 0) {
|
||||||
|
// Copy all fields from source type
|
||||||
|
$sourceFields = $sourceType->fetchFields(0); // Get all fields including inactive
|
||||||
|
foreach ($sourceFields as $field) {
|
||||||
|
$sql = "INSERT INTO ".MAIN_DB_PREFIX."kundenkarte_anlage_type_field";
|
||||||
|
$sql .= " (fk_anlage_type, field_code, field_label, field_type, field_options, show_in_tree, show_in_hover, required, position, active)";
|
||||||
|
$sql .= " VALUES (".((int) $newType->id).", '".$db->escape($field->field_code)."', '".$db->escape($field->field_label)."',";
|
||||||
|
$sql .= " '".$db->escape($field->field_type)."', '".$db->escape($field->field_options)."', ".((int) $field->show_in_tree).",";
|
||||||
|
$sql .= " ".((int) $field->show_in_hover).", ".((int) $field->required).", ".((int) $field->position).", ".((int) $field->active).")";
|
||||||
|
$db->query($sql);
|
||||||
|
}
|
||||||
|
|
||||||
|
setEventMessages($langs->trans('RecordSaved'), null, 'mesgs');
|
||||||
|
header('Location: '.$_SERVER['PHP_SELF'].'?action=edit&typeid='.$newType->id.'&system='.$newType->fk_system);
|
||||||
|
exit;
|
||||||
|
} else {
|
||||||
|
setEventMessages($newType->error, $newType->errors, 'errors');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$action = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Field actions
|
||||||
|
$fieldId = GETPOSTINT('fieldid');
|
||||||
|
|
||||||
|
if ($action == 'add_field') {
|
||||||
|
$fieldCode = GETPOST('field_code', 'aZ09');
|
||||||
|
$fieldLabel = GETPOST('field_label', 'alphanohtml');
|
||||||
|
$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');
|
||||||
|
|
||||||
|
if (empty($fieldCode) || empty($fieldLabel) || empty($fieldType)) {
|
||||||
|
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, 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).", '".$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');
|
||||||
|
} else {
|
||||||
|
setEventMessages($db->lasterror(), null, 'errors');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$action = 'edit';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($action == 'update_field') {
|
||||||
|
$fieldCode = GETPOST('field_code', 'aZ09');
|
||||||
|
$fieldLabel = GETPOST('field_label', 'alphanohtml');
|
||||||
|
$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');
|
||||||
|
|
||||||
|
$sql = "UPDATE ".MAIN_DB_PREFIX."kundenkarte_anlage_type_field SET";
|
||||||
|
$sql .= " field_code = '".$db->escape($fieldCode)."',";
|
||||||
|
$sql .= " field_label = '".$db->escape($fieldLabel)."',";
|
||||||
|
$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);
|
||||||
|
|
||||||
|
if ($db->query($sql)) {
|
||||||
|
setEventMessages($langs->trans('RecordSaved'), null, 'mesgs');
|
||||||
|
} else {
|
||||||
|
setEventMessages($db->lasterror(), null, 'errors');
|
||||||
|
}
|
||||||
|
$action = 'edit';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($action == 'confirm_delete_field' && $confirm == 'yes') {
|
||||||
|
$sql = "DELETE FROM ".MAIN_DB_PREFIX."kundenkarte_anlage_type_field WHERE rowid = ".((int) $fieldId);
|
||||||
|
if ($db->query($sql)) {
|
||||||
|
setEventMessages($langs->trans('RecordDeleted'), null, 'mesgs');
|
||||||
|
} else {
|
||||||
|
setEventMessages($db->lasterror(), null, 'errors');
|
||||||
|
}
|
||||||
|
// Redirect back to edit page to stay in the fields list
|
||||||
|
header('Location: '.$_SERVER['PHP_SELF'].'?action=edit&typeid='.$typeId.'&system='.$systemFilter);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($action == 'activate_field') {
|
||||||
|
$sql = "UPDATE ".MAIN_DB_PREFIX."kundenkarte_anlage_type_field SET active = 1 WHERE rowid = ".((int) $fieldId);
|
||||||
|
$db->query($sql);
|
||||||
|
$action = 'edit';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($action == 'deactivate_field') {
|
||||||
|
$sql = "UPDATE ".MAIN_DB_PREFIX."kundenkarte_anlage_type_field SET active = 0 WHERE rowid = ".((int) $fieldId);
|
||||||
|
$db->query($sql);
|
||||||
|
$action = 'edit';
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* View
|
||||||
|
*/
|
||||||
|
|
||||||
|
$title = $langs->trans('AnlagenTypes');
|
||||||
|
|
||||||
|
// Include CSS and JS
|
||||||
|
$morejs = array('/kundenkarte/js/kundenkarte.js?v=1769962608');
|
||||||
|
$morecss = array('/kundenkarte/css/kundenkarte.css?v=1769962608');
|
||||||
|
|
||||||
|
llxHeader('', $title, '', '', 0, 0, $morejs, $morecss);
|
||||||
|
|
||||||
|
$head = kundenkarteAdminPrepareHead();
|
||||||
|
print dol_get_fiche_head($head, 'types', $langs->trans('ModuleKundenKarteName'), -1, 'fa-file');
|
||||||
|
|
||||||
|
// Confirmation for type deletion
|
||||||
|
if ($action == 'delete') {
|
||||||
|
print $form->formconfirm(
|
||||||
|
$_SERVER['PHP_SELF'].'?typeid='.$typeId.'&system='.$systemFilter,
|
||||||
|
$langs->trans('Delete'),
|
||||||
|
$langs->trans('ConfirmDeleteType'),
|
||||||
|
'confirm_delete',
|
||||||
|
'',
|
||||||
|
'yes',
|
||||||
|
1
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Confirmation for field deletion
|
||||||
|
if ($action == 'delete_field') {
|
||||||
|
print $form->formconfirm(
|
||||||
|
$_SERVER['PHP_SELF'].'?typeid='.$typeId.'&fieldid='.$fieldId.'&system='.$systemFilter,
|
||||||
|
$langs->trans('Delete'),
|
||||||
|
$langs->trans('ConfirmDeleteField'),
|
||||||
|
'confirm_delete_field',
|
||||||
|
'',
|
||||||
|
'yes',
|
||||||
|
1
|
||||||
|
);
|
||||||
|
$action = 'edit'; // Stay in edit mode to show the fields
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add/Edit form
|
||||||
|
if (in_array($action, array('create', 'edit'))) {
|
||||||
|
if ($action == 'edit' && $typeId > 0) {
|
||||||
|
$anlageType->fetch($typeId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all types for parent selection
|
||||||
|
// We need to filter by the same system OR show all types if this type is for all systems
|
||||||
|
$allTypes = $anlageType->fetchAllBySystem(0, 0); // Get all types, we'll filter in the template
|
||||||
|
|
||||||
|
print '<form method="POST" action="'.$_SERVER['PHP_SELF'].'">';
|
||||||
|
print '<input type="hidden" name="token" value="'.newToken().'">';
|
||||||
|
print '<input type="hidden" name="action" value="'.($action == 'edit' ? 'update' : 'add').'">';
|
||||||
|
if ($action == 'edit') {
|
||||||
|
print '<input type="hidden" name="typeid" value="'.$typeId.'">';
|
||||||
|
}
|
||||||
|
|
||||||
|
print '<table class="border centpercent">';
|
||||||
|
|
||||||
|
// System
|
||||||
|
print '<tr><td class="titlefield">'.$langs->trans('System').'</td>';
|
||||||
|
print '<td><select name="fk_system" class="flat minwidth200">';
|
||||||
|
$selAll = (empty($anlageType->fk_system)) ? ' selected' : '';
|
||||||
|
print '<option value="0"'.$selAll.'>'.$langs->trans('AllSystems').'</option>';
|
||||||
|
foreach ($systems as $sys) {
|
||||||
|
if ($sys->code === 'GLOBAL') continue; // Gebaeude-Typen haben eigenen Tab
|
||||||
|
$sel = ($anlageType->fk_system == $sys->rowid) ? ' selected' : '';
|
||||||
|
print '<option value="'.$sys->rowid.'"'.$sel.'>'.dol_escape_htmltag($sys->label).'</option>';
|
||||||
|
}
|
||||||
|
print '</select>';
|
||||||
|
print ' <span class="opacitymedium">('.$langs->trans('AllSystemsHint').')</span>';
|
||||||
|
print '</td></tr>';
|
||||||
|
|
||||||
|
// Reference
|
||||||
|
print '<tr><td class="fieldrequired">'.$langs->trans('TypeRef').'</td>';
|
||||||
|
print '<td><input type="text" name="ref" class="flat minwidth200" value="'.dol_escape_htmltag($anlageType->ref).'" maxlength="64" required>';
|
||||||
|
print ' <span class="opacitymedium">(UPPERCASE, no spaces)</span></td></tr>';
|
||||||
|
|
||||||
|
// Label
|
||||||
|
print '<tr><td class="fieldrequired">'.$langs->trans('TypeLabel').'</td>';
|
||||||
|
print '<td><input type="text" name="label" class="flat minwidth300" value="'.dol_escape_htmltag($anlageType->label).'" required></td></tr>';
|
||||||
|
|
||||||
|
// Short label
|
||||||
|
print '<tr><td>'.$langs->trans('TypeShortLabel').'</td>';
|
||||||
|
print '<td><input type="text" name="label_short" class="flat" value="'.dol_escape_htmltag($anlageType->label_short).'" maxlength="64"></td></tr>';
|
||||||
|
|
||||||
|
// Description
|
||||||
|
print '<tr><td>'.$langs->trans('Description').'</td>';
|
||||||
|
print '<td><textarea name="description" class="flat minwidth300" rows="2">'.dol_escape_htmltag($anlageType->description).'</textarea></td></tr>';
|
||||||
|
|
||||||
|
// Can have children
|
||||||
|
print '<tr><td>'.$langs->trans('CanHaveChildren').'</td>';
|
||||||
|
print '<td><input type="checkbox" name="can_have_children" value="1"'.($anlageType->can_have_children ? ' checked' : '').'></td></tr>';
|
||||||
|
|
||||||
|
// Can be nested
|
||||||
|
print '<tr><td>'.$langs->trans('CanBeNested').'</td>';
|
||||||
|
print '<td><input type="checkbox" name="can_be_nested" value="1"'.($anlageType->can_be_nested ? ' checked' : '').'>';
|
||||||
|
print ' <span class="opacitymedium">('.$langs->trans('SameTypeUnderItself').')</span></td></tr>';
|
||||||
|
|
||||||
|
// Can have equipment (Hutschienen-Komponenten)
|
||||||
|
print '<tr><td>'.$langs->trans('CanHaveEquipment').'</td>';
|
||||||
|
print '<td><input type="checkbox" name="can_have_equipment" value="1"'.($anlageType->can_have_equipment ? ' checked' : '').'>';
|
||||||
|
print ' <span class="opacitymedium">('.$langs->trans('CanHaveEquipmentHelp').')</span></td></tr>';
|
||||||
|
|
||||||
|
// Allowed parent types - with multi-select UI
|
||||||
|
print '<tr><td>'.$langs->trans('AllowedParentTypes').'</td>';
|
||||||
|
print '<td>';
|
||||||
|
|
||||||
|
// Hidden field to store the actual value
|
||||||
|
print '<input type="hidden" name="allowed_parent_types" id="allowed_parent_types" value="'.dol_escape_htmltag($anlageType->allowed_parent_types).'">';
|
||||||
|
|
||||||
|
// Selection UI
|
||||||
|
print '<div class="kundenkarte-parent-types-selector">';
|
||||||
|
|
||||||
|
// Select dropdown with add button
|
||||||
|
// Get current type's system for filtering (when editing)
|
||||||
|
$currentTypeSystem = ($action == 'edit') ? $anlageType->fk_system : GETPOSTINT('fk_system');
|
||||||
|
|
||||||
|
print '<div style="display:flex;gap:5px;margin-bottom:10px;align-items:center;">';
|
||||||
|
print '<select id="parent_type_select" class="flat" style="height:30px;">';
|
||||||
|
print '<option value="">'.$langs->trans('SelectType').'</option>';
|
||||||
|
foreach ($allTypes as $t) {
|
||||||
|
// Don't show current type in list (can't be parent of itself unless can_be_nested)
|
||||||
|
if ($action == 'edit' && $t->id == $typeId) continue;
|
||||||
|
|
||||||
|
// Filter by system: Show type if:
|
||||||
|
// 1. Current type is for all systems (fk_system = 0) - show all parent types
|
||||||
|
// 2. Parent type is for all systems (fk_system = 0) - always available
|
||||||
|
// 3. Parent type has same system as current type
|
||||||
|
$showType = false;
|
||||||
|
if (empty($currentTypeSystem)) {
|
||||||
|
// Current type is for all systems - show all possible parent types
|
||||||
|
$showType = true;
|
||||||
|
} elseif (empty($t->fk_system)) {
|
||||||
|
// Parent type is global (all systems) - always show
|
||||||
|
$showType = true;
|
||||||
|
} elseif ($t->fk_system == $currentTypeSystem) {
|
||||||
|
// Same system
|
||||||
|
$showType = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$showType) continue;
|
||||||
|
|
||||||
|
// Add system indicator for clarity
|
||||||
|
$systemHint = '';
|
||||||
|
if (empty($t->fk_system)) {
|
||||||
|
$systemHint = ' ['.$langs->trans('AllSystems').']';
|
||||||
|
} elseif (isset($systems[$t->fk_system])) {
|
||||||
|
$systemHint = ' ['.$systems[$t->fk_system]->label.']';
|
||||||
|
}
|
||||||
|
|
||||||
|
print '<option value="'.dol_escape_htmltag($t->ref).'" data-label="'.dol_escape_htmltag($t->label).'" data-system="'.$t->fk_system.'">'.dol_escape_htmltag($t->ref).' - '.dol_escape_htmltag($t->label).$systemHint.'</option>';
|
||||||
|
}
|
||||||
|
print '</select>';
|
||||||
|
print '<button type="button" class="button" id="add_parent_type_btn"><i class="fa fa-plus"></i> '.$langs->trans('Add').'</button>';
|
||||||
|
print '</div>';
|
||||||
|
|
||||||
|
// List of selected parent types
|
||||||
|
print '<div id="selected_parent_types" class="kundenkarte-selected-items">';
|
||||||
|
// Will be filled by JavaScript
|
||||||
|
print '</div>';
|
||||||
|
|
||||||
|
print '</div>';
|
||||||
|
|
||||||
|
print '<span class="opacitymedium">('.$langs->trans('AllowedParentTypesHelp').')</span>';
|
||||||
|
print '</td></tr>';
|
||||||
|
|
||||||
|
// Icon
|
||||||
|
print '<tr><td>'.$langs->trans('SystemPicto').'</td>';
|
||||||
|
print '<td><div class="kundenkarte-icon-picker-wrapper">';
|
||||||
|
print '<span class="kundenkarte-icon-preview">';
|
||||||
|
if ($anlageType->picto) {
|
||||||
|
print kundenkarte_render_icon($anlageType->picto);
|
||||||
|
}
|
||||||
|
print '</span>';
|
||||||
|
print '<input type="text" name="picto" class="flat minwidth200" value="'.dol_escape_htmltag($anlageType->picto).'" placeholder="fa-cube">';
|
||||||
|
print '<button type="button" class="kundenkarte-icon-picker-btn" data-input="picto"><i class="fa fa-th"></i> '.$langs->trans('SelectIcon').'</button>';
|
||||||
|
print '</div></td></tr>';
|
||||||
|
|
||||||
|
// Position
|
||||||
|
print '<tr><td>'.$langs->trans('Position').'</td>';
|
||||||
|
print '<td><input type="number" name="position" class="flat" value="'.($anlageType->position ?: 0).'" min="0"></td></tr>';
|
||||||
|
|
||||||
|
print '</table>';
|
||||||
|
|
||||||
|
print '<div class="center" style="margin-top:20px;">';
|
||||||
|
print '<button type="submit" class="button">'.$langs->trans('Save').'</button>';
|
||||||
|
print ' <a class="button button-cancel" href="'.$_SERVER['PHP_SELF'].'?system='.$systemFilter.'">'.$langs->trans('Cancel').'</a>';
|
||||||
|
print '</div>';
|
||||||
|
|
||||||
|
print '</form>';
|
||||||
|
|
||||||
|
// Fields management for existing type
|
||||||
|
if ($action == 'edit' && $typeId > 0) {
|
||||||
|
$editFieldId = GETPOSTINT('editfield');
|
||||||
|
|
||||||
|
print '<br><br>';
|
||||||
|
print '<h3>'.$langs->trans('AnlagenTypeFields').'</h3>';
|
||||||
|
|
||||||
|
$fields = $anlageType->fetchFields(0);
|
||||||
|
|
||||||
|
// Field types available
|
||||||
|
$fieldTypes = array(
|
||||||
|
'header' => '── Überschrift ──',
|
||||||
|
'text' => 'Textfeld (einzeilig)',
|
||||||
|
'textarea' => 'Textfeld (mehrzeilig)',
|
||||||
|
'number' => 'Zahlenfeld',
|
||||||
|
'select' => 'Dropdown-Auswahl',
|
||||||
|
'date' => 'Datumsfeld',
|
||||||
|
'checkbox' => 'Checkbox (Ja/Nein)',
|
||||||
|
);
|
||||||
|
|
||||||
|
// Output edit forms BEFORE the table (forms cannot be inside tables)
|
||||||
|
foreach ($fields as $field) {
|
||||||
|
if ($editFieldId == $field->rowid) {
|
||||||
|
$formId = 'editfield_'.$field->rowid;
|
||||||
|
print '<form method="POST" action="'.$_SERVER['PHP_SELF'].'" id="'.$formId.'">';
|
||||||
|
print '<input type="hidden" name="token" value="'.newToken().'">';
|
||||||
|
print '<input type="hidden" name="action" value="update_field">';
|
||||||
|
print '<input type="hidden" name="typeid" value="'.$typeId.'">';
|
||||||
|
print '<input type="hidden" name="fieldid" value="'.$field->rowid.'">';
|
||||||
|
print '<input type="hidden" name="system" value="'.$systemFilter.'">';
|
||||||
|
print '</form>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
print '<table class="noborder centpercent">';
|
||||||
|
print '<tr class="liste_titre">';
|
||||||
|
print '<th>'.$langs->trans('FieldCode').'</th>';
|
||||||
|
print '<th>'.$langs->trans('FieldLabel').'</th>';
|
||||||
|
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>';
|
||||||
|
print '<th class="center">'.$langs->trans('Actions').'</th>';
|
||||||
|
print '</tr>';
|
||||||
|
|
||||||
|
foreach ($fields as $field) {
|
||||||
|
// Check if we're editing this field
|
||||||
|
if ($editFieldId == $field->rowid) {
|
||||||
|
// Edit row - inputs linked to form outside table via form attribute
|
||||||
|
$formId = 'editfield_'.$field->rowid;
|
||||||
|
|
||||||
|
print '<tr class="oddeven">';
|
||||||
|
print '<td><input type="text" name="field_code" form="'.$formId.'" class="flat minwidth100" value="'.dol_escape_htmltag($field->field_code).'" required></td>';
|
||||||
|
print '<td><input type="text" name="field_label" form="'.$formId.'" class="flat minwidth150" value="'.dol_escape_htmltag($field->field_label).'" required></td>';
|
||||||
|
print '<td><select name="field_type" form="'.$formId.'" class="flat">';
|
||||||
|
foreach ($fieldTypes as $ftype => $flabel) {
|
||||||
|
$sel = ($field->field_type == $ftype) ? ' selected' : '';
|
||||||
|
print '<option value="'.$ftype.'"'.$sel.'>'.$flabel.'</option>';
|
||||||
|
}
|
||||||
|
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>';
|
||||||
|
print '<td class="center nowraponall">';
|
||||||
|
print '<div style="display:inline-flex;gap:8px;">';
|
||||||
|
print '<button type="submit" form="'.$formId.'" class="button buttongen" style="width:32px;height:32px;padding:0;display:inline-flex;align-items:center;justify-content:center;background:#28a745;border-color:#28a745;color:#fff;" title="'.$langs->trans('Save').'"><i class="fas fa-save"></i></button>';
|
||||||
|
print '<a class="button buttongen" style="width:32px;height:32px;padding:0;display:inline-flex;align-items:center;justify-content:center;" href="'.$_SERVER['PHP_SELF'].'?action=edit&typeid='.$typeId.'&system='.$systemFilter.'" title="'.$langs->trans('Cancel').'"><i class="fas fa-times"></i></a>';
|
||||||
|
print '</div>';
|
||||||
|
print '</td>';
|
||||||
|
print '</tr>';
|
||||||
|
} else {
|
||||||
|
// Display row
|
||||||
|
print '<tr class="oddeven">';
|
||||||
|
print '<td>'.dol_escape_htmltag($field->field_code).'</td>';
|
||||||
|
print '<td>'.dol_escape_htmltag($field->field_label).'</td>';
|
||||||
|
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>';
|
||||||
|
$enableAutocomplete = isset($field->enable_autocomplete) ? $field->enable_autocomplete : 0;
|
||||||
|
print '<td class="center">'.($enableAutocomplete ? '<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">';
|
||||||
|
if ($field->active) {
|
||||||
|
print '<a href="'.$_SERVER['PHP_SELF'].'?action=deactivate_field&typeid='.$typeId.'&fieldid='.$field->rowid.'&system='.$systemFilter.'&token='.newToken().'">'.img_picto($langs->trans('Enabled'), 'switch_on').'</a>';
|
||||||
|
} else {
|
||||||
|
print '<a href="'.$_SERVER['PHP_SELF'].'?action=activate_field&typeid='.$typeId.'&fieldid='.$field->rowid.'&system='.$systemFilter.'&token='.newToken().'">'.img_picto($langs->trans('Disabled'), 'switch_off').'</a>';
|
||||||
|
}
|
||||||
|
print '</td>';
|
||||||
|
print '<td class="center nowraponall">';
|
||||||
|
print '<div style="display:inline-flex;gap:8px;">';
|
||||||
|
print '<a class="button buttongen" style="width:32px;height:32px;padding:0;display:inline-flex;align-items:center;justify-content:center;" href="'.$_SERVER['PHP_SELF'].'?action=edit&typeid='.$typeId.'&editfield='.$field->rowid.'&system='.$systemFilter.'" title="'.$langs->trans('Edit').'"><i class="fas fa-pen"></i></a>';
|
||||||
|
print '<a class="button buttongen" style="width:32px;height:32px;padding:0;display:inline-flex;align-items:center;justify-content:center;background:#dc3545;border-color:#dc3545;color:#fff;" href="'.$_SERVER['PHP_SELF'].'?action=delete_field&typeid='.$typeId.'&fieldid='.$field->rowid.'&system='.$systemFilter.'&token='.newToken().'" title="'.$langs->trans('Delete').'"><i class="fas fa-trash"></i></a>';
|
||||||
|
print '</div>';
|
||||||
|
print '</td>';
|
||||||
|
print '</tr>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($fields)) {
|
||||||
|
print '<tr class="oddeven"><td colspan="12" class="opacitymedium">'.$langs->trans('NoFieldsDefined').'</td></tr>';
|
||||||
|
}
|
||||||
|
|
||||||
|
print '</table>';
|
||||||
|
|
||||||
|
// Add new field form - completely separate from table
|
||||||
|
print '<div class="margintoponlyshort">';
|
||||||
|
print '<form method="POST" action="'.$_SERVER['PHP_SELF'].'">';
|
||||||
|
print '<input type="hidden" name="token" value="'.newToken().'">';
|
||||||
|
print '<input type="hidden" name="action" value="add_field">';
|
||||||
|
print '<input type="hidden" name="typeid" value="'.$typeId.'">';
|
||||||
|
print '<input type="hidden" name="system" value="'.$systemFilter.'">';
|
||||||
|
print '<table class="noborder centpercent">';
|
||||||
|
print '<tr class="liste_titre">';
|
||||||
|
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>';
|
||||||
|
print '<td><input type="text" name="field_label" class="flat minwidth150" placeholder="'.$langs->trans('FieldLabel').'" required></td>';
|
||||||
|
print '<td><select name="field_type" class="flat" required>';
|
||||||
|
print '<option value="">'.$langs->trans('Select').'</option>';
|
||||||
|
foreach ($fieldTypes as $ftype => $flabel) {
|
||||||
|
print '<option value="'.$ftype.'">'.$flabel.'</option>';
|
||||||
|
}
|
||||||
|
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>';
|
||||||
|
print '<td class="center"><button type="submit" class="button buttongen"><i class="fa fa-plus"></i> '.$langs->trans('Add').'</button></td>';
|
||||||
|
print '</tr>';
|
||||||
|
print '</table>';
|
||||||
|
print '</form>';
|
||||||
|
print '</div>';
|
||||||
|
|
||||||
|
// Help box for field options
|
||||||
|
print '<div class="info" style="margin-top:15px;">';
|
||||||
|
print '<p><strong><i class="fa fa-info-circle"></i> Hilfe: Feld-Optionen nach Feldtyp</strong></p>';
|
||||||
|
print '<table class="noborder" style="margin-top:10px;">';
|
||||||
|
print '<tr><td style="width:200px;"><strong>Textfeld (einzeilig)</strong></td><td>Keine Optionen nötig</td></tr>';
|
||||||
|
print '<tr><td><strong>Textfeld (mehrzeilig)</strong></td><td>Keine Optionen nötig</td></tr>';
|
||||||
|
print '<tr><td><strong>Zahlenfeld</strong></td><td>Optional: <code>min:0|max:100|step:0.1</code></td></tr>';
|
||||||
|
print '<tr><td><strong>Dropdown-Auswahl</strong></td><td><span style="color:#c00;">Pflicht!</span> Optionen mit <code>|</code> trennen, z.B.: <code>Option A|Option B|Option C</code></td></tr>';
|
||||||
|
print '<tr><td><strong>Datumsfeld</strong></td><td>Keine Optionen nötig</td></tr>';
|
||||||
|
print '<tr><td><strong>Checkbox (Ja/Nein)</strong></td><td>Keine Optionen nötig</td></tr>';
|
||||||
|
print '</table>';
|
||||||
|
print '</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// System filter
|
||||||
|
print '<form method="GET" action="'.$_SERVER['PHP_SELF'].'" style="margin-bottom:15px;">';
|
||||||
|
print $langs->trans('FilterBySystem').': ';
|
||||||
|
print '<select name="system" class="flat" onchange="this.form.submit();">';
|
||||||
|
print '<option value="0">'.$langs->trans('All').'</option>';
|
||||||
|
foreach ($systems as $sys) {
|
||||||
|
if ($sys->code === 'GLOBAL') continue; // Gebaeude-Typen haben eigenen Tab
|
||||||
|
$sel = ($systemFilter == $sys->rowid) ? ' selected' : '';
|
||||||
|
print '<option value="'.$sys->rowid.'"'.$sel.'>'.dol_escape_htmltag($sys->label).'</option>';
|
||||||
|
}
|
||||||
|
print '</select>';
|
||||||
|
print '</form>';
|
||||||
|
|
||||||
|
// Add button
|
||||||
|
print '<div style="margin-bottom:15px;">';
|
||||||
|
print '<a class="button" href="'.$_SERVER['PHP_SELF'].'?action=create&system='.$systemFilter.'">';
|
||||||
|
print '<i class="fa fa-plus"></i> '.$langs->trans('AddType');
|
||||||
|
print '</a>';
|
||||||
|
print '</div>';
|
||||||
|
|
||||||
|
// List
|
||||||
|
$types = $anlageType->fetchAllBySystem($systemFilter, 0, 1);
|
||||||
|
|
||||||
|
print '<table class="noborder centpercent">';
|
||||||
|
print '<tr class="liste_titre">';
|
||||||
|
print '<th>'.$langs->trans('TypeRef').'</th>';
|
||||||
|
print '<th>'.$langs->trans('TypeLabel').'</th>';
|
||||||
|
print '<th>'.$langs->trans('System').'</th>';
|
||||||
|
print '<th class="center">'.$langs->trans('CanHaveChildren').'</th>';
|
||||||
|
print '<th class="center">'.$langs->trans('Position').'</th>';
|
||||||
|
print '<th class="center">'.$langs->trans('Status').'</th>';
|
||||||
|
print '<th class="right">'.$langs->trans('Actions').'</th>';
|
||||||
|
print '</tr>';
|
||||||
|
|
||||||
|
foreach ($types as $type) {
|
||||||
|
print '<tr class="oddeven">';
|
||||||
|
|
||||||
|
print '<td>';
|
||||||
|
if ($type->picto) {
|
||||||
|
print kundenkarte_render_icon($type->picto).' ';
|
||||||
|
}
|
||||||
|
print dol_escape_htmltag($type->ref).'</td>';
|
||||||
|
|
||||||
|
print '<td>'.dol_escape_htmltag($type->label);
|
||||||
|
if ($type->label_short) {
|
||||||
|
print ' <span class="opacitymedium">('.$type->label_short.')</span>';
|
||||||
|
}
|
||||||
|
print '</td>';
|
||||||
|
|
||||||
|
if (empty($type->fk_system)) {
|
||||||
|
print '<td><span class="badge badge-info">'.$langs->trans('AllSystems').'</span></td>';
|
||||||
|
} else {
|
||||||
|
print '<td>'.dol_escape_htmltag($type->system_label).'</td>';
|
||||||
|
}
|
||||||
|
print '<td class="center">'.($type->can_have_children ? img_picto('', 'tick') : '').'</td>';
|
||||||
|
print '<td class="center">'.$type->position.'</td>';
|
||||||
|
|
||||||
|
print '<td class="center">';
|
||||||
|
if ($type->active) {
|
||||||
|
print '<a href="'.$_SERVER['PHP_SELF'].'?action=deactivate&typeid='.$type->id.'&system='.$systemFilter.'&token='.newToken().'">'.img_picto($langs->trans('Enabled'), 'switch_on').'</a>';
|
||||||
|
} else {
|
||||||
|
print '<a href="'.$_SERVER['PHP_SELF'].'?action=activate&typeid='.$type->id.'&system='.$systemFilter.'&token='.newToken().'">'.img_picto($langs->trans('Disabled'), 'switch_off').'</a>';
|
||||||
|
}
|
||||||
|
print '</td>';
|
||||||
|
|
||||||
|
print '<td class="right nowraponall">';
|
||||||
|
print '<div style="display:inline-flex;gap:8px;justify-content:flex-end;">';
|
||||||
|
print '<a class="button buttongen" style="width:32px;height:32px;padding:0;display:inline-flex;align-items:center;justify-content:center;" href="'.$_SERVER['PHP_SELF'].'?action=edit&typeid='.$type->id.'&system='.$systemFilter.'" title="'.$langs->trans('Edit').'"><i class="fas fa-pen"></i></a>';
|
||||||
|
print '<a class="button buttongen" style="width:32px;height:32px;padding:0;display:inline-flex;align-items:center;justify-content:center;" href="'.$_SERVER['PHP_SELF'].'?action=copy&typeid='.$type->id.'&system='.$systemFilter.'&token='.newToken().'" title="'.$langs->trans('Copy').'"><i class="fas fa-copy"></i></a>';
|
||||||
|
if (!$type->is_system) {
|
||||||
|
print '<a class="button buttongen" style="width:32px;height:32px;padding:0;display:inline-flex;align-items:center;justify-content:center;background:#dc3545;border-color:#dc3545;color:#fff;" href="'.$_SERVER['PHP_SELF'].'?action=delete&typeid='.$type->id.'&system='.$systemFilter.'" title="'.$langs->trans('Delete').'" onclick="return confirm(\''.$langs->trans('ConfirmDelete').'\');"><i class="fas fa-trash"></i></a>';
|
||||||
|
}
|
||||||
|
print '</div>';
|
||||||
|
print '</td>';
|
||||||
|
|
||||||
|
print '</tr>';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($types)) {
|
||||||
|
print '<tr class="oddeven"><td colspan="7" class="opacitymedium">'.$langs->trans('NoRecords').'</td></tr>';
|
||||||
|
}
|
||||||
|
|
||||||
|
print '</table>';
|
||||||
|
}
|
||||||
|
|
||||||
|
print dol_get_fiche_end();
|
||||||
|
|
||||||
|
// JavaScript for parent type selector
|
||||||
|
print '<script>
|
||||||
|
$(document).ready(function() {
|
||||||
|
var $hidden = $("#allowed_parent_types");
|
||||||
|
var $select = $("#parent_type_select");
|
||||||
|
var $container = $("#selected_parent_types");
|
||||||
|
|
||||||
|
// Initialize from existing value
|
||||||
|
function initSelected() {
|
||||||
|
var val = $hidden.val();
|
||||||
|
$container.empty();
|
||||||
|
if (val) {
|
||||||
|
var types = val.split(",");
|
||||||
|
types.forEach(function(ref) {
|
||||||
|
ref = ref.trim();
|
||||||
|
if (ref) {
|
||||||
|
var $opt = $select.find("option[value=\"" + ref + "\"]");
|
||||||
|
var label = $opt.length ? $opt.data("label") : ref;
|
||||||
|
addTag(ref, label);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add a tag to the list
|
||||||
|
function addTag(ref, label) {
|
||||||
|
var $tag = $("<span class=\"kundenkarte-tag\">");
|
||||||
|
$tag.text(ref + (label ? " (" + label + ")" : ""));
|
||||||
|
var $remove = $("<span class=\"kundenkarte-tag-remove\">×</span>");
|
||||||
|
$remove.on("click", function() {
|
||||||
|
$tag.remove();
|
||||||
|
updateHidden();
|
||||||
|
});
|
||||||
|
$tag.append($remove);
|
||||||
|
$container.append($tag);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update hidden field from tags
|
||||||
|
function updateHidden() {
|
||||||
|
var refs = [];
|
||||||
|
$container.find(".kundenkarte-tag").each(function() {
|
||||||
|
var text = $(this).text();
|
||||||
|
var ref = text.split(" (")[0].trim();
|
||||||
|
refs.push(ref);
|
||||||
|
});
|
||||||
|
$hidden.val(refs.join(","));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add button click
|
||||||
|
$("#add_parent_type_btn").on("click", function() {
|
||||||
|
var ref = $select.val();
|
||||||
|
if (!ref) return;
|
||||||
|
|
||||||
|
// Check if already added
|
||||||
|
var current = $hidden.val();
|
||||||
|
if (current && current.split(",").indexOf(ref) >= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var label = $select.find("option:selected").data("label");
|
||||||
|
addTag(ref, label);
|
||||||
|
updateHidden();
|
||||||
|
$select.val("");
|
||||||
|
});
|
||||||
|
|
||||||
|
// Filter parent type options when system changes
|
||||||
|
$("select[name=fk_system]").on("change", function() {
|
||||||
|
var selectedSystem = $(this).val();
|
||||||
|
$select.find("option").each(function() {
|
||||||
|
var optSystem = $(this).data("system");
|
||||||
|
if ($(this).val() === "") {
|
||||||
|
// Keep the placeholder option visible
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show option if:
|
||||||
|
// 1. Selected system is 0 (all systems) - show all options
|
||||||
|
// 2. Option system is 0 (global type) - always show
|
||||||
|
// 3. Option system matches selected system
|
||||||
|
var show = false;
|
||||||
|
if (selectedSystem == "0" || selectedSystem === "") {
|
||||||
|
show = true;
|
||||||
|
} else if (optSystem == 0 || optSystem === "" || optSystem === undefined) {
|
||||||
|
show = true;
|
||||||
|
} else if (optSystem == selectedSystem) {
|
||||||
|
show = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (show) {
|
||||||
|
$(this).show();
|
||||||
|
} else {
|
||||||
|
$(this).hide();
|
||||||
|
// Deselect if hidden
|
||||||
|
if ($(this).is(":selected")) {
|
||||||
|
$select.val("");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
initSelected();
|
||||||
|
|
||||||
|
// Trigger initial filtering
|
||||||
|
$("select[name=fk_system]").trigger("change");
|
||||||
|
});
|
||||||
|
</script>';
|
||||||
|
|
||||||
|
llxFooter();
|
||||||
|
$db->close();
|
||||||
364
admin/backup.php
Executable file
364
admin/backup.php
Executable 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();
|
||||||
351
admin/building_types.php
Executable file
351
admin/building_types.php
Executable file
|
|
@ -0,0 +1,351 @@
|
||||||
|
<?php
|
||||||
|
/* Copyright (C) 2026 Alles Watt lauft
|
||||||
|
*
|
||||||
|
* Admin page for Building Types (Gebäudetypen)
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 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");
|
||||||
|
|
||||||
|
require_once DOL_DOCUMENT_ROOT.'/core/lib/admin.lib.php';
|
||||||
|
require_once DOL_DOCUMENT_ROOT.'/core/class/html.formadmin.class.php';
|
||||||
|
dol_include_once('/kundenkarte/lib/kundenkarte.lib.php');
|
||||||
|
dol_include_once('/kundenkarte/class/buildingtype.class.php');
|
||||||
|
|
||||||
|
// Load translation files
|
||||||
|
$langs->loadLangs(array('admin', 'kundenkarte@kundenkarte'));
|
||||||
|
|
||||||
|
// Security check
|
||||||
|
if (!$user->admin) {
|
||||||
|
accessforbidden();
|
||||||
|
}
|
||||||
|
|
||||||
|
$action = GETPOST('action', 'aZ09');
|
||||||
|
$confirm = GETPOST('confirm', 'alpha');
|
||||||
|
$id = GETPOSTINT('id');
|
||||||
|
$levelFilter = GETPOST('level_filter', 'alpha');
|
||||||
|
|
||||||
|
$buildingType = new BuildingType($db);
|
||||||
|
$error = 0;
|
||||||
|
|
||||||
|
// Actions
|
||||||
|
if ($action == 'add' && $user->admin) {
|
||||||
|
$buildingType->ref = GETPOST('ref', 'alphanohtml');
|
||||||
|
$buildingType->label = GETPOST('label', 'alphanohtml');
|
||||||
|
$buildingType->label_short = GETPOST('label_short', 'alphanohtml');
|
||||||
|
$buildingType->description = GETPOST('description', 'restricthtml');
|
||||||
|
$buildingType->fk_parent = GETPOSTINT('fk_parent');
|
||||||
|
$buildingType->level_type = GETPOST('level_type', 'alpha');
|
||||||
|
$buildingType->icon = GETPOST('icon', 'alphanohtml');
|
||||||
|
$buildingType->color = GETPOST('color', 'alphanohtml');
|
||||||
|
$buildingType->can_have_children = GETPOSTINT('can_have_children');
|
||||||
|
$buildingType->position = GETPOSTINT('position');
|
||||||
|
$buildingType->active = GETPOSTINT('active');
|
||||||
|
|
||||||
|
if (empty($buildingType->ref)) {
|
||||||
|
setEventMessages($langs->trans('ErrorFieldRequired', $langs->transnoentitiesaliases('Ref')), null, 'errors');
|
||||||
|
$error++;
|
||||||
|
}
|
||||||
|
if (empty($buildingType->label)) {
|
||||||
|
setEventMessages($langs->trans('ErrorFieldRequired', $langs->transnoentitiesaliases('Label')), null, 'errors');
|
||||||
|
$error++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$error) {
|
||||||
|
$result = $buildingType->create($user);
|
||||||
|
if ($result > 0) {
|
||||||
|
setEventMessages($langs->trans('RecordCreatedSuccessfully'), null, 'mesgs');
|
||||||
|
header('Location: '.$_SERVER['PHP_SELF']);
|
||||||
|
exit;
|
||||||
|
} else {
|
||||||
|
setEventMessages($buildingType->error, $buildingType->errors, 'errors');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$action = 'create';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($action == 'update' && $user->admin) {
|
||||||
|
$result = $buildingType->fetch($id);
|
||||||
|
if ($result > 0) {
|
||||||
|
// Don't allow editing ref of system types
|
||||||
|
if (!$buildingType->is_system) {
|
||||||
|
$buildingType->ref = GETPOST('ref', 'alphanohtml');
|
||||||
|
}
|
||||||
|
$buildingType->label = GETPOST('label', 'alphanohtml');
|
||||||
|
$buildingType->label_short = GETPOST('label_short', 'alphanohtml');
|
||||||
|
$buildingType->description = GETPOST('description', 'restricthtml');
|
||||||
|
$buildingType->fk_parent = GETPOSTINT('fk_parent');
|
||||||
|
$buildingType->level_type = GETPOST('level_type', 'alpha');
|
||||||
|
$buildingType->icon = GETPOST('icon', 'alphanohtml');
|
||||||
|
$buildingType->color = GETPOST('color', 'alphanohtml');
|
||||||
|
$buildingType->can_have_children = GETPOSTINT('can_have_children');
|
||||||
|
$buildingType->position = GETPOSTINT('position');
|
||||||
|
$buildingType->active = GETPOSTINT('active');
|
||||||
|
|
||||||
|
$result = $buildingType->update($user);
|
||||||
|
if ($result > 0) {
|
||||||
|
setEventMessages($langs->trans('RecordModifiedSuccessfully'), null, 'mesgs');
|
||||||
|
header('Location: '.$_SERVER['PHP_SELF']);
|
||||||
|
exit;
|
||||||
|
} else {
|
||||||
|
setEventMessages($buildingType->error, $buildingType->errors, 'errors');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($action == 'confirm_delete' && $confirm == 'yes' && $user->admin) {
|
||||||
|
$result = $buildingType->fetch($id);
|
||||||
|
if ($result > 0) {
|
||||||
|
$result = $buildingType->delete($user);
|
||||||
|
if ($result > 0) {
|
||||||
|
setEventMessages($langs->trans('RecordDeleted'), null, 'mesgs');
|
||||||
|
} else {
|
||||||
|
setEventMessages($langs->trans($buildingType->error), $buildingType->errors, 'errors');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
header('Location: '.$_SERVER['PHP_SELF']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load data for edit
|
||||||
|
if (($action == 'edit' || $action == 'delete') && $id > 0) {
|
||||||
|
$result = $buildingType->fetch($id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* View
|
||||||
|
*/
|
||||||
|
|
||||||
|
$page_name = "BuildingTypesSetup";
|
||||||
|
llxHeader('', $langs->trans($page_name), '', '', 0, 0, '', '', '', 'mod-kundenkarte page-admin-building_types');
|
||||||
|
|
||||||
|
$linkback = '<a href="'.DOL_URL_ROOT.'/admin/modules.php?restore_lastsearch_values=1">'.$langs->trans("BackToModuleList").'</a>';
|
||||||
|
print load_fiche_titre($langs->trans($page_name), $linkback, 'object_kundenkarte@kundenkarte');
|
||||||
|
|
||||||
|
print '<div class="fichecenter">';
|
||||||
|
|
||||||
|
$head = kundenkarteAdminPrepareHead();
|
||||||
|
print dol_get_fiche_head($head, 'building_types', $langs->trans("Module500015Name"), -1, 'kundenkarte@kundenkarte');
|
||||||
|
|
||||||
|
// Delete confirmation
|
||||||
|
if ($action == 'delete') {
|
||||||
|
print $form->formconfirm(
|
||||||
|
$_SERVER['PHP_SELF'].'?id='.$buildingType->id,
|
||||||
|
$langs->trans('DeleteBuildingType'),
|
||||||
|
$langs->trans('ConfirmDeleteBuildingType', $buildingType->label),
|
||||||
|
'confirm_delete',
|
||||||
|
'',
|
||||||
|
0,
|
||||||
|
1
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Level type filter
|
||||||
|
$levelTypes = BuildingType::getLevelTypes();
|
||||||
|
print '<div class="div-table-responsive-no-min">';
|
||||||
|
print '<form method="get" action="'.$_SERVER['PHP_SELF'].'">';
|
||||||
|
print '<div class="inline-block valignmiddle" style="margin-bottom: 10px;">';
|
||||||
|
print '<label for="level_filter">'.$langs->trans('FilterByLevel').': </label>';
|
||||||
|
print '<select name="level_filter" id="level_filter" class="flat minwidth200" onchange="this.form.submit()">';
|
||||||
|
print '<option value="">'.$langs->trans('All').'</option>';
|
||||||
|
foreach ($levelTypes as $code => $label) {
|
||||||
|
$selected = ($levelFilter == $code) ? ' selected' : '';
|
||||||
|
print '<option value="'.$code.'"'.$selected.'>'.$label.'</option>';
|
||||||
|
}
|
||||||
|
print '</select>';
|
||||||
|
print '</div>';
|
||||||
|
print '</form>';
|
||||||
|
print '</div>';
|
||||||
|
|
||||||
|
// Add/Edit form
|
||||||
|
if ($action == 'create' || $action == 'edit') {
|
||||||
|
print '<form method="POST" action="'.$_SERVER['PHP_SELF'].'">';
|
||||||
|
print '<input type="hidden" name="token" value="'.newToken().'">';
|
||||||
|
print '<input type="hidden" name="action" value="'.($action == 'edit' ? 'update' : 'add').'">';
|
||||||
|
if ($action == 'edit') {
|
||||||
|
print '<input type="hidden" name="id" value="'.$buildingType->id.'">';
|
||||||
|
}
|
||||||
|
|
||||||
|
print '<table class="border centpercent tableforfield">';
|
||||||
|
|
||||||
|
// Ref
|
||||||
|
print '<tr><td class="titlefieldcreate fieldrequired">'.$langs->trans('Ref').'</td><td>';
|
||||||
|
if ($action == 'edit' && $buildingType->is_system) {
|
||||||
|
print '<input type="hidden" name="ref" value="'.$buildingType->ref.'">';
|
||||||
|
print $buildingType->ref.' <span class="opacitymedium">('.$langs->trans('SystemType').')</span>';
|
||||||
|
} else {
|
||||||
|
print '<input type="text" name="ref" class="flat minwidth200" value="'.($buildingType->ref ?: '').'" maxlength="50">';
|
||||||
|
}
|
||||||
|
print '</td></tr>';
|
||||||
|
|
||||||
|
// Label
|
||||||
|
print '<tr><td class="fieldrequired">'.$langs->trans('Label').'</td><td>';
|
||||||
|
print '<input type="text" name="label" class="flat minwidth300" value="'.dol_escape_htmltag($buildingType->label ?: '').'">';
|
||||||
|
print '</td></tr>';
|
||||||
|
|
||||||
|
// Label Short
|
||||||
|
print '<tr><td>'.$langs->trans('LabelShort').'</td><td>';
|
||||||
|
print '<input type="text" name="label_short" class="flat minwidth150" value="'.dol_escape_htmltag($buildingType->label_short ?: '').'" maxlength="32">';
|
||||||
|
print '</td></tr>';
|
||||||
|
|
||||||
|
// Level Type
|
||||||
|
print '<tr><td>'.$langs->trans('LevelType').'</td><td>';
|
||||||
|
print '<select name="level_type" class="flat minwidth200">';
|
||||||
|
print '<option value="">--</option>';
|
||||||
|
foreach ($levelTypes as $code => $label) {
|
||||||
|
$selected = ($buildingType->level_type == $code) ? ' selected' : '';
|
||||||
|
print '<option value="'.$code.'"'.$selected.'>'.$label.'</option>';
|
||||||
|
}
|
||||||
|
print '</select>';
|
||||||
|
print '</td></tr>';
|
||||||
|
|
||||||
|
// Icon
|
||||||
|
print '<tr><td>'.$langs->trans('Icon').'</td><td>';
|
||||||
|
print '<input type="text" name="icon" class="flat minwidth200" value="'.dol_escape_htmltag($buildingType->icon ?: '').'" placeholder="fa-home">';
|
||||||
|
if ($buildingType->icon) {
|
||||||
|
print ' <i class="fas '.$buildingType->icon.'"></i>';
|
||||||
|
}
|
||||||
|
print ' <span class="opacitymedium">(FontAwesome, z.B. fa-home, fa-building)</span>';
|
||||||
|
print '</td></tr>';
|
||||||
|
|
||||||
|
// Color
|
||||||
|
print '<tr><td>'.$langs->trans('Color').'</td><td>';
|
||||||
|
print '<input type="color" name="color" id="color_picker" value="'.($buildingType->color ?: '#3498db').'" style="width:50px;height:30px;vertical-align:middle;">';
|
||||||
|
print ' <input type="text" name="color_text" id="color_text" class="flat" value="'.($buildingType->color ?: '#3498db').'" size="10" onchange="document.getElementById(\'color_picker\').value=this.value;">';
|
||||||
|
print '</td></tr>';
|
||||||
|
|
||||||
|
// Can have children
|
||||||
|
print '<tr><td>'.$langs->trans('CanHaveChildren').'</td><td>';
|
||||||
|
print '<input type="checkbox" name="can_have_children" value="1"'.($buildingType->can_have_children || $action != 'edit' ? ' checked' : '').'>';
|
||||||
|
print '</td></tr>';
|
||||||
|
|
||||||
|
// Position
|
||||||
|
print '<tr><td>'.$langs->trans('Position').'</td><td>';
|
||||||
|
$defaultPos = $action == 'create' ? $buildingType->getNextPosition() : $buildingType->position;
|
||||||
|
print '<input type="number" name="position" class="flat" value="'.(int)$defaultPos.'" min="0" step="10">';
|
||||||
|
print '</td></tr>';
|
||||||
|
|
||||||
|
// Active
|
||||||
|
print '<tr><td>'.$langs->trans('Active').'</td><td>';
|
||||||
|
print '<input type="checkbox" name="active" value="1"'.($buildingType->active || $action != 'edit' ? ' checked' : '').'>';
|
||||||
|
print '</td></tr>';
|
||||||
|
|
||||||
|
// Description
|
||||||
|
print '<tr><td>'.$langs->trans('Description').'</td><td>';
|
||||||
|
print '<textarea name="description" class="flat" rows="3" cols="60">'.$buildingType->description.'</textarea>';
|
||||||
|
print '</td></tr>';
|
||||||
|
|
||||||
|
print '</table>';
|
||||||
|
|
||||||
|
print '<div class="center" style="margin-top: 10px;">';
|
||||||
|
print '<input type="submit" class="button button-save" value="'.$langs->trans('Save').'">';
|
||||||
|
print ' <a class="button button-cancel" href="'.$_SERVER['PHP_SELF'].'">'.$langs->trans('Cancel').'</a>';
|
||||||
|
print '</div>';
|
||||||
|
|
||||||
|
print '</form>';
|
||||||
|
|
||||||
|
// Sync color inputs
|
||||||
|
print '<script>
|
||||||
|
document.getElementById("color_picker").addEventListener("input", function() {
|
||||||
|
document.getElementById("color_text").value = this.value;
|
||||||
|
});
|
||||||
|
</script>';
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// List of building types
|
||||||
|
print '<div class="div-table-responsive-no-min">';
|
||||||
|
print '<table class="noborder centpercent">';
|
||||||
|
|
||||||
|
// Header
|
||||||
|
print '<tr class="liste_titre">';
|
||||||
|
print '<td>'.$langs->trans('Ref').'</td>';
|
||||||
|
print '<td>'.$langs->trans('Label').'</td>';
|
||||||
|
print '<td>'.$langs->trans('LevelType').'</td>';
|
||||||
|
print '<td class="center">'.$langs->trans('Icon').'</td>';
|
||||||
|
print '<td class="center">'.$langs->trans('Color').'</td>';
|
||||||
|
print '<td class="center">'.$langs->trans('Position').'</td>';
|
||||||
|
print '<td class="center">'.$langs->trans('Active').'</td>';
|
||||||
|
print '<td class="center">'.$langs->trans('Actions').'</td>';
|
||||||
|
print '</tr>';
|
||||||
|
|
||||||
|
// Fetch types
|
||||||
|
$types = $buildingType->fetchAll(0, $levelFilter);
|
||||||
|
|
||||||
|
if (count($types) > 0) {
|
||||||
|
foreach ($types as $type) {
|
||||||
|
print '<tr class="oddeven">';
|
||||||
|
|
||||||
|
// Ref
|
||||||
|
print '<td>'.$type->ref;
|
||||||
|
if ($type->is_system) {
|
||||||
|
print ' <span class="badge badge-secondary">'.$langs->trans('System').'</span>';
|
||||||
|
}
|
||||||
|
print '</td>';
|
||||||
|
|
||||||
|
// Label
|
||||||
|
print '<td>';
|
||||||
|
if ($type->icon) {
|
||||||
|
print '<i class="fas '.$type->icon.'" style="color:'.$type->color.';margin-right:5px;"></i> ';
|
||||||
|
}
|
||||||
|
print dol_escape_htmltag($type->label);
|
||||||
|
if ($type->label_short) {
|
||||||
|
print ' <span class="opacitymedium">('.$type->label_short.')</span>';
|
||||||
|
}
|
||||||
|
print '</td>';
|
||||||
|
|
||||||
|
// Level Type
|
||||||
|
print '<td>'.$type->getLevelTypeLabel().'</td>';
|
||||||
|
|
||||||
|
// Icon
|
||||||
|
print '<td class="center">';
|
||||||
|
if ($type->icon) {
|
||||||
|
print '<i class="fas '.$type->icon.'"></i> '.$type->icon;
|
||||||
|
}
|
||||||
|
print '</td>';
|
||||||
|
|
||||||
|
// Color
|
||||||
|
print '<td class="center">';
|
||||||
|
if ($type->color) {
|
||||||
|
print '<span style="display:inline-block;width:20px;height:20px;background:'.$type->color.';border:1px solid #ccc;border-radius:3px;vertical-align:middle;"></span> '.$type->color;
|
||||||
|
}
|
||||||
|
print '</td>';
|
||||||
|
|
||||||
|
// Position
|
||||||
|
print '<td class="center">'.$type->position.'</td>';
|
||||||
|
|
||||||
|
// Active
|
||||||
|
print '<td class="center">';
|
||||||
|
print $type->active ? '<span class="badge badge-status4">'.$langs->trans('Yes').'</span>' : '<span class="badge badge-status5">'.$langs->trans('No').'</span>';
|
||||||
|
print '</td>';
|
||||||
|
|
||||||
|
// Actions
|
||||||
|
print '<td class="center nowrap">';
|
||||||
|
print '<a class="editfielda" href="'.$_SERVER['PHP_SELF'].'?action=edit&id='.$type->id.'">'.img_edit().'</a>';
|
||||||
|
if (!$type->is_system) {
|
||||||
|
print ' <a class="deletefielda" href="'.$_SERVER['PHP_SELF'].'?action=delete&id='.$type->id.'">'.img_delete().'</a>';
|
||||||
|
}
|
||||||
|
print '</td>';
|
||||||
|
|
||||||
|
print '</tr>';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
print '<tr class="oddeven"><td colspan="8" class="opacitymedium">'.$langs->trans('NoRecordFound').'</td></tr>';
|
||||||
|
}
|
||||||
|
|
||||||
|
print '</table>';
|
||||||
|
print '</div>';
|
||||||
|
|
||||||
|
// Add button
|
||||||
|
print '<div class="tabsAction">';
|
||||||
|
print '<a class="butAction" href="'.$_SERVER['PHP_SELF'].'?action=create">'.$langs->trans('AddBuildingType').'</a>';
|
||||||
|
print '</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
print dol_get_fiche_end();
|
||||||
|
print '</div>';
|
||||||
|
|
||||||
|
llxFooter();
|
||||||
|
$db->close();
|
||||||
460
admin/busbar_types.php
Executable file
460
admin/busbar_types.php
Executable file
|
|
@ -0,0 +1,460 @@
|
||||||
|
<?php
|
||||||
|
/* Copyright (C) 2026 Alles Watt lauft
|
||||||
|
*
|
||||||
|
* Admin page to manage busbar types (Sammelschienen-Typen)
|
||||||
|
*/
|
||||||
|
|
||||||
|
$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");
|
||||||
|
|
||||||
|
require_once DOL_DOCUMENT_ROOT.'/core/lib/admin.lib.php';
|
||||||
|
require_once DOL_DOCUMENT_ROOT.'/core/class/html.form.class.php';
|
||||||
|
dol_include_once('/kundenkarte/lib/kundenkarte.lib.php');
|
||||||
|
dol_include_once('/kundenkarte/class/busbartype.class.php');
|
||||||
|
|
||||||
|
$langs->loadLangs(array('admin', 'kundenkarte@kundenkarte', 'products'));
|
||||||
|
|
||||||
|
// Security check
|
||||||
|
if (!$user->admin && !$user->hasRight('kundenkarte', 'admin')) {
|
||||||
|
accessforbidden();
|
||||||
|
}
|
||||||
|
|
||||||
|
$action = GETPOST('action', 'aZ09');
|
||||||
|
$confirm = GETPOST('confirm', 'alpha');
|
||||||
|
$typeId = GETPOSTINT('typeid');
|
||||||
|
|
||||||
|
// System filter - save in session for persistence
|
||||||
|
$sessionKey = 'kundenkarte_busbar_types_system_filter';
|
||||||
|
if (GETPOSTISSET('system')) {
|
||||||
|
$systemFilter = GETPOSTINT('system');
|
||||||
|
$_SESSION[$sessionKey] = $systemFilter;
|
||||||
|
} elseif (isset($_SESSION[$sessionKey])) {
|
||||||
|
$systemFilter = $_SESSION[$sessionKey];
|
||||||
|
} else {
|
||||||
|
$systemFilter = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$form = new Form($db);
|
||||||
|
$busbarType = new BusbarType($db);
|
||||||
|
|
||||||
|
// Load systems
|
||||||
|
$systems = array();
|
||||||
|
$sql = "SELECT rowid, code, label FROM ".MAIN_DB_PREFIX."c_kundenkarte_anlage_system WHERE active = 1 ORDER BY position ASC";
|
||||||
|
$resql = $db->query($sql);
|
||||||
|
if ($resql) {
|
||||||
|
while ($obj = $db->fetch_object($resql)) {
|
||||||
|
$systems[$obj->rowid] = $obj;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load products for dropdown
|
||||||
|
$products = array();
|
||||||
|
$sql = "SELECT rowid, ref, label FROM ".MAIN_DB_PREFIX."product WHERE tosell = 1 ORDER BY ref ASC";
|
||||||
|
$resql = $db->query($sql);
|
||||||
|
if ($resql) {
|
||||||
|
while ($obj = $db->fetch_object($resql)) {
|
||||||
|
$products[$obj->rowid] = $obj;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Predefined channel configurations (system-independent)
|
||||||
|
$channelPresets = array(
|
||||||
|
// General presets
|
||||||
|
'A' => array('label' => 'Kanal A (1 Linie)', 'num_lines' => 1, 'colors' => '#e74c3c'),
|
||||||
|
'B' => array('label' => 'Kanal B (1 Linie)', 'num_lines' => 1, 'colors' => '#2ecc71'),
|
||||||
|
'C' => array('label' => 'Kanal C (1 Linie)', 'num_lines' => 1, 'colors' => '#9b59b6'),
|
||||||
|
'AB' => array('label' => 'Kanal A+B (2 Linien)', 'num_lines' => 2, 'colors' => '#e74c3c,#2ecc71'),
|
||||||
|
'ABC' => array('label' => 'Kanal A+B+C (3 Linien)', 'num_lines' => 3, 'colors' => '#e74c3c,#2ecc71,#9b59b6'),
|
||||||
|
'4CH' => array('label' => '4 Kanaele', 'num_lines' => 4, 'colors' => '#e74c3c,#2ecc71,#9b59b6,#3498db'),
|
||||||
|
'5CH' => array('label' => '5 Kanaele', 'num_lines' => 5, 'colors' => '#e74c3c,#2ecc71,#9b59b6,#3498db,#f1c40f'),
|
||||||
|
// Electrical presets (for backward compatibility)
|
||||||
|
'L1' => array('label' => 'L1 (Strom)', 'num_lines' => 1, 'colors' => '#e74c3c'),
|
||||||
|
'3P+N' => array('label' => '3P+N (Strom)', 'num_lines' => 4, 'colors' => '#e74c3c,#2ecc71,#9b59b6,#3498db'),
|
||||||
|
);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Actions
|
||||||
|
*/
|
||||||
|
|
||||||
|
if ($action == 'add') {
|
||||||
|
$busbarType->ref = GETPOST('ref', 'aZ09');
|
||||||
|
$busbarType->label = GETPOST('label', 'alphanohtml');
|
||||||
|
$busbarType->label_short = GETPOST('label_short', 'alphanohtml');
|
||||||
|
$busbarType->description = GETPOST('description', 'restricthtml');
|
||||||
|
$busbarType->fk_system = GETPOSTINT('fk_system');
|
||||||
|
$busbarType->phases = GETPOST('phases', 'alphanohtml');
|
||||||
|
$busbarType->num_lines = GETPOSTINT('num_lines');
|
||||||
|
$busbarType->color = GETPOST('color', 'alphanohtml');
|
||||||
|
$busbarType->default_color = GETPOST('default_color', 'alphanohtml');
|
||||||
|
$busbarType->line_height = GETPOSTINT('line_height') ?: 3;
|
||||||
|
$busbarType->line_spacing = GETPOSTINT('line_spacing') ?: 4;
|
||||||
|
$busbarType->position_default = GETPOST('position_default', 'alphanohtml') ?: 'below';
|
||||||
|
$busbarType->fk_product = GETPOSTINT('fk_product');
|
||||||
|
$busbarType->picto = GETPOST('picto', 'alphanohtml');
|
||||||
|
$busbarType->position = GETPOSTINT('position');
|
||||||
|
$busbarType->active = 1;
|
||||||
|
|
||||||
|
if (empty($busbarType->ref) || empty($busbarType->label) || empty($busbarType->phases)) {
|
||||||
|
setEventMessages($langs->trans('ErrorFieldRequired'), null, 'errors');
|
||||||
|
$action = 'create';
|
||||||
|
} else {
|
||||||
|
$result = $busbarType->create($user);
|
||||||
|
if ($result > 0) {
|
||||||
|
setEventMessages($langs->trans('RecordSaved'), null, 'mesgs');
|
||||||
|
header('Location: '.$_SERVER['PHP_SELF'].'?system='.$busbarType->fk_system);
|
||||||
|
exit;
|
||||||
|
} else {
|
||||||
|
setEventMessages($busbarType->error, $busbarType->errors, 'errors');
|
||||||
|
$action = 'create';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($action == 'update') {
|
||||||
|
$busbarType->fetch($typeId);
|
||||||
|
$busbarType->ref = GETPOST('ref', 'aZ09');
|
||||||
|
$busbarType->label = GETPOST('label', 'alphanohtml');
|
||||||
|
$busbarType->label_short = GETPOST('label_short', 'alphanohtml');
|
||||||
|
$busbarType->description = GETPOST('description', 'restricthtml');
|
||||||
|
$busbarType->fk_system = GETPOSTINT('fk_system');
|
||||||
|
$busbarType->phases = GETPOST('phases', 'alphanohtml');
|
||||||
|
$busbarType->num_lines = GETPOSTINT('num_lines');
|
||||||
|
$busbarType->color = GETPOST('color', 'alphanohtml');
|
||||||
|
$busbarType->default_color = GETPOST('default_color', 'alphanohtml');
|
||||||
|
$busbarType->line_height = GETPOSTINT('line_height') ?: 3;
|
||||||
|
$busbarType->line_spacing = GETPOSTINT('line_spacing') ?: 4;
|
||||||
|
$busbarType->position_default = GETPOST('position_default', 'alphanohtml') ?: 'below';
|
||||||
|
$busbarType->fk_product = GETPOSTINT('fk_product');
|
||||||
|
$busbarType->picto = GETPOST('picto', 'alphanohtml');
|
||||||
|
$busbarType->position = GETPOSTINT('position');
|
||||||
|
|
||||||
|
$result = $busbarType->update($user);
|
||||||
|
if ($result > 0) {
|
||||||
|
setEventMessages($langs->trans('RecordSaved'), null, 'mesgs');
|
||||||
|
header('Location: '.$_SERVER['PHP_SELF'].'?system='.$busbarType->fk_system);
|
||||||
|
exit;
|
||||||
|
} else {
|
||||||
|
setEventMessages($busbarType->error, $busbarType->errors, 'errors');
|
||||||
|
$action = 'edit';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($action == 'confirm_delete' && $confirm == 'yes') {
|
||||||
|
$busbarType->fetch($typeId);
|
||||||
|
$result = $busbarType->delete($user);
|
||||||
|
if ($result > 0) {
|
||||||
|
setEventMessages($langs->trans('RecordDeleted'), null, 'mesgs');
|
||||||
|
} else {
|
||||||
|
setEventMessages($busbarType->error, $busbarType->errors, 'errors');
|
||||||
|
}
|
||||||
|
$action = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($action == 'activate') {
|
||||||
|
$sql = "UPDATE ".MAIN_DB_PREFIX."kundenkarte_busbar_type SET active = 1 WHERE rowid = ".((int) $typeId);
|
||||||
|
$db->query($sql);
|
||||||
|
$action = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($action == 'deactivate') {
|
||||||
|
$sql = "UPDATE ".MAIN_DB_PREFIX."kundenkarte_busbar_type SET active = 0 WHERE rowid = ".((int) $typeId);
|
||||||
|
$db->query($sql);
|
||||||
|
$action = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* View
|
||||||
|
*/
|
||||||
|
|
||||||
|
llxHeader('', $langs->trans('BusbarTypes'));
|
||||||
|
|
||||||
|
$head = kundenkarteAdminPrepareHead();
|
||||||
|
print dol_get_fiche_head($head, 'busbar_types', $langs->trans('KundenkarteSetup'), -1, 'kundenkarte@kundenkarte');
|
||||||
|
|
||||||
|
// System filter
|
||||||
|
print '<div class="tabBar" style="margin-bottom:15px;">';
|
||||||
|
print '<form method="get" action="'.$_SERVER['PHP_SELF'].'">';
|
||||||
|
print $langs->trans('System').': ';
|
||||||
|
print '<select name="system" class="flat" onchange="this.form.submit()">';
|
||||||
|
print '<option value="0">'.$langs->trans('All').'</option>';
|
||||||
|
foreach ($systems as $sys) {
|
||||||
|
$selected = ($systemFilter == $sys->rowid) ? ' selected' : '';
|
||||||
|
print '<option value="'.$sys->rowid.'"'.$selected.'>'.dol_escape_htmltag($sys->label).'</option>';
|
||||||
|
}
|
||||||
|
print '</select>';
|
||||||
|
print ' <button type="submit" class="button">'.$langs->trans('Filter').'</button>';
|
||||||
|
print ' <a class="button" href="'.$_SERVER['PHP_SELF'].'?action=create&system='.$systemFilter.'">'.$langs->trans('NewBusbarType').'</a>';
|
||||||
|
print '</form>';
|
||||||
|
print '</div>';
|
||||||
|
|
||||||
|
// Delete confirmation
|
||||||
|
if ($action == 'delete') {
|
||||||
|
$busbarType->fetch($typeId);
|
||||||
|
print $form->formconfirm(
|
||||||
|
$_SERVER['PHP_SELF'].'?typeid='.$typeId.'&system='.$systemFilter,
|
||||||
|
$langs->trans('DeleteBusbarType'),
|
||||||
|
$langs->trans('ConfirmDeleteBusbarType', $busbarType->label),
|
||||||
|
'confirm_delete',
|
||||||
|
'',
|
||||||
|
0,
|
||||||
|
1
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create/Edit form
|
||||||
|
if ($action == 'create' || $action == 'edit') {
|
||||||
|
if ($action == 'edit') {
|
||||||
|
$busbarType->fetch($typeId);
|
||||||
|
}
|
||||||
|
|
||||||
|
print '<form method="post" action="'.$_SERVER['PHP_SELF'].'">';
|
||||||
|
print '<input type="hidden" name="token" value="'.newToken().'">';
|
||||||
|
print '<input type="hidden" name="action" value="'.($action == 'edit' ? 'update' : 'add').'">';
|
||||||
|
if ($action == 'edit') {
|
||||||
|
print '<input type="hidden" name="typeid" value="'.$busbarType->id.'">';
|
||||||
|
}
|
||||||
|
|
||||||
|
print '<table class="border centpercent">';
|
||||||
|
|
||||||
|
// Ref
|
||||||
|
print '<tr><td class="fieldrequired">'.$langs->trans('Ref').'</td>';
|
||||||
|
print '<td><input type="text" name="ref" class="flat minwidth200" value="'.dol_escape_htmltag($busbarType->ref).'" required></td></tr>';
|
||||||
|
|
||||||
|
// Label
|
||||||
|
print '<tr><td class="fieldrequired">'.$langs->trans('Label').'</td>';
|
||||||
|
print '<td><input type="text" name="label" class="flat minwidth300" value="'.dol_escape_htmltag($busbarType->label).'" required></td></tr>';
|
||||||
|
|
||||||
|
// Label Short
|
||||||
|
print '<tr><td>'.$langs->trans('LabelShort').'</td>';
|
||||||
|
print '<td><input type="text" name="label_short" class="flat minwidth100" value="'.dol_escape_htmltag($busbarType->label_short).'" maxlength="32"></td></tr>';
|
||||||
|
|
||||||
|
// Description
|
||||||
|
print '<tr><td>'.$langs->trans('Description').'</td>';
|
||||||
|
print '<td><textarea name="description" class="flat" rows="3" cols="60">'.dol_escape_htmltag($busbarType->description).'</textarea></td></tr>';
|
||||||
|
|
||||||
|
// System
|
||||||
|
print '<tr><td>'.$langs->trans('System').'</td>';
|
||||||
|
print '<td><select name="fk_system" class="flat">';
|
||||||
|
$selectedAll = (empty($busbarType->fk_system) && $action == 'edit') || ($action == 'create' && $systemFilter == 0) ? ' selected' : '';
|
||||||
|
print '<option value="0"'.$selectedAll.'>'.$langs->trans('AllSystems').'</option>';
|
||||||
|
foreach ($systems as $sys) {
|
||||||
|
$selected = ($busbarType->fk_system == $sys->rowid || ($action == 'create' && $systemFilter == $sys->rowid && $systemFilter > 0)) ? ' selected' : '';
|
||||||
|
print '<option value="'.$sys->rowid.'"'.$selected.'>'.dol_escape_htmltag($sys->label).'</option>';
|
||||||
|
}
|
||||||
|
print '</select>';
|
||||||
|
print '<div class="opacitymedium small">'.$langs->trans('AllSystemsHint').'</div>';
|
||||||
|
print '</td></tr>';
|
||||||
|
|
||||||
|
// Channel configuration
|
||||||
|
print '<tr><td class="fieldrequired">'.$langs->trans('Channels').'</td>';
|
||||||
|
print '<td>';
|
||||||
|
print '<div style="margin-bottom:10px;">';
|
||||||
|
print '<strong>'.$langs->trans('QuickSelect').':</strong><br>';
|
||||||
|
foreach ($channelPresets as $code => $preset) {
|
||||||
|
$style = 'display:inline-block;margin:3px;padding:5px 10px;border:1px solid #ccc;border-radius:4px;cursor:pointer;background:#f8f8f8;';
|
||||||
|
print '<span class="channel-preset" data-channels="'.$code.'" data-numlines="'.$preset['num_lines'].'" data-colors="'.$preset['colors'].'" style="'.$style.'">';
|
||||||
|
print dol_escape_htmltag($preset['label']);
|
||||||
|
print '</span>';
|
||||||
|
}
|
||||||
|
print '</div>';
|
||||||
|
print '<input type="text" name="phases" id="channels-input" class="flat minwidth150" value="'.dol_escape_htmltag($busbarType->phases).'" placeholder="z.B. A, AB, ABC" required>';
|
||||||
|
print '<div class="opacitymedium small">'.$langs->trans('ChannelsHint').'</div>';
|
||||||
|
print '</td></tr>';
|
||||||
|
|
||||||
|
// Number of lines
|
||||||
|
print '<tr><td>'.$langs->trans('NumLines').'</td>';
|
||||||
|
print '<td><input type="number" name="num_lines" id="numlines-input" class="flat" value="'.($busbarType->num_lines ?: 1).'" min="1" max="10"></td></tr>';
|
||||||
|
|
||||||
|
// Colors
|
||||||
|
print '<tr><td>'.$langs->trans('Colors').'</td>';
|
||||||
|
print '<td>';
|
||||||
|
print '<input type="text" name="color" id="colors-input" class="flat minwidth300" value="'.dol_escape_htmltag($busbarType->color).'" placeholder="Kommagetrennt: #e74c3c,#3498db">';
|
||||||
|
print '<div class="opacitymedium small">Kommagetrennte Farbcodes fuer jede Linie (z.B. #e74c3c,#2ecc71,#9b59b6)</div>';
|
||||||
|
print '</td></tr>';
|
||||||
|
|
||||||
|
// Default color
|
||||||
|
print '<tr><td>'.$langs->trans('DefaultColor').'</td>';
|
||||||
|
print '<td><input type="color" name="default_color" class="flat" value="'.($busbarType->default_color ?: '#e74c3c').'" style="width:60px;height:30px;"></td></tr>';
|
||||||
|
|
||||||
|
// Line height
|
||||||
|
print '<tr><td>'.$langs->trans('LineHeight').'</td>';
|
||||||
|
print '<td><input type="number" name="line_height" class="flat" value="'.($busbarType->line_height ?: 3).'" min="1" max="10"> px</td></tr>';
|
||||||
|
|
||||||
|
// Line spacing
|
||||||
|
print '<tr><td>'.$langs->trans('LineSpacing').'</td>';
|
||||||
|
print '<td><input type="number" name="line_spacing" class="flat" value="'.($busbarType->line_spacing ?: 4).'" min="1" max="20"> px</td></tr>';
|
||||||
|
|
||||||
|
// Default position
|
||||||
|
print '<tr><td>'.$langs->trans('DefaultPosition').'</td>';
|
||||||
|
print '<td><select name="position_default" class="flat">';
|
||||||
|
print '<option value="below"'.($busbarType->position_default == 'below' ? ' selected' : '').'>Unterhalb (below)</option>';
|
||||||
|
print '<option value="above"'.($busbarType->position_default == 'above' ? ' selected' : '').'>Oberhalb (above)</option>';
|
||||||
|
print '</select></td></tr>';
|
||||||
|
|
||||||
|
// Product link
|
||||||
|
print '<tr><td>'.$langs->trans('LinkedProduct').'</td>';
|
||||||
|
print '<td><select name="fk_product" class="flat minwidth300">';
|
||||||
|
print '<option value="0">-- '.$langs->trans('None').' --</option>';
|
||||||
|
foreach ($products as $prod) {
|
||||||
|
$selected = ($busbarType->fk_product == $prod->rowid) ? ' selected' : '';
|
||||||
|
print '<option value="'.$prod->rowid.'"'.$selected.'>'.dol_escape_htmltag($prod->ref.' - '.$prod->label).'</option>';
|
||||||
|
}
|
||||||
|
print '</select></td></tr>';
|
||||||
|
|
||||||
|
// Position
|
||||||
|
print '<tr><td>'.$langs->trans('Position').'</td>';
|
||||||
|
print '<td><input type="number" name="position" class="flat" value="'.($busbarType->position ?: 0).'" min="0"></td></tr>';
|
||||||
|
|
||||||
|
// Preview
|
||||||
|
print '<tr><td>'.$langs->trans('Preview').'</td>';
|
||||||
|
print '<td>';
|
||||||
|
print '<div id="busbar-preview" style="background:#1a1a2e;padding:20px;border-radius:8px;min-height:80px;">';
|
||||||
|
print '<svg id="preview-svg" width="200" height="60"></svg>';
|
||||||
|
print '</div>';
|
||||||
|
print '</td></tr>';
|
||||||
|
|
||||||
|
print '</table>';
|
||||||
|
|
||||||
|
print '<div class="center" style="margin-top:20px;">';
|
||||||
|
print '<button type="submit" class="button">'.$langs->trans('Save').'</button>';
|
||||||
|
print ' <a class="button button-cancel" href="'.$_SERVER['PHP_SELF'].'?system='.$systemFilter.'">'.$langs->trans('Cancel').'</a>';
|
||||||
|
print '</div>';
|
||||||
|
|
||||||
|
print '</form>';
|
||||||
|
|
||||||
|
// JavaScript for preset selection and preview
|
||||||
|
print '<script>
|
||||||
|
document.querySelectorAll(".channel-preset").forEach(function(el) {
|
||||||
|
el.addEventListener("click", function() {
|
||||||
|
document.getElementById("channels-input").value = this.dataset.channels;
|
||||||
|
document.getElementById("numlines-input").value = this.dataset.numlines;
|
||||||
|
document.getElementById("colors-input").value = this.dataset.colors;
|
||||||
|
updatePreview();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function updatePreview() {
|
||||||
|
var numLines = parseInt(document.getElementById("numlines-input").value) || 1;
|
||||||
|
var colorsStr = document.getElementById("colors-input").value;
|
||||||
|
var colors = colorsStr ? colorsStr.split(",") : ["#e74c3c"];
|
||||||
|
var lineHeight = parseInt(document.querySelector("input[name=line_height]").value) || 3;
|
||||||
|
var lineSpacing = parseInt(document.querySelector("input[name=line_spacing]").value) || 4;
|
||||||
|
|
||||||
|
var svg = document.getElementById("preview-svg");
|
||||||
|
var html = "";
|
||||||
|
var y = 10;
|
||||||
|
|
||||||
|
for (var i = 0; i < numLines; i++) {
|
||||||
|
var color = colors[i % colors.length] || "#e74c3c";
|
||||||
|
html += "<rect x=\"10\" y=\"" + y + "\" width=\"180\" height=\"" + lineHeight + "\" fill=\"" + color + "\" rx=\"1\"/>";
|
||||||
|
y += lineHeight + lineSpacing;
|
||||||
|
}
|
||||||
|
|
||||||
|
svg.innerHTML = html;
|
||||||
|
svg.setAttribute("height", y + 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update preview on input change
|
||||||
|
document.getElementById("numlines-input").addEventListener("change", updatePreview);
|
||||||
|
document.getElementById("colors-input").addEventListener("input", updatePreview);
|
||||||
|
document.querySelector("input[name=line_height]").addEventListener("change", updatePreview);
|
||||||
|
document.querySelector("input[name=line_spacing]").addEventListener("change", updatePreview);
|
||||||
|
|
||||||
|
// Initial preview
|
||||||
|
updatePreview();
|
||||||
|
</script>';
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// List of busbar types
|
||||||
|
$types = $busbarType->fetchAllBySystem($systemFilter, 0);
|
||||||
|
|
||||||
|
print '<div class="div-table-responsive">';
|
||||||
|
print '<table class="tagtable nobordernopadding liste centpercent">';
|
||||||
|
print '<tr class="liste_titre">';
|
||||||
|
print '<th>'.$langs->trans('Ref').'</th>';
|
||||||
|
print '<th>'.$langs->trans('Label').'</th>';
|
||||||
|
print '<th>'.$langs->trans('Channels').'</th>';
|
||||||
|
print '<th class="center">'.$langs->trans('Lines').'</th>';
|
||||||
|
print '<th>'.$langs->trans('Colors').'</th>';
|
||||||
|
print '<th>'.$langs->trans('System').'</th>';
|
||||||
|
print '<th class="center">'.$langs->trans('Position').'</th>';
|
||||||
|
print '<th class="center">'.$langs->trans('Status').'</th>';
|
||||||
|
print '<th class="center">'.$langs->trans('Actions').'</th>';
|
||||||
|
print '</tr>';
|
||||||
|
|
||||||
|
if (empty($types)) {
|
||||||
|
print '<tr><td colspan="9" class="opacitymedium">'.$langs->trans('NoRecordFound').'</td></tr>';
|
||||||
|
} else {
|
||||||
|
foreach ($types as $type) {
|
||||||
|
print '<tr class="oddeven">';
|
||||||
|
print '<td><a href="'.$_SERVER['PHP_SELF'].'?action=edit&typeid='.$type->id.'&system='.$systemFilter.'">'.dol_escape_htmltag($type->ref).'</a></td>';
|
||||||
|
print '<td>'.dol_escape_htmltag($type->label);
|
||||||
|
if ($type->label_short) {
|
||||||
|
print ' <span class="opacitymedium">('.dol_escape_htmltag($type->label_short).')</span>';
|
||||||
|
}
|
||||||
|
print '</td>';
|
||||||
|
print '<td>'.dol_escape_htmltag($type->phases).'</td>';
|
||||||
|
print '<td class="center">'.$type->num_lines.'</td>';
|
||||||
|
|
||||||
|
// Color preview
|
||||||
|
print '<td>';
|
||||||
|
$colors = $type->color ? explode(',', $type->color) : array($type->default_color ?: '#e74c3c');
|
||||||
|
foreach ($colors as $c) {
|
||||||
|
print '<span style="display:inline-block;width:16px;height:16px;background:'.trim($c).';border-radius:2px;margin-right:2px;border:1px solid #555;"></span>';
|
||||||
|
}
|
||||||
|
print '</td>';
|
||||||
|
|
||||||
|
print '<td>';
|
||||||
|
if (empty($type->fk_system)) {
|
||||||
|
print '<span class="badge badge-secondary">'.$langs->trans('AllSystems').'</span>';
|
||||||
|
} else {
|
||||||
|
print dol_escape_htmltag($type->system_label);
|
||||||
|
}
|
||||||
|
print '</td>';
|
||||||
|
print '<td class="center">'.$type->position.'</td>';
|
||||||
|
|
||||||
|
// Status
|
||||||
|
print '<td class="center">';
|
||||||
|
if ($type->active) {
|
||||||
|
print '<span class="badge badge-status4">'.$langs->trans('Enabled').'</span>';
|
||||||
|
} else {
|
||||||
|
print '<span class="badge badge-status5">'.$langs->trans('Disabled').'</span>';
|
||||||
|
}
|
||||||
|
print '</td>';
|
||||||
|
|
||||||
|
// Actions
|
||||||
|
print '<td class="center nowraponall">';
|
||||||
|
print '<a href="'.$_SERVER['PHP_SELF'].'?action=edit&typeid='.$type->id.'&system='.$systemFilter.'" title="'.$langs->trans('Edit').'">';
|
||||||
|
print img_edit();
|
||||||
|
print '</a> ';
|
||||||
|
|
||||||
|
if ($type->active) {
|
||||||
|
print '<a href="'.$_SERVER['PHP_SELF'].'?action=deactivate&typeid='.$type->id.'&system='.$systemFilter.'&token='.newToken().'" title="'.$langs->trans('Disable').'">';
|
||||||
|
print img_picto($langs->trans('Disable'), 'switch_on');
|
||||||
|
print '</a> ';
|
||||||
|
} else {
|
||||||
|
print '<a href="'.$_SERVER['PHP_SELF'].'?action=activate&typeid='.$type->id.'&system='.$systemFilter.'&token='.newToken().'" title="'.$langs->trans('Enable').'">';
|
||||||
|
print img_picto($langs->trans('Enable'), 'switch_off');
|
||||||
|
print '</a> ';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$type->is_system) {
|
||||||
|
print '<a href="'.$_SERVER['PHP_SELF'].'?action=delete&typeid='.$type->id.'&system='.$systemFilter.'" title="'.$langs->trans('Delete').'">';
|
||||||
|
print img_delete();
|
||||||
|
print '</a>';
|
||||||
|
}
|
||||||
|
print '</td>';
|
||||||
|
print '</tr>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
print '</table>';
|
||||||
|
print '</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
print dol_get_fiche_end();
|
||||||
|
|
||||||
|
llxFooter();
|
||||||
|
$db->close();
|
||||||
984
admin/equipment_types.php
Executable file
984
admin/equipment_types.php
Executable file
|
|
@ -0,0 +1,984 @@
|
||||||
|
<?php
|
||||||
|
/* Copyright (C) 2026 Alles Watt lauft
|
||||||
|
*
|
||||||
|
* Admin page to manage equipment types (Hutschienen-Komponenten)
|
||||||
|
*/
|
||||||
|
|
||||||
|
$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");
|
||||||
|
|
||||||
|
require_once DOL_DOCUMENT_ROOT.'/core/lib/admin.lib.php';
|
||||||
|
require_once DOL_DOCUMENT_ROOT.'/core/class/html.form.class.php';
|
||||||
|
dol_include_once('/kundenkarte/lib/kundenkarte.lib.php');
|
||||||
|
dol_include_once('/kundenkarte/class/equipmenttype.class.php');
|
||||||
|
|
||||||
|
$langs->loadLangs(array('admin', 'kundenkarte@kundenkarte', 'products'));
|
||||||
|
|
||||||
|
// Security check
|
||||||
|
if (!$user->admin && !$user->hasRight('kundenkarte', 'admin')) {
|
||||||
|
accessforbidden();
|
||||||
|
}
|
||||||
|
|
||||||
|
$action = GETPOST('action', 'aZ09');
|
||||||
|
$confirm = GETPOST('confirm', 'alpha');
|
||||||
|
$typeId = GETPOSTINT('typeid');
|
||||||
|
|
||||||
|
// System filter - save in session for persistence
|
||||||
|
$sessionKey = 'kundenkarte_equipment_types_system_filter';
|
||||||
|
if (GETPOSTISSET('system')) {
|
||||||
|
$systemFilter = GETPOSTINT('system');
|
||||||
|
$_SESSION[$sessionKey] = $systemFilter;
|
||||||
|
} elseif (isset($_SESSION[$sessionKey])) {
|
||||||
|
$systemFilter = $_SESSION[$sessionKey];
|
||||||
|
} else {
|
||||||
|
$systemFilter = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$form = new Form($db);
|
||||||
|
$equipmentType = new EquipmentType($db);
|
||||||
|
|
||||||
|
// Load systems
|
||||||
|
$systems = array();
|
||||||
|
$sql = "SELECT rowid, code, label FROM ".MAIN_DB_PREFIX."c_kundenkarte_anlage_system WHERE active = 1 ORDER BY position ASC";
|
||||||
|
$resql = $db->query($sql);
|
||||||
|
if ($resql) {
|
||||||
|
while ($obj = $db->fetch_object($resql)) {
|
||||||
|
$systems[$obj->rowid] = $obj;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load products for dropdown
|
||||||
|
$products = array();
|
||||||
|
$sql = "SELECT rowid, ref, label FROM ".MAIN_DB_PREFIX."product WHERE tosell = 1 ORDER BY ref ASC";
|
||||||
|
$resql = $db->query($sql);
|
||||||
|
if ($resql) {
|
||||||
|
while ($obj = $db->fetch_object($resql)) {
|
||||||
|
$products[$obj->rowid] = $obj;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Actions
|
||||||
|
*/
|
||||||
|
|
||||||
|
if ($action == 'add') {
|
||||||
|
$equipmentType->ref = GETPOST('ref', 'aZ09');
|
||||||
|
$equipmentType->label = GETPOST('label', 'alphanohtml');
|
||||||
|
$equipmentType->label_short = GETPOST('label_short', 'alphanohtml');
|
||||||
|
$equipmentType->description = GETPOST('description', 'restricthtml');
|
||||||
|
$equipmentType->fk_system = GETPOSTINT('fk_system');
|
||||||
|
$equipmentType->width_te = GETPOSTINT('width_te');
|
||||||
|
$equipmentType->color = GETPOST('color', 'alphanohtml');
|
||||||
|
// fk_product removed - products are selected per equipment in editor
|
||||||
|
$equipmentType->terminals_config = GETPOST('terminals_config', 'nohtml');
|
||||||
|
$equipmentType->flow_direction = GETPOST('flow_direction', 'alphanohtml');
|
||||||
|
$equipmentType->terminal_position = GETPOST('terminal_position', 'alphanohtml') ?: 'both';
|
||||||
|
$equipmentType->picto = GETPOST('picto', 'alphanohtml');
|
||||||
|
$equipmentType->position = GETPOSTINT('position');
|
||||||
|
$equipmentType->active = 1;
|
||||||
|
|
||||||
|
if (empty($equipmentType->ref) || empty($equipmentType->label) || empty($equipmentType->fk_system)) {
|
||||||
|
setEventMessages($langs->trans('ErrorFieldRequired'), null, 'errors');
|
||||||
|
$action = 'create';
|
||||||
|
} else {
|
||||||
|
$result = $equipmentType->create($user);
|
||||||
|
if ($result > 0) {
|
||||||
|
// Create default fields for common equipment
|
||||||
|
$defaultFields = array(
|
||||||
|
array('code' => 'characteristic', 'label' => 'Charakteristik', 'type' => 'select', 'options' => 'B|C|D|K|Z', 'position' => 10, 'show_in_hover' => 1, 'show_on_block' => 1),
|
||||||
|
array('code' => 'ampere', 'label' => 'Nennstrom (A)', 'type' => 'select', 'options' => '6|10|13|16|20|25|32|40|50|63', 'position' => 20, 'show_in_hover' => 1, 'show_on_block' => 1),
|
||||||
|
array('code' => 'pole', 'label' => 'Polzahl', 'type' => 'select', 'options' => '1|2|3|3+N', 'position' => 30, 'show_in_hover' => 1, 'show_on_block' => 0),
|
||||||
|
array('code' => 'circuit', 'label' => 'Stromkreis', 'type' => 'text', 'options' => '', 'position' => 40, 'show_in_hover' => 1, 'show_on_block' => 0),
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach ($defaultFields as $field) {
|
||||||
|
$sql = "INSERT INTO ".MAIN_DB_PREFIX."kundenkarte_equipment_type_field";
|
||||||
|
$sql .= " (fk_equipment_type, field_code, field_label, field_type, field_options, show_in_hover, show_on_block, required, position, active)";
|
||||||
|
$sql .= " VALUES (".((int) $equipmentType->id).", '".$db->escape($field['code'])."', '".$db->escape($field['label'])."',";
|
||||||
|
$sql .= " '".$db->escape($field['type'])."', '".$db->escape($field['options'])."', ".((int) $field['show_in_hover']).", ".((int) $field['show_on_block']).", 0, ".((int) $field['position']).", 1)";
|
||||||
|
$db->query($sql);
|
||||||
|
}
|
||||||
|
|
||||||
|
setEventMessages($langs->trans('RecordSaved'), null, 'mesgs');
|
||||||
|
header('Location: '.$_SERVER['PHP_SELF'].'?action=edit&typeid='.$equipmentType->id.'&system='.$equipmentType->fk_system);
|
||||||
|
exit;
|
||||||
|
} else {
|
||||||
|
setEventMessages($equipmentType->error, $equipmentType->errors, 'errors');
|
||||||
|
$action = 'create';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($action == 'update') {
|
||||||
|
$equipmentType->fetch($typeId);
|
||||||
|
$equipmentType->ref = GETPOST('ref', 'aZ09');
|
||||||
|
$equipmentType->label = GETPOST('label', 'alphanohtml');
|
||||||
|
$equipmentType->label_short = GETPOST('label_short', 'alphanohtml');
|
||||||
|
$equipmentType->description = GETPOST('description', 'restricthtml');
|
||||||
|
$equipmentType->fk_system = GETPOSTINT('fk_system');
|
||||||
|
$equipmentType->width_te = GETPOSTINT('width_te');
|
||||||
|
$equipmentType->color = GETPOST('color', 'alphanohtml');
|
||||||
|
// fk_product removed - products are selected per equipment in editor
|
||||||
|
$equipmentType->terminals_config = GETPOST('terminals_config', 'nohtml');
|
||||||
|
$equipmentType->flow_direction = GETPOST('flow_direction', 'alphanohtml');
|
||||||
|
$equipmentType->terminal_position = GETPOST('terminal_position', 'alphanohtml') ?: 'both';
|
||||||
|
$equipmentType->picto = GETPOST('picto', 'alphanohtml');
|
||||||
|
$equipmentType->position = GETPOSTINT('position');
|
||||||
|
|
||||||
|
$result = $equipmentType->update($user);
|
||||||
|
if ($result > 0) {
|
||||||
|
setEventMessages($langs->trans('RecordSaved'), null, 'mesgs');
|
||||||
|
header('Location: '.$_SERVER['PHP_SELF'].'?system='.$equipmentType->fk_system);
|
||||||
|
exit;
|
||||||
|
} else {
|
||||||
|
setEventMessages($equipmentType->error, $equipmentType->errors, 'errors');
|
||||||
|
$action = 'edit';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($action == 'confirm_delete' && $confirm == 'yes') {
|
||||||
|
$equipmentType->fetch($typeId);
|
||||||
|
$result = $equipmentType->delete($user);
|
||||||
|
if ($result > 0) {
|
||||||
|
setEventMessages($langs->trans('RecordDeleted'), null, 'mesgs');
|
||||||
|
} else {
|
||||||
|
setEventMessages($equipmentType->error, $equipmentType->errors, 'errors');
|
||||||
|
}
|
||||||
|
$action = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($action == 'activate') {
|
||||||
|
$sql = "UPDATE ".MAIN_DB_PREFIX."kundenkarte_equipment_type SET active = 1 WHERE rowid = ".((int) $typeId);
|
||||||
|
$db->query($sql);
|
||||||
|
$action = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($action == 'deactivate') {
|
||||||
|
$sql = "UPDATE ".MAIN_DB_PREFIX."kundenkarte_equipment_type SET active = 0 WHERE rowid = ".((int) $typeId);
|
||||||
|
$db->query($sql);
|
||||||
|
$action = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy type with all fields
|
||||||
|
if ($action == 'copy' && $typeId > 0) {
|
||||||
|
$sourceType = new EquipmentType($db);
|
||||||
|
if ($sourceType->fetch($typeId) > 0) {
|
||||||
|
$newType = new EquipmentType($db);
|
||||||
|
$newType->ref = $sourceType->ref.'_COPY';
|
||||||
|
$newType->label = $sourceType->label.' (Kopie)';
|
||||||
|
$newType->label_short = $sourceType->label_short;
|
||||||
|
$newType->description = $sourceType->description;
|
||||||
|
$newType->fk_system = $sourceType->fk_system;
|
||||||
|
$newType->width_te = $sourceType->width_te;
|
||||||
|
$newType->color = $sourceType->color;
|
||||||
|
// fk_product not copied - products are selected per equipment in editor
|
||||||
|
$newType->picto = $sourceType->picto;
|
||||||
|
$newType->position = $sourceType->position + 1;
|
||||||
|
$newType->active = 1;
|
||||||
|
|
||||||
|
$result = $newType->create($user);
|
||||||
|
if ($result > 0) {
|
||||||
|
// Copy all fields from source type
|
||||||
|
$sourceFields = $sourceType->fetchFields(0);
|
||||||
|
foreach ($sourceFields as $field) {
|
||||||
|
$sql = "INSERT INTO ".MAIN_DB_PREFIX."kundenkarte_equipment_type_field";
|
||||||
|
$sql .= " (fk_equipment_type, field_code, field_label, field_type, field_options, show_in_hover, show_on_block, required, position, active)";
|
||||||
|
$sql .= " VALUES (".((int) $newType->id).", '".$db->escape($field->field_code)."', '".$db->escape($field->field_label)."',";
|
||||||
|
$sql .= " '".$db->escape($field->field_type)."', '".$db->escape($field->field_options)."', ".((int) $field->show_in_hover).",";
|
||||||
|
$sql .= " ".((int) $field->show_on_block).", ".((int) $field->required).", ".((int) $field->position).", ".((int) $field->active).")";
|
||||||
|
$db->query($sql);
|
||||||
|
}
|
||||||
|
|
||||||
|
setEventMessages($langs->trans('RecordSaved'), null, 'mesgs');
|
||||||
|
header('Location: '.$_SERVER['PHP_SELF'].'?action=edit&typeid='.$newType->id.'&system='.$newType->fk_system);
|
||||||
|
exit;
|
||||||
|
} else {
|
||||||
|
setEventMessages($newType->error, $newType->errors, 'errors');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$action = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Field actions
|
||||||
|
$fieldId = GETPOSTINT('fieldid');
|
||||||
|
|
||||||
|
if ($action == 'add_field') {
|
||||||
|
$fieldCode = GETPOST('field_code', 'aZ09');
|
||||||
|
$fieldLabel = GETPOST('field_label', 'alphanohtml');
|
||||||
|
$fieldType = GETPOST('field_type', 'aZ09');
|
||||||
|
$fieldOptions = GETPOST('field_options', 'nohtml');
|
||||||
|
$showInHover = GETPOSTINT('show_in_hover');
|
||||||
|
$showOnBlock = GETPOSTINT('show_on_block');
|
||||||
|
$isRequired = GETPOSTINT('is_required');
|
||||||
|
$fieldPosition = GETPOSTINT('field_position');
|
||||||
|
|
||||||
|
if (empty($fieldCode) || empty($fieldLabel) || empty($fieldType)) {
|
||||||
|
setEventMessages($langs->trans('ErrorFieldRequired'), null, 'errors');
|
||||||
|
} else {
|
||||||
|
$sql = "INSERT INTO ".MAIN_DB_PREFIX."kundenkarte_equipment_type_field";
|
||||||
|
$sql .= " (fk_equipment_type, field_code, field_label, field_type, field_options, show_in_hover, show_on_block, required, position, active)";
|
||||||
|
$sql .= " VALUES (".((int) $typeId).", '".$db->escape($fieldCode)."', '".$db->escape($fieldLabel)."',";
|
||||||
|
$sql .= " '".$db->escape($fieldType)."', '".$db->escape($fieldOptions)."',";
|
||||||
|
$sql .= " ".((int) $showInHover).", ".((int) $showOnBlock).", ".((int) $isRequired).", ".((int) $fieldPosition).", 1)";
|
||||||
|
|
||||||
|
if ($db->query($sql)) {
|
||||||
|
setEventMessages($langs->trans('RecordSaved'), null, 'mesgs');
|
||||||
|
} else {
|
||||||
|
setEventMessages($db->lasterror(), null, 'errors');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$action = 'edit';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($action == 'update_field') {
|
||||||
|
$fieldCode = GETPOST('field_code', 'aZ09');
|
||||||
|
$fieldLabel = GETPOST('field_label', 'alphanohtml');
|
||||||
|
$fieldType = GETPOST('field_type', 'aZ09');
|
||||||
|
$fieldOptions = GETPOST('field_options', 'nohtml');
|
||||||
|
$showInHover = GETPOSTINT('show_in_hover');
|
||||||
|
$showOnBlock = GETPOSTINT('show_on_block');
|
||||||
|
$isRequired = GETPOSTINT('is_required');
|
||||||
|
$fieldPosition = GETPOSTINT('field_position');
|
||||||
|
|
||||||
|
$sql = "UPDATE ".MAIN_DB_PREFIX."kundenkarte_equipment_type_field SET";
|
||||||
|
$sql .= " field_code = '".$db->escape($fieldCode)."',";
|
||||||
|
$sql .= " field_label = '".$db->escape($fieldLabel)."',";
|
||||||
|
$sql .= " field_type = '".$db->escape($fieldType)."',";
|
||||||
|
$sql .= " field_options = '".$db->escape($fieldOptions)."',";
|
||||||
|
$sql .= " show_in_hover = ".((int) $showInHover).",";
|
||||||
|
$sql .= " show_on_block = ".((int) $showOnBlock).",";
|
||||||
|
$sql .= " required = ".((int) $isRequired).",";
|
||||||
|
$sql .= " position = ".((int) $fieldPosition);
|
||||||
|
$sql .= " WHERE rowid = ".((int) $fieldId);
|
||||||
|
|
||||||
|
if ($db->query($sql)) {
|
||||||
|
setEventMessages($langs->trans('RecordSaved'), null, 'mesgs');
|
||||||
|
} else {
|
||||||
|
setEventMessages($db->lasterror(), null, 'errors');
|
||||||
|
}
|
||||||
|
$action = 'edit';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($action == 'confirm_delete_field' && $confirm == 'yes') {
|
||||||
|
$sql = "DELETE FROM ".MAIN_DB_PREFIX."kundenkarte_equipment_type_field WHERE rowid = ".((int) $fieldId);
|
||||||
|
if ($db->query($sql)) {
|
||||||
|
setEventMessages($langs->trans('RecordDeleted'), null, 'mesgs');
|
||||||
|
} else {
|
||||||
|
setEventMessages($db->lasterror(), null, 'errors');
|
||||||
|
}
|
||||||
|
header('Location: '.$_SERVER['PHP_SELF'].'?action=edit&typeid='.$typeId.'&system='.$systemFilter);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($action == 'activate_field') {
|
||||||
|
$sql = "UPDATE ".MAIN_DB_PREFIX."kundenkarte_equipment_type_field SET active = 1 WHERE rowid = ".((int) $fieldId);
|
||||||
|
$db->query($sql);
|
||||||
|
$action = 'edit';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($action == 'deactivate_field') {
|
||||||
|
$sql = "UPDATE ".MAIN_DB_PREFIX."kundenkarte_equipment_type_field SET active = 0 WHERE rowid = ".((int) $fieldId);
|
||||||
|
$db->query($sql);
|
||||||
|
$action = 'edit';
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* View
|
||||||
|
*/
|
||||||
|
|
||||||
|
$title = $langs->trans('EquipmentTypes');
|
||||||
|
|
||||||
|
$morejs = array('/kundenkarte/js/kundenkarte.js?v='.time());
|
||||||
|
$morecss = array('/kundenkarte/css/kundenkarte.css?v='.time());
|
||||||
|
|
||||||
|
llxHeader('', $title, '', '', 0, 0, $morejs, $morecss);
|
||||||
|
|
||||||
|
$head = kundenkarteAdminPrepareHead();
|
||||||
|
print dol_get_fiche_head($head, 'equipment_types', $langs->trans('ModuleKundenKarteName'), -1, 'fa-file');
|
||||||
|
|
||||||
|
// Confirmation for type deletion
|
||||||
|
if ($action == 'delete') {
|
||||||
|
print $form->formconfirm(
|
||||||
|
$_SERVER['PHP_SELF'].'?typeid='.$typeId.'&system='.$systemFilter,
|
||||||
|
$langs->trans('Delete'),
|
||||||
|
$langs->trans('ConfirmDeleteType'),
|
||||||
|
'confirm_delete',
|
||||||
|
'',
|
||||||
|
'yes',
|
||||||
|
1
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Confirmation for field deletion
|
||||||
|
if ($action == 'delete_field') {
|
||||||
|
print $form->formconfirm(
|
||||||
|
$_SERVER['PHP_SELF'].'?typeid='.$typeId.'&fieldid='.$fieldId.'&system='.$systemFilter,
|
||||||
|
$langs->trans('Delete'),
|
||||||
|
$langs->trans('ConfirmDeleteField'),
|
||||||
|
'confirm_delete_field',
|
||||||
|
'',
|
||||||
|
'yes',
|
||||||
|
1
|
||||||
|
);
|
||||||
|
$action = 'edit';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add/Edit form
|
||||||
|
if (in_array($action, array('create', 'edit'))) {
|
||||||
|
if ($action == 'edit' && $typeId > 0) {
|
||||||
|
$equipmentType->fetch($typeId);
|
||||||
|
}
|
||||||
|
|
||||||
|
print '<form method="POST" action="'.$_SERVER['PHP_SELF'].'">';
|
||||||
|
print '<input type="hidden" name="token" value="'.newToken().'">';
|
||||||
|
print '<input type="hidden" name="action" value="'.($action == 'edit' ? 'update' : 'add').'">';
|
||||||
|
if ($action == 'edit') {
|
||||||
|
print '<input type="hidden" name="typeid" value="'.$typeId.'">';
|
||||||
|
}
|
||||||
|
|
||||||
|
print '<table class="border centpercent">';
|
||||||
|
|
||||||
|
// System
|
||||||
|
print '<tr><td class="titlefield fieldrequired">'.$langs->trans('System').'</td>';
|
||||||
|
print '<td><select name="fk_system" class="flat minwidth200" required>';
|
||||||
|
print '<option value="">'.$langs->trans('SelectSystem').'</option>';
|
||||||
|
foreach ($systems as $sys) {
|
||||||
|
$sel = ($equipmentType->fk_system == $sys->rowid) ? ' selected' : '';
|
||||||
|
print '<option value="'.$sys->rowid.'"'.$sel.'>'.dol_escape_htmltag($sys->label).'</option>';
|
||||||
|
}
|
||||||
|
print '</select></td></tr>';
|
||||||
|
|
||||||
|
// Reference
|
||||||
|
print '<tr><td class="fieldrequired">'.$langs->trans('TypeRef').'</td>';
|
||||||
|
print '<td><input type="text" name="ref" class="flat minwidth200" value="'.dol_escape_htmltag($equipmentType->ref).'" maxlength="64" required>';
|
||||||
|
print ' <span class="opacitymedium">(UPPERCASE, no spaces)</span></td></tr>';
|
||||||
|
|
||||||
|
// Label
|
||||||
|
print '<tr><td class="fieldrequired">'.$langs->trans('TypeLabel').'</td>';
|
||||||
|
print '<td><input type="text" name="label" class="flat minwidth300" value="'.dol_escape_htmltag($equipmentType->label).'" required></td></tr>';
|
||||||
|
|
||||||
|
// Short label
|
||||||
|
print '<tr><td>'.$langs->trans('TypeShortLabel').'</td>';
|
||||||
|
print '<td><input type="text" name="label_short" class="flat" value="'.dol_escape_htmltag($equipmentType->label_short).'" maxlength="32">';
|
||||||
|
print ' <span class="opacitymedium">(z.B. LS, FI, FI/LS)</span></td></tr>';
|
||||||
|
|
||||||
|
// Description
|
||||||
|
print '<tr><td>'.$langs->trans('Description').'</td>';
|
||||||
|
print '<td><textarea name="description" class="flat minwidth300" rows="2">'.dol_escape_htmltag($equipmentType->description).'</textarea></td></tr>';
|
||||||
|
|
||||||
|
// Width in TE
|
||||||
|
print '<tr><td class="fieldrequired">'.$langs->trans('WidthTE').'</td>';
|
||||||
|
print '<td><input type="number" name="width_te" class="flat" value="'.($equipmentType->width_te ?: 1).'" min="1" max="12" required>';
|
||||||
|
print ' <span class="opacitymedium">'.$langs->trans('WidthTEHelp').'</span></td></tr>';
|
||||||
|
|
||||||
|
// Color
|
||||||
|
print '<tr><td>'.$langs->trans('Color').'</td>';
|
||||||
|
print '<td><input type="color" name="color" value="'.($equipmentType->color ?: '#3498db').'" style="width:60px;height:30px;padding:0;border:1px solid #ccc;">';
|
||||||
|
print ' <span class="opacitymedium">'.$langs->trans('ColorForSVG').'</span></td></tr>';
|
||||||
|
|
||||||
|
// Note: Product selection removed from type - products are now selected per equipment in the editor
|
||||||
|
// based on the actual configuration (type + characteristic + ampere)
|
||||||
|
|
||||||
|
// FontAwesome Icon (fallback)
|
||||||
|
print '<tr><td>'.$langs->trans('SystemPicto').'</td>';
|
||||||
|
print '<td><div class="kundenkarte-icon-picker-wrapper">';
|
||||||
|
print '<span class="kundenkarte-icon-preview">';
|
||||||
|
if ($equipmentType->picto) {
|
||||||
|
print kundenkarte_render_icon($equipmentType->picto);
|
||||||
|
}
|
||||||
|
print '</span>';
|
||||||
|
print '<input type="text" name="picto" class="flat minwidth200" value="'.dol_escape_htmltag($equipmentType->picto).'" placeholder="fa-bolt">';
|
||||||
|
print '<button type="button" class="kundenkarte-icon-picker-btn" data-input="picto"><i class="fa fa-th"></i> '.$langs->trans('SelectIcon').'</button>';
|
||||||
|
print '</div>';
|
||||||
|
print '<span class="opacitymedium small">Fallback-Icon wenn kein Schaltzeichen hochgeladen</span>';
|
||||||
|
print '</td></tr>';
|
||||||
|
|
||||||
|
// Schaltzeichen Upload (SVG/PNG)
|
||||||
|
print '<tr><td>'.$langs->trans('SchematicSymbol').'</td>';
|
||||||
|
print '<td>';
|
||||||
|
print '<div id="icon-upload-area" style="display:flex;align-items:center;gap:15px;">';
|
||||||
|
|
||||||
|
// Preview area
|
||||||
|
print '<div id="icon-preview" style="width:80px;height:80px;border:2px dashed #ccc;border-radius:8px;display:flex;align-items:center;justify-content:center;background:#f8f8f8;">';
|
||||||
|
if ($action == 'edit' && $equipmentType->icon_file) {
|
||||||
|
$iconUrl = DOL_URL_ROOT.'/document.php?modulepart=kundenkarte&file=equipment_icons/'.urlencode($equipmentType->icon_file);
|
||||||
|
print '<img src="'.$iconUrl.'" style="max-width:70px;max-height:70px;" alt="Icon">';
|
||||||
|
} else {
|
||||||
|
print '<span style="color:#999;font-size:11px;text-align:center;">Kein<br>Symbol</span>';
|
||||||
|
}
|
||||||
|
print '</div>';
|
||||||
|
|
||||||
|
// Upload controls
|
||||||
|
print '<div>';
|
||||||
|
print '<input type="file" id="icon-file-input" accept=".svg,.png" style="display:none;">';
|
||||||
|
print '<button type="button" id="icon-upload-btn" class="button" onclick="document.getElementById(\'icon-file-input\').click();">';
|
||||||
|
print '<i class="fa fa-upload"></i> SVG/PNG hochladen</button>';
|
||||||
|
if ($action == 'edit' && $equipmentType->icon_file) {
|
||||||
|
print ' <button type="button" id="icon-delete-btn" class="button" style="background:#e74c3c;border-color:#c0392b;color:#fff;">';
|
||||||
|
print '<i class="fa fa-trash"></i> Löschen</button>';
|
||||||
|
}
|
||||||
|
print '<div class="opacitymedium small" style="margin-top:5px;">Normgerechte Symbole nach IEC 60617 / DIN EN 60617</div>';
|
||||||
|
print '</div>';
|
||||||
|
|
||||||
|
print '</div>';
|
||||||
|
print '</td></tr>';
|
||||||
|
|
||||||
|
// Block Image Upload (for SchematicEditor display)
|
||||||
|
print '<tr><td>'.$langs->trans('BlockImage').'</td>';
|
||||||
|
print '<td>';
|
||||||
|
print '<div id="block-image-upload-area" style="display:flex;align-items:center;gap:15px;">';
|
||||||
|
|
||||||
|
// Preview area
|
||||||
|
print '<div id="block-image-preview" style="width:80px;height:80px;border:2px dashed #ccc;border-radius:8px;display:flex;align-items:center;justify-content:center;background:#f8f8f8;">';
|
||||||
|
if ($action == 'edit' && $equipmentType->block_image) {
|
||||||
|
$blockImageUrl = DOL_URL_ROOT.'/document.php?modulepart=kundenkarte&file=block_images/'.urlencode($equipmentType->block_image);
|
||||||
|
print '<img src="'.$blockImageUrl.'" style="max-width:70px;max-height:70px;" alt="Block Image">';
|
||||||
|
} else {
|
||||||
|
print '<span style="color:#999;font-size:11px;text-align:center;">Kein<br>Bild</span>';
|
||||||
|
}
|
||||||
|
print '</div>';
|
||||||
|
|
||||||
|
// Upload controls
|
||||||
|
print '<div>';
|
||||||
|
print '<input type="file" id="block-image-file-input" accept=".svg,.png,.jpg,.jpeg,.gif,.webp" style="display:none;">';
|
||||||
|
print '<button type="button" id="block-image-upload-btn" class="button" onclick="document.getElementById(\'block-image-file-input\').click();">';
|
||||||
|
print '<i class="fa fa-upload"></i> Bild hochladen</button>';
|
||||||
|
if ($action == 'edit' && $equipmentType->block_image) {
|
||||||
|
print ' <button type="button" id="block-image-delete-btn" class="button" style="background:#e74c3c;border-color:#c0392b;color:#fff;">';
|
||||||
|
print '<i class="fa fa-trash"></i> Löschen</button>';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dropdown to select existing images
|
||||||
|
$blockImagesDir = DOL_DATA_ROOT.'/kundenkarte/block_images/';
|
||||||
|
$existingImages = array();
|
||||||
|
if (is_dir($blockImagesDir)) {
|
||||||
|
$files = scandir($blockImagesDir);
|
||||||
|
foreach ($files as $file) {
|
||||||
|
if ($file != '.' && $file != '..' && preg_match('/\.(png|jpg|jpeg|gif|svg|webp)$/i', $file)) {
|
||||||
|
$existingImages[] = $file;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sort($existingImages);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($existingImages)) {
|
||||||
|
print '<div style="margin-top:10px;">';
|
||||||
|
print '<select id="block-image-select" class="flat minwidth200">';
|
||||||
|
print '<option value="">-- Vorhandenes Bild wählen --</option>';
|
||||||
|
foreach ($existingImages as $img) {
|
||||||
|
$sel = ($equipmentType->block_image == $img) ? ' selected' : '';
|
||||||
|
print '<option value="'.dol_escape_htmltag($img).'"'.$sel.'>'.dol_escape_htmltag($img).'</option>';
|
||||||
|
}
|
||||||
|
print '</select>';
|
||||||
|
print ' <button type="button" id="block-image-select-btn" class="button">';
|
||||||
|
print '<i class="fa fa-check"></i> Übernehmen</button>';
|
||||||
|
print '</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
print '<div class="opacitymedium small" style="margin-top:5px;">Bild wird im SchematicEditor als Block-Hintergrund angezeigt</div>';
|
||||||
|
print '</div>';
|
||||||
|
|
||||||
|
print '</div>';
|
||||||
|
print '</td></tr>';
|
||||||
|
|
||||||
|
// Position
|
||||||
|
print '<tr><td>'.$langs->trans('Position').'</td>';
|
||||||
|
print '<td><input type="number" name="position" class="flat" value="'.($equipmentType->position ?: 0).'" min="0"></td></tr>';
|
||||||
|
|
||||||
|
// Terminal configuration
|
||||||
|
print '<tr><td>'.$langs->trans('TerminalConfig').'</td>';
|
||||||
|
print '<td>';
|
||||||
|
print '<div style="margin-bottom:10px;">';
|
||||||
|
print '<label style="margin-right:15px;"><strong>Vorlagen:</strong></label>';
|
||||||
|
print '<button type="button" class="button small" onclick="setTerminals(\'1P\')">1-polig</button> ';
|
||||||
|
print '<button type="button" class="button small" onclick="setTerminals(\'2P\')">2-polig (L+N)</button> ';
|
||||||
|
print '<button type="button" class="button small" onclick="setTerminals(\'3P\')">3-polig</button> ';
|
||||||
|
print '<button type="button" class="button small" onclick="setTerminals(\'4P\')">4-polig (3P+N)</button>';
|
||||||
|
print '</div>';
|
||||||
|
print '<textarea name="terminals_config" id="terminals_config" class="flat" rows="4" style="width:100%;font-family:monospace;font-size:11px;" placeholder=\'{"terminals":[{"id":"t1","label":"L","pos":"top"},{"id":"t2","label":"L","pos":"bottom"}]}\'>';
|
||||||
|
print dol_escape_htmltag($equipmentType->terminals_config);
|
||||||
|
print '</textarea>';
|
||||||
|
print '<div class="opacitymedium small" style="margin-top:5px;">';
|
||||||
|
print 'JSON-Format: <code>{"terminals":[{"id":"t1","label":"L","pos":"top"},...]}</code> - pos: "top" oder "bottom"';
|
||||||
|
print '</div>';
|
||||||
|
print '</td></tr>';
|
||||||
|
|
||||||
|
// Terminal Position
|
||||||
|
print '<tr><td>Anschlusspunkt-Position</td>';
|
||||||
|
print '<td>';
|
||||||
|
print '<select name="terminal_position" class="flat minwidth200">';
|
||||||
|
print '<option value="both"'.($equipmentType->terminal_position == 'both' ? ' selected' : '').'>Beidseitig (oben + unten)</option>';
|
||||||
|
print '<option value="top_only"'.($equipmentType->terminal_position == 'top_only' ? ' selected' : '').'>Nur oben</option>';
|
||||||
|
print '<option value="bottom_only"'.($equipmentType->terminal_position == 'bottom_only' ? ' selected' : '').'>Nur unten</option>';
|
||||||
|
print '</select>';
|
||||||
|
print '<div class="opacitymedium small" style="margin-top:5px;">Wo sollen die Anschlusspunkte angezeigt werden?</div>';
|
||||||
|
print '</td></tr>';
|
||||||
|
|
||||||
|
// Flow Direction
|
||||||
|
print '<tr><td>Richtung (Pfeil)</td>';
|
||||||
|
print '<td>';
|
||||||
|
print '<select name="flow_direction" class="flat minwidth200">';
|
||||||
|
print '<option value=""'.(!$equipmentType->flow_direction ? ' selected' : '').'>Keine</option>';
|
||||||
|
print '<option value="top_to_bottom"'.($equipmentType->flow_direction == 'top_to_bottom' ? ' selected' : '').'>Von oben nach unten ↓</option>';
|
||||||
|
print '<option value="bottom_to_top"'.($equipmentType->flow_direction == 'bottom_to_top' ? ' selected' : '').'>Von unten nach oben ↑</option>';
|
||||||
|
print '</select>';
|
||||||
|
print '<div class="opacitymedium small" style="margin-top:5px;">Zeigt einen Richtungspfeil im Block an (z.B. für Typ B FI-Schalter)</div>';
|
||||||
|
print '</td></tr>';
|
||||||
|
|
||||||
|
print '</table>';
|
||||||
|
|
||||||
|
// JavaScript for terminal presets and icon upload
|
||||||
|
$typeIdJs = $action == 'edit' ? $typeId : 0;
|
||||||
|
print '<script>
|
||||||
|
function setTerminals(type) {
|
||||||
|
var configs = {
|
||||||
|
"1P": {"terminals":[{"id":"t1","label":"●","pos":"top"},{"id":"t2","label":"●","pos":"bottom"}]},
|
||||||
|
"2P": {"terminals":[{"id":"t1","label":"L","pos":"top"},{"id":"t2","label":"N","pos":"top"},{"id":"t3","label":"L","pos":"bottom"},{"id":"t4","label":"N","pos":"bottom"}]},
|
||||||
|
"3P": {"terminals":[{"id":"t1","label":"L1","pos":"top"},{"id":"t2","label":"L2","pos":"top"},{"id":"t3","label":"L3","pos":"top"},{"id":"t4","label":"L1","pos":"bottom"},{"id":"t5","label":"L2","pos":"bottom"},{"id":"t6","label":"L3","pos":"bottom"}]},
|
||||||
|
"4P": {"terminals":[{"id":"t1","label":"L1","pos":"top"},{"id":"t2","label":"L2","pos":"top"},{"id":"t3","label":"L3","pos":"top"},{"id":"t4","label":"N","pos":"top"},{"id":"t5","label":"L1","pos":"bottom"},{"id":"t6","label":"L2","pos":"bottom"},{"id":"t7","label":"L3","pos":"bottom"},{"id":"t8","label":"N","pos":"bottom"}]}
|
||||||
|
};
|
||||||
|
if (configs[type]) {
|
||||||
|
document.getElementById("terminals_config").value = JSON.stringify(configs[type], null, 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Icon upload handling
|
||||||
|
var typeId = '.$typeIdJs.';
|
||||||
|
|
||||||
|
document.getElementById("icon-file-input").addEventListener("change", function(e) {
|
||||||
|
if (!typeId || typeId == 0) {
|
||||||
|
alert("Bitte speichern Sie zuerst den Equipment-Typ bevor Sie ein Symbol hochladen.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var file = e.target.files[0];
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
|
var formData = new FormData();
|
||||||
|
formData.append("action", "upload");
|
||||||
|
formData.append("type_id", typeId);
|
||||||
|
formData.append("icon_file", file);
|
||||||
|
formData.append("token", "'.newToken().'");
|
||||||
|
|
||||||
|
fetch("'.DOL_URL_ROOT.'/custom/kundenkarte/ajax/equipment_type_icon.php", {
|
||||||
|
method: "POST",
|
||||||
|
body: formData
|
||||||
|
})
|
||||||
|
.then(function(response) { return response.json(); })
|
||||||
|
.then(function(data) {
|
||||||
|
if (data.success) {
|
||||||
|
var preview = document.getElementById("icon-preview");
|
||||||
|
preview.innerHTML = \'<img src="\' + data.icon_url + \'?t=\' + Date.now() + \'" style="max-width:70px;max-height:70px;" alt="Icon">\';
|
||||||
|
|
||||||
|
// Add delete button if not present
|
||||||
|
if (!document.getElementById("icon-delete-btn")) {
|
||||||
|
var btn = document.createElement("button");
|
||||||
|
btn.type = "button";
|
||||||
|
btn.id = "icon-delete-btn";
|
||||||
|
btn.className = "button";
|
||||||
|
btn.style.cssText = "background:#e74c3c;border-color:#c0392b;color:#fff;margin-left:5px;";
|
||||||
|
btn.innerHTML = \'<i class="fa fa-trash"></i> Löschen\';
|
||||||
|
btn.onclick = deleteIcon;
|
||||||
|
document.getElementById("icon-upload-btn").after(btn);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
alert("Fehler: " + data.error);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(function(err) {
|
||||||
|
alert("Upload fehlgeschlagen: " + err);
|
||||||
|
});
|
||||||
|
|
||||||
|
e.target.value = "";
|
||||||
|
});
|
||||||
|
|
||||||
|
function deleteIcon() {
|
||||||
|
if (!confirm("Symbol wirklich löschen?")) return;
|
||||||
|
|
||||||
|
fetch("'.DOL_URL_ROOT.'/custom/kundenkarte/ajax/equipment_type_icon.php", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {"Content-Type": "application/x-www-form-urlencoded"},
|
||||||
|
body: "action=delete&type_id=" + typeId + "&token='.newToken().'"
|
||||||
|
})
|
||||||
|
.then(function(response) { return response.json(); })
|
||||||
|
.then(function(data) {
|
||||||
|
if (data.success) {
|
||||||
|
var preview = document.getElementById("icon-preview");
|
||||||
|
preview.innerHTML = \'<span style="color:#999;font-size:11px;text-align:center;">Kein<br>Symbol</span>\';
|
||||||
|
var delBtn = document.getElementById("icon-delete-btn");
|
||||||
|
if (delBtn) delBtn.remove();
|
||||||
|
} else {
|
||||||
|
alert("Fehler: " + data.error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var delBtn = document.getElementById("icon-delete-btn");
|
||||||
|
if (delBtn) delBtn.onclick = deleteIcon;
|
||||||
|
|
||||||
|
// Block Image upload handling
|
||||||
|
document.getElementById("block-image-file-input").addEventListener("change", function(e) {
|
||||||
|
if (!typeId || typeId == 0) {
|
||||||
|
alert("Bitte speichern Sie zuerst den Equipment-Typ bevor Sie ein Bild hochladen.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var file = e.target.files[0];
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
|
var formData = new FormData();
|
||||||
|
formData.append("action", "upload");
|
||||||
|
formData.append("type_id", typeId);
|
||||||
|
formData.append("block_image", file);
|
||||||
|
formData.append("token", "'.newToken().'");
|
||||||
|
|
||||||
|
fetch("'.DOL_URL_ROOT.'/custom/kundenkarte/ajax/equipment_type_block_image.php", {
|
||||||
|
method: "POST",
|
||||||
|
body: formData
|
||||||
|
})
|
||||||
|
.then(function(response) { return response.json(); })
|
||||||
|
.then(function(data) {
|
||||||
|
if (data.success) {
|
||||||
|
var preview = document.getElementById("block-image-preview");
|
||||||
|
preview.innerHTML = \'<img src="\' + data.block_image_url + \'&t=\' + Date.now() + \'" style="max-width:70px;max-height:70px;" alt="Block Image">\';
|
||||||
|
|
||||||
|
// Add delete button if not present
|
||||||
|
if (!document.getElementById("block-image-delete-btn")) {
|
||||||
|
var btn = document.createElement("button");
|
||||||
|
btn.type = "button";
|
||||||
|
btn.id = "block-image-delete-btn";
|
||||||
|
btn.className = "button";
|
||||||
|
btn.style.cssText = "background:#e74c3c;border-color:#c0392b;color:#fff;margin-left:5px;";
|
||||||
|
btn.innerHTML = \'<i class="fa fa-trash"></i> Löschen\';
|
||||||
|
btn.onclick = deleteBlockImage;
|
||||||
|
document.getElementById("block-image-upload-btn").after(btn);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
alert("Fehler: " + data.error);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(function(err) {
|
||||||
|
alert("Upload fehlgeschlagen: " + err);
|
||||||
|
});
|
||||||
|
|
||||||
|
e.target.value = "";
|
||||||
|
});
|
||||||
|
|
||||||
|
function deleteBlockImage() {
|
||||||
|
if (!confirm("Bild wirklich löschen?")) return;
|
||||||
|
|
||||||
|
fetch("'.DOL_URL_ROOT.'/custom/kundenkarte/ajax/equipment_type_block_image.php", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {"Content-Type": "application/x-www-form-urlencoded"},
|
||||||
|
body: "action=delete&type_id=" + typeId + "&token='.newToken().'"
|
||||||
|
})
|
||||||
|
.then(function(response) { return response.json(); })
|
||||||
|
.then(function(data) {
|
||||||
|
if (data.success) {
|
||||||
|
var preview = document.getElementById("block-image-preview");
|
||||||
|
preview.innerHTML = \'<span style="color:#999;font-size:11px;text-align:center;">Kein<br>Bild</span>\';
|
||||||
|
var delBtn = document.getElementById("block-image-delete-btn");
|
||||||
|
if (delBtn) delBtn.remove();
|
||||||
|
} else {
|
||||||
|
alert("Fehler: " + data.error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var blockImgDelBtn = document.getElementById("block-image-delete-btn");
|
||||||
|
if (blockImgDelBtn) blockImgDelBtn.onclick = deleteBlockImage;
|
||||||
|
|
||||||
|
// Select existing image
|
||||||
|
var selectBtn = document.getElementById("block-image-select-btn");
|
||||||
|
if (selectBtn) {
|
||||||
|
selectBtn.onclick = function() {
|
||||||
|
if (!typeId || typeId == 0) {
|
||||||
|
alert("Bitte speichern Sie zuerst den Equipment-Typ.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var select = document.getElementById("block-image-select");
|
||||||
|
var selectedImage = select.value;
|
||||||
|
if (!selectedImage) {
|
||||||
|
alert("Bitte wählen Sie ein Bild aus.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch("'.DOL_URL_ROOT.'/custom/kundenkarte/ajax/equipment_type_block_image.php", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {"Content-Type": "application/x-www-form-urlencoded"},
|
||||||
|
body: "action=select&type_id=" + typeId + "&image=" + encodeURIComponent(selectedImage) + "&token='.newToken().'"
|
||||||
|
})
|
||||||
|
.then(function(response) { return response.json(); })
|
||||||
|
.then(function(data) {
|
||||||
|
if (data.success) {
|
||||||
|
var preview = document.getElementById("block-image-preview");
|
||||||
|
preview.innerHTML = \'<img src="\' + data.block_image_url + \'&t=\' + Date.now() + \'" style="max-width:70px;max-height:70px;" alt="Block Image">\';
|
||||||
|
|
||||||
|
// Add delete button if not present
|
||||||
|
if (!document.getElementById("block-image-delete-btn")) {
|
||||||
|
var btn = document.createElement("button");
|
||||||
|
btn.type = "button";
|
||||||
|
btn.id = "block-image-delete-btn";
|
||||||
|
btn.className = "button";
|
||||||
|
btn.style.cssText = "background:#e74c3c;border-color:#c0392b;color:#fff;margin-left:5px;";
|
||||||
|
btn.innerHTML = \'<i class="fa fa-trash"></i> Löschen\';
|
||||||
|
btn.onclick = deleteBlockImage;
|
||||||
|
document.getElementById("block-image-upload-btn").after(btn);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
alert("Fehler: " + data.error);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(function(err) {
|
||||||
|
alert("Fehler: " + err);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
</script>';
|
||||||
|
|
||||||
|
print '<div class="center" style="margin-top:20px;">';
|
||||||
|
print '<button type="submit" class="button">'.$langs->trans('Save').'</button>';
|
||||||
|
print ' <a class="button button-cancel" href="'.$_SERVER['PHP_SELF'].'?system='.$systemFilter.'">'.$langs->trans('Cancel').'</a>';
|
||||||
|
print '</div>';
|
||||||
|
|
||||||
|
print '</form>';
|
||||||
|
|
||||||
|
// Fields management for existing type
|
||||||
|
if ($action == 'edit' && $typeId > 0) {
|
||||||
|
$editFieldId = GETPOSTINT('editfield');
|
||||||
|
|
||||||
|
print '<br><br>';
|
||||||
|
print '<h3>'.$langs->trans('EquipmentTypeFields').'</h3>';
|
||||||
|
|
||||||
|
$fields = $equipmentType->fetchFields(0);
|
||||||
|
|
||||||
|
// Field types available
|
||||||
|
$fieldTypes = array(
|
||||||
|
'text' => 'Textfeld (einzeilig)',
|
||||||
|
'textarea' => 'Textfeld (mehrzeilig)',
|
||||||
|
'number' => 'Zahlenfeld',
|
||||||
|
'select' => 'Dropdown-Auswahl',
|
||||||
|
'date' => 'Datumsfeld',
|
||||||
|
'checkbox' => 'Checkbox (Ja/Nein)',
|
||||||
|
);
|
||||||
|
|
||||||
|
// Output edit forms BEFORE the table
|
||||||
|
foreach ($fields as $field) {
|
||||||
|
if ($editFieldId == $field->rowid) {
|
||||||
|
$formId = 'editfield_'.$field->rowid;
|
||||||
|
print '<form method="POST" action="'.$_SERVER['PHP_SELF'].'" id="'.$formId.'">';
|
||||||
|
print '<input type="hidden" name="token" value="'.newToken().'">';
|
||||||
|
print '<input type="hidden" name="action" value="update_field">';
|
||||||
|
print '<input type="hidden" name="typeid" value="'.$typeId.'">';
|
||||||
|
print '<input type="hidden" name="fieldid" value="'.$field->rowid.'">';
|
||||||
|
print '<input type="hidden" name="system" value="'.$systemFilter.'">';
|
||||||
|
print '</form>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
print '<table class="noborder centpercent">';
|
||||||
|
print '<tr class="liste_titre">';
|
||||||
|
print '<th>'.$langs->trans('FieldCode').'</th>';
|
||||||
|
print '<th>'.$langs->trans('FieldLabel').'</th>';
|
||||||
|
print '<th>'.$langs->trans('FieldType').'</th>';
|
||||||
|
print '<th>'.$langs->trans('FieldOptions').'</th>';
|
||||||
|
print '<th class="center">'.$langs->trans('ShowInHover').'</th>';
|
||||||
|
print '<th class="center">'.$langs->trans('ShowOnBlock').'</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>';
|
||||||
|
print '<th class="center">'.$langs->trans('Actions').'</th>';
|
||||||
|
print '</tr>';
|
||||||
|
|
||||||
|
foreach ($fields as $field) {
|
||||||
|
if ($editFieldId == $field->rowid) {
|
||||||
|
$formId = 'editfield_'.$field->rowid;
|
||||||
|
|
||||||
|
print '<tr class="oddeven">';
|
||||||
|
print '<td><input type="text" name="field_code" form="'.$formId.'" class="flat minwidth100" value="'.dol_escape_htmltag($field->field_code).'" required></td>';
|
||||||
|
print '<td><input type="text" name="field_label" form="'.$formId.'" class="flat minwidth150" value="'.dol_escape_htmltag($field->field_label).'" required></td>';
|
||||||
|
print '<td><select name="field_type" form="'.$formId.'" class="flat">';
|
||||||
|
foreach ($fieldTypes as $ftype => $flabel) {
|
||||||
|
$sel = ($field->field_type == $ftype) ? ' selected' : '';
|
||||||
|
print '<option value="'.$ftype.'"'.$sel.'>'.$flabel.'</option>';
|
||||||
|
}
|
||||||
|
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_hover" form="'.$formId.'" value="1"'.($field->show_in_hover ? ' checked' : '').'></td>';
|
||||||
|
print '<td class="center"><input type="checkbox" name="show_on_block" form="'.$formId.'" value="1"'.($field->show_on_block ? ' checked' : '').'></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>';
|
||||||
|
print '<td class="center nowraponall">';
|
||||||
|
print '<div style="display:inline-flex;gap:8px;">';
|
||||||
|
print '<button type="submit" form="'.$formId.'" class="button buttongen" style="width:32px;height:32px;padding:0;display:inline-flex;align-items:center;justify-content:center;background:#28a745;border-color:#28a745;color:#fff;" title="'.$langs->trans('Save').'"><i class="fas fa-save"></i></button>';
|
||||||
|
print '<a class="button buttongen" style="width:32px;height:32px;padding:0;display:inline-flex;align-items:center;justify-content:center;" href="'.$_SERVER['PHP_SELF'].'?action=edit&typeid='.$typeId.'&system='.$systemFilter.'" title="'.$langs->trans('Cancel').'"><i class="fas fa-times"></i></a>';
|
||||||
|
print '</div>';
|
||||||
|
print '</td>';
|
||||||
|
print '</tr>';
|
||||||
|
} else {
|
||||||
|
print '<tr class="oddeven">';
|
||||||
|
print '<td>'.dol_escape_htmltag($field->field_code).'</td>';
|
||||||
|
print '<td>'.dol_escape_htmltag($field->field_label).'</td>';
|
||||||
|
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_hover ? img_picto('', 'tick') : '').'</td>';
|
||||||
|
print '<td class="center">'.($field->show_on_block ? img_picto('', 'tick') : '').'</td>';
|
||||||
|
print '<td class="center">'.($field->required ? img_picto('', 'tick') : '').'</td>';
|
||||||
|
print '<td class="center">'.$field->position.'</td>';
|
||||||
|
print '<td class="center">';
|
||||||
|
if ($field->active) {
|
||||||
|
print '<a href="'.$_SERVER['PHP_SELF'].'?action=deactivate_field&typeid='.$typeId.'&fieldid='.$field->rowid.'&system='.$systemFilter.'&token='.newToken().'">'.img_picto($langs->trans('Enabled'), 'switch_on').'</a>';
|
||||||
|
} else {
|
||||||
|
print '<a href="'.$_SERVER['PHP_SELF'].'?action=activate_field&typeid='.$typeId.'&fieldid='.$field->rowid.'&system='.$systemFilter.'&token='.newToken().'">'.img_picto($langs->trans('Disabled'), 'switch_off').'</a>';
|
||||||
|
}
|
||||||
|
print '</td>';
|
||||||
|
print '<td class="center nowraponall">';
|
||||||
|
print '<div style="display:inline-flex;gap:8px;">';
|
||||||
|
print '<a class="button buttongen" style="width:32px;height:32px;padding:0;display:inline-flex;align-items:center;justify-content:center;" href="'.$_SERVER['PHP_SELF'].'?action=edit&typeid='.$typeId.'&editfield='.$field->rowid.'&system='.$systemFilter.'" title="'.$langs->trans('Edit').'"><i class="fas fa-pen"></i></a>';
|
||||||
|
print '<a class="button buttongen" style="width:32px;height:32px;padding:0;display:inline-flex;align-items:center;justify-content:center;background:#dc3545;border-color:#dc3545;color:#fff;" href="'.$_SERVER['PHP_SELF'].'?action=delete_field&typeid='.$typeId.'&fieldid='.$field->rowid.'&system='.$systemFilter.'&token='.newToken().'" title="'.$langs->trans('Delete').'"><i class="fas fa-trash"></i></a>';
|
||||||
|
print '</div>';
|
||||||
|
print '</td>';
|
||||||
|
print '</tr>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($fields)) {
|
||||||
|
print '<tr class="oddeven"><td colspan="10" class="opacitymedium">'.$langs->trans('NoFieldsDefined').'</td></tr>';
|
||||||
|
}
|
||||||
|
|
||||||
|
print '</table>';
|
||||||
|
|
||||||
|
// Add new field form
|
||||||
|
print '<div class="margintoponlyshort">';
|
||||||
|
print '<form method="POST" action="'.$_SERVER['PHP_SELF'].'">';
|
||||||
|
print '<input type="hidden" name="token" value="'.newToken().'">';
|
||||||
|
print '<input type="hidden" name="action" value="add_field">';
|
||||||
|
print '<input type="hidden" name="typeid" value="'.$typeId.'">';
|
||||||
|
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 '</tr>';
|
||||||
|
print '<tr class="oddeven">';
|
||||||
|
print '<td><input type="text" name="field_code" class="flat minwidth100" placeholder="CODE" required></td>';
|
||||||
|
print '<td><input type="text" name="field_label" class="flat minwidth150" placeholder="'.$langs->trans('FieldLabel').'" required></td>';
|
||||||
|
print '<td><select name="field_type" class="flat" required>';
|
||||||
|
print '<option value="">'.$langs->trans('Select').'</option>';
|
||||||
|
foreach ($fieldTypes as $ftype => $flabel) {
|
||||||
|
print '<option value="'.$ftype.'">'.$flabel.'</option>';
|
||||||
|
}
|
||||||
|
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_hover" value="1" checked></td>';
|
||||||
|
print '<td class="center"><input type="checkbox" name="show_on_block" value="1"></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>';
|
||||||
|
print '<td class="center"><button type="submit" class="button buttongen"><i class="fa fa-plus"></i> '.$langs->trans('Add').'</button></td>';
|
||||||
|
print '</tr>';
|
||||||
|
print '</table>';
|
||||||
|
print '</form>';
|
||||||
|
print '</div>';
|
||||||
|
|
||||||
|
// Help box
|
||||||
|
print '<div class="info" style="margin-top:15px;">';
|
||||||
|
print '<p><strong><i class="fa fa-info-circle"></i> Hilfe: Equipment-Felder</strong></p>';
|
||||||
|
print '<table class="noborder" style="margin-top:10px;">';
|
||||||
|
print '<tr><td style="width:200px;"><strong>Show in Hover</strong></td><td>Feld wird im Tooltip angezeigt</td></tr>';
|
||||||
|
print '<tr><td><strong>Show on Block</strong></td><td>Feld wird direkt auf dem SVG-Block angezeigt (z.B. "B16")</td></tr>';
|
||||||
|
print '<tr><td><strong>Dropdown-Auswahl</strong></td><td>Optionen mit <code>|</code> trennen, z.B.: <code>B|C|D</code> oder <code>6|10|16|20|25|32</code></td></tr>';
|
||||||
|
print '</table>';
|
||||||
|
print '</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// System filter
|
||||||
|
print '<form method="GET" action="'.$_SERVER['PHP_SELF'].'" style="margin-bottom:15px;">';
|
||||||
|
print $langs->trans('FilterBySystem').': ';
|
||||||
|
print '<select name="system" class="flat" onchange="this.form.submit();">';
|
||||||
|
print '<option value="0">'.$langs->trans('All').'</option>';
|
||||||
|
foreach ($systems as $sys) {
|
||||||
|
$sel = ($systemFilter == $sys->rowid) ? ' selected' : '';
|
||||||
|
print '<option value="'.$sys->rowid.'"'.$sel.'>'.dol_escape_htmltag($sys->label).'</option>';
|
||||||
|
}
|
||||||
|
print '</select>';
|
||||||
|
print '</form>';
|
||||||
|
|
||||||
|
// Add button
|
||||||
|
print '<div style="margin-bottom:15px;">';
|
||||||
|
print '<a class="button" href="'.$_SERVER['PHP_SELF'].'?action=create&system='.$systemFilter.'">';
|
||||||
|
print '<i class="fa fa-plus"></i> '.$langs->trans('AddEquipmentType');
|
||||||
|
print '</a>';
|
||||||
|
print '</div>';
|
||||||
|
|
||||||
|
// List
|
||||||
|
$types = $equipmentType->fetchAllBySystem($systemFilter, 0);
|
||||||
|
|
||||||
|
print '<table class="noborder centpercent">';
|
||||||
|
print '<tr class="liste_titre">';
|
||||||
|
print '<th>'.$langs->trans('TypeRef').'</th>';
|
||||||
|
print '<th>'.$langs->trans('TypeLabel').'</th>';
|
||||||
|
print '<th>'.$langs->trans('System').'</th>';
|
||||||
|
print '<th class="center">'.$langs->trans('WidthTE').'</th>';
|
||||||
|
print '<th class="center">'.$langs->trans('Color').'</th>';
|
||||||
|
print '<th class="center">'.$langs->trans('Position').'</th>';
|
||||||
|
print '<th class="center">'.$langs->trans('Status').'</th>';
|
||||||
|
print '<th class="center">'.$langs->trans('Actions').'</th>';
|
||||||
|
print '</tr>';
|
||||||
|
|
||||||
|
foreach ($types as $type) {
|
||||||
|
print '<tr class="oddeven">';
|
||||||
|
|
||||||
|
print '<td>';
|
||||||
|
if ($type->picto) {
|
||||||
|
print kundenkarte_render_icon($type->picto).' ';
|
||||||
|
}
|
||||||
|
print dol_escape_htmltag($type->ref).'</td>';
|
||||||
|
|
||||||
|
print '<td>'.dol_escape_htmltag($type->label);
|
||||||
|
if ($type->label_short) {
|
||||||
|
print ' <span class="opacitymedium">('.$type->label_short.')</span>';
|
||||||
|
}
|
||||||
|
print '</td>';
|
||||||
|
|
||||||
|
print '<td>'.dol_escape_htmltag($type->system_label).'</td>';
|
||||||
|
print '<td class="center">'.$type->width_te.' TE</td>';
|
||||||
|
print '<td class="center"><span style="display:inline-block;width:24px;height:24px;background:'.($type->color ?: '#3498db').';border-radius:3px;"></span></td>';
|
||||||
|
print '<td class="center">'.$type->position.'</td>';
|
||||||
|
|
||||||
|
print '<td class="center">';
|
||||||
|
if ($type->active) {
|
||||||
|
print '<a href="'.$_SERVER['PHP_SELF'].'?action=deactivate&typeid='.$type->id.'&system='.$systemFilter.'&token='.newToken().'">'.img_picto($langs->trans('Enabled'), 'switch_on').'</a>';
|
||||||
|
} else {
|
||||||
|
print '<a href="'.$_SERVER['PHP_SELF'].'?action=activate&typeid='.$type->id.'&system='.$systemFilter.'&token='.newToken().'">'.img_picto($langs->trans('Disabled'), 'switch_off').'</a>';
|
||||||
|
}
|
||||||
|
print '</td>';
|
||||||
|
|
||||||
|
print '<td class="center nowraponall">';
|
||||||
|
print '<div style="display:inline-flex;gap:8px;">';
|
||||||
|
print '<a class="button buttongen" style="width:32px;height:32px;padding:0;display:inline-flex;align-items:center;justify-content:center;" href="'.$_SERVER['PHP_SELF'].'?action=edit&typeid='.$type->id.'&system='.$systemFilter.'" title="'.$langs->trans('Edit').'"><i class="fas fa-pen"></i></a>';
|
||||||
|
print '<a class="button buttongen" style="width:32px;height:32px;padding:0;display:inline-flex;align-items:center;justify-content:center;" href="'.$_SERVER['PHP_SELF'].'?action=copy&typeid='.$type->id.'&system='.$systemFilter.'&token='.newToken().'" title="'.$langs->trans('Copy').'"><i class="fas fa-copy"></i></a>';
|
||||||
|
if (!$type->is_system) {
|
||||||
|
print '<a class="button buttongen" style="width:32px;height:32px;padding:0;display:inline-flex;align-items:center;justify-content:center;background:#dc3545;border-color:#dc3545;color:#fff;" href="'.$_SERVER['PHP_SELF'].'?action=delete&typeid='.$type->id.'&system='.$systemFilter.'" title="'.$langs->trans('Delete').'" onclick="return confirm(\''.$langs->trans('ConfirmDelete').'\');"><i class="fas fa-trash"></i></a>';
|
||||||
|
}
|
||||||
|
print '</div>';
|
||||||
|
print '</td>';
|
||||||
|
|
||||||
|
print '</tr>';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($types)) {
|
||||||
|
print '<tr class="oddeven"><td colspan="8" class="opacitymedium">'.$langs->trans('NoRecords').'</td></tr>';
|
||||||
|
}
|
||||||
|
|
||||||
|
print '</table>';
|
||||||
|
}
|
||||||
|
|
||||||
|
print dol_get_fiche_end();
|
||||||
|
|
||||||
|
llxFooter();
|
||||||
|
$db->close();
|
||||||
332
admin/medium_types.php
Executable file
332
admin/medium_types.php
Executable file
|
|
@ -0,0 +1,332 @@
|
||||||
|
<?php
|
||||||
|
/* Copyright (C) 2026 Alles Watt lauft
|
||||||
|
*
|
||||||
|
* Admin page for managing Medium Types (Kabeltypen)
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 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");
|
||||||
|
|
||||||
|
require_once DOL_DOCUMENT_ROOT.'/core/lib/admin.lib.php';
|
||||||
|
dol_include_once('/kundenkarte/class/mediumtype.class.php');
|
||||||
|
dol_include_once('/kundenkarte/lib/kundenkarte.lib.php');
|
||||||
|
|
||||||
|
$langs->loadLangs(array("admin", "kundenkarte@kundenkarte"));
|
||||||
|
|
||||||
|
// Security check
|
||||||
|
if (!$user->admin) {
|
||||||
|
accessforbidden();
|
||||||
|
}
|
||||||
|
|
||||||
|
$action = GETPOST('action', 'aZ09');
|
||||||
|
$typeId = GETPOSTINT('typeid');
|
||||||
|
|
||||||
|
$mediumType = new MediumType($db);
|
||||||
|
|
||||||
|
// Load systems for dropdown
|
||||||
|
$systems = array();
|
||||||
|
$sql = "SELECT rowid, code, label FROM ".MAIN_DB_PREFIX."c_kundenkarte_anlage_system WHERE active = 1 ORDER BY position, label";
|
||||||
|
$resql = $db->query($sql);
|
||||||
|
if ($resql) {
|
||||||
|
while ($obj = $db->fetch_object($resql)) {
|
||||||
|
$systems[$obj->rowid] = $obj;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$error = 0;
|
||||||
|
$message = '';
|
||||||
|
|
||||||
|
// Actions
|
||||||
|
if ($action == 'add' && $user->admin) {
|
||||||
|
$mediumType->ref = GETPOST('ref', 'alphanohtml');
|
||||||
|
$mediumType->label = GETPOST('label', 'alphanohtml');
|
||||||
|
$mediumType->label_short = GETPOST('label_short', 'alphanohtml');
|
||||||
|
$mediumType->description = GETPOST('description', 'restricthtml');
|
||||||
|
$mediumType->fk_system = GETPOSTINT('fk_system');
|
||||||
|
$mediumType->category = GETPOST('category', 'alphanohtml');
|
||||||
|
$mediumType->default_spec = GETPOST('default_spec', 'alphanohtml');
|
||||||
|
$mediumType->color = GETPOST('color', 'alphanohtml');
|
||||||
|
$mediumType->position = GETPOSTINT('position');
|
||||||
|
$mediumType->active = GETPOSTINT('active');
|
||||||
|
|
||||||
|
// Available specs as JSON array
|
||||||
|
$specsText = GETPOST('available_specs', 'nohtml');
|
||||||
|
if ($specsText) {
|
||||||
|
$specsArray = array_map('trim', explode(',', $specsText));
|
||||||
|
$mediumType->available_specs = json_encode($specsArray);
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = $mediumType->create($user);
|
||||||
|
if ($result > 0) {
|
||||||
|
setEventMessages($langs->trans('RecordSaved'), null, 'mesgs');
|
||||||
|
header('Location: '.$_SERVER['PHP_SELF']);
|
||||||
|
exit;
|
||||||
|
} else {
|
||||||
|
setEventMessages($mediumType->error, $mediumType->errors, 'errors');
|
||||||
|
$action = 'create';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($action == 'update' && $user->admin) {
|
||||||
|
if ($mediumType->fetch($typeId) > 0) {
|
||||||
|
$mediumType->ref = GETPOST('ref', 'alphanohtml');
|
||||||
|
$mediumType->label = GETPOST('label', 'alphanohtml');
|
||||||
|
$mediumType->label_short = GETPOST('label_short', 'alphanohtml');
|
||||||
|
$mediumType->description = GETPOST('description', 'restricthtml');
|
||||||
|
$mediumType->fk_system = GETPOSTINT('fk_system');
|
||||||
|
$mediumType->category = GETPOST('category', 'alphanohtml');
|
||||||
|
$mediumType->default_spec = GETPOST('default_spec', 'alphanohtml');
|
||||||
|
$mediumType->color = GETPOST('color', 'alphanohtml');
|
||||||
|
$mediumType->position = GETPOSTINT('position');
|
||||||
|
$mediumType->active = GETPOSTINT('active');
|
||||||
|
|
||||||
|
$specsText = GETPOST('available_specs', 'nohtml');
|
||||||
|
if ($specsText) {
|
||||||
|
$specsArray = array_map('trim', explode(',', $specsText));
|
||||||
|
$mediumType->available_specs = json_encode($specsArray);
|
||||||
|
} else {
|
||||||
|
$mediumType->available_specs = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = $mediumType->update($user);
|
||||||
|
if ($result > 0) {
|
||||||
|
setEventMessages($langs->trans('RecordSaved'), null, 'mesgs');
|
||||||
|
header('Location: '.$_SERVER['PHP_SELF']);
|
||||||
|
exit;
|
||||||
|
} else {
|
||||||
|
setEventMessages($mediumType->error, $mediumType->errors, 'errors');
|
||||||
|
$action = 'edit';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($action == 'confirm_delete' && GETPOST('confirm') == 'yes' && $user->admin) {
|
||||||
|
if ($mediumType->fetch($typeId) > 0) {
|
||||||
|
$result = $mediumType->delete($user);
|
||||||
|
if ($result > 0) {
|
||||||
|
setEventMessages($langs->trans('RecordDeleted'), null, 'mesgs');
|
||||||
|
} else {
|
||||||
|
setEventMessages($mediumType->error, $mediumType->errors, 'errors');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
header('Location: '.$_SERVER['PHP_SELF']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* View
|
||||||
|
*/
|
||||||
|
|
||||||
|
$title = $langs->trans('MediumTypes');
|
||||||
|
llxHeader('', $title);
|
||||||
|
|
||||||
|
$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');
|
||||||
|
|
||||||
|
// Admin tabs
|
||||||
|
$head = kundenkarteAdminPrepareHead();
|
||||||
|
print dol_get_fiche_head($head, 'medium_types', $langs->trans('KundenKarte'), -1, 'kundenkarte@kundenkarte');
|
||||||
|
|
||||||
|
// Delete confirmation
|
||||||
|
if ($action == 'delete') {
|
||||||
|
if ($mediumType->fetch($typeId) > 0) {
|
||||||
|
print $form->formconfirm(
|
||||||
|
$_SERVER['PHP_SELF'].'?typeid='.$typeId,
|
||||||
|
$langs->trans('DeleteMediumType'),
|
||||||
|
$langs->trans('ConfirmDeleteMediumType', $mediumType->label),
|
||||||
|
'confirm_delete',
|
||||||
|
'',
|
||||||
|
0,
|
||||||
|
1
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add/Edit form
|
||||||
|
if (in_array($action, array('create', 'edit'))) {
|
||||||
|
if ($action == 'edit' && $typeId > 0) {
|
||||||
|
$mediumType->fetch($typeId);
|
||||||
|
}
|
||||||
|
|
||||||
|
print '<form method="POST" action="'.$_SERVER['PHP_SELF'].'">';
|
||||||
|
print '<input type="hidden" name="token" value="'.newToken().'">';
|
||||||
|
print '<input type="hidden" name="action" value="'.($action == 'edit' ? 'update' : 'add').'">';
|
||||||
|
if ($action == 'edit') {
|
||||||
|
print '<input type="hidden" name="typeid" value="'.$typeId.'">';
|
||||||
|
}
|
||||||
|
|
||||||
|
print '<table class="border centpercent">';
|
||||||
|
|
||||||
|
// Ref
|
||||||
|
print '<tr><td class="titlefieldcreate fieldrequired">'.$langs->trans('Ref').'</td>';
|
||||||
|
print '<td><input type="text" name="ref" value="'.dol_escape_htmltag($mediumType->ref ?: GETPOST('ref')).'" size="20" maxlength="50" required></td></tr>';
|
||||||
|
|
||||||
|
// Label
|
||||||
|
print '<tr><td class="fieldrequired">'.$langs->trans('Label').'</td>';
|
||||||
|
print '<td><input type="text" name="label" value="'.dol_escape_htmltag($mediumType->label ?: GETPOST('label')).'" size="40" maxlength="128" required></td></tr>';
|
||||||
|
|
||||||
|
// Label short
|
||||||
|
print '<tr><td>'.$langs->trans('LabelShort').'</td>';
|
||||||
|
print '<td><input type="text" name="label_short" value="'.dol_escape_htmltag($mediumType->label_short ?: GETPOST('label_short')).'" size="20" maxlength="32"></td></tr>';
|
||||||
|
|
||||||
|
// System
|
||||||
|
print '<tr><td>'.$langs->trans('System').'</td>';
|
||||||
|
print '<td><select name="fk_system" class="flat">';
|
||||||
|
print '<option value="0">'.$langs->trans('AllSystems').'</option>';
|
||||||
|
foreach ($systems as $sys) {
|
||||||
|
$selected = ($mediumType->fk_system == $sys->rowid) ? ' selected' : '';
|
||||||
|
print '<option value="'.$sys->rowid.'"'.$selected.'>'.dol_escape_htmltag($sys->label).'</option>';
|
||||||
|
}
|
||||||
|
print '</select></td></tr>';
|
||||||
|
|
||||||
|
// Category
|
||||||
|
print '<tr><td>'.$langs->trans('Category').'</td>';
|
||||||
|
print '<td><select name="category" class="flat">';
|
||||||
|
$categories = MediumType::getCategoryOptions();
|
||||||
|
foreach ($categories as $code => $label) {
|
||||||
|
$selected = ($mediumType->category == $code) ? ' selected' : '';
|
||||||
|
print '<option value="'.$code.'"'.$selected.'>'.dol_escape_htmltag($label).'</option>';
|
||||||
|
}
|
||||||
|
print '</select></td></tr>';
|
||||||
|
|
||||||
|
// Default spec
|
||||||
|
print '<tr><td>'.$langs->trans('DefaultSpec').'</td>';
|
||||||
|
print '<td><input type="text" name="default_spec" value="'.dol_escape_htmltag($mediumType->default_spec ?: GETPOST('default_spec')).'" size="20" maxlength="100">';
|
||||||
|
print '<br><span class="opacitymedium">'.$langs->trans('DefaultSpecHelp').'</span></td></tr>';
|
||||||
|
|
||||||
|
// Available specs
|
||||||
|
$specsText = '';
|
||||||
|
if ($mediumType->available_specs) {
|
||||||
|
$specsArray = json_decode($mediumType->available_specs, true);
|
||||||
|
if (is_array($specsArray)) {
|
||||||
|
$specsText = implode(', ', $specsArray);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
print '<tr><td>'.$langs->trans('AvailableSpecs').'</td>';
|
||||||
|
print '<td><input type="text" name="available_specs" value="'.dol_escape_htmltag($specsText ?: GETPOST('available_specs')).'" size="60">';
|
||||||
|
print '<br><span class="opacitymedium">'.$langs->trans('AvailableSpecsHelp').'</span></td></tr>';
|
||||||
|
|
||||||
|
// Color
|
||||||
|
print '<tr><td>'.$langs->trans('Color').'</td>';
|
||||||
|
print '<td><input type="color" name="color" value="'.dol_escape_htmltag($mediumType->color ?: '#666666').'" style="width:50px;height:30px;">';
|
||||||
|
print ' <input type="text" name="color_text" value="'.dol_escape_htmltag($mediumType->color ?: '#666666').'" size="10" onchange="this.form.color.value=this.value" placeholder="#RRGGBB"></td></tr>';
|
||||||
|
|
||||||
|
// Position
|
||||||
|
print '<tr><td>'.$langs->trans('Position').'</td>';
|
||||||
|
print '<td><input type="number" name="position" value="'.((int) ($mediumType->position ?: GETPOSTINT('position'))).'" min="0" max="999"></td></tr>';
|
||||||
|
|
||||||
|
// Status
|
||||||
|
print '<tr><td>'.$langs->trans('Status').'</td>';
|
||||||
|
print '<td><select name="active" class="flat">';
|
||||||
|
$activeValue = ($action == 'edit') ? $mediumType->active : 1;
|
||||||
|
print '<option value="1"'.($activeValue == 1 ? ' selected' : '').'>'.$langs->trans('Enabled').'</option>';
|
||||||
|
print '<option value="0"'.($activeValue == 0 ? ' selected' : '').'>'.$langs->trans('Disabled').'</option>';
|
||||||
|
print '</select></td></tr>';
|
||||||
|
|
||||||
|
// Description
|
||||||
|
print '<tr><td>'.$langs->trans('Description').'</td>';
|
||||||
|
print '<td><textarea name="description" rows="3" cols="60">'.dol_escape_htmltag($mediumType->description ?: GETPOST('description')).'</textarea></td></tr>';
|
||||||
|
|
||||||
|
print '</table>';
|
||||||
|
|
||||||
|
print '<div class="center" style="margin-top:15px;">';
|
||||||
|
print '<input type="submit" class="button button-save" value="'.$langs->trans('Save').'">';
|
||||||
|
print ' <a class="button button-cancel" href="'.$_SERVER['PHP_SELF'].'">'.$langs->trans('Cancel').'</a>';
|
||||||
|
print '</div>';
|
||||||
|
|
||||||
|
print '</form>';
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// List view
|
||||||
|
|
||||||
|
// Button to add
|
||||||
|
print '<div class="tabsAction">';
|
||||||
|
print '<a class="butAction" href="'.$_SERVER['PHP_SELF'].'?action=create">'.$langs->trans('AddMediumType').'</a>';
|
||||||
|
print '</div>';
|
||||||
|
|
||||||
|
// Filter by category
|
||||||
|
$filterCategory = GETPOST('filter_category', 'alphanohtml');
|
||||||
|
print '<form method="GET" action="'.$_SERVER['PHP_SELF'].'" style="margin-bottom:15px;">';
|
||||||
|
print '<label>'.$langs->trans('FilterByCategory').': </label>';
|
||||||
|
print '<select name="filter_category" class="flat" onchange="this.form.submit()">';
|
||||||
|
print '<option value="">'.$langs->trans('All').'</option>';
|
||||||
|
$categories = MediumType::getCategoryOptions();
|
||||||
|
foreach ($categories as $code => $label) {
|
||||||
|
$selected = ($filterCategory == $code) ? ' selected' : '';
|
||||||
|
print '<option value="'.$code.'"'.$selected.'>'.dol_escape_htmltag($label).'</option>';
|
||||||
|
}
|
||||||
|
print '</select>';
|
||||||
|
print '</form>';
|
||||||
|
|
||||||
|
// List
|
||||||
|
$allTypes = $mediumType->fetchAllBySystem(0, 0);
|
||||||
|
|
||||||
|
print '<table class="noborder centpercent">';
|
||||||
|
print '<tr class="liste_titre">';
|
||||||
|
print '<th>'.$langs->trans('Ref').'</th>';
|
||||||
|
print '<th>'.$langs->trans('Label').'</th>';
|
||||||
|
print '<th>'.$langs->trans('Category').'</th>';
|
||||||
|
print '<th>'.$langs->trans('System').'</th>';
|
||||||
|
print '<th>'.$langs->trans('DefaultSpec').'</th>';
|
||||||
|
print '<th class="center">'.$langs->trans('Color').'</th>';
|
||||||
|
print '<th class="center">'.$langs->trans('Position').'</th>';
|
||||||
|
print '<th class="center">'.$langs->trans('Status').'</th>';
|
||||||
|
print '<th class="center">'.$langs->trans('Actions').'</th>';
|
||||||
|
print '</tr>';
|
||||||
|
|
||||||
|
if (empty($allTypes)) {
|
||||||
|
print '<tr><td colspan="9" class="opacitymedium">'.$langs->trans('NoRecords').'</td></tr>';
|
||||||
|
} else {
|
||||||
|
$i = 0;
|
||||||
|
foreach ($allTypes as $t) {
|
||||||
|
// Filter
|
||||||
|
if ($filterCategory && $t->category != $filterCategory) continue;
|
||||||
|
|
||||||
|
print '<tr class="oddeven">';
|
||||||
|
print '<td><strong>'.dol_escape_htmltag($t->ref).'</strong></td>';
|
||||||
|
print '<td>'.dol_escape_htmltag($t->label);
|
||||||
|
if ($t->label_short) print ' <span class="opacitymedium">('.dol_escape_htmltag($t->label_short).')</span>';
|
||||||
|
print '</td>';
|
||||||
|
print '<td>'.dol_escape_htmltag($t->getCategoryLabel()).'</td>';
|
||||||
|
print '<td>';
|
||||||
|
if ($t->fk_system > 0 && $t->system_label) {
|
||||||
|
print dol_escape_htmltag($t->system_label);
|
||||||
|
} else {
|
||||||
|
print '<em>'.$langs->trans('AllSystems').'</em>';
|
||||||
|
}
|
||||||
|
print '</td>';
|
||||||
|
print '<td>'.dol_escape_htmltag($t->default_spec).'</td>';
|
||||||
|
print '<td class="center"><span style="display:inline-block;width:20px;height:20px;background:'.dol_escape_htmltag($t->color ?: '#ccc').';border:1px solid #999;border-radius:3px;"></span></td>';
|
||||||
|
print '<td class="center">'.$t->position.'</td>';
|
||||||
|
print '<td class="center">';
|
||||||
|
print $t->active ? '<span class="badge badge-status4">'.$langs->trans('Enabled').'</span>' : '<span class="badge badge-status5">'.$langs->trans('Disabled').'</span>';
|
||||||
|
print '</td>';
|
||||||
|
print '<td class="center nowraponall">';
|
||||||
|
print '<a href="'.$_SERVER['PHP_SELF'].'?action=edit&typeid='.$t->id.'"><i class="fa fa-edit" title="'.$langs->trans('Edit').'"></i></a>';
|
||||||
|
if (!$t->is_system) {
|
||||||
|
print ' <a href="'.$_SERVER['PHP_SELF'].'?action=delete&typeid='.$t->id.'"><i class="fa fa-trash" style="color:#c00;" title="'.$langs->trans('Delete').'"></i></a>';
|
||||||
|
}
|
||||||
|
print '</td>';
|
||||||
|
print '</tr>';
|
||||||
|
$i++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
print '</table>';
|
||||||
|
}
|
||||||
|
|
||||||
|
print dol_get_fiche_end();
|
||||||
|
|
||||||
|
// JavaScript for color picker sync
|
||||||
|
print '<script>
|
||||||
|
document.querySelector("input[name=color]").addEventListener("input", function() {
|
||||||
|
document.querySelector("input[name=color_text]").value = this.value;
|
||||||
|
});
|
||||||
|
</script>';
|
||||||
|
|
||||||
|
llxFooter();
|
||||||
|
$db->close();
|
||||||
412
admin/setup.php
Executable file
412
admin/setup.php
Executable file
|
|
@ -0,0 +1,412 @@
|
||||||
|
<?php
|
||||||
|
/* Copyright (C) 2026 Eduard Wisch <data@data-it-solution.de>
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* \file kundenkarte/admin/setup.php
|
||||||
|
* \ingroup kundenkarte
|
||||||
|
* \brief KundenKarte setup page.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 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 '../lib/kundenkarte.lib.php';
|
||||||
|
|
||||||
|
// Translations
|
||||||
|
$langs->loadLangs(array("admin", "kundenkarte@kundenkarte"));
|
||||||
|
|
||||||
|
// Access control
|
||||||
|
if (!$user->admin && !$user->hasRight('kundenkarte', 'admin')) {
|
||||||
|
accessforbidden();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parameters
|
||||||
|
$action = GETPOST('action', 'aZ09');
|
||||||
|
|
||||||
|
// Directory for PDF templates
|
||||||
|
$uploadDir = $conf->kundenkarte->dir_output.'/templates';
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Actions
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Handle PDF template upload
|
||||||
|
if ($action == 'upload_template') {
|
||||||
|
$error = 0;
|
||||||
|
|
||||||
|
if (!empty($_FILES['pdf_template']['name'])) {
|
||||||
|
// Check file type
|
||||||
|
$fileInfo = pathinfo($_FILES['pdf_template']['name']);
|
||||||
|
if (strtolower($fileInfo['extension']) !== 'pdf') {
|
||||||
|
setEventMessages($langs->trans("ErrorOnlyPDFAllowed"), null, 'errors');
|
||||||
|
$error++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$error) {
|
||||||
|
// Create directory if not exists
|
||||||
|
if (!is_dir($uploadDir)) {
|
||||||
|
dol_mkdir($uploadDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save template as fixed name
|
||||||
|
$targetFile = $uploadDir.'/export_template.pdf';
|
||||||
|
|
||||||
|
if (move_uploaded_file($_FILES['pdf_template']['tmp_name'], $targetFile)) {
|
||||||
|
dolibarr_set_const($db, 'KUNDENKARTE_PDF_TEMPLATE', 'export_template.pdf', 'chaine', 0, '', $conf->entity);
|
||||||
|
setEventMessages($langs->trans("TemplateUploadSuccess"), null, 'mesgs');
|
||||||
|
} else {
|
||||||
|
setEventMessages($langs->trans("ErrorUploadFailed"), null, 'errors');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setEventMessages($langs->trans("ErrorNoFileSelected"), null, 'errors');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle template deletion
|
||||||
|
if ($action == 'delete_template') {
|
||||||
|
$templateFile = $uploadDir.'/export_template.pdf';
|
||||||
|
if (file_exists($templateFile)) {
|
||||||
|
unlink($templateFile);
|
||||||
|
dolibarr_set_const($db, 'KUNDENKARTE_PDF_TEMPLATE', '', 'chaine', 0, '', $conf->entity);
|
||||||
|
setEventMessages($langs->trans("TemplateDeleted"), null, 'mesgs');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($action == 'update') {
|
||||||
|
$error = 0;
|
||||||
|
|
||||||
|
// Save settings
|
||||||
|
$res = dolibarr_set_const($db, 'KUNDENKARTE_SHOW_FAVORITES_TAB', GETPOSTINT('KUNDENKARTE_SHOW_FAVORITES_TAB'), 'chaine', 0, '', $conf->entity);
|
||||||
|
if (!($res > 0)) {
|
||||||
|
$error++;
|
||||||
|
}
|
||||||
|
|
||||||
|
$res = dolibarr_set_const($db, 'KUNDENKARTE_SHOW_ANLAGEN_TAB', GETPOSTINT('KUNDENKARTE_SHOW_ANLAGEN_TAB'), 'chaine', 0, '', $conf->entity);
|
||||||
|
if (!($res > 0)) {
|
||||||
|
$error++;
|
||||||
|
}
|
||||||
|
|
||||||
|
$res = dolibarr_set_const($db, 'KUNDENKARTE_DEFAULT_ORDER_TYPE', GETPOSTINT('KUNDENKARTE_DEFAULT_ORDER_TYPE'), 'chaine', 0, '', $conf->entity);
|
||||||
|
if (!($res > 0)) {
|
||||||
|
$error++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// PDF font size settings
|
||||||
|
dolibarr_set_const($db, 'KUNDENKARTE_PDF_FONT_HEADER', GETPOSTINT('KUNDENKARTE_PDF_FONT_HEADER'), 'chaine', 0, '', $conf->entity);
|
||||||
|
dolibarr_set_const($db, 'KUNDENKARTE_PDF_FONT_CONTENT', GETPOSTINT('KUNDENKARTE_PDF_FONT_CONTENT'), 'chaine', 0, '', $conf->entity);
|
||||||
|
dolibarr_set_const($db, 'KUNDENKARTE_PDF_FONT_FIELDS', GETPOSTINT('KUNDENKARTE_PDF_FONT_FIELDS'), 'chaine', 0, '', $conf->entity);
|
||||||
|
|
||||||
|
// View mode
|
||||||
|
dolibarr_set_const($db, 'KUNDENKARTE_DEFAULT_VIEW', GETPOST('KUNDENKARTE_DEFAULT_VIEW', 'aZ09'), '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 {
|
||||||
|
setEventMessages($langs->trans("Error"), null, 'errors');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* View
|
||||||
|
*/
|
||||||
|
|
||||||
|
$form = new Form($db);
|
||||||
|
|
||||||
|
$title = $langs->trans("KundenKarteSetup");
|
||||||
|
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, 'settings', $langs->trans('ModuleKundenKarteName'), -1, "fa-address-card");
|
||||||
|
|
||||||
|
print '<span class="opacitymedium">'.$langs->trans("KundenKarteSetupPage").'</span><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="update">';
|
||||||
|
|
||||||
|
print '<table class="noborder centpercent">';
|
||||||
|
|
||||||
|
// Header
|
||||||
|
print '<tr class="liste_titre">';
|
||||||
|
print '<td>'.$langs->trans("Parameter").'</td>';
|
||||||
|
print '<td>'.$langs->trans("Value").'</td>';
|
||||||
|
print '</tr>';
|
||||||
|
|
||||||
|
// Show Favorites Tab
|
||||||
|
print '<tr class="oddeven">';
|
||||||
|
print '<td>'.$langs->trans("ShowFavoritesTab").'</td>';
|
||||||
|
print '<td>';
|
||||||
|
print $form->selectyesno('KUNDENKARTE_SHOW_FAVORITES_TAB', getDolGlobalInt('KUNDENKARTE_SHOW_FAVORITES_TAB', 1), 1);
|
||||||
|
print '</td>';
|
||||||
|
print '</tr>';
|
||||||
|
|
||||||
|
// Show Anlagen Tab
|
||||||
|
print '<tr class="oddeven">';
|
||||||
|
print '<td>'.$langs->trans("ShowAnlagenTab").'</td>';
|
||||||
|
print '<td>';
|
||||||
|
print $form->selectyesno('KUNDENKARTE_SHOW_ANLAGEN_TAB', getDolGlobalInt('KUNDENKARTE_SHOW_ANLAGEN_TAB', 1), 1);
|
||||||
|
print '</td>';
|
||||||
|
print '</tr>';
|
||||||
|
|
||||||
|
// Default Order Type for Favorites
|
||||||
|
print '<tr class="oddeven">';
|
||||||
|
print '<td>'.$langs->trans("DefaultOrderType").'</td>';
|
||||||
|
print '<td>';
|
||||||
|
$orderTypes = array(
|
||||||
|
0 => $langs->trans("OrderTypeOrder"),
|
||||||
|
1 => $langs->trans("OrderTypeProposal"),
|
||||||
|
);
|
||||||
|
print $form->selectarray('KUNDENKARTE_DEFAULT_ORDER_TYPE', $orderTypes, getDolGlobalInt('KUNDENKARTE_DEFAULT_ORDER_TYPE', 0));
|
||||||
|
print '</td>';
|
||||||
|
print '</tr>';
|
||||||
|
|
||||||
|
// Default View Mode for Anlagen
|
||||||
|
print '<tr class="oddeven">';
|
||||||
|
print '<td>'.$langs->trans("DefaultViewMode").'</td>';
|
||||||
|
print '<td>';
|
||||||
|
$viewModes = array(
|
||||||
|
'tree' => $langs->trans("ViewModeTree"),
|
||||||
|
'graph' => $langs->trans("ViewModeGraph"),
|
||||||
|
);
|
||||||
|
print $form->selectarray('KUNDENKARTE_DEFAULT_VIEW', $viewModes, getDolGlobalString('KUNDENKARTE_DEFAULT_VIEW', 'tree'));
|
||||||
|
print '</td>';
|
||||||
|
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>';
|
||||||
|
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 class="opacitymedium">'.$langs->trans("Description").'</td>';
|
||||||
|
print '</tr>';
|
||||||
|
|
||||||
|
// Header font size
|
||||||
|
print '<tr class="oddeven">';
|
||||||
|
print '<td>'.$langs->trans("PDFFontHeader").'</td>';
|
||||||
|
print '<td>';
|
||||||
|
print '<select name="KUNDENKARTE_PDF_FONT_HEADER" class="flat">';
|
||||||
|
for ($i = 7; $i <= 14; $i++) {
|
||||||
|
$sel = (getDolGlobalInt('KUNDENKARTE_PDF_FONT_HEADER', 9) == $i) ? ' selected' : '';
|
||||||
|
print '<option value="'.$i.'"'.$sel.'>'.$i.' pt</option>';
|
||||||
|
}
|
||||||
|
print '</select>';
|
||||||
|
print '</td>';
|
||||||
|
print '<td class="opacitymedium small">'.$langs->trans("PDFFontHeaderHelp").'</td>';
|
||||||
|
print '</tr>';
|
||||||
|
|
||||||
|
// Content font size
|
||||||
|
print '<tr class="oddeven">';
|
||||||
|
print '<td>'.$langs->trans("PDFFontContent").'</td>';
|
||||||
|
print '<td>';
|
||||||
|
print '<select name="KUNDENKARTE_PDF_FONT_CONTENT" class="flat">';
|
||||||
|
for ($i = 6; $i <= 12; $i++) {
|
||||||
|
$sel = (getDolGlobalInt('KUNDENKARTE_PDF_FONT_CONTENT', 7) == $i) ? ' selected' : '';
|
||||||
|
print '<option value="'.$i.'"'.$sel.'>'.$i.' pt</option>';
|
||||||
|
}
|
||||||
|
print '</select>';
|
||||||
|
print '</td>';
|
||||||
|
print '<td class="opacitymedium small">'.$langs->trans("PDFFontContentHelp").'</td>';
|
||||||
|
print '</tr>';
|
||||||
|
|
||||||
|
// Field label font size
|
||||||
|
print '<tr class="oddeven">';
|
||||||
|
print '<td>'.$langs->trans("PDFFontFields").'</td>';
|
||||||
|
print '<td>';
|
||||||
|
print '<select name="KUNDENKARTE_PDF_FONT_FIELDS" class="flat">';
|
||||||
|
for ($i = 5; $i <= 10; $i++) {
|
||||||
|
$sel = (getDolGlobalInt('KUNDENKARTE_PDF_FONT_FIELDS', 7) == $i) ? ' selected' : '';
|
||||||
|
print '<option value="'.$i.'"'.$sel.'>'.$i.' pt</option>';
|
||||||
|
}
|
||||||
|
print '</select>';
|
||||||
|
print '</td>';
|
||||||
|
print '<td class="opacitymedium small">'.$langs->trans("PDFFontFieldsHelp").'</td>';
|
||||||
|
print '</tr>';
|
||||||
|
|
||||||
|
print '</table>';
|
||||||
|
|
||||||
|
print '<br>';
|
||||||
|
print '<div class="center">';
|
||||||
|
print '<input type="submit" class="button" value="'.$langs->trans("Save").'">';
|
||||||
|
print '</div>';
|
||||||
|
|
||||||
|
print '</form>';
|
||||||
|
|
||||||
|
// PDF Template Section
|
||||||
|
print '<br><br>';
|
||||||
|
print '<div class="titre inline-block">'.$langs->trans("PDFExportTemplate").'</div>';
|
||||||
|
print '<br><br>';
|
||||||
|
|
||||||
|
$templateFile = $uploadDir.'/export_template.pdf';
|
||||||
|
$hasTemplate = file_exists($templateFile);
|
||||||
|
|
||||||
|
print '<table class="noborder centpercent">';
|
||||||
|
print '<tr class="liste_titre">';
|
||||||
|
print '<td colspan="2">'.$langs->trans("PDFTemplate").'</td>';
|
||||||
|
print '</tr>';
|
||||||
|
|
||||||
|
print '<tr class="oddeven">';
|
||||||
|
print '<td style="width:50%">'.$langs->trans("CurrentTemplate").'</td>';
|
||||||
|
print '<td>';
|
||||||
|
if ($hasTemplate) {
|
||||||
|
print '<span class="badge badge-success" style="padding: 5px 10px;">';
|
||||||
|
print '<i class="fas fa-file-pdf"></i> export_template.pdf';
|
||||||
|
print '</span>';
|
||||||
|
print ' <span class="opacitymedium">('.dol_print_size(filesize($templateFile)).')</span>';
|
||||||
|
print '<br><br>';
|
||||||
|
print '<a class="button buttongen" href="'.$_SERVER['PHP_SELF'].'?action=delete_template&token='.newToken().'" onclick="return confirm(\''.$langs->trans('ConfirmDeleteTemplate').'\');">';
|
||||||
|
print '<i class="fas fa-trash"></i> '.$langs->trans("DeleteTemplate");
|
||||||
|
print '</a>';
|
||||||
|
} else {
|
||||||
|
print '<span class="opacitymedium">'.$langs->trans("NoTemplateUploaded").'</span>';
|
||||||
|
}
|
||||||
|
print '</td>';
|
||||||
|
print '</tr>';
|
||||||
|
|
||||||
|
print '<tr class="oddeven">';
|
||||||
|
print '<td>'.$langs->trans("UploadNewTemplate").'</td>';
|
||||||
|
print '<td>';
|
||||||
|
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_template">';
|
||||||
|
print '<input type="file" name="pdf_template" accept=".pdf" class="flat" style="max-width:300px;">';
|
||||||
|
print ' <input type="submit" class="button buttongen smallpaddingimp" value="'.$langs->trans("Upload").'">';
|
||||||
|
print '</form>';
|
||||||
|
print '<br><span class="opacitymedium small">'.$langs->trans("PDFTemplateHelp").'</span>';
|
||||||
|
print '</td>';
|
||||||
|
print '</tr>';
|
||||||
|
|
||||||
|
print '</table>';
|
||||||
|
|
||||||
|
// Info section
|
||||||
|
print '<br>';
|
||||||
|
print '<div class="info">';
|
||||||
|
print '<strong>'.$langs->trans("ConfigurationHelp").':</strong><br>';
|
||||||
|
print '• '.$langs->trans("ConfigHelpSystems").'<br>';
|
||||||
|
print '• '.$langs->trans("ConfigHelpTypes").'<br>';
|
||||||
|
print '</div>';
|
||||||
|
|
||||||
|
print dol_get_fiche_end();
|
||||||
|
|
||||||
|
llxFooter();
|
||||||
|
$db->close();
|
||||||
158
ajax/anlage.php
Executable file
158
ajax/anlage.php
Executable file
|
|
@ -0,0 +1,158 @@
|
||||||
|
<?php
|
||||||
|
/* Copyright (C) 2026 Alles Watt lauft
|
||||||
|
*
|
||||||
|
* AJAX endpoint for Anlage operations
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (!defined('NOTOKENRENEWAL')) define('NOTOKENRENEWAL', '1');
|
||||||
|
if (!defined('NOREQUIREMENU')) define('NOREQUIREMENU', '1');
|
||||||
|
if (!defined('NOREQUIREHTML')) define('NOREQUIREHTML', '1');
|
||||||
|
if (!defined('NOREQUIREAJAX')) define('NOREQUIREAJAX', '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/anlage.class.php');
|
||||||
|
|
||||||
|
header('Content-Type: application/json; charset=UTF-8');
|
||||||
|
|
||||||
|
$langs->loadLangs(array('kundenkarte@kundenkarte'));
|
||||||
|
|
||||||
|
$action = GETPOST('action', 'aZ09');
|
||||||
|
$socId = GETPOSTINT('socid');
|
||||||
|
$contactId = GETPOSTINT('contactid');
|
||||||
|
$systemId = GETPOSTINT('system_id');
|
||||||
|
$anlageId = GETPOSTINT('anlage_id');
|
||||||
|
|
||||||
|
$response = array('success' => false, 'error' => '');
|
||||||
|
|
||||||
|
// Security check
|
||||||
|
if (!$user->hasRight('kundenkarte', 'read')) {
|
||||||
|
$response['error'] = $langs->trans('ErrorPermissionDenied');
|
||||||
|
echo json_encode($response);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$anlage = new Anlage($db);
|
||||||
|
|
||||||
|
// Helper function to convert tree objects to clean arrays
|
||||||
|
function treeToArray($nodes) {
|
||||||
|
$result = array();
|
||||||
|
foreach ($nodes as $node) {
|
||||||
|
$item = array(
|
||||||
|
'id' => $node->id,
|
||||||
|
'ref' => $node->ref,
|
||||||
|
'label' => $node->label,
|
||||||
|
'fk_parent' => $node->fk_parent,
|
||||||
|
'fk_system' => $node->fk_system,
|
||||||
|
'type_label' => $node->type_label,
|
||||||
|
'status' => $node->status
|
||||||
|
);
|
||||||
|
if (!empty($node->children)) {
|
||||||
|
$item['children'] = treeToArray($node->children);
|
||||||
|
} else {
|
||||||
|
$item['children'] = array();
|
||||||
|
}
|
||||||
|
$result[] = $item;
|
||||||
|
}
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch ($action) {
|
||||||
|
case 'tree':
|
||||||
|
// Get tree structure for a customer/system
|
||||||
|
if ($socId > 0) {
|
||||||
|
if ($contactId > 0) {
|
||||||
|
$tree = $anlage->fetchTreeByContact($socId, $contactId, $systemId);
|
||||||
|
} else {
|
||||||
|
$tree = $anlage->fetchTree($socId, $systemId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to clean array (removes db connection and other internal data)
|
||||||
|
$response['success'] = true;
|
||||||
|
$response['tree'] = treeToArray($tree);
|
||||||
|
} else {
|
||||||
|
$response['error'] = 'Missing socid';
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'list':
|
||||||
|
// Get flat list of anlagen for a customer/system (derived from tree)
|
||||||
|
if ($socId > 0) {
|
||||||
|
$tree = $anlage->fetchTree($socId, $systemId);
|
||||||
|
|
||||||
|
// Flatten tree to list
|
||||||
|
$result = array();
|
||||||
|
$flattenTree = function($nodes, $prefix = '') use (&$flattenTree, &$result) {
|
||||||
|
foreach ($nodes as $node) {
|
||||||
|
$result[] = array(
|
||||||
|
'id' => $node->id,
|
||||||
|
'ref' => $node->ref,
|
||||||
|
'label' => $node->label,
|
||||||
|
'display_label' => $prefix . $node->label,
|
||||||
|
'fk_parent' => $node->fk_parent,
|
||||||
|
'type_label' => $node->type_label,
|
||||||
|
'status' => $node->status
|
||||||
|
);
|
||||||
|
if (!empty($node->children)) {
|
||||||
|
$flattenTree($node->children, $prefix . ' ');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
$flattenTree($tree);
|
||||||
|
|
||||||
|
$response['success'] = true;
|
||||||
|
$response['anlagen'] = $result;
|
||||||
|
} else {
|
||||||
|
$response['error'] = 'Missing socid';
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'get':
|
||||||
|
// Get single anlage
|
||||||
|
if ($anlageId > 0 && $anlage->fetch($anlageId) > 0) {
|
||||||
|
$response['success'] = true;
|
||||||
|
$response['anlage'] = array(
|
||||||
|
'id' => $anlage->id,
|
||||||
|
'ref' => $anlage->ref,
|
||||||
|
'label' => $anlage->label,
|
||||||
|
'fk_parent' => $anlage->fk_parent,
|
||||||
|
'fk_anlage_type' => $anlage->fk_anlage_type,
|
||||||
|
'type_label' => $anlage->type_label,
|
||||||
|
'fk_system' => $anlage->fk_system,
|
||||||
|
'status' => $anlage->status,
|
||||||
|
'field_values' => $anlage->getFieldValues()
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
$response['error'] = $langs->trans('ErrorRecordNotFound');
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'reorder':
|
||||||
|
// Reihenfolge der Elemente aktualisieren
|
||||||
|
if (!$user->hasRight('kundenkarte', 'write')) {
|
||||||
|
$response['error'] = $langs->trans('ErrorPermissionDenied');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
$idsRaw = GETPOST('ids', 'array');
|
||||||
|
if (!empty($idsRaw) && is_array($idsRaw)) {
|
||||||
|
$ids = array_map('intval', $idsRaw);
|
||||||
|
$result = $anlage->updateRangs($ids);
|
||||||
|
if ($result > 0) {
|
||||||
|
$response['success'] = true;
|
||||||
|
} else {
|
||||||
|
$response['error'] = 'Fehler beim Speichern der Reihenfolge';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$response['error'] = 'Keine IDs übergeben';
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
$response['error'] = 'Unknown action';
|
||||||
|
}
|
||||||
|
|
||||||
|
echo json_encode($response);
|
||||||
193
ajax/anlage_connection.php
Executable file
193
ajax/anlage_connection.php
Executable file
|
|
@ -0,0 +1,193 @@
|
||||||
|
<?php
|
||||||
|
/* Copyright (C) 2026 Alles Watt lauft
|
||||||
|
*
|
||||||
|
* AJAX endpoint for anlage connections (Verbindungen zwischen Anlagen-Elementen)
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (!defined('NOTOKENRENEWAL')) define('NOTOKENRENEWAL', '1');
|
||||||
|
if (!defined('NOREQUIREMENU')) define('NOREQUIREMENU', '1');
|
||||||
|
if (!defined('NOREQUIREHTML')) define('NOREQUIREHTML', '1');
|
||||||
|
if (!defined('NOREQUIREAJAX')) define('NOREQUIREAJAX', '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/anlageconnection.class.php');
|
||||||
|
dol_include_once('/kundenkarte/class/mediumtype.class.php');
|
||||||
|
|
||||||
|
header('Content-Type: application/json; charset=UTF-8');
|
||||||
|
|
||||||
|
$langs->loadLangs(array('kundenkarte@kundenkarte'));
|
||||||
|
|
||||||
|
$action = GETPOST('action', 'aZ09');
|
||||||
|
$connectionId = GETPOSTINT('connection_id');
|
||||||
|
$anlageId = GETPOSTINT('anlage_id');
|
||||||
|
$socId = GETPOSTINT('soc_id');
|
||||||
|
$systemId = GETPOSTINT('system_id');
|
||||||
|
|
||||||
|
$response = array('success' => false, 'error' => '');
|
||||||
|
|
||||||
|
// Security check
|
||||||
|
if (!$user->hasRight('kundenkarte', 'read')) {
|
||||||
|
$response['error'] = $langs->trans('ErrorPermissionDenied');
|
||||||
|
echo json_encode($response);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$connection = new AnlageConnection($db);
|
||||||
|
|
||||||
|
switch ($action) {
|
||||||
|
case 'list':
|
||||||
|
// List connections for an anlage or customer
|
||||||
|
if ($anlageId > 0) {
|
||||||
|
$connections = $connection->fetchByAnlage($anlageId);
|
||||||
|
} elseif ($socId > 0) {
|
||||||
|
$connections = $connection->fetchBySociete($socId, $systemId);
|
||||||
|
} else {
|
||||||
|
$response['error'] = 'Missing anlage_id or soc_id';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = array();
|
||||||
|
foreach ($connections as $c) {
|
||||||
|
$result[] = array(
|
||||||
|
'id' => $c->id,
|
||||||
|
'fk_source' => $c->fk_source,
|
||||||
|
'source_label' => $c->source_label,
|
||||||
|
'source_ref' => $c->source_ref,
|
||||||
|
'fk_target' => $c->fk_target,
|
||||||
|
'target_label' => $c->target_label,
|
||||||
|
'target_ref' => $c->target_ref,
|
||||||
|
'label' => $c->label,
|
||||||
|
'fk_medium_type' => $c->fk_medium_type,
|
||||||
|
'medium_type_label' => $c->medium_type_label,
|
||||||
|
'medium_type_text' => $c->medium_type_text,
|
||||||
|
'medium_spec' => $c->medium_spec,
|
||||||
|
'medium_length' => $c->medium_length,
|
||||||
|
'medium_color' => $c->medium_color,
|
||||||
|
'route_description' => $c->route_description,
|
||||||
|
'installation_date' => $c->installation_date,
|
||||||
|
'status' => $c->status,
|
||||||
|
'display_label' => $c->getDisplayLabel()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$response['success'] = true;
|
||||||
|
$response['connections'] = $result;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'get':
|
||||||
|
// Get single connection
|
||||||
|
if ($connectionId > 0 && $connection->fetch($connectionId) > 0) {
|
||||||
|
$response['success'] = true;
|
||||||
|
$response['connection'] = array(
|
||||||
|
'id' => $connection->id,
|
||||||
|
'fk_source' => $connection->fk_source,
|
||||||
|
'source_label' => $connection->source_label,
|
||||||
|
'source_ref' => $connection->source_ref,
|
||||||
|
'fk_target' => $connection->fk_target,
|
||||||
|
'target_label' => $connection->target_label,
|
||||||
|
'target_ref' => $connection->target_ref,
|
||||||
|
'label' => $connection->label,
|
||||||
|
'fk_medium_type' => $connection->fk_medium_type,
|
||||||
|
'medium_type_label' => $connection->medium_type_label,
|
||||||
|
'medium_type_text' => $connection->medium_type_text,
|
||||||
|
'medium_spec' => $connection->medium_spec,
|
||||||
|
'medium_length' => $connection->medium_length,
|
||||||
|
'medium_color' => $connection->medium_color,
|
||||||
|
'route_description' => $connection->route_description,
|
||||||
|
'installation_date' => $connection->installation_date,
|
||||||
|
'status' => $connection->status
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
$response['error'] = $langs->trans('ErrorRecordNotFound');
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'create':
|
||||||
|
if (!$user->hasRight('kundenkarte', 'write')) {
|
||||||
|
$response['error'] = $langs->trans('ErrorPermissionDenied');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
$connection->fk_source = GETPOSTINT('fk_source');
|
||||||
|
$connection->fk_target = GETPOSTINT('fk_target');
|
||||||
|
$connection->label = GETPOST('label', 'alphanohtml');
|
||||||
|
$connection->fk_medium_type = GETPOSTINT('fk_medium_type');
|
||||||
|
$connection->medium_type_text = GETPOST('medium_type_text', 'alphanohtml');
|
||||||
|
$connection->medium_spec = GETPOST('medium_spec', 'alphanohtml');
|
||||||
|
$connection->medium_length = GETPOST('medium_length', 'alphanohtml');
|
||||||
|
$connection->medium_color = GETPOST('medium_color', 'alphanohtml');
|
||||||
|
$connection->route_description = GETPOST('route_description', 'restricthtml');
|
||||||
|
$connection->installation_date = GETPOST('installation_date', 'alpha');
|
||||||
|
$connection->status = 1;
|
||||||
|
|
||||||
|
if (empty($connection->fk_source) || empty($connection->fk_target)) {
|
||||||
|
$response['error'] = $langs->trans('ErrorFieldRequired', 'Source/Target');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = $connection->create($user);
|
||||||
|
if ($result > 0) {
|
||||||
|
$response['success'] = true;
|
||||||
|
$response['connection_id'] = $result;
|
||||||
|
} else {
|
||||||
|
$response['error'] = $connection->error;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'update':
|
||||||
|
if (!$user->hasRight('kundenkarte', 'write')) {
|
||||||
|
$response['error'] = $langs->trans('ErrorPermissionDenied');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($connection->fetch($connectionId) > 0) {
|
||||||
|
if (GETPOSTISSET('fk_source')) $connection->fk_source = GETPOSTINT('fk_source');
|
||||||
|
if (GETPOSTISSET('fk_target')) $connection->fk_target = GETPOSTINT('fk_target');
|
||||||
|
if (GETPOSTISSET('label')) $connection->label = GETPOST('label', 'alphanohtml');
|
||||||
|
if (GETPOSTISSET('fk_medium_type')) $connection->fk_medium_type = GETPOSTINT('fk_medium_type');
|
||||||
|
if (GETPOSTISSET('medium_type_text')) $connection->medium_type_text = GETPOST('medium_type_text', 'alphanohtml');
|
||||||
|
if (GETPOSTISSET('medium_spec')) $connection->medium_spec = GETPOST('medium_spec', 'alphanohtml');
|
||||||
|
if (GETPOSTISSET('medium_length')) $connection->medium_length = GETPOST('medium_length', 'alphanohtml');
|
||||||
|
if (GETPOSTISSET('medium_color')) $connection->medium_color = GETPOST('medium_color', 'alphanohtml');
|
||||||
|
if (GETPOSTISSET('route_description')) $connection->route_description = GETPOST('route_description', 'restricthtml');
|
||||||
|
if (GETPOSTISSET('installation_date')) $connection->installation_date = GETPOST('installation_date', 'alpha');
|
||||||
|
if (GETPOSTISSET('status')) $connection->status = GETPOSTINT('status');
|
||||||
|
|
||||||
|
$result = $connection->update($user);
|
||||||
|
if ($result > 0) {
|
||||||
|
$response['success'] = true;
|
||||||
|
} else {
|
||||||
|
$response['error'] = $connection->error;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$response['error'] = $langs->trans('ErrorRecordNotFound');
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'delete':
|
||||||
|
if (!$user->hasRight('kundenkarte', 'write')) {
|
||||||
|
$response['error'] = $langs->trans('ErrorPermissionDenied');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($connection->fetch($connectionId) > 0) {
|
||||||
|
$result = $connection->delete($user);
|
||||||
|
if ($result > 0) {
|
||||||
|
$response['success'] = true;
|
||||||
|
} else {
|
||||||
|
$response['error'] = $connection->error;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$response['error'] = $langs->trans('ErrorRecordNotFound');
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
$response['error'] = 'Unknown action';
|
||||||
|
}
|
||||||
|
|
||||||
|
echo json_encode($response);
|
||||||
61
ajax/anlage_docs.php
Executable file
61
ajax/anlage_docs.php
Executable file
|
|
@ -0,0 +1,61 @@
|
||||||
|
<?php
|
||||||
|
/* Copyright (C) 2026 Alles Watt lauft
|
||||||
|
*
|
||||||
|
* AJAX endpoint to get documents 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');
|
||||||
|
|
||||||
|
// Berechtigungsprüfung
|
||||||
|
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', 'docs' => array()));
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the anlage to know the socid
|
||||||
|
$anlage = new Anlage($db);
|
||||||
|
if ($anlage->fetch($anlageId) <= 0) {
|
||||||
|
echo json_encode(array('error' => 'Element not found', 'docs' => array()));
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get documents for this element (pdf and document types)
|
||||||
|
$anlagefile = new AnlageFile($db);
|
||||||
|
$files = $anlagefile->fetchAllByAnlage($anlageId);
|
||||||
|
|
||||||
|
$docs = array();
|
||||||
|
foreach ($files as $file) {
|
||||||
|
// Only include PDFs and documents, not images
|
||||||
|
if (in_array($file->file_type, array('pdf', 'document'))) {
|
||||||
|
$docs[] = array(
|
||||||
|
'id' => $file->id,
|
||||||
|
'name' => $file->filename,
|
||||||
|
'url' => $file->getUrl(),
|
||||||
|
'type' => $file->file_type,
|
||||||
|
'icon' => $file->file_type == 'pdf' ? 'fa-file-pdf-o' : 'fa-file-text-o',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
echo json_encode(array('docs' => $docs));
|
||||||
57
ajax/anlage_images.php
Executable file
57
ajax/anlage_images.php
Executable file
|
|
@ -0,0 +1,57 @@
|
||||||
|
<?php
|
||||||
|
/* Copyright (C) 2026 Alles Watt lauft
|
||||||
|
*
|
||||||
|
* AJAX endpoint to get images 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');
|
||||||
|
|
||||||
|
// Berechtigungsprüfung
|
||||||
|
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', 'images' => array()));
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the anlage to know the socid
|
||||||
|
$anlage = new Anlage($db);
|
||||||
|
if ($anlage->fetch($anlageId) <= 0) {
|
||||||
|
echo json_encode(array('error' => 'Element not found', 'images' => array()));
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get images for this element
|
||||||
|
$anlagefile = new AnlageFile($db);
|
||||||
|
$files = $anlagefile->fetchAllByAnlage($anlageId, 'image');
|
||||||
|
|
||||||
|
$images = array();
|
||||||
|
foreach ($files as $file) {
|
||||||
|
$images[] = array(
|
||||||
|
'id' => $file->id,
|
||||||
|
'name' => $file->filename,
|
||||||
|
'url' => $file->getUrl(),
|
||||||
|
'thumb' => $file->getThumbUrl() ?: $file->getUrl(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
echo json_encode(array('images' => $images));
|
||||||
106
ajax/anlage_tooltip.php
Executable file
106
ajax/anlage_tooltip.php
Executable file
|
|
@ -0,0 +1,106 @@
|
||||||
|
<?php
|
||||||
|
/* Copyright (C) 2026 Alles Watt lauft
|
||||||
|
*
|
||||||
|
* AJAX handler for tooltip data
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (!defined('NOTOKENRENEWAL')) define('NOTOKENRENEWAL', '1');
|
||||||
|
if (!defined('NOREQUIREMENU')) define('NOREQUIREMENU', '1');
|
||||||
|
if (!defined('NOREQUIREAJAX')) define('NOREQUIREAJAX', '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 && file_exists("../../../../main.inc.php")) $res = @include "../../../../main.inc.php";
|
||||||
|
if (!$res) die("Include of main fails");
|
||||||
|
|
||||||
|
dol_include_once('/kundenkarte/class/anlage.class.php');
|
||||||
|
dol_include_once('/kundenkarte/class/anlagetype.class.php');
|
||||||
|
dol_include_once('/kundenkarte/class/anlagefile.class.php');
|
||||||
|
|
||||||
|
$langs->loadLangs(array('kundenkarte@kundenkarte'));
|
||||||
|
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
$id = GETPOSTINT('id');
|
||||||
|
|
||||||
|
if (!$user->hasRight('kundenkarte', 'read')) {
|
||||||
|
echo json_encode(array('success' => false, 'error' => 'Access denied'));
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($id <= 0) {
|
||||||
|
echo json_encode(array('success' => false, 'error' => 'Invalid ID'));
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$anlage = new Anlage($db);
|
||||||
|
$result = $anlage->fetch($id);
|
||||||
|
|
||||||
|
if ($result <= 0) {
|
||||||
|
echo json_encode(array('success' => false, 'error' => 'Element not found'));
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get type and fields
|
||||||
|
$type = new AnlageType($db);
|
||||||
|
$type->fetch($anlage->fk_anlage_type);
|
||||||
|
$typeFields = $type->fetchFields();
|
||||||
|
|
||||||
|
// Get field values
|
||||||
|
$fieldValues = $anlage->getFieldValues();
|
||||||
|
$fieldsData = array();
|
||||||
|
|
||||||
|
foreach ($typeFields as $field) {
|
||||||
|
if ($field->show_in_hover) {
|
||||||
|
// Handle header fields
|
||||||
|
if ($field->field_type === 'header') {
|
||||||
|
$fieldsData[$field->field_code] = array(
|
||||||
|
'label' => $field->field_label,
|
||||||
|
'value' => '',
|
||||||
|
'type' => 'header'
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$value = isset($fieldValues[$field->field_code]) ? $fieldValues[$field->field_code] : '';
|
||||||
|
|
||||||
|
// Format date values
|
||||||
|
$displayValue = $value;
|
||||||
|
if ($field->field_type === 'date' && $value) {
|
||||||
|
$displayValue = dol_print_date(strtotime($value), 'day');
|
||||||
|
}
|
||||||
|
|
||||||
|
$fieldsData[$field->field_code] = array(
|
||||||
|
'label' => $field->field_label,
|
||||||
|
'value' => $displayValue,
|
||||||
|
'type' => $field->field_type
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get images
|
||||||
|
$anlagefile = new AnlageFile($db);
|
||||||
|
$files = $anlagefile->fetchAllByAnlage($id, 'image');
|
||||||
|
|
||||||
|
$images = array();
|
||||||
|
foreach ($files as $file) {
|
||||||
|
$images[] = array(
|
||||||
|
'id' => $file->id,
|
||||||
|
'filename' => $file->filename,
|
||||||
|
'url' => $file->getUrl(),
|
||||||
|
'thumb_url' => $file->getThumbUrl() ?: $file->getUrl()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = array(
|
||||||
|
'id' => $anlage->id,
|
||||||
|
'label' => $anlage->label,
|
||||||
|
'type_label' => $anlage->type_label,
|
||||||
|
'picto' => $anlage->type_picto,
|
||||||
|
'note_html' => $anlage->note_private ? nl2br(htmlspecialchars($anlage->note_private, ENT_QUOTES, 'UTF-8')) : '',
|
||||||
|
'fields' => $fieldsData,
|
||||||
|
'images' => $images
|
||||||
|
);
|
||||||
|
|
||||||
|
echo json_encode(array('success' => true, 'data' => $data));
|
||||||
153
ajax/audit_log.php
Executable file
153
ajax/audit_log.php
Executable file
|
|
@ -0,0 +1,153 @@
|
||||||
|
<?php
|
||||||
|
/* Copyright (C) 2026 Alles Watt lauft
|
||||||
|
*
|
||||||
|
* AJAX endpoint for Audit Log
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (!defined('NOTOKENRENEWAL')) define('NOTOKENRENEWAL', '1');
|
||||||
|
if (!defined('NOREQUIREMENU')) define('NOREQUIREMENU', '1');
|
||||||
|
if (!defined('NOREQUIREHTML')) define('NOREQUIREHTML', '1');
|
||||||
|
if (!defined('NOREQUIREAJAX')) define('NOREQUIREAJAX', '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");
|
||||||
|
|
||||||
|
require_once DOL_DOCUMENT_ROOT.'/core/lib/date.lib.php';
|
||||||
|
dol_include_once('/kundenkarte/class/auditlog.class.php');
|
||||||
|
|
||||||
|
header('Content-Type: application/json; charset=UTF-8');
|
||||||
|
|
||||||
|
$langs->loadLangs(array('kundenkarte@kundenkarte'));
|
||||||
|
|
||||||
|
$action = GETPOST('action', 'aZ09');
|
||||||
|
$objectType = GETPOST('object_type', 'aZ09');
|
||||||
|
$objectId = GETPOSTINT('object_id');
|
||||||
|
$anlageId = GETPOSTINT('anlage_id');
|
||||||
|
$socid = GETPOSTINT('socid');
|
||||||
|
$limit = GETPOSTINT('limit') ?: 50;
|
||||||
|
|
||||||
|
$response = array('success' => false, 'error' => '');
|
||||||
|
|
||||||
|
// Security check
|
||||||
|
if (!$user->hasRight('kundenkarte', 'read')) {
|
||||||
|
$response['error'] = $langs->trans('ErrorPermissionDenied');
|
||||||
|
echo json_encode($response);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$auditLog = new AuditLog($db);
|
||||||
|
|
||||||
|
switch ($action) {
|
||||||
|
case 'fetch_object':
|
||||||
|
// Fetch logs for a specific object
|
||||||
|
if (empty($objectType) || $objectId <= 0) {
|
||||||
|
$response['error'] = $langs->trans('ErrorMissingParameters');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
$logs = $auditLog->fetchByObject($objectType, $objectId, $limit);
|
||||||
|
|
||||||
|
$response['success'] = true;
|
||||||
|
$response['logs'] = array();
|
||||||
|
|
||||||
|
foreach ($logs as $log) {
|
||||||
|
$response['logs'][] = array(
|
||||||
|
'id' => $log->id,
|
||||||
|
'object_type' => $log->object_type,
|
||||||
|
'object_type_label' => $log->getObjectTypeLabel(),
|
||||||
|
'object_id' => $log->object_id,
|
||||||
|
'object_ref' => $log->object_ref,
|
||||||
|
'action' => $log->action,
|
||||||
|
'action_label' => $log->getActionLabel(),
|
||||||
|
'action_icon' => $log->getActionIcon(),
|
||||||
|
'action_color' => $log->getActionColor(),
|
||||||
|
'field_changed' => $log->field_changed,
|
||||||
|
'old_value' => $log->old_value,
|
||||||
|
'new_value' => $log->new_value,
|
||||||
|
'user_login' => $log->user_login,
|
||||||
|
'user_name' => $log->user_name ?: $log->user_login,
|
||||||
|
'date_action' => dol_print_date($log->date_action, 'dayhour'),
|
||||||
|
'timestamp' => $log->date_action,
|
||||||
|
'note' => $log->note
|
||||||
|
);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'fetch_anlage':
|
||||||
|
// Fetch logs for an Anlage
|
||||||
|
if ($anlageId <= 0) {
|
||||||
|
$response['error'] = $langs->trans('ErrorMissingParameters');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
$logs = $auditLog->fetchByAnlage($anlageId, $limit);
|
||||||
|
|
||||||
|
$response['success'] = true;
|
||||||
|
$response['logs'] = array();
|
||||||
|
|
||||||
|
foreach ($logs as $log) {
|
||||||
|
$response['logs'][] = array(
|
||||||
|
'id' => $log->id,
|
||||||
|
'object_type' => $log->object_type,
|
||||||
|
'object_type_label' => $log->getObjectTypeLabel(),
|
||||||
|
'object_id' => $log->object_id,
|
||||||
|
'object_ref' => $log->object_ref,
|
||||||
|
'action' => $log->action,
|
||||||
|
'action_label' => $log->getActionLabel(),
|
||||||
|
'action_icon' => $log->getActionIcon(),
|
||||||
|
'action_color' => $log->getActionColor(),
|
||||||
|
'field_changed' => $log->field_changed,
|
||||||
|
'old_value' => $log->old_value,
|
||||||
|
'new_value' => $log->new_value,
|
||||||
|
'user_login' => $log->user_login,
|
||||||
|
'user_name' => $log->user_name ?: $log->user_login,
|
||||||
|
'date_action' => dol_print_date($log->date_action, 'dayhour'),
|
||||||
|
'timestamp' => $log->date_action,
|
||||||
|
'note' => $log->note
|
||||||
|
);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'fetch_societe':
|
||||||
|
// Fetch logs for a customer
|
||||||
|
if ($socid <= 0) {
|
||||||
|
$response['error'] = $langs->trans('ErrorMissingParameters');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
$logs = $auditLog->fetchBySociete($socid, $limit);
|
||||||
|
|
||||||
|
$response['success'] = true;
|
||||||
|
$response['logs'] = array();
|
||||||
|
|
||||||
|
foreach ($logs as $log) {
|
||||||
|
$response['logs'][] = array(
|
||||||
|
'id' => $log->id,
|
||||||
|
'object_type' => $log->object_type,
|
||||||
|
'object_type_label' => $log->getObjectTypeLabel(),
|
||||||
|
'object_id' => $log->object_id,
|
||||||
|
'object_ref' => $log->object_ref,
|
||||||
|
'fk_anlage' => $log->fk_anlage,
|
||||||
|
'action' => $log->action,
|
||||||
|
'action_label' => $log->getActionLabel(),
|
||||||
|
'action_icon' => $log->getActionIcon(),
|
||||||
|
'action_color' => $log->getActionColor(),
|
||||||
|
'field_changed' => $log->field_changed,
|
||||||
|
'old_value' => $log->old_value,
|
||||||
|
'new_value' => $log->new_value,
|
||||||
|
'user_login' => $log->user_login,
|
||||||
|
'user_name' => $log->user_name ?: $log->user_login,
|
||||||
|
'date_action' => dol_print_date($log->date_action, 'dayhour'),
|
||||||
|
'timestamp' => $log->date_action,
|
||||||
|
'note' => $log->note
|
||||||
|
);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
$response['error'] = 'Unknown action';
|
||||||
|
}
|
||||||
|
|
||||||
|
echo json_encode($response);
|
||||||
250
ajax/bom_generator.php
Executable file
250
ajax/bom_generator.php
Executable file
|
|
@ -0,0 +1,250 @@
|
||||||
|
<?php
|
||||||
|
/* Copyright (C) 2026 Alles Watt lauft
|
||||||
|
*
|
||||||
|
* AJAX endpoint for Bill of Materials (Stückliste) generation from schematic
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (!defined('NOTOKENRENEWAL')) define('NOTOKENRENEWAL', '1');
|
||||||
|
if (!defined('NOREQUIREMENU')) define('NOREQUIREMENU', '1');
|
||||||
|
if (!defined('NOREQUIREHTML')) define('NOREQUIREHTML', '1');
|
||||||
|
if (!defined('NOREQUIREAJAX')) define('NOREQUIREAJAX', '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");
|
||||||
|
|
||||||
|
header('Content-Type: application/json; charset=UTF-8');
|
||||||
|
|
||||||
|
$langs->loadLangs(array('kundenkarte@kundenkarte', 'products'));
|
||||||
|
|
||||||
|
$action = GETPOST('action', 'aZ09');
|
||||||
|
$anlageId = GETPOSTINT('anlage_id');
|
||||||
|
|
||||||
|
$response = array('success' => false, 'error' => '');
|
||||||
|
|
||||||
|
// Security check
|
||||||
|
if (!$user->hasRight('kundenkarte', 'read')) {
|
||||||
|
$response['error'] = $langs->trans('ErrorPermissionDenied');
|
||||||
|
echo json_encode($response);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch ($action) {
|
||||||
|
case 'generate':
|
||||||
|
// Generate BOM from all equipment in this installation (anlage)
|
||||||
|
if ($anlageId <= 0) {
|
||||||
|
$response['error'] = $langs->trans('ErrorRecordNotFound');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all equipment for this anlage through carriers and panels
|
||||||
|
$sql = "SELECT e.rowid as equipment_id, e.label as equipment_label, e.width_te, e.fk_product,";
|
||||||
|
$sql .= " et.rowid as type_id, et.ref as type_ref, et.label as type_label, et.fk_product as type_product,";
|
||||||
|
$sql .= " p.rowid as product_id, p.ref as product_ref, p.label as product_label, p.price, p.tva_tx,";
|
||||||
|
$sql .= " c.label as carrier_label, c.rowid as carrier_id,";
|
||||||
|
$sql .= " pan.label as panel_label, pan.rowid as panel_id";
|
||||||
|
$sql .= " FROM ".MAIN_DB_PREFIX."kundenkarte_equipment e";
|
||||||
|
$sql .= " JOIN ".MAIN_DB_PREFIX."kundenkarte_equipment_carrier c ON e.fk_carrier = c.rowid";
|
||||||
|
$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."kundenkarte_equipment_panel pan ON c.fk_panel = pan.rowid";
|
||||||
|
$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."kundenkarte_equipment_type et ON e.fk_equipment_type = et.rowid";
|
||||||
|
$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."product p ON COALESCE(e.fk_product, et.fk_product) = p.rowid";
|
||||||
|
$sql .= " WHERE (pan.fk_anlage = ".((int) $anlageId)." OR c.fk_anlage = ".((int) $anlageId).")";
|
||||||
|
$sql .= " AND e.status = 1";
|
||||||
|
$sql .= " ORDER BY pan.position ASC, c.position ASC, e.position_te ASC";
|
||||||
|
|
||||||
|
$resql = $db->query($sql);
|
||||||
|
if (!$resql) {
|
||||||
|
$response['error'] = $db->lasterror();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
$items = array();
|
||||||
|
$summary = array(); // Grouped by product
|
||||||
|
|
||||||
|
while ($obj = $db->fetch_object($resql)) {
|
||||||
|
$item = array(
|
||||||
|
'equipment_id' => $obj->equipment_id,
|
||||||
|
'equipment_label' => $obj->equipment_label ?: $obj->type_label,
|
||||||
|
'type_ref' => $obj->type_ref,
|
||||||
|
'type_label' => $obj->type_label,
|
||||||
|
'width_te' => $obj->width_te,
|
||||||
|
'carrier_label' => $obj->carrier_label,
|
||||||
|
'panel_label' => $obj->panel_label,
|
||||||
|
'product_id' => $obj->product_id,
|
||||||
|
'product_ref' => $obj->product_ref,
|
||||||
|
'product_label' => $obj->product_label,
|
||||||
|
'price' => $obj->price,
|
||||||
|
'tva_tx' => $obj->tva_tx
|
||||||
|
);
|
||||||
|
$items[] = $item;
|
||||||
|
|
||||||
|
// Group by product for summary
|
||||||
|
if ($obj->product_id) {
|
||||||
|
$key = $obj->product_id;
|
||||||
|
if (!isset($summary[$key])) {
|
||||||
|
$summary[$key] = array(
|
||||||
|
'product_id' => $obj->product_id,
|
||||||
|
'product_ref' => $obj->product_ref,
|
||||||
|
'product_label' => $obj->product_label,
|
||||||
|
'price' => $obj->price,
|
||||||
|
'tva_tx' => $obj->tva_tx,
|
||||||
|
'quantity' => 0,
|
||||||
|
'total' => 0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
$summary[$key]['quantity']++;
|
||||||
|
$summary[$key]['total'] = $summary[$key]['quantity'] * $summary[$key]['price'];
|
||||||
|
} else {
|
||||||
|
// Group by type if no product linked
|
||||||
|
$key = 'type_'.$obj->type_id;
|
||||||
|
if (!isset($summary[$key])) {
|
||||||
|
$summary[$key] = array(
|
||||||
|
'product_id' => null,
|
||||||
|
'product_ref' => $obj->type_ref,
|
||||||
|
'product_label' => $obj->type_label.' (kein Produkt)',
|
||||||
|
'price' => 0,
|
||||||
|
'tva_tx' => 0,
|
||||||
|
'quantity' => 0,
|
||||||
|
'total' => 0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
$summary[$key]['quantity']++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$db->free($resql);
|
||||||
|
|
||||||
|
// Also include busbar types (connections with is_rail = 1)
|
||||||
|
$sql = "SELECT conn.rowid as connection_id, conn.rail_phases, conn.rail_start_te, conn.rail_end_te,";
|
||||||
|
$sql .= " bt.rowid as busbar_type_id, bt.ref as busbar_ref, bt.label as busbar_label, bt.fk_product,";
|
||||||
|
$sql .= " p.rowid as product_id, p.ref as product_ref, p.label as product_label, p.price, p.tva_tx,";
|
||||||
|
$sql .= " c.label as carrier_label, pan.label as panel_label";
|
||||||
|
$sql .= " FROM ".MAIN_DB_PREFIX."kundenkarte_equipment_connection conn";
|
||||||
|
$sql .= " JOIN ".MAIN_DB_PREFIX."kundenkarte_equipment_carrier c ON conn.fk_carrier = c.rowid";
|
||||||
|
$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."kundenkarte_equipment_panel pan ON c.fk_panel = pan.rowid";
|
||||||
|
$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."kundenkarte_busbar_type bt ON conn.fk_busbar_type = bt.rowid";
|
||||||
|
$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."product p ON bt.fk_product = p.rowid";
|
||||||
|
$sql .= " WHERE (pan.fk_anlage = ".((int) $anlageId)." OR c.fk_anlage = ".((int) $anlageId).")";
|
||||||
|
$sql .= " AND conn.is_rail = 1";
|
||||||
|
$sql .= " AND conn.status = 1";
|
||||||
|
|
||||||
|
$resql = $db->query($sql);
|
||||||
|
if ($resql) {
|
||||||
|
while ($obj = $db->fetch_object($resql)) {
|
||||||
|
// Calculate busbar length in TE
|
||||||
|
$lengthTE = max(1, intval($obj->rail_end_te) - intval($obj->rail_start_te) + 1);
|
||||||
|
|
||||||
|
$item = array(
|
||||||
|
'equipment_id' => 'busbar_'.$obj->connection_id,
|
||||||
|
'equipment_label' => $obj->busbar_label ?: 'Sammelschiene '.$obj->rail_phases,
|
||||||
|
'type_ref' => $obj->busbar_ref ?: 'BUSBAR',
|
||||||
|
'type_label' => 'Sammelschiene',
|
||||||
|
'width_te' => $lengthTE,
|
||||||
|
'carrier_label' => $obj->carrier_label,
|
||||||
|
'panel_label' => $obj->panel_label,
|
||||||
|
'product_id' => $obj->product_id,
|
||||||
|
'product_ref' => $obj->product_ref,
|
||||||
|
'product_label' => $obj->product_label,
|
||||||
|
'price' => $obj->price,
|
||||||
|
'tva_tx' => $obj->tva_tx
|
||||||
|
);
|
||||||
|
$items[] = $item;
|
||||||
|
|
||||||
|
// Add to summary
|
||||||
|
if ($obj->product_id) {
|
||||||
|
$key = $obj->product_id;
|
||||||
|
if (!isset($summary[$key])) {
|
||||||
|
$summary[$key] = array(
|
||||||
|
'product_id' => $obj->product_id,
|
||||||
|
'product_ref' => $obj->product_ref,
|
||||||
|
'product_label' => $obj->product_label,
|
||||||
|
'price' => $obj->price,
|
||||||
|
'tva_tx' => $obj->tva_tx,
|
||||||
|
'quantity' => 0,
|
||||||
|
'total' => 0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
$summary[$key]['quantity']++;
|
||||||
|
$summary[$key]['total'] = $summary[$key]['quantity'] * $summary[$key]['price'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$db->free($resql);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate totals
|
||||||
|
$totalQuantity = 0;
|
||||||
|
$totalPrice = 0;
|
||||||
|
foreach ($summary as $s) {
|
||||||
|
$totalQuantity += $s['quantity'];
|
||||||
|
$totalPrice += $s['total'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$response['success'] = true;
|
||||||
|
$response['items'] = $items;
|
||||||
|
$response['summary'] = array_values($summary);
|
||||||
|
$response['total_quantity'] = $totalQuantity;
|
||||||
|
$response['total_price'] = $totalPrice;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'create_order':
|
||||||
|
// Create a Dolibarr order from the BOM
|
||||||
|
if (!$user->hasRight('kundenkarte', 'write')) {
|
||||||
|
$response['error'] = $langs->trans('ErrorPermissionDenied');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
$socid = GETPOSTINT('socid');
|
||||||
|
$productData = GETPOST('products', 'array');
|
||||||
|
|
||||||
|
if ($socid <= 0 || empty($productData)) {
|
||||||
|
$response['error'] = $langs->trans('ErrorMissingParameters');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
require_once DOL_DOCUMENT_ROOT.'/commande/class/commande.class.php';
|
||||||
|
|
||||||
|
$order = new Commande($db);
|
||||||
|
$order->socid = $socid;
|
||||||
|
$order->date_commande = dol_now();
|
||||||
|
$order->note_private = 'Generiert aus Schaltplan-Stückliste';
|
||||||
|
$order->source = 1; // Web
|
||||||
|
|
||||||
|
$result = $order->create($user);
|
||||||
|
if ($result <= 0) {
|
||||||
|
$response['error'] = $order->error ?: 'Fehler beim Erstellen der Bestellung';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add lines
|
||||||
|
$lineErrors = 0;
|
||||||
|
foreach ($productData as $prod) {
|
||||||
|
$productId = intval($prod['product_id']);
|
||||||
|
$qty = floatval($prod['quantity']);
|
||||||
|
|
||||||
|
if ($productId <= 0 || $qty <= 0) continue;
|
||||||
|
|
||||||
|
$result = $order->addline(
|
||||||
|
'', // Description (auto from product)
|
||||||
|
0, // Unit price (auto from product)
|
||||||
|
$qty,
|
||||||
|
0, // TVA rate (auto)
|
||||||
|
0, 0, // Remise
|
||||||
|
$productId
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($result < 0) {
|
||||||
|
$lineErrors++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$response['success'] = true;
|
||||||
|
$response['order_id'] = $order->id;
|
||||||
|
$response['order_ref'] = $order->ref;
|
||||||
|
$response['line_errors'] = $lineErrors;
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
$response['error'] = 'Unknown action';
|
||||||
|
}
|
||||||
|
|
||||||
|
echo json_encode($response);
|
||||||
121
ajax/building_types.php
Executable file
121
ajax/building_types.php
Executable file
|
|
@ -0,0 +1,121 @@
|
||||||
|
<?php
|
||||||
|
/* Copyright (C) 2026 Alles Watt lauft
|
||||||
|
*
|
||||||
|
* AJAX endpoint for building types (Gebäudetypen)
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (!defined('NOTOKENRENEWAL')) define('NOTOKENRENEWAL', '1');
|
||||||
|
if (!defined('NOREQUIREMENU')) define('NOREQUIREMENU', '1');
|
||||||
|
if (!defined('NOREQUIREHTML')) define('NOREQUIREHTML', '1');
|
||||||
|
if (!defined('NOREQUIREAJAX')) define('NOREQUIREAJAX', '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/buildingtype.class.php');
|
||||||
|
|
||||||
|
header('Content-Type: application/json; charset=UTF-8');
|
||||||
|
|
||||||
|
$langs->loadLangs(array('kundenkarte@kundenkarte'));
|
||||||
|
|
||||||
|
$action = GETPOST('action', 'aZ09');
|
||||||
|
$levelType = GETPOST('level_type', 'alpha');
|
||||||
|
|
||||||
|
$response = array('success' => false, 'error' => '');
|
||||||
|
|
||||||
|
// Security check
|
||||||
|
if (!$user->hasRight('kundenkarte', 'read')) {
|
||||||
|
$response['error'] = $langs->trans('ErrorPermissionDenied');
|
||||||
|
echo json_encode($response);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$buildingType = new BuildingType($db);
|
||||||
|
|
||||||
|
switch ($action) {
|
||||||
|
case 'list':
|
||||||
|
// Get all building types
|
||||||
|
$types = $buildingType->fetchAll(1, $levelType);
|
||||||
|
|
||||||
|
$result = array();
|
||||||
|
foreach ($types as $t) {
|
||||||
|
$result[] = array(
|
||||||
|
'id' => $t->id,
|
||||||
|
'ref' => $t->ref,
|
||||||
|
'label' => $t->label,
|
||||||
|
'label_short' => $t->label_short,
|
||||||
|
'level_type' => $t->level_type,
|
||||||
|
'level_type_label' => $t->getLevelTypeLabel(),
|
||||||
|
'icon' => $t->icon,
|
||||||
|
'color' => $t->color,
|
||||||
|
'can_have_children' => $t->can_have_children,
|
||||||
|
'is_system' => $t->is_system
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$response['success'] = true;
|
||||||
|
$response['types'] = $result;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'list_grouped':
|
||||||
|
// Get types grouped by level
|
||||||
|
$grouped = $buildingType->fetchGroupedByLevel(1);
|
||||||
|
|
||||||
|
$result = array();
|
||||||
|
$levelTypes = BuildingType::getLevelTypes();
|
||||||
|
|
||||||
|
foreach ($grouped as $level => $types) {
|
||||||
|
$levelLabel = isset($levelTypes[$level]) ? $levelTypes[$level] : $level;
|
||||||
|
$levelTypes_data = array();
|
||||||
|
foreach ($types as $t) {
|
||||||
|
$levelTypes_data[] = array(
|
||||||
|
'id' => $t->id,
|
||||||
|
'ref' => $t->ref,
|
||||||
|
'label' => $t->label,
|
||||||
|
'label_short' => $t->label_short,
|
||||||
|
'icon' => $t->icon,
|
||||||
|
'color' => $t->color,
|
||||||
|
'can_have_children' => $t->can_have_children
|
||||||
|
);
|
||||||
|
}
|
||||||
|
$result[] = array(
|
||||||
|
'level_type' => $level,
|
||||||
|
'level_type_label' => $levelLabel,
|
||||||
|
'types' => $levelTypes_data
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$response['success'] = true;
|
||||||
|
$response['groups'] = $result;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'get':
|
||||||
|
// Get single type details
|
||||||
|
$typeId = GETPOSTINT('type_id');
|
||||||
|
if ($typeId > 0 && $buildingType->fetch($typeId) > 0) {
|
||||||
|
$response['success'] = true;
|
||||||
|
$response['type'] = array(
|
||||||
|
'id' => $buildingType->id,
|
||||||
|
'ref' => $buildingType->ref,
|
||||||
|
'label' => $buildingType->label,
|
||||||
|
'label_short' => $buildingType->label_short,
|
||||||
|
'description' => $buildingType->description,
|
||||||
|
'level_type' => $buildingType->level_type,
|
||||||
|
'level_type_label' => $buildingType->getLevelTypeLabel(),
|
||||||
|
'icon' => $buildingType->icon,
|
||||||
|
'color' => $buildingType->color,
|
||||||
|
'can_have_children' => $buildingType->can_have_children,
|
||||||
|
'is_system' => $buildingType->is_system
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
$response['error'] = $langs->trans('ErrorRecordNotFound');
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
$response['error'] = 'Unknown action';
|
||||||
|
}
|
||||||
|
|
||||||
|
echo json_encode($response);
|
||||||
687
ajax/equipment.php
Executable file
687
ajax/equipment.php
Executable file
|
|
@ -0,0 +1,687 @@
|
||||||
|
<?php
|
||||||
|
/* Copyright (C) 2026 Alles Watt lauft
|
||||||
|
*
|
||||||
|
* AJAX endpoint for equipment (Sicherungsautomaten, etc.)
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (!defined('NOTOKENRENEWAL')) define('NOTOKENRENEWAL', '1');
|
||||||
|
if (!defined('NOREQUIREMENU')) define('NOREQUIREMENU', '1');
|
||||||
|
if (!defined('NOREQUIREHTML')) define('NOREQUIREHTML', '1');
|
||||||
|
if (!defined('NOREQUIREAJAX')) define('NOREQUIREAJAX', '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");
|
||||||
|
|
||||||
|
require_once DOL_DOCUMENT_ROOT.'/custom/kundenkarte/class/equipment.class.php';
|
||||||
|
require_once DOL_DOCUMENT_ROOT.'/custom/kundenkarte/class/equipmentcarrier.class.php';
|
||||||
|
require_once DOL_DOCUMENT_ROOT.'/custom/kundenkarte/class/equipmenttype.class.php';
|
||||||
|
dol_include_once('/kundenkarte/class/auditlog.class.php');
|
||||||
|
|
||||||
|
header('Content-Type: application/json; charset=UTF-8');
|
||||||
|
|
||||||
|
$action = GETPOST('action', 'aZ09');
|
||||||
|
$equipmentId = GETPOSTINT('equipment_id');
|
||||||
|
$carrierId = GETPOSTINT('carrier_id');
|
||||||
|
|
||||||
|
$equipment = new Equipment($db);
|
||||||
|
$auditLog = new AuditLog($db);
|
||||||
|
|
||||||
|
$response = array('success' => false, 'error' => '');
|
||||||
|
|
||||||
|
// Security check
|
||||||
|
if (!$user->hasRight('kundenkarte', 'read')) {
|
||||||
|
$response['error'] = 'Permission denied';
|
||||||
|
echo json_encode($response);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch ($action) {
|
||||||
|
case 'get_products':
|
||||||
|
// Get products for equipment selection (electrical components)
|
||||||
|
require_once DOL_DOCUMENT_ROOT.'/product/class/product.class.php';
|
||||||
|
$search = GETPOST('search', 'alphanohtml');
|
||||||
|
$limit = GETPOSTINT('limit') ?: 50;
|
||||||
|
|
||||||
|
$sql = "SELECT p.rowid, p.ref, p.label, p.price, p.fk_product_type";
|
||||||
|
$sql .= " FROM ".MAIN_DB_PREFIX."product as p";
|
||||||
|
$sql .= " WHERE p.entity IN (".getEntity('product').")";
|
||||||
|
$sql .= " AND p.tosell = 1";
|
||||||
|
if (!empty($search)) {
|
||||||
|
$sql .= " AND (p.ref LIKE '%".$db->escape($search)."%' OR p.label LIKE '%".$db->escape($search)."%')";
|
||||||
|
}
|
||||||
|
$sql .= " ORDER BY p.ref ASC";
|
||||||
|
$sql .= " LIMIT ".((int) $limit);
|
||||||
|
|
||||||
|
$resql = $db->query($sql);
|
||||||
|
$products = array();
|
||||||
|
if ($resql) {
|
||||||
|
while ($obj = $db->fetch_object($resql)) {
|
||||||
|
$products[] = array(
|
||||||
|
'id' => $obj->rowid,
|
||||||
|
'ref' => $obj->ref,
|
||||||
|
'label' => $obj->label,
|
||||||
|
'price' => $obj->price,
|
||||||
|
'display' => $obj->ref.' - '.$obj->label
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$response['success'] = true;
|
||||||
|
$response['products'] = $products;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'get_product':
|
||||||
|
// Get single product by ID
|
||||||
|
require_once DOL_DOCUMENT_ROOT.'/product/class/product.class.php';
|
||||||
|
$productId = GETPOSTINT('product_id');
|
||||||
|
if ($productId > 0) {
|
||||||
|
$product = new Product($db);
|
||||||
|
if ($product->fetch($productId) > 0) {
|
||||||
|
$response['success'] = true;
|
||||||
|
$response['product'] = array(
|
||||||
|
'id' => $product->id,
|
||||||
|
'ref' => $product->ref,
|
||||||
|
'label' => $product->label,
|
||||||
|
'display' => $product->ref.' - '.$product->label
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
$response['error'] = 'Product not found';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$response['error'] = 'No product_id provided';
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'get_types':
|
||||||
|
// Get all equipment types for dropdown
|
||||||
|
require_once DOL_DOCUMENT_ROOT.'/custom/kundenkarte/class/equipmenttype.class.php';
|
||||||
|
$eqType = new EquipmentType($db);
|
||||||
|
$systemId = GETPOSTINT('system_id');
|
||||||
|
$types = $eqType->fetchAllBySystem($systemId, 1); // Filter by system if provided, only active
|
||||||
|
|
||||||
|
$result = array();
|
||||||
|
foreach ($types as $t) {
|
||||||
|
$result[] = array(
|
||||||
|
'id' => $t->id,
|
||||||
|
'ref' => $t->ref,
|
||||||
|
'label' => $t->label,
|
||||||
|
'label_short' => $t->label_short,
|
||||||
|
'width_te' => $t->width_te,
|
||||||
|
'color' => $t->color,
|
||||||
|
'picto' => $t->picto
|
||||||
|
);
|
||||||
|
}
|
||||||
|
$response['success'] = true;
|
||||||
|
$response['types'] = $result;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'get_type_fields':
|
||||||
|
// Get fields for a specific equipment type
|
||||||
|
$typeId = GETPOSTINT('type_id');
|
||||||
|
if ($typeId > 0) {
|
||||||
|
$sql = "SELECT field_code, field_label, field_type, field_options, required, position, show_on_block, show_in_hover";
|
||||||
|
$sql .= " FROM ".MAIN_DB_PREFIX."kundenkarte_equipment_type_field";
|
||||||
|
$sql .= " WHERE fk_equipment_type = ".((int) $typeId);
|
||||||
|
$sql .= " AND active = 1";
|
||||||
|
$sql .= " ORDER BY position ASC";
|
||||||
|
|
||||||
|
$resql = $db->query($sql);
|
||||||
|
$fields = array();
|
||||||
|
if ($resql) {
|
||||||
|
while ($obj = $db->fetch_object($resql)) {
|
||||||
|
$fields[] = array(
|
||||||
|
'field_code' => $obj->field_code,
|
||||||
|
'field_label' => $obj->field_label,
|
||||||
|
'field_type' => $obj->field_type,
|
||||||
|
'field_options' => $obj->field_options,
|
||||||
|
'required' => $obj->required,
|
||||||
|
'show_on_block' => $obj->show_on_block,
|
||||||
|
'show_in_hover' => $obj->show_in_hover
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$response['success'] = true;
|
||||||
|
$response['fields'] = $fields;
|
||||||
|
} else {
|
||||||
|
$response['error'] = 'No type_id provided';
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'get':
|
||||||
|
// Get single equipment data
|
||||||
|
if ($equipmentId > 0 && $equipment->fetch($equipmentId) > 0) {
|
||||||
|
$response['success'] = true;
|
||||||
|
$response['equipment'] = array(
|
||||||
|
'id' => $equipment->id,
|
||||||
|
'fk_carrier' => $equipment->fk_carrier,
|
||||||
|
'type_id' => $equipment->fk_equipment_type,
|
||||||
|
'type_label' => $equipment->type_label,
|
||||||
|
'type_label_short' => $equipment->type_label_short,
|
||||||
|
'type_color' => $equipment->type_color,
|
||||||
|
'type_icon_file' => $equipment->type_icon_file,
|
||||||
|
'label' => $equipment->label,
|
||||||
|
'position_te' => $equipment->position_te,
|
||||||
|
'width_te' => $equipment->width_te,
|
||||||
|
'field_values' => $equipment->getFieldValues(),
|
||||||
|
'fk_product' => $equipment->fk_product,
|
||||||
|
'fk_protection' => $equipment->fk_protection,
|
||||||
|
'protection_label' => $equipment->protection_label
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
$response['error'] = 'Equipment not found';
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'get_protection_devices':
|
||||||
|
// Get all protection devices (FI/RCD) for an Anlage
|
||||||
|
$anlageId = GETPOSTINT('anlage_id');
|
||||||
|
if ($anlageId > 0) {
|
||||||
|
$devices = $equipment->fetchProtectionDevices($anlageId);
|
||||||
|
$result = array();
|
||||||
|
foreach ($devices as $d) {
|
||||||
|
$result[] = array(
|
||||||
|
'id' => $d->id,
|
||||||
|
'label' => $d->label ?: $d->type_label,
|
||||||
|
'type_label' => $d->type_label,
|
||||||
|
'type_label_short' => $d->type_label_short,
|
||||||
|
'display_label' => ($d->label ?: $d->type_label_short ?: $d->type_label).' (Pos. '.$d->position_te.')'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
$response['success'] = true;
|
||||||
|
$response['devices'] = $result;
|
||||||
|
} else {
|
||||||
|
$response['error'] = 'Missing anlage_id';
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'list':
|
||||||
|
// List all equipment on a carrier
|
||||||
|
if ($carrierId > 0) {
|
||||||
|
$items = $equipment->fetchByCarrier($carrierId);
|
||||||
|
$result = array();
|
||||||
|
|
||||||
|
// Cache type fields for performance
|
||||||
|
$typeFieldsCache = array();
|
||||||
|
|
||||||
|
foreach ($items as $eq) {
|
||||||
|
$iconUrl = '';
|
||||||
|
if (!empty($eq->type_icon_file)) {
|
||||||
|
$iconUrl = DOL_URL_ROOT.'/document.php?modulepart=kundenkarte&file=equipment_icons/'.urlencode($eq->type_icon_file);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load type fields if not cached
|
||||||
|
$typeId = $eq->fk_equipment_type;
|
||||||
|
if (!isset($typeFieldsCache[$typeId])) {
|
||||||
|
$typeFieldsCache[$typeId] = array();
|
||||||
|
$sql = "SELECT field_code, field_label, show_on_block, show_in_hover";
|
||||||
|
$sql .= " FROM ".MAIN_DB_PREFIX."kundenkarte_equipment_type_field";
|
||||||
|
$sql .= " WHERE fk_equipment_type = ".((int) $typeId);
|
||||||
|
$sql .= " AND active = 1";
|
||||||
|
$sql .= " ORDER BY position ASC";
|
||||||
|
$resql = $db->query($sql);
|
||||||
|
if ($resql) {
|
||||||
|
while ($obj = $db->fetch_object($resql)) {
|
||||||
|
$typeFieldsCache[$typeId][] = array(
|
||||||
|
'field_code' => $obj->field_code,
|
||||||
|
'field_label' => $obj->field_label,
|
||||||
|
'show_on_block' => (int) $obj->show_on_block,
|
||||||
|
'show_in_hover' => (int) $obj->show_in_hover
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load product data if assigned
|
||||||
|
$productRef = '';
|
||||||
|
$productLabel = '';
|
||||||
|
if (!empty($eq->fk_product)) {
|
||||||
|
$sqlProd = "SELECT ref, label FROM ".MAIN_DB_PREFIX."product WHERE rowid = ".((int) $eq->fk_product);
|
||||||
|
$resProd = $db->query($sqlProd);
|
||||||
|
if ($resProd && ($objProd = $db->fetch_object($resProd))) {
|
||||||
|
$productRef = $objProd->ref;
|
||||||
|
$productLabel = $objProd->label;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$result[] = array(
|
||||||
|
'id' => $eq->id,
|
||||||
|
'type_id' => $eq->fk_equipment_type,
|
||||||
|
'type_label' => $eq->type_label,
|
||||||
|
'type_label_short' => $eq->type_label_short,
|
||||||
|
'type_ref' => $eq->type_ref,
|
||||||
|
'type_color' => $eq->type_color,
|
||||||
|
'type_icon_file' => $eq->type_icon_file,
|
||||||
|
'type_icon_url' => $iconUrl,
|
||||||
|
'type_block_image' => $eq->type_block_image,
|
||||||
|
'type_block_image_url' => !empty($eq->type_block_image) ? DOL_URL_ROOT.'/document.php?modulepart=kundenkarte&file=block_images/'.urlencode($eq->type_block_image) : '',
|
||||||
|
'type_flow_direction' => $eq->type_flow_direction,
|
||||||
|
'type_terminal_position' => $eq->type_terminal_position ?: 'both',
|
||||||
|
'terminals_config' => $eq->terminals_config,
|
||||||
|
'type_fields' => $typeFieldsCache[$typeId],
|
||||||
|
'label' => $eq->label,
|
||||||
|
'position_te' => $eq->position_te,
|
||||||
|
'width_te' => $eq->width_te,
|
||||||
|
'block_label' => $eq->getBlockLabel(),
|
||||||
|
'block_color' => $eq->getBlockColor(),
|
||||||
|
'field_values' => $eq->getFieldValues(),
|
||||||
|
'fk_product' => $eq->fk_product,
|
||||||
|
'product_ref' => $productRef,
|
||||||
|
'product_label' => $productLabel
|
||||||
|
);
|
||||||
|
}
|
||||||
|
$response['success'] = true;
|
||||||
|
$response['equipment'] = $result;
|
||||||
|
} else {
|
||||||
|
$response['error'] = 'Missing carrier_id';
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'create':
|
||||||
|
if (!$user->hasRight('kundenkarte', 'write')) {
|
||||||
|
$response['error'] = 'Permission denied';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
$equipment->fk_carrier = $carrierId;
|
||||||
|
$equipment->fk_equipment_type = GETPOSTINT('type_id');
|
||||||
|
$equipment->label = GETPOST('label', 'alphanohtml');
|
||||||
|
$equipment->position_te = GETPOSTINT('position_te');
|
||||||
|
$equipment->width_te = GETPOSTINT('width_te');
|
||||||
|
$equipment->fk_product = GETPOSTINT('fk_product');
|
||||||
|
$equipment->fk_protection = GETPOSTINT('fk_protection');
|
||||||
|
$equipment->protection_label = GETPOST('protection_label', 'alphanohtml');
|
||||||
|
|
||||||
|
// Field values
|
||||||
|
$fieldValues = GETPOST('field_values', 'nohtml');
|
||||||
|
if ($fieldValues) {
|
||||||
|
$equipment->field_values = $fieldValues;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no width specified, get from type
|
||||||
|
if (empty($equipment->width_te)) {
|
||||||
|
$type = new EquipmentType($db);
|
||||||
|
if ($type->fetch($equipment->fk_equipment_type) > 0) {
|
||||||
|
$equipment->width_te = $type->width_te;
|
||||||
|
} else {
|
||||||
|
$equipment->width_te = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check carrier and position
|
||||||
|
$carrier = new EquipmentCarrier($db);
|
||||||
|
if ($carrier->fetch($carrierId) > 0) {
|
||||||
|
// If no position specified, find next free position (1-based)
|
||||||
|
if (empty($equipment->position_te)) {
|
||||||
|
$equipment->position_te = $carrier->getNextFreePosition($equipment->width_te);
|
||||||
|
if ($equipment->position_te < 0) {
|
||||||
|
$response['error'] = 'No free position available on carrier';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if position is available
|
||||||
|
if (!$carrier->isPositionAvailable($equipment->position_te, $equipment->width_te)) {
|
||||||
|
$response['error'] = 'Position not available';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-generate label if empty: R2.1 or R2.1-3 for multi-TE
|
||||||
|
if (empty($equipment->label)) {
|
||||||
|
$carrierLabel = $carrier->label ?: ('R'.$carrier->id);
|
||||||
|
$posStart = $equipment->position_te;
|
||||||
|
$posEnd = $posStart + $equipment->width_te - 1;
|
||||||
|
if ($equipment->width_te > 1) {
|
||||||
|
$equipment->label = $carrierLabel.'.'.$posStart.'-'.$posEnd;
|
||||||
|
} else {
|
||||||
|
$equipment->label = $carrierLabel.'.'.$posStart;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = $equipment->create($user);
|
||||||
|
if ($result > 0) {
|
||||||
|
$response['success'] = true;
|
||||||
|
$response['equipment_id'] = $result;
|
||||||
|
$response['block_label'] = $equipment->getBlockLabel();
|
||||||
|
|
||||||
|
// Audit log
|
||||||
|
$anlageId = 0;
|
||||||
|
if ($carrier->fk_panel > 0) {
|
||||||
|
require_once DOL_DOCUMENT_ROOT.'/custom/kundenkarte/class/equipmentpanel.class.php';
|
||||||
|
$panel = new EquipmentPanel($db);
|
||||||
|
if ($panel->fetch($carrier->fk_panel) > 0) {
|
||||||
|
$anlageId = $panel->fk_anlage;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$anlageId = $carrier->fk_anlage;
|
||||||
|
}
|
||||||
|
$auditLog->logCreate($user, AuditLog::TYPE_EQUIPMENT, $result, $equipment->label ?: $equipment->type_label, 0, $anlageId, array(
|
||||||
|
'type_id' => $equipment->fk_equipment_type,
|
||||||
|
'position_te' => $equipment->position_te,
|
||||||
|
'width_te' => $equipment->width_te
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
$response['error'] = $equipment->error;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'update':
|
||||||
|
if (!$user->hasRight('kundenkarte', 'write')) {
|
||||||
|
$response['error'] = 'Permission denied';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if ($equipment->fetch($equipmentId) > 0) {
|
||||||
|
$newPosition = GETPOSTINT('position_te');
|
||||||
|
$newWidth = GETPOSTINT('width_te') ?: $equipment->width_te;
|
||||||
|
|
||||||
|
// Check if new position is available (excluding current equipment)
|
||||||
|
if ($newPosition != $equipment->position_te || $newWidth != $equipment->width_te) {
|
||||||
|
$carrier = new EquipmentCarrier($db);
|
||||||
|
if ($carrier->fetch($equipment->fk_carrier) > 0) {
|
||||||
|
if (!$carrier->isPositionAvailable($newPosition, $newWidth, $equipmentId)) {
|
||||||
|
$response['error'] = 'Position not available';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$equipment->fk_equipment_type = GETPOSTINT('type_id') ?: $equipment->fk_equipment_type;
|
||||||
|
$equipment->label = GETPOST('label', 'alphanohtml');
|
||||||
|
$equipment->position_te = $newPosition;
|
||||||
|
$equipment->width_te = $newWidth;
|
||||||
|
$equipment->fk_product = GETPOSTINT('fk_product');
|
||||||
|
$equipment->fk_protection = GETPOSTINT('fk_protection');
|
||||||
|
$equipment->protection_label = GETPOST('protection_label', 'alphanohtml');
|
||||||
|
|
||||||
|
$fieldValues = GETPOST('field_values', 'nohtml');
|
||||||
|
if ($fieldValues) {
|
||||||
|
$equipment->field_values = $fieldValues;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-generate label if empty
|
||||||
|
if (empty(trim($equipment->label))) {
|
||||||
|
$carrier = new EquipmentCarrier($db);
|
||||||
|
if ($carrier->fetch($equipment->fk_carrier) > 0) {
|
||||||
|
$carrierLabel = $carrier->label ?: ('R'.$carrier->id);
|
||||||
|
$posStart = $equipment->position_te;
|
||||||
|
$posEnd = $posStart + $equipment->width_te - 1;
|
||||||
|
if ($equipment->width_te > 1) {
|
||||||
|
$equipment->label = $carrierLabel.'.'.$posStart.'-'.$posEnd;
|
||||||
|
} else {
|
||||||
|
$equipment->label = $carrierLabel.'.'.$posStart;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$oldLabel = isset($oldLabel) ? $oldLabel : $equipment->label;
|
||||||
|
$oldPosition = isset($oldPosition) ? $oldPosition : $equipment->position_te;
|
||||||
|
$result = $equipment->update($user);
|
||||||
|
if ($result > 0) {
|
||||||
|
$response['success'] = true;
|
||||||
|
$response['block_label'] = $equipment->getBlockLabel();
|
||||||
|
|
||||||
|
// Audit log
|
||||||
|
$anlageId = 0;
|
||||||
|
$carrier = new EquipmentCarrier($db);
|
||||||
|
if ($carrier->fetch($equipment->fk_carrier) > 0) {
|
||||||
|
if ($carrier->fk_panel > 0) {
|
||||||
|
require_once DOL_DOCUMENT_ROOT.'/custom/kundenkarte/class/equipmentpanel.class.php';
|
||||||
|
$panel = new EquipmentPanel($db);
|
||||||
|
if ($panel->fetch($carrier->fk_panel) > 0) {
|
||||||
|
$anlageId = $panel->fk_anlage;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$anlageId = $carrier->fk_anlage;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$auditLog->logUpdate($user, AuditLog::TYPE_EQUIPMENT, $equipment->id, $equipment->label ?: $equipment->type_label, 'properties', null, null, 0, $anlageId);
|
||||||
|
} else {
|
||||||
|
$response['error'] = $equipment->error;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$response['error'] = 'Equipment not found';
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'update_position':
|
||||||
|
// Quick position update for drag-drop
|
||||||
|
if (!$user->hasRight('kundenkarte', 'write')) {
|
||||||
|
$response['error'] = 'Permission denied';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if ($equipment->fetch($equipmentId) > 0) {
|
||||||
|
$newPosition = GETPOSTINT('position_te');
|
||||||
|
|
||||||
|
// Check if new position is available
|
||||||
|
$carrier = new EquipmentCarrier($db);
|
||||||
|
if ($newPosition != $equipment->position_te) {
|
||||||
|
if ($carrier->fetch($equipment->fk_carrier) > 0) {
|
||||||
|
if (!$carrier->isPositionAvailable($newPosition, $equipment->width_te, $equipmentId)) {
|
||||||
|
$response['error'] = 'Position not available';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$carrier->fetch($equipment->fk_carrier);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update auto-generated label if it matches the pattern Rxx.xx or Rxx.xx-xx
|
||||||
|
$oldLabel = $equipment->label;
|
||||||
|
$carrierLabel = $carrier->label ?: ('R'.$carrier->id);
|
||||||
|
if (preg_match('/^'.preg_quote($carrierLabel, '/').'\.(\d+)(-\d+)?$/', $oldLabel)) {
|
||||||
|
$posStart = $newPosition;
|
||||||
|
$posEnd = $posStart + $equipment->width_te - 1;
|
||||||
|
if ($equipment->width_te > 1) {
|
||||||
|
$equipment->label = $carrierLabel.'.'.$posStart.'-'.$posEnd;
|
||||||
|
} else {
|
||||||
|
$equipment->label = $carrierLabel.'.'.$posStart;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$equipment->position_te = $newPosition;
|
||||||
|
$result = $equipment->update($user);
|
||||||
|
if ($result > 0) {
|
||||||
|
$response['success'] = true;
|
||||||
|
$response['new_label'] = $equipment->label;
|
||||||
|
} else {
|
||||||
|
$response['error'] = $equipment->error;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$response['error'] = 'Equipment not found';
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'move_to_carrier':
|
||||||
|
// Move equipment to different carrier (drag-drop between carriers)
|
||||||
|
if (!$user->hasRight('kundenkarte', 'write')) {
|
||||||
|
$response['error'] = 'Permission denied';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if ($equipment->fetch($equipmentId) > 0) {
|
||||||
|
$newCarrierId = GETPOSTINT('carrier_id');
|
||||||
|
$newPosition = GETPOSTINT('position_te') ?: 1;
|
||||||
|
|
||||||
|
// Get old carrier for label pattern check
|
||||||
|
$oldCarrier = new EquipmentCarrier($db);
|
||||||
|
$oldCarrier->fetch($equipment->fk_carrier);
|
||||||
|
$oldCarrierLabel = $oldCarrier->label ?: ('R'.$oldCarrier->id);
|
||||||
|
|
||||||
|
// Check if target carrier exists
|
||||||
|
$targetCarrier = new EquipmentCarrier($db);
|
||||||
|
if ($targetCarrier->fetch($newCarrierId) <= 0) {
|
||||||
|
$response['error'] = 'Target carrier not found';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if position is available on target carrier
|
||||||
|
if (!$targetCarrier->isPositionAvailable($newPosition, $equipment->width_te, 0)) {
|
||||||
|
$response['error'] = 'Position auf Ziel-Hutschiene nicht verfügbar';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update auto-generated label if it matches the old carrier pattern
|
||||||
|
$oldLabel = $equipment->label;
|
||||||
|
$newCarrierLabel = $targetCarrier->label ?: ('R'.$targetCarrier->id);
|
||||||
|
if (preg_match('/^'.preg_quote($oldCarrierLabel, '/').'\.(\d+)(-\d+)?$/', $oldLabel)) {
|
||||||
|
$posStart = $newPosition;
|
||||||
|
$posEnd = $posStart + $equipment->width_te - 1;
|
||||||
|
if ($equipment->width_te > 1) {
|
||||||
|
$equipment->label = $newCarrierLabel.'.'.$posStart.'-'.$posEnd;
|
||||||
|
} else {
|
||||||
|
$equipment->label = $newCarrierLabel.'.'.$posStart;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update equipment
|
||||||
|
$equipment->fk_carrier = $newCarrierId;
|
||||||
|
$equipment->position_te = $newPosition;
|
||||||
|
$result = $equipment->update($user);
|
||||||
|
if ($result > 0) {
|
||||||
|
$response['success'] = true;
|
||||||
|
$response['message'] = 'Equipment verschoben';
|
||||||
|
$response['new_label'] = $equipment->label;
|
||||||
|
} else {
|
||||||
|
$response['error'] = $equipment->error;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$response['error'] = 'Equipment not found';
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'delete':
|
||||||
|
if (!$user->hasRight('kundenkarte', 'delete')) {
|
||||||
|
$response['error'] = 'Permission denied';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if ($equipment->fetch($equipmentId) > 0) {
|
||||||
|
// Get anlage_id before deletion for audit log
|
||||||
|
$anlageId = 0;
|
||||||
|
$deletedLabel = $equipment->label ?: $equipment->type_label;
|
||||||
|
$deletedData = array(
|
||||||
|
'type_id' => $equipment->fk_equipment_type,
|
||||||
|
'type_label' => $equipment->type_label,
|
||||||
|
'position_te' => $equipment->position_te,
|
||||||
|
'width_te' => $equipment->width_te,
|
||||||
|
'carrier_id' => $equipment->fk_carrier
|
||||||
|
);
|
||||||
|
|
||||||
|
$carrier = new EquipmentCarrier($db);
|
||||||
|
if ($carrier->fetch($equipment->fk_carrier) > 0) {
|
||||||
|
if ($carrier->fk_panel > 0) {
|
||||||
|
require_once DOL_DOCUMENT_ROOT.'/custom/kundenkarte/class/equipmentpanel.class.php';
|
||||||
|
$panel = new EquipmentPanel($db);
|
||||||
|
if ($panel->fetch($carrier->fk_panel) > 0) {
|
||||||
|
$anlageId = $panel->fk_anlage;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$anlageId = $carrier->fk_anlage;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = $equipment->delete($user);
|
||||||
|
if ($result > 0) {
|
||||||
|
$response['success'] = true;
|
||||||
|
|
||||||
|
// Audit log
|
||||||
|
$auditLog->logDelete($user, AuditLog::TYPE_EQUIPMENT, $equipmentId, $deletedLabel, 0, $anlageId, $deletedData);
|
||||||
|
} else {
|
||||||
|
$response['error'] = $equipment->error;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$response['error'] = 'Equipment not found';
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'duplicate':
|
||||||
|
if (!$user->hasRight('kundenkarte', 'write')) {
|
||||||
|
$response['error'] = 'Permission denied';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if ($equipment->fetch($equipmentId) > 0) {
|
||||||
|
$sourceId = $equipmentId;
|
||||||
|
$newId = $equipment->duplicate($user);
|
||||||
|
if ($newId > 0) {
|
||||||
|
$response['success'] = true;
|
||||||
|
$response['equipment_id'] = $newId;
|
||||||
|
|
||||||
|
// Fetch the new equipment to return its data
|
||||||
|
$newEquipment = new Equipment($db);
|
||||||
|
if ($newEquipment->fetch($newId) > 0) {
|
||||||
|
$response['equipment'] = array(
|
||||||
|
'id' => $newEquipment->id,
|
||||||
|
'fk_carrier' => $newEquipment->fk_carrier,
|
||||||
|
'type_id' => $newEquipment->fk_equipment_type,
|
||||||
|
'type_label' => $newEquipment->type_label,
|
||||||
|
'type_label_short' => $newEquipment->type_label_short,
|
||||||
|
'type_color' => $newEquipment->type_color,
|
||||||
|
'type_ref' => $newEquipment->type_ref,
|
||||||
|
'type_icon_file' => $newEquipment->type_icon_file,
|
||||||
|
'terminals_config' => $newEquipment->terminals_config,
|
||||||
|
'label' => $newEquipment->label,
|
||||||
|
'position_te' => $newEquipment->position_te,
|
||||||
|
'width_te' => $newEquipment->width_te,
|
||||||
|
'block_label' => $newEquipment->getBlockLabel(),
|
||||||
|
'block_color' => $newEquipment->getBlockColor(),
|
||||||
|
'field_values' => $newEquipment->getFieldValues(),
|
||||||
|
'fk_product' => $newEquipment->fk_product
|
||||||
|
);
|
||||||
|
|
||||||
|
// Audit log
|
||||||
|
$anlageId = 0;
|
||||||
|
$carrier = new EquipmentCarrier($db);
|
||||||
|
if ($carrier->fetch($newEquipment->fk_carrier) > 0) {
|
||||||
|
if ($carrier->fk_panel > 0) {
|
||||||
|
require_once DOL_DOCUMENT_ROOT.'/custom/kundenkarte/class/equipmentpanel.class.php';
|
||||||
|
$panel = new EquipmentPanel($db);
|
||||||
|
if ($panel->fetch($carrier->fk_panel) > 0) {
|
||||||
|
$anlageId = $panel->fk_anlage;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$anlageId = $carrier->fk_anlage;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$auditLog->logDuplicate($user, AuditLog::TYPE_EQUIPMENT, $newId, $newEquipment->label ?: $newEquipment->type_label, $sourceId, 0, $anlageId);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$response['error'] = $equipment->error ?: 'Duplication failed';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$response['error'] = 'Equipment not found';
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'move':
|
||||||
|
// Move equipment to new position (for drag & drop)
|
||||||
|
if (!$user->hasRight('kundenkarte', 'write')) {
|
||||||
|
$response['error'] = 'Permission denied';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if ($equipment->fetch($equipmentId) > 0) {
|
||||||
|
$newPosition = GETPOSTINT('position_te');
|
||||||
|
|
||||||
|
$carrier = new EquipmentCarrier($db);
|
||||||
|
if ($carrier->fetch($equipment->fk_carrier) > 0) {
|
||||||
|
if ($carrier->isPositionAvailable($newPosition, $equipment->width_te, $equipmentId)) {
|
||||||
|
$equipment->position_te = $newPosition;
|
||||||
|
$result = $equipment->update($user);
|
||||||
|
if ($result > 0) {
|
||||||
|
$response['success'] = true;
|
||||||
|
} else {
|
||||||
|
$response['error'] = $equipment->error;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$response['error'] = 'Position not available';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$response['error'] = 'Equipment not found';
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
$response['error'] = 'Unknown action';
|
||||||
|
}
|
||||||
|
|
||||||
|
echo json_encode($response);
|
||||||
|
$db->close();
|
||||||
188
ajax/equipment_carrier.php
Executable file
188
ajax/equipment_carrier.php
Executable file
|
|
@ -0,0 +1,188 @@
|
||||||
|
<?php
|
||||||
|
/* Copyright (C) 2026 Alles Watt lauft
|
||||||
|
*
|
||||||
|
* AJAX endpoint for equipment carriers (Hutschienen)
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (!defined('NOTOKENRENEWAL')) define('NOTOKENRENEWAL', '1');
|
||||||
|
if (!defined('NOREQUIREMENU')) define('NOREQUIREMENU', '1');
|
||||||
|
if (!defined('NOREQUIREHTML')) define('NOREQUIREHTML', '1');
|
||||||
|
if (!defined('NOREQUIREAJAX')) define('NOREQUIREAJAX', '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");
|
||||||
|
|
||||||
|
require_once DOL_DOCUMENT_ROOT.'/custom/kundenkarte/class/equipmentcarrier.class.php';
|
||||||
|
require_once DOL_DOCUMENT_ROOT.'/custom/kundenkarte/class/equipment.class.php';
|
||||||
|
|
||||||
|
header('Content-Type: application/json; charset=UTF-8');
|
||||||
|
|
||||||
|
$action = GETPOST('action', 'aZ09');
|
||||||
|
$anlageId = GETPOSTINT('anlage_id');
|
||||||
|
$carrierId = GETPOSTINT('carrier_id');
|
||||||
|
$panelId = GETPOSTINT('panel_id');
|
||||||
|
|
||||||
|
$carrier = new EquipmentCarrier($db);
|
||||||
|
|
||||||
|
$response = array('success' => false, 'error' => '');
|
||||||
|
|
||||||
|
// Security check
|
||||||
|
if (!$user->hasRight('kundenkarte', 'read')) {
|
||||||
|
$response['error'] = 'Permission denied';
|
||||||
|
echo json_encode($response);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch ($action) {
|
||||||
|
case 'get':
|
||||||
|
// Get single carrier data
|
||||||
|
if ($carrierId > 0 && $carrier->fetch($carrierId) > 0) {
|
||||||
|
$response['success'] = true;
|
||||||
|
$response['carrier'] = array(
|
||||||
|
'id' => $carrier->id,
|
||||||
|
'fk_anlage' => $carrier->fk_anlage,
|
||||||
|
'fk_panel' => $carrier->fk_panel,
|
||||||
|
'label' => $carrier->label,
|
||||||
|
'total_te' => $carrier->total_te,
|
||||||
|
'position' => $carrier->position,
|
||||||
|
'panel_label' => $carrier->panel_label
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
$response['error'] = 'Carrier not found';
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'list':
|
||||||
|
// List all carriers for an Anlage
|
||||||
|
if ($anlageId > 0) {
|
||||||
|
$carriers = $carrier->fetchByAnlage($anlageId);
|
||||||
|
$result = array();
|
||||||
|
foreach ($carriers as $c) {
|
||||||
|
$c->fetchEquipment();
|
||||||
|
$equipment = array();
|
||||||
|
foreach ($c->equipment as $eq) {
|
||||||
|
$equipment[] = array(
|
||||||
|
'id' => $eq->id,
|
||||||
|
'type_id' => $eq->fk_equipment_type,
|
||||||
|
'type_label' => $eq->type_label,
|
||||||
|
'type_label_short' => $eq->type_label_short,
|
||||||
|
'type_color' => $eq->type_color,
|
||||||
|
'label' => $eq->label,
|
||||||
|
'position_te' => $eq->position_te,
|
||||||
|
'width_te' => $eq->width_te,
|
||||||
|
'block_label' => $eq->getBlockLabel(),
|
||||||
|
'block_color' => $eq->getBlockColor(),
|
||||||
|
'field_values' => $eq->getFieldValues()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
$result[] = array(
|
||||||
|
'id' => $c->id,
|
||||||
|
'label' => $c->label,
|
||||||
|
'total_te' => $c->total_te,
|
||||||
|
'used_te' => $c->getUsedTE(),
|
||||||
|
'free_te' => $c->getFreeTE(),
|
||||||
|
'position' => $c->position,
|
||||||
|
'equipment' => $equipment
|
||||||
|
);
|
||||||
|
}
|
||||||
|
$response['success'] = true;
|
||||||
|
$response['carriers'] = $result;
|
||||||
|
} else {
|
||||||
|
$response['error'] = 'Missing anlage_id';
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'create':
|
||||||
|
if (!$user->hasRight('kundenkarte', 'write')) {
|
||||||
|
$response['error'] = 'Permission denied';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
$carrier->fk_anlage = $anlageId;
|
||||||
|
$carrier->fk_panel = $panelId > 0 ? $panelId : null;
|
||||||
|
$carrier->label = GETPOST('label', 'alphanohtml');
|
||||||
|
$carrier->total_te = GETPOSTINT('total_te') ?: 12;
|
||||||
|
|
||||||
|
$result = $carrier->create($user);
|
||||||
|
if ($result > 0) {
|
||||||
|
$response['success'] = true;
|
||||||
|
$response['carrier_id'] = $result;
|
||||||
|
} else {
|
||||||
|
$response['error'] = $carrier->error;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'update':
|
||||||
|
if (!$user->hasRight('kundenkarte', 'write')) {
|
||||||
|
$response['error'] = 'Permission denied';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if ($carrier->fetch($carrierId) > 0) {
|
||||||
|
$carrier->label = GETPOST('label', 'alphanohtml');
|
||||||
|
$carrier->total_te = GETPOSTINT('total_te') ?: $carrier->total_te;
|
||||||
|
$carrier->position = GETPOSTINT('position');
|
||||||
|
// Allow changing panel (0 or empty = no panel)
|
||||||
|
$newPanelId = GETPOSTINT('panel_id');
|
||||||
|
$carrier->fk_panel = $newPanelId > 0 ? $newPanelId : null;
|
||||||
|
|
||||||
|
$result = $carrier->update($user);
|
||||||
|
if ($result > 0) {
|
||||||
|
$response['success'] = true;
|
||||||
|
} else {
|
||||||
|
$response['error'] = $carrier->error;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$response['error'] = 'Carrier not found';
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'delete':
|
||||||
|
if (!$user->hasRight('kundenkarte', 'delete')) {
|
||||||
|
$response['error'] = 'Permission denied';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if ($carrier->fetch($carrierId) > 0) {
|
||||||
|
$result = $carrier->delete($user);
|
||||||
|
if ($result > 0) {
|
||||||
|
$response['success'] = true;
|
||||||
|
} else {
|
||||||
|
$response['error'] = $carrier->error;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$response['error'] = 'Carrier not found';
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'duplicate':
|
||||||
|
if (!$user->hasRight('kundenkarte', 'write')) {
|
||||||
|
$response['error'] = 'Permission denied';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if ($carrier->fetch($carrierId) > 0) {
|
||||||
|
// Create a copy of the carrier
|
||||||
|
$newCarrier = new EquipmentCarrier($db);
|
||||||
|
$newCarrier->fk_anlage = $carrier->fk_anlage;
|
||||||
|
$newCarrier->fk_panel = $carrier->fk_panel;
|
||||||
|
$newCarrier->label = $carrier->label;
|
||||||
|
$newCarrier->total_te = $carrier->total_te;
|
||||||
|
$newCarrier->note_private = $carrier->note_private;
|
||||||
|
|
||||||
|
$result = $newCarrier->create($user);
|
||||||
|
if ($result > 0) {
|
||||||
|
$response['success'] = true;
|
||||||
|
$response['carrier_id'] = $result;
|
||||||
|
} else {
|
||||||
|
$response['error'] = $newCarrier->error;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$response['error'] = 'Carrier not found';
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
$response['error'] = 'Unknown action';
|
||||||
|
}
|
||||||
|
|
||||||
|
echo json_encode($response);
|
||||||
|
$db->close();
|
||||||
556
ajax/equipment_connection.php
Executable file
556
ajax/equipment_connection.php
Executable file
|
|
@ -0,0 +1,556 @@
|
||||||
|
<?php
|
||||||
|
/* Copyright (C) 2026 Alles Watt lauft
|
||||||
|
*
|
||||||
|
* AJAX endpoint for equipment connections (generic for all system types)
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (!defined('NOTOKENRENEWAL')) define('NOTOKENRENEWAL', '1');
|
||||||
|
if (!defined('NOREQUIREMENU')) define('NOREQUIREMENU', '1');
|
||||||
|
if (!defined('NOREQUIREHTML')) define('NOREQUIREHTML', '1');
|
||||||
|
if (!defined('NOREQUIREAJAX')) define('NOREQUIREAJAX', '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");
|
||||||
|
|
||||||
|
require_once DOL_DOCUMENT_ROOT.'/custom/kundenkarte/class/equipmentconnection.class.php';
|
||||||
|
require_once DOL_DOCUMENT_ROOT.'/custom/kundenkarte/class/equipment.class.php';
|
||||||
|
|
||||||
|
header('Content-Type: application/json; charset=UTF-8');
|
||||||
|
|
||||||
|
$action = GETPOST('action', 'aZ09');
|
||||||
|
$connectionId = GETPOSTINT('connection_id');
|
||||||
|
$carrierId = GETPOSTINT('carrier_id');
|
||||||
|
$equipmentId = GETPOSTINT('equipment_id');
|
||||||
|
|
||||||
|
$connection = new EquipmentConnection($db);
|
||||||
|
|
||||||
|
$response = array('success' => false, 'error' => '');
|
||||||
|
|
||||||
|
// Security check
|
||||||
|
if (!$user->hasRight('kundenkarte', 'read')) {
|
||||||
|
$response['error'] = 'Permission denied';
|
||||||
|
echo json_encode($response);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch ($action) {
|
||||||
|
case 'get':
|
||||||
|
// Get single connection data
|
||||||
|
if ($connectionId > 0 && $connection->fetch($connectionId) > 0) {
|
||||||
|
$response['success'] = true;
|
||||||
|
$response['connection'] = array(
|
||||||
|
'id' => $connection->id,
|
||||||
|
'fk_source' => $connection->fk_source,
|
||||||
|
'source_terminal' => $connection->source_terminal,
|
||||||
|
'fk_target' => $connection->fk_target,
|
||||||
|
'target_terminal' => $connection->target_terminal,
|
||||||
|
'connection_type' => $connection->connection_type,
|
||||||
|
'color' => $connection->color,
|
||||||
|
'output_label' => $connection->output_label,
|
||||||
|
'medium_type' => $connection->medium_type,
|
||||||
|
'medium_spec' => $connection->medium_spec,
|
||||||
|
'medium_length' => $connection->medium_length,
|
||||||
|
'is_rail' => $connection->is_rail,
|
||||||
|
'rail_start_te' => $connection->rail_start_te,
|
||||||
|
'rail_end_te' => $connection->rail_end_te,
|
||||||
|
'rail_phases' => $connection->rail_phases,
|
||||||
|
'excluded_te' => $connection->excluded_te,
|
||||||
|
'fk_carrier' => $connection->fk_carrier,
|
||||||
|
'position_y' => $connection->position_y,
|
||||||
|
'source_label' => $connection->source_label,
|
||||||
|
'target_label' => $connection->target_label
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
$response['error'] = 'Connection not found';
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'list':
|
||||||
|
// List all connections for a carrier
|
||||||
|
if ($carrierId > 0) {
|
||||||
|
$connections = $connection->fetchByCarrier($carrierId);
|
||||||
|
$result = array();
|
||||||
|
foreach ($connections as $c) {
|
||||||
|
$result[] = array(
|
||||||
|
'id' => $c->id,
|
||||||
|
'fk_source' => $c->fk_source,
|
||||||
|
'source_terminal' => $c->source_terminal,
|
||||||
|
'source_label' => $c->source_label,
|
||||||
|
'source_pos' => $c->source_pos,
|
||||||
|
'source_width' => $c->source_width,
|
||||||
|
'fk_target' => $c->fk_target,
|
||||||
|
'target_terminal' => $c->target_terminal,
|
||||||
|
'target_label' => $c->target_label,
|
||||||
|
'target_pos' => $c->target_pos,
|
||||||
|
'connection_type' => $c->connection_type,
|
||||||
|
'color' => $c->getColor(),
|
||||||
|
'output_label' => $c->output_label,
|
||||||
|
'medium_type' => $c->medium_type,
|
||||||
|
'medium_spec' => $c->medium_spec,
|
||||||
|
'medium_length' => $c->medium_length,
|
||||||
|
'is_rail' => $c->is_rail,
|
||||||
|
'rail_start_te' => $c->rail_start_te,
|
||||||
|
'rail_end_te' => $c->rail_end_te,
|
||||||
|
'rail_phases' => $c->rail_phases,
|
||||||
|
'excluded_te' => $c->excluded_te,
|
||||||
|
'position_y' => $c->position_y,
|
||||||
|
'display_label' => $c->getDisplayLabel()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
$response['success'] = true;
|
||||||
|
$response['connections'] = $result;
|
||||||
|
} else {
|
||||||
|
$response['error'] = 'Missing carrier_id';
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'list_outputs':
|
||||||
|
// List outputs for an equipment
|
||||||
|
if ($equipmentId > 0) {
|
||||||
|
$outputs = $connection->fetchOutputs($equipmentId);
|
||||||
|
$result = array();
|
||||||
|
foreach ($outputs as $c) {
|
||||||
|
$result[] = array(
|
||||||
|
'id' => $c->id,
|
||||||
|
'connection_type' => $c->connection_type,
|
||||||
|
'color' => $c->getColor(),
|
||||||
|
'output_label' => $c->output_label,
|
||||||
|
'medium_type' => $c->medium_type,
|
||||||
|
'medium_spec' => $c->medium_spec,
|
||||||
|
'medium_length' => $c->medium_length,
|
||||||
|
'display_label' => $c->getDisplayLabel()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
$response['success'] = true;
|
||||||
|
$response['outputs'] = $result;
|
||||||
|
} else {
|
||||||
|
$response['error'] = 'Missing equipment_id';
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'create':
|
||||||
|
if (!$user->hasRight('kundenkarte', 'write')) {
|
||||||
|
$response['error'] = 'Permission denied';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
$connection->fk_source = GETPOSTINT('fk_source');
|
||||||
|
$connection->source_terminal = GETPOST('source_terminal', 'alphanohtml') ?: 'output';
|
||||||
|
$connection->source_terminal_id = GETPOST('source_terminal_id', 'alphanohtml');
|
||||||
|
$connection->fk_target = GETPOSTINT('fk_target');
|
||||||
|
$connection->target_terminal = GETPOST('target_terminal', 'alphanohtml') ?: 'input';
|
||||||
|
$connection->target_terminal_id = GETPOST('target_terminal_id', 'alphanohtml');
|
||||||
|
$connection->connection_type = GETPOST('connection_type', 'alphanohtml');
|
||||||
|
$connection->color = GETPOST('color', 'alphanohtml');
|
||||||
|
$connection->output_label = GETPOST('output_label', 'alphanohtml');
|
||||||
|
$connection->medium_type = GETPOST('medium_type', 'alphanohtml');
|
||||||
|
$connection->medium_spec = GETPOST('medium_spec', 'alphanohtml');
|
||||||
|
$connection->medium_length = GETPOST('medium_length', 'alphanohtml');
|
||||||
|
$connection->is_rail = GETPOSTINT('is_rail');
|
||||||
|
$connection->rail_start_te = GETPOSTINT('rail_start_te');
|
||||||
|
$connection->rail_end_te = GETPOSTINT('rail_end_te');
|
||||||
|
$connection->fk_carrier = $carrierId;
|
||||||
|
$connection->position_y = GETPOSTINT('position_y');
|
||||||
|
$connection->path_data = GETPOST('path_data', 'nohtml');
|
||||||
|
|
||||||
|
$result = $connection->create($user);
|
||||||
|
if ($result > 0) {
|
||||||
|
$response['success'] = true;
|
||||||
|
$response['connection_id'] = $result;
|
||||||
|
} else {
|
||||||
|
$response['error'] = $connection->error ?: 'Create failed';
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'update':
|
||||||
|
if (!$user->hasRight('kundenkarte', 'write')) {
|
||||||
|
$response['error'] = 'Permission denied';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if ($connection->fetch($connectionId) > 0) {
|
||||||
|
// Only update fields that are actually sent (preserve existing values)
|
||||||
|
if (GETPOSTISSET('fk_source')) $connection->fk_source = GETPOSTINT('fk_source');
|
||||||
|
if (GETPOSTISSET('source_terminal')) $connection->source_terminal = GETPOST('source_terminal', 'alphanohtml') ?: $connection->source_terminal;
|
||||||
|
if (GETPOSTISSET('fk_target')) $connection->fk_target = GETPOSTINT('fk_target');
|
||||||
|
if (GETPOSTISSET('target_terminal')) $connection->target_terminal = GETPOST('target_terminal', 'alphanohtml') ?: $connection->target_terminal;
|
||||||
|
if (GETPOSTISSET('connection_type')) $connection->connection_type = GETPOST('connection_type', 'alphanohtml');
|
||||||
|
if (GETPOSTISSET('color')) $connection->color = GETPOST('color', 'alphanohtml');
|
||||||
|
if (GETPOSTISSET('output_label')) $connection->output_label = GETPOST('output_label', 'alphanohtml');
|
||||||
|
if (GETPOSTISSET('medium_type')) $connection->medium_type = GETPOST('medium_type', 'alphanohtml');
|
||||||
|
if (GETPOSTISSET('medium_spec')) $connection->medium_spec = GETPOST('medium_spec', 'alphanohtml');
|
||||||
|
if (GETPOSTISSET('medium_length')) $connection->medium_length = GETPOST('medium_length', 'alphanohtml');
|
||||||
|
if (GETPOSTISSET('is_rail')) $connection->is_rail = GETPOSTINT('is_rail');
|
||||||
|
if (GETPOSTISSET('rail_start_te')) $connection->rail_start_te = GETPOSTINT('rail_start_te');
|
||||||
|
if (GETPOSTISSET('rail_end_te')) $connection->rail_end_te = GETPOSTINT('rail_end_te');
|
||||||
|
if (GETPOSTISSET('rail_phases')) $connection->rail_phases = GETPOST('rail_phases', 'alphanohtml');
|
||||||
|
if (GETPOSTISSET('excluded_te')) $connection->excluded_te = GETPOST('excluded_te', 'alphanohtml');
|
||||||
|
if (GETPOSTISSET('position_y')) $connection->position_y = GETPOSTINT('position_y');
|
||||||
|
if (GETPOSTISSET('path_data')) $connection->path_data = GETPOST('path_data', 'nohtml');
|
||||||
|
|
||||||
|
$result = $connection->update($user);
|
||||||
|
if ($result > 0) {
|
||||||
|
$response['success'] = true;
|
||||||
|
} else {
|
||||||
|
$response['error'] = $connection->error ?: 'Update failed';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$response['error'] = 'Connection not found';
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'delete':
|
||||||
|
if (!$user->hasRight('kundenkarte', 'delete')) {
|
||||||
|
$response['error'] = 'Permission denied';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if ($connection->fetch($connectionId) > 0) {
|
||||||
|
$result = $connection->delete($user);
|
||||||
|
if ($result > 0) {
|
||||||
|
$response['success'] = true;
|
||||||
|
} else {
|
||||||
|
$response['error'] = $connection->error ?: 'Delete failed';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$response['error'] = 'Connection not found';
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'create_rail':
|
||||||
|
// Create a rail/bar connection spanning multiple equipment
|
||||||
|
if (!$user->hasRight('kundenkarte', 'write')) {
|
||||||
|
$response['error'] = 'Permission denied';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
$connection->is_rail = 1;
|
||||||
|
$connection->connection_type = GETPOST('connection_type', 'alphanohtml');
|
||||||
|
$connection->color = GETPOST('color', 'alphanohtml');
|
||||||
|
$connection->rail_start_te = GETPOSTINT('rail_start_te');
|
||||||
|
$connection->rail_end_te = GETPOSTINT('rail_end_te');
|
||||||
|
$connection->rail_phases = GETPOST('rail_phases', 'alphanohtml');
|
||||||
|
$connection->excluded_te = GETPOST('excluded_te', 'alphanohtml');
|
||||||
|
$connection->fk_carrier = $carrierId;
|
||||||
|
$connection->position_y = GETPOSTINT('position_y');
|
||||||
|
|
||||||
|
$result = $connection->create($user);
|
||||||
|
if ($result > 0) {
|
||||||
|
$response['success'] = true;
|
||||||
|
$response['connection_id'] = $result;
|
||||||
|
} else {
|
||||||
|
$response['error'] = $connection->error ?: 'Create failed';
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'update_rail_position':
|
||||||
|
// Update rail/busbar start and end position (for drag & drop)
|
||||||
|
// Also supports moving to a different carrier (different panel/hutschiene)
|
||||||
|
if (!$user->hasRight('kundenkarte', 'write')) {
|
||||||
|
$response['error'] = 'Permission denied';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if ($connection->fetch($connectionId) > 0) {
|
||||||
|
// Only allow updating rail connections
|
||||||
|
if (!$connection->is_rail) {
|
||||||
|
$response['error'] = 'Not a rail connection';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
$connection->rail_start_te = GETPOSTINT('rail_start_te');
|
||||||
|
$connection->rail_end_te = GETPOSTINT('rail_end_te');
|
||||||
|
|
||||||
|
// Update carrier if provided (for moving between panels)
|
||||||
|
if (GETPOSTISSET('carrier_id') && GETPOSTINT('carrier_id') > 0) {
|
||||||
|
$connection->fk_carrier = GETPOSTINT('carrier_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = $connection->update($user);
|
||||||
|
if ($result > 0) {
|
||||||
|
$response['success'] = true;
|
||||||
|
} else {
|
||||||
|
$response['error'] = $connection->error ?: 'Update failed';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$response['error'] = 'Connection not found';
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'create_output':
|
||||||
|
// Create an output connection
|
||||||
|
if (!$user->hasRight('kundenkarte', 'write')) {
|
||||||
|
$response['error'] = 'Permission denied';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
$connection->fk_source = $equipmentId;
|
||||||
|
$connection->source_terminal = 'output';
|
||||||
|
$connection->fk_target = null;
|
||||||
|
$connection->connection_type = GETPOST('connection_type', 'alphanohtml');
|
||||||
|
$connection->color = GETPOST('color', 'alphanohtml');
|
||||||
|
$connection->output_label = GETPOST('output_label', 'alphanohtml');
|
||||||
|
$connection->medium_type = GETPOST('medium_type', 'alphanohtml');
|
||||||
|
$connection->medium_spec = GETPOST('medium_spec', 'alphanohtml');
|
||||||
|
$connection->medium_length = GETPOST('medium_length', 'alphanohtml');
|
||||||
|
$connection->fk_carrier = $carrierId;
|
||||||
|
$connection->position_y = 0;
|
||||||
|
|
||||||
|
$result = $connection->create($user);
|
||||||
|
if ($result > 0) {
|
||||||
|
$response['success'] = true;
|
||||||
|
$response['connection_id'] = $result;
|
||||||
|
} else {
|
||||||
|
$response['error'] = $connection->error ?: 'Create failed';
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'list_all':
|
||||||
|
// List all connections for an anlage (across all carriers)
|
||||||
|
$anlageId = GETPOSTINT('anlage_id');
|
||||||
|
if ($anlageId > 0) {
|
||||||
|
// Get all carriers for this anlage
|
||||||
|
require_once DOL_DOCUMENT_ROOT.'/custom/kundenkarte/class/equipmentcarrier.class.php';
|
||||||
|
$carrierObj = new EquipmentCarrier($db);
|
||||||
|
$carriers = $carrierObj->fetchByAnlage($anlageId);
|
||||||
|
$carrierIds = array();
|
||||||
|
foreach ($carriers as $carrier) {
|
||||||
|
$carrierIds[] = (int)$carrier->id;
|
||||||
|
}
|
||||||
|
|
||||||
|
$allConnections = array();
|
||||||
|
|
||||||
|
if (!empty($carrierIds)) {
|
||||||
|
// Find all connections where source OR target equipment belongs to this anlage's carriers
|
||||||
|
// This includes connections with fk_carrier=NULL
|
||||||
|
$sql = "SELECT DISTINCT c.*,
|
||||||
|
se.label as source_label, se.position_te as source_pos, se.width_te as source_width,
|
||||||
|
te.label as target_label, te.position_te as target_pos
|
||||||
|
FROM ".MAIN_DB_PREFIX."kundenkarte_equipment_connection c
|
||||||
|
LEFT JOIN ".MAIN_DB_PREFIX."kundenkarte_equipment se ON c.fk_source = se.rowid
|
||||||
|
LEFT JOIN ".MAIN_DB_PREFIX."kundenkarte_equipment te ON c.fk_target = te.rowid
|
||||||
|
WHERE (c.fk_carrier IN (".implode(',', $carrierIds).")
|
||||||
|
OR se.fk_carrier IN (".implode(',', $carrierIds).")
|
||||||
|
OR te.fk_carrier IN (".implode(',', $carrierIds)."))
|
||||||
|
AND c.status = 1";
|
||||||
|
|
||||||
|
$resql = $db->query($sql);
|
||||||
|
if ($resql) {
|
||||||
|
while ($obj = $db->fetch_object($resql)) {
|
||||||
|
$allConnections[] = array(
|
||||||
|
'id' => $obj->rowid,
|
||||||
|
'fk_source' => $obj->fk_source,
|
||||||
|
'source_terminal' => $obj->source_terminal,
|
||||||
|
'source_terminal_id' => $obj->source_terminal_id,
|
||||||
|
'source_label' => $obj->source_label,
|
||||||
|
'source_pos' => $obj->source_pos,
|
||||||
|
'source_width' => $obj->source_width,
|
||||||
|
'fk_target' => $obj->fk_target,
|
||||||
|
'target_terminal' => $obj->target_terminal,
|
||||||
|
'target_terminal_id' => $obj->target_terminal_id,
|
||||||
|
'target_label' => $obj->target_label,
|
||||||
|
'target_pos' => $obj->target_pos,
|
||||||
|
'connection_type' => $obj->connection_type,
|
||||||
|
'color' => $obj->color ?: '#3498db',
|
||||||
|
'output_label' => $obj->output_label,
|
||||||
|
'medium_type' => $obj->medium_type,
|
||||||
|
'medium_spec' => $obj->medium_spec,
|
||||||
|
'medium_length' => $obj->medium_length,
|
||||||
|
'is_rail' => $obj->is_rail,
|
||||||
|
'rail_start_te' => $obj->rail_start_te,
|
||||||
|
'rail_end_te' => $obj->rail_end_te,
|
||||||
|
'rail_phases' => $obj->rail_phases,
|
||||||
|
'position_y' => $obj->position_y,
|
||||||
|
'fk_carrier' => $obj->fk_carrier,
|
||||||
|
'path_data' => isset($obj->path_data) ? $obj->path_data : null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
$db->free($resql);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$response['success'] = true;
|
||||||
|
$response['connections'] = $allConnections;
|
||||||
|
} else {
|
||||||
|
$response['error'] = 'Missing anlage_id';
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'clear_all':
|
||||||
|
// Delete all connections for an anlage
|
||||||
|
if (!$user->hasRight('kundenkarte', 'delete')) {
|
||||||
|
$response['error'] = 'Permission denied';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
$anlageId = GETPOSTINT('anlage_id');
|
||||||
|
if ($anlageId > 0) {
|
||||||
|
// Get all carriers for this anlage
|
||||||
|
require_once DOL_DOCUMENT_ROOT.'/custom/kundenkarte/class/equipmentcarrier.class.php';
|
||||||
|
$carrierObj = new EquipmentCarrier($db);
|
||||||
|
$carriers = $carrierObj->fetchByAnlage($anlageId);
|
||||||
|
|
||||||
|
$deletedCount = 0;
|
||||||
|
foreach ($carriers as $carrier) {
|
||||||
|
$sql = "DELETE FROM ".MAIN_DB_PREFIX."kundenkarte_equipment_connection WHERE fk_carrier = ".((int)$carrier->id);
|
||||||
|
$resql = $db->query($sql);
|
||||||
|
if ($resql) {
|
||||||
|
$deletedCount += $db->affected_rows($resql);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$response['success'] = true;
|
||||||
|
$response['deleted_count'] = $deletedCount;
|
||||||
|
} else {
|
||||||
|
$response['error'] = 'Missing anlage_id';
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// Bridge Actions (Brücken zwischen Klemmen)
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
case 'list_bridges':
|
||||||
|
// List all bridges for an anlage
|
||||||
|
$anlageId = GETPOSTINT('anlage_id');
|
||||||
|
if ($anlageId > 0) {
|
||||||
|
require_once DOL_DOCUMENT_ROOT.'/custom/kundenkarte/class/terminalbridge.class.php';
|
||||||
|
$bridgeObj = new TerminalBridge($db);
|
||||||
|
$bridges = $bridgeObj->fetchAllByAnlage($anlageId);
|
||||||
|
|
||||||
|
$bridgeList = array();
|
||||||
|
foreach ($bridges as $bridge) {
|
||||||
|
$bridgeList[] = array(
|
||||||
|
'id' => $bridge->id,
|
||||||
|
'fk_carrier' => $bridge->fk_carrier,
|
||||||
|
'start_te' => $bridge->start_te,
|
||||||
|
'end_te' => $bridge->end_te,
|
||||||
|
'terminal_side' => $bridge->terminal_side,
|
||||||
|
'terminal_row' => $bridge->terminal_row,
|
||||||
|
'color' => $bridge->color,
|
||||||
|
'bridge_type' => $bridge->bridge_type,
|
||||||
|
'label' => $bridge->label
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$response['success'] = true;
|
||||||
|
$response['bridges'] = $bridgeList;
|
||||||
|
} else {
|
||||||
|
$response['error'] = 'Missing anlage_id';
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'create_bridge':
|
||||||
|
// Create a new terminal bridge
|
||||||
|
if (!$user->hasRight('kundenkarte', 'write')) {
|
||||||
|
$response['error'] = 'Permission denied';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
$anlageId = GETPOSTINT('anlage_id');
|
||||||
|
$carrierId = GETPOSTINT('carrier_id');
|
||||||
|
$startTE = GETPOSTINT('start_te');
|
||||||
|
$endTE = GETPOSTINT('end_te');
|
||||||
|
|
||||||
|
if (empty($anlageId) || empty($carrierId) || empty($startTE) || empty($endTE)) {
|
||||||
|
$response['error'] = 'Missing required parameters';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
require_once DOL_DOCUMENT_ROOT.'/custom/kundenkarte/class/terminalbridge.class.php';
|
||||||
|
$bridge = new TerminalBridge($db);
|
||||||
|
$bridge->fk_anlage = $anlageId;
|
||||||
|
$bridge->fk_carrier = $carrierId;
|
||||||
|
$bridge->start_te = min($startTE, $endTE);
|
||||||
|
$bridge->end_te = max($startTE, $endTE);
|
||||||
|
$bridge->terminal_side = GETPOST('terminal_side', 'alpha') ?: 'top';
|
||||||
|
$bridge->terminal_row = GETPOSTINT('terminal_row');
|
||||||
|
$bridge->color = GETPOST('color', 'alphanohtml') ?: '#e74c3c';
|
||||||
|
$bridge->bridge_type = GETPOST('bridge_type', 'alpha') ?: 'standard';
|
||||||
|
$bridge->label = GETPOST('label', 'alphanohtml');
|
||||||
|
|
||||||
|
$result = $bridge->create($user);
|
||||||
|
if ($result > 0) {
|
||||||
|
$response['success'] = true;
|
||||||
|
$response['bridge_id'] = $result;
|
||||||
|
$response['bridge'] = array(
|
||||||
|
'id' => $bridge->id,
|
||||||
|
'fk_carrier' => $bridge->fk_carrier,
|
||||||
|
'start_te' => $bridge->start_te,
|
||||||
|
'end_te' => $bridge->end_te,
|
||||||
|
'terminal_side' => $bridge->terminal_side,
|
||||||
|
'terminal_row' => $bridge->terminal_row,
|
||||||
|
'color' => $bridge->color,
|
||||||
|
'bridge_type' => $bridge->bridge_type,
|
||||||
|
'label' => $bridge->label
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
$response['error'] = $bridge->error ?: 'Create failed';
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'update_bridge':
|
||||||
|
// Update an existing bridge
|
||||||
|
if (!$user->hasRight('kundenkarte', 'write')) {
|
||||||
|
$response['error'] = 'Permission denied';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
$bridgeId = GETPOSTINT('bridge_id');
|
||||||
|
if ($bridgeId > 0) {
|
||||||
|
require_once DOL_DOCUMENT_ROOT.'/custom/kundenkarte/class/terminalbridge.class.php';
|
||||||
|
$bridge = new TerminalBridge($db);
|
||||||
|
if ($bridge->fetch($bridgeId) > 0) {
|
||||||
|
if (GETPOSTISSET('start_te')) $bridge->start_te = GETPOSTINT('start_te');
|
||||||
|
if (GETPOSTISSET('end_te')) $bridge->end_te = GETPOSTINT('end_te');
|
||||||
|
if (GETPOSTISSET('terminal_side')) $bridge->terminal_side = GETPOST('terminal_side', 'alpha');
|
||||||
|
if (GETPOSTISSET('terminal_row')) $bridge->terminal_row = GETPOSTINT('terminal_row');
|
||||||
|
if (GETPOSTISSET('color')) $bridge->color = GETPOST('color', 'alphanohtml');
|
||||||
|
if (GETPOSTISSET('bridge_type')) $bridge->bridge_type = GETPOST('bridge_type', 'alpha');
|
||||||
|
if (GETPOSTISSET('label')) $bridge->label = GETPOST('label', 'alphanohtml');
|
||||||
|
|
||||||
|
$result = $bridge->update($user);
|
||||||
|
if ($result > 0) {
|
||||||
|
$response['success'] = true;
|
||||||
|
} else {
|
||||||
|
$response['error'] = $bridge->error ?: 'Update failed';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$response['error'] = 'Bridge not found';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$response['error'] = 'Missing bridge_id';
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'delete_bridge':
|
||||||
|
// Delete a bridge
|
||||||
|
if (!$user->hasRight('kundenkarte', 'write')) {
|
||||||
|
$response['error'] = 'Permission denied';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
$bridgeId = GETPOSTINT('bridge_id');
|
||||||
|
if ($bridgeId > 0) {
|
||||||
|
require_once DOL_DOCUMENT_ROOT.'/custom/kundenkarte/class/terminalbridge.class.php';
|
||||||
|
$bridge = new TerminalBridge($db);
|
||||||
|
if ($bridge->fetch($bridgeId) > 0) {
|
||||||
|
$result = $bridge->delete($user);
|
||||||
|
if ($result > 0) {
|
||||||
|
$response['success'] = true;
|
||||||
|
} else {
|
||||||
|
$response['error'] = $bridge->error ?: 'Delete failed';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$response['error'] = 'Bridge not found';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$response['error'] = 'Missing bridge_id';
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
$response['error'] = 'Unknown action';
|
||||||
|
}
|
||||||
|
|
||||||
|
echo json_encode($response);
|
||||||
|
$db->close();
|
||||||
201
ajax/equipment_panel.php
Executable file
201
ajax/equipment_panel.php
Executable file
|
|
@ -0,0 +1,201 @@
|
||||||
|
<?php
|
||||||
|
/* Copyright (C) 2026 Alles Watt lauft
|
||||||
|
*
|
||||||
|
* AJAX endpoint for equipment panels (Schaltschrankfelder)
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (!defined('NOTOKENRENEWAL')) define('NOTOKENRENEWAL', '1');
|
||||||
|
if (!defined('NOREQUIREMENU')) define('NOREQUIREMENU', '1');
|
||||||
|
if (!defined('NOREQUIREHTML')) define('NOREQUIREHTML', '1');
|
||||||
|
if (!defined('NOREQUIREAJAX')) define('NOREQUIREAJAX', '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");
|
||||||
|
|
||||||
|
require_once DOL_DOCUMENT_ROOT.'/custom/kundenkarte/class/equipmentpanel.class.php';
|
||||||
|
require_once DOL_DOCUMENT_ROOT.'/custom/kundenkarte/class/equipmentcarrier.class.php';
|
||||||
|
|
||||||
|
header('Content-Type: application/json; charset=UTF-8');
|
||||||
|
|
||||||
|
$action = GETPOST('action', 'aZ09');
|
||||||
|
$panelId = GETPOSTINT('panel_id');
|
||||||
|
$anlageId = GETPOSTINT('anlage_id');
|
||||||
|
|
||||||
|
$panel = new EquipmentPanel($db);
|
||||||
|
|
||||||
|
$response = array('success' => false, 'error' => '');
|
||||||
|
|
||||||
|
// Security check
|
||||||
|
if (!$user->hasRight('kundenkarte', 'read')) {
|
||||||
|
$response['error'] = 'Permission denied';
|
||||||
|
echo json_encode($response);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch ($action) {
|
||||||
|
case 'get':
|
||||||
|
// Get single panel data
|
||||||
|
if ($panelId > 0 && $panel->fetch($panelId) > 0) {
|
||||||
|
$response['success'] = true;
|
||||||
|
$response['panel'] = array(
|
||||||
|
'id' => $panel->id,
|
||||||
|
'fk_anlage' => $panel->fk_anlage,
|
||||||
|
'label' => $panel->label,
|
||||||
|
'position' => $panel->position,
|
||||||
|
'note_private' => $panel->note_private,
|
||||||
|
'status' => $panel->status
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
$response['error'] = 'Panel not found';
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'list':
|
||||||
|
// List all panels for an Anlage
|
||||||
|
if ($anlageId > 0) {
|
||||||
|
$panels = $panel->fetchByAnlage($anlageId);
|
||||||
|
$result = array();
|
||||||
|
foreach ($panels as $p) {
|
||||||
|
$result[] = array(
|
||||||
|
'id' => $p->id,
|
||||||
|
'fk_anlage' => $p->fk_anlage,
|
||||||
|
'label' => $p->label,
|
||||||
|
'position' => $p->position,
|
||||||
|
'status' => $p->status
|
||||||
|
);
|
||||||
|
}
|
||||||
|
$response['success'] = true;
|
||||||
|
$response['panels'] = $result;
|
||||||
|
} else {
|
||||||
|
$response['error'] = 'Missing anlage_id';
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'create':
|
||||||
|
if (!$user->hasRight('kundenkarte', 'write')) {
|
||||||
|
$response['error'] = 'Permission denied';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
$panel->fk_anlage = $anlageId;
|
||||||
|
$panel->label = GETPOST('label', 'alphanohtml');
|
||||||
|
$panel->position = GETPOSTINT('position');
|
||||||
|
$panel->note_private = GETPOST('note_private', 'restricthtml');
|
||||||
|
|
||||||
|
$result = $panel->create($user);
|
||||||
|
if ($result > 0) {
|
||||||
|
$response['success'] = true;
|
||||||
|
$response['panel_id'] = $result;
|
||||||
|
$response['label'] = $panel->label;
|
||||||
|
} else {
|
||||||
|
$response['error'] = $panel->error;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'update':
|
||||||
|
if (!$user->hasRight('kundenkarte', 'write')) {
|
||||||
|
$response['error'] = 'Permission denied';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if ($panel->fetch($panelId) > 0) {
|
||||||
|
$panel->label = GETPOST('label', 'alphanohtml') ?: $panel->label;
|
||||||
|
$panel->position = GETPOSTINT('position') ?: $panel->position;
|
||||||
|
$panel->note_private = GETPOST('note_private', 'restricthtml');
|
||||||
|
|
||||||
|
$result = $panel->update($user);
|
||||||
|
if ($result > 0) {
|
||||||
|
$response['success'] = true;
|
||||||
|
} else {
|
||||||
|
$response['error'] = $panel->error;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$response['error'] = 'Panel not found';
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'delete':
|
||||||
|
if (!$user->hasRight('kundenkarte', 'delete')) {
|
||||||
|
$response['error'] = 'Permission denied';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if ($panel->fetch($panelId) > 0) {
|
||||||
|
$result = $panel->delete($user);
|
||||||
|
if ($result > 0) {
|
||||||
|
$response['success'] = true;
|
||||||
|
} else {
|
||||||
|
$response['error'] = $panel->error;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$response['error'] = 'Panel not found';
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'list_with_carriers':
|
||||||
|
// List all panels with their carriers for an Anlage
|
||||||
|
if ($anlageId > 0) {
|
||||||
|
$panels = $panel->fetchByAnlage($anlageId);
|
||||||
|
$result = array();
|
||||||
|
|
||||||
|
foreach ($panels as $p) {
|
||||||
|
$panelData = array(
|
||||||
|
'id' => $p->id,
|
||||||
|
'fk_anlage' => $p->fk_anlage,
|
||||||
|
'label' => $p->label,
|
||||||
|
'position' => $p->position,
|
||||||
|
'status' => $p->status,
|
||||||
|
'carriers' => array()
|
||||||
|
);
|
||||||
|
|
||||||
|
// Fetch carriers for this panel
|
||||||
|
$p->fetchCarriers();
|
||||||
|
foreach ($p->carriers as $c) {
|
||||||
|
$panelData['carriers'][] = array(
|
||||||
|
'id' => $c->id,
|
||||||
|
'label' => $c->label,
|
||||||
|
'total_te' => $c->total_te,
|
||||||
|
'position' => $c->position
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$result[] = $panelData;
|
||||||
|
}
|
||||||
|
|
||||||
|
$response['success'] = true;
|
||||||
|
$response['panels'] = $result;
|
||||||
|
} else {
|
||||||
|
$response['error'] = 'Missing anlage_id';
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'duplicate':
|
||||||
|
if (!$user->hasRight('kundenkarte', 'write')) {
|
||||||
|
$response['error'] = 'Permission denied';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if ($panel->fetch($panelId) > 0) {
|
||||||
|
// Create a copy of the panel
|
||||||
|
$newPanel = new EquipmentPanel($db);
|
||||||
|
$newPanel->fk_anlage = $panel->fk_anlage;
|
||||||
|
$newPanel->label = $panel->label.' (Kopie)';
|
||||||
|
$newPanel->note_private = $panel->note_private;
|
||||||
|
|
||||||
|
$result = $newPanel->create($user);
|
||||||
|
if ($result > 0) {
|
||||||
|
$response['success'] = true;
|
||||||
|
$response['panel_id'] = $result;
|
||||||
|
} else {
|
||||||
|
$response['error'] = $newPanel->error;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$response['error'] = 'Panel not found';
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
$response['error'] = 'Unknown action';
|
||||||
|
}
|
||||||
|
|
||||||
|
echo json_encode($response);
|
||||||
|
$db->close();
|
||||||
190
ajax/equipment_type_block_image.php
Executable file
190
ajax/equipment_type_block_image.php
Executable file
|
|
@ -0,0 +1,190 @@
|
||||||
|
<?php
|
||||||
|
/* Copyright (C) 2026 Alles Watt lauft
|
||||||
|
*
|
||||||
|
* AJAX handler for equipment type block image upload
|
||||||
|
* Accepts image files for SchematicEditor block display
|
||||||
|
*/
|
||||||
|
|
||||||
|
$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");
|
||||||
|
|
||||||
|
require_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php';
|
||||||
|
dol_include_once('/kundenkarte/class/equipmenttype.class.php');
|
||||||
|
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
// Security check
|
||||||
|
if (!$user->admin && !$user->hasRight('kundenkarte', 'admin')) {
|
||||||
|
echo json_encode(array('success' => false, 'error' => 'Access denied'));
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$action = GETPOST('action', 'aZ09');
|
||||||
|
$typeId = GETPOSTINT('type_id');
|
||||||
|
|
||||||
|
$response = array('success' => false);
|
||||||
|
|
||||||
|
// Directory for block images
|
||||||
|
$uploadDir = DOL_DATA_ROOT.'/kundenkarte/block_images/';
|
||||||
|
|
||||||
|
// Create directory if not exists
|
||||||
|
if (!is_dir($uploadDir)) {
|
||||||
|
dol_mkdir($uploadDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
switch ($action) {
|
||||||
|
case 'upload':
|
||||||
|
if (empty($_FILES['block_image']) || $_FILES['block_image']['error'] !== UPLOAD_ERR_OK) {
|
||||||
|
$response['error'] = 'No file uploaded or upload error';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
$file = $_FILES['block_image'];
|
||||||
|
$fileName = dol_sanitizeFileName($file['name']);
|
||||||
|
$fileExt = strtolower(pathinfo($fileName, PATHINFO_EXTENSION));
|
||||||
|
|
||||||
|
// Validate file type
|
||||||
|
$allowedExtensions = array('svg', 'png', 'jpg', 'jpeg', 'gif', 'webp');
|
||||||
|
if (!in_array($fileExt, $allowedExtensions)) {
|
||||||
|
$response['error'] = 'Invalid file type. Only SVG, PNG, JPG, GIF, WEBP are allowed.';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate MIME type
|
||||||
|
$mimeType = mime_content_type($file['tmp_name']);
|
||||||
|
$allowedMimes = array('image/svg+xml', 'image/png', 'image/jpeg', 'image/gif', 'image/webp', 'text/plain', 'text/xml', 'application/xml');
|
||||||
|
if (!in_array($mimeType, $allowedMimes)) {
|
||||||
|
$response['error'] = 'Invalid MIME type: '.$mimeType;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For SVG files, do basic security check
|
||||||
|
if ($fileExt === 'svg') {
|
||||||
|
$content = file_get_contents($file['tmp_name']);
|
||||||
|
// Check for potentially dangerous content
|
||||||
|
$dangerous = array('<script', 'javascript:', 'onload=', 'onerror=', 'onclick=', 'onmouseover=');
|
||||||
|
foreach ($dangerous as $pattern) {
|
||||||
|
if (stripos($content, $pattern) !== false) {
|
||||||
|
$response['error'] = 'SVG contains potentially dangerous content';
|
||||||
|
break 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate unique filename
|
||||||
|
$newFileName = 'block_'.$typeId.'_'.dol_now().'.'.$fileExt;
|
||||||
|
$destPath = $uploadDir.$newFileName;
|
||||||
|
|
||||||
|
// Move uploaded file
|
||||||
|
if (move_uploaded_file($file['tmp_name'], $destPath)) {
|
||||||
|
// Update database
|
||||||
|
$equipmentType = new EquipmentType($db);
|
||||||
|
if ($equipmentType->fetch($typeId) > 0) {
|
||||||
|
// Delete old block image file if exists
|
||||||
|
if ($equipmentType->block_image && file_exists($uploadDir.$equipmentType->block_image)) {
|
||||||
|
unlink($uploadDir.$equipmentType->block_image);
|
||||||
|
}
|
||||||
|
|
||||||
|
$equipmentType->block_image = $newFileName;
|
||||||
|
$result = $equipmentType->update($user);
|
||||||
|
|
||||||
|
if ($result > 0) {
|
||||||
|
$response['success'] = true;
|
||||||
|
$response['block_image'] = $newFileName;
|
||||||
|
$response['block_image_url'] = DOL_URL_ROOT.'/document.php?modulepart=kundenkarte&file=block_images/'.$newFileName;
|
||||||
|
} else {
|
||||||
|
$response['error'] = 'Database update failed';
|
||||||
|
// Remove uploaded file on DB error
|
||||||
|
unlink($destPath);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$response['error'] = 'Equipment type not found';
|
||||||
|
unlink($destPath);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$response['error'] = 'Failed to move uploaded file';
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'delete':
|
||||||
|
$equipmentType = new EquipmentType($db);
|
||||||
|
if ($equipmentType->fetch($typeId) > 0) {
|
||||||
|
if ($equipmentType->block_image && file_exists($uploadDir.$equipmentType->block_image)) {
|
||||||
|
unlink($uploadDir.$equipmentType->block_image);
|
||||||
|
}
|
||||||
|
$equipmentType->block_image = '';
|
||||||
|
$result = $equipmentType->update($user);
|
||||||
|
if ($result > 0) {
|
||||||
|
$response['success'] = true;
|
||||||
|
} else {
|
||||||
|
$response['error'] = 'Database update failed';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$response['error'] = 'Equipment type not found';
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'get':
|
||||||
|
$equipmentType = new EquipmentType($db);
|
||||||
|
if ($equipmentType->fetch($typeId) > 0) {
|
||||||
|
$response['success'] = true;
|
||||||
|
$response['block_image'] = $equipmentType->block_image;
|
||||||
|
if ($equipmentType->block_image) {
|
||||||
|
$response['block_image_url'] = DOL_URL_ROOT.'/document.php?modulepart=kundenkarte&file=block_images/'.$equipmentType->block_image;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$response['error'] = 'Equipment type not found';
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'select':
|
||||||
|
// Select an existing image from the block_images folder
|
||||||
|
$selectedImage = GETPOST('image', 'alphanohtml');
|
||||||
|
if (empty($selectedImage)) {
|
||||||
|
$response['error'] = 'No image selected';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Path-Traversal-Schutz: nur Dateiname ohne Verzeichnisanteile
|
||||||
|
$selectedImage = basename($selectedImage);
|
||||||
|
|
||||||
|
// Validate that the image exists
|
||||||
|
$imagePath = $uploadDir . $selectedImage;
|
||||||
|
if (!file_exists($imagePath)) {
|
||||||
|
$response['error'] = 'Image file not found';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate file extension
|
||||||
|
$fileExt = strtolower(pathinfo($selectedImage, PATHINFO_EXTENSION));
|
||||||
|
$allowedExtensions = array('svg', 'png', 'jpg', 'jpeg', 'gif', 'webp');
|
||||||
|
if (!in_array($fileExt, $allowedExtensions)) {
|
||||||
|
$response['error'] = 'Invalid file type';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
$equipmentType = new EquipmentType($db);
|
||||||
|
if ($equipmentType->fetch($typeId) > 0) {
|
||||||
|
$equipmentType->block_image = $selectedImage;
|
||||||
|
$result = $equipmentType->update($user);
|
||||||
|
if ($result > 0) {
|
||||||
|
$response['success'] = true;
|
||||||
|
$response['block_image'] = $selectedImage;
|
||||||
|
$response['block_image_url'] = DOL_URL_ROOT.'/document.php?modulepart=kundenkarte&file=block_images/'.urlencode($selectedImage);
|
||||||
|
} else {
|
||||||
|
$response['error'] = 'Database update failed';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$response['error'] = 'Equipment type not found';
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
$response['error'] = 'Unknown action';
|
||||||
|
}
|
||||||
|
|
||||||
|
echo json_encode($response);
|
||||||
|
$db->close();
|
||||||
74
ajax/equipment_type_fields.php
Executable file
74
ajax/equipment_type_fields.php
Executable file
|
|
@ -0,0 +1,74 @@
|
||||||
|
<?php
|
||||||
|
/* Copyright (C) 2026 Alles Watt lauft
|
||||||
|
*
|
||||||
|
* AJAX endpoint to get equipment type fields
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (!defined('NOTOKENRENEWAL')) define('NOTOKENRENEWAL', '1');
|
||||||
|
if (!defined('NOREQUIREMENU')) define('NOREQUIREMENU', '1');
|
||||||
|
if (!defined('NOREQUIREHTML')) define('NOREQUIREHTML', '1');
|
||||||
|
if (!defined('NOREQUIREAJAX')) define('NOREQUIREAJAX', '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");
|
||||||
|
|
||||||
|
require_once DOL_DOCUMENT_ROOT.'/custom/kundenkarte/class/equipmenttype.class.php';
|
||||||
|
require_once DOL_DOCUMENT_ROOT.'/custom/kundenkarte/class/equipment.class.php';
|
||||||
|
|
||||||
|
header('Content-Type: application/json; charset=UTF-8');
|
||||||
|
|
||||||
|
$typeId = GETPOSTINT('type_id');
|
||||||
|
$equipmentId = GETPOSTINT('equipment_id');
|
||||||
|
|
||||||
|
$response = array('success' => false, 'fields' => array());
|
||||||
|
|
||||||
|
if (!$user->hasRight('kundenkarte', 'read')) {
|
||||||
|
$response['error'] = 'Permission denied';
|
||||||
|
echo json_encode($response);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($typeId > 0) {
|
||||||
|
$type = new EquipmentType($db);
|
||||||
|
if ($type->fetch($typeId) > 0) {
|
||||||
|
$fields = $type->fetchFields(1);
|
||||||
|
|
||||||
|
// Get existing values if editing
|
||||||
|
$existingValues = array();
|
||||||
|
if ($equipmentId > 0) {
|
||||||
|
$equipment = new Equipment($db);
|
||||||
|
if ($equipment->fetch($equipmentId) > 0) {
|
||||||
|
$existingValues = $equipment->getFieldValues();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = array();
|
||||||
|
foreach ($fields as $field) {
|
||||||
|
$value = isset($existingValues[$field->field_code]) ? $existingValues[$field->field_code] : $field->field_default;
|
||||||
|
$result[] = array(
|
||||||
|
'code' => $field->field_code,
|
||||||
|
'label' => $field->field_label,
|
||||||
|
'type' => $field->field_type,
|
||||||
|
'options' => $field->field_options,
|
||||||
|
'required' => $field->required,
|
||||||
|
'show_on_block' => $field->show_on_block,
|
||||||
|
'value' => $value
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$response['success'] = true;
|
||||||
|
$response['fields'] = $result;
|
||||||
|
$response['type'] = array(
|
||||||
|
'id' => $type->id,
|
||||||
|
'label' => $type->label,
|
||||||
|
'label_short' => $type->label_short,
|
||||||
|
'width_te' => $type->width_te,
|
||||||
|
'color' => $type->color
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
echo json_encode($response);
|
||||||
|
$db->close();
|
||||||
148
ajax/equipment_type_icon.php
Executable file
148
ajax/equipment_type_icon.php
Executable file
|
|
@ -0,0 +1,148 @@
|
||||||
|
<?php
|
||||||
|
/* Copyright (C) 2026 Alles Watt lauft
|
||||||
|
*
|
||||||
|
* AJAX handler for equipment type icon upload
|
||||||
|
* Accepts SVG and PNG files for schematic symbols
|
||||||
|
*/
|
||||||
|
|
||||||
|
$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");
|
||||||
|
|
||||||
|
require_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php';
|
||||||
|
dol_include_once('/kundenkarte/class/equipmenttype.class.php');
|
||||||
|
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
// Security check
|
||||||
|
if (!$user->admin && !$user->hasRight('kundenkarte', 'admin')) {
|
||||||
|
echo json_encode(array('success' => false, 'error' => 'Access denied'));
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$action = GETPOST('action', 'aZ09');
|
||||||
|
$typeId = GETPOSTINT('type_id');
|
||||||
|
|
||||||
|
$response = array('success' => false);
|
||||||
|
|
||||||
|
// Directory for equipment type icons
|
||||||
|
$uploadDir = DOL_DATA_ROOT.'/kundenkarte/equipment_icons/';
|
||||||
|
|
||||||
|
// Create directory if not exists
|
||||||
|
if (!is_dir($uploadDir)) {
|
||||||
|
dol_mkdir($uploadDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
switch ($action) {
|
||||||
|
case 'upload':
|
||||||
|
if (empty($_FILES['icon_file']) || $_FILES['icon_file']['error'] !== UPLOAD_ERR_OK) {
|
||||||
|
$response['error'] = 'No file uploaded or upload error';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
$file = $_FILES['icon_file'];
|
||||||
|
$fileName = dol_sanitizeFileName($file['name']);
|
||||||
|
$fileExt = strtolower(pathinfo($fileName, PATHINFO_EXTENSION));
|
||||||
|
|
||||||
|
// Validate file type
|
||||||
|
$allowedExtensions = array('svg', 'png');
|
||||||
|
if (!in_array($fileExt, $allowedExtensions)) {
|
||||||
|
$response['error'] = 'Invalid file type. Only SVG and PNG are allowed.';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate MIME type
|
||||||
|
$mimeType = mime_content_type($file['tmp_name']);
|
||||||
|
$allowedMimes = array('image/svg+xml', 'image/png', 'text/plain', 'text/xml', 'application/xml');
|
||||||
|
if (!in_array($mimeType, $allowedMimes)) {
|
||||||
|
$response['error'] = 'Invalid MIME type: '.$mimeType;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For SVG files, do basic security check
|
||||||
|
if ($fileExt === 'svg') {
|
||||||
|
$content = file_get_contents($file['tmp_name']);
|
||||||
|
// Check for potentially dangerous content
|
||||||
|
$dangerous = array('<script', 'javascript:', 'onload=', 'onerror=', 'onclick=', 'onmouseover=');
|
||||||
|
foreach ($dangerous as $pattern) {
|
||||||
|
if (stripos($content, $pattern) !== false) {
|
||||||
|
$response['error'] = 'SVG contains potentially dangerous content';
|
||||||
|
break 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate unique filename
|
||||||
|
$newFileName = 'icon_'.$typeId.'_'.dol_now().'.'.$fileExt;
|
||||||
|
$destPath = $uploadDir.$newFileName;
|
||||||
|
|
||||||
|
// Move uploaded file
|
||||||
|
if (move_uploaded_file($file['tmp_name'], $destPath)) {
|
||||||
|
// Update database
|
||||||
|
$equipmentType = new EquipmentType($db);
|
||||||
|
if ($equipmentType->fetch($typeId) > 0) {
|
||||||
|
// Delete old icon file if exists
|
||||||
|
if ($equipmentType->icon_file && file_exists($uploadDir.$equipmentType->icon_file)) {
|
||||||
|
unlink($uploadDir.$equipmentType->icon_file);
|
||||||
|
}
|
||||||
|
|
||||||
|
$equipmentType->icon_file = $newFileName;
|
||||||
|
$result = $equipmentType->update($user);
|
||||||
|
|
||||||
|
if ($result > 0) {
|
||||||
|
$response['success'] = true;
|
||||||
|
$response['icon_file'] = $newFileName;
|
||||||
|
$response['icon_url'] = DOL_URL_ROOT.'/document.php?modulepart=kundenkarte&file=equipment_icons/'.$newFileName;
|
||||||
|
} else {
|
||||||
|
$response['error'] = 'Database update failed';
|
||||||
|
// Remove uploaded file on DB error
|
||||||
|
unlink($destPath);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$response['error'] = 'Equipment type not found';
|
||||||
|
unlink($destPath);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$response['error'] = 'Failed to move uploaded file';
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'delete':
|
||||||
|
$equipmentType = new EquipmentType($db);
|
||||||
|
if ($equipmentType->fetch($typeId) > 0) {
|
||||||
|
if ($equipmentType->icon_file && file_exists($uploadDir.$equipmentType->icon_file)) {
|
||||||
|
unlink($uploadDir.$equipmentType->icon_file);
|
||||||
|
}
|
||||||
|
$equipmentType->icon_file = '';
|
||||||
|
$result = $equipmentType->update($user);
|
||||||
|
if ($result > 0) {
|
||||||
|
$response['success'] = true;
|
||||||
|
} else {
|
||||||
|
$response['error'] = 'Database update failed';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$response['error'] = 'Equipment type not found';
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'get':
|
||||||
|
$equipmentType = new EquipmentType($db);
|
||||||
|
if ($equipmentType->fetch($typeId) > 0) {
|
||||||
|
$response['success'] = true;
|
||||||
|
$response['icon_file'] = $equipmentType->icon_file;
|
||||||
|
if ($equipmentType->icon_file) {
|
||||||
|
$response['icon_url'] = DOL_URL_ROOT.'/document.php?modulepart=kundenkarte&file=equipment_icons/'.$equipmentType->icon_file;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$response['error'] = 'Equipment type not found';
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
$response['error'] = 'Unknown action';
|
||||||
|
}
|
||||||
|
|
||||||
|
echo json_encode($response);
|
||||||
|
$db->close();
|
||||||
717
ajax/export_schematic_pdf.php
Executable file
717
ajax/export_schematic_pdf.php
Executable file
|
|
@ -0,0 +1,717 @@
|
||||||
|
<?php
|
||||||
|
/* Copyright (C) 2026 Alles Watt lauft
|
||||||
|
*
|
||||||
|
* PDF Export for Schematic Editor (Leitungslaufplan)
|
||||||
|
* Following DIN EN 61082 (Document structure) and DIN EN 81346 (Reference designation)
|
||||||
|
*/
|
||||||
|
|
||||||
|
$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");
|
||||||
|
|
||||||
|
require_once DOL_DOCUMENT_ROOT.'/core/lib/pdf.lib.php';
|
||||||
|
require_once DOL_DOCUMENT_ROOT.'/societe/class/societe.class.php';
|
||||||
|
dol_include_once('/kundenkarte/class/anlage.class.php');
|
||||||
|
dol_include_once('/kundenkarte/class/equipment.class.php');
|
||||||
|
dol_include_once('/kundenkarte/class/equipmentcarrier.class.php');
|
||||||
|
dol_include_once('/kundenkarte/class/equipmentconnection.class.php');
|
||||||
|
|
||||||
|
$langs->loadLangs(array('companies', 'kundenkarte@kundenkarte'));
|
||||||
|
|
||||||
|
// Get parameters
|
||||||
|
$anlageId = GETPOSTINT('anlage_id');
|
||||||
|
$svgContent = GETPOST('svg_content', 'restricthtml');
|
||||||
|
$format = GETPOST('format', 'alpha') ?: 'A4';
|
||||||
|
$orientation = GETPOST('orientation', 'alpha') ?: 'L'; // L=Landscape, P=Portrait
|
||||||
|
|
||||||
|
// Security check
|
||||||
|
if (!$user->hasRight('kundenkarte', 'read')) {
|
||||||
|
accessforbidden();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load Anlage data
|
||||||
|
$anlage = new Anlage($db);
|
||||||
|
if ($anlage->fetch($anlageId) <= 0) {
|
||||||
|
die('Anlage not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load company
|
||||||
|
$societe = new Societe($db);
|
||||||
|
$societe->fetch($anlage->fk_soc);
|
||||||
|
|
||||||
|
// Load carriers for this anlage
|
||||||
|
$carrier = new EquipmentCarrier($db);
|
||||||
|
$carriers = $carrier->fetchByAnlage($anlageId);
|
||||||
|
|
||||||
|
// Load equipment
|
||||||
|
$equipment = new Equipment($db);
|
||||||
|
$equipmentList = array();
|
||||||
|
foreach ($carriers as $c) {
|
||||||
|
$eqList = $equipment->fetchByCarrier($c->id);
|
||||||
|
$equipmentList = array_merge($equipmentList, $eqList);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load connections
|
||||||
|
$connection = new EquipmentConnection($db);
|
||||||
|
$connections = array();
|
||||||
|
foreach ($carriers as $c) {
|
||||||
|
$connList = $connection->fetchByCarrier($c->id);
|
||||||
|
$connections = array_merge($connections, $connList);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create PDF - Landscape A3 or A4 for schematic
|
||||||
|
$pdf = pdf_getInstance();
|
||||||
|
$pdf->SetCreator('Dolibarr - Kundenkarte Schaltplan');
|
||||||
|
$pdf->SetAuthor($user->getFullName($langs));
|
||||||
|
$pdf->SetTitle('Leitungslaufplan - '.$anlage->label);
|
||||||
|
|
||||||
|
// Page format
|
||||||
|
if ($format == 'A3') {
|
||||||
|
$pageWidth = 420;
|
||||||
|
$pageHeight = 297;
|
||||||
|
} else {
|
||||||
|
$pageWidth = 297;
|
||||||
|
$pageHeight = 210;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($orientation == 'P') {
|
||||||
|
$tmp = $pageWidth;
|
||||||
|
$pageWidth = $pageHeight;
|
||||||
|
$pageHeight = $tmp;
|
||||||
|
}
|
||||||
|
|
||||||
|
$pdf->SetMargins(10, 10, 10);
|
||||||
|
$pdf->SetAutoPageBreak(false);
|
||||||
|
$pdf->AddPage($orientation, array($pageWidth, $pageHeight));
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// DIN EN 61082 / ISO 7200 Title Block (Schriftfeld)
|
||||||
|
// Position: Bottom right corner
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
$titleBlockWidth = 180;
|
||||||
|
$titleBlockHeight = 56;
|
||||||
|
$titleBlockX = $pageWidth - $titleBlockWidth - 10;
|
||||||
|
$titleBlockY = $pageHeight - $titleBlockHeight - 10;
|
||||||
|
|
||||||
|
// Draw title block frame
|
||||||
|
$pdf->SetDrawColor(0, 0, 0);
|
||||||
|
$pdf->SetLineWidth(0.5);
|
||||||
|
$pdf->Rect($titleBlockX, $titleBlockY, $titleBlockWidth, $titleBlockHeight);
|
||||||
|
|
||||||
|
// Title block grid - following DIN structure
|
||||||
|
// Row heights from bottom: 8, 8, 8, 8, 8, 8, 8 = 56mm total
|
||||||
|
$rowHeight = 8;
|
||||||
|
$rows = 7;
|
||||||
|
|
||||||
|
// Column widths: 30 | 50 | 50 | 50 = 180mm
|
||||||
|
$col1 = 30; // Labels
|
||||||
|
$col2 = 50; // Company info
|
||||||
|
$col3 = 50; // Document info
|
||||||
|
$col4 = 50; // Revision info
|
||||||
|
|
||||||
|
// Draw horizontal lines
|
||||||
|
for ($i = 1; $i < $rows; $i++) {
|
||||||
|
$y = $titleBlockY + ($i * $rowHeight);
|
||||||
|
$pdf->Line($titleBlockX, $y, $titleBlockX + $titleBlockWidth, $y);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw vertical lines
|
||||||
|
$pdf->Line($titleBlockX + $col1, $titleBlockY, $titleBlockX + $col1, $titleBlockY + $titleBlockHeight);
|
||||||
|
$pdf->Line($titleBlockX + $col1 + $col2, $titleBlockY, $titleBlockX + $col1 + $col2, $titleBlockY + $titleBlockHeight);
|
||||||
|
$pdf->Line($titleBlockX + $col1 + $col2 + $col3, $titleBlockY, $titleBlockX + $col1 + $col2 + $col3, $titleBlockY + $titleBlockHeight);
|
||||||
|
|
||||||
|
// Fill in title block content
|
||||||
|
$pdf->SetFont('dejavusans', '', 6);
|
||||||
|
$pdf->SetTextColor(0, 0, 0);
|
||||||
|
|
||||||
|
// Row 1 (from top): Document title spanning full width
|
||||||
|
$pdf->SetFont('dejavusans', 'B', 12);
|
||||||
|
$pdf->SetXY($titleBlockX + 2, $titleBlockY + 1);
|
||||||
|
$pdf->Cell($titleBlockWidth - 4, $rowHeight - 2, 'LEITUNGSLAUFPLAN', 0, 0, 'C');
|
||||||
|
|
||||||
|
// Row 2: Installation name
|
||||||
|
$pdf->SetFont('dejavusans', 'B', 10);
|
||||||
|
$pdf->SetXY($titleBlockX + 2, $titleBlockY + $rowHeight + 1);
|
||||||
|
$pdf->Cell($titleBlockWidth - 4, $rowHeight - 2, $anlage->label, 0, 0, 'C');
|
||||||
|
|
||||||
|
// Row 3: Labels
|
||||||
|
$pdf->SetFont('dejavusans', '', 6);
|
||||||
|
$y = $titleBlockY + (2 * $rowHeight);
|
||||||
|
$pdf->SetXY($titleBlockX + 1, $y + 1);
|
||||||
|
$pdf->Cell($col1 - 2, 3, 'Erstellt', 0, 0, 'L');
|
||||||
|
$pdf->SetXY($titleBlockX + $col1 + 1, $y + 1);
|
||||||
|
$pdf->Cell($col2 - 2, 3, 'Kunde', 0, 0, 'L');
|
||||||
|
$pdf->SetXY($titleBlockX + $col1 + $col2 + 1, $y + 1);
|
||||||
|
$pdf->Cell($col3 - 2, 3, 'Projekt-Nr.', 0, 0, 'L');
|
||||||
|
$pdf->SetXY($titleBlockX + $col1 + $col2 + $col3 + 1, $y + 1);
|
||||||
|
$pdf->Cell($col4 - 2, 3, 'Blatt', 0, 0, 'L');
|
||||||
|
|
||||||
|
// Row 3: Values
|
||||||
|
$pdf->SetFont('dejavusans', '', 8);
|
||||||
|
$pdf->SetXY($titleBlockX + 1, $y + 4);
|
||||||
|
$pdf->Cell($col1 - 2, 4, dol_print_date(dol_now(), 'day'), 0, 0, 'L');
|
||||||
|
$pdf->SetXY($titleBlockX + $col1 + 1, $y + 4);
|
||||||
|
$pdf->Cell($col2 - 2, 4, dol_trunc($societe->name, 25), 0, 0, 'L');
|
||||||
|
$pdf->SetXY($titleBlockX + $col1 + $col2 + 1, $y + 4);
|
||||||
|
$pdf->Cell($col3 - 2, 4, $anlage->ref ?: '-', 0, 0, 'L');
|
||||||
|
$pdf->SetXY($titleBlockX + $col1 + $col2 + $col3 + 1, $y + 4);
|
||||||
|
$pdf->Cell($col4 - 2, 4, '1 / 1', 0, 0, 'L');
|
||||||
|
|
||||||
|
// Row 4: More labels
|
||||||
|
$y = $titleBlockY + (3 * $rowHeight);
|
||||||
|
$pdf->SetFont('dejavusans', '', 6);
|
||||||
|
$pdf->SetXY($titleBlockX + 1, $y + 1);
|
||||||
|
$pdf->Cell($col1 - 2, 3, 'Bearbeiter', 0, 0, 'L');
|
||||||
|
$pdf->SetXY($titleBlockX + $col1 + 1, $y + 1);
|
||||||
|
$pdf->Cell($col2 - 2, 3, 'Adresse', 0, 0, 'L');
|
||||||
|
$pdf->SetXY($titleBlockX + $col1 + $col2 + 1, $y + 1);
|
||||||
|
$pdf->Cell($col3 - 2, 3, 'Anlage', 0, 0, 'L');
|
||||||
|
$pdf->SetXY($titleBlockX + $col1 + $col2 + $col3 + 1, $y + 1);
|
||||||
|
$pdf->Cell($col4 - 2, 3, 'Revision', 0, 0, 'L');
|
||||||
|
|
||||||
|
// Row 4: Values
|
||||||
|
$pdf->SetFont('dejavusans', '', 8);
|
||||||
|
$pdf->SetXY($titleBlockX + 1, $y + 4);
|
||||||
|
$pdf->Cell($col1 - 2, 4, dol_trunc($user->getFullName($langs), 15), 0, 0, 'L');
|
||||||
|
$pdf->SetXY($titleBlockX + $col1 + 1, $y + 4);
|
||||||
|
$address = trim($societe->address.' '.$societe->zip.' '.$societe->town);
|
||||||
|
$pdf->Cell($col2 - 2, 4, dol_trunc($address, 25), 0, 0, 'L');
|
||||||
|
$pdf->SetXY($titleBlockX + $col1 + $col2 + 1, $y + 4);
|
||||||
|
$pdf->Cell($col3 - 2, 4, $anlage->type_label ?: '-', 0, 0, 'L');
|
||||||
|
$pdf->SetXY($titleBlockX + $col1 + $col2 + $col3 + 1, $y + 4);
|
||||||
|
$pdf->Cell($col4 - 2, 4, 'A', 0, 0, 'L');
|
||||||
|
|
||||||
|
// Row 5: Equipment count
|
||||||
|
$y = $titleBlockY + (4 * $rowHeight);
|
||||||
|
$pdf->SetFont('dejavusans', '', 6);
|
||||||
|
$pdf->SetXY($titleBlockX + 1, $y + 1);
|
||||||
|
$pdf->Cell($col1 - 2, 3, 'Komponenten', 0, 0, 'L');
|
||||||
|
$pdf->SetXY($titleBlockX + $col1 + 1, $y + 1);
|
||||||
|
$pdf->Cell($col2 - 2, 3, 'Verbindungen', 0, 0, 'L');
|
||||||
|
$pdf->SetXY($titleBlockX + $col1 + $col2 + 1, $y + 1);
|
||||||
|
$pdf->Cell($col3 - 2, 3, 'Hutschienen', 0, 0, 'L');
|
||||||
|
$pdf->SetXY($titleBlockX + $col1 + $col2 + $col3 + 1, $y + 1);
|
||||||
|
$pdf->Cell($col4 - 2, 3, 'Format', 0, 0, 'L');
|
||||||
|
|
||||||
|
$pdf->SetFont('dejavusans', '', 8);
|
||||||
|
$pdf->SetXY($titleBlockX + 1, $y + 4);
|
||||||
|
$pdf->Cell($col1 - 2, 4, count($equipmentList), 0, 0, 'L');
|
||||||
|
$pdf->SetXY($titleBlockX + $col1 + 1, $y + 4);
|
||||||
|
$pdf->Cell($col2 - 2, 4, count($connections), 0, 0, 'L');
|
||||||
|
$pdf->SetXY($titleBlockX + $col1 + $col2 + 1, $y + 4);
|
||||||
|
$pdf->Cell($col3 - 2, 4, count($carriers), 0, 0, 'L');
|
||||||
|
$pdf->SetXY($titleBlockX + $col1 + $col2 + $col3 + 1, $y + 4);
|
||||||
|
$pdf->Cell($col4 - 2, 4, $format.' '.$orientation, 0, 0, 'L');
|
||||||
|
|
||||||
|
// Row 6: Norm reference
|
||||||
|
$y = $titleBlockY + (5 * $rowHeight);
|
||||||
|
$pdf->SetFont('dejavusans', '', 6);
|
||||||
|
$pdf->SetXY($titleBlockX + 1, $y + 2);
|
||||||
|
$pdf->Cell($titleBlockWidth - 2, 4, 'Erstellt nach DIN EN 61082 / DIN EN 81346', 0, 0, 'C');
|
||||||
|
|
||||||
|
// Row 7: Company info
|
||||||
|
$y = $titleBlockY + (6 * $rowHeight);
|
||||||
|
$pdf->SetFont('dejavusans', 'B', 7);
|
||||||
|
$pdf->SetXY($titleBlockX + 1, $y + 2);
|
||||||
|
$pdf->Cell($titleBlockWidth - 2, 4, $mysoc->name, 0, 0, 'C');
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// Draw the Schematic Content Area
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
$schematicX = 10;
|
||||||
|
$schematicY = 10;
|
||||||
|
$schematicWidth = $pageWidth - 20;
|
||||||
|
$schematicHeight = $titleBlockY - 15;
|
||||||
|
|
||||||
|
// Draw frame around schematic area
|
||||||
|
$pdf->SetDrawColor(0, 0, 0);
|
||||||
|
$pdf->SetLineWidth(0.3);
|
||||||
|
$pdf->Rect($schematicX, $schematicY, $schematicWidth, $schematicHeight);
|
||||||
|
|
||||||
|
// If SVG content provided, embed it
|
||||||
|
if (!empty($svgContent)) {
|
||||||
|
// Clean SVG for TCPDF
|
||||||
|
$svgContent = preg_replace('/<\?xml[^>]*\?>/', '', $svgContent);
|
||||||
|
$svgContent = preg_replace('/<!DOCTYPE[^>]*>/', '', $svgContent);
|
||||||
|
|
||||||
|
// Try to embed SVG
|
||||||
|
try {
|
||||||
|
// Scale SVG to fit in schematic area
|
||||||
|
$pdf->ImageSVG('@'.$svgContent, $schematicX + 2, $schematicY + 2, $schematicWidth - 4, $schematicHeight - 4, '', '', '', 0, false);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
// SVG embedding failed - draw placeholder
|
||||||
|
$pdf->SetFont('dejavusans', 'I', 10);
|
||||||
|
$pdf->SetXY($schematicX + 10, $schematicY + 10);
|
||||||
|
$pdf->Cell(0, 10, 'SVG konnte nicht eingebettet werden: '.$e->getMessage(), 0, 1);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Draw schematic manually if no SVG provided
|
||||||
|
drawSchematicContent($pdf, $carriers, $equipmentList, $connections, $schematicX, $schematicY, $schematicWidth, $schematicHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// Add Wiring List on second page (Verdrahtungsliste)
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
if (count($connections) > 0) {
|
||||||
|
$pdf->AddPage($orientation, array($pageWidth, $pageHeight));
|
||||||
|
|
||||||
|
// Title
|
||||||
|
$pdf->SetFont('dejavusans', 'B', 14);
|
||||||
|
$pdf->SetXY(10, 10);
|
||||||
|
$pdf->Cell(0, 8, 'VERDRAHTUNGSLISTE / KLEMMENPLAN', 0, 1, 'L');
|
||||||
|
|
||||||
|
$pdf->SetFont('dejavusans', '', 9);
|
||||||
|
$pdf->SetXY(10, 18);
|
||||||
|
$pdf->Cell(0, 5, $anlage->label.' - '.$societe->name, 0, 1, 'L');
|
||||||
|
|
||||||
|
// Table header
|
||||||
|
$pdf->SetY(28);
|
||||||
|
$pdf->SetFont('dejavusans', 'B', 8);
|
||||||
|
$pdf->SetFillColor(220, 220, 220);
|
||||||
|
|
||||||
|
$colWidths = array(15, 35, 25, 35, 25, 25, 30, 30);
|
||||||
|
$headers = array('Nr.', 'Von (Quelle)', 'Klemme', 'Nach (Ziel)', 'Klemme', 'Typ', 'Leitung', 'Bemerkung');
|
||||||
|
|
||||||
|
$x = 10;
|
||||||
|
for ($i = 0; $i < count($headers); $i++) {
|
||||||
|
$pdf->SetXY($x, 28);
|
||||||
|
$pdf->Cell($colWidths[$i], 6, $headers[$i], 1, 0, 'C', true);
|
||||||
|
$x += $colWidths[$i];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Table content
|
||||||
|
$pdf->SetFont('dejavusans', '', 7);
|
||||||
|
$y = 34;
|
||||||
|
$lineNum = 1;
|
||||||
|
|
||||||
|
// Build equipment lookup
|
||||||
|
$eqLookup = array();
|
||||||
|
foreach ($equipmentList as $eq) {
|
||||||
|
$eqLookup[$eq->id] = $eq;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($connections as $conn) {
|
||||||
|
// Skip rails/busbars in wiring list (they're separate)
|
||||||
|
if ($conn->is_rail) continue;
|
||||||
|
|
||||||
|
$sourceName = '-';
|
||||||
|
$sourceTerminal = $conn->source_terminal ?: '-';
|
||||||
|
$targetName = '-';
|
||||||
|
$targetTerminal = $conn->target_terminal ?: '-';
|
||||||
|
|
||||||
|
if ($conn->fk_source && isset($eqLookup[$conn->fk_source])) {
|
||||||
|
$sourceName = $eqLookup[$conn->fk_source]->label ?: $eqLookup[$conn->fk_source]->type_label_short;
|
||||||
|
}
|
||||||
|
if ($conn->fk_target && isset($eqLookup[$conn->fk_target])) {
|
||||||
|
$targetName = $eqLookup[$conn->fk_target]->label ?: $eqLookup[$conn->fk_target]->type_label_short;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connection type / medium
|
||||||
|
$connType = $conn->connection_type ?: '-';
|
||||||
|
$medium = trim($conn->medium_type.' '.$conn->medium_spec);
|
||||||
|
if (empty($medium)) $medium = '-';
|
||||||
|
|
||||||
|
$remark = $conn->output_label ?: '';
|
||||||
|
|
||||||
|
if ($y > $pageHeight - 25) {
|
||||||
|
// New page
|
||||||
|
$pdf->AddPage($orientation, array($pageWidth, $pageHeight));
|
||||||
|
$y = 10;
|
||||||
|
|
||||||
|
// Repeat header
|
||||||
|
$pdf->SetFont('dejavusans', 'B', 8);
|
||||||
|
$x = 10;
|
||||||
|
for ($i = 0; $i < count($headers); $i++) {
|
||||||
|
$pdf->SetXY($x, $y);
|
||||||
|
$pdf->Cell($colWidths[$i], 6, $headers[$i], 1, 0, 'C', true);
|
||||||
|
$x += $colWidths[$i];
|
||||||
|
}
|
||||||
|
$y += 6;
|
||||||
|
$pdf->SetFont('dejavusans', '', 7);
|
||||||
|
}
|
||||||
|
|
||||||
|
$x = 10;
|
||||||
|
$pdf->SetXY($x, $y);
|
||||||
|
$pdf->Cell($colWidths[0], 5, $lineNum, 1, 0, 'C');
|
||||||
|
$x += $colWidths[0];
|
||||||
|
|
||||||
|
$pdf->SetXY($x, $y);
|
||||||
|
$pdf->Cell($colWidths[1], 5, dol_trunc($sourceName, 18), 1, 0, 'L');
|
||||||
|
$x += $colWidths[1];
|
||||||
|
|
||||||
|
$pdf->SetXY($x, $y);
|
||||||
|
$pdf->Cell($colWidths[2], 5, $sourceTerminal, 1, 0, 'C');
|
||||||
|
$x += $colWidths[2];
|
||||||
|
|
||||||
|
$pdf->SetXY($x, $y);
|
||||||
|
$pdf->Cell($colWidths[3], 5, dol_trunc($targetName, 18), 1, 0, 'L');
|
||||||
|
$x += $colWidths[3];
|
||||||
|
|
||||||
|
$pdf->SetXY($x, $y);
|
||||||
|
$pdf->Cell($colWidths[4], 5, $targetTerminal, 1, 0, 'C');
|
||||||
|
$x += $colWidths[4];
|
||||||
|
|
||||||
|
$pdf->SetXY($x, $y);
|
||||||
|
$pdf->Cell($colWidths[5], 5, $connType, 1, 0, 'C');
|
||||||
|
$x += $colWidths[5];
|
||||||
|
|
||||||
|
$pdf->SetXY($x, $y);
|
||||||
|
$pdf->Cell($colWidths[6], 5, dol_trunc($medium, 15), 1, 0, 'L');
|
||||||
|
$x += $colWidths[6];
|
||||||
|
|
||||||
|
$pdf->SetXY($x, $y);
|
||||||
|
$pdf->Cell($colWidths[7], 5, dol_trunc($remark, 15), 1, 0, 'L');
|
||||||
|
|
||||||
|
$y += 5;
|
||||||
|
$lineNum++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add busbars section if any
|
||||||
|
$busbars = array_filter($connections, function($c) { return $c->is_rail; });
|
||||||
|
if (count($busbars) > 0) {
|
||||||
|
$y += 10;
|
||||||
|
if ($y > $pageHeight - 40) {
|
||||||
|
$pdf->AddPage($orientation, array($pageWidth, $pageHeight));
|
||||||
|
$y = 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
$pdf->SetFont('dejavusans', 'B', 10);
|
||||||
|
$pdf->SetXY(10, $y);
|
||||||
|
$pdf->Cell(0, 6, 'SAMMELSCHIENEN / PHASENSCHIENEN', 0, 1, 'L');
|
||||||
|
$y += 8;
|
||||||
|
|
||||||
|
$pdf->SetFont('dejavusans', 'B', 8);
|
||||||
|
$bbHeaders = array('Nr.', 'Bezeichnung', 'Typ', 'Von TE', 'Bis TE', 'Phasen', 'Ausnahmen');
|
||||||
|
$bbWidths = array(15, 50, 30, 20, 20, 30, 50);
|
||||||
|
|
||||||
|
$x = 10;
|
||||||
|
for ($i = 0; $i < count($bbHeaders); $i++) {
|
||||||
|
$pdf->SetXY($x, $y);
|
||||||
|
$pdf->Cell($bbWidths[$i], 6, $bbHeaders[$i], 1, 0, 'C', true);
|
||||||
|
$x += $bbWidths[$i];
|
||||||
|
}
|
||||||
|
$y += 6;
|
||||||
|
|
||||||
|
$pdf->SetFont('dejavusans', '', 7);
|
||||||
|
$bbNum = 1;
|
||||||
|
foreach ($busbars as $bb) {
|
||||||
|
$x = 10;
|
||||||
|
$pdf->SetXY($x, $y);
|
||||||
|
$pdf->Cell($bbWidths[0], 5, $bbNum, 1, 0, 'C');
|
||||||
|
$x += $bbWidths[0];
|
||||||
|
|
||||||
|
$pdf->SetXY($x, $y);
|
||||||
|
$pdf->Cell($bbWidths[1], 5, $bb->output_label ?: 'Sammelschiene '.$bbNum, 1, 0, 'L');
|
||||||
|
$x += $bbWidths[1];
|
||||||
|
|
||||||
|
$pdf->SetXY($x, $y);
|
||||||
|
$pdf->Cell($bbWidths[2], 5, $bb->connection_type ?: '-', 1, 0, 'C');
|
||||||
|
$x += $bbWidths[2];
|
||||||
|
|
||||||
|
$pdf->SetXY($x, $y);
|
||||||
|
$pdf->Cell($bbWidths[3], 5, $bb->rail_start_te ?: '-', 1, 0, 'C');
|
||||||
|
$x += $bbWidths[3];
|
||||||
|
|
||||||
|
$pdf->SetXY($x, $y);
|
||||||
|
$pdf->Cell($bbWidths[4], 5, $bb->rail_end_te ?: '-', 1, 0, 'C');
|
||||||
|
$x += $bbWidths[4];
|
||||||
|
|
||||||
|
$pdf->SetXY($x, $y);
|
||||||
|
$pdf->Cell($bbWidths[5], 5, $bb->rail_phases ?: '-', 1, 0, 'C');
|
||||||
|
$x += $bbWidths[5];
|
||||||
|
|
||||||
|
$pdf->SetXY($x, $y);
|
||||||
|
$pdf->Cell($bbWidths[6], 5, $bb->excluded_te ?: '-', 1, 0, 'L');
|
||||||
|
|
||||||
|
$y += 5;
|
||||||
|
$bbNum++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Output PDF
|
||||||
|
$filename = 'Leitungslaufplan_'.dol_sanitizeFileName($anlage->label).'_'.date('Y-m-d').'.pdf';
|
||||||
|
$pdf->Output($filename, 'D');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draw schematic content directly in PDF
|
||||||
|
* Shows only the actual equipment and connections from the database (what was drawn in the editor)
|
||||||
|
*/
|
||||||
|
function drawSchematicContent(&$pdf, $carriers, $equipment, $connections, $startX, $startY, $width, $height) {
|
||||||
|
// Phase colors (DIN VDE compliant)
|
||||||
|
$phaseColors = array(
|
||||||
|
'L1' => array(139, 69, 19), // Brown
|
||||||
|
'L2' => array(0, 0, 0), // Black
|
||||||
|
'L3' => array(128, 128, 128), // Gray
|
||||||
|
'N' => array(0, 102, 204), // Blue
|
||||||
|
'PE' => array(0, 128, 0) // Green (simplified from green-yellow)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Layout constants
|
||||||
|
$teWidth = 10; // mm per TE
|
||||||
|
$equipmentStartY = $startY + 20;
|
||||||
|
$blockWidth = 8;
|
||||||
|
$blockHeight = 20;
|
||||||
|
|
||||||
|
// Calculate total width needed
|
||||||
|
$maxTE = 0;
|
||||||
|
foreach ($carriers as $carrier) {
|
||||||
|
$maxTE = max($maxTE, $carrier->total_te ?: 12);
|
||||||
|
}
|
||||||
|
$contentWidth = min($width - 40, $maxTE * $teWidth + 40);
|
||||||
|
$contentStartX = $startX + 20;
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// Draw equipment and connections
|
||||||
|
// ========================================
|
||||||
|
$carrierIndex = 0;
|
||||||
|
foreach ($carriers as $carrier) {
|
||||||
|
$carrierY = $equipmentStartY + $carrierIndex * 50;
|
||||||
|
$carrierX = $contentStartX;
|
||||||
|
$totalTE = $carrier->total_te ?: 12;
|
||||||
|
|
||||||
|
// Carrier label
|
||||||
|
$pdf->SetFont('dejavusans', '', 6);
|
||||||
|
$pdf->SetTextColor(100, 100, 100);
|
||||||
|
$pdf->SetXY($carrierX - 15, $carrierY + $blockHeight / 2 - 2);
|
||||||
|
$pdf->Cell(12, 4, $carrier->label ?: 'H'.($carrierIndex+1), 0, 0, 'R');
|
||||||
|
|
||||||
|
// Get equipment on this carrier
|
||||||
|
$carrierEquipment = array_filter($equipment, function($eq) use ($carrier) {
|
||||||
|
return $eq->fk_carrier == $carrier->id;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get busbars for this carrier
|
||||||
|
$carrierBusbars = array_filter($connections, function($c) use ($carrier) {
|
||||||
|
return $c->is_rail && $c->fk_carrier == $carrier->id;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Sort equipment by position
|
||||||
|
usort($carrierEquipment, function($a, $b) {
|
||||||
|
return ($a->position_te ?: 1) - ($b->position_te ?: 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Draw each equipment
|
||||||
|
foreach ($carrierEquipment as $eq) {
|
||||||
|
$eqPosTE = $eq->position_te ?: 1;
|
||||||
|
$eqWidthTE = $eq->width_te ?: 1;
|
||||||
|
$eqX = $carrierX + ($eqPosTE - 1) * $teWidth;
|
||||||
|
$eqWidth = $eqWidthTE * $teWidth - 2;
|
||||||
|
|
||||||
|
// Equipment block
|
||||||
|
$color = $eq->type_color ?: '#3498db';
|
||||||
|
list($r, $g, $b) = sscanf($color, "#%02x%02x%02x");
|
||||||
|
$pdf->SetFillColor($r ?: 52, $g ?: 152, $b ?: 219);
|
||||||
|
$pdf->Rect($eqX, $carrierY, $eqWidth, $blockHeight, 'F');
|
||||||
|
|
||||||
|
// Equipment label
|
||||||
|
$pdf->SetFont('dejavusans', 'B', 5);
|
||||||
|
$pdf->SetTextColor(255, 255, 255);
|
||||||
|
$label = $eq->type_label_short ?: $eq->label;
|
||||||
|
$pdf->SetXY($eqX, $carrierY + 3);
|
||||||
|
$pdf->Cell($eqWidth, 4, dol_trunc($label, 8), 0, 0, 'C');
|
||||||
|
|
||||||
|
// Second line label
|
||||||
|
if ($eq->label && $eq->type_label_short) {
|
||||||
|
$pdf->SetFont('dejavusans', '', 4);
|
||||||
|
$pdf->SetXY($eqX, $carrierY + 8);
|
||||||
|
$pdf->Cell($eqWidth, 3, dol_trunc($eq->label, 10), 0, 0, 'C');
|
||||||
|
}
|
||||||
|
|
||||||
|
$pdf->SetTextColor(0, 0, 0);
|
||||||
|
|
||||||
|
// Consumer label below equipment
|
||||||
|
$pdf->SetFont('dejavusans', '', 5);
|
||||||
|
$pdf->SetTextColor(80, 80, 80);
|
||||||
|
$pdf->SetXY($eqX - 2, $carrierY + $blockHeight + 1);
|
||||||
|
$consumerLabel = $eq->label ?: '';
|
||||||
|
$pdf->Cell($eqWidth + 4, 4, dol_trunc($consumerLabel, 12), 0, 0, 'C');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw busbars (Phasenschienen) for this carrier
|
||||||
|
foreach ($carrierBusbars as $busbar) {
|
||||||
|
$busbarStartTE = $busbar->rail_start_te ?: 1;
|
||||||
|
$busbarEndTE = $busbar->rail_end_te ?: $busbarStartTE;
|
||||||
|
$busbarX = $carrierX + ($busbarStartTE - 1) * $teWidth;
|
||||||
|
$busbarWidth = ($busbarEndTE - $busbarStartTE + 1) * $teWidth;
|
||||||
|
$busbarY = $carrierY - 5;
|
||||||
|
|
||||||
|
// Busbar color
|
||||||
|
$phase = $busbar->rail_phases ?: $busbar->connection_type ?: 'L1';
|
||||||
|
$color = $phaseColors[$phase] ?? array(200, 100, 50);
|
||||||
|
$pdf->SetFillColor($color[0], $color[1], $color[2]);
|
||||||
|
$pdf->Rect($busbarX, $busbarY, $busbarWidth, 3, 'F');
|
||||||
|
|
||||||
|
// Draw vertical taps from busbar to equipment
|
||||||
|
$pdf->SetDrawColor($color[0], $color[1], $color[2]);
|
||||||
|
for ($te = $busbarStartTE; $te <= $busbarEndTE; $te++) {
|
||||||
|
// Check if there's equipment at this TE
|
||||||
|
$hasEquipment = false;
|
||||||
|
foreach ($carrierEquipment as $eq) {
|
||||||
|
$eqStart = $eq->position_te ?: 1;
|
||||||
|
$eqEnd = $eqStart + ($eq->width_te ?: 1) - 1;
|
||||||
|
if ($te >= $eqStart && $te <= $eqEnd) {
|
||||||
|
$hasEquipment = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($hasEquipment) {
|
||||||
|
$tapX = $carrierX + ($te - 1) * $teWidth + $teWidth / 2;
|
||||||
|
$pdf->Line($tapX, $busbarY + 3, $tapX, $carrierY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Busbar label
|
||||||
|
$pdf->SetFont('dejavusans', 'B', 5);
|
||||||
|
$pdf->SetTextColor(255, 255, 255);
|
||||||
|
$pdf->SetXY($busbarX, $busbarY - 0.5);
|
||||||
|
$pdf->Cell($busbarWidth, 4, $phase, 0, 0, 'C');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// Draw Inputs (Anschlusspunkte) and Outputs (Abgänge)
|
||||||
|
// ========================================
|
||||||
|
|
||||||
|
// Get non-rail connections for this carrier's equipment
|
||||||
|
$carrierEqIds = array_map(function($eq) { return $eq->id; }, $carrierEquipment);
|
||||||
|
|
||||||
|
foreach ($connections as $conn) {
|
||||||
|
if ($conn->is_rail) continue;
|
||||||
|
|
||||||
|
// ANSCHLUSSPUNKT (Input) - fk_source is NULL, fk_target exists
|
||||||
|
if (empty($conn->fk_source) && !empty($conn->fk_target)) {
|
||||||
|
// Find target equipment
|
||||||
|
$targetEq = null;
|
||||||
|
foreach ($carrierEquipment as $eq) {
|
||||||
|
if ($eq->id == $conn->fk_target) {
|
||||||
|
$targetEq = $eq;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!$targetEq) continue;
|
||||||
|
|
||||||
|
$eqPosTE = $targetEq->position_te ?: 1;
|
||||||
|
$eqWidthTE = $targetEq->width_te ?: 1;
|
||||||
|
$eqCenterX = $carrierX + ($eqPosTE - 1) * $teWidth + ($eqWidthTE * $teWidth) / 2;
|
||||||
|
$lineStartY = $carrierY - 18;
|
||||||
|
$lineEndY = $carrierY;
|
||||||
|
|
||||||
|
// Phase color
|
||||||
|
$phase = $conn->connection_type ?: 'L1';
|
||||||
|
$color = $phaseColors[$phase] ?? array(100, 100, 100);
|
||||||
|
$pdf->SetDrawColor($color[0], $color[1], $color[2]);
|
||||||
|
$pdf->SetFillColor($color[0], $color[1], $color[2]);
|
||||||
|
|
||||||
|
// Vertical line from top
|
||||||
|
$pdf->Line($eqCenterX, $lineStartY, $eqCenterX, $lineEndY);
|
||||||
|
|
||||||
|
// Circle at top (source indicator)
|
||||||
|
$pdf->Circle($eqCenterX, $lineStartY, 1.5, 0, 360, 'F');
|
||||||
|
|
||||||
|
// Phase label
|
||||||
|
$pdf->SetFont('dejavusans', 'B', 7);
|
||||||
|
$pdf->SetTextColor($color[0], $color[1], $color[2]);
|
||||||
|
$pdf->SetXY($eqCenterX - 5, $lineStartY - 5);
|
||||||
|
$pdf->Cell(10, 4, $phase, 0, 0, 'C');
|
||||||
|
|
||||||
|
// Optional label
|
||||||
|
if (!empty($conn->output_label)) {
|
||||||
|
$pdf->SetFont('dejavusans', '', 5);
|
||||||
|
$pdf->SetTextColor(100, 100, 100);
|
||||||
|
$pdf->SetXY($eqCenterX + 3, $lineStartY - 4);
|
||||||
|
$pdf->Cell(20, 3, dol_trunc($conn->output_label, 12), 0, 0, 'L');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ABGANG (Output) - fk_source exists, fk_target is NULL
|
||||||
|
if (!empty($conn->fk_source) && empty($conn->fk_target)) {
|
||||||
|
// Find source equipment
|
||||||
|
$sourceEq = null;
|
||||||
|
foreach ($carrierEquipment as $eq) {
|
||||||
|
if ($eq->id == $conn->fk_source) {
|
||||||
|
$sourceEq = $eq;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!$sourceEq) continue;
|
||||||
|
|
||||||
|
$eqPosTE = $sourceEq->position_te ?: 1;
|
||||||
|
$eqWidthTE = $sourceEq->width_te ?: 1;
|
||||||
|
$eqCenterX = $carrierX + ($eqPosTE - 1) * $teWidth + ($eqWidthTE * $teWidth) / 2;
|
||||||
|
$lineStartY = $carrierY + $blockHeight;
|
||||||
|
$lineLength = 18;
|
||||||
|
$lineEndY = $lineStartY + $lineLength;
|
||||||
|
|
||||||
|
// Phase color
|
||||||
|
$phase = $conn->connection_type ?: 'L1N';
|
||||||
|
$color = $phaseColors[$phase] ?? $phaseColors['L1'] ?? array(139, 69, 19);
|
||||||
|
$pdf->SetDrawColor($color[0], $color[1], $color[2]);
|
||||||
|
$pdf->SetFillColor($color[0], $color[1], $color[2]);
|
||||||
|
|
||||||
|
// Vertical line going down
|
||||||
|
$pdf->Line($eqCenterX, $lineStartY, $eqCenterX, $lineEndY);
|
||||||
|
|
||||||
|
// Arrow at end
|
||||||
|
$pdf->Polygon(array(
|
||||||
|
$eqCenterX - 1.5, $lineEndY - 2,
|
||||||
|
$eqCenterX, $lineEndY,
|
||||||
|
$eqCenterX + 1.5, $lineEndY - 2
|
||||||
|
), 'F');
|
||||||
|
|
||||||
|
// Left label: Bezeichnung (rotated text not easy in TCPDF, use horizontal)
|
||||||
|
if (!empty($conn->output_label)) {
|
||||||
|
$pdf->SetFont('dejavusans', 'B', 5);
|
||||||
|
$pdf->SetTextColor(0, 0, 0);
|
||||||
|
$pdf->SetXY($eqCenterX - 15, $lineEndY + 1);
|
||||||
|
$pdf->Cell(30, 3, dol_trunc($conn->output_label, 15), 0, 0, 'C');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Right label: Kabeltyp + Größe
|
||||||
|
$cableInfo = trim(($conn->medium_type ?: '') . ' ' . ($conn->medium_spec ?: ''));
|
||||||
|
if (!empty($cableInfo)) {
|
||||||
|
$pdf->SetFont('dejavusans', '', 4);
|
||||||
|
$pdf->SetTextColor(100, 100, 100);
|
||||||
|
$pdf->SetXY($eqCenterX - 15, $lineEndY + 4);
|
||||||
|
$pdf->Cell(30, 3, dol_trunc($cableInfo, 18), 0, 0, 'C');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Phase type
|
||||||
|
$pdf->SetFont('dejavusans', 'B', 5);
|
||||||
|
$pdf->SetTextColor($color[0], $color[1], $color[2]);
|
||||||
|
$pdf->SetXY($eqCenterX - 5, $lineEndY + 7);
|
||||||
|
$pdf->Cell(10, 3, $phase, 0, 0, 'C');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$carrierIndex++;
|
||||||
|
}
|
||||||
|
|
||||||
|
$pdf->SetTextColor(0, 0, 0);
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// Legend - show phase colors used in busbars
|
||||||
|
// ========================================
|
||||||
|
$legendY = $startY + $height - 20;
|
||||||
|
$pdf->SetFont('dejavusans', '', 6);
|
||||||
|
$pdf->SetXY($startX + 5, $legendY);
|
||||||
|
$pdf->Cell(0, 4, 'Phasenfarben nach DIN VDE:', 0, 1, 'L');
|
||||||
|
|
||||||
|
$legendX = $startX + 5;
|
||||||
|
$phases = array('L1', 'L2', 'L3', 'N', 'PE');
|
||||||
|
foreach ($phases as $idx => $phase) {
|
||||||
|
$color = $phaseColors[$phase];
|
||||||
|
$pdf->SetFillColor($color[0], $color[1], $color[2]);
|
||||||
|
$pdf->Rect($legendX + $idx * 25, $legendY + 5, 8, 3, 'F');
|
||||||
|
$pdf->SetTextColor($color[0], $color[1], $color[2]);
|
||||||
|
$pdf->SetXY($legendX + $idx * 25 + 10, $legendY + 4);
|
||||||
|
$pdf->Cell(12, 4, $phase, 0, 0, 'L');
|
||||||
|
}
|
||||||
|
|
||||||
|
$pdf->SetTextColor(0, 0, 0);
|
||||||
|
}
|
||||||
432
ajax/export_tree_pdf.php
Executable file
432
ajax/export_tree_pdf.php
Executable file
|
|
@ -0,0 +1,432 @@
|
||||||
|
<?php
|
||||||
|
/* Copyright (C) 2026 Alles Watt lauft
|
||||||
|
*
|
||||||
|
* PDF Export for Anlage Tree
|
||||||
|
*/
|
||||||
|
|
||||||
|
$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");
|
||||||
|
|
||||||
|
require_once DOL_DOCUMENT_ROOT.'/core/lib/pdf.lib.php';
|
||||||
|
require_once DOL_DOCUMENT_ROOT.'/societe/class/societe.class.php';
|
||||||
|
require_once DOL_DOCUMENT_ROOT.'/contact/class/contact.class.php';
|
||||||
|
dol_include_once('/kundenkarte/class/anlage.class.php');
|
||||||
|
dol_include_once('/kundenkarte/class/anlagetype.class.php');
|
||||||
|
dol_include_once('/kundenkarte/class/anlagefile.class.php');
|
||||||
|
|
||||||
|
$langs->loadLangs(array('companies', 'kundenkarte@kundenkarte'));
|
||||||
|
|
||||||
|
// Get parameters
|
||||||
|
$socId = GETPOSTINT('socid');
|
||||||
|
$contactId = GETPOSTINT('contactid');
|
||||||
|
$systemId = GETPOSTINT('system');
|
||||||
|
|
||||||
|
// Security check
|
||||||
|
if (!$user->hasRight('kundenkarte', 'read')) {
|
||||||
|
accessforbidden();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load company
|
||||||
|
$societe = new Societe($db);
|
||||||
|
$societe->fetch($socId);
|
||||||
|
|
||||||
|
// Load contact if specified
|
||||||
|
$contact = null;
|
||||||
|
if ($contactId > 0) {
|
||||||
|
$contact = new Contact($db);
|
||||||
|
$contact->fetch($contactId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load system info
|
||||||
|
$systemLabel = '';
|
||||||
|
$sql = "SELECT label FROM ".MAIN_DB_PREFIX."c_kundenkarte_anlage_system WHERE rowid = ".((int) $systemId);
|
||||||
|
$resql = $db->query($sql);
|
||||||
|
if ($resql && $obj = $db->fetch_object($resql)) {
|
||||||
|
$systemLabel = $obj->label;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load tree
|
||||||
|
$anlage = new Anlage($db);
|
||||||
|
if ($contactId > 0) {
|
||||||
|
$tree = $anlage->fetchTreeByContact($socId, $contactId, $systemId);
|
||||||
|
} else {
|
||||||
|
$tree = $anlage->fetchTree($socId, $systemId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load all type fields for display (including headers)
|
||||||
|
$typeFieldsMap = array();
|
||||||
|
$sql = "SELECT f.*, f.fk_anlage_type FROM ".MAIN_DB_PREFIX."kundenkarte_anlage_type_field f WHERE f.active = 1 ORDER BY f.position ASC";
|
||||||
|
$resql = $db->query($sql);
|
||||||
|
if ($resql) {
|
||||||
|
while ($obj = $db->fetch_object($resql)) {
|
||||||
|
if (!isset($typeFieldsMap[$obj->fk_anlage_type])) {
|
||||||
|
$typeFieldsMap[$obj->fk_anlage_type] = array();
|
||||||
|
}
|
||||||
|
$typeFieldsMap[$obj->fk_anlage_type][] = $obj;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create PDF
|
||||||
|
$pdf = pdf_getInstance();
|
||||||
|
$pdf->SetCreator('Dolibarr - Kundenkarte');
|
||||||
|
$pdf->SetAuthor($user->getFullName($langs));
|
||||||
|
|
||||||
|
$title = $systemLabel.' - '.$societe->name;
|
||||||
|
if ($contact) {
|
||||||
|
$title .= ' - '.$contact->getFullName($langs);
|
||||||
|
}
|
||||||
|
$pdf->SetTitle($title);
|
||||||
|
|
||||||
|
$pdf->SetMargins(15, 15, 15);
|
||||||
|
$pdf->SetAutoPageBreak(true, 15);
|
||||||
|
$pdf->SetFont('dejavusans', '', 9);
|
||||||
|
|
||||||
|
// Check for PDF template
|
||||||
|
$tplidx = null;
|
||||||
|
$templateFile = $conf->kundenkarte->dir_output.'/templates/export_template.pdf';
|
||||||
|
if (file_exists($templateFile) && is_readable($templateFile)) {
|
||||||
|
try {
|
||||||
|
$pagecount = $pdf->setSourceFile($templateFile);
|
||||||
|
$tplidx = $pdf->importPage(1);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
// Template could not be loaded, continue without
|
||||||
|
$tplidx = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$pdf->AddPage();
|
||||||
|
if (!empty($tplidx)) {
|
||||||
|
$pdf->useTemplate($tplidx);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compact header - left aligned
|
||||||
|
$pdf->SetFont('dejavusans', 'B', 14);
|
||||||
|
$pdf->Cell(120, 6, $systemLabel, 0, 1, 'L');
|
||||||
|
|
||||||
|
$pdf->SetFont('dejavusans', '', 9);
|
||||||
|
$pdf->SetTextColor(80, 80, 80);
|
||||||
|
|
||||||
|
// Customer info in one compact block
|
||||||
|
$customerInfo = $societe->name;
|
||||||
|
if ($contact) {
|
||||||
|
$customerInfo .= ' | '.$contact->getFullName($langs);
|
||||||
|
}
|
||||||
|
$pdf->Cell(120, 4, $customerInfo, 0, 1, 'L');
|
||||||
|
|
||||||
|
// Address
|
||||||
|
$address = array();
|
||||||
|
if ($societe->address) $address[] = $societe->address;
|
||||||
|
if ($societe->zip || $societe->town) $address[] = trim($societe->zip.' '.$societe->town);
|
||||||
|
if (!empty($address)) {
|
||||||
|
$pdf->Cell(120, 4, implode(', ', $address), 0, 1, 'L');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Date and count
|
||||||
|
$totalElements = countTreeElements($tree);
|
||||||
|
$pdf->Cell(120, 4, dol_print_date(dol_now(), 'day').' | '.$totalElements.' Elemente', 0, 1, 'L');
|
||||||
|
|
||||||
|
$pdf->SetTextColor(0, 0, 0);
|
||||||
|
$pdf->Ln(2);
|
||||||
|
|
||||||
|
// Separator line
|
||||||
|
$pdf->SetDrawColor(200, 200, 200);
|
||||||
|
$pdf->Line(15, $pdf->GetY(), $pdf->getPageWidth() - 15, $pdf->GetY());
|
||||||
|
$pdf->Ln(4);
|
||||||
|
|
||||||
|
// Get font size settings
|
||||||
|
$fontSettings = array(
|
||||||
|
'header' => getDolGlobalInt('KUNDENKARTE_PDF_FONT_HEADER', 9),
|
||||||
|
'content' => getDolGlobalInt('KUNDENKARTE_PDF_FONT_CONTENT', 7),
|
||||||
|
'fields' => getDolGlobalInt('KUNDENKARTE_PDF_FONT_FIELDS', 7)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Draw tree
|
||||||
|
if (!empty($tree)) {
|
||||||
|
drawTreePdf($pdf, $tree, $typeFieldsMap, $langs, 0, $tplidx, $fontSettings);
|
||||||
|
} else {
|
||||||
|
$pdf->SetFont('dejavusans', 'I', 10);
|
||||||
|
$pdf->Cell(0, 10, $langs->trans('NoInstallations'), 0, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Count total elements in tree
|
||||||
|
*/
|
||||||
|
function countTreeElements($nodes) {
|
||||||
|
$count = 0;
|
||||||
|
if (!empty($nodes)) {
|
||||||
|
foreach ($nodes as $node) {
|
||||||
|
$count++;
|
||||||
|
if (!empty($node->children)) {
|
||||||
|
$count += countTreeElements($node->children);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $count;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Output PDF
|
||||||
|
$filename = 'Export_'.$systemLabel.'_'.dol_sanitizeFileName($societe->name);
|
||||||
|
if ($contact) {
|
||||||
|
$filename .= '_'.dol_sanitizeFileName($contact->lastname);
|
||||||
|
}
|
||||||
|
$filename .= '_'.date('Y-m-d').'.pdf';
|
||||||
|
|
||||||
|
$pdf->Output($filename, 'D');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draw tree recursively in PDF with visual hierarchy
|
||||||
|
*/
|
||||||
|
function drawTreePdf(&$pdf, $nodes, $typeFieldsMap, $langs, $level = 0, $tplidx = null, $fontSettings = null)
|
||||||
|
{
|
||||||
|
// Default font settings if not provided
|
||||||
|
if ($fontSettings === null) {
|
||||||
|
$fontSettings = array('header' => 9, 'content' => 7, 'fields' => 7);
|
||||||
|
}
|
||||||
|
|
||||||
|
$indent = $level * 12;
|
||||||
|
$leftMargin = 15;
|
||||||
|
$pageWidth = $pdf->getPageWidth() - 30;
|
||||||
|
$contentWidth = $pageWidth - $indent;
|
||||||
|
|
||||||
|
// Subtle gray tones - darker for higher levels, lighter for deeper levels
|
||||||
|
$levelColors = array(
|
||||||
|
0 => array('bg' => array(70, 70, 70), 'border' => array(50, 50, 50)), // Dark gray
|
||||||
|
1 => array('bg' => array(100, 100, 100), 'border' => array(80, 80, 80)), // Medium dark
|
||||||
|
2 => array('bg' => array(130, 130, 130), 'border' => array(110, 110, 110)), // Medium
|
||||||
|
3 => array('bg' => array(150, 150, 150), 'border' => array(130, 130, 130)), // Medium light
|
||||||
|
4 => array('bg' => array(170, 170, 170), 'border' => array(150, 150, 150)), // Light gray
|
||||||
|
);
|
||||||
|
$colorIndex = min($level, count($levelColors) - 1);
|
||||||
|
$colors = $levelColors[$colorIndex];
|
||||||
|
|
||||||
|
$nodeCount = count($nodes);
|
||||||
|
$nodeIndex = 0;
|
||||||
|
|
||||||
|
foreach ($nodes as $node) {
|
||||||
|
$nodeIndex++;
|
||||||
|
$isLast = ($nodeIndex == $nodeCount);
|
||||||
|
|
||||||
|
// Calculate content height to check page break
|
||||||
|
$estimatedHeight = 12; // Header height
|
||||||
|
$fieldValues = $node->getFieldValues();
|
||||||
|
if (!empty($typeFieldsMap[$node->fk_anlage_type])) {
|
||||||
|
foreach ($typeFieldsMap[$node->fk_anlage_type] as $fieldDef) {
|
||||||
|
if ($fieldDef->field_type === 'header') {
|
||||||
|
$estimatedHeight += 5;
|
||||||
|
} else {
|
||||||
|
$value = isset($fieldValues[$fieldDef->field_code]) ? $fieldValues[$fieldDef->field_code] : '';
|
||||||
|
if ($value !== '') $estimatedHeight += 4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($node->note_private) $estimatedHeight += 8;
|
||||||
|
if ($node->image_count > 0 || $node->doc_count > 0) $estimatedHeight += 4;
|
||||||
|
|
||||||
|
// Check if we need a new page
|
||||||
|
if ($pdf->GetY() + $estimatedHeight > 265) {
|
||||||
|
$pdf->AddPage();
|
||||||
|
if (!empty($tplidx)) {
|
||||||
|
$pdf->useTemplate($tplidx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$startY = $pdf->GetY();
|
||||||
|
$boxX = $leftMargin + $indent;
|
||||||
|
|
||||||
|
// Draw tree connector lines
|
||||||
|
if ($level > 0) {
|
||||||
|
$pdf->SetDrawColor(180, 180, 180);
|
||||||
|
$pdf->SetLineWidth(0.3);
|
||||||
|
|
||||||
|
// Horizontal line to element
|
||||||
|
$lineStartX = $boxX - 8;
|
||||||
|
$lineEndX = $boxX - 2;
|
||||||
|
$lineY = $startY + 4;
|
||||||
|
$pdf->Line($lineStartX, $lineY, $lineEndX, $lineY);
|
||||||
|
|
||||||
|
// Vertical line segment
|
||||||
|
$pdf->Line($lineStartX, $startY - 2, $lineStartX, $lineY);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw element box with rounded corners
|
||||||
|
$pdf->SetDrawColor($colors['border'][0], $colors['border'][1], $colors['border'][2]);
|
||||||
|
$pdf->SetLineWidth(0.4);
|
||||||
|
|
||||||
|
// Header bar with color
|
||||||
|
$pdf->SetFillColor($colors['bg'][0], $colors['bg'][1], $colors['bg'][2]);
|
||||||
|
$pdf->RoundedRect($boxX, $startY, $contentWidth, 8, 1.5, '1100', 'DF');
|
||||||
|
|
||||||
|
// Content area with light background
|
||||||
|
$pdf->SetFillColor(250, 250, 250);
|
||||||
|
$pdf->SetDrawColor(220, 220, 220);
|
||||||
|
|
||||||
|
// Element header text (white on colored background)
|
||||||
|
$pdf->SetXY($boxX + 3, $startY + 1.5);
|
||||||
|
$pdf->SetFont('dejavusans', 'B', $fontSettings['header']);
|
||||||
|
$pdf->SetTextColor(255, 255, 255);
|
||||||
|
|
||||||
|
$headerText = $node->label;
|
||||||
|
if ($node->type_label) {
|
||||||
|
$headerText .= ' · '.$node->type_label;
|
||||||
|
}
|
||||||
|
$pdf->Cell($contentWidth - 6, 5, $headerText, 0, 1, 'L');
|
||||||
|
|
||||||
|
$pdf->SetTextColor(0, 0, 0);
|
||||||
|
$contentStartY = $startY + 8;
|
||||||
|
$pdf->SetY($contentStartY);
|
||||||
|
|
||||||
|
// Collect content to measure height
|
||||||
|
$hasContent = false;
|
||||||
|
$contentY = $contentStartY + 2;
|
||||||
|
|
||||||
|
// Draw fields
|
||||||
|
if (!empty($typeFieldsMap[$node->fk_anlage_type])) {
|
||||||
|
foreach ($typeFieldsMap[$node->fk_anlage_type] as $fieldDef) {
|
||||||
|
// Handle header fields as section titles
|
||||||
|
if ($fieldDef->field_type === 'header') {
|
||||||
|
$pdf->SetXY($boxX + 4, $contentY);
|
||||||
|
$pdf->SetFont('dejavusans', 'B', $fontSettings['fields']);
|
||||||
|
$pdf->SetTextColor(100, 100, 100);
|
||||||
|
$pdf->Cell($contentWidth - 8, 4, strtoupper($fieldDef->field_label), 0, 1);
|
||||||
|
$pdf->SetTextColor(0, 0, 0);
|
||||||
|
$contentY += 4;
|
||||||
|
$hasContent = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$value = isset($fieldValues[$fieldDef->field_code]) ? $fieldValues[$fieldDef->field_code] : '';
|
||||||
|
|
||||||
|
if ($value !== '') {
|
||||||
|
// Format date values
|
||||||
|
if ($fieldDef->field_type === 'date' && $value) {
|
||||||
|
$value = dol_print_date(strtotime($value), 'day');
|
||||||
|
}
|
||||||
|
|
||||||
|
$pdf->SetXY($boxX + 4, $contentY);
|
||||||
|
$pdf->SetFont('dejavusans', '', $fontSettings['fields']);
|
||||||
|
$pdf->SetTextColor(120, 120, 120);
|
||||||
|
$pdf->Cell(30, 3.5, $fieldDef->field_label.':', 0, 0);
|
||||||
|
$pdf->SetFont('dejavusans', '', $fontSettings['content']);
|
||||||
|
$pdf->SetTextColor(50, 50, 50);
|
||||||
|
$pdf->Cell($contentWidth - 38, 3.5, $value, 0, 1);
|
||||||
|
$contentY += 3.5;
|
||||||
|
$hasContent = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Notes
|
||||||
|
if ($node->note_private) {
|
||||||
|
$pdf->SetXY($boxX + 4, $contentY);
|
||||||
|
$pdf->SetFont('dejavusans', 'I', $fontSettings['content']);
|
||||||
|
$pdf->SetTextColor(100, 100, 100);
|
||||||
|
$noteText = strip_tags($node->note_private);
|
||||||
|
if (strlen($noteText) > 80) {
|
||||||
|
$noteText = substr($noteText, 0, 77).'...';
|
||||||
|
}
|
||||||
|
$pdf->Cell($contentWidth - 8, 3.5, $noteText, 0, 1);
|
||||||
|
$contentY += 3.5;
|
||||||
|
$hasContent = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// File counts
|
||||||
|
if ($node->image_count > 0 || $node->doc_count > 0) {
|
||||||
|
$pdf->SetXY($boxX + 4, $contentY);
|
||||||
|
$pdf->SetFont('dejavusans', '', $fontSettings['content']);
|
||||||
|
$pdf->SetTextColor(150, 150, 150);
|
||||||
|
$fileInfo = array();
|
||||||
|
if ($node->image_count > 0) {
|
||||||
|
$fileInfo[] = $node->image_count.' '.($node->image_count == 1 ? 'Bild' : 'Bilder');
|
||||||
|
}
|
||||||
|
if ($node->doc_count > 0) {
|
||||||
|
$fileInfo[] = $node->doc_count.' '.($node->doc_count == 1 ? 'Dok.' : 'Dok.');
|
||||||
|
}
|
||||||
|
$pdf->Cell($contentWidth - 8, 3.5, implode(' | ', $fileInfo), 0, 1);
|
||||||
|
$contentY += 3.5;
|
||||||
|
$hasContent = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw content box if there's content
|
||||||
|
if ($hasContent) {
|
||||||
|
$contentHeight = $contentY - $contentStartY + 2;
|
||||||
|
$pdf->SetDrawColor(220, 220, 220);
|
||||||
|
$pdf->SetFillColor(252, 252, 252);
|
||||||
|
$pdf->RoundedRect($boxX, $contentStartY, $contentWidth, $contentHeight, 1.5, '0011', 'DF');
|
||||||
|
|
||||||
|
// Redraw content on top of box
|
||||||
|
$contentY = $contentStartY + 2;
|
||||||
|
|
||||||
|
if (!empty($typeFieldsMap[$node->fk_anlage_type])) {
|
||||||
|
foreach ($typeFieldsMap[$node->fk_anlage_type] as $fieldDef) {
|
||||||
|
if ($fieldDef->field_type === 'header') {
|
||||||
|
$pdf->SetXY($boxX + 4, $contentY);
|
||||||
|
$pdf->SetFont('dejavusans', 'B', $fontSettings['fields']);
|
||||||
|
$pdf->SetTextColor(100, 100, 100);
|
||||||
|
$pdf->Cell($contentWidth - 8, 4, strtoupper($fieldDef->field_label), 0, 1);
|
||||||
|
$pdf->SetTextColor(0, 0, 0);
|
||||||
|
$contentY += 4;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$value = isset($fieldValues[$fieldDef->field_code]) ? $fieldValues[$fieldDef->field_code] : '';
|
||||||
|
|
||||||
|
if ($value !== '') {
|
||||||
|
if ($fieldDef->field_type === 'date' && $value) {
|
||||||
|
$value = dol_print_date(strtotime($value), 'day');
|
||||||
|
}
|
||||||
|
|
||||||
|
$pdf->SetXY($boxX + 4, $contentY);
|
||||||
|
$pdf->SetFont('dejavusans', '', $fontSettings['fields']);
|
||||||
|
$pdf->SetTextColor(120, 120, 120);
|
||||||
|
$pdf->Cell(30, 3.5, $fieldDef->field_label.':', 0, 0);
|
||||||
|
$pdf->SetFont('dejavusans', '', $fontSettings['content']);
|
||||||
|
$pdf->SetTextColor(50, 50, 50);
|
||||||
|
$pdf->Cell($contentWidth - 38, 3.5, $value, 0, 1);
|
||||||
|
$contentY += 3.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($node->note_private) {
|
||||||
|
$pdf->SetXY($boxX + 4, $contentY);
|
||||||
|
$pdf->SetFont('dejavusans', 'I', $fontSettings['content']);
|
||||||
|
$pdf->SetTextColor(100, 100, 100);
|
||||||
|
$noteText = strip_tags($node->note_private);
|
||||||
|
if (strlen($noteText) > 80) {
|
||||||
|
$noteText = substr($noteText, 0, 77).'...';
|
||||||
|
}
|
||||||
|
$pdf->Cell($contentWidth - 8, 3.5, $noteText, 0, 1);
|
||||||
|
$contentY += 3.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($node->image_count > 0 || $node->doc_count > 0) {
|
||||||
|
$pdf->SetXY($boxX + 4, $contentY);
|
||||||
|
$pdf->SetFont('dejavusans', '', $fontSettings['content']);
|
||||||
|
$pdf->SetTextColor(150, 150, 150);
|
||||||
|
$fileInfo = array();
|
||||||
|
if ($node->image_count > 0) {
|
||||||
|
$fileInfo[] = $node->image_count.' '.($node->image_count == 1 ? 'Bild' : 'Bilder');
|
||||||
|
}
|
||||||
|
if ($node->doc_count > 0) {
|
||||||
|
$fileInfo[] = $node->doc_count.' '.($node->doc_count == 1 ? 'Dok.' : 'Dok.');
|
||||||
|
}
|
||||||
|
$pdf->Cell($contentWidth - 8, 3.5, implode(' | ', $fileInfo), 0, 1);
|
||||||
|
$contentY += 3.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
$pdf->SetY($contentStartY + $contentHeight + 3);
|
||||||
|
} else {
|
||||||
|
$pdf->SetY($startY + 11);
|
||||||
|
}
|
||||||
|
|
||||||
|
$pdf->SetTextColor(0, 0, 0);
|
||||||
|
|
||||||
|
// Children
|
||||||
|
if (!empty($node->children)) {
|
||||||
|
drawTreePdf($pdf, $node->children, $typeFieldsMap, $langs, $level + 1, $tplidx, $fontSettings);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
78
ajax/favorite_update.php
Executable file
78
ajax/favorite_update.php
Executable file
|
|
@ -0,0 +1,78 @@
|
||||||
|
<?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 ajax/favorite_update.php
|
||||||
|
* \brief AJAX handler for updating favorite product quantity
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (!defined('NOTOKENRENEWAL')) {
|
||||||
|
define('NOTOKENRENEWAL', '1');
|
||||||
|
}
|
||||||
|
if (!defined('NOREQUIREMENU')) {
|
||||||
|
define('NOREQUIREMENU', '1');
|
||||||
|
}
|
||||||
|
if (!defined('NOREQUIREAJAX')) {
|
||||||
|
define('NOREQUIREAJAX', '1');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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");
|
||||||
|
|
||||||
|
dol_include_once('/kundenkarte/class/favoriteproduct.class.php');
|
||||||
|
|
||||||
|
// Check permission
|
||||||
|
if (!$user->hasRight('kundenkarte', 'write')) {
|
||||||
|
http_response_code(403);
|
||||||
|
echo json_encode(array('error' => 'Permission denied'));
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
$id = GETPOSTINT('id');
|
||||||
|
$qty = GETPOST('qty', 'alpha');
|
||||||
|
|
||||||
|
if ($id <= 0) {
|
||||||
|
echo json_encode(array('error' => 'Invalid ID'));
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simple check: just needs to be numeric
|
||||||
|
if (!is_numeric($qty)) {
|
||||||
|
echo json_encode(array('error' => 'Invalid quantity'));
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$qty = (float) $qty;
|
||||||
|
|
||||||
|
$favoriteProduct = new FavoriteProduct($db);
|
||||||
|
$result = $favoriteProduct->fetch($id);
|
||||||
|
|
||||||
|
if ($result <= 0) {
|
||||||
|
echo json_encode(array('error' => 'Record not found'));
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$favoriteProduct->qty = $qty;
|
||||||
|
$result = $favoriteProduct->update($user);
|
||||||
|
|
||||||
|
if ($result > 0) {
|
||||||
|
echo json_encode(array(
|
||||||
|
'success' => true,
|
||||||
|
'id' => $id,
|
||||||
|
'qty' => $qty
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
echo json_encode(array('error' => $favoriteProduct->error));
|
||||||
|
}
|
||||||
106
ajax/field_autocomplete.php
Executable file
106
ajax/field_autocomplete.php
Executable 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
Executable file
101
ajax/file_preview.php
Executable 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),
|
||||||
|
));
|
||||||
307
ajax/graph_data.php
Executable file
307
ajax/graph_data.php
Executable file
|
|
@ -0,0 +1,307 @@
|
||||||
|
<?php
|
||||||
|
/* Copyright (C) 2026 Alles Watt lauft
|
||||||
|
*
|
||||||
|
* AJAX-Endpunkt: Liefert Anlagen als Cytoscape.js Graph
|
||||||
|
* Gebäude-Typen (type_system_code=GLOBAL) = Räume (Compound-Container)
|
||||||
|
* Geräte-Typen = Geräte (Nodes innerhalb der Räume)
|
||||||
|
* Hierarchie über fk_parent (wie im Baum)
|
||||||
|
* Connections = Kabel-Edges zwischen Geräten
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (!defined('NOTOKENRENEWAL')) define('NOTOKENRENEWAL', '1');
|
||||||
|
if (!defined('NOREQUIREMENU')) define('NOREQUIREMENU', '1');
|
||||||
|
if (!defined('NOREQUIREHTML')) define('NOREQUIREHTML', '1');
|
||||||
|
if (!defined('NOREQUIREAJAX')) define('NOREQUIREAJAX', '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/anlageconnection.class.php');
|
||||||
|
|
||||||
|
header('Content-Type: application/json; charset=UTF-8');
|
||||||
|
|
||||||
|
$langs->loadLangs(array('kundenkarte@kundenkarte'));
|
||||||
|
|
||||||
|
$socId = GETPOSTINT('socid');
|
||||||
|
$contactId = GETPOSTINT('contactid');
|
||||||
|
|
||||||
|
$response = array('success' => false, 'error' => '');
|
||||||
|
|
||||||
|
// Berechtigungsprüfung
|
||||||
|
if (!$user->hasRight('kundenkarte', 'read')) {
|
||||||
|
$response['error'] = $langs->trans('ErrorPermissionDenied');
|
||||||
|
echo json_encode($response);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($socId <= 0) {
|
||||||
|
$response['error'] = 'Missing socid';
|
||||||
|
echo json_encode($response);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Feld-Metadaten laden (field_code → Label, Display-Modus, Badge-Farbe pro Typ)
|
||||||
|
// Sortiert nach position → Reihenfolge wird in der Graph-Ansicht beibehalten
|
||||||
|
$fieldMeta = array(); // [fk_anlage_type][field_code] = {label, display_mode, badge_color, show_in_tree, show_in_hover}
|
||||||
|
$sqlFields = "SELECT fk_anlage_type, field_code, field_label, field_type, tree_display_mode, badge_color, show_in_tree, show_in_hover";
|
||||||
|
$sqlFields .= " FROM ".MAIN_DB_PREFIX."kundenkarte_anlage_type_field WHERE active = 1 ORDER BY position";
|
||||||
|
$resFields = $db->query($sqlFields);
|
||||||
|
if ($resFields) {
|
||||||
|
while ($fObj = $db->fetch_object($resFields)) {
|
||||||
|
if ($fObj->field_type === 'header') continue;
|
||||||
|
$fieldMeta[(int)$fObj->fk_anlage_type][$fObj->field_code] = array(
|
||||||
|
'label' => $fObj->field_label,
|
||||||
|
'display' => $fObj->tree_display_mode ?: 'badge',
|
||||||
|
'color' => $fObj->badge_color ?: '',
|
||||||
|
'type' => $fObj->field_type,
|
||||||
|
'show_in_tree' => (int) $fObj->show_in_tree,
|
||||||
|
'show_in_hover' => (int) $fObj->show_in_hover,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
$db->free($resFields);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Elemente laden - OHNE GLOBAL-System (das ist nur die separate Gebäudestruktur)
|
||||||
|
// Gebäude/Räume werden über den Typ erkannt (type_system_code = GLOBAL)
|
||||||
|
// Hierarchie kommt aus fk_parent (wie im Baum)
|
||||||
|
$sql = "SELECT a.rowid, a.label, a.fk_parent, a.fk_system, a.fk_anlage_type,";
|
||||||
|
$sql .= " a.field_values, a.fk_contact, a.graph_x, a.graph_y,";
|
||||||
|
$sql .= " t.label as type_label, t.picto as type_picto, t.color as type_color,";
|
||||||
|
$sql .= " s.code as system_code, s.label as system_label,";
|
||||||
|
$sql .= " ts.code as type_system_code,";
|
||||||
|
$sql .= " (SELECT COUNT(*) FROM ".MAIN_DB_PREFIX."kundenkarte_anlage_files f WHERE f.fk_anlage = a.rowid AND f.file_type IN ('image/jpeg','image/png','image/gif','image/webp')) as image_count,";
|
||||||
|
$sql .= " (SELECT COUNT(*) FROM ".MAIN_DB_PREFIX."kundenkarte_anlage_files f WHERE f.fk_anlage = a.rowid AND f.file_type NOT IN ('image/jpeg','image/png','image/gif','image/webp')) as doc_count";
|
||||||
|
$sql .= " FROM ".MAIN_DB_PREFIX."kundenkarte_anlage as a";
|
||||||
|
$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."kundenkarte_anlage_type as t ON a.fk_anlage_type = t.rowid";
|
||||||
|
$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."c_kundenkarte_anlage_system as s ON a.fk_system = s.rowid";
|
||||||
|
$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."c_kundenkarte_anlage_system as ts ON t.fk_system = ts.rowid";
|
||||||
|
$sql .= " WHERE a.fk_soc = ".(int)$socId;
|
||||||
|
$sql .= " AND s.code != 'GLOBAL'";
|
||||||
|
if ($contactId > 0) {
|
||||||
|
$sql .= " AND a.fk_contact = ".(int)$contactId;
|
||||||
|
} else {
|
||||||
|
// Auf Kunden-Ebene nur Elemente ohne Kontaktzuweisung (wie im Baum)
|
||||||
|
$sql .= " AND (a.fk_contact IS NULL OR a.fk_contact = 0)";
|
||||||
|
}
|
||||||
|
$sql .= " AND a.status = 1";
|
||||||
|
$sql .= " ORDER BY a.fk_parent, a.rang, a.rowid";
|
||||||
|
|
||||||
|
$elements = array('nodes' => array(), 'edges' => array());
|
||||||
|
$nodeIds = array();
|
||||||
|
// Zwischenspeicher: rowid → isBuilding (für Compound-Entscheidung)
|
||||||
|
$nodeIsBuilding = array();
|
||||||
|
// Hierarchie-Kanten (Gerät→Gerät), werden durch echte Kabel ersetzt falls vorhanden
|
||||||
|
$hierarchyEdges = array();
|
||||||
|
// Zwischenspeicher: alle DB-Zeilen für Zwei-Pass-Verarbeitung
|
||||||
|
$rows = array();
|
||||||
|
|
||||||
|
$resql = $db->query($sql);
|
||||||
|
if ($resql) {
|
||||||
|
// 1. Pass: Alle Zeilen laden und Gebäude-Typen merken
|
||||||
|
while ($obj = $db->fetch_object($resql)) {
|
||||||
|
$isBuilding = (!empty($obj->type_system_code) && $obj->type_system_code === 'GLOBAL');
|
||||||
|
$nodeIsBuilding[(int)$obj->rowid] = $isBuilding;
|
||||||
|
$nodeIds[$obj->rowid] = true;
|
||||||
|
$rows[] = $obj;
|
||||||
|
}
|
||||||
|
$db->free($resql);
|
||||||
|
|
||||||
|
// 2. Pass: Nodes und Hierarchie-Edges aufbauen
|
||||||
|
foreach ($rows as $obj) {
|
||||||
|
$isBuilding = $nodeIsBuilding[(int)$obj->rowid];
|
||||||
|
|
||||||
|
$nodeId = 'n_'.$obj->rowid;
|
||||||
|
|
||||||
|
$nodeData = array(
|
||||||
|
'id' => $nodeId,
|
||||||
|
'label' => $obj->label,
|
||||||
|
'type_label' => $obj->type_label ?: '',
|
||||||
|
'type_picto' => $obj->type_picto ?: '',
|
||||||
|
'type_color' => $obj->type_color ?: '',
|
||||||
|
'system_code' => $obj->system_code ?: '',
|
||||||
|
'system_label' => $obj->system_label ?: '',
|
||||||
|
'fk_parent' => (int) $obj->fk_parent,
|
||||||
|
'fk_anlage_type' => (int) $obj->fk_anlage_type,
|
||||||
|
'is_building' => $isBuilding,
|
||||||
|
'image_count' => (int) $obj->image_count,
|
||||||
|
'doc_count' => (int) $obj->doc_count,
|
||||||
|
'graph_x' => $obj->graph_x !== null ? (float) $obj->graph_x : null,
|
||||||
|
'graph_y' => $obj->graph_y !== null ? (float) $obj->graph_y : null,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Feldwerte mit Metadaten (Label, Display-Modus, Badge-Farbe)
|
||||||
|
// Iteration über $fieldMeta (nach position sortiert), nicht über $rawValues (JSON-Reihenfolge)
|
||||||
|
// Aufteilen: fields = auf dem Node (show_in_tree=1), hover_fields = im Tooltip (show_in_hover=1)
|
||||||
|
if (!empty($obj->field_values)) {
|
||||||
|
$rawValues = json_decode($obj->field_values, true);
|
||||||
|
if (is_array($rawValues) && !empty($rawValues)) {
|
||||||
|
$typeId = (int) $obj->fk_anlage_type;
|
||||||
|
$meta = isset($fieldMeta[$typeId]) ? $fieldMeta[$typeId] : array();
|
||||||
|
$treeFields = array();
|
||||||
|
$hoverFields = array();
|
||||||
|
foreach ($meta as $code => $fm) {
|
||||||
|
$val = isset($rawValues[$code]) ? $rawValues[$code] : null;
|
||||||
|
if ($val === '' || $val === null) continue;
|
||||||
|
// Checkbox-Werte anpassen
|
||||||
|
if ($fm['type'] === 'checkbox') {
|
||||||
|
$val = $val ? '1' : '0';
|
||||||
|
}
|
||||||
|
$fieldEntry = array(
|
||||||
|
'label' => $fm['label'],
|
||||||
|
'value' => $val,
|
||||||
|
'display' => $fm['display'],
|
||||||
|
'color' => $fm['color'],
|
||||||
|
'type' => $fm['type'],
|
||||||
|
);
|
||||||
|
// Auf dem Node: nur Felder mit show_in_tree=1
|
||||||
|
if (!empty($fm['show_in_tree'])) {
|
||||||
|
$treeFields[] = $fieldEntry;
|
||||||
|
}
|
||||||
|
// Im Tooltip: nur Felder mit show_in_hover=1
|
||||||
|
if (!empty($fm['show_in_hover'])) {
|
||||||
|
$hoverFields[] = $fieldEntry;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$nodeData['fields'] = $treeFields;
|
||||||
|
$nodeData['hover_fields'] = $hoverFields;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compound-Parent: NUR wenn Eltern-Node ein Gebäude/Raum ist
|
||||||
|
// Gerät→Gerät Hierarchie wird als Kante dargestellt (nicht verschachtelt)
|
||||||
|
$parentId = (int) $obj->fk_parent;
|
||||||
|
if ($parentId > 0 && isset($nodeIds[$parentId])) {
|
||||||
|
$parentIsBuilding = !empty($nodeIsBuilding[$parentId]);
|
||||||
|
if ($parentIsBuilding) {
|
||||||
|
// Gebäude/Raum als Container → Compound-Parent
|
||||||
|
$nodeData['parent'] = 'n_'.$parentId;
|
||||||
|
} else {
|
||||||
|
// Gerät→Gerät → Hierarchie-Kante vormerken (wird ggf. durch Kabel ersetzt)
|
||||||
|
$hierKey = min($parentId, (int)$obj->rowid).'_'.max($parentId, (int)$obj->rowid);
|
||||||
|
$hierarchyEdges[$hierKey] = array(
|
||||||
|
'data' => array(
|
||||||
|
'id' => 'hier_'.$parentId.'_'.$obj->rowid,
|
||||||
|
'source' => 'n_'.$parentId,
|
||||||
|
'target' => 'n_'.$obj->rowid,
|
||||||
|
'is_hierarchy' => true,
|
||||||
|
),
|
||||||
|
'classes' => 'hierarchy-edge'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$elements['nodes'][] = array(
|
||||||
|
'data' => $nodeData,
|
||||||
|
'classes' => $isBuilding ? 'building-node' : 'device-node'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verbindungen laden
|
||||||
|
$connObj = new AnlageConnection($db);
|
||||||
|
$connections = $connObj->fetchBySociete($socId, 0);
|
||||||
|
|
||||||
|
// Verwendete Kabeltypen für die Legende sammeln
|
||||||
|
$usedCableTypes = array();
|
||||||
|
|
||||||
|
// Kabel-Paare merken (um Hierarchie-Kanten zu ersetzen)
|
||||||
|
$cableConnectedPairs = array();
|
||||||
|
|
||||||
|
foreach ($connections as $conn) {
|
||||||
|
// Nur Edges für tatsächlich geladene Nodes
|
||||||
|
if (!isset($nodeIds[$conn->fk_source]) || !isset($nodeIds[$conn->fk_target])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$isPassthrough = empty($conn->label) && empty($conn->medium_type_label) && empty($conn->medium_type_text) && empty($conn->medium_spec);
|
||||||
|
|
||||||
|
// Edge-Label zusammenbauen
|
||||||
|
$edgeLabel = '';
|
||||||
|
$mediumType = !empty($conn->medium_type_label) ? $conn->medium_type_label : $conn->medium_type_text;
|
||||||
|
if (!empty($mediumType)) {
|
||||||
|
$edgeLabel = $mediumType;
|
||||||
|
if (!empty($conn->medium_spec)) {
|
||||||
|
$edgeLabel .= ' '.$conn->medium_spec;
|
||||||
|
}
|
||||||
|
if (!empty($conn->medium_length)) {
|
||||||
|
$edgeLabel .= ', '.$conn->medium_length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Farbe: aus Connection oder aus Medium-Type
|
||||||
|
$color = $conn->medium_color;
|
||||||
|
if (empty($color) && !empty($conn->fk_medium_type)) {
|
||||||
|
// Farbe aus Medium-Type-Tabelle holen
|
||||||
|
$sqlColor = "SELECT color, label_short FROM ".MAIN_DB_PREFIX."kundenkarte_medium_type WHERE rowid = ".(int)$conn->fk_medium_type;
|
||||||
|
$resColor = $db->query($sqlColor);
|
||||||
|
if ($resColor && $mtObj = $db->fetch_object($resColor)) {
|
||||||
|
$color = $mtObj->color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Kabeltyp für Legende merken
|
||||||
|
if (!$isPassthrough && !empty($mediumType)) {
|
||||||
|
$typeKey = $mediumType;
|
||||||
|
if (!isset($usedCableTypes[$typeKey])) {
|
||||||
|
$usedCableTypes[$typeKey] = array(
|
||||||
|
'label' => $mediumType,
|
||||||
|
'color' => $color ?: '#5a8a5a',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Echte Kabelverbindung (nicht durchgeschleift) → Hierarchie-Kante überflüssig
|
||||||
|
if (!$isPassthrough) {
|
||||||
|
$src = (int) $conn->fk_source;
|
||||||
|
$tgt = (int) $conn->fk_target;
|
||||||
|
$pairKey = min($src, $tgt).'_'.max($src, $tgt);
|
||||||
|
$cableConnectedPairs[$pairKey] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
$elements['edges'][] = array(
|
||||||
|
'data' => array(
|
||||||
|
'id' => 'conn_'.$conn->id,
|
||||||
|
'source' => 'n_'.$conn->fk_source,
|
||||||
|
'target' => 'n_'.$conn->fk_target,
|
||||||
|
'label' => $edgeLabel,
|
||||||
|
'medium_type' => $mediumType,
|
||||||
|
'medium_spec' => $conn->medium_spec,
|
||||||
|
'medium_length' => $conn->medium_length,
|
||||||
|
'medium_color' => $color,
|
||||||
|
'connection_id' => (int) $conn->id,
|
||||||
|
'is_passthrough' => $isPassthrough,
|
||||||
|
),
|
||||||
|
'classes' => $isPassthrough ? 'passthrough-edge' : 'cable-edge'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hierarchie-Kanten hinzufügen, aber nur wenn kein echtes Kabel zwischen den Geräten existiert
|
||||||
|
foreach ($hierarchyEdges as $hierKey => $hierEdge) {
|
||||||
|
if (!isset($cableConnectedPairs[$hierKey])) {
|
||||||
|
$elements['edges'][] = $hierEdge;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prüfen ob gespeicherte Positionen vorhanden sind
|
||||||
|
$hasPositions = false;
|
||||||
|
foreach ($elements['nodes'] as $node) {
|
||||||
|
if ($node['data']['graph_x'] !== null) {
|
||||||
|
$hasPositions = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$response['success'] = true;
|
||||||
|
$response['elements'] = $elements;
|
||||||
|
$response['cable_types'] = array_values($usedCableTypes);
|
||||||
|
$response['has_positions'] = $hasPositions;
|
||||||
|
$response['meta'] = array(
|
||||||
|
'socid' => $socId,
|
||||||
|
'contactid' => $contactId,
|
||||||
|
'total_nodes' => count($elements['nodes']),
|
||||||
|
'total_connections' => count($connections),
|
||||||
|
);
|
||||||
|
|
||||||
|
echo json_encode($response);
|
||||||
93
ajax/graph_save_positions.php
Executable file
93
ajax/graph_save_positions.php
Executable file
|
|
@ -0,0 +1,93 @@
|
||||||
|
<?php
|
||||||
|
/* Copyright (C) 2026 Alles Watt lauft
|
||||||
|
*
|
||||||
|
* AJAX-Endpunkt: Graph-Positionen speichern
|
||||||
|
* Speichert x/y-Koordinaten der Nodes nach Drag&Drop
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (!defined('NOTOKENRENEWAL')) define('NOTOKENRENEWAL', '1');
|
||||||
|
if (!defined('NOREQUIREMENU')) define('NOREQUIREMENU', '1');
|
||||||
|
if (!defined('NOREQUIREHTML')) define('NOREQUIREHTML', '1');
|
||||||
|
if (!defined('NOREQUIREAJAX')) define('NOREQUIREAJAX', '1');
|
||||||
|
if (!defined('NOCSRFCHECK')) define('NOCSRFCHECK', '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");
|
||||||
|
|
||||||
|
header('Content-Type: application/json; charset=UTF-8');
|
||||||
|
|
||||||
|
$response = array('success' => false, 'error' => '');
|
||||||
|
|
||||||
|
// Berechtigungsprüfung
|
||||||
|
if (!$user->hasRight('kundenkarte', 'write')) {
|
||||||
|
$response['error'] = 'Keine Berechtigung';
|
||||||
|
echo json_encode($response);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$action = GETPOST('action', 'aZ');
|
||||||
|
|
||||||
|
if ($action === 'save') {
|
||||||
|
// Positionen als JSON-Array: [{id: 123, x: 45.6, y: 78.9}, ...]
|
||||||
|
$rawInput = file_get_contents('php://input');
|
||||||
|
$input = json_decode($rawInput, true);
|
||||||
|
|
||||||
|
if (!is_array($input) || empty($input['positions'])) {
|
||||||
|
$response['error'] = 'Keine Positionen übergeben';
|
||||||
|
echo json_encode($response);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$saved = 0;
|
||||||
|
$db->begin();
|
||||||
|
|
||||||
|
foreach ($input['positions'] as $pos) {
|
||||||
|
$anlageId = (int) ($pos['id'] ?? 0);
|
||||||
|
$x = (float) ($pos['x'] ?? 0);
|
||||||
|
$y = (float) ($pos['y'] ?? 0);
|
||||||
|
if ($anlageId <= 0) continue;
|
||||||
|
|
||||||
|
$sql = "UPDATE ".MAIN_DB_PREFIX."kundenkarte_anlage";
|
||||||
|
$sql .= " SET graph_x = ".$x.", graph_y = ".$y;
|
||||||
|
$sql .= " WHERE rowid = ".$anlageId;
|
||||||
|
if ($db->query($sql)) {
|
||||||
|
$saved++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$db->commit();
|
||||||
|
$response['success'] = true;
|
||||||
|
$response['saved'] = $saved;
|
||||||
|
|
||||||
|
} elseif ($action === 'reset') {
|
||||||
|
// Alle Positionen für einen Kunden zurücksetzen
|
||||||
|
$socId = GETPOSTINT('socid');
|
||||||
|
$contactId = GETPOSTINT('contactid');
|
||||||
|
|
||||||
|
if ($socId <= 0) {
|
||||||
|
$response['error'] = 'Fehlende socid';
|
||||||
|
echo json_encode($response);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$sql = "UPDATE ".MAIN_DB_PREFIX."kundenkarte_anlage";
|
||||||
|
$sql .= " SET graph_x = NULL, graph_y = NULL";
|
||||||
|
$sql .= " WHERE fk_soc = ".(int)$socId;
|
||||||
|
if ($contactId > 0) {
|
||||||
|
$sql .= " AND fk_contact = ".(int)$contactId;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($db->query($sql)) {
|
||||||
|
$response['success'] = true;
|
||||||
|
$response['reset'] = $db->affected_rows;
|
||||||
|
} else {
|
||||||
|
$response['error'] = 'Datenbankfehler';
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
$response['error'] = 'Unbekannte Aktion';
|
||||||
|
}
|
||||||
|
|
||||||
|
echo json_encode($response);
|
||||||
145
ajax/icon_upload.php
Executable file
145
ajax/icon_upload.php
Executable file
|
|
@ -0,0 +1,145 @@
|
||||||
|
<?php
|
||||||
|
/* Copyright (C) 2026 Alles Watt lauft
|
||||||
|
*
|
||||||
|
* AJAX handler for custom icon upload
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 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) {
|
||||||
|
http_response_code(500);
|
||||||
|
die(json_encode(array('error' => 'Include of main fails')));
|
||||||
|
}
|
||||||
|
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
// Security check
|
||||||
|
if (!$user->admin && !$user->hasRight('kundenkarte', 'admin')) {
|
||||||
|
http_response_code(403);
|
||||||
|
die(json_encode(array('error' => 'Access denied')));
|
||||||
|
}
|
||||||
|
|
||||||
|
$action = GETPOST('action', 'aZ09');
|
||||||
|
|
||||||
|
// Directory for custom icons
|
||||||
|
$iconDir = DOL_DATA_ROOT.'/kundenkarte/icons';
|
||||||
|
$iconUrl = DOL_URL_ROOT.'/document.php?modulepart=kundenkarte&file=icons/';
|
||||||
|
|
||||||
|
// Create directory if not exists
|
||||||
|
if (!is_dir($iconDir)) {
|
||||||
|
dol_mkdir($iconDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List all custom icons
|
||||||
|
*/
|
||||||
|
if ($action == 'list') {
|
||||||
|
$icons = array();
|
||||||
|
|
||||||
|
if (is_dir($iconDir)) {
|
||||||
|
$files = scandir($iconDir);
|
||||||
|
foreach ($files as $file) {
|
||||||
|
if ($file == '.' || $file == '..') continue;
|
||||||
|
$ext = strtolower(pathinfo($file, PATHINFO_EXTENSION));
|
||||||
|
if (in_array($ext, array('png', 'jpg', 'jpeg', 'gif', 'svg', 'webp'))) {
|
||||||
|
$icons[] = array(
|
||||||
|
'filename' => $file,
|
||||||
|
'url' => $iconUrl.urlencode($file),
|
||||||
|
'name' => pathinfo($file, PATHINFO_FILENAME)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
echo json_encode(array('success' => true, 'icons' => $icons));
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Upload a new icon
|
||||||
|
*/
|
||||||
|
if ($action == 'upload') {
|
||||||
|
if (empty($_FILES['icon']) || $_FILES['icon']['error'] != 0) {
|
||||||
|
http_response_code(400);
|
||||||
|
die(json_encode(array('error' => 'No file uploaded or upload error')));
|
||||||
|
}
|
||||||
|
|
||||||
|
$file = $_FILES['icon'];
|
||||||
|
$ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
|
||||||
|
|
||||||
|
// Check extension
|
||||||
|
$allowedExt = array('png', 'jpg', 'jpeg', 'gif', 'svg', 'webp');
|
||||||
|
if (!in_array($ext, $allowedExt)) {
|
||||||
|
http_response_code(400);
|
||||||
|
die(json_encode(array('error' => 'Invalid file type. Allowed: '.implode(', ', $allowedExt))));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check size (max 500KB)
|
||||||
|
if ($file['size'] > 512000) {
|
||||||
|
http_response_code(400);
|
||||||
|
die(json_encode(array('error' => 'File too large. Max 500KB')));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sanitize filename
|
||||||
|
$filename = dol_sanitizeFileName($file['name']);
|
||||||
|
$filename = preg_replace('/[^a-zA-Z0-9_\-\.]/', '_', $filename);
|
||||||
|
|
||||||
|
// Check if file exists, add number if so
|
||||||
|
$baseName = pathinfo($filename, PATHINFO_FILENAME);
|
||||||
|
$counter = 1;
|
||||||
|
while (file_exists($iconDir.'/'.$filename)) {
|
||||||
|
$filename = $baseName.'_'.$counter.'.'.$ext;
|
||||||
|
$counter++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move file
|
||||||
|
if (move_uploaded_file($file['tmp_name'], $iconDir.'/'.$filename)) {
|
||||||
|
echo json_encode(array(
|
||||||
|
'success' => true,
|
||||||
|
'icon' => array(
|
||||||
|
'filename' => $filename,
|
||||||
|
'url' => $iconUrl.urlencode($filename),
|
||||||
|
'name' => pathinfo($filename, PATHINFO_FILENAME)
|
||||||
|
)
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
http_response_code(500);
|
||||||
|
die(json_encode(array('error' => 'Failed to save file')));
|
||||||
|
}
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete an icon
|
||||||
|
*/
|
||||||
|
if ($action == 'delete') {
|
||||||
|
$filename = GETPOST('filename', 'alphanohtml');
|
||||||
|
|
||||||
|
if (empty($filename)) {
|
||||||
|
http_response_code(400);
|
||||||
|
die(json_encode(array('error' => 'No filename provided')));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sanitize to prevent directory traversal
|
||||||
|
$filename = basename($filename);
|
||||||
|
$filepath = $iconDir.'/'.$filename;
|
||||||
|
|
||||||
|
if (file_exists($filepath)) {
|
||||||
|
if (unlink($filepath)) {
|
||||||
|
echo json_encode(array('success' => true));
|
||||||
|
} else {
|
||||||
|
http_response_code(500);
|
||||||
|
die(json_encode(array('error' => 'Failed to delete file')));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
http_response_code(404);
|
||||||
|
die(json_encode(array('error' => 'File not found')));
|
||||||
|
}
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
http_response_code(400);
|
||||||
|
die(json_encode(array('error' => 'Invalid action')));
|
||||||
119
ajax/medium_types.php
Executable file
119
ajax/medium_types.php
Executable file
|
|
@ -0,0 +1,119 @@
|
||||||
|
<?php
|
||||||
|
/* Copyright (C) 2026 Alles Watt lauft
|
||||||
|
*
|
||||||
|
* AJAX endpoint for medium types (Kabeltypen)
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (!defined('NOTOKENRENEWAL')) define('NOTOKENRENEWAL', '1');
|
||||||
|
if (!defined('NOREQUIREMENU')) define('NOREQUIREMENU', '1');
|
||||||
|
if (!defined('NOREQUIREHTML')) define('NOREQUIREHTML', '1');
|
||||||
|
if (!defined('NOREQUIREAJAX')) define('NOREQUIREAJAX', '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/mediumtype.class.php');
|
||||||
|
|
||||||
|
header('Content-Type: application/json; charset=UTF-8');
|
||||||
|
|
||||||
|
$langs->loadLangs(array('kundenkarte@kundenkarte'));
|
||||||
|
|
||||||
|
$action = GETPOST('action', 'aZ09');
|
||||||
|
$systemId = GETPOSTINT('system_id');
|
||||||
|
|
||||||
|
$response = array('success' => false, 'error' => '');
|
||||||
|
|
||||||
|
// Security check
|
||||||
|
if (!$user->hasRight('kundenkarte', 'read')) {
|
||||||
|
$response['error'] = $langs->trans('ErrorPermissionDenied');
|
||||||
|
echo json_encode($response);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$mediumType = new MediumType($db);
|
||||||
|
|
||||||
|
switch ($action) {
|
||||||
|
case 'list':
|
||||||
|
// Get all medium types for a system (or all)
|
||||||
|
$types = $mediumType->fetchAllBySystem($systemId, 1);
|
||||||
|
|
||||||
|
$result = array();
|
||||||
|
foreach ($types as $t) {
|
||||||
|
$specs = $t->getAvailableSpecsArray();
|
||||||
|
$result[] = array(
|
||||||
|
'id' => $t->id,
|
||||||
|
'ref' => $t->ref,
|
||||||
|
'label' => $t->label,
|
||||||
|
'label_short' => $t->label_short,
|
||||||
|
'category' => $t->category,
|
||||||
|
'category_label' => $t->getCategoryLabel(),
|
||||||
|
'fk_system' => $t->fk_system,
|
||||||
|
'system_label' => $t->system_label,
|
||||||
|
'default_spec' => $t->default_spec,
|
||||||
|
'available_specs' => $specs,
|
||||||
|
'color' => $t->color
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$response['success'] = true;
|
||||||
|
$response['types'] = $result;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'list_grouped':
|
||||||
|
// Get types grouped by category
|
||||||
|
$grouped = $mediumType->fetchGroupedByCategory($systemId);
|
||||||
|
|
||||||
|
$result = array();
|
||||||
|
foreach ($grouped as $category => $types) {
|
||||||
|
$catTypes = array();
|
||||||
|
foreach ($types as $t) {
|
||||||
|
$catTypes[] = array(
|
||||||
|
'id' => $t->id,
|
||||||
|
'ref' => $t->ref,
|
||||||
|
'label' => $t->label,
|
||||||
|
'label_short' => $t->label_short,
|
||||||
|
'default_spec' => $t->default_spec,
|
||||||
|
'available_specs' => $t->getAvailableSpecsArray(),
|
||||||
|
'color' => $t->color
|
||||||
|
);
|
||||||
|
}
|
||||||
|
$result[] = array(
|
||||||
|
'category' => $category,
|
||||||
|
'category_label' => $types[0]->getCategoryLabel(),
|
||||||
|
'types' => $catTypes
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$response['success'] = true;
|
||||||
|
$response['groups'] = $result;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'get':
|
||||||
|
// Get single type details
|
||||||
|
$typeId = GETPOSTINT('type_id');
|
||||||
|
if ($typeId > 0 && $mediumType->fetch($typeId) > 0) {
|
||||||
|
$response['success'] = true;
|
||||||
|
$response['type'] = array(
|
||||||
|
'id' => $mediumType->id,
|
||||||
|
'ref' => $mediumType->ref,
|
||||||
|
'label' => $mediumType->label,
|
||||||
|
'label_short' => $mediumType->label_short,
|
||||||
|
'category' => $mediumType->category,
|
||||||
|
'category_label' => $mediumType->getCategoryLabel(),
|
||||||
|
'default_spec' => $mediumType->default_spec,
|
||||||
|
'available_specs' => $mediumType->getAvailableSpecsArray(),
|
||||||
|
'color' => $mediumType->color,
|
||||||
|
'description' => $mediumType->description
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
$response['error'] = $langs->trans('ErrorRecordNotFound');
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
$response['error'] = 'Unknown action';
|
||||||
|
}
|
||||||
|
|
||||||
|
echo json_encode($response);
|
||||||
333
ajax/pwa_api.php
Normal file
333
ajax/pwa_api.php
Normal file
|
|
@ -0,0 +1,333 @@
|
||||||
|
<?php
|
||||||
|
/* Copyright (C) 2026 Alles Watt lauft
|
||||||
|
*
|
||||||
|
* PWA API - AJAX Endpoints für die mobile App
|
||||||
|
* Token-basierte Authentifizierung
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (!defined('NOLOGIN')) {
|
||||||
|
define('NOLOGIN', '1');
|
||||||
|
}
|
||||||
|
if (!defined('NOREQUIREMENU')) {
|
||||||
|
define('NOREQUIREMENU', '1');
|
||||||
|
}
|
||||||
|
if (!defined('NOREQUIREHTML')) {
|
||||||
|
define('NOREQUIREHTML', '1');
|
||||||
|
}
|
||||||
|
if (!defined('NOREQUIREAJAX')) {
|
||||||
|
define('NOREQUIREAJAX', '1');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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(json_encode(array('success' => false, 'error' => 'Dolibarr not loaded')));
|
||||||
|
}
|
||||||
|
|
||||||
|
header('Content-Type: application/json; charset=UTF-8');
|
||||||
|
|
||||||
|
$response = array('success' => false);
|
||||||
|
|
||||||
|
// Verify token
|
||||||
|
$token = GETPOST('token', 'none');
|
||||||
|
if (empty($token)) {
|
||||||
|
echo json_encode(array('success' => false, 'error' => 'Nicht authentifiziert'));
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$tokenData = json_decode(base64_decode($token), true);
|
||||||
|
if (!$tokenData || empty($tokenData['user_id']) || empty($tokenData['expires']) || $tokenData['expires'] < time()) {
|
||||||
|
echo json_encode(array('success' => false, 'error' => 'Token ungültig oder abgelaufen'));
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify hash
|
||||||
|
$expectedHash = md5($tokenData['user_id'] . $tokenData['login'] . getDolGlobalString('MAIN_SECURITY_SALT', 'defaultsalt'));
|
||||||
|
if ($tokenData['hash'] !== $expectedHash) {
|
||||||
|
echo json_encode(array('success' => false, 'error' => 'Token manipuliert'));
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load user
|
||||||
|
require_once DOL_DOCUMENT_ROOT.'/user/class/user.class.php';
|
||||||
|
$user = new User($db);
|
||||||
|
$user->fetch($tokenData['user_id']);
|
||||||
|
|
||||||
|
if ($user->id <= 0 || $user->statut != 1) {
|
||||||
|
echo json_encode(array('success' => false, 'error' => 'Benutzer nicht mehr aktiv'));
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check permission
|
||||||
|
if (!$user->hasRight('kundenkarte', 'read')) {
|
||||||
|
echo json_encode(array('success' => false, 'error' => 'Keine Berechtigung'));
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load required classes
|
||||||
|
require_once DOL_DOCUMENT_ROOT.'/custom/kundenkarte/class/anlage.class.php';
|
||||||
|
require_once DOL_DOCUMENT_ROOT.'/custom/kundenkarte/class/equipmentpanel.class.php';
|
||||||
|
require_once DOL_DOCUMENT_ROOT.'/custom/kundenkarte/class/equipmentcarrier.class.php';
|
||||||
|
require_once DOL_DOCUMENT_ROOT.'/custom/kundenkarte/class/equipment.class.php';
|
||||||
|
require_once DOL_DOCUMENT_ROOT.'/custom/kundenkarte/class/equipmenttype.class.php';
|
||||||
|
|
||||||
|
$action = GETPOST('action', 'aZ09');
|
||||||
|
|
||||||
|
switch ($action) {
|
||||||
|
// ============================================
|
||||||
|
// CUSTOMER SEARCH
|
||||||
|
// ============================================
|
||||||
|
case 'search_customers':
|
||||||
|
$query = GETPOST('query', 'alphanohtml');
|
||||||
|
if (strlen($query) < 2) {
|
||||||
|
$response['error'] = 'Mindestens 2 Zeichen';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
$sql = "SELECT s.rowid, s.nom as name, s.town";
|
||||||
|
$sql .= " FROM ".MAIN_DB_PREFIX."societe as s";
|
||||||
|
$sql .= " WHERE s.entity IN (".getEntity('societe').")";
|
||||||
|
$sql .= " AND s.status = 1";
|
||||||
|
$sql .= " AND (s.nom LIKE '%".$db->escape($query)."%'";
|
||||||
|
$sql .= " OR s.name_alias LIKE '%".$db->escape($query)."%')";
|
||||||
|
$sql .= " ORDER BY s.nom ASC";
|
||||||
|
$sql .= " LIMIT 30";
|
||||||
|
|
||||||
|
$resql = $db->query($sql);
|
||||||
|
$customers = array();
|
||||||
|
|
||||||
|
if ($resql) {
|
||||||
|
while ($obj = $db->fetch_object($resql)) {
|
||||||
|
$customers[] = array(
|
||||||
|
'id' => $obj->rowid,
|
||||||
|
'name' => $obj->name,
|
||||||
|
'town' => $obj->town
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$response['success'] = true;
|
||||||
|
$response['customers'] = $customers;
|
||||||
|
break;
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// GET ANLAGEN FOR CUSTOMER
|
||||||
|
// ============================================
|
||||||
|
case 'get_anlagen':
|
||||||
|
$customerId = GETPOSTINT('customer_id');
|
||||||
|
if ($customerId <= 0) {
|
||||||
|
$response['error'] = 'Keine Kunden-ID';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
$anlage = new Anlage($db);
|
||||||
|
$anlagen = $anlage->fetchAll('ASC', 'label', 0, 0, array('fk_soc' => $customerId));
|
||||||
|
|
||||||
|
$result = array();
|
||||||
|
if (is_array($anlagen)) {
|
||||||
|
foreach ($anlagen as $a) {
|
||||||
|
$result[] = array(
|
||||||
|
'id' => $a->id,
|
||||||
|
'label' => $a->label,
|
||||||
|
'has_editor' => !empty($a->schematic_editor_enabled)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$response['success'] = true;
|
||||||
|
$response['anlagen'] = $result;
|
||||||
|
break;
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// GET ANLAGE DATA (Panels, Carriers, Equipment)
|
||||||
|
// ============================================
|
||||||
|
case 'get_anlage_data':
|
||||||
|
$anlageId = GETPOSTINT('anlage_id');
|
||||||
|
if ($anlageId <= 0) {
|
||||||
|
$response['error'] = 'Keine Anlagen-ID';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load panels
|
||||||
|
$panel = new EquipmentPanel($db);
|
||||||
|
$panels = $panel->fetchByAnlage($anlageId);
|
||||||
|
$panelsData = array();
|
||||||
|
foreach ($panels as $p) {
|
||||||
|
$panelsData[] = array(
|
||||||
|
'id' => $p->id,
|
||||||
|
'label' => $p->label,
|
||||||
|
'position' => $p->position
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load carriers
|
||||||
|
$carrier = new EquipmentCarrier($db);
|
||||||
|
$carriersData = array();
|
||||||
|
foreach ($panels as $p) {
|
||||||
|
$p->fetchCarriers();
|
||||||
|
foreach ($p->carriers as $c) {
|
||||||
|
$carriersData[] = array(
|
||||||
|
'id' => $c->id,
|
||||||
|
'fk_panel' => $c->fk_panel,
|
||||||
|
'label' => $c->label,
|
||||||
|
'total_te' => $c->total_te,
|
||||||
|
'position' => $c->position
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load equipment
|
||||||
|
$equipment = new Equipment($db);
|
||||||
|
$equipmentData = array();
|
||||||
|
foreach ($carriersData as $c) {
|
||||||
|
$items = $equipment->fetchByCarrier($c['id']);
|
||||||
|
foreach ($items as $eq) {
|
||||||
|
$equipmentData[] = array(
|
||||||
|
'id' => $eq->id,
|
||||||
|
'fk_carrier' => $eq->fk_carrier,
|
||||||
|
'fk_equipment_type' => $eq->fk_equipment_type,
|
||||||
|
'label' => $eq->label,
|
||||||
|
'position_te' => $eq->position_te,
|
||||||
|
'width_te' => $eq->width_te,
|
||||||
|
'field_values' => $eq->getFieldValues()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load equipment types
|
||||||
|
$eqType = new EquipmentType($db);
|
||||||
|
$types = $eqType->fetchAllBySystem(1, 1); // System 1 = Elektro, nur aktive
|
||||||
|
$typesData = array();
|
||||||
|
foreach ($types as $t) {
|
||||||
|
$typesData[] = array(
|
||||||
|
'id' => $t->id,
|
||||||
|
'ref' => $t->ref,
|
||||||
|
'label' => $t->label,
|
||||||
|
'label_short' => $t->label_short,
|
||||||
|
'width_te' => $t->width_te,
|
||||||
|
'color' => $t->color
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$response['success'] = true;
|
||||||
|
$response['panels'] = $panelsData;
|
||||||
|
$response['carriers'] = $carriersData;
|
||||||
|
$response['equipment'] = $equipmentData;
|
||||||
|
$response['types'] = $typesData;
|
||||||
|
break;
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// CREATE PANEL
|
||||||
|
// ============================================
|
||||||
|
case 'create_panel':
|
||||||
|
if (!$user->hasRight('kundenkarte', 'write')) {
|
||||||
|
$response['error'] = 'Keine Schreibberechtigung';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
$anlageId = GETPOSTINT('anlage_id');
|
||||||
|
$label = GETPOST('label', 'alphanohtml');
|
||||||
|
|
||||||
|
if ($anlageId <= 0) {
|
||||||
|
$response['error'] = 'Keine Anlagen-ID';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
$panel = new EquipmentPanel($db);
|
||||||
|
$panel->fk_anlage = $anlageId;
|
||||||
|
$panel->label = $label ?: 'Feld';
|
||||||
|
|
||||||
|
$result = $panel->create($user);
|
||||||
|
if ($result > 0) {
|
||||||
|
$response['success'] = true;
|
||||||
|
$response['panel_id'] = $result;
|
||||||
|
} else {
|
||||||
|
$response['error'] = $panel->error ?: 'Fehler beim Anlegen';
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// CREATE CARRIER
|
||||||
|
// ============================================
|
||||||
|
case 'create_carrier':
|
||||||
|
if (!$user->hasRight('kundenkarte', 'write')) {
|
||||||
|
$response['error'] = 'Keine Schreibberechtigung';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
$panelId = GETPOSTINT('panel_id');
|
||||||
|
$totalTe = GETPOSTINT('total_te') ?: 12;
|
||||||
|
$label = GETPOST('label', 'alphanohtml');
|
||||||
|
|
||||||
|
if ($panelId <= 0) {
|
||||||
|
$response['error'] = 'Keine Panel-ID';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
$carrier = new EquipmentCarrier($db);
|
||||||
|
$carrier->fk_panel = $panelId;
|
||||||
|
$carrier->label = $label ?: 'Hutschiene';
|
||||||
|
$carrier->total_te = $totalTe;
|
||||||
|
|
||||||
|
$result = $carrier->create($user);
|
||||||
|
if ($result > 0) {
|
||||||
|
$response['success'] = true;
|
||||||
|
$response['carrier_id'] = $result;
|
||||||
|
} else {
|
||||||
|
$response['error'] = $carrier->error ?: 'Fehler beim Anlegen';
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// CREATE EQUIPMENT
|
||||||
|
// ============================================
|
||||||
|
case 'create_equipment':
|
||||||
|
if (!$user->hasRight('kundenkarte', 'write')) {
|
||||||
|
$response['error'] = 'Keine Schreibberechtigung';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
$carrierId = GETPOSTINT('carrier_id');
|
||||||
|
$typeId = GETPOSTINT('type_id');
|
||||||
|
$label = GETPOST('label', 'alphanohtml');
|
||||||
|
$positionTe = GETPOSTINT('position_te') ?: 1;
|
||||||
|
$fieldValues = GETPOST('field_values', 'nohtml');
|
||||||
|
|
||||||
|
if ($carrierId <= 0 || $typeId <= 0) {
|
||||||
|
$response['error'] = 'Carrier-ID und Typ-ID erforderlich';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load type for width
|
||||||
|
$eqType = new EquipmentType($db);
|
||||||
|
$eqType->fetch($typeId);
|
||||||
|
|
||||||
|
$equipment = new Equipment($db);
|
||||||
|
$equipment->fk_carrier = $carrierId;
|
||||||
|
$equipment->fk_equipment_type = $typeId;
|
||||||
|
$equipment->label = $label;
|
||||||
|
$equipment->position_te = $positionTe;
|
||||||
|
$equipment->width_te = $eqType->width_te ?: 1;
|
||||||
|
$equipment->field_values = $fieldValues;
|
||||||
|
|
||||||
|
$result = $equipment->create($user);
|
||||||
|
if ($result > 0) {
|
||||||
|
$response['success'] = true;
|
||||||
|
$response['equipment_id'] = $result;
|
||||||
|
} else {
|
||||||
|
$response['error'] = $equipment->error ?: 'Fehler beim Anlegen';
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
$response['error'] = 'Unbekannte Aktion: ' . $action;
|
||||||
|
}
|
||||||
|
|
||||||
|
echo json_encode($response);
|
||||||
|
$db->close();
|
||||||
107
ajax/tree_config.php
Executable file
107
ajax/tree_config.php
Executable file
|
|
@ -0,0 +1,107 @@
|
||||||
|
<?php
|
||||||
|
/* Copyright (C) 2026 Alles Watt lauft
|
||||||
|
*
|
||||||
|
* AJAX endpoint for tree display configuration
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (!defined('NOTOKENRENEWAL')) define('NOTOKENRENEWAL', '1');
|
||||||
|
if (!defined('NOREQUIREMENU')) define('NOREQUIREMENU', '1');
|
||||||
|
if (!defined('NOREQUIREHTML')) define('NOREQUIREHTML', '1');
|
||||||
|
if (!defined('NOREQUIREAJAX')) define('NOREQUIREAJAX', '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");
|
||||||
|
|
||||||
|
header('Content-Type: application/json; charset=UTF-8');
|
||||||
|
|
||||||
|
$langs->loadLangs(array('kundenkarte@kundenkarte'));
|
||||||
|
|
||||||
|
$action = GETPOST('action', 'aZ09');
|
||||||
|
$systemId = GETPOSTINT('system_id');
|
||||||
|
|
||||||
|
$response = array('success' => false, 'error' => '');
|
||||||
|
|
||||||
|
// Security check
|
||||||
|
if (!$user->hasRight('kundenkarte', 'read')) {
|
||||||
|
$response['error'] = $langs->trans('ErrorPermissionDenied');
|
||||||
|
echo json_encode($response);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch ($action) {
|
||||||
|
case 'get':
|
||||||
|
// Get tree config for a system
|
||||||
|
$defaultConfig = array(
|
||||||
|
'show_ref' => true,
|
||||||
|
'show_label' => true,
|
||||||
|
'show_type' => true,
|
||||||
|
'show_icon' => true,
|
||||||
|
'show_status' => true,
|
||||||
|
'show_fields' => false,
|
||||||
|
'expand_default' => true,
|
||||||
|
'indent_style' => 'lines'
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($systemId > 0) {
|
||||||
|
$sql = "SELECT tree_display_config FROM ".MAIN_DB_PREFIX."c_kundenkarte_anlage_system WHERE rowid = ".(int)$systemId;
|
||||||
|
$resql = $db->query($sql);
|
||||||
|
if ($resql && $obj = $db->fetch_object($resql)) {
|
||||||
|
if (!empty($obj->tree_display_config)) {
|
||||||
|
$savedConfig = json_decode($obj->tree_display_config, true);
|
||||||
|
if (is_array($savedConfig)) {
|
||||||
|
$defaultConfig = array_merge($defaultConfig, $savedConfig);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$response['success'] = true;
|
||||||
|
$response['config'] = $defaultConfig;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'list':
|
||||||
|
// Get all system configs
|
||||||
|
$sql = "SELECT rowid, code, label, tree_display_config FROM ".MAIN_DB_PREFIX."c_kundenkarte_anlage_system WHERE active = 1 ORDER BY position";
|
||||||
|
$resql = $db->query($sql);
|
||||||
|
|
||||||
|
$configs = array();
|
||||||
|
if ($resql) {
|
||||||
|
while ($obj = $db->fetch_object($resql)) {
|
||||||
|
$config = array(
|
||||||
|
'show_ref' => true,
|
||||||
|
'show_label' => true,
|
||||||
|
'show_type' => true,
|
||||||
|
'show_icon' => true,
|
||||||
|
'show_status' => true,
|
||||||
|
'show_fields' => false,
|
||||||
|
'expand_default' => true,
|
||||||
|
'indent_style' => 'lines'
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!empty($obj->tree_display_config)) {
|
||||||
|
$savedConfig = json_decode($obj->tree_display_config, true);
|
||||||
|
if (is_array($savedConfig)) {
|
||||||
|
$config = array_merge($config, $savedConfig);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$configs[$obj->rowid] = array(
|
||||||
|
'id' => $obj->rowid,
|
||||||
|
'code' => $obj->code,
|
||||||
|
'label' => $obj->label,
|
||||||
|
'config' => $config
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$response['success'] = true;
|
||||||
|
$response['systems'] = $configs;
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
$response['error'] = 'Unknown action';
|
||||||
|
}
|
||||||
|
|
||||||
|
echo json_encode($response);
|
||||||
61
ajax/type_fields.php
Executable file
61
ajax/type_fields.php
Executable file
|
|
@ -0,0 +1,61 @@
|
||||||
|
<?php
|
||||||
|
/* Copyright (C) 2026 Alles Watt lauft
|
||||||
|
*
|
||||||
|
* AJAX endpoint to get fields for an anlage type
|
||||||
|
*/
|
||||||
|
|
||||||
|
$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");
|
||||||
|
|
||||||
|
require_once DOL_DOCUMENT_ROOT.'/core/class/html.form.class.php';
|
||||||
|
dol_include_once('/kundenkarte/class/anlagetype.class.php');
|
||||||
|
dol_include_once('/kundenkarte/class/anlage.class.php');
|
||||||
|
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
$typeId = GETPOSTINT('type_id');
|
||||||
|
$anlageId = GETPOSTINT('anlage_id');
|
||||||
|
|
||||||
|
if (empty($typeId)) {
|
||||||
|
echo json_encode(array('fields' => array()));
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$type = new AnlageType($db);
|
||||||
|
if ($type->fetch($typeId) <= 0) {
|
||||||
|
echo json_encode(array('fields' => array()));
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$fields = $type->fetchFields();
|
||||||
|
|
||||||
|
// Get existing values if editing
|
||||||
|
$existingValues = array();
|
||||||
|
if ($anlageId > 0) {
|
||||||
|
$anlage = new Anlage($db);
|
||||||
|
if ($anlage->fetch($anlageId) > 0) {
|
||||||
|
$existingValues = $anlage->getFieldValues();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = array('fields' => array());
|
||||||
|
|
||||||
|
foreach ($fields as $field) {
|
||||||
|
$fieldData = array(
|
||||||
|
'code' => $field->field_code,
|
||||||
|
'label' => $field->field_label,
|
||||||
|
'type' => $field->field_type,
|
||||||
|
'options' => $field->field_options,
|
||||||
|
'required' => (int)$field->required === 1,
|
||||||
|
'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);
|
||||||
306
anlage_connection.php
Executable file
306
anlage_connection.php
Executable file
|
|
@ -0,0 +1,306 @@
|
||||||
|
<?php
|
||||||
|
/* Copyright (C) 2026 Alles Watt lauft
|
||||||
|
*
|
||||||
|
* Edit page for Anlage Connection (cable/wire between elements)
|
||||||
|
*/
|
||||||
|
|
||||||
|
$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");
|
||||||
|
|
||||||
|
require_once DOL_DOCUMENT_ROOT.'/core/class/html.form.class.php';
|
||||||
|
dol_include_once('/kundenkarte/class/anlageconnection.class.php');
|
||||||
|
dol_include_once('/kundenkarte/class/anlage.class.php');
|
||||||
|
dol_include_once('/kundenkarte/class/mediumtype.class.php');
|
||||||
|
|
||||||
|
$langs->loadLangs(array('kundenkarte@kundenkarte'));
|
||||||
|
|
||||||
|
$id = GETPOSTINT('id');
|
||||||
|
$socId = GETPOSTINT('socid');
|
||||||
|
$contactId = GETPOSTINT('contactid');
|
||||||
|
$systemId = GETPOSTINT('system_id');
|
||||||
|
$sourceId = GETPOSTINT('source_id');
|
||||||
|
$action = GETPOST('action', 'aZ09');
|
||||||
|
|
||||||
|
// Security check
|
||||||
|
if (!$user->hasRight('kundenkarte', 'read')) {
|
||||||
|
accessforbidden();
|
||||||
|
}
|
||||||
|
|
||||||
|
$connection = new AnlageConnection($db);
|
||||||
|
$anlage = new Anlage($db);
|
||||||
|
$form = new Form($db);
|
||||||
|
|
||||||
|
// Load existing connection
|
||||||
|
if ($id > 0) {
|
||||||
|
$result = $connection->fetch($id);
|
||||||
|
if ($result <= 0) {
|
||||||
|
setEventMessages($langs->trans('ErrorRecordNotFound'), null, 'errors');
|
||||||
|
header('Location: '.DOL_URL_ROOT.'/societe/card.php?socid='.$socId);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
// Get socId from source anlage if not provided
|
||||||
|
if (empty($socId)) {
|
||||||
|
$tmpAnlage = new Anlage($db);
|
||||||
|
if ($tmpAnlage->fetch($connection->fk_source) > 0) {
|
||||||
|
$socId = $tmpAnlage->fk_soc;
|
||||||
|
$systemId = $tmpAnlage->fk_system;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Redirect-URL: zurück zur Kontakt- oder Kunden-Anlagenansicht
|
||||||
|
if ($contactId > 0) {
|
||||||
|
$backUrl = dol_buildpath('/kundenkarte/tabs/contact_anlagen.php', 1).'?id='.$contactId.'&system='.$systemId;
|
||||||
|
} else {
|
||||||
|
$backUrl = dol_buildpath('/kundenkarte/tabs/anlagen.php', 1).'?id='.$socId.'&system='.$systemId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Actions
|
||||||
|
*/
|
||||||
|
|
||||||
|
if ($action == 'update' && $user->hasRight('kundenkarte', 'write')) {
|
||||||
|
$connection->fk_source = GETPOSTINT('fk_source');
|
||||||
|
$connection->fk_target = GETPOSTINT('fk_target');
|
||||||
|
$connection->label = GETPOST('label', 'alphanohtml');
|
||||||
|
$connection->fk_medium_type = GETPOSTINT('fk_medium_type');
|
||||||
|
$connection->medium_type_text = GETPOST('medium_type_text', 'alphanohtml');
|
||||||
|
$connection->medium_spec = GETPOST('medium_spec', 'alphanohtml');
|
||||||
|
$connection->medium_length = GETPOST('medium_length', 'alphanohtml');
|
||||||
|
$connection->medium_color = GETPOST('medium_color', 'alphanohtml');
|
||||||
|
$connection->route_description = GETPOST('route_description', 'restricthtml');
|
||||||
|
$connection->installation_date = GETPOST('installation_date', 'alpha');
|
||||||
|
|
||||||
|
if (empty($connection->fk_source) || empty($connection->fk_target)) {
|
||||||
|
setEventMessages($langs->trans('ErrorFieldRequired', 'Quelle/Ziel'), null, 'errors');
|
||||||
|
} else {
|
||||||
|
$result = $connection->update($user);
|
||||||
|
if ($result > 0) {
|
||||||
|
setEventMessages($langs->trans('RecordSaved'), null, 'mesgs');
|
||||||
|
header('Location: '.$backUrl);
|
||||||
|
exit;
|
||||||
|
} else {
|
||||||
|
setEventMessages($connection->error, null, 'errors');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($action == 'add' && $user->hasRight('kundenkarte', 'write')) {
|
||||||
|
$connection->fk_source = GETPOSTINT('fk_source');
|
||||||
|
$connection->fk_target = GETPOSTINT('fk_target');
|
||||||
|
$connection->label = GETPOST('label', 'alphanohtml');
|
||||||
|
$connection->fk_medium_type = GETPOSTINT('fk_medium_type');
|
||||||
|
$connection->medium_type_text = GETPOST('medium_type_text', 'alphanohtml');
|
||||||
|
$connection->medium_spec = GETPOST('medium_spec', 'alphanohtml');
|
||||||
|
$connection->medium_length = GETPOST('medium_length', 'alphanohtml');
|
||||||
|
$connection->medium_color = GETPOST('medium_color', 'alphanohtml');
|
||||||
|
$connection->route_description = GETPOST('route_description', 'restricthtml');
|
||||||
|
$connection->installation_date = GETPOST('installation_date', 'alpha');
|
||||||
|
$connection->status = 1;
|
||||||
|
|
||||||
|
if (empty($connection->fk_source) || empty($connection->fk_target)) {
|
||||||
|
setEventMessages($langs->trans('ErrorFieldRequired', 'Quelle/Ziel'), null, 'errors');
|
||||||
|
} else {
|
||||||
|
$result = $connection->create($user);
|
||||||
|
if ($result > 0) {
|
||||||
|
setEventMessages($langs->trans('RecordSaved'), null, 'mesgs');
|
||||||
|
header('Location: '.$backUrl);
|
||||||
|
exit;
|
||||||
|
} else {
|
||||||
|
setEventMessages($connection->error, null, 'errors');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($action == 'delete' && $user->hasRight('kundenkarte', 'write')) {
|
||||||
|
$result = $connection->delete($user);
|
||||||
|
if ($result > 0) {
|
||||||
|
setEventMessages($langs->trans('RecordDeleted'), null, 'mesgs');
|
||||||
|
header('Location: '.$backUrl);
|
||||||
|
exit;
|
||||||
|
} else {
|
||||||
|
setEventMessages($connection->error, null, 'errors');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* View
|
||||||
|
*/
|
||||||
|
|
||||||
|
$title = $id > 0 ? 'Verbindung bearbeiten' : 'Neue Verbindung';
|
||||||
|
llxHeader('', $title);
|
||||||
|
|
||||||
|
// Gebäude-Typ-IDs ermitteln (Verbindungen nur zwischen Geräten, nicht Gebäuden)
|
||||||
|
$buildingTypeIds = array();
|
||||||
|
$sqlBt = "SELECT t.rowid FROM ".MAIN_DB_PREFIX."kundenkarte_anlage_type t";
|
||||||
|
$sqlBt .= " LEFT JOIN ".MAIN_DB_PREFIX."c_kundenkarte_anlage_system ts ON t.fk_system = ts.rowid";
|
||||||
|
$sqlBt .= " WHERE ts.code = 'GLOBAL'";
|
||||||
|
$resBt = $db->query($sqlBt);
|
||||||
|
if ($resBt) {
|
||||||
|
while ($btObj = $db->fetch_object($resBt)) {
|
||||||
|
$buildingTypeIds[] = (int) $btObj->rowid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Alle Elemente für Dropdowns laden (OHNE System-Filter, da Kabel systemübergreifend sein können)
|
||||||
|
$anlagenList = array();
|
||||||
|
if ($socId > 0) {
|
||||||
|
if ($contactId > 0) {
|
||||||
|
$tree = $anlage->fetchTreeByContact($socId, $contactId, 0);
|
||||||
|
} else {
|
||||||
|
$tree = $anlage->fetchTree($socId, 0);
|
||||||
|
}
|
||||||
|
// Baum flach machen - nur Geräte, Gebäude als Pfad-Kontext
|
||||||
|
$flattenTree = function($nodes, $path = '') use (&$flattenTree, &$anlagenList, &$buildingTypeIds) {
|
||||||
|
foreach ($nodes as $node) {
|
||||||
|
$isBuilding = in_array((int) $node->fk_anlage_type, $buildingTypeIds);
|
||||||
|
|
||||||
|
if ($isBuilding) {
|
||||||
|
// Gebäude/Raum: nicht wählbar, aber Pfad als Kontext weitergeben
|
||||||
|
$newPath = $path ? $path.' > '.$node->label : $node->label;
|
||||||
|
if (!empty($node->children)) {
|
||||||
|
$flattenTree($node->children, $newPath);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Gerät: in Liste aufnehmen mit Gebäude-Pfad als Kontext
|
||||||
|
$typeInfo = !empty($node->type_short) ? $node->type_short : (!empty($node->type_label) ? $node->type_label : '');
|
||||||
|
$label = '';
|
||||||
|
if (!empty($path)) {
|
||||||
|
$label = $path.' > ';
|
||||||
|
}
|
||||||
|
$label .= $node->label;
|
||||||
|
if (!empty($typeInfo)) {
|
||||||
|
$label .= ' ['.$typeInfo.']';
|
||||||
|
}
|
||||||
|
$anlagenList[$node->id] = array(
|
||||||
|
'label' => $label,
|
||||||
|
'picto' => !empty($node->type_picto) ? $node->type_picto : 'fa-cube',
|
||||||
|
);
|
||||||
|
// Rekursion in Geräte-Kinder
|
||||||
|
if (!empty($node->children)) {
|
||||||
|
$flattenTree($node->children, $path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
$flattenTree($tree);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load medium types
|
||||||
|
$mediumTypes = array();
|
||||||
|
$sql = "SELECT rowid, label, category FROM ".MAIN_DB_PREFIX."kundenkarte_medium_type WHERE active = 1 ORDER BY category, label";
|
||||||
|
$resql = $db->query($sql);
|
||||||
|
if ($resql) {
|
||||||
|
while ($obj = $db->fetch_object($resql)) {
|
||||||
|
$mediumTypes[$obj->rowid] = $obj->label;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
print '<form method="POST" action="'.$_SERVER['PHP_SELF'].'">';
|
||||||
|
print '<input type="hidden" name="token" value="'.newToken().'">';
|
||||||
|
print '<input type="hidden" name="id" value="'.$id.'">';
|
||||||
|
print '<input type="hidden" name="socid" value="'.$socId.'">';
|
||||||
|
print '<input type="hidden" name="contactid" value="'.$contactId.'">';
|
||||||
|
print '<input type="hidden" name="system_id" value="'.$systemId.'">';
|
||||||
|
print '<input type="hidden" name="action" value="'.($id > 0 ? 'update' : 'add').'">';
|
||||||
|
|
||||||
|
print load_fiche_titre($title, '', 'object_kundenkarte@kundenkarte');
|
||||||
|
|
||||||
|
print '<table class="border centpercent">';
|
||||||
|
|
||||||
|
// Source
|
||||||
|
print '<tr><td class="titlefield fieldrequired">'.$langs->trans('Von (Quelle)').'</td>';
|
||||||
|
print '<td><select name="fk_source" id="fk_source" class="flat minwidth300">';
|
||||||
|
print '<option value="">-- Quelle wählen --</option>';
|
||||||
|
foreach ($anlagenList as $aid => $ainfo) {
|
||||||
|
$selected = ($connection->fk_source == $aid || $sourceId == $aid) ? ' selected' : '';
|
||||||
|
print '<option value="'.$aid.'" data-picto="'.dol_escape_htmltag($ainfo['picto']).'"'.$selected.'>'.dol_escape_htmltag($ainfo['label']).'</option>';
|
||||||
|
}
|
||||||
|
print '</select></td></tr>';
|
||||||
|
|
||||||
|
// Target
|
||||||
|
print '<tr><td class="fieldrequired">'.$langs->trans('Nach (Ziel)').'</td>';
|
||||||
|
print '<td><select name="fk_target" id="fk_target" class="flat minwidth300">';
|
||||||
|
print '<option value="">-- Ziel wählen --</option>';
|
||||||
|
foreach ($anlagenList as $aid => $ainfo) {
|
||||||
|
$selected = ($connection->fk_target == $aid) ? ' selected' : '';
|
||||||
|
print '<option value="'.$aid.'" data-picto="'.dol_escape_htmltag($ainfo['picto']).'"'.$selected.'>'.dol_escape_htmltag($ainfo['label']).'</option>';
|
||||||
|
}
|
||||||
|
print '</select></td></tr>';
|
||||||
|
|
||||||
|
// Medium type
|
||||||
|
print '<tr><td>'.$langs->trans('Kabeltyp').'</td>';
|
||||||
|
print '<td><select name="fk_medium_type" class="flat minwidth200">';
|
||||||
|
print '<option value="">-- oder Freitext unten --</option>';
|
||||||
|
foreach ($mediumTypes as $mid => $mlabel) {
|
||||||
|
$selected = ($connection->fk_medium_type == $mid) ? ' selected' : '';
|
||||||
|
print '<option value="'.$mid.'"'.$selected.'>'.dol_escape_htmltag($mlabel).'</option>';
|
||||||
|
}
|
||||||
|
print '</select></td></tr>';
|
||||||
|
|
||||||
|
// Medium type text (free text)
|
||||||
|
print '<tr><td>'.$langs->trans('Kabeltyp (Freitext)').'</td>';
|
||||||
|
print '<td><input type="text" name="medium_type_text" class="flat minwidth300" value="'.dol_escape_htmltag($connection->medium_type_text).'" placeholder="z.B. NYM-J"></td></tr>';
|
||||||
|
|
||||||
|
// Medium spec
|
||||||
|
print '<tr><td>'.$langs->trans('Querschnitt/Typ').'</td>';
|
||||||
|
print '<td><input type="text" name="medium_spec" class="flat minwidth200" value="'.dol_escape_htmltag($connection->medium_spec).'" placeholder="z.B. 5x2,5mm²"></td></tr>';
|
||||||
|
|
||||||
|
// Length
|
||||||
|
print '<tr><td>'.$langs->trans('Länge').'</td>';
|
||||||
|
print '<td><input type="text" name="medium_length" class="flat minwidth150" value="'.dol_escape_htmltag($connection->medium_length).'" placeholder="z.B. 15m"></td></tr>';
|
||||||
|
|
||||||
|
// Color
|
||||||
|
print '<tr><td>'.$langs->trans('Farbe').'</td>';
|
||||||
|
print '<td><input type="text" name="medium_color" class="flat minwidth150" value="'.dol_escape_htmltag($connection->medium_color).'" placeholder="z.B. grau"></td></tr>';
|
||||||
|
|
||||||
|
// Label
|
||||||
|
print '<tr><td>'.$langs->trans('Bezeichnung').'</td>';
|
||||||
|
print '<td><input type="text" name="label" class="flat minwidth300" value="'.dol_escape_htmltag($connection->label).'" placeholder="z.B. Zuleitung HAK"></td></tr>';
|
||||||
|
|
||||||
|
// Route description
|
||||||
|
print '<tr><td>'.$langs->trans('Verlegungsweg').'</td>';
|
||||||
|
print '<td><textarea name="route_description" class="flat" rows="3" style="width:90%;">'.dol_escape_htmltag($connection->route_description).'</textarea></td></tr>';
|
||||||
|
|
||||||
|
// Installation date
|
||||||
|
print '<tr><td>'.$langs->trans('Installationsdatum').'</td>';
|
||||||
|
print '<td><input type="date" name="installation_date" class="flat" value="'.dol_escape_htmltag($connection->installation_date).'"></td></tr>';
|
||||||
|
|
||||||
|
print '</table>';
|
||||||
|
|
||||||
|
print '<div class="center" style="margin-top:20px;">';
|
||||||
|
print '<button type="submit" class="button button-save">'.$langs->trans('Save').'</button>';
|
||||||
|
print ' <a class="button button-cancel" href="'.$backUrl.'">'.$langs->trans('Cancel').'</a>';
|
||||||
|
|
||||||
|
if ($id > 0 && $user->hasRight('kundenkarte', 'write')) {
|
||||||
|
print ' <a class="button button-delete" style="margin-left:20px;" href="'.$_SERVER['PHP_SELF'].'?id='.$id.'&socid='.$socId.'&contactid='.$contactId.'&system_id='.$systemId.'&action=delete&token='.newToken().'" onclick="return confirm(\'Verbindung wirklich löschen?\');">'.$langs->trans('Delete').'</a>';
|
||||||
|
}
|
||||||
|
print '</div>';
|
||||||
|
|
||||||
|
print '</form>';
|
||||||
|
|
||||||
|
// Select2 mit Icons für Quelle/Ziel-Dropdowns
|
||||||
|
print '<script>
|
||||||
|
$(document).ready(function() {
|
||||||
|
function formatAnlageOption(option) {
|
||||||
|
if (!option.id) return option.text;
|
||||||
|
var picto = $(option.element).data("picto");
|
||||||
|
if (picto) {
|
||||||
|
return $("<span><i class=\"fa " + picto + "\" style=\"width:20px;margin-right:8px;text-align:center;color:#666;\"></i>" + $("<span>").text(option.text).html() + "</span>");
|
||||||
|
}
|
||||||
|
return option.text;
|
||||||
|
}
|
||||||
|
$("#fk_source, #fk_target").select2({
|
||||||
|
templateResult: formatAnlageOption,
|
||||||
|
templateSelection: formatAnlageOption,
|
||||||
|
width: "100%",
|
||||||
|
placeholder: "-- Gerät wählen --",
|
||||||
|
allowClear: true
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>';
|
||||||
|
|
||||||
|
llxFooter();
|
||||||
|
$db->close();
|
||||||
316
build/buildzip.php
Executable file
316
build/buildzip.php
Executable file
|
|
@ -0,0 +1,316 @@
|
||||||
|
#!/usr/bin/env php -d memory_limit=256M
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* buildzip.php
|
||||||
|
*
|
||||||
|
* Copyright (c) 2023-2025 Eric Seigne <eric.seigne@cap-rel.fr>
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
The goal of that php CLI script is to make zip package of your module
|
||||||
|
as an alternative to web "build zip" or "perl script makepack"
|
||||||
|
*/
|
||||||
|
|
||||||
|
// ============================================= configuration
|
||||||
|
|
||||||
|
/**
|
||||||
|
* list of files & dirs of your module
|
||||||
|
*
|
||||||
|
* @var string[]
|
||||||
|
*/
|
||||||
|
$listOfModuleContent = [
|
||||||
|
'admin',
|
||||||
|
'ajax',
|
||||||
|
'backport',
|
||||||
|
'class',
|
||||||
|
'css',
|
||||||
|
'COPYING',
|
||||||
|
'core',
|
||||||
|
'img',
|
||||||
|
'js',
|
||||||
|
'langs',
|
||||||
|
'lib',
|
||||||
|
'sql',
|
||||||
|
'tpl',
|
||||||
|
'*.md',
|
||||||
|
'*.json',
|
||||||
|
'*.php',
|
||||||
|
'modulebuilder.txt',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* if you want to exclude some files from your zip
|
||||||
|
*
|
||||||
|
* @var string[]
|
||||||
|
*/
|
||||||
|
$exclude_list = [
|
||||||
|
'/^.git$/',
|
||||||
|
'/.*js.map/',
|
||||||
|
'/DEV.md/'
|
||||||
|
];
|
||||||
|
|
||||||
|
// ============================================= end of configuration
|
||||||
|
|
||||||
|
/**
|
||||||
|
* auto detect module name and version from file name
|
||||||
|
*
|
||||||
|
* @return (string|string)[] module name and module version
|
||||||
|
*/
|
||||||
|
function detectModule()
|
||||||
|
{
|
||||||
|
$name = $version = "";
|
||||||
|
$tab = glob("core/modules/mod*.class.php");
|
||||||
|
if (count($tab) == 0) {
|
||||||
|
echo "[fail] Error on auto detect data : there is no mod*.class.php file into core/modules dir\n";
|
||||||
|
exit(-1);
|
||||||
|
}
|
||||||
|
if (count($tab) == 1) {
|
||||||
|
$file = $tab[0];
|
||||||
|
$pattern = "/.*mod(?<mod>.*)\.class\.php/";
|
||||||
|
if (preg_match_all($pattern, $file, $matches)) {
|
||||||
|
$name = strtolower(reset($matches['mod']));
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "extract data from $file\n";
|
||||||
|
if (!file_exists($file) || $name == "") {
|
||||||
|
echo "[fail] Error on auto detect data\n";
|
||||||
|
exit(-2);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
echo "[fail] Error there is more than one mod*.class.php file into core/modules dir\n";
|
||||||
|
exit(-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
//extract version from file
|
||||||
|
$contents = file_get_contents($file);
|
||||||
|
$pattern = "/^.*this->version\s*=\s*'(?<version>.*)'\s*;.*\$/m";
|
||||||
|
|
||||||
|
// search, and store all matching occurrences in $matches
|
||||||
|
if (preg_match_all($pattern, $contents, $matches)) {
|
||||||
|
$version = reset($matches['version']);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (version_compare($version, '0.0.1', '>=') != 1) {
|
||||||
|
echo "[fail] Error auto extract version fail\n";
|
||||||
|
exit(-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "module name = $name, version = $version\n";
|
||||||
|
return [(string) $name, (string) $version];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* delete recursively a directory
|
||||||
|
*
|
||||||
|
* @param string $dir dir path to delete
|
||||||
|
*
|
||||||
|
* @return bool true on success or false on failure.
|
||||||
|
*/
|
||||||
|
function delTree($dir)
|
||||||
|
{
|
||||||
|
$files = array_diff(scandir($dir), array('.', '..'));
|
||||||
|
foreach ($files as $file) {
|
||||||
|
(is_dir("$dir/$file")) ? delTree("$dir/$file") : secureUnlink("$dir/$file");
|
||||||
|
}
|
||||||
|
return rmdir($dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* do a secure delete file/dir with double check
|
||||||
|
* (don't trust unlink return)
|
||||||
|
*
|
||||||
|
* @param string $path full path to delete
|
||||||
|
*
|
||||||
|
* @return bool true on success ($path does not exists at the end of process), else exit
|
||||||
|
*/
|
||||||
|
function secureUnlink($path)
|
||||||
|
{
|
||||||
|
if (file_exists($path)) {
|
||||||
|
if (unlink($path)) {
|
||||||
|
//then check if really deleted
|
||||||
|
clearstatcache();
|
||||||
|
if (file_exists($path)) { // @phpstan-ignore-line
|
||||||
|
echo "[fail] unlink of $path fail !\n";
|
||||||
|
exit(-5);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
echo "[fail] unlink of $path fail !\n";
|
||||||
|
exit(-6);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* create a directory and check if dir exists
|
||||||
|
*
|
||||||
|
* @param string $path path to make
|
||||||
|
*
|
||||||
|
* @return bool true on success ($path exists at the end of process), else exit
|
||||||
|
*/
|
||||||
|
function mkdirAndCheck($path)
|
||||||
|
{
|
||||||
|
if (mkdir($path)) {
|
||||||
|
clearstatcache();
|
||||||
|
if (is_dir($path)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
echo "[fail] Error on $path (mkdir)\n";
|
||||||
|
exit(7);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* check if that filename is concerned by exclude filter
|
||||||
|
*
|
||||||
|
* @param string $filename file name to check
|
||||||
|
*
|
||||||
|
* @return bool true if file is in excluded list
|
||||||
|
*/
|
||||||
|
function is_excluded($filename)
|
||||||
|
{
|
||||||
|
global $exclude_list;
|
||||||
|
$count = 0;
|
||||||
|
$notused = preg_filter($exclude_list, '1', $filename, -1, $count);
|
||||||
|
if ($count > 0) {
|
||||||
|
echo " - exclude $filename\n";
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* recursive copy files & dirs
|
||||||
|
*
|
||||||
|
* @param string $src source dir
|
||||||
|
* @param string $dst target dir
|
||||||
|
*
|
||||||
|
* @return bool true on success or false on failure.
|
||||||
|
*/
|
||||||
|
function rcopy($src, $dst)
|
||||||
|
{
|
||||||
|
if (is_dir($src)) {
|
||||||
|
// Make the destination directory if not exist
|
||||||
|
mkdirAndCheck($dst);
|
||||||
|
// open the source directory
|
||||||
|
$dir = opendir($src);
|
||||||
|
|
||||||
|
// Loop through the files in source directory
|
||||||
|
while ($file = readdir($dir)) {
|
||||||
|
if (($file != '.') && ($file != '..')) {
|
||||||
|
if (is_dir($src . '/' . $file)) {
|
||||||
|
// Recursively calling custom copy function
|
||||||
|
// for sub directory
|
||||||
|
if (!rcopy($src . '/' . $file, $dst . '/' . $file)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (!is_excluded($file)) {
|
||||||
|
if (!copy($src . '/' . $file, $dst . '/' . $file)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
closedir($dir);
|
||||||
|
} elseif (is_file($src)) {
|
||||||
|
if (!is_excluded($src)) {
|
||||||
|
if (!copy($src, $dst)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* build a zip file with only php code and no external depends
|
||||||
|
* on "zip" exec for example
|
||||||
|
*
|
||||||
|
* @param string $folder folder to use as zip root
|
||||||
|
* @param ZipArchive $zip zip object (ZipArchive)
|
||||||
|
* @param string $root relative root path into the zip
|
||||||
|
*
|
||||||
|
* @return bool true on success or false on failure.
|
||||||
|
*/
|
||||||
|
function zipDir($folder, &$zip, $root = "")
|
||||||
|
{
|
||||||
|
foreach (new \DirectoryIterator($folder) as $f) {
|
||||||
|
if ($f->isDot()) {
|
||||||
|
continue;
|
||||||
|
} //skip . ..
|
||||||
|
$src = $folder . '/' . $f;
|
||||||
|
$dst = substr($f->getPathname(), strlen($root));
|
||||||
|
if ($f->isDir()) {
|
||||||
|
if ($zip->addEmptyDir($dst)) {
|
||||||
|
if (zipDir($src, $zip, $root)) {
|
||||||
|
continue;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($f->isFile()) {
|
||||||
|
if (! $zip->addFile($src, $dst)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* main part of script
|
||||||
|
*/
|
||||||
|
|
||||||
|
list($mod, $version) = detectModule();
|
||||||
|
$outzip = sys_get_temp_dir() . "/module_" . $mod . "-" . $version . ".zip";
|
||||||
|
if (file_exists($outzip)) {
|
||||||
|
secureUnlink($outzip);
|
||||||
|
}
|
||||||
|
|
||||||
|
//copy all sources into system temp directory
|
||||||
|
$tmpdir = tempnam(sys_get_temp_dir(), $mod . "-module");
|
||||||
|
secureUnlink($tmpdir);
|
||||||
|
mkdirAndCheck($tmpdir);
|
||||||
|
$dst = $tmpdir . "/" . $mod;
|
||||||
|
mkdirAndCheck($dst);
|
||||||
|
|
||||||
|
foreach ($listOfModuleContent as $moduleContent) {
|
||||||
|
foreach (glob($moduleContent) as $entry) {
|
||||||
|
if (!rcopy($entry, $dst . '/' . $entry)) {
|
||||||
|
echo "[fail] Error on copy " . $entry . " to " . $dst . "/" . $entry . "\n";
|
||||||
|
echo "Please take time to analyze the problem and fix the bug\n";
|
||||||
|
exit(-8);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$z = new ZipArchive();
|
||||||
|
$z->open($outzip, ZIPARCHIVE::CREATE);
|
||||||
|
zipDir($tmpdir, $z, $tmpdir . '/');
|
||||||
|
$z->close();
|
||||||
|
delTree($tmpdir);
|
||||||
|
if (file_exists($outzip)) {
|
||||||
|
echo "[success] module archive is ready : $outzip ...\n";
|
||||||
|
} else {
|
||||||
|
echo "[fail] build zip error\n";
|
||||||
|
exit(-9);
|
||||||
|
}
|
||||||
11
build/makepack-kundenkarte.conf
Executable file
11
build/makepack-kundenkarte.conf
Executable file
|
|
@ -0,0 +1,11 @@
|
||||||
|
# Your module name here
|
||||||
|
#
|
||||||
|
# Goal: Goal of module
|
||||||
|
# Version: <version>
|
||||||
|
# Author: Copyright <year> - <name of author>
|
||||||
|
# License: GPLv3
|
||||||
|
# Install: Just unpack content of module package in Dolibarr directory.
|
||||||
|
# Setup: Go on Dolibarr setup - modules to enable module.
|
||||||
|
#
|
||||||
|
# Files in module
|
||||||
|
mymodule/
|
||||||
763
class/anlage.class.php
Executable file
763
class/anlage.class.php
Executable file
|
|
@ -0,0 +1,763 @@
|
||||||
|
<?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 Anlage
|
||||||
|
* Manages technical installation elements for customers
|
||||||
|
*/
|
||||||
|
class Anlage extends CommonObject
|
||||||
|
{
|
||||||
|
public $element = 'anlage';
|
||||||
|
public $table_element = 'kundenkarte_anlage';
|
||||||
|
|
||||||
|
public $ref;
|
||||||
|
public $label;
|
||||||
|
public $fk_soc;
|
||||||
|
public $fk_contact;
|
||||||
|
public $fk_anlage_type;
|
||||||
|
public $fk_parent;
|
||||||
|
public $fk_system;
|
||||||
|
public $fk_building_node;
|
||||||
|
|
||||||
|
public $manufacturer;
|
||||||
|
public $model;
|
||||||
|
public $serial_number;
|
||||||
|
public $power_rating;
|
||||||
|
public $field_values;
|
||||||
|
|
||||||
|
public $location;
|
||||||
|
public $installation_date;
|
||||||
|
public $warranty_until;
|
||||||
|
|
||||||
|
public $rang;
|
||||||
|
public $level;
|
||||||
|
public $note_private;
|
||||||
|
public $note_public;
|
||||||
|
public $status;
|
||||||
|
|
||||||
|
public $date_creation;
|
||||||
|
public $fk_user_creat;
|
||||||
|
public $fk_user_modif;
|
||||||
|
|
||||||
|
// Loaded objects
|
||||||
|
public $type;
|
||||||
|
public $children = array();
|
||||||
|
public $files = array();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor
|
||||||
|
*
|
||||||
|
* @param DoliDB $db Database handler
|
||||||
|
*/
|
||||||
|
public function __construct($db)
|
||||||
|
{
|
||||||
|
$this->db = $db;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create object in database
|
||||||
|
*
|
||||||
|
* @param User $user User that creates
|
||||||
|
* @param bool $notrigger false=launch triggers, true=disable triggers
|
||||||
|
* @return int Return integer <0 if KO, Id of created object if OK
|
||||||
|
*/
|
||||||
|
public function create($user, $notrigger = false)
|
||||||
|
{
|
||||||
|
global $conf;
|
||||||
|
|
||||||
|
$error = 0;
|
||||||
|
$now = dol_now();
|
||||||
|
|
||||||
|
// Check parameters
|
||||||
|
if (empty($this->fk_soc) || empty($this->fk_anlage_type) || empty($this->label)) {
|
||||||
|
$this->error = 'ErrorMissingParameters';
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note: Circular reference check not needed on create since element doesn't exist yet
|
||||||
|
|
||||||
|
// Calculate level
|
||||||
|
$this->level = 0;
|
||||||
|
if ($this->fk_parent > 0) {
|
||||||
|
$parent = new Anlage($this->db);
|
||||||
|
if ($parent->fetch($this->fk_parent) > 0) {
|
||||||
|
$this->level = $parent->level + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->db->begin();
|
||||||
|
|
||||||
|
$sql = "INSERT INTO ".MAIN_DB_PREFIX.$this->table_element." (";
|
||||||
|
$sql .= "entity, ref, label, fk_soc, fk_contact, fk_anlage_type, fk_parent, fk_system,";
|
||||||
|
$sql .= " manufacturer, model, serial_number, power_rating, field_values,";
|
||||||
|
$sql .= " location, installation_date, warranty_until,";
|
||||||
|
$sql .= " rang, level, note_private, note_public, status,";
|
||||||
|
$sql .= " date_creation, fk_user_creat";
|
||||||
|
$sql .= ") VALUES (";
|
||||||
|
$sql .= ((int) $conf->entity);
|
||||||
|
$sql .= ", ".($this->ref ? "'".$this->db->escape($this->ref)."'" : "NULL");
|
||||||
|
$sql .= ", '".$this->db->escape($this->label)."'";
|
||||||
|
$sql .= ", ".((int) $this->fk_soc);
|
||||||
|
$sql .= ", ".($this->fk_contact > 0 ? ((int) $this->fk_contact) : "NULL");
|
||||||
|
$sql .= ", ".((int) $this->fk_anlage_type);
|
||||||
|
$sql .= ", ".((int) ($this->fk_parent > 0 ? $this->fk_parent : 0));
|
||||||
|
$sql .= ", ".((int) $this->fk_system);
|
||||||
|
$sql .= ", ".($this->manufacturer ? "'".$this->db->escape($this->manufacturer)."'" : "NULL");
|
||||||
|
$sql .= ", ".($this->model ? "'".$this->db->escape($this->model)."'" : "NULL");
|
||||||
|
$sql .= ", ".($this->serial_number ? "'".$this->db->escape($this->serial_number)."'" : "NULL");
|
||||||
|
$sql .= ", ".($this->power_rating ? "'".$this->db->escape($this->power_rating)."'" : "NULL");
|
||||||
|
$sql .= ", ".($this->field_values ? "'".$this->db->escape($this->field_values)."'" : "NULL");
|
||||||
|
$sql .= ", ".($this->location ? "'".$this->db->escape($this->location)."'" : "NULL");
|
||||||
|
$sql .= ", ".($this->installation_date ? "'".$this->db->idate($this->installation_date)."'" : "NULL");
|
||||||
|
$sql .= ", ".($this->warranty_until ? "'".$this->db->idate($this->warranty_until)."'" : "NULL");
|
||||||
|
$sql .= ", ".((int) $this->rang);
|
||||||
|
$sql .= ", ".((int) $this->level);
|
||||||
|
$sql .= ", ".($this->note_private ? "'".$this->db->escape($this->note_private)."'" : "NULL");
|
||||||
|
$sql .= ", ".($this->note_public ? "'".$this->db->escape($this->note_public)."'" : "NULL");
|
||||||
|
$sql .= ", ".((int) ($this->status !== null ? $this->status : 1));
|
||||||
|
$sql .= ", '".$this->db->idate($now)."'";
|
||||||
|
$sql .= ", ".((int) $user->id);
|
||||||
|
$sql .= ")";
|
||||||
|
|
||||||
|
$resql = $this->db->query($sql);
|
||||||
|
if (!$resql) {
|
||||||
|
$error++;
|
||||||
|
$this->errors[] = "Error ".$this->db->lasterror();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$error) {
|
||||||
|
$this->id = $this->db->last_insert_id(MAIN_DB_PREFIX.$this->table_element);
|
||||||
|
$this->date_creation = $now;
|
||||||
|
$this->fk_user_creat = $user->id;
|
||||||
|
|
||||||
|
// Create directory for files
|
||||||
|
$this->createFileDirectory();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($error) {
|
||||||
|
$this->db->rollback();
|
||||||
|
return -1 * $error;
|
||||||
|
} else {
|
||||||
|
$this->db->commit();
|
||||||
|
return $this->id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load object from database
|
||||||
|
*
|
||||||
|
* @param int $id ID of record
|
||||||
|
* @return int Return integer <0 if KO, 0 if not found, >0 if OK
|
||||||
|
*/
|
||||||
|
public function fetch($id)
|
||||||
|
{
|
||||||
|
global $conf;
|
||||||
|
|
||||||
|
$sql = "SELECT a.*, t.label as type_label, t.label_short as type_short, t.picto as type_picto,";
|
||||||
|
$sql .= " s.label as system_label, s.code as system_code";
|
||||||
|
$sql .= " FROM ".MAIN_DB_PREFIX.$this->table_element." as a";
|
||||||
|
$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."kundenkarte_anlage_type as t ON a.fk_anlage_type = t.rowid";
|
||||||
|
$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."c_kundenkarte_anlage_system as s ON a.fk_system = s.rowid";
|
||||||
|
$sql .= " WHERE a.rowid = ".((int) $id);
|
||||||
|
$sql .= " AND a.entity = ".((int) $conf->entity);
|
||||||
|
|
||||||
|
$resql = $this->db->query($sql);
|
||||||
|
if ($resql) {
|
||||||
|
if ($this->db->num_rows($resql)) {
|
||||||
|
$obj = $this->db->fetch_object($resql);
|
||||||
|
$this->setFromObject($obj);
|
||||||
|
$this->db->free($resql);
|
||||||
|
return 1;
|
||||||
|
} else {
|
||||||
|
$this->db->free($resql);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$this->error = $this->db->lasterror();
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set object properties from database object
|
||||||
|
*
|
||||||
|
* @param object $obj Database object
|
||||||
|
*/
|
||||||
|
private function setFromObject($obj)
|
||||||
|
{
|
||||||
|
$this->id = $obj->rowid;
|
||||||
|
$this->entity = $obj->entity;
|
||||||
|
$this->ref = $obj->ref;
|
||||||
|
$this->label = $obj->label;
|
||||||
|
$this->fk_soc = $obj->fk_soc;
|
||||||
|
$this->fk_contact = isset($obj->fk_contact) ? $obj->fk_contact : null;
|
||||||
|
$this->fk_anlage_type = $obj->fk_anlage_type;
|
||||||
|
$this->fk_parent = $obj->fk_parent;
|
||||||
|
$this->fk_system = $obj->fk_system;
|
||||||
|
$this->fk_building_node = isset($obj->fk_building_node) ? (int) $obj->fk_building_node : 0;
|
||||||
|
|
||||||
|
$this->manufacturer = $obj->manufacturer;
|
||||||
|
$this->model = $obj->model;
|
||||||
|
$this->serial_number = $obj->serial_number;
|
||||||
|
$this->power_rating = $obj->power_rating;
|
||||||
|
$this->field_values = $obj->field_values;
|
||||||
|
|
||||||
|
$this->location = $obj->location;
|
||||||
|
$this->installation_date = $this->db->jdate($obj->installation_date);
|
||||||
|
$this->warranty_until = $this->db->jdate($obj->warranty_until);
|
||||||
|
|
||||||
|
$this->rang = $obj->rang;
|
||||||
|
$this->level = $obj->level;
|
||||||
|
$this->note_private = $obj->note_private;
|
||||||
|
$this->note_public = $obj->note_public;
|
||||||
|
$this->status = $obj->status;
|
||||||
|
|
||||||
|
$this->date_creation = $this->db->jdate($obj->date_creation);
|
||||||
|
$this->tms = $this->db->jdate($obj->tms);
|
||||||
|
$this->fk_user_creat = $obj->fk_user_creat;
|
||||||
|
$this->fk_user_modif = $obj->fk_user_modif;
|
||||||
|
|
||||||
|
// Type info
|
||||||
|
$this->type_label = $obj->type_label;
|
||||||
|
$this->type_short = $obj->type_short;
|
||||||
|
$this->type_picto = $obj->type_picto;
|
||||||
|
|
||||||
|
// System info
|
||||||
|
$this->system_label = $obj->system_label;
|
||||||
|
$this->system_code = $obj->system_code;
|
||||||
|
|
||||||
|
// File counts (from tree query)
|
||||||
|
$this->image_count = isset($obj->image_count) ? (int) $obj->image_count : 0;
|
||||||
|
$this->doc_count = isset($obj->doc_count) ? (int) $obj->doc_count : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update object in database
|
||||||
|
*
|
||||||
|
* @param User $user User that modifies
|
||||||
|
* @param bool $notrigger false=launch triggers, true=disable triggers
|
||||||
|
* @return int Return integer <0 if KO, >0 if OK
|
||||||
|
*/
|
||||||
|
public function update($user, $notrigger = false)
|
||||||
|
{
|
||||||
|
$error = 0;
|
||||||
|
|
||||||
|
// Check for circular reference
|
||||||
|
if ($this->fk_parent > 0 && $this->wouldCreateCircularReference($this->fk_parent)) {
|
||||||
|
$this->error = 'ErrorCircularReference';
|
||||||
|
$this->errors[] = 'Das Element kann nicht unter sich selbst oder einem seiner Unterelemente platziert werden.';
|
||||||
|
return -2;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recalculate level if parent changed
|
||||||
|
$this->level = 0;
|
||||||
|
if ($this->fk_parent > 0) {
|
||||||
|
$parent = new Anlage($this->db);
|
||||||
|
if ($parent->fetch($this->fk_parent) > 0) {
|
||||||
|
$this->level = $parent->level + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->db->begin();
|
||||||
|
|
||||||
|
$sql = "UPDATE ".MAIN_DB_PREFIX.$this->table_element." SET";
|
||||||
|
$sql .= " ref = ".($this->ref ? "'".$this->db->escape($this->ref)."'" : "NULL");
|
||||||
|
$sql .= ", label = '".$this->db->escape($this->label)."'";
|
||||||
|
$sql .= ", fk_anlage_type = ".((int) $this->fk_anlage_type);
|
||||||
|
$sql .= ", fk_parent = ".((int) ($this->fk_parent > 0 ? $this->fk_parent : 0));
|
||||||
|
$sql .= ", fk_system = ".((int) $this->fk_system);
|
||||||
|
$sql .= ", manufacturer = ".($this->manufacturer ? "'".$this->db->escape($this->manufacturer)."'" : "NULL");
|
||||||
|
$sql .= ", model = ".($this->model ? "'".$this->db->escape($this->model)."'" : "NULL");
|
||||||
|
$sql .= ", serial_number = ".($this->serial_number ? "'".$this->db->escape($this->serial_number)."'" : "NULL");
|
||||||
|
$sql .= ", power_rating = ".($this->power_rating ? "'".$this->db->escape($this->power_rating)."'" : "NULL");
|
||||||
|
$sql .= ", field_values = ".($this->field_values ? "'".$this->db->escape($this->field_values)."'" : "NULL");
|
||||||
|
$sql .= ", location = ".($this->location ? "'".$this->db->escape($this->location)."'" : "NULL");
|
||||||
|
$sql .= ", installation_date = ".($this->installation_date ? "'".$this->db->idate($this->installation_date)."'" : "NULL");
|
||||||
|
$sql .= ", warranty_until = ".($this->warranty_until ? "'".$this->db->idate($this->warranty_until)."'" : "NULL");
|
||||||
|
$sql .= ", rang = ".((int) $this->rang);
|
||||||
|
$sql .= ", level = ".((int) $this->level);
|
||||||
|
$sql .= ", note_private = ".($this->note_private ? "'".$this->db->escape($this->note_private)."'" : "NULL");
|
||||||
|
$sql .= ", note_public = ".($this->note_public ? "'".$this->db->escape($this->note_public)."'" : "NULL");
|
||||||
|
$sql .= ", status = ".((int) $this->status);
|
||||||
|
$sql .= ", fk_user_modif = ".((int) $user->id);
|
||||||
|
$sql .= " WHERE rowid = ".((int) $this->id);
|
||||||
|
|
||||||
|
$resql = $this->db->query($sql);
|
||||||
|
if (!$resql) {
|
||||||
|
$error++;
|
||||||
|
$this->errors[] = "Error ".$this->db->lasterror();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($error) {
|
||||||
|
$this->db->rollback();
|
||||||
|
return -1 * $error;
|
||||||
|
} else {
|
||||||
|
$this->db->commit();
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reihenfolge (rang) für mehrere Elemente aktualisieren
|
||||||
|
*
|
||||||
|
* @param array $ids Array von Anlage-IDs in gewünschter Reihenfolge
|
||||||
|
* @return int 1 bei Erfolg, <0 bei Fehler
|
||||||
|
*/
|
||||||
|
public function updateRangs($ids)
|
||||||
|
{
|
||||||
|
$this->db->begin();
|
||||||
|
$error = 0;
|
||||||
|
|
||||||
|
foreach ($ids as $rang => $id) {
|
||||||
|
$sql = "UPDATE ".MAIN_DB_PREFIX."kundenkarte_anlage";
|
||||||
|
$sql .= " SET rang = ".((int) $rang);
|
||||||
|
$sql .= " WHERE rowid = ".((int) $id);
|
||||||
|
if (!$this->db->query($sql)) {
|
||||||
|
$error++;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($error) {
|
||||||
|
$this->db->rollback();
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->db->commit();
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete object in database
|
||||||
|
*
|
||||||
|
* @param User $user User that deletes
|
||||||
|
* @param bool $notrigger false=launch triggers, true=disable triggers
|
||||||
|
* @return int Return integer <0 if KO, >0 if OK
|
||||||
|
*/
|
||||||
|
public function delete($user, $notrigger = false)
|
||||||
|
{
|
||||||
|
$error = 0;
|
||||||
|
|
||||||
|
$this->db->begin();
|
||||||
|
|
||||||
|
// First delete all children recursively
|
||||||
|
$children = $this->fetchChildren($this->id);
|
||||||
|
foreach ($children as $child) {
|
||||||
|
$childObj = new Anlage($this->db);
|
||||||
|
if ($childObj->fetch($child->id) > 0) {
|
||||||
|
$result = $childObj->delete($user, $notrigger);
|
||||||
|
if ($result < 0) {
|
||||||
|
$error++;
|
||||||
|
$this->errors = array_merge($this->errors, $childObj->errors);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$error) {
|
||||||
|
// Delete files
|
||||||
|
$this->deleteFileDirectory();
|
||||||
|
|
||||||
|
// Delete file records
|
||||||
|
$sql = "DELETE FROM ".MAIN_DB_PREFIX."kundenkarte_anlage_files WHERE fk_anlage = ".((int) $this->id);
|
||||||
|
$this->db->query($sql);
|
||||||
|
|
||||||
|
// Delete the element
|
||||||
|
$sql = "DELETE FROM ".MAIN_DB_PREFIX.$this->table_element." WHERE rowid = ".((int) $this->id);
|
||||||
|
$resql = $this->db->query($sql);
|
||||||
|
if (!$resql) {
|
||||||
|
$error++;
|
||||||
|
$this->errors[] = "Error ".$this->db->lasterror();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($error) {
|
||||||
|
$this->db->rollback();
|
||||||
|
return -1 * $error;
|
||||||
|
} else {
|
||||||
|
$this->db->commit();
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch children of an element
|
||||||
|
*
|
||||||
|
* @param int $parentId Parent ID (0 for root elements)
|
||||||
|
* @param int $socid Customer ID (required for root elements)
|
||||||
|
* @param int $systemId System ID (optional filter)
|
||||||
|
* @return array Array of Anlage objects
|
||||||
|
*/
|
||||||
|
public function fetchChildren($parentId = 0, $socid = 0, $systemId = 0)
|
||||||
|
{
|
||||||
|
global $conf;
|
||||||
|
|
||||||
|
$results = array();
|
||||||
|
|
||||||
|
$sql = "SELECT a.*, t.label as type_label, t.label_short as type_short, t.picto as type_picto,";
|
||||||
|
$sql .= " s.label as system_label, s.code as system_code,";
|
||||||
|
// Count images
|
||||||
|
$sql .= " (SELECT COUNT(*) FROM ".MAIN_DB_PREFIX."kundenkarte_anlage_files f WHERE f.fk_anlage = a.rowid AND f.file_type = 'image') as image_count,";
|
||||||
|
// Count documents (pdf + document)
|
||||||
|
$sql .= " (SELECT COUNT(*) FROM ".MAIN_DB_PREFIX."kundenkarte_anlage_files f WHERE f.fk_anlage = a.rowid AND f.file_type IN ('pdf', 'document')) as doc_count";
|
||||||
|
$sql .= " FROM ".MAIN_DB_PREFIX.$this->table_element." as a";
|
||||||
|
$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."kundenkarte_anlage_type as t ON a.fk_anlage_type = t.rowid";
|
||||||
|
$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."c_kundenkarte_anlage_system as s ON a.fk_system = s.rowid";
|
||||||
|
$sql .= " WHERE a.fk_parent = ".((int) $parentId);
|
||||||
|
$sql .= " AND a.entity = ".((int) $conf->entity);
|
||||||
|
$sql .= " AND a.status = 1";
|
||||||
|
|
||||||
|
if ($parentId == 0 && $socid > 0) {
|
||||||
|
$sql .= " AND a.fk_soc = ".((int) $socid);
|
||||||
|
// Only show elements without contact assignment (thirdparty-level)
|
||||||
|
$sql .= " AND (a.fk_contact IS NULL OR a.fk_contact = 0)";
|
||||||
|
}
|
||||||
|
if ($systemId > 0) {
|
||||||
|
$sql .= " AND a.fk_system = ".((int) $systemId);
|
||||||
|
}
|
||||||
|
|
||||||
|
$sql .= " ORDER BY a.rang ASC, a.label ASC";
|
||||||
|
|
||||||
|
$resql = $this->db->query($sql);
|
||||||
|
if ($resql) {
|
||||||
|
while ($obj = $this->db->fetch_object($resql)) {
|
||||||
|
$anlage = new Anlage($this->db);
|
||||||
|
$anlage->setFromObject($obj);
|
||||||
|
$results[] = $anlage;
|
||||||
|
}
|
||||||
|
$this->db->free($resql);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $results;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch full tree for a customer and system
|
||||||
|
*
|
||||||
|
* @param int $socid Customer ID
|
||||||
|
* @param int $systemId System ID
|
||||||
|
* @return array Tree structure
|
||||||
|
*/
|
||||||
|
public function fetchTree($socid, $systemId)
|
||||||
|
{
|
||||||
|
$tree = array();
|
||||||
|
$roots = $this->fetchChildren(0, $socid, $systemId);
|
||||||
|
|
||||||
|
foreach ($roots as $root) {
|
||||||
|
$root->children = $this->fetchChildrenRecursive($root->id);
|
||||||
|
$tree[] = $root;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $tree;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch children recursively
|
||||||
|
*
|
||||||
|
* @param int $parentId Parent ID
|
||||||
|
* @return array Array of Anlage objects with children
|
||||||
|
*/
|
||||||
|
private function fetchChildrenRecursive($parentId)
|
||||||
|
{
|
||||||
|
$children = $this->fetchChildren($parentId);
|
||||||
|
foreach ($children as $child) {
|
||||||
|
$child->children = $this->fetchChildrenRecursive($child->id);
|
||||||
|
}
|
||||||
|
return $children;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the file storage directory path
|
||||||
|
*
|
||||||
|
* @return string Directory path
|
||||||
|
*/
|
||||||
|
public function getFileDirectory()
|
||||||
|
{
|
||||||
|
global $conf;
|
||||||
|
return $conf->kundenkarte->dir_output.'/anlagen/'.$this->fk_soc.'/'.$this->id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create the file directory
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function createFileDirectory()
|
||||||
|
{
|
||||||
|
$dir = $this->getFileDirectory();
|
||||||
|
if (!is_dir($dir)) {
|
||||||
|
return dol_mkdir($dir);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete the file directory
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function deleteFileDirectory()
|
||||||
|
{
|
||||||
|
$dir = $this->getFileDirectory();
|
||||||
|
if (is_dir($dir)) {
|
||||||
|
return dol_delete_dir_recursive($dir);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get decoded field values
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function getFieldValues()
|
||||||
|
{
|
||||||
|
if (empty($this->field_values)) {
|
||||||
|
return array();
|
||||||
|
}
|
||||||
|
$values = json_decode($this->field_values, true);
|
||||||
|
return is_array($values) ? $values : array();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set field values from array
|
||||||
|
*
|
||||||
|
* @param array $values Field values
|
||||||
|
*/
|
||||||
|
public function setFieldValues($values)
|
||||||
|
{
|
||||||
|
$this->field_values = json_encode($values);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a specific field value
|
||||||
|
*
|
||||||
|
* @param string $fieldCode Field code
|
||||||
|
* @return mixed|null
|
||||||
|
*/
|
||||||
|
public function getFieldValue($fieldCode)
|
||||||
|
{
|
||||||
|
$values = $this->getFieldValues();
|
||||||
|
return isset($values[$fieldCode]) ? $values[$fieldCode] : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if setting a parent would create a circular reference
|
||||||
|
*
|
||||||
|
* @param int $newParentId The proposed new parent ID
|
||||||
|
* @return bool True if circular reference would be created, false otherwise
|
||||||
|
*/
|
||||||
|
public function wouldCreateCircularReference($newParentId)
|
||||||
|
{
|
||||||
|
if (empty($this->id) || empty($newParentId)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cannot be own parent
|
||||||
|
if ($newParentId == $this->id) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if newParentId is a descendant of this element
|
||||||
|
return $this->isDescendant($newParentId, $this->id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if an element is a descendant of another
|
||||||
|
*
|
||||||
|
* @param int $elementId Element to check
|
||||||
|
* @param int $ancestorId Potential ancestor
|
||||||
|
* @param int $maxDepth Maximum depth to check (prevent infinite loops)
|
||||||
|
* @return bool True if elementId is a descendant of ancestorId
|
||||||
|
*/
|
||||||
|
private function isDescendant($elementId, $ancestorId, $maxDepth = 50)
|
||||||
|
{
|
||||||
|
if ($maxDepth <= 0) {
|
||||||
|
return true; // Safety: assume circular if too deep
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all children of ancestorId
|
||||||
|
$sql = "SELECT rowid FROM ".MAIN_DB_PREFIX.$this->table_element;
|
||||||
|
$sql .= " WHERE fk_parent = ".((int) $ancestorId);
|
||||||
|
$sql .= " AND status = 1";
|
||||||
|
|
||||||
|
$resql = $this->db->query($sql);
|
||||||
|
if ($resql) {
|
||||||
|
while ($obj = $this->db->fetch_object($resql)) {
|
||||||
|
if ($obj->rowid == $elementId) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// Recursively check children
|
||||||
|
if ($this->isDescendant($elementId, $obj->rowid, $maxDepth - 1)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$this->db->free($resql);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all ancestor IDs of this element
|
||||||
|
*
|
||||||
|
* @param int $maxDepth Maximum depth to check
|
||||||
|
* @return array Array of ancestor IDs
|
||||||
|
*/
|
||||||
|
public function getAncestorIds($maxDepth = 50)
|
||||||
|
{
|
||||||
|
$ancestors = array();
|
||||||
|
$currentParentId = $this->fk_parent;
|
||||||
|
$depth = 0;
|
||||||
|
|
||||||
|
while ($currentParentId > 0 && $depth < $maxDepth) {
|
||||||
|
$ancestors[] = $currentParentId;
|
||||||
|
|
||||||
|
$sql = "SELECT fk_parent FROM ".MAIN_DB_PREFIX.$this->table_element;
|
||||||
|
$sql .= " WHERE rowid = ".((int) $currentParentId);
|
||||||
|
|
||||||
|
$resql = $this->db->query($sql);
|
||||||
|
if ($resql && $this->db->num_rows($resql) > 0) {
|
||||||
|
$obj = $this->db->fetch_object($resql);
|
||||||
|
$currentParentId = $obj->fk_parent;
|
||||||
|
$this->db->free($resql);
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
$depth++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $ancestors;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get info for tree display
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getTreeInfo()
|
||||||
|
{
|
||||||
|
$info = array();
|
||||||
|
|
||||||
|
// Get type fields that should show in tree
|
||||||
|
$sql = "SELECT field_code, field_label FROM ".MAIN_DB_PREFIX."kundenkarte_anlage_type_field";
|
||||||
|
$sql .= " WHERE fk_anlage_type = ".((int) $this->fk_anlage_type);
|
||||||
|
$sql .= " AND show_in_tree = 1 AND active = 1";
|
||||||
|
$sql .= " ORDER BY position ASC";
|
||||||
|
|
||||||
|
$resql = $this->db->query($sql);
|
||||||
|
if ($resql) {
|
||||||
|
$values = $this->getFieldValues();
|
||||||
|
while ($obj = $this->db->fetch_object($resql)) {
|
||||||
|
if (isset($values[$obj->field_code]) && $values[$obj->field_code] !== '') {
|
||||||
|
$info[] = $values[$obj->field_code];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$this->db->free($resql);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add common fields
|
||||||
|
if ($this->manufacturer) {
|
||||||
|
$info[] = $this->manufacturer;
|
||||||
|
}
|
||||||
|
if ($this->power_rating) {
|
||||||
|
$info[] = $this->power_rating;
|
||||||
|
}
|
||||||
|
|
||||||
|
return implode(', ', $info);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch full tree for a contact and system
|
||||||
|
*
|
||||||
|
* @param int $socid Customer ID
|
||||||
|
* @param int $contactid Contact ID
|
||||||
|
* @param int $systemId System ID
|
||||||
|
* @return array Tree structure
|
||||||
|
*/
|
||||||
|
public function fetchTreeByContact($socid, $contactid, $systemId)
|
||||||
|
{
|
||||||
|
$tree = array();
|
||||||
|
$roots = $this->fetchChildrenByContact(0, $socid, $contactid, $systemId);
|
||||||
|
|
||||||
|
foreach ($roots as $root) {
|
||||||
|
$root->children = $this->fetchChildrenByContactRecursive($root->id, $socid, $contactid);
|
||||||
|
$tree[] = $root;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $tree;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch children of an element filtered by contact
|
||||||
|
*
|
||||||
|
* @param int $parentId Parent ID (0 for root elements)
|
||||||
|
* @param int $socid Customer ID
|
||||||
|
* @param int $contactid Contact ID
|
||||||
|
* @param int $systemId System ID (optional filter)
|
||||||
|
* @return array Array of Anlage objects
|
||||||
|
*/
|
||||||
|
public function fetchChildrenByContact($parentId = 0, $socid = 0, $contactid = 0, $systemId = 0)
|
||||||
|
{
|
||||||
|
global $conf;
|
||||||
|
|
||||||
|
$results = array();
|
||||||
|
|
||||||
|
$sql = "SELECT a.*, t.label as type_label, t.label_short as type_short, t.picto as type_picto,";
|
||||||
|
$sql .= " s.label as system_label, s.code as system_code,";
|
||||||
|
// Count images
|
||||||
|
$sql .= " (SELECT COUNT(*) FROM ".MAIN_DB_PREFIX."kundenkarte_anlage_files f WHERE f.fk_anlage = a.rowid AND f.file_type = 'image') as image_count,";
|
||||||
|
// Count documents (pdf + document)
|
||||||
|
$sql .= " (SELECT COUNT(*) FROM ".MAIN_DB_PREFIX."kundenkarte_anlage_files f WHERE f.fk_anlage = a.rowid AND f.file_type IN ('pdf', 'document')) as doc_count";
|
||||||
|
$sql .= " FROM ".MAIN_DB_PREFIX.$this->table_element." as a";
|
||||||
|
$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."kundenkarte_anlage_type as t ON a.fk_anlage_type = t.rowid";
|
||||||
|
$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."c_kundenkarte_anlage_system as s ON a.fk_system = s.rowid";
|
||||||
|
$sql .= " WHERE a.fk_parent = ".((int) $parentId);
|
||||||
|
$sql .= " AND a.entity = ".((int) $conf->entity);
|
||||||
|
$sql .= " AND a.status = 1";
|
||||||
|
|
||||||
|
if ($parentId == 0) {
|
||||||
|
$sql .= " AND a.fk_soc = ".((int) $socid);
|
||||||
|
$sql .= " AND a.fk_contact = ".((int) $contactid);
|
||||||
|
}
|
||||||
|
if ($systemId > 0) {
|
||||||
|
$sql .= " AND a.fk_system = ".((int) $systemId);
|
||||||
|
}
|
||||||
|
|
||||||
|
$sql .= " ORDER BY a.rang ASC, a.label ASC";
|
||||||
|
|
||||||
|
$resql = $this->db->query($sql);
|
||||||
|
if ($resql) {
|
||||||
|
while ($obj = $this->db->fetch_object($resql)) {
|
||||||
|
$anlage = new Anlage($this->db);
|
||||||
|
$anlage->setFromObject($obj);
|
||||||
|
$results[] = $anlage;
|
||||||
|
}
|
||||||
|
$this->db->free($resql);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $results;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch children recursively for contact
|
||||||
|
*
|
||||||
|
* @param int $parentId Parent ID
|
||||||
|
* @param int $socid Customer ID
|
||||||
|
* @param int $contactid Contact ID
|
||||||
|
* @return array Array of Anlage objects with children
|
||||||
|
*/
|
||||||
|
private function fetchChildrenByContactRecursive($parentId, $socid, $contactid)
|
||||||
|
{
|
||||||
|
$children = $this->fetchChildrenByContact($parentId, $socid, $contactid);
|
||||||
|
foreach ($children as $child) {
|
||||||
|
$child->children = $this->fetchChildrenByContactRecursive($child->id, $socid, $contactid);
|
||||||
|
}
|
||||||
|
return $children;
|
||||||
|
}
|
||||||
|
}
|
||||||
558
class/anlagebackup.class.php
Executable file
558
class/anlagebackup.class.php
Executable 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
385
class/anlageconnection.class.php
Executable file
385
class/anlageconnection.class.php
Executable file
|
|
@ -0,0 +1,385 @@
|
||||||
|
<?php
|
||||||
|
/* Copyright (C) 2026 Alles Watt lauft
|
||||||
|
*
|
||||||
|
* Anlage Connection class - connections between Anlage elements in tree
|
||||||
|
* Describes cables/wires between structure elements like HAK → Zählerschrank
|
||||||
|
*/
|
||||||
|
|
||||||
|
require_once DOL_DOCUMENT_ROOT.'/core/class/commonobject.class.php';
|
||||||
|
|
||||||
|
class AnlageConnection extends CommonObject
|
||||||
|
{
|
||||||
|
public $element = 'anlageconnection';
|
||||||
|
public $table_element = 'kundenkarte_anlage_connection';
|
||||||
|
|
||||||
|
public $id;
|
||||||
|
public $entity;
|
||||||
|
public $fk_source;
|
||||||
|
public $fk_target;
|
||||||
|
public $label;
|
||||||
|
public $fk_medium_type;
|
||||||
|
public $medium_type_text;
|
||||||
|
public $medium_spec;
|
||||||
|
public $medium_length;
|
||||||
|
public $medium_color;
|
||||||
|
public $route_description;
|
||||||
|
public $installation_date;
|
||||||
|
public $status;
|
||||||
|
public $note_private;
|
||||||
|
public $note_public;
|
||||||
|
public $date_creation;
|
||||||
|
public $fk_user_creat;
|
||||||
|
public $fk_user_modif;
|
||||||
|
|
||||||
|
// Loaded related data
|
||||||
|
public $source_label;
|
||||||
|
public $source_ref;
|
||||||
|
public $target_label;
|
||||||
|
public $target_ref;
|
||||||
|
public $medium_type_label;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor
|
||||||
|
*
|
||||||
|
* @param DoliDB $db Database handler
|
||||||
|
*/
|
||||||
|
public function __construct($db)
|
||||||
|
{
|
||||||
|
$this->db = $db;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create connection
|
||||||
|
*
|
||||||
|
* @param User $user User object
|
||||||
|
* @return int >0 if OK, <0 if KO
|
||||||
|
*/
|
||||||
|
public function create($user)
|
||||||
|
{
|
||||||
|
global $conf;
|
||||||
|
|
||||||
|
$error = 0;
|
||||||
|
$now = dol_now();
|
||||||
|
|
||||||
|
$this->db->begin();
|
||||||
|
|
||||||
|
// installation_date als DATE-Feld (YYYY-MM-DD String) sicher escapen
|
||||||
|
$installDateSQL = "NULL";
|
||||||
|
if ($this->installation_date) {
|
||||||
|
$installDateSQL = "'".$this->db->escape($this->installation_date)."'";
|
||||||
|
}
|
||||||
|
|
||||||
|
$sql = "INSERT INTO ".MAIN_DB_PREFIX.$this->table_element." (";
|
||||||
|
$sql .= "entity, fk_source, fk_target, label,";
|
||||||
|
$sql .= "fk_medium_type, medium_type_text, medium_spec, medium_length, medium_color,";
|
||||||
|
$sql .= "route_description, installation_date, status,";
|
||||||
|
$sql .= "note_private, note_public, date_creation, fk_user_creat";
|
||||||
|
$sql .= ") VALUES (";
|
||||||
|
$sql .= (int)$conf->entity;
|
||||||
|
$sql .= ", ".(int)$this->fk_source;
|
||||||
|
$sql .= ", ".(int)$this->fk_target;
|
||||||
|
$sql .= ", ".($this->label ? "'".$this->db->escape($this->label)."'" : "NULL");
|
||||||
|
$sql .= ", ".($this->fk_medium_type > 0 ? (int)$this->fk_medium_type : "NULL");
|
||||||
|
$sql .= ", ".($this->medium_type_text ? "'".$this->db->escape($this->medium_type_text)."'" : "NULL");
|
||||||
|
$sql .= ", ".($this->medium_spec ? "'".$this->db->escape($this->medium_spec)."'" : "NULL");
|
||||||
|
$sql .= ", ".($this->medium_length ? "'".$this->db->escape($this->medium_length)."'" : "NULL");
|
||||||
|
$sql .= ", ".($this->medium_color ? "'".$this->db->escape($this->medium_color)."'" : "NULL");
|
||||||
|
$sql .= ", ".($this->route_description ? "'".$this->db->escape($this->route_description)."'" : "NULL");
|
||||||
|
$sql .= ", ".$installDateSQL;
|
||||||
|
$sql .= ", ".(int)($this->status ?: 1);
|
||||||
|
$sql .= ", ".($this->note_private ? "'".$this->db->escape($this->note_private)."'" : "NULL");
|
||||||
|
$sql .= ", ".($this->note_public ? "'".$this->db->escape($this->note_public)."'" : "NULL");
|
||||||
|
$sql .= ", '".$this->db->idate($now)."'";
|
||||||
|
$sql .= ", ".(int)$user->id;
|
||||||
|
$sql .= ")";
|
||||||
|
|
||||||
|
$resql = $this->db->query($sql);
|
||||||
|
if ($resql) {
|
||||||
|
$this->id = $this->db->last_insert_id(MAIN_DB_PREFIX.$this->table_element);
|
||||||
|
} else {
|
||||||
|
$error++;
|
||||||
|
$this->error = $this->db->lasterror();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($error) {
|
||||||
|
$this->db->rollback();
|
||||||
|
return -1;
|
||||||
|
} else {
|
||||||
|
$this->db->commit();
|
||||||
|
return $this->id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch connection
|
||||||
|
*
|
||||||
|
* @param int $id ID
|
||||||
|
* @return int >0 if OK, <0 if KO
|
||||||
|
*/
|
||||||
|
public function fetch($id)
|
||||||
|
{
|
||||||
|
$sql = "SELECT c.*,";
|
||||||
|
$sql .= " src.label as source_label, src.ref as source_ref,";
|
||||||
|
$sql .= " tgt.label as target_label, tgt.ref as target_ref,";
|
||||||
|
$sql .= " mt.label as medium_type_label";
|
||||||
|
$sql .= " FROM ".MAIN_DB_PREFIX.$this->table_element." as c";
|
||||||
|
$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."kundenkarte_anlage as src ON c.fk_source = src.rowid";
|
||||||
|
$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."kundenkarte_anlage as tgt ON c.fk_target = tgt.rowid";
|
||||||
|
$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."kundenkarte_medium_type as mt ON c.fk_medium_type = mt.rowid";
|
||||||
|
$sql .= " WHERE c.rowid = ".(int)$id;
|
||||||
|
|
||||||
|
$resql = $this->db->query($sql);
|
||||||
|
if ($resql) {
|
||||||
|
if ($obj = $this->db->fetch_object($resql)) {
|
||||||
|
$this->id = $obj->rowid;
|
||||||
|
$this->entity = $obj->entity;
|
||||||
|
$this->fk_source = $obj->fk_source;
|
||||||
|
$this->fk_target = $obj->fk_target;
|
||||||
|
$this->label = $obj->label;
|
||||||
|
$this->fk_medium_type = $obj->fk_medium_type;
|
||||||
|
$this->medium_type_text = $obj->medium_type_text;
|
||||||
|
$this->medium_spec = $obj->medium_spec;
|
||||||
|
$this->medium_length = $obj->medium_length;
|
||||||
|
$this->medium_color = $obj->medium_color;
|
||||||
|
$this->route_description = $obj->route_description;
|
||||||
|
$this->installation_date = $obj->installation_date;
|
||||||
|
$this->status = $obj->status;
|
||||||
|
$this->note_private = $obj->note_private;
|
||||||
|
$this->note_public = $obj->note_public;
|
||||||
|
$this->date_creation = $this->db->jdate($obj->date_creation);
|
||||||
|
$this->fk_user_creat = $obj->fk_user_creat;
|
||||||
|
$this->fk_user_modif = $obj->fk_user_modif;
|
||||||
|
|
||||||
|
$this->source_label = $obj->source_label;
|
||||||
|
$this->source_ref = $obj->source_ref;
|
||||||
|
$this->target_label = $obj->target_label;
|
||||||
|
$this->target_ref = $obj->target_ref;
|
||||||
|
$this->medium_type_label = $obj->medium_type_label;
|
||||||
|
|
||||||
|
$this->db->free($resql);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
$this->db->free($resql);
|
||||||
|
return 0;
|
||||||
|
} else {
|
||||||
|
$this->error = $this->db->lasterror();
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update connection
|
||||||
|
*
|
||||||
|
* @param User $user User object
|
||||||
|
* @return int >0 if OK, <0 if KO
|
||||||
|
*/
|
||||||
|
public function update($user)
|
||||||
|
{
|
||||||
|
$error = 0;
|
||||||
|
|
||||||
|
$this->db->begin();
|
||||||
|
|
||||||
|
// installation_date als DATE-Feld (YYYY-MM-DD String) sicher escapen
|
||||||
|
$installDateSQL = "NULL";
|
||||||
|
if ($this->installation_date) {
|
||||||
|
$installDateSQL = "'".$this->db->escape($this->installation_date)."'";
|
||||||
|
}
|
||||||
|
|
||||||
|
$sql = "UPDATE ".MAIN_DB_PREFIX.$this->table_element." SET";
|
||||||
|
$sql .= " fk_source = ".(int)$this->fk_source;
|
||||||
|
$sql .= ", fk_target = ".(int)$this->fk_target;
|
||||||
|
$sql .= ", label = ".($this->label ? "'".$this->db->escape($this->label)."'" : "NULL");
|
||||||
|
$sql .= ", fk_medium_type = ".($this->fk_medium_type > 0 ? (int)$this->fk_medium_type : "NULL");
|
||||||
|
$sql .= ", medium_type_text = ".($this->medium_type_text ? "'".$this->db->escape($this->medium_type_text)."'" : "NULL");
|
||||||
|
$sql .= ", medium_spec = ".($this->medium_spec ? "'".$this->db->escape($this->medium_spec)."'" : "NULL");
|
||||||
|
$sql .= ", medium_length = ".($this->medium_length ? "'".$this->db->escape($this->medium_length)."'" : "NULL");
|
||||||
|
$sql .= ", medium_color = ".($this->medium_color ? "'".$this->db->escape($this->medium_color)."'" : "NULL");
|
||||||
|
$sql .= ", route_description = ".($this->route_description ? "'".$this->db->escape($this->route_description)."'" : "NULL");
|
||||||
|
$sql .= ", installation_date = ".$installDateSQL;
|
||||||
|
$sql .= ", status = ".(int)$this->status;
|
||||||
|
$sql .= ", note_private = ".($this->note_private ? "'".$this->db->escape($this->note_private)."'" : "NULL");
|
||||||
|
$sql .= ", note_public = ".($this->note_public ? "'".$this->db->escape($this->note_public)."'" : "NULL");
|
||||||
|
$sql .= ", fk_user_modif = ".(int)$user->id;
|
||||||
|
$sql .= " WHERE rowid = ".(int)$this->id;
|
||||||
|
|
||||||
|
$resql = $this->db->query($sql);
|
||||||
|
if (!$resql) {
|
||||||
|
$error++;
|
||||||
|
$this->error = $this->db->lasterror();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($error) {
|
||||||
|
$this->db->rollback();
|
||||||
|
return -1;
|
||||||
|
} else {
|
||||||
|
$this->db->commit();
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete connection
|
||||||
|
*
|
||||||
|
* @param User $user User object
|
||||||
|
* @return int >0 if OK, <0 if KO
|
||||||
|
*/
|
||||||
|
public function delete($user)
|
||||||
|
{
|
||||||
|
$error = 0;
|
||||||
|
|
||||||
|
$this->db->begin();
|
||||||
|
|
||||||
|
$sql = "DELETE FROM ".MAIN_DB_PREFIX.$this->table_element;
|
||||||
|
$sql .= " WHERE rowid = ".(int)$this->id;
|
||||||
|
|
||||||
|
$resql = $this->db->query($sql);
|
||||||
|
if (!$resql) {
|
||||||
|
$error++;
|
||||||
|
$this->error = $this->db->lasterror();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($error) {
|
||||||
|
$this->db->rollback();
|
||||||
|
return -1;
|
||||||
|
} else {
|
||||||
|
$this->db->commit();
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch all connections for an Anlage (as source or target)
|
||||||
|
*
|
||||||
|
* @param int $anlageId Anlage ID
|
||||||
|
* @return array Array of AnlageConnection objects
|
||||||
|
*/
|
||||||
|
public function fetchByAnlage($anlageId)
|
||||||
|
{
|
||||||
|
$result = array();
|
||||||
|
|
||||||
|
$sql = "SELECT c.*,";
|
||||||
|
$sql .= " src.label as source_label, src.ref as source_ref,";
|
||||||
|
$sql .= " tgt.label as target_label, tgt.ref as target_ref,";
|
||||||
|
$sql .= " mt.label as medium_type_label";
|
||||||
|
$sql .= " FROM ".MAIN_DB_PREFIX.$this->table_element." as c";
|
||||||
|
$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."kundenkarte_anlage as src ON c.fk_source = src.rowid";
|
||||||
|
$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."kundenkarte_anlage as tgt ON c.fk_target = tgt.rowid";
|
||||||
|
$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."kundenkarte_medium_type as mt ON c.fk_medium_type = mt.rowid";
|
||||||
|
$sql .= " WHERE c.fk_source = ".(int)$anlageId." OR c.fk_target = ".(int)$anlageId;
|
||||||
|
$sql .= " ORDER BY c.rowid";
|
||||||
|
|
||||||
|
$resql = $this->db->query($sql);
|
||||||
|
if ($resql) {
|
||||||
|
while ($obj = $this->db->fetch_object($resql)) {
|
||||||
|
$conn = new AnlageConnection($this->db);
|
||||||
|
$conn->id = $obj->rowid;
|
||||||
|
$conn->entity = $obj->entity;
|
||||||
|
$conn->fk_source = $obj->fk_source;
|
||||||
|
$conn->fk_target = $obj->fk_target;
|
||||||
|
$conn->label = $obj->label;
|
||||||
|
$conn->fk_medium_type = $obj->fk_medium_type;
|
||||||
|
$conn->medium_type_text = $obj->medium_type_text;
|
||||||
|
$conn->medium_spec = $obj->medium_spec;
|
||||||
|
$conn->medium_length = $obj->medium_length;
|
||||||
|
$conn->medium_color = $obj->medium_color;
|
||||||
|
$conn->route_description = $obj->route_description;
|
||||||
|
$conn->installation_date = $obj->installation_date;
|
||||||
|
$conn->status = $obj->status;
|
||||||
|
$conn->source_label = $obj->source_label;
|
||||||
|
$conn->source_ref = $obj->source_ref;
|
||||||
|
$conn->target_label = $obj->target_label;
|
||||||
|
$conn->target_ref = $obj->target_ref;
|
||||||
|
$conn->medium_type_label = $obj->medium_type_label;
|
||||||
|
$result[] = $conn;
|
||||||
|
}
|
||||||
|
$this->db->free($resql);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch all connections for a customer (across all anlagen)
|
||||||
|
*
|
||||||
|
* @param int $socId Societe ID
|
||||||
|
* @param int $systemId Optional system filter
|
||||||
|
* @return array Array of AnlageConnection objects
|
||||||
|
*/
|
||||||
|
public function fetchBySociete($socId, $systemId = 0)
|
||||||
|
{
|
||||||
|
$result = array();
|
||||||
|
|
||||||
|
$sql = "SELECT c.*,";
|
||||||
|
$sql .= " src.label as source_label, src.ref as source_ref,";
|
||||||
|
$sql .= " tgt.label as target_label, tgt.ref as target_ref,";
|
||||||
|
$sql .= " mt.label as medium_type_label";
|
||||||
|
$sql .= " FROM ".MAIN_DB_PREFIX.$this->table_element." as c";
|
||||||
|
$sql .= " JOIN ".MAIN_DB_PREFIX."kundenkarte_anlage as src ON c.fk_source = src.rowid";
|
||||||
|
$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."kundenkarte_anlage as tgt ON c.fk_target = tgt.rowid";
|
||||||
|
$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."kundenkarte_medium_type as mt ON c.fk_medium_type = mt.rowid";
|
||||||
|
$sql .= " WHERE src.fk_soc = ".(int)$socId;
|
||||||
|
if ($systemId > 0) {
|
||||||
|
$sql .= " AND src.fk_system = ".(int)$systemId;
|
||||||
|
}
|
||||||
|
$sql .= " ORDER BY src.label, c.rowid";
|
||||||
|
|
||||||
|
$resql = $this->db->query($sql);
|
||||||
|
if ($resql) {
|
||||||
|
while ($obj = $this->db->fetch_object($resql)) {
|
||||||
|
$conn = new AnlageConnection($this->db);
|
||||||
|
$conn->id = $obj->rowid;
|
||||||
|
$conn->entity = $obj->entity;
|
||||||
|
$conn->fk_source = $obj->fk_source;
|
||||||
|
$conn->fk_target = $obj->fk_target;
|
||||||
|
$conn->label = $obj->label;
|
||||||
|
$conn->fk_medium_type = $obj->fk_medium_type;
|
||||||
|
$conn->medium_type_text = $obj->medium_type_text;
|
||||||
|
$conn->medium_spec = $obj->medium_spec;
|
||||||
|
$conn->medium_length = $obj->medium_length;
|
||||||
|
$conn->medium_color = $obj->medium_color;
|
||||||
|
$conn->route_description = $obj->route_description;
|
||||||
|
$conn->installation_date = $obj->installation_date;
|
||||||
|
$conn->status = $obj->status;
|
||||||
|
$conn->source_label = $obj->source_label;
|
||||||
|
$conn->source_ref = $obj->source_ref;
|
||||||
|
$conn->target_label = $obj->target_label;
|
||||||
|
$conn->target_ref = $obj->target_ref;
|
||||||
|
$conn->medium_type_label = $obj->medium_type_label;
|
||||||
|
$result[] = $conn;
|
||||||
|
}
|
||||||
|
$this->db->free($resql);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get display label for connection
|
||||||
|
*
|
||||||
|
* @return string Display label
|
||||||
|
*/
|
||||||
|
public function getDisplayLabel()
|
||||||
|
{
|
||||||
|
$parts = array();
|
||||||
|
|
||||||
|
// Medium type
|
||||||
|
$medium = $this->medium_type_label ?: $this->medium_type_text;
|
||||||
|
if ($medium) {
|
||||||
|
$mediumInfo = $medium;
|
||||||
|
if ($this->medium_spec) {
|
||||||
|
$mediumInfo .= ' '.$this->medium_spec;
|
||||||
|
}
|
||||||
|
if ($this->medium_length) {
|
||||||
|
$mediumInfo .= ' ('.$this->medium_length.')';
|
||||||
|
}
|
||||||
|
$parts[] = $mediumInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Label
|
||||||
|
if ($this->label) {
|
||||||
|
$parts[] = $this->label;
|
||||||
|
}
|
||||||
|
|
||||||
|
return implode(' - ', $parts);
|
||||||
|
}
|
||||||
|
}
|
||||||
447
class/anlagefile.class.php
Executable file
447
class/anlagefile.class.php
Executable file
|
|
@ -0,0 +1,447 @@
|
||||||
|
<?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 AnlageFile
|
||||||
|
* Manages file attachments for installation elements
|
||||||
|
*/
|
||||||
|
class AnlageFile extends CommonObject
|
||||||
|
{
|
||||||
|
public $element = 'anlagefile';
|
||||||
|
public $table_element = 'kundenkarte_anlage_files';
|
||||||
|
|
||||||
|
public $fk_anlage;
|
||||||
|
public $filename;
|
||||||
|
public $filepath;
|
||||||
|
public $filesize;
|
||||||
|
public $mimetype;
|
||||||
|
public $file_type;
|
||||||
|
public $label;
|
||||||
|
public $description;
|
||||||
|
public $is_cover;
|
||||||
|
public $is_pinned;
|
||||||
|
public $position;
|
||||||
|
public $share;
|
||||||
|
|
||||||
|
public $date_creation;
|
||||||
|
public $fk_user_creat;
|
||||||
|
public $fk_user_modif;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor
|
||||||
|
*
|
||||||
|
* @param DoliDB $db Database handler
|
||||||
|
*/
|
||||||
|
public function __construct($db)
|
||||||
|
{
|
||||||
|
$this->db = $db;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create object in database
|
||||||
|
*
|
||||||
|
* @param User $user User that creates
|
||||||
|
* @return int Return integer <0 if KO, Id of created object if OK
|
||||||
|
*/
|
||||||
|
public function create($user)
|
||||||
|
{
|
||||||
|
global $conf;
|
||||||
|
|
||||||
|
$error = 0;
|
||||||
|
$now = dol_now();
|
||||||
|
|
||||||
|
if (empty($this->fk_anlage) || empty($this->filename) || empty($this->filepath)) {
|
||||||
|
$this->error = 'ErrorMissingParameters';
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine file type
|
||||||
|
if (empty($this->file_type)) {
|
||||||
|
$this->file_type = $this->determineFileType($this->mimetype, $this->filename);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->db->begin();
|
||||||
|
|
||||||
|
$sql = "INSERT INTO ".MAIN_DB_PREFIX.$this->table_element." (";
|
||||||
|
$sql .= "entity, fk_anlage, filename, filepath, filesize, mimetype,";
|
||||||
|
$sql .= " file_type, label, description, is_cover, is_pinned, position, share,";
|
||||||
|
$sql .= " date_creation, fk_user_creat";
|
||||||
|
$sql .= ") VALUES (";
|
||||||
|
$sql .= ((int) $conf->entity);
|
||||||
|
$sql .= ", ".((int) $this->fk_anlage);
|
||||||
|
$sql .= ", '".$this->db->escape($this->filename)."'";
|
||||||
|
$sql .= ", '".$this->db->escape($this->filepath)."'";
|
||||||
|
$sql .= ", ".((int) $this->filesize);
|
||||||
|
$sql .= ", ".($this->mimetype ? "'".$this->db->escape($this->mimetype)."'" : "NULL");
|
||||||
|
$sql .= ", '".$this->db->escape($this->file_type)."'";
|
||||||
|
$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)."'";
|
||||||
|
$sql .= ", ".((int) $user->id);
|
||||||
|
$sql .= ")";
|
||||||
|
|
||||||
|
$resql = $this->db->query($sql);
|
||||||
|
if (!$resql) {
|
||||||
|
$error++;
|
||||||
|
$this->errors[] = "Error ".$this->db->lasterror();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$error) {
|
||||||
|
$this->id = $this->db->last_insert_id(MAIN_DB_PREFIX.$this->table_element);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($error) {
|
||||||
|
$this->db->rollback();
|
||||||
|
return -1 * $error;
|
||||||
|
} else {
|
||||||
|
$this->db->commit();
|
||||||
|
return $this->id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load object from database
|
||||||
|
*
|
||||||
|
* @param int $id ID of record
|
||||||
|
* @return int Return integer <0 if KO, 0 if not found, >0 if OK
|
||||||
|
*/
|
||||||
|
public function fetch($id)
|
||||||
|
{
|
||||||
|
global $conf;
|
||||||
|
|
||||||
|
$sql = "SELECT * FROM ".MAIN_DB_PREFIX.$this->table_element;
|
||||||
|
$sql .= " WHERE rowid = ".((int) $id);
|
||||||
|
$sql .= " AND entity = ".((int) $conf->entity);
|
||||||
|
|
||||||
|
$resql = $this->db->query($sql);
|
||||||
|
if ($resql) {
|
||||||
|
if ($this->db->num_rows($resql)) {
|
||||||
|
$obj = $this->db->fetch_object($resql);
|
||||||
|
|
||||||
|
$this->id = $obj->rowid;
|
||||||
|
$this->entity = $obj->entity;
|
||||||
|
$this->fk_anlage = $obj->fk_anlage;
|
||||||
|
$this->filename = $obj->filename;
|
||||||
|
$this->filepath = $obj->filepath;
|
||||||
|
$this->filesize = $obj->filesize;
|
||||||
|
$this->mimetype = $obj->mimetype;
|
||||||
|
$this->file_type = $obj->file_type;
|
||||||
|
$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);
|
||||||
|
$this->fk_user_creat = $obj->fk_user_creat;
|
||||||
|
$this->fk_user_modif = $obj->fk_user_modif;
|
||||||
|
|
||||||
|
$this->db->free($resql);
|
||||||
|
return 1;
|
||||||
|
} else {
|
||||||
|
$this->db->free($resql);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$this->error = $this->db->lasterror();
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete object in database and file from disk
|
||||||
|
*
|
||||||
|
* @param User $user User that deletes
|
||||||
|
* @return int Return integer <0 if KO, >0 if OK
|
||||||
|
*/
|
||||||
|
public function delete($user)
|
||||||
|
{
|
||||||
|
global $conf;
|
||||||
|
|
||||||
|
$error = 0;
|
||||||
|
|
||||||
|
// Delete physical file (use clean filepath)
|
||||||
|
$fullpath = $this->getFullPath();
|
||||||
|
if (file_exists($fullpath)) {
|
||||||
|
dol_delete_file($fullpath);
|
||||||
|
|
||||||
|
// Delete thumbnail if exists
|
||||||
|
$thumbpath = dirname($fullpath).'/thumbs/'.pathinfo($this->filename, PATHINFO_FILENAME).'_small.'.pathinfo($this->filename, PATHINFO_EXTENSION);
|
||||||
|
if (file_exists($thumbpath)) {
|
||||||
|
dol_delete_file($thumbpath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->db->begin();
|
||||||
|
|
||||||
|
$sql = "DELETE FROM ".MAIN_DB_PREFIX.$this->table_element." WHERE rowid = ".((int) $this->id);
|
||||||
|
$resql = $this->db->query($sql);
|
||||||
|
if (!$resql) {
|
||||||
|
$error++;
|
||||||
|
$this->errors[] = "Error ".$this->db->lasterror();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($error) {
|
||||||
|
$this->db->rollback();
|
||||||
|
return -1 * $error;
|
||||||
|
} else {
|
||||||
|
$this->db->commit();
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch all files for an installation element
|
||||||
|
*
|
||||||
|
* @param int $anlageId Installation element ID
|
||||||
|
* @param string $fileType Filter by file type (image, pdf, document, other)
|
||||||
|
* @return array Array of AnlageFile objects
|
||||||
|
*/
|
||||||
|
public function fetchAllByAnlage($anlageId, $fileType = '')
|
||||||
|
{
|
||||||
|
global $conf;
|
||||||
|
|
||||||
|
$results = array();
|
||||||
|
|
||||||
|
$sql = "SELECT * FROM ".MAIN_DB_PREFIX.$this->table_element;
|
||||||
|
$sql .= " WHERE fk_anlage = ".((int) $anlageId);
|
||||||
|
$sql .= " AND entity = ".((int) $conf->entity);
|
||||||
|
if ($fileType) {
|
||||||
|
$sql .= " AND file_type = '".$this->db->escape($fileType)."'";
|
||||||
|
}
|
||||||
|
$sql .= " ORDER BY is_pinned DESC, is_cover DESC, position ASC, filename ASC";
|
||||||
|
|
||||||
|
$resql = $this->db->query($sql);
|
||||||
|
if ($resql) {
|
||||||
|
while ($obj = $this->db->fetch_object($resql)) {
|
||||||
|
$file = new AnlageFile($this->db);
|
||||||
|
$file->id = $obj->rowid;
|
||||||
|
$file->fk_anlage = $obj->fk_anlage;
|
||||||
|
$file->filename = $obj->filename;
|
||||||
|
$file->filepath = $obj->filepath;
|
||||||
|
$file->filesize = $obj->filesize;
|
||||||
|
$file->mimetype = $obj->mimetype;
|
||||||
|
$file->file_type = $obj->file_type;
|
||||||
|
$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);
|
||||||
|
|
||||||
|
$results[] = $file;
|
||||||
|
}
|
||||||
|
$this->db->free($resql);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $results;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine file type from mimetype and filename
|
||||||
|
*
|
||||||
|
* @param string $mimetype MIME type
|
||||||
|
* @param string $filename Filename
|
||||||
|
* @return string File type (image, pdf, document, other)
|
||||||
|
*/
|
||||||
|
public function determineFileType($mimetype, $filename)
|
||||||
|
{
|
||||||
|
$ext = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
|
||||||
|
|
||||||
|
if (in_array($ext, array('jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp')) || strpos($mimetype, 'image/') === 0) {
|
||||||
|
return 'image';
|
||||||
|
}
|
||||||
|
if ($ext == 'pdf' || $mimetype == 'application/pdf') {
|
||||||
|
return 'pdf';
|
||||||
|
}
|
||||||
|
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';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get full file path
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getFullPath()
|
||||||
|
{
|
||||||
|
global $conf;
|
||||||
|
return $conf->kundenkarte->dir_output.'/'.$this->getCleanFilepath();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get URL to view/download the file
|
||||||
|
*
|
||||||
|
* @param bool $forceDownload If true, force download instead of inline display
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getUrl($forceDownload = false)
|
||||||
|
{
|
||||||
|
// Sanitize filepath - ensure it's relative, not absolute
|
||||||
|
$filepath = $this->getCleanFilepath();
|
||||||
|
$url = DOL_URL_ROOT.'/document.php?modulepart=kundenkarte&file='.urlencode($filepath);
|
||||||
|
|
||||||
|
// Add attachment=0 for inline display (especially for PDFs)
|
||||||
|
// If forceDownload is true, don't add this parameter (default Dolibarr behavior is download)
|
||||||
|
if (!$forceDownload) {
|
||||||
|
$url .= '&attachment=0';
|
||||||
|
}
|
||||||
|
|
||||||
|
return $url;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get clean relative filepath (fixes bad data)
|
||||||
|
*
|
||||||
|
* @return string Clean relative filepath
|
||||||
|
*/
|
||||||
|
public function getCleanFilepath()
|
||||||
|
{
|
||||||
|
$filepath = $this->filepath;
|
||||||
|
|
||||||
|
// If filepath contains "anlagen/" extract from there
|
||||||
|
if (strpos($filepath, 'anlagen/') !== false) {
|
||||||
|
$filepath = substr($filepath, strpos($filepath, 'anlagen/'));
|
||||||
|
}
|
||||||
|
// If it's still an absolute path (starts with / or file:// or Windows drive), just use filename
|
||||||
|
elseif (preg_match('/^(\/|file:\/\/|[A-Za-z]:)/', $filepath)) {
|
||||||
|
// Try to get just the filename and rebuild proper path
|
||||||
|
$filename = basename($filepath);
|
||||||
|
// Use fk_anlage to build correct path if available
|
||||||
|
if ($this->fk_anlage > 0) {
|
||||||
|
global $db;
|
||||||
|
$sql = "SELECT fk_soc FROM ".MAIN_DB_PREFIX."kundenkarte_anlage WHERE rowid = ".((int) $this->fk_anlage);
|
||||||
|
$resql = $db->query($sql);
|
||||||
|
if ($resql && $db->num_rows($resql)) {
|
||||||
|
$obj = $db->fetch_object($resql);
|
||||||
|
$filepath = 'anlagen/'.$obj->fk_soc.'/'.$this->fk_anlage.'/'.$filename;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $filepath;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get thumbnail URL for images
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getThumbUrl()
|
||||||
|
{
|
||||||
|
if ($this->file_type != 'image') {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
$filepath = $this->getCleanFilepath();
|
||||||
|
$dir = dirname($filepath);
|
||||||
|
$filename = pathinfo($this->filename, PATHINFO_FILENAME);
|
||||||
|
$ext = pathinfo($this->filename, PATHINFO_EXTENSION);
|
||||||
|
|
||||||
|
$thumbpath = $dir.'/thumbs/'.$filename.'_small.'.$ext;
|
||||||
|
|
||||||
|
// Use attachment=0 for inline display
|
||||||
|
return DOL_URL_ROOT.'/document.php?modulepart=kundenkarte&file='.urlencode($thumbpath).'&attachment=0';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set this file as cover image
|
||||||
|
*
|
||||||
|
* @param User $user User making the change
|
||||||
|
* @return int <0 if KO, >0 if OK
|
||||||
|
*/
|
||||||
|
public function setAsCover($user)
|
||||||
|
{
|
||||||
|
$this->db->begin();
|
||||||
|
|
||||||
|
// Unset other covers for this anlage
|
||||||
|
$sql = "UPDATE ".MAIN_DB_PREFIX.$this->table_element." SET is_cover = 0";
|
||||||
|
$sql .= " WHERE fk_anlage = ".((int) $this->fk_anlage);
|
||||||
|
$this->db->query($sql);
|
||||||
|
|
||||||
|
// Set this as cover
|
||||||
|
$sql = "UPDATE ".MAIN_DB_PREFIX.$this->table_element." SET is_cover = 1, fk_user_modif = ".((int) $user->id);
|
||||||
|
$sql .= " WHERE rowid = ".((int) $this->id);
|
||||||
|
$resql = $this->db->query($sql);
|
||||||
|
|
||||||
|
if ($resql) {
|
||||||
|
$this->is_cover = 1;
|
||||||
|
$this->db->commit();
|
||||||
|
return 1;
|
||||||
|
} else {
|
||||||
|
$this->db->rollback();
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function generateThumbnail()
|
||||||
|
{
|
||||||
|
global $conf;
|
||||||
|
|
||||||
|
if ($this->file_type != 'image') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
require_once DOL_DOCUMENT_ROOT.'/core/lib/images.lib.php';
|
||||||
|
|
||||||
|
$fullpath = $this->getFullPath();
|
||||||
|
if (!file_exists($fullpath)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$thumbdir = dirname($fullpath).'/thumbs';
|
||||||
|
if (!is_dir($thumbdir)) {
|
||||||
|
dol_mkdir($thumbdir);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate small thumbnail
|
||||||
|
$result = vignette($fullpath, 200, 160, '_small', 80, 'thumbs');
|
||||||
|
|
||||||
|
return ($result !== false);
|
||||||
|
}
|
||||||
|
}
|
||||||
386
class/anlagetype.class.php
Executable file
386
class/anlagetype.class.php
Executable file
|
|
@ -0,0 +1,386 @@
|
||||||
|
<?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 AnlageType
|
||||||
|
* Manages element type templates
|
||||||
|
*/
|
||||||
|
class AnlageType extends CommonObject
|
||||||
|
{
|
||||||
|
public $element = 'anlagetype';
|
||||||
|
public $table_element = 'kundenkarte_anlage_type';
|
||||||
|
|
||||||
|
public $ref;
|
||||||
|
public $label;
|
||||||
|
public $label_short;
|
||||||
|
public $description;
|
||||||
|
public $fk_system;
|
||||||
|
|
||||||
|
public $can_have_children;
|
||||||
|
public $can_be_nested;
|
||||||
|
public $allowed_parent_types;
|
||||||
|
public $can_have_equipment;
|
||||||
|
|
||||||
|
public $picto;
|
||||||
|
public $color;
|
||||||
|
public $is_system;
|
||||||
|
public $position;
|
||||||
|
public $active;
|
||||||
|
|
||||||
|
public $date_creation;
|
||||||
|
public $fk_user_creat;
|
||||||
|
public $fk_user_modif;
|
||||||
|
|
||||||
|
// Loaded objects
|
||||||
|
public $system;
|
||||||
|
public $fields = array();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor
|
||||||
|
*
|
||||||
|
* @param DoliDB $db Database handler
|
||||||
|
*/
|
||||||
|
public function __construct($db)
|
||||||
|
{
|
||||||
|
$this->db = $db;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create object in database
|
||||||
|
*
|
||||||
|
* @param User $user User that creates
|
||||||
|
* @return int Return integer <0 if KO, Id of created object if OK
|
||||||
|
*/
|
||||||
|
public function create($user)
|
||||||
|
{
|
||||||
|
global $conf;
|
||||||
|
|
||||||
|
$error = 0;
|
||||||
|
$now = dol_now();
|
||||||
|
|
||||||
|
if (empty($this->ref) || empty($this->label) || empty($this->fk_system)) {
|
||||||
|
$this->error = 'ErrorMissingParameters';
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->db->begin();
|
||||||
|
|
||||||
|
$sql = "INSERT INTO ".MAIN_DB_PREFIX.$this->table_element." (";
|
||||||
|
$sql .= "entity, ref, label, label_short, description, fk_system,";
|
||||||
|
$sql .= " can_have_children, can_be_nested, allowed_parent_types, can_have_equipment,";
|
||||||
|
$sql .= " picto, color, is_system, position, active,";
|
||||||
|
$sql .= " date_creation, fk_user_creat";
|
||||||
|
$sql .= ") VALUES (";
|
||||||
|
$sql .= "0"; // entity 0 = global
|
||||||
|
$sql .= ", '".$this->db->escape($this->ref)."'";
|
||||||
|
$sql .= ", '".$this->db->escape($this->label)."'";
|
||||||
|
$sql .= ", ".($this->label_short ? "'".$this->db->escape($this->label_short)."'" : "NULL");
|
||||||
|
$sql .= ", ".($this->description ? "'".$this->db->escape($this->description)."'" : "NULL");
|
||||||
|
$sql .= ", ".((int) $this->fk_system);
|
||||||
|
$sql .= ", ".((int) $this->can_have_children);
|
||||||
|
$sql .= ", ".((int) $this->can_be_nested);
|
||||||
|
$sql .= ", ".($this->allowed_parent_types ? "'".$this->db->escape($this->allowed_parent_types)."'" : "NULL");
|
||||||
|
$sql .= ", ".((int) $this->can_have_equipment);
|
||||||
|
$sql .= ", ".($this->picto ? "'".$this->db->escape($this->picto)."'" : "NULL");
|
||||||
|
$sql .= ", ".($this->color ? "'".$this->db->escape($this->color)."'" : "NULL");
|
||||||
|
$sql .= ", 0"; // is_system = 0 for user-created
|
||||||
|
$sql .= ", ".((int) $this->position);
|
||||||
|
$sql .= ", ".((int) ($this->active !== null ? $this->active : 1));
|
||||||
|
$sql .= ", '".$this->db->idate($now)."'";
|
||||||
|
$sql .= ", ".((int) $user->id);
|
||||||
|
$sql .= ")";
|
||||||
|
|
||||||
|
$resql = $this->db->query($sql);
|
||||||
|
if (!$resql) {
|
||||||
|
$error++;
|
||||||
|
$this->errors[] = "Error ".$this->db->lasterror();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$error) {
|
||||||
|
$this->id = $this->db->last_insert_id(MAIN_DB_PREFIX.$this->table_element);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($error) {
|
||||||
|
$this->db->rollback();
|
||||||
|
return -1 * $error;
|
||||||
|
} else {
|
||||||
|
$this->db->commit();
|
||||||
|
return $this->id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load object from database
|
||||||
|
*
|
||||||
|
* @param int $id ID of record
|
||||||
|
* @return int Return integer <0 if KO, 0 if not found, >0 if OK
|
||||||
|
*/
|
||||||
|
public function fetch($id)
|
||||||
|
{
|
||||||
|
$sql = "SELECT t.*, s.label as system_label, s.code as system_code";
|
||||||
|
$sql .= " FROM ".MAIN_DB_PREFIX.$this->table_element." as t";
|
||||||
|
$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."c_kundenkarte_anlage_system as s ON t.fk_system = s.rowid";
|
||||||
|
$sql .= " WHERE t.rowid = ".((int) $id);
|
||||||
|
|
||||||
|
$resql = $this->db->query($sql);
|
||||||
|
if ($resql) {
|
||||||
|
if ($this->db->num_rows($resql)) {
|
||||||
|
$obj = $this->db->fetch_object($resql);
|
||||||
|
|
||||||
|
$this->id = $obj->rowid;
|
||||||
|
$this->entity = $obj->entity;
|
||||||
|
$this->ref = $obj->ref;
|
||||||
|
$this->label = $obj->label;
|
||||||
|
$this->label_short = $obj->label_short;
|
||||||
|
$this->description = $obj->description;
|
||||||
|
$this->fk_system = $obj->fk_system;
|
||||||
|
$this->can_have_children = $obj->can_have_children;
|
||||||
|
$this->can_be_nested = $obj->can_be_nested;
|
||||||
|
$this->allowed_parent_types = $obj->allowed_parent_types;
|
||||||
|
$this->can_have_equipment = $obj->can_have_equipment ?? 0;
|
||||||
|
$this->picto = $obj->picto;
|
||||||
|
$this->color = $obj->color;
|
||||||
|
$this->is_system = $obj->is_system;
|
||||||
|
$this->position = $obj->position;
|
||||||
|
$this->active = $obj->active;
|
||||||
|
$this->date_creation = $this->db->jdate($obj->date_creation);
|
||||||
|
$this->fk_user_creat = $obj->fk_user_creat;
|
||||||
|
$this->fk_user_modif = $obj->fk_user_modif;
|
||||||
|
|
||||||
|
$this->system_label = $obj->system_label;
|
||||||
|
$this->system_code = $obj->system_code;
|
||||||
|
|
||||||
|
$this->db->free($resql);
|
||||||
|
return 1;
|
||||||
|
} else {
|
||||||
|
$this->db->free($resql);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$this->error = $this->db->lasterror();
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update object in database
|
||||||
|
*
|
||||||
|
* @param User $user User that modifies
|
||||||
|
* @return int Return integer <0 if KO, >0 if OK
|
||||||
|
*/
|
||||||
|
public function update($user)
|
||||||
|
{
|
||||||
|
$error = 0;
|
||||||
|
|
||||||
|
$this->db->begin();
|
||||||
|
|
||||||
|
$sql = "UPDATE ".MAIN_DB_PREFIX.$this->table_element." SET";
|
||||||
|
$sql .= " ref = '".$this->db->escape($this->ref)."'";
|
||||||
|
$sql .= ", label = '".$this->db->escape($this->label)."'";
|
||||||
|
$sql .= ", label_short = ".($this->label_short ? "'".$this->db->escape($this->label_short)."'" : "NULL");
|
||||||
|
$sql .= ", description = ".($this->description ? "'".$this->db->escape($this->description)."'" : "NULL");
|
||||||
|
$sql .= ", fk_system = ".((int) $this->fk_system);
|
||||||
|
$sql .= ", can_have_children = ".((int) $this->can_have_children);
|
||||||
|
$sql .= ", can_be_nested = ".((int) $this->can_be_nested);
|
||||||
|
$sql .= ", allowed_parent_types = ".($this->allowed_parent_types ? "'".$this->db->escape($this->allowed_parent_types)."'" : "NULL");
|
||||||
|
$sql .= ", can_have_equipment = ".((int) $this->can_have_equipment);
|
||||||
|
$sql .= ", picto = ".($this->picto ? "'".$this->db->escape($this->picto)."'" : "NULL");
|
||||||
|
$sql .= ", color = ".($this->color ? "'".$this->db->escape($this->color)."'" : "NULL");
|
||||||
|
$sql .= ", position = ".((int) $this->position);
|
||||||
|
$sql .= ", active = ".((int) $this->active);
|
||||||
|
$sql .= ", fk_user_modif = ".((int) $user->id);
|
||||||
|
$sql .= " WHERE rowid = ".((int) $this->id);
|
||||||
|
|
||||||
|
$resql = $this->db->query($sql);
|
||||||
|
if (!$resql) {
|
||||||
|
$error++;
|
||||||
|
$this->errors[] = "Error ".$this->db->lasterror();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($error) {
|
||||||
|
$this->db->rollback();
|
||||||
|
return -1 * $error;
|
||||||
|
} else {
|
||||||
|
$this->db->commit();
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete object in database
|
||||||
|
*
|
||||||
|
* @param User $user User that deletes
|
||||||
|
* @return int Return integer <0 if KO, >0 if OK
|
||||||
|
*/
|
||||||
|
public function delete($user)
|
||||||
|
{
|
||||||
|
global $conf;
|
||||||
|
|
||||||
|
// Check if type is in use
|
||||||
|
$sql = "SELECT COUNT(*) as cnt FROM ".MAIN_DB_PREFIX."kundenkarte_anlage";
|
||||||
|
$sql .= " WHERE fk_anlage_type = ".((int) $this->id);
|
||||||
|
$resql = $this->db->query($sql);
|
||||||
|
if ($resql) {
|
||||||
|
$obj = $this->db->fetch_object($resql);
|
||||||
|
if ($obj->cnt > 0) {
|
||||||
|
$this->error = 'ErrorTypeInUse';
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cannot delete system types
|
||||||
|
if ($this->is_system) {
|
||||||
|
$this->error = 'ErrorCannotDeleteSystemType';
|
||||||
|
return -2;
|
||||||
|
}
|
||||||
|
|
||||||
|
$error = 0;
|
||||||
|
$this->db->begin();
|
||||||
|
|
||||||
|
// Delete fields first
|
||||||
|
$sql = "DELETE FROM ".MAIN_DB_PREFIX."kundenkarte_anlage_type_field WHERE fk_anlage_type = ".((int) $this->id);
|
||||||
|
$this->db->query($sql);
|
||||||
|
|
||||||
|
// Delete type
|
||||||
|
$sql = "DELETE FROM ".MAIN_DB_PREFIX.$this->table_element." WHERE rowid = ".((int) $this->id);
|
||||||
|
$resql = $this->db->query($sql);
|
||||||
|
if (!$resql) {
|
||||||
|
$error++;
|
||||||
|
$this->errors[] = "Error ".$this->db->lasterror();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($error) {
|
||||||
|
$this->db->rollback();
|
||||||
|
return -1 * $error;
|
||||||
|
} else {
|
||||||
|
$this->db->commit();
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch all types for a system
|
||||||
|
*
|
||||||
|
* @param int $systemId System ID (0 = all)
|
||||||
|
* @param int $activeOnly Only active types
|
||||||
|
* @param int $excludeGlobal 1 = GLOBAL-Typen ausschliessen (fuer Admin-Ansicht)
|
||||||
|
* @return array Array of AnlageType objects
|
||||||
|
*/
|
||||||
|
public function fetchAllBySystem($systemId = 0, $activeOnly = 1, $excludeGlobal = 0)
|
||||||
|
{
|
||||||
|
$results = array();
|
||||||
|
|
||||||
|
$sql = "SELECT t.*, s.label as system_label, s.code as system_code";
|
||||||
|
$sql .= " FROM ".MAIN_DB_PREFIX.$this->table_element." as t";
|
||||||
|
$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."c_kundenkarte_anlage_system as s ON t.fk_system = s.rowid";
|
||||||
|
$sql .= " WHERE 1 = 1";
|
||||||
|
if ($excludeGlobal) {
|
||||||
|
// GLOBAL-Typen (Gebaeude) ausschliessen (Admin-Ansicht)
|
||||||
|
$sql .= " AND (s.code IS NULL OR s.code != 'GLOBAL')";
|
||||||
|
}
|
||||||
|
if ($systemId > 0) {
|
||||||
|
if (!$excludeGlobal) {
|
||||||
|
// Typen dieses Systems UND GLOBAL-Typen (fuer Tabs-Ansicht)
|
||||||
|
$sql .= " AND (t.fk_system = ".((int) $systemId)." OR s.code = 'GLOBAL')";
|
||||||
|
} else {
|
||||||
|
// Nur Typen dieses Systems (fuer Admin-Ansicht)
|
||||||
|
$sql .= " AND t.fk_system = ".((int) $systemId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($activeOnly) {
|
||||||
|
$sql .= " AND t.active = 1";
|
||||||
|
}
|
||||||
|
// Sort: GLOBAL types first (position 0), then by position, then by label
|
||||||
|
$sql .= " ORDER BY s.position ASC, t.position ASC, t.label ASC";
|
||||||
|
|
||||||
|
$resql = $this->db->query($sql);
|
||||||
|
if ($resql) {
|
||||||
|
while ($obj = $this->db->fetch_object($resql)) {
|
||||||
|
$type = new AnlageType($this->db);
|
||||||
|
$type->id = $obj->rowid;
|
||||||
|
$type->ref = $obj->ref;
|
||||||
|
$type->label = $obj->label;
|
||||||
|
$type->label_short = $obj->label_short;
|
||||||
|
$type->fk_system = $obj->fk_system;
|
||||||
|
$type->can_have_children = $obj->can_have_children;
|
||||||
|
$type->can_be_nested = $obj->can_be_nested;
|
||||||
|
$type->allowed_parent_types = $obj->allowed_parent_types;
|
||||||
|
$type->can_have_equipment = $obj->can_have_equipment ?? 0;
|
||||||
|
$type->picto = $obj->picto;
|
||||||
|
$type->is_system = $obj->is_system;
|
||||||
|
$type->position = $obj->position;
|
||||||
|
$type->active = $obj->active;
|
||||||
|
$type->system_label = $obj->system_label;
|
||||||
|
$type->system_code = $obj->system_code;
|
||||||
|
|
||||||
|
$results[] = $type;
|
||||||
|
}
|
||||||
|
$this->db->free($resql);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $results;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch fields for this type
|
||||||
|
*
|
||||||
|
* @param int $activeOnly Only active fields
|
||||||
|
* @return array Array of field objects
|
||||||
|
*/
|
||||||
|
public function fetchFields($activeOnly = 1)
|
||||||
|
{
|
||||||
|
$results = array();
|
||||||
|
|
||||||
|
$sql = "SELECT * FROM ".MAIN_DB_PREFIX."kundenkarte_anlage_type_field";
|
||||||
|
$sql .= " WHERE fk_anlage_type = ".((int) $this->id);
|
||||||
|
if ($activeOnly) {
|
||||||
|
$sql .= " AND active = 1";
|
||||||
|
}
|
||||||
|
$sql .= " ORDER BY position ASC";
|
||||||
|
|
||||||
|
$resql = $this->db->query($sql);
|
||||||
|
if ($resql) {
|
||||||
|
while ($obj = $this->db->fetch_object($resql)) {
|
||||||
|
$results[] = $obj;
|
||||||
|
}
|
||||||
|
$this->db->free($resql);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->fields = $results;
|
||||||
|
return $results;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get allowed parent types as array
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function getAllowedParentTypesArray()
|
||||||
|
{
|
||||||
|
if (empty($this->allowed_parent_types)) {
|
||||||
|
return array();
|
||||||
|
}
|
||||||
|
return array_map('trim', explode(',', $this->allowed_parent_types));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if this type can be child of another type
|
||||||
|
*
|
||||||
|
* @param string $parentTypeRef Parent type reference
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function canBeChildOf($parentTypeRef)
|
||||||
|
{
|
||||||
|
if (empty($this->allowed_parent_types)) {
|
||||||
|
return true; // No restriction = can be anywhere
|
||||||
|
}
|
||||||
|
$allowed = $this->getAllowedParentTypesArray();
|
||||||
|
return in_array($parentTypeRef, $allowed);
|
||||||
|
}
|
||||||
|
}
|
||||||
455
class/auditlog.class.php
Executable file
455
class/auditlog.class.php
Executable file
|
|
@ -0,0 +1,455 @@
|
||||||
|
<?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 AuditLog
|
||||||
|
* Manages audit logging for KundenKarte module
|
||||||
|
*/
|
||||||
|
class AuditLog extends CommonObject
|
||||||
|
{
|
||||||
|
public $element = 'auditlog';
|
||||||
|
public $table_element = 'kundenkarte_audit_log';
|
||||||
|
|
||||||
|
public $object_type;
|
||||||
|
public $object_id;
|
||||||
|
public $object_ref;
|
||||||
|
public $fk_societe;
|
||||||
|
public $fk_anlage;
|
||||||
|
public $action;
|
||||||
|
public $field_changed;
|
||||||
|
public $old_value;
|
||||||
|
public $new_value;
|
||||||
|
public $fk_user;
|
||||||
|
public $user_login;
|
||||||
|
public $date_action;
|
||||||
|
public $note;
|
||||||
|
public $ip_address;
|
||||||
|
|
||||||
|
// Loaded properties
|
||||||
|
public $user_name;
|
||||||
|
public $societe_name;
|
||||||
|
|
||||||
|
// Action constants
|
||||||
|
const ACTION_CREATE = 'create';
|
||||||
|
const ACTION_UPDATE = 'update';
|
||||||
|
const ACTION_DELETE = 'delete';
|
||||||
|
const ACTION_MOVE = 'move';
|
||||||
|
const ACTION_DUPLICATE = 'duplicate';
|
||||||
|
const ACTION_STATUS_CHANGE = 'status';
|
||||||
|
|
||||||
|
// Object type constants
|
||||||
|
const TYPE_EQUIPMENT = 'equipment';
|
||||||
|
const TYPE_CARRIER = 'carrier';
|
||||||
|
const TYPE_PANEL = 'panel';
|
||||||
|
const TYPE_ANLAGE = 'anlage';
|
||||||
|
const TYPE_CONNECTION = 'connection';
|
||||||
|
const TYPE_BUSBAR = 'busbar';
|
||||||
|
const TYPE_EQUIPMENT_TYPE = 'equipment_type';
|
||||||
|
const TYPE_BUSBAR_TYPE = 'busbar_type';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor
|
||||||
|
*
|
||||||
|
* @param DoliDB $db Database handler
|
||||||
|
*/
|
||||||
|
public function __construct($db)
|
||||||
|
{
|
||||||
|
$this->db = $db;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log an action
|
||||||
|
*
|
||||||
|
* @param User $user User performing the action
|
||||||
|
* @param string $objectType Type of object (equipment, carrier, panel, etc.)
|
||||||
|
* @param int $objectId ID of the object
|
||||||
|
* @param string $action Action performed (create, update, delete, etc.)
|
||||||
|
* @param string $objectRef Reference/label of the object (optional)
|
||||||
|
* @param string $fieldChanged Specific field changed (optional)
|
||||||
|
* @param mixed $oldValue Previous value (optional)
|
||||||
|
* @param mixed $newValue New value (optional)
|
||||||
|
* @param int $socid Customer ID (optional)
|
||||||
|
* @param int $anlageId Anlage ID (optional)
|
||||||
|
* @param string $note Additional note (optional)
|
||||||
|
* @return int Log entry ID or <0 on error
|
||||||
|
*/
|
||||||
|
public function log($user, $objectType, $objectId, $action, $objectRef = '', $fieldChanged = '', $oldValue = null, $newValue = null, $socid = 0, $anlageId = 0, $note = '')
|
||||||
|
{
|
||||||
|
global $conf;
|
||||||
|
|
||||||
|
$now = dol_now();
|
||||||
|
|
||||||
|
// Serialize complex values
|
||||||
|
if (is_array($oldValue) || is_object($oldValue)) {
|
||||||
|
$oldValue = json_encode($oldValue);
|
||||||
|
}
|
||||||
|
if (is_array($newValue) || is_object($newValue)) {
|
||||||
|
$newValue = json_encode($newValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get IP address
|
||||||
|
$ipAddress = '';
|
||||||
|
if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
|
||||||
|
$ipAddress = $_SERVER['HTTP_X_FORWARDED_FOR'];
|
||||||
|
} elseif (!empty($_SERVER['REMOTE_ADDR'])) {
|
||||||
|
$ipAddress = $_SERVER['REMOTE_ADDR'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$sql = "INSERT INTO ".MAIN_DB_PREFIX.$this->table_element." (";
|
||||||
|
$sql .= "entity, object_type, object_id, object_ref, fk_societe, fk_anlage,";
|
||||||
|
$sql .= " action, field_changed, old_value, new_value,";
|
||||||
|
$sql .= " fk_user, user_login, date_action, note, ip_address";
|
||||||
|
$sql .= ") VALUES (";
|
||||||
|
$sql .= ((int) $conf->entity);
|
||||||
|
$sql .= ", '".$this->db->escape($objectType)."'";
|
||||||
|
$sql .= ", ".((int) $objectId);
|
||||||
|
$sql .= ", ".($objectRef ? "'".$this->db->escape($objectRef)."'" : "NULL");
|
||||||
|
$sql .= ", ".($socid > 0 ? ((int) $socid) : "NULL");
|
||||||
|
$sql .= ", ".($anlageId > 0 ? ((int) $anlageId) : "NULL");
|
||||||
|
$sql .= ", '".$this->db->escape($action)."'";
|
||||||
|
$sql .= ", ".($fieldChanged ? "'".$this->db->escape($fieldChanged)."'" : "NULL");
|
||||||
|
$sql .= ", ".($oldValue !== null ? "'".$this->db->escape($oldValue)."'" : "NULL");
|
||||||
|
$sql .= ", ".($newValue !== null ? "'".$this->db->escape($newValue)."'" : "NULL");
|
||||||
|
$sql .= ", ".((int) $user->id);
|
||||||
|
$sql .= ", '".$this->db->escape($user->login)."'";
|
||||||
|
$sql .= ", '".$this->db->idate($now)."'";
|
||||||
|
$sql .= ", ".($note ? "'".$this->db->escape($note)."'" : "NULL");
|
||||||
|
$sql .= ", ".($ipAddress ? "'".$this->db->escape($ipAddress)."'" : "NULL");
|
||||||
|
$sql .= ")";
|
||||||
|
|
||||||
|
$resql = $this->db->query($sql);
|
||||||
|
if (!$resql) {
|
||||||
|
$this->error = $this->db->lasterror();
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->db->last_insert_id(MAIN_DB_PREFIX.$this->table_element);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log object creation
|
||||||
|
*/
|
||||||
|
public function logCreate($user, $objectType, $objectId, $objectRef = '', $socid = 0, $anlageId = 0, $data = null)
|
||||||
|
{
|
||||||
|
return $this->log($user, $objectType, $objectId, self::ACTION_CREATE, $objectRef, '', null, $data, $socid, $anlageId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log object update
|
||||||
|
*/
|
||||||
|
public function logUpdate($user, $objectType, $objectId, $objectRef = '', $fieldChanged = '', $oldValue = null, $newValue = null, $socid = 0, $anlageId = 0)
|
||||||
|
{
|
||||||
|
return $this->log($user, $objectType, $objectId, self::ACTION_UPDATE, $objectRef, $fieldChanged, $oldValue, $newValue, $socid, $anlageId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log object deletion
|
||||||
|
*/
|
||||||
|
public function logDelete($user, $objectType, $objectId, $objectRef = '', $socid = 0, $anlageId = 0, $data = null)
|
||||||
|
{
|
||||||
|
return $this->log($user, $objectType, $objectId, self::ACTION_DELETE, $objectRef, '', $data, null, $socid, $anlageId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log object move (position change)
|
||||||
|
*/
|
||||||
|
public function logMove($user, $objectType, $objectId, $objectRef = '', $oldPosition = null, $newPosition = null, $socid = 0, $anlageId = 0)
|
||||||
|
{
|
||||||
|
return $this->log($user, $objectType, $objectId, self::ACTION_MOVE, $objectRef, 'position', $oldPosition, $newPosition, $socid, $anlageId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log object duplication
|
||||||
|
*/
|
||||||
|
public function logDuplicate($user, $objectType, $objectId, $objectRef = '', $sourceId = 0, $socid = 0, $anlageId = 0)
|
||||||
|
{
|
||||||
|
return $this->log($user, $objectType, $objectId, self::ACTION_DUPLICATE, $objectRef, '', $sourceId, $objectId, $socid, $anlageId, 'Kopiert von ID '.$sourceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch audit log entries for an object
|
||||||
|
*
|
||||||
|
* @param string $objectType Object type
|
||||||
|
* @param int $objectId Object ID
|
||||||
|
* @param int $limit Max entries (0 = no limit)
|
||||||
|
* @return array Array of AuditLog objects
|
||||||
|
*/
|
||||||
|
public function fetchByObject($objectType, $objectId, $limit = 50)
|
||||||
|
{
|
||||||
|
$results = array();
|
||||||
|
|
||||||
|
$sql = "SELECT a.*, u.firstname, u.lastname, s.nom as societe_name";
|
||||||
|
$sql .= " FROM ".MAIN_DB_PREFIX.$this->table_element." as a";
|
||||||
|
$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."user as u ON a.fk_user = u.rowid";
|
||||||
|
$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."societe as s ON a.fk_societe = s.rowid";
|
||||||
|
$sql .= " WHERE a.object_type = '".$this->db->escape($objectType)."'";
|
||||||
|
$sql .= " AND a.object_id = ".((int) $objectId);
|
||||||
|
$sql .= " ORDER BY a.date_action DESC";
|
||||||
|
if ($limit > 0) {
|
||||||
|
$sql .= " LIMIT ".((int) $limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
$resql = $this->db->query($sql);
|
||||||
|
if ($resql) {
|
||||||
|
while ($obj = $this->db->fetch_object($resql)) {
|
||||||
|
$log = new AuditLog($this->db);
|
||||||
|
$log->id = $obj->rowid;
|
||||||
|
$log->object_type = $obj->object_type;
|
||||||
|
$log->object_id = $obj->object_id;
|
||||||
|
$log->object_ref = $obj->object_ref;
|
||||||
|
$log->fk_societe = $obj->fk_societe;
|
||||||
|
$log->fk_anlage = $obj->fk_anlage;
|
||||||
|
$log->action = $obj->action;
|
||||||
|
$log->field_changed = $obj->field_changed;
|
||||||
|
$log->old_value = $obj->old_value;
|
||||||
|
$log->new_value = $obj->new_value;
|
||||||
|
$log->fk_user = $obj->fk_user;
|
||||||
|
$log->user_login = $obj->user_login;
|
||||||
|
$log->date_action = $this->db->jdate($obj->date_action);
|
||||||
|
$log->note = $obj->note;
|
||||||
|
$log->ip_address = $obj->ip_address;
|
||||||
|
$log->user_name = trim($obj->firstname.' '.$obj->lastname);
|
||||||
|
$log->societe_name = $obj->societe_name;
|
||||||
|
|
||||||
|
$results[] = $log;
|
||||||
|
}
|
||||||
|
$this->db->free($resql);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $results;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch audit log entries for an Anlage (installation)
|
||||||
|
*
|
||||||
|
* @param int $anlageId Anlage ID
|
||||||
|
* @param int $limit Max entries
|
||||||
|
* @return array Array of AuditLog objects
|
||||||
|
*/
|
||||||
|
public function fetchByAnlage($anlageId, $limit = 100)
|
||||||
|
{
|
||||||
|
$results = array();
|
||||||
|
|
||||||
|
$sql = "SELECT a.*, u.firstname, u.lastname";
|
||||||
|
$sql .= " FROM ".MAIN_DB_PREFIX.$this->table_element." as a";
|
||||||
|
$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."user as u ON a.fk_user = u.rowid";
|
||||||
|
$sql .= " WHERE a.fk_anlage = ".((int) $anlageId);
|
||||||
|
$sql .= " ORDER BY a.date_action DESC";
|
||||||
|
if ($limit > 0) {
|
||||||
|
$sql .= " LIMIT ".((int) $limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
$resql = $this->db->query($sql);
|
||||||
|
if ($resql) {
|
||||||
|
while ($obj = $this->db->fetch_object($resql)) {
|
||||||
|
$log = new AuditLog($this->db);
|
||||||
|
$log->id = $obj->rowid;
|
||||||
|
$log->object_type = $obj->object_type;
|
||||||
|
$log->object_id = $obj->object_id;
|
||||||
|
$log->object_ref = $obj->object_ref;
|
||||||
|
$log->fk_societe = $obj->fk_societe;
|
||||||
|
$log->fk_anlage = $obj->fk_anlage;
|
||||||
|
$log->action = $obj->action;
|
||||||
|
$log->field_changed = $obj->field_changed;
|
||||||
|
$log->old_value = $obj->old_value;
|
||||||
|
$log->new_value = $obj->new_value;
|
||||||
|
$log->fk_user = $obj->fk_user;
|
||||||
|
$log->user_login = $obj->user_login;
|
||||||
|
$log->date_action = $this->db->jdate($obj->date_action);
|
||||||
|
$log->note = $obj->note;
|
||||||
|
$log->ip_address = $obj->ip_address;
|
||||||
|
$log->user_name = trim($obj->firstname.' '.$obj->lastname);
|
||||||
|
|
||||||
|
$results[] = $log;
|
||||||
|
}
|
||||||
|
$this->db->free($resql);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $results;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch audit log entries for a customer
|
||||||
|
*
|
||||||
|
* @param int $socid Societe ID
|
||||||
|
* @param int $limit Max entries
|
||||||
|
* @return array Array of AuditLog objects
|
||||||
|
*/
|
||||||
|
public function fetchBySociete($socid, $limit = 100)
|
||||||
|
{
|
||||||
|
$results = array();
|
||||||
|
|
||||||
|
$sql = "SELECT a.*, u.firstname, u.lastname";
|
||||||
|
$sql .= " FROM ".MAIN_DB_PREFIX.$this->table_element." as a";
|
||||||
|
$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."user as u ON a.fk_user = u.rowid";
|
||||||
|
$sql .= " WHERE a.fk_societe = ".((int) $socid);
|
||||||
|
$sql .= " ORDER BY a.date_action DESC";
|
||||||
|
if ($limit > 0) {
|
||||||
|
$sql .= " LIMIT ".((int) $limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
$resql = $this->db->query($sql);
|
||||||
|
if ($resql) {
|
||||||
|
while ($obj = $this->db->fetch_object($resql)) {
|
||||||
|
$log = new AuditLog($this->db);
|
||||||
|
$log->id = $obj->rowid;
|
||||||
|
$log->object_type = $obj->object_type;
|
||||||
|
$log->object_id = $obj->object_id;
|
||||||
|
$log->object_ref = $obj->object_ref;
|
||||||
|
$log->fk_societe = $obj->fk_societe;
|
||||||
|
$log->fk_anlage = $obj->fk_anlage;
|
||||||
|
$log->action = $obj->action;
|
||||||
|
$log->field_changed = $obj->field_changed;
|
||||||
|
$log->old_value = $obj->old_value;
|
||||||
|
$log->new_value = $obj->new_value;
|
||||||
|
$log->fk_user = $obj->fk_user;
|
||||||
|
$log->user_login = $obj->user_login;
|
||||||
|
$log->date_action = $this->db->jdate($obj->date_action);
|
||||||
|
$log->note = $obj->note;
|
||||||
|
$log->ip_address = $obj->ip_address;
|
||||||
|
$log->user_name = trim($obj->firstname.' '.$obj->lastname);
|
||||||
|
|
||||||
|
$results[] = $log;
|
||||||
|
}
|
||||||
|
$this->db->free($resql);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $results;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get human-readable action label
|
||||||
|
*
|
||||||
|
* @return string Translated action label
|
||||||
|
*/
|
||||||
|
public function getActionLabel()
|
||||||
|
{
|
||||||
|
global $langs;
|
||||||
|
|
||||||
|
switch ($this->action) {
|
||||||
|
case self::ACTION_CREATE:
|
||||||
|
return $langs->trans('AuditActionCreate');
|
||||||
|
case self::ACTION_UPDATE:
|
||||||
|
return $langs->trans('AuditActionUpdate');
|
||||||
|
case self::ACTION_DELETE:
|
||||||
|
return $langs->trans('AuditActionDelete');
|
||||||
|
case self::ACTION_MOVE:
|
||||||
|
return $langs->trans('AuditActionMove');
|
||||||
|
case self::ACTION_DUPLICATE:
|
||||||
|
return $langs->trans('AuditActionDuplicate');
|
||||||
|
case self::ACTION_STATUS_CHANGE:
|
||||||
|
return $langs->trans('AuditActionStatus');
|
||||||
|
default:
|
||||||
|
return $this->action;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get human-readable object type label
|
||||||
|
*
|
||||||
|
* @return string Translated object type label
|
||||||
|
*/
|
||||||
|
public function getObjectTypeLabel()
|
||||||
|
{
|
||||||
|
global $langs;
|
||||||
|
|
||||||
|
switch ($this->object_type) {
|
||||||
|
case self::TYPE_EQUIPMENT:
|
||||||
|
return $langs->trans('Equipment');
|
||||||
|
case self::TYPE_CARRIER:
|
||||||
|
return $langs->trans('CarrierLabel');
|
||||||
|
case self::TYPE_PANEL:
|
||||||
|
return $langs->trans('PanelLabel');
|
||||||
|
case self::TYPE_ANLAGE:
|
||||||
|
return $langs->trans('Installation');
|
||||||
|
case self::TYPE_CONNECTION:
|
||||||
|
return $langs->trans('Connection');
|
||||||
|
case self::TYPE_BUSBAR:
|
||||||
|
return $langs->trans('Busbar');
|
||||||
|
case self::TYPE_EQUIPMENT_TYPE:
|
||||||
|
return $langs->trans('EquipmentType');
|
||||||
|
case self::TYPE_BUSBAR_TYPE:
|
||||||
|
return $langs->trans('BusbarType');
|
||||||
|
default:
|
||||||
|
return $this->object_type;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get action icon
|
||||||
|
*
|
||||||
|
* @return string FontAwesome icon class
|
||||||
|
*/
|
||||||
|
public function getActionIcon()
|
||||||
|
{
|
||||||
|
switch ($this->action) {
|
||||||
|
case self::ACTION_CREATE:
|
||||||
|
return 'fa-plus-circle';
|
||||||
|
case self::ACTION_UPDATE:
|
||||||
|
return 'fa-edit';
|
||||||
|
case self::ACTION_DELETE:
|
||||||
|
return 'fa-trash';
|
||||||
|
case self::ACTION_MOVE:
|
||||||
|
return 'fa-arrows';
|
||||||
|
case self::ACTION_DUPLICATE:
|
||||||
|
return 'fa-copy';
|
||||||
|
case self::ACTION_STATUS_CHANGE:
|
||||||
|
return 'fa-toggle-on';
|
||||||
|
default:
|
||||||
|
return 'fa-question';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get action color
|
||||||
|
*
|
||||||
|
* @return string CSS color
|
||||||
|
*/
|
||||||
|
public function getActionColor()
|
||||||
|
{
|
||||||
|
switch ($this->action) {
|
||||||
|
case self::ACTION_CREATE:
|
||||||
|
return '#27ae60';
|
||||||
|
case self::ACTION_UPDATE:
|
||||||
|
return '#3498db';
|
||||||
|
case self::ACTION_DELETE:
|
||||||
|
return '#e74c3c';
|
||||||
|
case self::ACTION_MOVE:
|
||||||
|
return '#9b59b6';
|
||||||
|
case self::ACTION_DUPLICATE:
|
||||||
|
return '#f39c12';
|
||||||
|
case self::ACTION_STATUS_CHANGE:
|
||||||
|
return '#1abc9c';
|
||||||
|
default:
|
||||||
|
return '#95a5a6';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clean old audit log entries
|
||||||
|
*
|
||||||
|
* @param int $daysToKeep Number of days to keep (default: 365)
|
||||||
|
* @return int Number of deleted entries or -1 on error
|
||||||
|
*/
|
||||||
|
public function cleanOldEntries($daysToKeep = 365)
|
||||||
|
{
|
||||||
|
$cutoffDate = dol_now() - ($daysToKeep * 24 * 60 * 60);
|
||||||
|
|
||||||
|
$sql = "DELETE FROM ".MAIN_DB_PREFIX.$this->table_element;
|
||||||
|
$sql .= " WHERE date_action < '".$this->db->idate($cutoffDate)."'";
|
||||||
|
|
||||||
|
$resql = $this->db->query($sql);
|
||||||
|
if (!$resql) {
|
||||||
|
$this->error = $this->db->lasterror();
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->db->affected_rows($resql);
|
||||||
|
}
|
||||||
|
}
|
||||||
363
class/buildingtype.class.php
Executable file
363
class/buildingtype.class.php
Executable file
|
|
@ -0,0 +1,363 @@
|
||||||
|
<?php
|
||||||
|
/* Copyright (C) 2026 Alles Watt lauft
|
||||||
|
*
|
||||||
|
* Building Type class (Gebäudetypen)
|
||||||
|
* Global structure elements for all systems
|
||||||
|
*/
|
||||||
|
|
||||||
|
require_once DOL_DOCUMENT_ROOT.'/core/class/commonobject.class.php';
|
||||||
|
|
||||||
|
class BuildingType extends CommonObject
|
||||||
|
{
|
||||||
|
public $element = 'buildingtype';
|
||||||
|
public $table_element = 'kundenkarte_building_type';
|
||||||
|
|
||||||
|
// Level type constants
|
||||||
|
const LEVEL_BUILDING = 'building';
|
||||||
|
const LEVEL_FLOOR = 'floor';
|
||||||
|
const LEVEL_WING = 'wing';
|
||||||
|
const LEVEL_CORRIDOR = 'corridor';
|
||||||
|
const LEVEL_ROOM = 'room';
|
||||||
|
const LEVEL_AREA = 'area';
|
||||||
|
|
||||||
|
public $id;
|
||||||
|
public $entity;
|
||||||
|
public $ref;
|
||||||
|
public $label;
|
||||||
|
public $label_short;
|
||||||
|
public $description;
|
||||||
|
public $fk_parent;
|
||||||
|
public $level_type;
|
||||||
|
public $icon;
|
||||||
|
public $color;
|
||||||
|
public $picto;
|
||||||
|
public $is_system;
|
||||||
|
public $can_have_children;
|
||||||
|
public $position;
|
||||||
|
public $active;
|
||||||
|
public $date_creation;
|
||||||
|
public $tms;
|
||||||
|
public $fk_user_creat;
|
||||||
|
public $fk_user_modif;
|
||||||
|
|
||||||
|
// Loaded parent info
|
||||||
|
public $parent_label;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor
|
||||||
|
*
|
||||||
|
* @param DoliDB $db Database handler
|
||||||
|
*/
|
||||||
|
public function __construct($db)
|
||||||
|
{
|
||||||
|
$this->db = $db;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create building type
|
||||||
|
*
|
||||||
|
* @param User $user User object
|
||||||
|
* @return int >0 if OK, <0 if KO
|
||||||
|
*/
|
||||||
|
public function create($user)
|
||||||
|
{
|
||||||
|
global $conf;
|
||||||
|
|
||||||
|
$now = dol_now();
|
||||||
|
$this->ref = trim($this->ref);
|
||||||
|
$this->label = trim($this->label);
|
||||||
|
|
||||||
|
$sql = "INSERT INTO ".MAIN_DB_PREFIX.$this->table_element." (";
|
||||||
|
$sql .= "entity, ref, label, label_short, description, fk_parent, level_type,";
|
||||||
|
$sql .= "icon, color, picto, is_system, can_have_children, position, active,";
|
||||||
|
$sql .= "date_creation, fk_user_creat";
|
||||||
|
$sql .= ") VALUES (";
|
||||||
|
$sql .= (int)$conf->entity;
|
||||||
|
$sql .= ", '".$this->db->escape($this->ref)."'";
|
||||||
|
$sql .= ", '".$this->db->escape($this->label)."'";
|
||||||
|
$sql .= ", ".($this->label_short ? "'".$this->db->escape($this->label_short)."'" : "NULL");
|
||||||
|
$sql .= ", ".($this->description ? "'".$this->db->escape($this->description)."'" : "NULL");
|
||||||
|
$sql .= ", ".(int)($this->fk_parent ?: 0);
|
||||||
|
$sql .= ", ".($this->level_type ? "'".$this->db->escape($this->level_type)."'" : "NULL");
|
||||||
|
$sql .= ", ".($this->icon ? "'".$this->db->escape($this->icon)."'" : "NULL");
|
||||||
|
$sql .= ", ".($this->color ? "'".$this->db->escape($this->color)."'" : "NULL");
|
||||||
|
$sql .= ", ".($this->picto ? "'".$this->db->escape($this->picto)."'" : "NULL");
|
||||||
|
$sql .= ", ".(int)($this->is_system ?: 0);
|
||||||
|
$sql .= ", ".(int)($this->can_have_children !== null ? $this->can_have_children : 1);
|
||||||
|
$sql .= ", ".(int)($this->position ?: 0);
|
||||||
|
$sql .= ", ".(int)($this->active !== null ? $this->active : 1);
|
||||||
|
$sql .= ", '".$this->db->idate($now)."'";
|
||||||
|
$sql .= ", ".(int)$user->id;
|
||||||
|
$sql .= ")";
|
||||||
|
|
||||||
|
$resql = $this->db->query($sql);
|
||||||
|
if ($resql) {
|
||||||
|
$this->id = $this->db->last_insert_id(MAIN_DB_PREFIX.$this->table_element);
|
||||||
|
return $this->id;
|
||||||
|
} else {
|
||||||
|
$this->error = $this->db->lasterror();
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch building type
|
||||||
|
*
|
||||||
|
* @param int $id ID
|
||||||
|
* @param string $ref Reference
|
||||||
|
* @return int >0 if OK, <0 if KO
|
||||||
|
*/
|
||||||
|
public function fetch($id, $ref = '')
|
||||||
|
{
|
||||||
|
$sql = "SELECT t.*, p.label as parent_label";
|
||||||
|
$sql .= " FROM ".MAIN_DB_PREFIX.$this->table_element." as t";
|
||||||
|
$sql .= " LEFT JOIN ".MAIN_DB_PREFIX.$this->table_element." as p ON t.fk_parent = p.rowid";
|
||||||
|
$sql .= " WHERE ";
|
||||||
|
if ($id > 0) {
|
||||||
|
$sql .= "t.rowid = ".(int)$id;
|
||||||
|
} else {
|
||||||
|
$sql .= "t.ref = '".$this->db->escape($ref)."'";
|
||||||
|
}
|
||||||
|
|
||||||
|
$resql = $this->db->query($sql);
|
||||||
|
if ($resql) {
|
||||||
|
if ($obj = $this->db->fetch_object($resql)) {
|
||||||
|
$this->id = $obj->rowid;
|
||||||
|
$this->entity = $obj->entity;
|
||||||
|
$this->ref = $obj->ref;
|
||||||
|
$this->label = $obj->label;
|
||||||
|
$this->label_short = $obj->label_short;
|
||||||
|
$this->description = $obj->description;
|
||||||
|
$this->fk_parent = $obj->fk_parent;
|
||||||
|
$this->level_type = $obj->level_type;
|
||||||
|
$this->icon = $obj->icon;
|
||||||
|
$this->color = $obj->color;
|
||||||
|
$this->picto = $obj->picto;
|
||||||
|
$this->is_system = $obj->is_system;
|
||||||
|
$this->can_have_children = $obj->can_have_children;
|
||||||
|
$this->position = $obj->position;
|
||||||
|
$this->active = $obj->active;
|
||||||
|
$this->date_creation = $this->db->jdate($obj->date_creation);
|
||||||
|
$this->fk_user_creat = $obj->fk_user_creat;
|
||||||
|
$this->fk_user_modif = $obj->fk_user_modif;
|
||||||
|
$this->parent_label = $obj->parent_label;
|
||||||
|
|
||||||
|
$this->db->free($resql);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
$this->db->free($resql);
|
||||||
|
return 0;
|
||||||
|
} else {
|
||||||
|
$this->error = $this->db->lasterror();
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update building type
|
||||||
|
*
|
||||||
|
* @param User $user User object
|
||||||
|
* @return int >0 if OK, <0 if KO
|
||||||
|
*/
|
||||||
|
public function update($user)
|
||||||
|
{
|
||||||
|
$sql = "UPDATE ".MAIN_DB_PREFIX.$this->table_element." SET";
|
||||||
|
$sql .= " ref = '".$this->db->escape($this->ref)."'";
|
||||||
|
$sql .= ", label = '".$this->db->escape($this->label)."'";
|
||||||
|
$sql .= ", label_short = ".($this->label_short ? "'".$this->db->escape($this->label_short)."'" : "NULL");
|
||||||
|
$sql .= ", description = ".($this->description ? "'".$this->db->escape($this->description)."'" : "NULL");
|
||||||
|
$sql .= ", fk_parent = ".(int)($this->fk_parent ?: 0);
|
||||||
|
$sql .= ", level_type = ".($this->level_type ? "'".$this->db->escape($this->level_type)."'" : "NULL");
|
||||||
|
$sql .= ", icon = ".($this->icon ? "'".$this->db->escape($this->icon)."'" : "NULL");
|
||||||
|
$sql .= ", color = ".($this->color ? "'".$this->db->escape($this->color)."'" : "NULL");
|
||||||
|
$sql .= ", can_have_children = ".(int)$this->can_have_children;
|
||||||
|
$sql .= ", position = ".(int)$this->position;
|
||||||
|
$sql .= ", active = ".(int)$this->active;
|
||||||
|
$sql .= ", fk_user_modif = ".(int)$user->id;
|
||||||
|
$sql .= " WHERE rowid = ".(int)$this->id;
|
||||||
|
|
||||||
|
$resql = $this->db->query($sql);
|
||||||
|
if ($resql) {
|
||||||
|
return 1;
|
||||||
|
} else {
|
||||||
|
$this->error = $this->db->lasterror();
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete building type
|
||||||
|
*
|
||||||
|
* @param User $user User object
|
||||||
|
* @return int >0 if OK, <0 if KO
|
||||||
|
*/
|
||||||
|
public function delete($user)
|
||||||
|
{
|
||||||
|
// Don't allow deleting system types
|
||||||
|
if ($this->is_system) {
|
||||||
|
$this->error = 'CannotDeleteSystemType';
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if type is used as parent
|
||||||
|
$sql = "SELECT COUNT(*) as cnt FROM ".MAIN_DB_PREFIX.$this->table_element;
|
||||||
|
$sql .= " WHERE fk_parent = ".(int)$this->id;
|
||||||
|
$resql = $this->db->query($sql);
|
||||||
|
if ($resql) {
|
||||||
|
$obj = $this->db->fetch_object($resql);
|
||||||
|
if ($obj->cnt > 0) {
|
||||||
|
$this->error = 'CannotDeleteTypeWithChildren';
|
||||||
|
return -2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$sql = "DELETE FROM ".MAIN_DB_PREFIX.$this->table_element;
|
||||||
|
$sql .= " WHERE rowid = ".(int)$this->id;
|
||||||
|
|
||||||
|
$resql = $this->db->query($sql);
|
||||||
|
if ($resql) {
|
||||||
|
return 1;
|
||||||
|
} else {
|
||||||
|
$this->error = $this->db->lasterror();
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch all building types
|
||||||
|
*
|
||||||
|
* @param int $activeOnly Only active types
|
||||||
|
* @param string $levelType Filter by level type
|
||||||
|
* @return array Array of BuildingType objects
|
||||||
|
*/
|
||||||
|
public function fetchAll($activeOnly = 1, $levelType = '')
|
||||||
|
{
|
||||||
|
global $conf;
|
||||||
|
|
||||||
|
$result = array();
|
||||||
|
|
||||||
|
$sql = "SELECT t.*, p.label as parent_label";
|
||||||
|
$sql .= " FROM ".MAIN_DB_PREFIX.$this->table_element." as t";
|
||||||
|
$sql .= " LEFT JOIN ".MAIN_DB_PREFIX.$this->table_element." as p ON t.fk_parent = p.rowid";
|
||||||
|
$sql .= " WHERE (t.entity = ".(int)$conf->entity." OR t.entity = 0)";
|
||||||
|
if ($activeOnly) {
|
||||||
|
$sql .= " AND t.active = 1";
|
||||||
|
}
|
||||||
|
if ($levelType) {
|
||||||
|
$sql .= " AND t.level_type = '".$this->db->escape($levelType)."'";
|
||||||
|
}
|
||||||
|
$sql .= " ORDER BY t.level_type, t.position, t.label";
|
||||||
|
|
||||||
|
$resql = $this->db->query($sql);
|
||||||
|
if ($resql) {
|
||||||
|
while ($obj = $this->db->fetch_object($resql)) {
|
||||||
|
$type = new BuildingType($this->db);
|
||||||
|
$type->id = $obj->rowid;
|
||||||
|
$type->entity = $obj->entity;
|
||||||
|
$type->ref = $obj->ref;
|
||||||
|
$type->label = $obj->label;
|
||||||
|
$type->label_short = $obj->label_short;
|
||||||
|
$type->description = $obj->description;
|
||||||
|
$type->fk_parent = $obj->fk_parent;
|
||||||
|
$type->level_type = $obj->level_type;
|
||||||
|
$type->icon = $obj->icon;
|
||||||
|
$type->color = $obj->color;
|
||||||
|
$type->picto = $obj->picto;
|
||||||
|
$type->is_system = $obj->is_system;
|
||||||
|
$type->can_have_children = $obj->can_have_children;
|
||||||
|
$type->position = $obj->position;
|
||||||
|
$type->active = $obj->active;
|
||||||
|
$type->parent_label = $obj->parent_label;
|
||||||
|
$result[] = $type;
|
||||||
|
}
|
||||||
|
$this->db->free($resql);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch types grouped by level type
|
||||||
|
*
|
||||||
|
* @param int $activeOnly Only active types
|
||||||
|
* @return array Array grouped by level_type
|
||||||
|
*/
|
||||||
|
public function fetchGroupedByLevel($activeOnly = 1)
|
||||||
|
{
|
||||||
|
$all = $this->fetchAll($activeOnly);
|
||||||
|
$grouped = array();
|
||||||
|
|
||||||
|
foreach ($all as $type) {
|
||||||
|
$level = $type->level_type ?: 'other';
|
||||||
|
if (!isset($grouped[$level])) {
|
||||||
|
$grouped[$level] = array();
|
||||||
|
}
|
||||||
|
$grouped[$level][] = $type;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $grouped;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get level type label
|
||||||
|
*
|
||||||
|
* @return string Translated label
|
||||||
|
*/
|
||||||
|
public function getLevelTypeLabel()
|
||||||
|
{
|
||||||
|
global $langs;
|
||||||
|
$langs->load('kundenkarte@kundenkarte');
|
||||||
|
|
||||||
|
$labels = array(
|
||||||
|
self::LEVEL_BUILDING => $langs->trans('BuildingLevelBuilding'),
|
||||||
|
self::LEVEL_FLOOR => $langs->trans('BuildingLevelFloor'),
|
||||||
|
self::LEVEL_WING => $langs->trans('BuildingLevelWing'),
|
||||||
|
self::LEVEL_CORRIDOR => $langs->trans('BuildingLevelCorridor'),
|
||||||
|
self::LEVEL_ROOM => $langs->trans('BuildingLevelRoom'),
|
||||||
|
self::LEVEL_AREA => $langs->trans('BuildingLevelArea'),
|
||||||
|
);
|
||||||
|
|
||||||
|
return isset($labels[$this->level_type]) ? $labels[$this->level_type] : $this->level_type;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all level types with labels
|
||||||
|
*
|
||||||
|
* @return array Array of level_type => label
|
||||||
|
*/
|
||||||
|
public static function getLevelTypes()
|
||||||
|
{
|
||||||
|
global $langs;
|
||||||
|
$langs->load('kundenkarte@kundenkarte');
|
||||||
|
|
||||||
|
return array(
|
||||||
|
self::LEVEL_BUILDING => $langs->trans('BuildingLevelBuilding'),
|
||||||
|
self::LEVEL_FLOOR => $langs->trans('BuildingLevelFloor'),
|
||||||
|
self::LEVEL_WING => $langs->trans('BuildingLevelWing'),
|
||||||
|
self::LEVEL_CORRIDOR => $langs->trans('BuildingLevelCorridor'),
|
||||||
|
self::LEVEL_ROOM => $langs->trans('BuildingLevelRoom'),
|
||||||
|
self::LEVEL_AREA => $langs->trans('BuildingLevelArea'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get next available position
|
||||||
|
*
|
||||||
|
* @param string $levelType Level type
|
||||||
|
* @return int Next position
|
||||||
|
*/
|
||||||
|
public function getNextPosition($levelType = '')
|
||||||
|
{
|
||||||
|
$sql = "SELECT MAX(position) as maxpos FROM ".MAIN_DB_PREFIX.$this->table_element;
|
||||||
|
if ($levelType) {
|
||||||
|
$sql .= " WHERE level_type = '".$this->db->escape($levelType)."'";
|
||||||
|
}
|
||||||
|
|
||||||
|
$resql = $this->db->query($sql);
|
||||||
|
if ($resql) {
|
||||||
|
$obj = $this->db->fetch_object($resql);
|
||||||
|
return ($obj->maxpos ?: 0) + 10;
|
||||||
|
}
|
||||||
|
return 10;
|
||||||
|
}
|
||||||
|
}
|
||||||
391
class/busbartype.class.php
Executable file
391
class/busbartype.class.php
Executable file
|
|
@ -0,0 +1,391 @@
|
||||||
|
<?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 BusbarType
|
||||||
|
* Manages busbar type templates (Sammelschienen-Typen)
|
||||||
|
*/
|
||||||
|
class BusbarType extends CommonObject
|
||||||
|
{
|
||||||
|
public $element = 'busbartype';
|
||||||
|
public $table_element = 'kundenkarte_busbar_type';
|
||||||
|
|
||||||
|
public $ref;
|
||||||
|
public $label;
|
||||||
|
public $label_short;
|
||||||
|
public $description;
|
||||||
|
public $fk_system;
|
||||||
|
|
||||||
|
// Busbar-spezifische Felder
|
||||||
|
public $phases; // Channel configuration (A, B, AB, ABC, or legacy L1, L2, L3, N, PE, etc.)
|
||||||
|
public $num_lines = 1; // Anzahl der Linien
|
||||||
|
public $color; // Kommagetrennte Farben
|
||||||
|
public $default_color; // Standard-Einzelfarbe
|
||||||
|
public $line_height = 3;
|
||||||
|
public $line_spacing = 4;
|
||||||
|
public $position_default = 'below';
|
||||||
|
|
||||||
|
public $fk_product;
|
||||||
|
public $picto;
|
||||||
|
public $icon_file;
|
||||||
|
public $is_system;
|
||||||
|
public $position;
|
||||||
|
public $active;
|
||||||
|
|
||||||
|
public $date_creation;
|
||||||
|
public $fk_user_creat;
|
||||||
|
public $fk_user_modif;
|
||||||
|
|
||||||
|
// Loaded objects
|
||||||
|
public $system_label;
|
||||||
|
public $system_code;
|
||||||
|
public $product_ref;
|
||||||
|
public $product_label;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor
|
||||||
|
*
|
||||||
|
* @param DoliDB $db Database handler
|
||||||
|
*/
|
||||||
|
public function __construct($db)
|
||||||
|
{
|
||||||
|
$this->db = $db;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create object in database
|
||||||
|
*
|
||||||
|
* @param User $user User that creates
|
||||||
|
* @return int Return integer <0 if KO, Id of created object if OK
|
||||||
|
*/
|
||||||
|
public function create($user)
|
||||||
|
{
|
||||||
|
global $conf;
|
||||||
|
|
||||||
|
$error = 0;
|
||||||
|
$now = dol_now();
|
||||||
|
|
||||||
|
if (empty($this->ref) || empty($this->label) || empty($this->phases)) {
|
||||||
|
$this->error = 'ErrorMissingParameters';
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->db->begin();
|
||||||
|
|
||||||
|
$sql = "INSERT INTO ".MAIN_DB_PREFIX.$this->table_element." (";
|
||||||
|
$sql .= "entity, ref, label, label_short, description, fk_system,";
|
||||||
|
$sql .= " phases, num_lines, color, default_color, line_height, line_spacing, position_default,";
|
||||||
|
$sql .= " fk_product, picto, icon_file, is_system, position, active,";
|
||||||
|
$sql .= " date_creation, fk_user_creat";
|
||||||
|
$sql .= ") VALUES (";
|
||||||
|
$sql .= "0"; // entity 0 = global
|
||||||
|
$sql .= ", '".$this->db->escape($this->ref)."'";
|
||||||
|
$sql .= ", '".$this->db->escape($this->label)."'";
|
||||||
|
$sql .= ", ".($this->label_short ? "'".$this->db->escape($this->label_short)."'" : "NULL");
|
||||||
|
$sql .= ", ".($this->description ? "'".$this->db->escape($this->description)."'" : "NULL");
|
||||||
|
$sql .= ", ".((int) $this->fk_system);
|
||||||
|
$sql .= ", '".$this->db->escape($this->phases)."'";
|
||||||
|
$sql .= ", ".((int) ($this->num_lines > 0 ? $this->num_lines : 1));
|
||||||
|
$sql .= ", ".($this->color ? "'".$this->db->escape($this->color)."'" : "NULL");
|
||||||
|
$sql .= ", ".($this->default_color ? "'".$this->db->escape($this->default_color)."'" : "NULL");
|
||||||
|
$sql .= ", ".((int) ($this->line_height > 0 ? $this->line_height : 3));
|
||||||
|
$sql .= ", ".((int) ($this->line_spacing > 0 ? $this->line_spacing : 4));
|
||||||
|
$sql .= ", '".$this->db->escape($this->position_default ?: 'below')."'";
|
||||||
|
$sql .= ", ".($this->fk_product > 0 ? ((int) $this->fk_product) : "NULL");
|
||||||
|
$sql .= ", ".($this->picto ? "'".$this->db->escape($this->picto)."'" : "NULL");
|
||||||
|
$sql .= ", ".($this->icon_file ? "'".$this->db->escape($this->icon_file)."'" : "NULL");
|
||||||
|
$sql .= ", 0"; // is_system = 0 for user-created
|
||||||
|
$sql .= ", ".((int) $this->position);
|
||||||
|
$sql .= ", ".((int) ($this->active !== null ? $this->active : 1));
|
||||||
|
$sql .= ", '".$this->db->idate($now)."'";
|
||||||
|
$sql .= ", ".((int) $user->id);
|
||||||
|
$sql .= ")";
|
||||||
|
|
||||||
|
$resql = $this->db->query($sql);
|
||||||
|
if (!$resql) {
|
||||||
|
$error++;
|
||||||
|
$this->errors[] = "Error ".$this->db->lasterror();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$error) {
|
||||||
|
$this->id = $this->db->last_insert_id(MAIN_DB_PREFIX.$this->table_element);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($error) {
|
||||||
|
$this->db->rollback();
|
||||||
|
return -1 * $error;
|
||||||
|
} else {
|
||||||
|
$this->db->commit();
|
||||||
|
return $this->id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load object from database
|
||||||
|
*
|
||||||
|
* @param int $id ID of record
|
||||||
|
* @return int Return integer <0 if KO, 0 if not found, >0 if OK
|
||||||
|
*/
|
||||||
|
public function fetch($id)
|
||||||
|
{
|
||||||
|
$sql = "SELECT t.*, s.label as system_label, s.code as system_code,";
|
||||||
|
$sql .= " p.ref as product_ref, p.label as product_label";
|
||||||
|
$sql .= " FROM ".MAIN_DB_PREFIX.$this->table_element." as t";
|
||||||
|
$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."c_kundenkarte_anlage_system as s ON t.fk_system = s.rowid";
|
||||||
|
$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."product as p ON t.fk_product = p.rowid";
|
||||||
|
$sql .= " WHERE t.rowid = ".((int) $id);
|
||||||
|
|
||||||
|
$resql = $this->db->query($sql);
|
||||||
|
if ($resql) {
|
||||||
|
if ($this->db->num_rows($resql)) {
|
||||||
|
$obj = $this->db->fetch_object($resql);
|
||||||
|
|
||||||
|
$this->id = $obj->rowid;
|
||||||
|
$this->entity = $obj->entity;
|
||||||
|
$this->ref = $obj->ref;
|
||||||
|
$this->label = $obj->label;
|
||||||
|
$this->label_short = $obj->label_short;
|
||||||
|
$this->description = $obj->description;
|
||||||
|
$this->fk_system = $obj->fk_system;
|
||||||
|
$this->phases = $obj->phases;
|
||||||
|
$this->num_lines = $obj->num_lines;
|
||||||
|
$this->color = $obj->color;
|
||||||
|
$this->default_color = $obj->default_color;
|
||||||
|
$this->line_height = $obj->line_height;
|
||||||
|
$this->line_spacing = $obj->line_spacing;
|
||||||
|
$this->position_default = $obj->position_default ?: 'below';
|
||||||
|
$this->fk_product = $obj->fk_product;
|
||||||
|
$this->picto = $obj->picto;
|
||||||
|
$this->icon_file = $obj->icon_file;
|
||||||
|
$this->is_system = $obj->is_system;
|
||||||
|
$this->position = $obj->position;
|
||||||
|
$this->active = $obj->active;
|
||||||
|
$this->date_creation = $this->db->jdate($obj->date_creation);
|
||||||
|
$this->fk_user_creat = $obj->fk_user_creat;
|
||||||
|
$this->fk_user_modif = $obj->fk_user_modif;
|
||||||
|
|
||||||
|
$this->system_label = $obj->system_label;
|
||||||
|
$this->system_code = $obj->system_code;
|
||||||
|
$this->product_ref = $obj->product_ref;
|
||||||
|
$this->product_label = $obj->product_label;
|
||||||
|
|
||||||
|
$this->db->free($resql);
|
||||||
|
return 1;
|
||||||
|
} else {
|
||||||
|
$this->db->free($resql);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$this->error = $this->db->lasterror();
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update object in database
|
||||||
|
*
|
||||||
|
* @param User $user User that modifies
|
||||||
|
* @return int Return integer <0 if KO, >0 if OK
|
||||||
|
*/
|
||||||
|
public function update($user)
|
||||||
|
{
|
||||||
|
$error = 0;
|
||||||
|
|
||||||
|
$this->db->begin();
|
||||||
|
|
||||||
|
$sql = "UPDATE ".MAIN_DB_PREFIX.$this->table_element." SET";
|
||||||
|
$sql .= " ref = '".$this->db->escape($this->ref)."'";
|
||||||
|
$sql .= ", label = '".$this->db->escape($this->label)."'";
|
||||||
|
$sql .= ", label_short = ".($this->label_short ? "'".$this->db->escape($this->label_short)."'" : "NULL");
|
||||||
|
$sql .= ", description = ".($this->description ? "'".$this->db->escape($this->description)."'" : "NULL");
|
||||||
|
$sql .= ", fk_system = ".((int) $this->fk_system);
|
||||||
|
$sql .= ", phases = '".$this->db->escape($this->phases)."'";
|
||||||
|
$sql .= ", num_lines = ".((int) ($this->num_lines > 0 ? $this->num_lines : 1));
|
||||||
|
$sql .= ", color = ".($this->color ? "'".$this->db->escape($this->color)."'" : "NULL");
|
||||||
|
$sql .= ", default_color = ".($this->default_color ? "'".$this->db->escape($this->default_color)."'" : "NULL");
|
||||||
|
$sql .= ", line_height = ".((int) ($this->line_height > 0 ? $this->line_height : 3));
|
||||||
|
$sql .= ", line_spacing = ".((int) ($this->line_spacing > 0 ? $this->line_spacing : 4));
|
||||||
|
$sql .= ", position_default = '".$this->db->escape($this->position_default ?: 'below')."'";
|
||||||
|
$sql .= ", fk_product = ".($this->fk_product > 0 ? ((int) $this->fk_product) : "NULL");
|
||||||
|
$sql .= ", picto = ".($this->picto ? "'".$this->db->escape($this->picto)."'" : "NULL");
|
||||||
|
$sql .= ", icon_file = ".($this->icon_file ? "'".$this->db->escape($this->icon_file)."'" : "NULL");
|
||||||
|
$sql .= ", position = ".((int) $this->position);
|
||||||
|
$sql .= ", active = ".((int) $this->active);
|
||||||
|
$sql .= ", fk_user_modif = ".((int) $user->id);
|
||||||
|
$sql .= " WHERE rowid = ".((int) $this->id);
|
||||||
|
|
||||||
|
$resql = $this->db->query($sql);
|
||||||
|
if (!$resql) {
|
||||||
|
$error++;
|
||||||
|
$this->errors[] = "Error ".$this->db->lasterror();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($error) {
|
||||||
|
$this->db->rollback();
|
||||||
|
return -1 * $error;
|
||||||
|
} else {
|
||||||
|
$this->db->commit();
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete object in database
|
||||||
|
*
|
||||||
|
* @param User $user User that deletes
|
||||||
|
* @return int Return integer <0 if KO, >0 if OK
|
||||||
|
*/
|
||||||
|
public function delete($user)
|
||||||
|
{
|
||||||
|
global $conf;
|
||||||
|
|
||||||
|
// Check if type is in use (connections referencing this type)
|
||||||
|
$sql = "SELECT COUNT(*) as cnt FROM ".MAIN_DB_PREFIX."kundenkarte_equipment_connection";
|
||||||
|
$sql .= " WHERE fk_busbar_type = ".((int) $this->id);
|
||||||
|
$resql = $this->db->query($sql);
|
||||||
|
if ($resql) {
|
||||||
|
$obj = $this->db->fetch_object($resql);
|
||||||
|
if ($obj->cnt > 0) {
|
||||||
|
$this->error = 'ErrorTypeInUse';
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cannot delete system types
|
||||||
|
if ($this->is_system) {
|
||||||
|
$this->error = 'ErrorCannotDeleteSystemType';
|
||||||
|
return -2;
|
||||||
|
}
|
||||||
|
|
||||||
|
$error = 0;
|
||||||
|
$this->db->begin();
|
||||||
|
|
||||||
|
$sql = "DELETE FROM ".MAIN_DB_PREFIX.$this->table_element." WHERE rowid = ".((int) $this->id);
|
||||||
|
$resql = $this->db->query($sql);
|
||||||
|
if (!$resql) {
|
||||||
|
$error++;
|
||||||
|
$this->errors[] = "Error ".$this->db->lasterror();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($error) {
|
||||||
|
$this->db->rollback();
|
||||||
|
return -1 * $error;
|
||||||
|
} else {
|
||||||
|
$this->db->commit();
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch all busbar types for a system
|
||||||
|
*
|
||||||
|
* @param int $systemId System ID (0 = all)
|
||||||
|
* @param int $activeOnly Only active types
|
||||||
|
* @return array Array of BusbarType objects
|
||||||
|
*/
|
||||||
|
public function fetchAllBySystem($systemId = 0, $activeOnly = 1)
|
||||||
|
{
|
||||||
|
$results = array();
|
||||||
|
|
||||||
|
$sql = "SELECT t.*, s.label as system_label, s.code as system_code";
|
||||||
|
$sql .= " FROM ".MAIN_DB_PREFIX.$this->table_element." as t";
|
||||||
|
$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."c_kundenkarte_anlage_system as s ON t.fk_system = s.rowid";
|
||||||
|
$sql .= " WHERE 1 = 1";
|
||||||
|
if ($systemId > 0) {
|
||||||
|
// Show types for this system AND global types (fk_system = 0 or NULL)
|
||||||
|
$sql .= " AND (t.fk_system = ".((int) $systemId)." OR t.fk_system = 0 OR t.fk_system IS NULL)";
|
||||||
|
}
|
||||||
|
if ($activeOnly) {
|
||||||
|
$sql .= " AND t.active = 1";
|
||||||
|
}
|
||||||
|
$sql .= " ORDER BY t.fk_system ASC, t.position ASC, t.label ASC";
|
||||||
|
|
||||||
|
$resql = $this->db->query($sql);
|
||||||
|
if ($resql) {
|
||||||
|
while ($obj = $this->db->fetch_object($resql)) {
|
||||||
|
$type = new BusbarType($this->db);
|
||||||
|
$type->id = $obj->rowid;
|
||||||
|
$type->ref = $obj->ref;
|
||||||
|
$type->label = $obj->label;
|
||||||
|
$type->label_short = $obj->label_short;
|
||||||
|
$type->fk_system = $obj->fk_system;
|
||||||
|
$type->phases = $obj->phases;
|
||||||
|
$type->num_lines = $obj->num_lines;
|
||||||
|
$type->color = $obj->color;
|
||||||
|
$type->default_color = $obj->default_color;
|
||||||
|
$type->line_height = $obj->line_height;
|
||||||
|
$type->line_spacing = $obj->line_spacing;
|
||||||
|
$type->position_default = $obj->position_default;
|
||||||
|
$type->fk_product = $obj->fk_product;
|
||||||
|
$type->picto = $obj->picto;
|
||||||
|
$type->icon_file = $obj->icon_file;
|
||||||
|
$type->is_system = $obj->is_system;
|
||||||
|
$type->position = $obj->position;
|
||||||
|
$type->active = $obj->active;
|
||||||
|
$type->system_label = $obj->system_label;
|
||||||
|
$type->system_code = $obj->system_code;
|
||||||
|
|
||||||
|
$results[] = $type;
|
||||||
|
}
|
||||||
|
$this->db->free($resql);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $results;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get color array from comma-separated string
|
||||||
|
*
|
||||||
|
* @return array Array of color codes
|
||||||
|
*/
|
||||||
|
public function getColors()
|
||||||
|
{
|
||||||
|
if (empty($this->color)) {
|
||||||
|
return array($this->default_color ?: '#e74c3c');
|
||||||
|
}
|
||||||
|
return explode(',', $this->color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get phase labels array from phases string
|
||||||
|
*
|
||||||
|
* @return array Array of phase labels
|
||||||
|
*/
|
||||||
|
public function getPhaseLabels()
|
||||||
|
{
|
||||||
|
$phases = $this->phases;
|
||||||
|
|
||||||
|
// Parse common phase configurations
|
||||||
|
switch (strtoupper($phases)) {
|
||||||
|
case 'L1':
|
||||||
|
return array('L1');
|
||||||
|
case 'L2':
|
||||||
|
return array('L2');
|
||||||
|
case 'L3':
|
||||||
|
return array('L3');
|
||||||
|
case 'N':
|
||||||
|
return array('N');
|
||||||
|
case 'PE':
|
||||||
|
return array('PE');
|
||||||
|
case 'L1N':
|
||||||
|
return array('L1', 'N');
|
||||||
|
case '3P':
|
||||||
|
return array('L1', 'L2', 'L3');
|
||||||
|
case '3P+N':
|
||||||
|
case '3PN':
|
||||||
|
return array('L1', 'L2', 'L3', 'N');
|
||||||
|
case '3P+N+PE':
|
||||||
|
case '3PNPE':
|
||||||
|
return array('L1', 'L2', 'L3', 'N', 'PE');
|
||||||
|
default:
|
||||||
|
// Try to split by comma or +
|
||||||
|
return preg_split('/[,+]/', $phases);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
514
class/equipment.class.php
Executable file
514
class/equipment.class.php
Executable file
|
|
@ -0,0 +1,514 @@
|
||||||
|
<?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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
require_once DOL_DOCUMENT_ROOT.'/custom/kundenkarte/class/equipmenttype.class.php';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class Equipment
|
||||||
|
* Manages equipment instances (Sicherungsautomaten, FI-Schalter, etc.)
|
||||||
|
*/
|
||||||
|
class Equipment extends CommonObject
|
||||||
|
{
|
||||||
|
public $element = 'equipment';
|
||||||
|
public $table_element = 'kundenkarte_equipment';
|
||||||
|
|
||||||
|
public $fk_carrier;
|
||||||
|
public $fk_equipment_type;
|
||||||
|
public $label;
|
||||||
|
public $position_te;
|
||||||
|
public $width_te;
|
||||||
|
public $field_values;
|
||||||
|
public $fk_product;
|
||||||
|
public $fk_protection; // FK to protection device (FI/RCD)
|
||||||
|
public $protection_label; // Label shown above equipment when in protection group
|
||||||
|
public $note_private;
|
||||||
|
public $status;
|
||||||
|
|
||||||
|
public $date_creation;
|
||||||
|
public $fk_user_creat;
|
||||||
|
public $fk_user_modif;
|
||||||
|
|
||||||
|
// Loaded objects
|
||||||
|
public $type; // EquipmentType object
|
||||||
|
public $type_label;
|
||||||
|
public $type_label_short;
|
||||||
|
public $type_color;
|
||||||
|
public $type_picto;
|
||||||
|
public $type_icon_file; // SVG/PNG schematic symbol (PDF)
|
||||||
|
public $type_block_image; // Image for block display in SchematicEditor
|
||||||
|
public $type_flow_direction; // Richtung: NULL, top_to_bottom, bottom_to_top
|
||||||
|
public $type_terminal_position; // Terminal-Position: both, top_only, bottom_only
|
||||||
|
public $product_ref;
|
||||||
|
public $product_label;
|
||||||
|
public $protection_device_label; // Label of the protection device
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor
|
||||||
|
*
|
||||||
|
* @param DoliDB $db Database handler
|
||||||
|
*/
|
||||||
|
public function __construct($db)
|
||||||
|
{
|
||||||
|
$this->db = $db;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create object in database
|
||||||
|
*
|
||||||
|
* @param User $user User that creates
|
||||||
|
* @return int Return integer <0 if KO, Id of created object if OK
|
||||||
|
*/
|
||||||
|
public function create($user)
|
||||||
|
{
|
||||||
|
global $conf;
|
||||||
|
|
||||||
|
$error = 0;
|
||||||
|
$now = dol_now();
|
||||||
|
|
||||||
|
if (empty($this->fk_carrier) || empty($this->fk_equipment_type)) {
|
||||||
|
$this->error = 'ErrorMissingParameters';
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get default width from type if not set
|
||||||
|
if (empty($this->width_te)) {
|
||||||
|
$type = new EquipmentType($this->db);
|
||||||
|
if ($type->fetch($this->fk_equipment_type) > 0) {
|
||||||
|
$this->width_te = $type->width_te;
|
||||||
|
} else {
|
||||||
|
$this->width_te = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->db->begin();
|
||||||
|
|
||||||
|
$sql = "INSERT INTO ".MAIN_DB_PREFIX.$this->table_element." (";
|
||||||
|
$sql .= "entity, fk_carrier, fk_equipment_type, label,";
|
||||||
|
$sql .= " position_te, width_te, field_values, fk_product,";
|
||||||
|
$sql .= " fk_protection, protection_label,";
|
||||||
|
$sql .= " note_private, status,";
|
||||||
|
$sql .= " date_creation, fk_user_creat";
|
||||||
|
$sql .= ") VALUES (";
|
||||||
|
$sql .= ((int) $conf->entity);
|
||||||
|
$sql .= ", ".((int) $this->fk_carrier);
|
||||||
|
$sql .= ", ".((int) $this->fk_equipment_type);
|
||||||
|
$sql .= ", ".($this->label ? "'".$this->db->escape($this->label)."'" : "NULL");
|
||||||
|
$sql .= ", ".((int) $this->position_te);
|
||||||
|
$sql .= ", ".((int) $this->width_te);
|
||||||
|
$sql .= ", ".($this->field_values ? "'".$this->db->escape($this->field_values)."'" : "NULL");
|
||||||
|
$sql .= ", ".($this->fk_product > 0 ? ((int) $this->fk_product) : "NULL");
|
||||||
|
$sql .= ", ".($this->fk_protection > 0 ? ((int) $this->fk_protection) : "NULL");
|
||||||
|
$sql .= ", ".($this->protection_label ? "'".$this->db->escape($this->protection_label)."'" : "NULL");
|
||||||
|
$sql .= ", ".($this->note_private ? "'".$this->db->escape($this->note_private)."'" : "NULL");
|
||||||
|
$sql .= ", ".((int) ($this->status !== null ? $this->status : 1));
|
||||||
|
$sql .= ", '".$this->db->idate($now)."'";
|
||||||
|
$sql .= ", ".((int) $user->id);
|
||||||
|
$sql .= ")";
|
||||||
|
|
||||||
|
$resql = $this->db->query($sql);
|
||||||
|
if (!$resql) {
|
||||||
|
$error++;
|
||||||
|
$this->errors[] = "Error ".$this->db->lasterror();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$error) {
|
||||||
|
$this->id = $this->db->last_insert_id(MAIN_DB_PREFIX.$this->table_element);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($error) {
|
||||||
|
$this->db->rollback();
|
||||||
|
return -1 * $error;
|
||||||
|
} else {
|
||||||
|
$this->db->commit();
|
||||||
|
return $this->id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load object from database
|
||||||
|
*
|
||||||
|
* @param int $id ID of record
|
||||||
|
* @return int Return integer <0 if KO, 0 if not found, >0 if OK
|
||||||
|
*/
|
||||||
|
public function fetch($id)
|
||||||
|
{
|
||||||
|
$sql = "SELECT e.*, t.label as type_label, t.label_short as type_label_short,";
|
||||||
|
$sql .= " t.ref as type_ref, t.color as type_color, t.picto as type_picto, t.icon_file as type_icon_file,";
|
||||||
|
$sql .= " t.block_image as type_block_image,";
|
||||||
|
$sql .= " t.flow_direction as type_flow_direction, t.terminal_position as type_terminal_position,";
|
||||||
|
$sql .= " t.terminals_config as terminals_config,";
|
||||||
|
$sql .= " p.ref as product_ref, p.label as product_label,";
|
||||||
|
$sql .= " prot.label as protection_device_label";
|
||||||
|
$sql .= " FROM ".MAIN_DB_PREFIX.$this->table_element." as e";
|
||||||
|
$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."kundenkarte_equipment_type as t ON e.fk_equipment_type = t.rowid";
|
||||||
|
$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."product as p ON e.fk_product = p.rowid";
|
||||||
|
$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."kundenkarte_equipment as prot ON e.fk_protection = prot.rowid";
|
||||||
|
$sql .= " WHERE e.rowid = ".((int) $id);
|
||||||
|
|
||||||
|
$resql = $this->db->query($sql);
|
||||||
|
if ($resql) {
|
||||||
|
if ($this->db->num_rows($resql)) {
|
||||||
|
$obj = $this->db->fetch_object($resql);
|
||||||
|
|
||||||
|
$this->id = $obj->rowid;
|
||||||
|
$this->entity = $obj->entity;
|
||||||
|
$this->fk_carrier = $obj->fk_carrier;
|
||||||
|
$this->fk_equipment_type = $obj->fk_equipment_type;
|
||||||
|
$this->label = $obj->label;
|
||||||
|
$this->position_te = $obj->position_te;
|
||||||
|
$this->width_te = $obj->width_te;
|
||||||
|
$this->field_values = $obj->field_values;
|
||||||
|
$this->fk_product = $obj->fk_product;
|
||||||
|
$this->fk_protection = $obj->fk_protection;
|
||||||
|
$this->protection_label = $obj->protection_label;
|
||||||
|
$this->note_private = $obj->note_private;
|
||||||
|
$this->status = $obj->status;
|
||||||
|
$this->date_creation = $this->db->jdate($obj->date_creation);
|
||||||
|
$this->fk_user_creat = $obj->fk_user_creat;
|
||||||
|
$this->fk_user_modif = $obj->fk_user_modif;
|
||||||
|
|
||||||
|
$this->type_label = $obj->type_label;
|
||||||
|
$this->type_label_short = $obj->type_label_short;
|
||||||
|
$this->type_ref = $obj->type_ref;
|
||||||
|
$this->type_color = $obj->type_color;
|
||||||
|
$this->type_picto = $obj->type_picto;
|
||||||
|
$this->type_icon_file = $obj->type_icon_file;
|
||||||
|
$this->type_block_image = $obj->type_block_image;
|
||||||
|
$this->type_flow_direction = $obj->type_flow_direction;
|
||||||
|
$this->type_terminal_position = $obj->type_terminal_position ?: 'both';
|
||||||
|
$this->terminals_config = $obj->terminals_config;
|
||||||
|
$this->product_ref = $obj->product_ref;
|
||||||
|
$this->product_label = $obj->product_label;
|
||||||
|
$this->protection_device_label = $obj->protection_device_label;
|
||||||
|
|
||||||
|
$this->db->free($resql);
|
||||||
|
return 1;
|
||||||
|
} else {
|
||||||
|
$this->db->free($resql);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$this->error = $this->db->lasterror();
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update object in database
|
||||||
|
*
|
||||||
|
* @param User $user User that modifies
|
||||||
|
* @return int Return integer <0 if KO, >0 if OK
|
||||||
|
*/
|
||||||
|
public function update($user)
|
||||||
|
{
|
||||||
|
$error = 0;
|
||||||
|
|
||||||
|
$this->db->begin();
|
||||||
|
|
||||||
|
$sql = "UPDATE ".MAIN_DB_PREFIX.$this->table_element." SET";
|
||||||
|
$sql .= " fk_carrier = ".((int) $this->fk_carrier);
|
||||||
|
$sql .= ", fk_equipment_type = ".((int) $this->fk_equipment_type);
|
||||||
|
$sql .= ", label = ".($this->label ? "'".$this->db->escape($this->label)."'" : "NULL");
|
||||||
|
$sql .= ", position_te = ".((int) $this->position_te);
|
||||||
|
$sql .= ", width_te = ".((int) $this->width_te);
|
||||||
|
$sql .= ", field_values = ".($this->field_values ? "'".$this->db->escape($this->field_values)."'" : "NULL");
|
||||||
|
$sql .= ", fk_product = ".($this->fk_product > 0 ? ((int) $this->fk_product) : "NULL");
|
||||||
|
$sql .= ", fk_protection = ".($this->fk_protection > 0 ? ((int) $this->fk_protection) : "NULL");
|
||||||
|
$sql .= ", protection_label = ".($this->protection_label ? "'".$this->db->escape($this->protection_label)."'" : "NULL");
|
||||||
|
$sql .= ", note_private = ".($this->note_private ? "'".$this->db->escape($this->note_private)."'" : "NULL");
|
||||||
|
$sql .= ", status = ".((int) $this->status);
|
||||||
|
$sql .= ", fk_user_modif = ".((int) $user->id);
|
||||||
|
$sql .= " WHERE rowid = ".((int) $this->id);
|
||||||
|
|
||||||
|
$resql = $this->db->query($sql);
|
||||||
|
if (!$resql) {
|
||||||
|
$error++;
|
||||||
|
$this->errors[] = "Error ".$this->db->lasterror();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($error) {
|
||||||
|
$this->db->rollback();
|
||||||
|
return -1 * $error;
|
||||||
|
} else {
|
||||||
|
$this->db->commit();
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete object in database
|
||||||
|
*
|
||||||
|
* @param User $user User that deletes
|
||||||
|
* @return int Return integer <0 if KO, >0 if OK
|
||||||
|
*/
|
||||||
|
public function delete($user)
|
||||||
|
{
|
||||||
|
$error = 0;
|
||||||
|
$this->db->begin();
|
||||||
|
|
||||||
|
$sql = "DELETE FROM ".MAIN_DB_PREFIX.$this->table_element." WHERE rowid = ".((int) $this->id);
|
||||||
|
$resql = $this->db->query($sql);
|
||||||
|
if (!$resql) {
|
||||||
|
$error++;
|
||||||
|
$this->errors[] = "Error ".$this->db->lasterror();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($error) {
|
||||||
|
$this->db->rollback();
|
||||||
|
return -1 * $error;
|
||||||
|
} else {
|
||||||
|
$this->db->commit();
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch all equipment on a carrier
|
||||||
|
*
|
||||||
|
* @param int $carrierId Carrier ID
|
||||||
|
* @param int $activeOnly Only active equipment
|
||||||
|
* @return array Array of Equipment objects
|
||||||
|
*/
|
||||||
|
public function fetchByCarrier($carrierId, $activeOnly = 1)
|
||||||
|
{
|
||||||
|
$results = array();
|
||||||
|
|
||||||
|
$sql = "SELECT e.*, t.label as type_label, t.label_short as type_label_short,";
|
||||||
|
$sql .= " t.ref as type_ref, t.color as type_color, t.picto as type_picto, t.icon_file as type_icon_file,";
|
||||||
|
$sql .= " t.block_image as type_block_image,";
|
||||||
|
$sql .= " t.flow_direction as type_flow_direction, t.terminal_position as type_terminal_position,";
|
||||||
|
$sql .= " t.terminals_config as terminals_config,";
|
||||||
|
$sql .= " prot.label as protection_device_label";
|
||||||
|
$sql .= " FROM ".MAIN_DB_PREFIX.$this->table_element." as e";
|
||||||
|
$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."kundenkarte_equipment_type as t ON e.fk_equipment_type = t.rowid";
|
||||||
|
$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."kundenkarte_equipment as prot ON e.fk_protection = prot.rowid";
|
||||||
|
$sql .= " WHERE e.fk_carrier = ".((int) $carrierId);
|
||||||
|
if ($activeOnly) {
|
||||||
|
$sql .= " AND e.status = 1";
|
||||||
|
}
|
||||||
|
$sql .= " ORDER BY e.position_te ASC";
|
||||||
|
|
||||||
|
$resql = $this->db->query($sql);
|
||||||
|
if ($resql) {
|
||||||
|
while ($obj = $this->db->fetch_object($resql)) {
|
||||||
|
$eq = new Equipment($this->db);
|
||||||
|
$eq->id = $obj->rowid;
|
||||||
|
$eq->entity = $obj->entity;
|
||||||
|
$eq->fk_carrier = $obj->fk_carrier;
|
||||||
|
$eq->fk_equipment_type = $obj->fk_equipment_type;
|
||||||
|
$eq->label = $obj->label;
|
||||||
|
$eq->position_te = $obj->position_te;
|
||||||
|
$eq->width_te = $obj->width_te;
|
||||||
|
$eq->field_values = $obj->field_values;
|
||||||
|
$eq->fk_product = $obj->fk_product;
|
||||||
|
$eq->fk_protection = $obj->fk_protection;
|
||||||
|
$eq->protection_label = $obj->protection_label;
|
||||||
|
$eq->note_private = $obj->note_private;
|
||||||
|
$eq->status = $obj->status;
|
||||||
|
|
||||||
|
$eq->type_label = $obj->type_label;
|
||||||
|
$eq->type_label_short = $obj->type_label_short;
|
||||||
|
$eq->type_ref = $obj->type_ref;
|
||||||
|
$eq->type_color = $obj->type_color;
|
||||||
|
$eq->type_picto = $obj->type_picto;
|
||||||
|
$eq->type_icon_file = $obj->type_icon_file;
|
||||||
|
$eq->type_block_image = $obj->type_block_image;
|
||||||
|
$eq->type_flow_direction = $obj->type_flow_direction;
|
||||||
|
$eq->type_terminal_position = $obj->type_terminal_position ?: 'both';
|
||||||
|
$eq->terminals_config = $obj->terminals_config;
|
||||||
|
$eq->protection_device_label = $obj->protection_device_label;
|
||||||
|
|
||||||
|
$results[] = $eq;
|
||||||
|
}
|
||||||
|
$this->db->free($resql);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $results;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch protection devices (FI/RCD) for an Anlage
|
||||||
|
* Protection devices are equipment types that can protect other equipment
|
||||||
|
*
|
||||||
|
* @param int $anlageId Anlage ID
|
||||||
|
* @return array Array of Equipment objects that are protection devices
|
||||||
|
*/
|
||||||
|
public function fetchProtectionDevices($anlageId)
|
||||||
|
{
|
||||||
|
$results = array();
|
||||||
|
|
||||||
|
// Get all equipment for this anlage that have type with is_protection = 1
|
||||||
|
// Or equipment types that are typically protection devices (FI, RCD, etc.)
|
||||||
|
$sql = "SELECT e.*, t.label as type_label, t.label_short as type_label_short,";
|
||||||
|
$sql .= " t.color as type_color, t.ref as type_ref, c.fk_anlage";
|
||||||
|
$sql .= " FROM ".MAIN_DB_PREFIX.$this->table_element." as e";
|
||||||
|
$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."kundenkarte_equipment_type as t ON e.fk_equipment_type = t.rowid";
|
||||||
|
$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."kundenkarte_equipment_carrier as c ON e.fk_carrier = c.rowid";
|
||||||
|
$sql .= " WHERE c.fk_anlage = ".((int) $anlageId);
|
||||||
|
$sql .= " AND e.status = 1";
|
||||||
|
// Filter for protection device types (FI, RCD, FI_LS, etc.)
|
||||||
|
$sql .= " AND (t.ref LIKE '%FI%' OR t.ref LIKE '%RCD%' OR t.ref LIKE 'RCBO%')";
|
||||||
|
$sql .= " ORDER BY c.position ASC, e.position_te ASC";
|
||||||
|
|
||||||
|
$resql = $this->db->query($sql);
|
||||||
|
if ($resql) {
|
||||||
|
while ($obj = $this->db->fetch_object($resql)) {
|
||||||
|
$eq = new Equipment($this->db);
|
||||||
|
$eq->id = $obj->rowid;
|
||||||
|
$eq->fk_carrier = $obj->fk_carrier;
|
||||||
|
$eq->fk_equipment_type = $obj->fk_equipment_type;
|
||||||
|
$eq->label = $obj->label;
|
||||||
|
$eq->position_te = $obj->position_te;
|
||||||
|
$eq->width_te = $obj->width_te;
|
||||||
|
$eq->type_label = $obj->type_label;
|
||||||
|
$eq->type_label_short = $obj->type_label_short;
|
||||||
|
$eq->type_color = $obj->type_color;
|
||||||
|
|
||||||
|
$results[] = $eq;
|
||||||
|
}
|
||||||
|
$this->db->free($resql);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $results;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Duplicate this equipment (for "+" button)
|
||||||
|
*
|
||||||
|
* @param User $user User that creates
|
||||||
|
* @param int $carrierId Target carrier (0 = same carrier)
|
||||||
|
* @param int $position Target position (-1 = auto)
|
||||||
|
* @return int Return integer <0 if KO, Id of new object if OK
|
||||||
|
*/
|
||||||
|
public function duplicate($user, $carrierId = 0, $position = -1)
|
||||||
|
{
|
||||||
|
require_once DOL_DOCUMENT_ROOT.'/custom/kundenkarte/class/equipmentcarrier.class.php';
|
||||||
|
|
||||||
|
$newEquipment = new Equipment($this->db);
|
||||||
|
$newEquipment->fk_carrier = $carrierId > 0 ? $carrierId : $this->fk_carrier;
|
||||||
|
$newEquipment->fk_equipment_type = $this->fk_equipment_type;
|
||||||
|
$newEquipment->label = $this->label;
|
||||||
|
$newEquipment->width_te = $this->width_te;
|
||||||
|
$newEquipment->field_values = $this->field_values;
|
||||||
|
$newEquipment->fk_product = $this->fk_product;
|
||||||
|
$newEquipment->note_private = $this->note_private;
|
||||||
|
$newEquipment->status = 1;
|
||||||
|
|
||||||
|
// Find position
|
||||||
|
if ($position >= 0) {
|
||||||
|
$newEquipment->position_te = $position;
|
||||||
|
} else {
|
||||||
|
// Auto-find next position after this equipment
|
||||||
|
$carrier = new EquipmentCarrier($this->db);
|
||||||
|
$carrier->fetch($newEquipment->fk_carrier);
|
||||||
|
$carrier->fetchEquipment();
|
||||||
|
|
||||||
|
// Try position right after this element
|
||||||
|
$tryPos = $this->position_te + $this->width_te;
|
||||||
|
if ($carrier->isPositionAvailable($tryPos, $this->width_te)) {
|
||||||
|
$newEquipment->position_te = $tryPos;
|
||||||
|
} else {
|
||||||
|
// Find any free position
|
||||||
|
$newEquipment->position_te = $carrier->getNextFreePosition($this->width_te);
|
||||||
|
if ($newEquipment->position_te < 0) {
|
||||||
|
$this->error = 'ErrorNoSpaceOnCarrier';
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $newEquipment->create($user);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get field values as array
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function getFieldValues()
|
||||||
|
{
|
||||||
|
if (empty($this->field_values)) {
|
||||||
|
return array();
|
||||||
|
}
|
||||||
|
$values = json_decode($this->field_values, true);
|
||||||
|
return is_array($values) ? $values : array();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set field values from array
|
||||||
|
*
|
||||||
|
* @param array $values Key-value pairs
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function setFieldValues($values)
|
||||||
|
{
|
||||||
|
$this->field_values = json_encode($values);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get single field value
|
||||||
|
*
|
||||||
|
* @param string $fieldCode Field code
|
||||||
|
* @return mixed|null
|
||||||
|
*/
|
||||||
|
public function getFieldValue($fieldCode)
|
||||||
|
{
|
||||||
|
$values = $this->getFieldValues();
|
||||||
|
return isset($values[$fieldCode]) ? $values[$fieldCode] : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get label for SVG block display
|
||||||
|
* Combines fields with show_on_block = 1
|
||||||
|
*
|
||||||
|
* @return string Label to display on block (e.g. "B16")
|
||||||
|
*/
|
||||||
|
public function getBlockLabel()
|
||||||
|
{
|
||||||
|
$type = new EquipmentType($this->db);
|
||||||
|
if ($type->fetch($this->fk_equipment_type) <= 0) {
|
||||||
|
return $this->type_label_short ?: '';
|
||||||
|
}
|
||||||
|
|
||||||
|
$blockFields = $type->getBlockFields();
|
||||||
|
if (empty($blockFields)) {
|
||||||
|
return $this->type_label_short ?: '';
|
||||||
|
}
|
||||||
|
|
||||||
|
$values = $this->getFieldValues();
|
||||||
|
$parts = array();
|
||||||
|
|
||||||
|
foreach ($blockFields as $field) {
|
||||||
|
if (isset($values[$field->field_code]) && $values[$field->field_code] !== '') {
|
||||||
|
$parts[] = $values[$field->field_code];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($parts)) {
|
||||||
|
return $this->type_label_short ?: '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return implode('', $parts);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get color for SVG block
|
||||||
|
*
|
||||||
|
* @return string Hex color code
|
||||||
|
*/
|
||||||
|
public function getBlockColor()
|
||||||
|
{
|
||||||
|
if (!empty($this->type_color)) {
|
||||||
|
return $this->type_color;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default color
|
||||||
|
return '#3498db';
|
||||||
|
}
|
||||||
|
}
|
||||||
426
class/equipmentcarrier.class.php
Executable file
426
class/equipmentcarrier.class.php
Executable file
|
|
@ -0,0 +1,426 @@
|
||||||
|
<?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 EquipmentCarrier
|
||||||
|
* Manages equipment carriers (Hutschienen/Traeger)
|
||||||
|
*/
|
||||||
|
class EquipmentCarrier extends CommonObject
|
||||||
|
{
|
||||||
|
public $element = 'equipmentcarrier';
|
||||||
|
public $table_element = 'kundenkarte_equipment_carrier';
|
||||||
|
|
||||||
|
public $fk_anlage;
|
||||||
|
public $fk_panel;
|
||||||
|
public $label;
|
||||||
|
public $total_te = 12;
|
||||||
|
public $position;
|
||||||
|
public $note_private;
|
||||||
|
public $status;
|
||||||
|
|
||||||
|
public $date_creation;
|
||||||
|
public $fk_user_creat;
|
||||||
|
public $fk_user_modif;
|
||||||
|
|
||||||
|
// Loaded objects
|
||||||
|
public $equipment = array();
|
||||||
|
public $anlage_label;
|
||||||
|
public $panel_label;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor
|
||||||
|
*
|
||||||
|
* @param DoliDB $db Database handler
|
||||||
|
*/
|
||||||
|
public function __construct($db)
|
||||||
|
{
|
||||||
|
$this->db = $db;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create object in database
|
||||||
|
*
|
||||||
|
* @param User $user User that creates
|
||||||
|
* @return int Return integer <0 if KO, Id of created object if OK
|
||||||
|
*/
|
||||||
|
public function create($user)
|
||||||
|
{
|
||||||
|
global $conf;
|
||||||
|
|
||||||
|
$error = 0;
|
||||||
|
$now = dol_now();
|
||||||
|
|
||||||
|
if (empty($this->fk_anlage) || empty($this->label)) {
|
||||||
|
$this->error = 'ErrorMissingParameters';
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get next position
|
||||||
|
if (empty($this->position)) {
|
||||||
|
$sql = "SELECT MAX(position) as maxpos FROM ".MAIN_DB_PREFIX.$this->table_element;
|
||||||
|
$sql .= " WHERE fk_anlage = ".((int) $this->fk_anlage);
|
||||||
|
$resql = $this->db->query($sql);
|
||||||
|
if ($resql) {
|
||||||
|
$obj = $this->db->fetch_object($resql);
|
||||||
|
$this->position = ($obj->maxpos !== null) ? $obj->maxpos + 1 : 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->db->begin();
|
||||||
|
|
||||||
|
$sql = "INSERT INTO ".MAIN_DB_PREFIX.$this->table_element." (";
|
||||||
|
$sql .= "entity, fk_anlage, fk_panel, label, total_te, position, note_private, status,";
|
||||||
|
$sql .= " date_creation, fk_user_creat";
|
||||||
|
$sql .= ") VALUES (";
|
||||||
|
$sql .= ((int) $conf->entity);
|
||||||
|
$sql .= ", ".((int) $this->fk_anlage);
|
||||||
|
$sql .= ", ".($this->fk_panel > 0 ? ((int) $this->fk_panel) : "NULL");
|
||||||
|
$sql .= ", '".$this->db->escape($this->label)."'";
|
||||||
|
$sql .= ", ".((int) ($this->total_te > 0 ? $this->total_te : 12));
|
||||||
|
$sql .= ", ".((int) $this->position);
|
||||||
|
$sql .= ", ".($this->note_private ? "'".$this->db->escape($this->note_private)."'" : "NULL");
|
||||||
|
$sql .= ", ".((int) ($this->status !== null ? $this->status : 1));
|
||||||
|
$sql .= ", '".$this->db->idate($now)."'";
|
||||||
|
$sql .= ", ".((int) $user->id);
|
||||||
|
$sql .= ")";
|
||||||
|
|
||||||
|
$resql = $this->db->query($sql);
|
||||||
|
if (!$resql) {
|
||||||
|
$error++;
|
||||||
|
$this->errors[] = "Error ".$this->db->lasterror();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$error) {
|
||||||
|
$this->id = $this->db->last_insert_id(MAIN_DB_PREFIX.$this->table_element);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($error) {
|
||||||
|
$this->db->rollback();
|
||||||
|
return -1 * $error;
|
||||||
|
} else {
|
||||||
|
$this->db->commit();
|
||||||
|
return $this->id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load object from database
|
||||||
|
*
|
||||||
|
* @param int $id ID of record
|
||||||
|
* @return int Return integer <0 if KO, 0 if not found, >0 if OK
|
||||||
|
*/
|
||||||
|
public function fetch($id)
|
||||||
|
{
|
||||||
|
$sql = "SELECT c.*, a.label as anlage_label, p.label as panel_label";
|
||||||
|
$sql .= " FROM ".MAIN_DB_PREFIX.$this->table_element." as c";
|
||||||
|
$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."kundenkarte_anlage as a ON c.fk_anlage = a.rowid";
|
||||||
|
$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."kundenkarte_equipment_panel as p ON c.fk_panel = p.rowid";
|
||||||
|
$sql .= " WHERE c.rowid = ".((int) $id);
|
||||||
|
|
||||||
|
$resql = $this->db->query($sql);
|
||||||
|
if ($resql) {
|
||||||
|
if ($this->db->num_rows($resql)) {
|
||||||
|
$obj = $this->db->fetch_object($resql);
|
||||||
|
|
||||||
|
$this->id = $obj->rowid;
|
||||||
|
$this->entity = $obj->entity;
|
||||||
|
$this->fk_anlage = $obj->fk_anlage;
|
||||||
|
$this->fk_panel = $obj->fk_panel;
|
||||||
|
$this->label = $obj->label;
|
||||||
|
$this->total_te = $obj->total_te;
|
||||||
|
$this->position = $obj->position;
|
||||||
|
$this->note_private = $obj->note_private;
|
||||||
|
$this->status = $obj->status;
|
||||||
|
$this->date_creation = $this->db->jdate($obj->date_creation);
|
||||||
|
$this->fk_user_creat = $obj->fk_user_creat;
|
||||||
|
$this->fk_user_modif = $obj->fk_user_modif;
|
||||||
|
|
||||||
|
$this->anlage_label = $obj->anlage_label;
|
||||||
|
$this->panel_label = $obj->panel_label;
|
||||||
|
|
||||||
|
$this->db->free($resql);
|
||||||
|
return 1;
|
||||||
|
} else {
|
||||||
|
$this->db->free($resql);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$this->error = $this->db->lasterror();
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update object in database
|
||||||
|
*
|
||||||
|
* @param User $user User that modifies
|
||||||
|
* @return int Return integer <0 if KO, >0 if OK
|
||||||
|
*/
|
||||||
|
public function update($user)
|
||||||
|
{
|
||||||
|
$error = 0;
|
||||||
|
|
||||||
|
$this->db->begin();
|
||||||
|
|
||||||
|
$sql = "UPDATE ".MAIN_DB_PREFIX.$this->table_element." SET";
|
||||||
|
$sql .= " label = '".$this->db->escape($this->label)."'";
|
||||||
|
$sql .= ", fk_panel = ".($this->fk_panel > 0 ? ((int) $this->fk_panel) : "NULL");
|
||||||
|
$sql .= ", total_te = ".((int) ($this->total_te > 0 ? $this->total_te : 12));
|
||||||
|
$sql .= ", position = ".((int) $this->position);
|
||||||
|
$sql .= ", note_private = ".($this->note_private ? "'".$this->db->escape($this->note_private)."'" : "NULL");
|
||||||
|
$sql .= ", status = ".((int) $this->status);
|
||||||
|
$sql .= ", fk_user_modif = ".((int) $user->id);
|
||||||
|
$sql .= " WHERE rowid = ".((int) $this->id);
|
||||||
|
|
||||||
|
$resql = $this->db->query($sql);
|
||||||
|
if (!$resql) {
|
||||||
|
$error++;
|
||||||
|
$this->errors[] = "Error ".$this->db->lasterror();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($error) {
|
||||||
|
$this->db->rollback();
|
||||||
|
return -1 * $error;
|
||||||
|
} else {
|
||||||
|
$this->db->commit();
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete object in database
|
||||||
|
*
|
||||||
|
* @param User $user User that deletes
|
||||||
|
* @return int Return integer <0 if KO, >0 if OK
|
||||||
|
*/
|
||||||
|
public function delete($user)
|
||||||
|
{
|
||||||
|
$error = 0;
|
||||||
|
$this->db->begin();
|
||||||
|
|
||||||
|
// Equipment is deleted via CASCADE
|
||||||
|
|
||||||
|
$sql = "DELETE FROM ".MAIN_DB_PREFIX.$this->table_element." WHERE rowid = ".((int) $this->id);
|
||||||
|
$resql = $this->db->query($sql);
|
||||||
|
if (!$resql) {
|
||||||
|
$error++;
|
||||||
|
$this->errors[] = "Error ".$this->db->lasterror();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($error) {
|
||||||
|
$this->db->rollback();
|
||||||
|
return -1 * $error;
|
||||||
|
} else {
|
||||||
|
$this->db->commit();
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch all carriers for a Panel
|
||||||
|
*
|
||||||
|
* @param int $panelId Panel ID
|
||||||
|
* @param int $activeOnly Only active carriers
|
||||||
|
* @return array Array of EquipmentCarrier objects
|
||||||
|
*/
|
||||||
|
public function fetchByPanel($panelId, $activeOnly = 1)
|
||||||
|
{
|
||||||
|
$results = array();
|
||||||
|
|
||||||
|
$sql = "SELECT * FROM ".MAIN_DB_PREFIX.$this->table_element;
|
||||||
|
$sql .= " WHERE fk_panel = ".((int) $panelId);
|
||||||
|
if ($activeOnly) {
|
||||||
|
$sql .= " AND status = 1";
|
||||||
|
}
|
||||||
|
$sql .= " ORDER BY position ASC";
|
||||||
|
|
||||||
|
$resql = $this->db->query($sql);
|
||||||
|
if ($resql) {
|
||||||
|
while ($obj = $this->db->fetch_object($resql)) {
|
||||||
|
$carrier = new EquipmentCarrier($this->db);
|
||||||
|
$carrier->id = $obj->rowid;
|
||||||
|
$carrier->entity = $obj->entity;
|
||||||
|
$carrier->fk_anlage = $obj->fk_anlage;
|
||||||
|
$carrier->fk_panel = $obj->fk_panel;
|
||||||
|
$carrier->label = $obj->label;
|
||||||
|
$carrier->total_te = $obj->total_te;
|
||||||
|
$carrier->position = $obj->position;
|
||||||
|
$carrier->note_private = $obj->note_private;
|
||||||
|
$carrier->status = $obj->status;
|
||||||
|
|
||||||
|
$results[] = $carrier;
|
||||||
|
}
|
||||||
|
$this->db->free($resql);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $results;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch all carriers for an Anlage
|
||||||
|
*
|
||||||
|
* @param int $anlageId Anlage ID
|
||||||
|
* @param int $activeOnly Only active carriers
|
||||||
|
* @return array Array of EquipmentCarrier objects
|
||||||
|
*/
|
||||||
|
public function fetchByAnlage($anlageId, $activeOnly = 1)
|
||||||
|
{
|
||||||
|
$results = array();
|
||||||
|
|
||||||
|
$sql = "SELECT * FROM ".MAIN_DB_PREFIX.$this->table_element;
|
||||||
|
$sql .= " WHERE fk_anlage = ".((int) $anlageId);
|
||||||
|
if ($activeOnly) {
|
||||||
|
$sql .= " AND status = 1";
|
||||||
|
}
|
||||||
|
$sql .= " ORDER BY position ASC";
|
||||||
|
|
||||||
|
$resql = $this->db->query($sql);
|
||||||
|
if ($resql) {
|
||||||
|
while ($obj = $this->db->fetch_object($resql)) {
|
||||||
|
$carrier = new EquipmentCarrier($this->db);
|
||||||
|
$carrier->id = $obj->rowid;
|
||||||
|
$carrier->entity = $obj->entity;
|
||||||
|
$carrier->fk_anlage = $obj->fk_anlage;
|
||||||
|
$carrier->fk_panel = $obj->fk_panel;
|
||||||
|
$carrier->label = $obj->label;
|
||||||
|
$carrier->total_te = $obj->total_te;
|
||||||
|
$carrier->position = $obj->position;
|
||||||
|
$carrier->note_private = $obj->note_private;
|
||||||
|
$carrier->status = $obj->status;
|
||||||
|
|
||||||
|
$results[] = $carrier;
|
||||||
|
}
|
||||||
|
$this->db->free($resql);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $results;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch all equipment on this carrier
|
||||||
|
*
|
||||||
|
* @return array Array of Equipment objects
|
||||||
|
*/
|
||||||
|
public function fetchEquipment()
|
||||||
|
{
|
||||||
|
require_once DOL_DOCUMENT_ROOT.'/custom/kundenkarte/class/equipment.class.php';
|
||||||
|
|
||||||
|
$equipment = new Equipment($this->db);
|
||||||
|
$this->equipment = $equipment->fetchByCarrier($this->id);
|
||||||
|
|
||||||
|
return $this->equipment;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get array of occupied TE slots
|
||||||
|
*
|
||||||
|
* @return array Array of occupied slot numbers (0-based)
|
||||||
|
*/
|
||||||
|
public function getOccupiedSlots()
|
||||||
|
{
|
||||||
|
$occupied = array();
|
||||||
|
|
||||||
|
if (empty($this->equipment)) {
|
||||||
|
$this->fetchEquipment();
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($this->equipment as $eq) {
|
||||||
|
for ($i = $eq->position_te; $i < $eq->position_te + $eq->width_te; $i++) {
|
||||||
|
$occupied[] = $i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $occupied;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get used TE count
|
||||||
|
*
|
||||||
|
* @return int Number of used TE
|
||||||
|
*/
|
||||||
|
public function getUsedTE()
|
||||||
|
{
|
||||||
|
return count($this->getOccupiedSlots());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get free TE count
|
||||||
|
*
|
||||||
|
* @return int Number of free TE
|
||||||
|
*/
|
||||||
|
public function getFreeTE()
|
||||||
|
{
|
||||||
|
return $this->total_te - $this->getUsedTE();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find next free position for given width
|
||||||
|
*
|
||||||
|
* @param int $width Width in TE needed
|
||||||
|
* @return int Position (1-based) or -1 if no space
|
||||||
|
*/
|
||||||
|
public function getNextFreePosition($width = 1)
|
||||||
|
{
|
||||||
|
$occupied = $this->getOccupiedSlots();
|
||||||
|
|
||||||
|
// Positions are 1-based (1 to total_te)
|
||||||
|
for ($pos = 1; $pos <= $this->total_te - $width + 1; $pos++) {
|
||||||
|
$fits = true;
|
||||||
|
for ($i = $pos; $i < $pos + $width; $i++) {
|
||||||
|
if (in_array($i, $occupied)) {
|
||||||
|
$fits = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($fits) {
|
||||||
|
return $pos;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return -1; // No space available
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if position is available for given width
|
||||||
|
*
|
||||||
|
* @param int $position Start position (1-based)
|
||||||
|
* @param int $width Width in TE
|
||||||
|
* @param int $excludeEquipmentId Equipment ID to exclude (for updates)
|
||||||
|
* @return bool True if position is available
|
||||||
|
*/
|
||||||
|
public function isPositionAvailable($position, $width, $excludeEquipmentId = 0)
|
||||||
|
{
|
||||||
|
// Check bounds (positions are 1-based)
|
||||||
|
if ($position < 1 || $position + $width - 1 > $this->total_te) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($this->equipment)) {
|
||||||
|
$this->fetchEquipment();
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($this->equipment as $eq) {
|
||||||
|
if ($excludeEquipmentId > 0 && $eq->id == $excludeEquipmentId) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for overlap
|
||||||
|
$eqStart = $eq->position_te;
|
||||||
|
$eqEnd = $eq->position_te + $eq->width_te - 1;
|
||||||
|
$newStart = $position;
|
||||||
|
$newEnd = $position + $width - 1;
|
||||||
|
|
||||||
|
if ($newStart <= $eqEnd && $newEnd >= $eqStart) {
|
||||||
|
return false; // Overlap
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
427
class/equipmentconnection.class.php
Executable file
427
class/equipmentconnection.class.php
Executable file
|
|
@ -0,0 +1,427 @@
|
||||||
|
<?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 EquipmentConnection
|
||||||
|
* Manages connections between equipment (generic for all system types)
|
||||||
|
*/
|
||||||
|
class EquipmentConnection extends CommonObject
|
||||||
|
{
|
||||||
|
public $element = 'equipmentconnection';
|
||||||
|
public $table_element = 'kundenkarte_equipment_connection';
|
||||||
|
|
||||||
|
public $fk_source;
|
||||||
|
public $source_terminal = 'output';
|
||||||
|
public $source_terminal_id;
|
||||||
|
public $fk_target;
|
||||||
|
public $target_terminal = 'input';
|
||||||
|
public $target_terminal_id;
|
||||||
|
|
||||||
|
// Connection properties
|
||||||
|
public $connection_type;
|
||||||
|
public $color;
|
||||||
|
|
||||||
|
// Output/endpoint info
|
||||||
|
public $output_label;
|
||||||
|
|
||||||
|
// Medium info (cable, wire, etc.)
|
||||||
|
public $medium_type;
|
||||||
|
public $medium_spec;
|
||||||
|
public $medium_length;
|
||||||
|
|
||||||
|
// Rail info
|
||||||
|
public $is_rail = 0;
|
||||||
|
public $rail_start_te;
|
||||||
|
public $rail_end_te;
|
||||||
|
public $rail_phases; // '3P', '3P+N', 'L1', 'L1N', etc.
|
||||||
|
public $excluded_te; // Comma-separated TE positions to exclude (gaps for FI)
|
||||||
|
|
||||||
|
public $fk_carrier;
|
||||||
|
public $position_y = 0;
|
||||||
|
public $path_data; // SVG path for manually drawn connections
|
||||||
|
public $note_private;
|
||||||
|
public $status = 1;
|
||||||
|
|
||||||
|
public $date_creation;
|
||||||
|
public $fk_user_creat;
|
||||||
|
public $fk_user_modif;
|
||||||
|
|
||||||
|
// Loaded objects
|
||||||
|
public $source_label;
|
||||||
|
public $target_label;
|
||||||
|
public $carrier_label;
|
||||||
|
public $source_pos;
|
||||||
|
public $source_width;
|
||||||
|
public $target_pos;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor
|
||||||
|
*
|
||||||
|
* @param DoliDB $db Database handler
|
||||||
|
*/
|
||||||
|
public function __construct($db)
|
||||||
|
{
|
||||||
|
$this->db = $db;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create object in database
|
||||||
|
*
|
||||||
|
* @param User $user User that creates
|
||||||
|
* @return int Return integer <0 if KO, Id of created object if OK
|
||||||
|
*/
|
||||||
|
public function create($user)
|
||||||
|
{
|
||||||
|
global $conf;
|
||||||
|
|
||||||
|
$error = 0;
|
||||||
|
$now = dol_now();
|
||||||
|
|
||||||
|
$this->db->begin();
|
||||||
|
|
||||||
|
$sql = "INSERT INTO ".MAIN_DB_PREFIX.$this->table_element." (";
|
||||||
|
$sql .= "entity, fk_source, source_terminal, source_terminal_id, fk_target, target_terminal, target_terminal_id,";
|
||||||
|
$sql .= " connection_type, color, output_label,";
|
||||||
|
$sql .= " medium_type, medium_spec, medium_length,";
|
||||||
|
$sql .= " is_rail, rail_start_te, rail_end_te, rail_phases, excluded_te, fk_carrier, position_y, path_data,";
|
||||||
|
$sql .= " note_private, status, date_creation, fk_user_creat";
|
||||||
|
$sql .= ") VALUES (";
|
||||||
|
$sql .= ((int) $conf->entity);
|
||||||
|
$sql .= ", ".($this->fk_source > 0 ? ((int) $this->fk_source) : "NULL");
|
||||||
|
$sql .= ", '".$this->db->escape($this->source_terminal ?: 'output')."'";
|
||||||
|
$sql .= ", ".($this->source_terminal_id ? "'".$this->db->escape($this->source_terminal_id)."'" : "NULL");
|
||||||
|
$sql .= ", ".($this->fk_target > 0 ? ((int) $this->fk_target) : "NULL");
|
||||||
|
$sql .= ", '".$this->db->escape($this->target_terminal ?: 'input')."'";
|
||||||
|
$sql .= ", ".($this->target_terminal_id ? "'".$this->db->escape($this->target_terminal_id)."'" : "NULL");
|
||||||
|
$sql .= ", ".($this->connection_type ? "'".$this->db->escape($this->connection_type)."'" : "NULL");
|
||||||
|
$sql .= ", ".($this->color ? "'".$this->db->escape($this->color)."'" : "NULL");
|
||||||
|
$sql .= ", ".($this->output_label ? "'".$this->db->escape($this->output_label)."'" : "NULL");
|
||||||
|
$sql .= ", ".($this->medium_type ? "'".$this->db->escape($this->medium_type)."'" : "NULL");
|
||||||
|
$sql .= ", ".($this->medium_spec ? "'".$this->db->escape($this->medium_spec)."'" : "NULL");
|
||||||
|
$sql .= ", ".($this->medium_length ? "'".$this->db->escape($this->medium_length)."'" : "NULL");
|
||||||
|
$sql .= ", ".((int) $this->is_rail);
|
||||||
|
$sql .= ", ".($this->rail_start_te > 0 ? ((int) $this->rail_start_te) : "NULL");
|
||||||
|
$sql .= ", ".($this->rail_end_te > 0 ? ((int) $this->rail_end_te) : "NULL");
|
||||||
|
$sql .= ", ".($this->rail_phases ? "'".$this->db->escape($this->rail_phases)."'" : "NULL");
|
||||||
|
$sql .= ", ".($this->excluded_te ? "'".$this->db->escape($this->excluded_te)."'" : "NULL");
|
||||||
|
$sql .= ", ".($this->fk_carrier > 0 ? ((int) $this->fk_carrier) : "NULL");
|
||||||
|
$sql .= ", ".((int) $this->position_y);
|
||||||
|
$sql .= ", ".($this->path_data ? "'".$this->db->escape($this->path_data)."'" : "NULL");
|
||||||
|
$sql .= ", ".($this->note_private ? "'".$this->db->escape($this->note_private)."'" : "NULL");
|
||||||
|
$sql .= ", ".((int) $this->status);
|
||||||
|
$sql .= ", '".$this->db->idate($now)."'";
|
||||||
|
$sql .= ", ".((int) $user->id);
|
||||||
|
$sql .= ")";
|
||||||
|
|
||||||
|
$resql = $this->db->query($sql);
|
||||||
|
if (!$resql) {
|
||||||
|
$error++;
|
||||||
|
$this->errors[] = "Error ".$this->db->lasterror();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$error) {
|
||||||
|
$this->id = $this->db->last_insert_id(MAIN_DB_PREFIX.$this->table_element);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($error) {
|
||||||
|
$this->db->rollback();
|
||||||
|
return -1 * $error;
|
||||||
|
} else {
|
||||||
|
$this->db->commit();
|
||||||
|
return $this->id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load object from database
|
||||||
|
*
|
||||||
|
* @param int $id ID of record
|
||||||
|
* @return int Return integer <0 if KO, 0 if not found, >0 if OK
|
||||||
|
*/
|
||||||
|
public function fetch($id)
|
||||||
|
{
|
||||||
|
$sql = "SELECT c.*, ";
|
||||||
|
$sql .= " src.label as source_label, tgt.label as target_label, car.label as carrier_label";
|
||||||
|
$sql .= " FROM ".MAIN_DB_PREFIX.$this->table_element." as c";
|
||||||
|
$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."kundenkarte_equipment as src ON c.fk_source = src.rowid";
|
||||||
|
$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."kundenkarte_equipment as tgt ON c.fk_target = tgt.rowid";
|
||||||
|
$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."kundenkarte_equipment_carrier as car ON c.fk_carrier = car.rowid";
|
||||||
|
$sql .= " WHERE c.rowid = ".((int) $id);
|
||||||
|
|
||||||
|
$resql = $this->db->query($sql);
|
||||||
|
if ($resql) {
|
||||||
|
if ($this->db->num_rows($resql)) {
|
||||||
|
$obj = $this->db->fetch_object($resql);
|
||||||
|
|
||||||
|
$this->id = $obj->rowid;
|
||||||
|
$this->entity = $obj->entity;
|
||||||
|
$this->fk_source = $obj->fk_source;
|
||||||
|
$this->source_terminal = $obj->source_terminal;
|
||||||
|
$this->source_terminal_id = $obj->source_terminal_id;
|
||||||
|
$this->fk_target = $obj->fk_target;
|
||||||
|
$this->target_terminal = $obj->target_terminal;
|
||||||
|
$this->target_terminal_id = $obj->target_terminal_id;
|
||||||
|
$this->connection_type = $obj->connection_type;
|
||||||
|
$this->color = $obj->color;
|
||||||
|
$this->output_label = $obj->output_label;
|
||||||
|
$this->medium_type = $obj->medium_type;
|
||||||
|
$this->medium_spec = $obj->medium_spec;
|
||||||
|
$this->medium_length = $obj->medium_length;
|
||||||
|
$this->is_rail = $obj->is_rail;
|
||||||
|
$this->rail_start_te = $obj->rail_start_te;
|
||||||
|
$this->rail_end_te = $obj->rail_end_te;
|
||||||
|
$this->rail_phases = $obj->rail_phases;
|
||||||
|
$this->excluded_te = $obj->excluded_te;
|
||||||
|
$this->fk_carrier = $obj->fk_carrier;
|
||||||
|
$this->position_y = $obj->position_y;
|
||||||
|
$this->path_data = isset($obj->path_data) ? $obj->path_data : null;
|
||||||
|
$this->note_private = $obj->note_private;
|
||||||
|
$this->status = $obj->status;
|
||||||
|
$this->date_creation = $this->db->jdate($obj->date_creation);
|
||||||
|
$this->fk_user_creat = $obj->fk_user_creat;
|
||||||
|
$this->fk_user_modif = $obj->fk_user_modif;
|
||||||
|
|
||||||
|
$this->source_label = $obj->source_label;
|
||||||
|
$this->target_label = $obj->target_label;
|
||||||
|
$this->carrier_label = $obj->carrier_label;
|
||||||
|
|
||||||
|
$this->db->free($resql);
|
||||||
|
return 1;
|
||||||
|
} else {
|
||||||
|
$this->db->free($resql);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$this->error = $this->db->lasterror();
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update object in database
|
||||||
|
*
|
||||||
|
* @param User $user User that modifies
|
||||||
|
* @return int Return integer <0 if KO, >0 if OK
|
||||||
|
*/
|
||||||
|
public function update($user)
|
||||||
|
{
|
||||||
|
$error = 0;
|
||||||
|
|
||||||
|
$this->db->begin();
|
||||||
|
|
||||||
|
$sql = "UPDATE ".MAIN_DB_PREFIX.$this->table_element." SET";
|
||||||
|
$sql .= " fk_source = ".($this->fk_source > 0 ? ((int) $this->fk_source) : "NULL");
|
||||||
|
$sql .= ", source_terminal = '".$this->db->escape($this->source_terminal ?: 'output')."'";
|
||||||
|
$sql .= ", fk_target = ".($this->fk_target > 0 ? ((int) $this->fk_target) : "NULL");
|
||||||
|
$sql .= ", target_terminal = '".$this->db->escape($this->target_terminal ?: 'input')."'";
|
||||||
|
$sql .= ", connection_type = ".($this->connection_type ? "'".$this->db->escape($this->connection_type)."'" : "NULL");
|
||||||
|
$sql .= ", color = ".($this->color ? "'".$this->db->escape($this->color)."'" : "NULL");
|
||||||
|
$sql .= ", output_label = ".($this->output_label ? "'".$this->db->escape($this->output_label)."'" : "NULL");
|
||||||
|
$sql .= ", medium_type = ".($this->medium_type ? "'".$this->db->escape($this->medium_type)."'" : "NULL");
|
||||||
|
$sql .= ", medium_spec = ".($this->medium_spec ? "'".$this->db->escape($this->medium_spec)."'" : "NULL");
|
||||||
|
$sql .= ", medium_length = ".($this->medium_length ? "'".$this->db->escape($this->medium_length)."'" : "NULL");
|
||||||
|
$sql .= ", is_rail = ".((int) $this->is_rail);
|
||||||
|
$sql .= ", rail_start_te = ".($this->rail_start_te > 0 ? ((int) $this->rail_start_te) : "NULL");
|
||||||
|
$sql .= ", rail_end_te = ".($this->rail_end_te > 0 ? ((int) $this->rail_end_te) : "NULL");
|
||||||
|
$sql .= ", rail_phases = ".($this->rail_phases ? "'".$this->db->escape($this->rail_phases)."'" : "NULL");
|
||||||
|
$sql .= ", excluded_te = ".($this->excluded_te ? "'".$this->db->escape($this->excluded_te)."'" : "NULL");
|
||||||
|
$sql .= ", fk_carrier = ".($this->fk_carrier > 0 ? ((int) $this->fk_carrier) : "NULL");
|
||||||
|
$sql .= ", position_y = ".((int) $this->position_y);
|
||||||
|
$sql .= ", path_data = ".($this->path_data ? "'".$this->db->escape($this->path_data)."'" : "NULL");
|
||||||
|
$sql .= ", note_private = ".($this->note_private ? "'".$this->db->escape($this->note_private)."'" : "NULL");
|
||||||
|
$sql .= ", status = ".((int) $this->status);
|
||||||
|
$sql .= ", fk_user_modif = ".((int) $user->id);
|
||||||
|
$sql .= " WHERE rowid = ".((int) $this->id);
|
||||||
|
|
||||||
|
$resql = $this->db->query($sql);
|
||||||
|
if (!$resql) {
|
||||||
|
$error++;
|
||||||
|
$this->errors[] = "Error ".$this->db->lasterror();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($error) {
|
||||||
|
$this->db->rollback();
|
||||||
|
return -1 * $error;
|
||||||
|
} else {
|
||||||
|
$this->db->commit();
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete object in database
|
||||||
|
*
|
||||||
|
* @param User $user User that deletes
|
||||||
|
* @return int Return integer <0 if KO, >0 if OK
|
||||||
|
*/
|
||||||
|
public function delete($user)
|
||||||
|
{
|
||||||
|
$error = 0;
|
||||||
|
$this->db->begin();
|
||||||
|
|
||||||
|
$sql = "DELETE FROM ".MAIN_DB_PREFIX.$this->table_element." WHERE rowid = ".((int) $this->id);
|
||||||
|
$resql = $this->db->query($sql);
|
||||||
|
if (!$resql) {
|
||||||
|
$error++;
|
||||||
|
$this->errors[] = "Error ".$this->db->lasterror();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($error) {
|
||||||
|
$this->db->rollback();
|
||||||
|
return -1 * $error;
|
||||||
|
} else {
|
||||||
|
$this->db->commit();
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch all connections for a carrier
|
||||||
|
*
|
||||||
|
* @param int $carrierId Carrier ID
|
||||||
|
* @param int $activeOnly Only active connections
|
||||||
|
* @return array Array of EquipmentConnection objects
|
||||||
|
*/
|
||||||
|
public function fetchByCarrier($carrierId, $activeOnly = 1)
|
||||||
|
{
|
||||||
|
$results = array();
|
||||||
|
|
||||||
|
$sql = "SELECT c.*, ";
|
||||||
|
$sql .= " src.label as source_label, src.position_te as source_pos, src.width_te as source_width,";
|
||||||
|
$sql .= " tgt.label as target_label, tgt.position_te as target_pos";
|
||||||
|
$sql .= " FROM ".MAIN_DB_PREFIX.$this->table_element." as c";
|
||||||
|
$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."kundenkarte_equipment as src ON c.fk_source = src.rowid";
|
||||||
|
$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."kundenkarte_equipment as tgt ON c.fk_target = tgt.rowid";
|
||||||
|
$sql .= " WHERE c.fk_carrier = ".((int) $carrierId);
|
||||||
|
if ($activeOnly) {
|
||||||
|
$sql .= " AND c.status = 1";
|
||||||
|
}
|
||||||
|
$sql .= " ORDER BY c.position_y ASC, c.rowid ASC";
|
||||||
|
|
||||||
|
$resql = $this->db->query($sql);
|
||||||
|
if ($resql) {
|
||||||
|
while ($obj = $this->db->fetch_object($resql)) {
|
||||||
|
$conn = new EquipmentConnection($this->db);
|
||||||
|
$conn->id = $obj->rowid;
|
||||||
|
$conn->entity = $obj->entity;
|
||||||
|
$conn->fk_source = $obj->fk_source;
|
||||||
|
$conn->source_terminal = $obj->source_terminal;
|
||||||
|
$conn->source_terminal_id = $obj->source_terminal_id;
|
||||||
|
$conn->fk_target = $obj->fk_target;
|
||||||
|
$conn->target_terminal = $obj->target_terminal;
|
||||||
|
$conn->target_terminal_id = $obj->target_terminal_id;
|
||||||
|
$conn->connection_type = $obj->connection_type;
|
||||||
|
$conn->color = $obj->color;
|
||||||
|
$conn->output_label = $obj->output_label;
|
||||||
|
$conn->medium_type = $obj->medium_type;
|
||||||
|
$conn->medium_spec = $obj->medium_spec;
|
||||||
|
$conn->medium_length = $obj->medium_length;
|
||||||
|
$conn->is_rail = $obj->is_rail;
|
||||||
|
$conn->rail_start_te = $obj->rail_start_te;
|
||||||
|
$conn->rail_end_te = $obj->rail_end_te;
|
||||||
|
$conn->rail_phases = $obj->rail_phases;
|
||||||
|
$conn->excluded_te = $obj->excluded_te;
|
||||||
|
$conn->fk_carrier = $obj->fk_carrier;
|
||||||
|
$conn->position_y = $obj->position_y;
|
||||||
|
$conn->path_data = isset($obj->path_data) ? $obj->path_data : null;
|
||||||
|
$conn->status = $obj->status;
|
||||||
|
|
||||||
|
$conn->source_label = $obj->source_label;
|
||||||
|
$conn->source_pos = $obj->source_pos;
|
||||||
|
$conn->source_width = $obj->source_width;
|
||||||
|
$conn->target_label = $obj->target_label;
|
||||||
|
$conn->target_pos = $obj->target_pos;
|
||||||
|
|
||||||
|
$results[] = $conn;
|
||||||
|
}
|
||||||
|
$this->db->free($resql);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $results;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch all outputs for an equipment
|
||||||
|
*
|
||||||
|
* @param int $equipmentId Equipment ID
|
||||||
|
* @return array Array of EquipmentConnection objects
|
||||||
|
*/
|
||||||
|
public function fetchOutputs($equipmentId)
|
||||||
|
{
|
||||||
|
$results = array();
|
||||||
|
|
||||||
|
$sql = "SELECT * FROM ".MAIN_DB_PREFIX.$this->table_element;
|
||||||
|
$sql .= " WHERE fk_source = ".((int) $equipmentId);
|
||||||
|
$sql .= " AND fk_target IS NULL";
|
||||||
|
$sql .= " AND status = 1";
|
||||||
|
$sql .= " ORDER BY position_y ASC";
|
||||||
|
|
||||||
|
$resql = $this->db->query($sql);
|
||||||
|
if ($resql) {
|
||||||
|
while ($obj = $this->db->fetch_object($resql)) {
|
||||||
|
$conn = new EquipmentConnection($this->db);
|
||||||
|
$conn->id = $obj->rowid;
|
||||||
|
$conn->fk_source = $obj->fk_source;
|
||||||
|
$conn->connection_type = $obj->connection_type;
|
||||||
|
$conn->color = $obj->color;
|
||||||
|
$conn->output_label = $obj->output_label;
|
||||||
|
$conn->medium_type = $obj->medium_type;
|
||||||
|
$conn->medium_spec = $obj->medium_spec;
|
||||||
|
$conn->medium_length = $obj->medium_length;
|
||||||
|
$conn->fk_carrier = $obj->fk_carrier;
|
||||||
|
$conn->position_y = $obj->position_y;
|
||||||
|
$conn->status = $obj->status;
|
||||||
|
|
||||||
|
$results[] = $conn;
|
||||||
|
}
|
||||||
|
$this->db->free($resql);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $results;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get display color
|
||||||
|
*
|
||||||
|
* @return string Color hex code
|
||||||
|
*/
|
||||||
|
public function getColor()
|
||||||
|
{
|
||||||
|
if (!empty($this->color)) {
|
||||||
|
return $this->color;
|
||||||
|
}
|
||||||
|
return '#888888'; // Default grey
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get display label for output
|
||||||
|
*
|
||||||
|
* @return string Display label
|
||||||
|
*/
|
||||||
|
public function getDisplayLabel()
|
||||||
|
{
|
||||||
|
$parts = array();
|
||||||
|
|
||||||
|
if ($this->output_label) {
|
||||||
|
$parts[] = $this->output_label;
|
||||||
|
}
|
||||||
|
if ($this->medium_type) {
|
||||||
|
$mediumInfo = $this->medium_type;
|
||||||
|
if ($this->medium_spec) {
|
||||||
|
$mediumInfo .= ' '.$this->medium_spec;
|
||||||
|
}
|
||||||
|
if ($this->medium_length) {
|
||||||
|
$mediumInfo .= ' ('.$this->medium_length.')';
|
||||||
|
}
|
||||||
|
$parts[] = $mediumInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
return implode(' - ', $parts);
|
||||||
|
}
|
||||||
|
}
|
||||||
285
class/equipmentpanel.class.php
Executable file
285
class/equipmentpanel.class.php
Executable file
|
|
@ -0,0 +1,285 @@
|
||||||
|
<?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 EquipmentPanel
|
||||||
|
* Manages equipment panels (Schaltschrankfelder)
|
||||||
|
* Physical sections in distribution boards containing carriers
|
||||||
|
*/
|
||||||
|
class EquipmentPanel extends CommonObject
|
||||||
|
{
|
||||||
|
public $element = 'equipmentpanel';
|
||||||
|
public $table_element = 'kundenkarte_equipment_panel';
|
||||||
|
|
||||||
|
public $fk_anlage;
|
||||||
|
public $label;
|
||||||
|
public $position;
|
||||||
|
public $note_private;
|
||||||
|
public $status;
|
||||||
|
|
||||||
|
public $date_creation;
|
||||||
|
public $fk_user_creat;
|
||||||
|
public $fk_user_modif;
|
||||||
|
|
||||||
|
// Loaded objects
|
||||||
|
public $carriers = array();
|
||||||
|
public $anlage_label;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor
|
||||||
|
*
|
||||||
|
* @param DoliDB $db Database handler
|
||||||
|
*/
|
||||||
|
public function __construct($db)
|
||||||
|
{
|
||||||
|
$this->db = $db;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create object in database
|
||||||
|
*
|
||||||
|
* @param User $user User that creates
|
||||||
|
* @return int Return integer <0 if KO, Id of created object if OK
|
||||||
|
*/
|
||||||
|
public function create($user)
|
||||||
|
{
|
||||||
|
global $conf;
|
||||||
|
|
||||||
|
$error = 0;
|
||||||
|
$now = dol_now();
|
||||||
|
|
||||||
|
if (empty($this->fk_anlage)) {
|
||||||
|
$this->error = 'ErrorMissingParameters';
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get next position
|
||||||
|
if (empty($this->position)) {
|
||||||
|
$sql = "SELECT MAX(position) as maxpos FROM ".MAIN_DB_PREFIX.$this->table_element;
|
||||||
|
$sql .= " WHERE fk_anlage = ".((int) $this->fk_anlage);
|
||||||
|
$resql = $this->db->query($sql);
|
||||||
|
if ($resql) {
|
||||||
|
$obj = $this->db->fetch_object($resql);
|
||||||
|
$this->position = ($obj->maxpos !== null) ? $obj->maxpos + 1 : 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default label if not set
|
||||||
|
if (empty($this->label)) {
|
||||||
|
$this->label = 'Feld '.($this->position + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->db->begin();
|
||||||
|
|
||||||
|
$sql = "INSERT INTO ".MAIN_DB_PREFIX.$this->table_element." (";
|
||||||
|
$sql .= "entity, fk_anlage, label, position, note_private, status,";
|
||||||
|
$sql .= " date_creation, fk_user_creat";
|
||||||
|
$sql .= ") VALUES (";
|
||||||
|
$sql .= ((int) $conf->entity);
|
||||||
|
$sql .= ", ".((int) $this->fk_anlage);
|
||||||
|
$sql .= ", '".$this->db->escape($this->label)."'";
|
||||||
|
$sql .= ", ".((int) $this->position);
|
||||||
|
$sql .= ", ".($this->note_private ? "'".$this->db->escape($this->note_private)."'" : "NULL");
|
||||||
|
$sql .= ", ".((int) ($this->status !== null ? $this->status : 1));
|
||||||
|
$sql .= ", '".$this->db->idate($now)."'";
|
||||||
|
$sql .= ", ".((int) $user->id);
|
||||||
|
$sql .= ")";
|
||||||
|
|
||||||
|
$resql = $this->db->query($sql);
|
||||||
|
if (!$resql) {
|
||||||
|
$error++;
|
||||||
|
$this->errors[] = "Error ".$this->db->lasterror();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$error) {
|
||||||
|
$this->id = $this->db->last_insert_id(MAIN_DB_PREFIX.$this->table_element);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($error) {
|
||||||
|
$this->db->rollback();
|
||||||
|
return -1 * $error;
|
||||||
|
} else {
|
||||||
|
$this->db->commit();
|
||||||
|
return $this->id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load object from database
|
||||||
|
*
|
||||||
|
* @param int $id ID of record
|
||||||
|
* @return int Return integer <0 if KO, 0 if not found, >0 if OK
|
||||||
|
*/
|
||||||
|
public function fetch($id)
|
||||||
|
{
|
||||||
|
$sql = "SELECT p.*, a.label as anlage_label";
|
||||||
|
$sql .= " FROM ".MAIN_DB_PREFIX.$this->table_element." as p";
|
||||||
|
$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."kundenkarte_anlage as a ON p.fk_anlage = a.rowid";
|
||||||
|
$sql .= " WHERE p.rowid = ".((int) $id);
|
||||||
|
|
||||||
|
$resql = $this->db->query($sql);
|
||||||
|
if ($resql) {
|
||||||
|
if ($this->db->num_rows($resql)) {
|
||||||
|
$obj = $this->db->fetch_object($resql);
|
||||||
|
|
||||||
|
$this->id = $obj->rowid;
|
||||||
|
$this->entity = $obj->entity;
|
||||||
|
$this->fk_anlage = $obj->fk_anlage;
|
||||||
|
$this->label = $obj->label;
|
||||||
|
$this->position = $obj->position;
|
||||||
|
$this->note_private = $obj->note_private;
|
||||||
|
$this->status = $obj->status;
|
||||||
|
$this->date_creation = $this->db->jdate($obj->date_creation);
|
||||||
|
$this->fk_user_creat = $obj->fk_user_creat;
|
||||||
|
$this->fk_user_modif = $obj->fk_user_modif;
|
||||||
|
|
||||||
|
$this->anlage_label = $obj->anlage_label;
|
||||||
|
|
||||||
|
$this->db->free($resql);
|
||||||
|
return 1;
|
||||||
|
} else {
|
||||||
|
$this->db->free($resql);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$this->error = $this->db->lasterror();
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update object in database
|
||||||
|
*
|
||||||
|
* @param User $user User that modifies
|
||||||
|
* @return int Return integer <0 if KO, >0 if OK
|
||||||
|
*/
|
||||||
|
public function update($user)
|
||||||
|
{
|
||||||
|
$error = 0;
|
||||||
|
|
||||||
|
$this->db->begin();
|
||||||
|
|
||||||
|
$sql = "UPDATE ".MAIN_DB_PREFIX.$this->table_element." SET";
|
||||||
|
$sql .= " label = '".$this->db->escape($this->label)."'";
|
||||||
|
$sql .= ", position = ".((int) $this->position);
|
||||||
|
$sql .= ", note_private = ".($this->note_private ? "'".$this->db->escape($this->note_private)."'" : "NULL");
|
||||||
|
$sql .= ", status = ".((int) $this->status);
|
||||||
|
$sql .= ", fk_user_modif = ".((int) $user->id);
|
||||||
|
$sql .= " WHERE rowid = ".((int) $this->id);
|
||||||
|
|
||||||
|
$resql = $this->db->query($sql);
|
||||||
|
if (!$resql) {
|
||||||
|
$error++;
|
||||||
|
$this->errors[] = "Error ".$this->db->lasterror();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($error) {
|
||||||
|
$this->db->rollback();
|
||||||
|
return -1 * $error;
|
||||||
|
} else {
|
||||||
|
$this->db->commit();
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete object in database
|
||||||
|
*
|
||||||
|
* @param User $user User that deletes
|
||||||
|
* @return int Return integer <0 if KO, >0 if OK
|
||||||
|
*/
|
||||||
|
public function delete($user)
|
||||||
|
{
|
||||||
|
$error = 0;
|
||||||
|
$this->db->begin();
|
||||||
|
|
||||||
|
// Carriers are deleted via CASCADE or need to be reassigned
|
||||||
|
|
||||||
|
$sql = "DELETE FROM ".MAIN_DB_PREFIX.$this->table_element." WHERE rowid = ".((int) $this->id);
|
||||||
|
$resql = $this->db->query($sql);
|
||||||
|
if (!$resql) {
|
||||||
|
$error++;
|
||||||
|
$this->errors[] = "Error ".$this->db->lasterror();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($error) {
|
||||||
|
$this->db->rollback();
|
||||||
|
return -1 * $error;
|
||||||
|
} else {
|
||||||
|
$this->db->commit();
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch all panels for an Anlage
|
||||||
|
*
|
||||||
|
* @param int $anlageId Anlage ID
|
||||||
|
* @param int $activeOnly Only active panels
|
||||||
|
* @return array Array of EquipmentPanel objects
|
||||||
|
*/
|
||||||
|
public function fetchByAnlage($anlageId, $activeOnly = 1)
|
||||||
|
{
|
||||||
|
$results = array();
|
||||||
|
|
||||||
|
$sql = "SELECT * FROM ".MAIN_DB_PREFIX.$this->table_element;
|
||||||
|
$sql .= " WHERE fk_anlage = ".((int) $anlageId);
|
||||||
|
if ($activeOnly) {
|
||||||
|
$sql .= " AND status = 1";
|
||||||
|
}
|
||||||
|
$sql .= " ORDER BY position ASC";
|
||||||
|
|
||||||
|
$resql = $this->db->query($sql);
|
||||||
|
if ($resql) {
|
||||||
|
while ($obj = $this->db->fetch_object($resql)) {
|
||||||
|
$panel = new EquipmentPanel($this->db);
|
||||||
|
$panel->id = $obj->rowid;
|
||||||
|
$panel->entity = $obj->entity;
|
||||||
|
$panel->fk_anlage = $obj->fk_anlage;
|
||||||
|
$panel->label = $obj->label;
|
||||||
|
$panel->position = $obj->position;
|
||||||
|
$panel->note_private = $obj->note_private;
|
||||||
|
$panel->status = $obj->status;
|
||||||
|
|
||||||
|
$results[] = $panel;
|
||||||
|
}
|
||||||
|
$this->db->free($resql);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $results;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch all carriers in this panel
|
||||||
|
*
|
||||||
|
* @return array Array of EquipmentCarrier objects
|
||||||
|
*/
|
||||||
|
public function fetchCarriers()
|
||||||
|
{
|
||||||
|
require_once DOL_DOCUMENT_ROOT.'/custom/kundenkarte/class/equipmentcarrier.class.php';
|
||||||
|
|
||||||
|
$carrier = new EquipmentCarrier($this->db);
|
||||||
|
$this->carriers = $carrier->fetchByPanel($this->id);
|
||||||
|
|
||||||
|
return $this->carriers;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get total carriers count
|
||||||
|
*
|
||||||
|
* @return int Number of carriers
|
||||||
|
*/
|
||||||
|
public function getCarrierCount()
|
||||||
|
{
|
||||||
|
if (empty($this->carriers)) {
|
||||||
|
$this->fetchCarriers();
|
||||||
|
}
|
||||||
|
return count($this->carriers);
|
||||||
|
}
|
||||||
|
}
|
||||||
394
class/equipmenttype.class.php
Executable file
394
class/equipmenttype.class.php
Executable file
|
|
@ -0,0 +1,394 @@
|
||||||
|
<?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 EquipmentType
|
||||||
|
* Manages equipment type templates (Hutschienen-Komponenten)
|
||||||
|
*/
|
||||||
|
class EquipmentType extends CommonObject
|
||||||
|
{
|
||||||
|
public $element = 'equipmenttype';
|
||||||
|
public $table_element = 'kundenkarte_equipment_type';
|
||||||
|
|
||||||
|
public $ref;
|
||||||
|
public $label;
|
||||||
|
public $label_short;
|
||||||
|
public $description;
|
||||||
|
public $fk_system;
|
||||||
|
|
||||||
|
// Equipment-spezifische Felder
|
||||||
|
public $width_te = 1;
|
||||||
|
public $color;
|
||||||
|
public $fk_product;
|
||||||
|
public $terminals_config; // JSON config for terminals
|
||||||
|
public $flow_direction; // Flussrichtung: NULL=keine, top_to_bottom, bottom_to_top
|
||||||
|
public $terminal_position = 'both'; // Terminal-Position: both, top_only, bottom_only
|
||||||
|
|
||||||
|
public $picto;
|
||||||
|
public $icon_file; // Uploaded SVG/PNG file for schematic symbol (PDF)
|
||||||
|
public $block_image; // Uploaded image for block display in SchematicEditor
|
||||||
|
public $is_system;
|
||||||
|
public $position;
|
||||||
|
public $active;
|
||||||
|
|
||||||
|
public $date_creation;
|
||||||
|
public $fk_user_creat;
|
||||||
|
public $fk_user_modif;
|
||||||
|
|
||||||
|
// Loaded objects
|
||||||
|
public $system_label;
|
||||||
|
public $system_code;
|
||||||
|
public $product; // Linked product object
|
||||||
|
public $fields = array();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor
|
||||||
|
*
|
||||||
|
* @param DoliDB $db Database handler
|
||||||
|
*/
|
||||||
|
public function __construct($db)
|
||||||
|
{
|
||||||
|
$this->db = $db;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create object in database
|
||||||
|
*
|
||||||
|
* @param User $user User that creates
|
||||||
|
* @return int Return integer <0 if KO, Id of created object if OK
|
||||||
|
*/
|
||||||
|
public function create($user)
|
||||||
|
{
|
||||||
|
global $conf;
|
||||||
|
|
||||||
|
$error = 0;
|
||||||
|
$now = dol_now();
|
||||||
|
|
||||||
|
if (empty($this->ref) || empty($this->label) || empty($this->fk_system)) {
|
||||||
|
$this->error = 'ErrorMissingParameters';
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->db->begin();
|
||||||
|
|
||||||
|
$sql = "INSERT INTO ".MAIN_DB_PREFIX.$this->table_element." (";
|
||||||
|
$sql .= "entity, ref, label, label_short, description, fk_system,";
|
||||||
|
$sql .= " width_te, color, fk_product, terminals_config, flow_direction, terminal_position,";
|
||||||
|
$sql .= " picto, icon_file, block_image, is_system, position, active,";
|
||||||
|
$sql .= " date_creation, fk_user_creat";
|
||||||
|
$sql .= ") VALUES (";
|
||||||
|
$sql .= "0"; // entity 0 = global
|
||||||
|
$sql .= ", '".$this->db->escape($this->ref)."'";
|
||||||
|
$sql .= ", '".$this->db->escape($this->label)."'";
|
||||||
|
$sql .= ", ".($this->label_short ? "'".$this->db->escape($this->label_short)."'" : "NULL");
|
||||||
|
$sql .= ", ".($this->description ? "'".$this->db->escape($this->description)."'" : "NULL");
|
||||||
|
$sql .= ", ".((int) $this->fk_system);
|
||||||
|
$sql .= ", ".((int) ($this->width_te > 0 ? $this->width_te : 1));
|
||||||
|
$sql .= ", ".($this->color ? "'".$this->db->escape($this->color)."'" : "NULL");
|
||||||
|
$sql .= ", ".($this->fk_product > 0 ? ((int) $this->fk_product) : "NULL");
|
||||||
|
$sql .= ", ".($this->terminals_config ? "'".$this->db->escape($this->terminals_config)."'" : "NULL");
|
||||||
|
$sql .= ", ".($this->flow_direction ? "'".$this->db->escape($this->flow_direction)."'" : "NULL");
|
||||||
|
$sql .= ", '".$this->db->escape($this->terminal_position ?: 'both')."'";
|
||||||
|
$sql .= ", ".($this->picto ? "'".$this->db->escape($this->picto)."'" : "NULL");
|
||||||
|
$sql .= ", ".($this->icon_file ? "'".$this->db->escape($this->icon_file)."'" : "NULL");
|
||||||
|
$sql .= ", ".($this->block_image ? "'".$this->db->escape($this->block_image)."'" : "NULL");
|
||||||
|
$sql .= ", 0"; // is_system = 0 for user-created
|
||||||
|
$sql .= ", ".((int) $this->position);
|
||||||
|
$sql .= ", ".((int) ($this->active !== null ? $this->active : 1));
|
||||||
|
$sql .= ", '".$this->db->idate($now)."'";
|
||||||
|
$sql .= ", ".((int) $user->id);
|
||||||
|
$sql .= ")";
|
||||||
|
|
||||||
|
$resql = $this->db->query($sql);
|
||||||
|
if (!$resql) {
|
||||||
|
$error++;
|
||||||
|
$this->errors[] = "Error ".$this->db->lasterror();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$error) {
|
||||||
|
$this->id = $this->db->last_insert_id(MAIN_DB_PREFIX.$this->table_element);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($error) {
|
||||||
|
$this->db->rollback();
|
||||||
|
return -1 * $error;
|
||||||
|
} else {
|
||||||
|
$this->db->commit();
|
||||||
|
return $this->id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load object from database
|
||||||
|
*
|
||||||
|
* @param int $id ID of record
|
||||||
|
* @return int Return integer <0 if KO, 0 if not found, >0 if OK
|
||||||
|
*/
|
||||||
|
public function fetch($id)
|
||||||
|
{
|
||||||
|
$sql = "SELECT t.*, s.label as system_label, s.code as system_code,";
|
||||||
|
$sql .= " p.ref as product_ref, p.label as product_label";
|
||||||
|
$sql .= " FROM ".MAIN_DB_PREFIX.$this->table_element." as t";
|
||||||
|
$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."c_kundenkarte_anlage_system as s ON t.fk_system = s.rowid";
|
||||||
|
$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."product as p ON t.fk_product = p.rowid";
|
||||||
|
$sql .= " WHERE t.rowid = ".((int) $id);
|
||||||
|
|
||||||
|
$resql = $this->db->query($sql);
|
||||||
|
if ($resql) {
|
||||||
|
if ($this->db->num_rows($resql)) {
|
||||||
|
$obj = $this->db->fetch_object($resql);
|
||||||
|
|
||||||
|
$this->id = $obj->rowid;
|
||||||
|
$this->entity = $obj->entity;
|
||||||
|
$this->ref = $obj->ref;
|
||||||
|
$this->label = $obj->label;
|
||||||
|
$this->label_short = $obj->label_short;
|
||||||
|
$this->description = $obj->description;
|
||||||
|
$this->fk_system = $obj->fk_system;
|
||||||
|
$this->width_te = $obj->width_te;
|
||||||
|
$this->color = $obj->color;
|
||||||
|
$this->fk_product = $obj->fk_product;
|
||||||
|
$this->terminals_config = $obj->terminals_config;
|
||||||
|
$this->flow_direction = $obj->flow_direction;
|
||||||
|
$this->terminal_position = $obj->terminal_position ?: 'both';
|
||||||
|
$this->picto = $obj->picto;
|
||||||
|
$this->icon_file = $obj->icon_file;
|
||||||
|
$this->block_image = $obj->block_image;
|
||||||
|
$this->is_system = $obj->is_system;
|
||||||
|
$this->position = $obj->position;
|
||||||
|
$this->active = $obj->active;
|
||||||
|
$this->date_creation = $this->db->jdate($obj->date_creation);
|
||||||
|
$this->fk_user_creat = $obj->fk_user_creat;
|
||||||
|
$this->fk_user_modif = $obj->fk_user_modif;
|
||||||
|
|
||||||
|
$this->system_label = $obj->system_label;
|
||||||
|
$this->system_code = $obj->system_code;
|
||||||
|
$this->product_ref = $obj->product_ref;
|
||||||
|
$this->product_label = $obj->product_label;
|
||||||
|
|
||||||
|
$this->db->free($resql);
|
||||||
|
return 1;
|
||||||
|
} else {
|
||||||
|
$this->db->free($resql);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$this->error = $this->db->lasterror();
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update object in database
|
||||||
|
*
|
||||||
|
* @param User $user User that modifies
|
||||||
|
* @return int Return integer <0 if KO, >0 if OK
|
||||||
|
*/
|
||||||
|
public function update($user)
|
||||||
|
{
|
||||||
|
$error = 0;
|
||||||
|
|
||||||
|
$this->db->begin();
|
||||||
|
|
||||||
|
$sql = "UPDATE ".MAIN_DB_PREFIX.$this->table_element." SET";
|
||||||
|
$sql .= " ref = '".$this->db->escape($this->ref)."'";
|
||||||
|
$sql .= ", label = '".$this->db->escape($this->label)."'";
|
||||||
|
$sql .= ", label_short = ".($this->label_short ? "'".$this->db->escape($this->label_short)."'" : "NULL");
|
||||||
|
$sql .= ", description = ".($this->description ? "'".$this->db->escape($this->description)."'" : "NULL");
|
||||||
|
$sql .= ", fk_system = ".((int) $this->fk_system);
|
||||||
|
$sql .= ", width_te = ".((int) ($this->width_te > 0 ? $this->width_te : 1));
|
||||||
|
$sql .= ", color = ".($this->color ? "'".$this->db->escape($this->color)."'" : "NULL");
|
||||||
|
$sql .= ", fk_product = ".($this->fk_product > 0 ? ((int) $this->fk_product) : "NULL");
|
||||||
|
$sql .= ", terminals_config = ".($this->terminals_config ? "'".$this->db->escape($this->terminals_config)."'" : "NULL");
|
||||||
|
$sql .= ", flow_direction = ".($this->flow_direction ? "'".$this->db->escape($this->flow_direction)."'" : "NULL");
|
||||||
|
$sql .= ", terminal_position = '".$this->db->escape($this->terminal_position ?: 'both')."'";
|
||||||
|
$sql .= ", picto = ".($this->picto ? "'".$this->db->escape($this->picto)."'" : "NULL");
|
||||||
|
$sql .= ", icon_file = ".($this->icon_file ? "'".$this->db->escape($this->icon_file)."'" : "NULL");
|
||||||
|
$sql .= ", block_image = ".($this->block_image ? "'".$this->db->escape($this->block_image)."'" : "NULL");
|
||||||
|
$sql .= ", position = ".((int) $this->position);
|
||||||
|
$sql .= ", active = ".((int) $this->active);
|
||||||
|
$sql .= ", fk_user_modif = ".((int) $user->id);
|
||||||
|
$sql .= " WHERE rowid = ".((int) $this->id);
|
||||||
|
|
||||||
|
$resql = $this->db->query($sql);
|
||||||
|
if (!$resql) {
|
||||||
|
$error++;
|
||||||
|
$this->errors[] = "Error ".$this->db->lasterror();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($error) {
|
||||||
|
$this->db->rollback();
|
||||||
|
return -1 * $error;
|
||||||
|
} else {
|
||||||
|
$this->db->commit();
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete object in database
|
||||||
|
*
|
||||||
|
* @param User $user User that deletes
|
||||||
|
* @return int Return integer <0 if KO, >0 if OK
|
||||||
|
*/
|
||||||
|
public function delete($user)
|
||||||
|
{
|
||||||
|
global $conf;
|
||||||
|
|
||||||
|
// Check if type is in use
|
||||||
|
$sql = "SELECT COUNT(*) as cnt FROM ".MAIN_DB_PREFIX."kundenkarte_equipment";
|
||||||
|
$sql .= " WHERE fk_equipment_type = ".((int) $this->id);
|
||||||
|
$resql = $this->db->query($sql);
|
||||||
|
if ($resql) {
|
||||||
|
$obj = $this->db->fetch_object($resql);
|
||||||
|
if ($obj->cnt > 0) {
|
||||||
|
$this->error = 'ErrorTypeInUse';
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cannot delete system types
|
||||||
|
if ($this->is_system) {
|
||||||
|
$this->error = 'ErrorCannotDeleteSystemType';
|
||||||
|
return -2;
|
||||||
|
}
|
||||||
|
|
||||||
|
$error = 0;
|
||||||
|
$this->db->begin();
|
||||||
|
|
||||||
|
// Delete fields first
|
||||||
|
$sql = "DELETE FROM ".MAIN_DB_PREFIX."kundenkarte_equipment_type_field WHERE fk_equipment_type = ".((int) $this->id);
|
||||||
|
$this->db->query($sql);
|
||||||
|
|
||||||
|
// Delete type
|
||||||
|
$sql = "DELETE FROM ".MAIN_DB_PREFIX.$this->table_element." WHERE rowid = ".((int) $this->id);
|
||||||
|
$resql = $this->db->query($sql);
|
||||||
|
if (!$resql) {
|
||||||
|
$error++;
|
||||||
|
$this->errors[] = "Error ".$this->db->lasterror();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($error) {
|
||||||
|
$this->db->rollback();
|
||||||
|
return -1 * $error;
|
||||||
|
} else {
|
||||||
|
$this->db->commit();
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch all equipment types for a system
|
||||||
|
*
|
||||||
|
* @param int $systemId System ID (0 = all)
|
||||||
|
* @param int $activeOnly Only active types
|
||||||
|
* @return array Array of EquipmentType objects
|
||||||
|
*/
|
||||||
|
public function fetchAllBySystem($systemId = 0, $activeOnly = 1)
|
||||||
|
{
|
||||||
|
$results = array();
|
||||||
|
|
||||||
|
$sql = "SELECT t.*, s.label as system_label, s.code as system_code";
|
||||||
|
$sql .= " FROM ".MAIN_DB_PREFIX.$this->table_element." as t";
|
||||||
|
$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."c_kundenkarte_anlage_system as s ON t.fk_system = s.rowid";
|
||||||
|
$sql .= " WHERE 1 = 1";
|
||||||
|
if ($systemId > 0) {
|
||||||
|
$sql .= " AND t.fk_system = ".((int) $systemId);
|
||||||
|
}
|
||||||
|
if ($activeOnly) {
|
||||||
|
$sql .= " AND t.active = 1";
|
||||||
|
}
|
||||||
|
$sql .= " ORDER BY t.fk_system ASC, t.position ASC, t.label ASC";
|
||||||
|
|
||||||
|
$resql = $this->db->query($sql);
|
||||||
|
if ($resql) {
|
||||||
|
while ($obj = $this->db->fetch_object($resql)) {
|
||||||
|
$type = new EquipmentType($this->db);
|
||||||
|
$type->id = $obj->rowid;
|
||||||
|
$type->ref = $obj->ref;
|
||||||
|
$type->label = $obj->label;
|
||||||
|
$type->label_short = $obj->label_short;
|
||||||
|
$type->fk_system = $obj->fk_system;
|
||||||
|
$type->width_te = $obj->width_te;
|
||||||
|
$type->color = $obj->color;
|
||||||
|
$type->fk_product = $obj->fk_product;
|
||||||
|
$type->flow_direction = $obj->flow_direction;
|
||||||
|
$type->terminal_position = $obj->terminal_position ?: 'both';
|
||||||
|
$type->picto = $obj->picto;
|
||||||
|
$type->icon_file = $obj->icon_file;
|
||||||
|
$type->block_image = $obj->block_image;
|
||||||
|
$type->is_system = $obj->is_system;
|
||||||
|
$type->position = $obj->position;
|
||||||
|
$type->active = $obj->active;
|
||||||
|
$type->system_label = $obj->system_label;
|
||||||
|
$type->system_code = $obj->system_code;
|
||||||
|
|
||||||
|
$results[] = $type;
|
||||||
|
}
|
||||||
|
$this->db->free($resql);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $results;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch fields for this equipment type
|
||||||
|
*
|
||||||
|
* @param int $activeOnly Only active fields
|
||||||
|
* @return array Array of field objects
|
||||||
|
*/
|
||||||
|
public function fetchFields($activeOnly = 1)
|
||||||
|
{
|
||||||
|
$results = array();
|
||||||
|
|
||||||
|
$sql = "SELECT * FROM ".MAIN_DB_PREFIX."kundenkarte_equipment_type_field";
|
||||||
|
$sql .= " WHERE fk_equipment_type = ".((int) $this->id);
|
||||||
|
if ($activeOnly) {
|
||||||
|
$sql .= " AND active = 1";
|
||||||
|
}
|
||||||
|
$sql .= " ORDER BY position ASC";
|
||||||
|
|
||||||
|
$resql = $this->db->query($sql);
|
||||||
|
if ($resql) {
|
||||||
|
while ($obj = $this->db->fetch_object($resql)) {
|
||||||
|
$results[] = $obj;
|
||||||
|
}
|
||||||
|
$this->db->free($resql);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->fields = $results;
|
||||||
|
return $results;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get fields that should be shown on the SVG block
|
||||||
|
*
|
||||||
|
* @return array Array of field objects with show_on_block = 1
|
||||||
|
*/
|
||||||
|
public function getBlockFields()
|
||||||
|
{
|
||||||
|
$results = array();
|
||||||
|
|
||||||
|
$sql = "SELECT * FROM ".MAIN_DB_PREFIX."kundenkarte_equipment_type_field";
|
||||||
|
$sql .= " WHERE fk_equipment_type = ".((int) $this->id);
|
||||||
|
$sql .= " AND show_on_block = 1";
|
||||||
|
$sql .= " AND active = 1";
|
||||||
|
$sql .= " ORDER BY position ASC";
|
||||||
|
|
||||||
|
$resql = $this->db->query($sql);
|
||||||
|
if ($resql) {
|
||||||
|
while ($obj = $this->db->fetch_object($resql)) {
|
||||||
|
$results[] = $obj;
|
||||||
|
}
|
||||||
|
$this->db->free($resql);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $results;
|
||||||
|
}
|
||||||
|
}
|
||||||
802
class/favoriteproduct.class.php
Executable file
802
class/favoriteproduct.class.php
Executable file
|
|
@ -0,0 +1,802 @@
|
||||||
|
<?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 FavoriteProduct
|
||||||
|
* Manages favorite products for customers
|
||||||
|
*/
|
||||||
|
class FavoriteProduct extends CommonObject
|
||||||
|
{
|
||||||
|
public $element = 'favoriteproduct';
|
||||||
|
public $table_element = 'kundenkarte_favorite_products';
|
||||||
|
|
||||||
|
public $fk_soc;
|
||||||
|
public $fk_contact;
|
||||||
|
public $fk_product;
|
||||||
|
public $qty;
|
||||||
|
public $rang;
|
||||||
|
public $note;
|
||||||
|
public $active;
|
||||||
|
public $date_creation;
|
||||||
|
public $fk_user_creat;
|
||||||
|
public $fk_user_modif;
|
||||||
|
|
||||||
|
// Loaded objects
|
||||||
|
public $product;
|
||||||
|
public $societe;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor
|
||||||
|
*
|
||||||
|
* @param DoliDB $db Database handler
|
||||||
|
*/
|
||||||
|
public function __construct($db)
|
||||||
|
{
|
||||||
|
$this->db = $db;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create object in database
|
||||||
|
*
|
||||||
|
* @param User $user User that creates
|
||||||
|
* @param bool $notrigger false=launch triggers, true=disable triggers
|
||||||
|
* @return int Return integer <0 if KO, Id of created object if OK
|
||||||
|
*/
|
||||||
|
public function create($user, $notrigger = false)
|
||||||
|
{
|
||||||
|
global $conf;
|
||||||
|
|
||||||
|
$error = 0;
|
||||||
|
$now = dol_now();
|
||||||
|
|
||||||
|
// Check parameters
|
||||||
|
if (empty($this->fk_soc) || empty($this->fk_product)) {
|
||||||
|
$this->error = 'ErrorMissingParameters';
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if already exists
|
||||||
|
if ($this->alreadyExists($this->fk_soc, $this->fk_product, $this->fk_contact)) {
|
||||||
|
$this->error = 'ErrorRecordAlreadyExists';
|
||||||
|
return -2;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->db->begin();
|
||||||
|
|
||||||
|
// Get max rang for this customer/contact to add at end of list
|
||||||
|
$maxRang = 0;
|
||||||
|
$sqlRang = "SELECT MAX(rang) as maxrang FROM ".MAIN_DB_PREFIX.$this->table_element;
|
||||||
|
$sqlRang .= " WHERE fk_soc = ".((int) $this->fk_soc);
|
||||||
|
if ($this->fk_contact > 0) {
|
||||||
|
$sqlRang .= " AND fk_contact = ".((int) $this->fk_contact);
|
||||||
|
} else {
|
||||||
|
$sqlRang .= " AND (fk_contact IS NULL OR fk_contact = 0)";
|
||||||
|
}
|
||||||
|
$sqlRang .= " AND entity = ".((int) $conf->entity);
|
||||||
|
$resRang = $this->db->query($sqlRang);
|
||||||
|
if ($resRang) {
|
||||||
|
$objRang = $this->db->fetch_object($resRang);
|
||||||
|
$maxRang = ($objRang->maxrang !== null) ? ((int) $objRang->maxrang + 1) : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$sql = "INSERT INTO ".MAIN_DB_PREFIX.$this->table_element." (";
|
||||||
|
$sql .= "entity, fk_soc, fk_contact, fk_product, qty, rang, note, active, date_creation, fk_user_creat";
|
||||||
|
$sql .= ") VALUES (";
|
||||||
|
$sql .= ((int) $conf->entity);
|
||||||
|
$sql .= ", ".((int) $this->fk_soc);
|
||||||
|
$sql .= ", ".($this->fk_contact > 0 ? ((int) $this->fk_contact) : "NULL");
|
||||||
|
$sql .= ", ".((int) $this->fk_product);
|
||||||
|
$sql .= ", ".((float) ($this->qty > 0 ? $this->qty : 1));
|
||||||
|
$sql .= ", ".((int) $maxRang);
|
||||||
|
$sql .= ", ".($this->note ? "'".$this->db->escape($this->note)."'" : "NULL");
|
||||||
|
$sql .= ", ".((int) ($this->active !== null ? $this->active : 1));
|
||||||
|
$sql .= ", '".$this->db->idate($now)."'";
|
||||||
|
$sql .= ", ".((int) $user->id);
|
||||||
|
$sql .= ")";
|
||||||
|
|
||||||
|
$resql = $this->db->query($sql);
|
||||||
|
if (!$resql) {
|
||||||
|
$error++;
|
||||||
|
$this->errors[] = "Error ".$this->db->lasterror();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$error) {
|
||||||
|
$this->id = $this->db->last_insert_id(MAIN_DB_PREFIX.$this->table_element);
|
||||||
|
$this->date_creation = $now;
|
||||||
|
$this->fk_user_creat = $user->id;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($error) {
|
||||||
|
$this->db->rollback();
|
||||||
|
return -1 * $error;
|
||||||
|
} else {
|
||||||
|
$this->db->commit();
|
||||||
|
return $this->id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load object from database
|
||||||
|
*
|
||||||
|
* @param int $id ID of record
|
||||||
|
* @return int Return integer <0 if KO, 0 if not found, >0 if OK
|
||||||
|
*/
|
||||||
|
public function fetch($id)
|
||||||
|
{
|
||||||
|
global $conf;
|
||||||
|
|
||||||
|
$sql = "SELECT rowid, entity, fk_soc, fk_contact, fk_product, qty, rang, note, active,";
|
||||||
|
$sql .= " date_creation, tms, fk_user_creat, fk_user_modif";
|
||||||
|
$sql .= " FROM ".MAIN_DB_PREFIX.$this->table_element;
|
||||||
|
$sql .= " WHERE rowid = ".((int) $id);
|
||||||
|
$sql .= " AND entity = ".((int) $conf->entity);
|
||||||
|
|
||||||
|
$resql = $this->db->query($sql);
|
||||||
|
if ($resql) {
|
||||||
|
if ($this->db->num_rows($resql)) {
|
||||||
|
$obj = $this->db->fetch_object($resql);
|
||||||
|
|
||||||
|
$this->id = $obj->rowid;
|
||||||
|
$this->entity = $obj->entity;
|
||||||
|
$this->fk_soc = $obj->fk_soc;
|
||||||
|
$this->fk_contact = $obj->fk_contact;
|
||||||
|
$this->fk_product = $obj->fk_product;
|
||||||
|
$this->qty = $obj->qty;
|
||||||
|
$this->rang = $obj->rang;
|
||||||
|
$this->note = $obj->note;
|
||||||
|
$this->active = $obj->active;
|
||||||
|
$this->date_creation = $this->db->jdate($obj->date_creation);
|
||||||
|
$this->tms = $this->db->jdate($obj->tms);
|
||||||
|
$this->fk_user_creat = $obj->fk_user_creat;
|
||||||
|
$this->fk_user_modif = $obj->fk_user_modif;
|
||||||
|
|
||||||
|
$this->db->free($resql);
|
||||||
|
return 1;
|
||||||
|
} else {
|
||||||
|
$this->db->free($resql);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$this->error = $this->db->lasterror();
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update object in database
|
||||||
|
*
|
||||||
|
* @param User $user User that modifies
|
||||||
|
* @param bool $notrigger false=launch triggers, true=disable triggers
|
||||||
|
* @return int Return integer <0 if KO, >0 if OK
|
||||||
|
*/
|
||||||
|
public function update($user, $notrigger = false)
|
||||||
|
{
|
||||||
|
$error = 0;
|
||||||
|
|
||||||
|
$this->db->begin();
|
||||||
|
|
||||||
|
$sql = "UPDATE ".MAIN_DB_PREFIX.$this->table_element." SET";
|
||||||
|
$sql .= " qty = ".((float) $this->qty);
|
||||||
|
$sql .= ", rang = ".((int) $this->rang);
|
||||||
|
$sql .= ", note = ".($this->note ? "'".$this->db->escape($this->note)."'" : "NULL");
|
||||||
|
$sql .= ", active = ".((int) $this->active);
|
||||||
|
$sql .= ", fk_user_modif = ".((int) $user->id);
|
||||||
|
$sql .= " WHERE rowid = ".((int) $this->id);
|
||||||
|
|
||||||
|
$resql = $this->db->query($sql);
|
||||||
|
if (!$resql) {
|
||||||
|
$error++;
|
||||||
|
$this->errors[] = "Error ".$this->db->lasterror();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($error) {
|
||||||
|
$this->db->rollback();
|
||||||
|
return -1 * $error;
|
||||||
|
} else {
|
||||||
|
$this->db->commit();
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete object in database
|
||||||
|
*
|
||||||
|
* @param User $user User that deletes
|
||||||
|
* @param bool $notrigger false=launch triggers, true=disable triggers
|
||||||
|
* @return int Return integer <0 if KO, >0 if OK
|
||||||
|
*/
|
||||||
|
public function delete($user, $notrigger = false)
|
||||||
|
{
|
||||||
|
$error = 0;
|
||||||
|
|
||||||
|
$this->db->begin();
|
||||||
|
|
||||||
|
$sql = "DELETE FROM ".MAIN_DB_PREFIX.$this->table_element;
|
||||||
|
$sql .= " WHERE rowid = ".((int) $this->id);
|
||||||
|
|
||||||
|
$resql = $this->db->query($sql);
|
||||||
|
if (!$resql) {
|
||||||
|
$error++;
|
||||||
|
$this->errors[] = "Error ".$this->db->lasterror();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($error) {
|
||||||
|
$this->db->rollback();
|
||||||
|
return -1 * $error;
|
||||||
|
} else {
|
||||||
|
$this->db->commit();
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a product is already a favorite for a customer/contact
|
||||||
|
*
|
||||||
|
* @param int $socid Customer ID
|
||||||
|
* @param int $productid Product ID
|
||||||
|
* @param int $contactid Contact ID (optional)
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function alreadyExists($socid, $productid, $contactid = 0)
|
||||||
|
{
|
||||||
|
global $conf;
|
||||||
|
|
||||||
|
$sql = "SELECT rowid FROM ".MAIN_DB_PREFIX.$this->table_element;
|
||||||
|
$sql .= " WHERE fk_soc = ".((int) $socid);
|
||||||
|
$sql .= " AND fk_product = ".((int) $productid);
|
||||||
|
if ($contactid > 0) {
|
||||||
|
$sql .= " AND fk_contact = ".((int) $contactid);
|
||||||
|
} else {
|
||||||
|
$sql .= " AND (fk_contact IS NULL OR fk_contact = 0)";
|
||||||
|
}
|
||||||
|
$sql .= " AND entity = ".((int) $conf->entity);
|
||||||
|
|
||||||
|
$resql = $this->db->query($sql);
|
||||||
|
if ($resql) {
|
||||||
|
return ($this->db->num_rows($resql) > 0);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all favorite products for a customer
|
||||||
|
*
|
||||||
|
* @param int $socid Customer ID
|
||||||
|
* @param int $activeonly Only active favorites
|
||||||
|
* @return array|int Array of FavoriteProduct objects or -1 if error
|
||||||
|
*/
|
||||||
|
public function fetchAllBySociete($socid, $activeonly = 1)
|
||||||
|
{
|
||||||
|
global $conf;
|
||||||
|
|
||||||
|
$results = array();
|
||||||
|
|
||||||
|
$sql = "SELECT fp.rowid, fp.fk_soc, fp.fk_product, fp.qty, fp.rang, fp.note, fp.active,";
|
||||||
|
$sql .= " p.ref as product_ref, p.label as product_label, p.price, p.price_ttc, p.tva_tx";
|
||||||
|
$sql .= " FROM ".MAIN_DB_PREFIX.$this->table_element." as fp";
|
||||||
|
$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."product as p ON fp.fk_product = p.rowid";
|
||||||
|
$sql .= " WHERE fp.fk_soc = ".((int) $socid);
|
||||||
|
// Only thirdparty-level favorites, not contact-specific
|
||||||
|
$sql .= " AND (fp.fk_contact IS NULL OR fp.fk_contact = 0)";
|
||||||
|
$sql .= " AND fp.entity = ".((int) $conf->entity);
|
||||||
|
if ($activeonly) {
|
||||||
|
$sql .= " AND fp.active = 1";
|
||||||
|
}
|
||||||
|
$sql .= " ORDER BY fp.rang ASC, p.label ASC";
|
||||||
|
|
||||||
|
$resql = $this->db->query($sql);
|
||||||
|
if ($resql) {
|
||||||
|
while ($obj = $this->db->fetch_object($resql)) {
|
||||||
|
$fav = new FavoriteProduct($this->db);
|
||||||
|
$fav->id = $obj->rowid;
|
||||||
|
$fav->fk_soc = $obj->fk_soc;
|
||||||
|
$fav->fk_product = $obj->fk_product;
|
||||||
|
$fav->qty = $obj->qty;
|
||||||
|
$fav->rang = $obj->rang;
|
||||||
|
$fav->note = $obj->note;
|
||||||
|
$fav->active = $obj->active;
|
||||||
|
|
||||||
|
// Product info
|
||||||
|
$fav->product_ref = $obj->product_ref;
|
||||||
|
$fav->product_label = $obj->product_label;
|
||||||
|
$fav->product_price = $obj->price;
|
||||||
|
$fav->product_price_ttc = $obj->price_ttc;
|
||||||
|
$fav->product_tva_tx = $obj->tva_tx;
|
||||||
|
|
||||||
|
$results[] = $fav;
|
||||||
|
}
|
||||||
|
$this->db->free($resql);
|
||||||
|
return $results;
|
||||||
|
} else {
|
||||||
|
$this->error = $this->db->lasterror();
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Move a favorite product up in the list
|
||||||
|
*
|
||||||
|
* @param int $id Favorite ID to move
|
||||||
|
* @param int $socid Customer ID
|
||||||
|
* @return int 1 if OK, <0 if KO
|
||||||
|
*/
|
||||||
|
public function moveUp($id, $socid)
|
||||||
|
{
|
||||||
|
return $this->movePosition($id, $socid, 'up');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Move a favorite product down in the list
|
||||||
|
*
|
||||||
|
* @param int $id Favorite ID to move
|
||||||
|
* @param int $socid Customer ID
|
||||||
|
* @return int 1 if OK, <0 if KO
|
||||||
|
*/
|
||||||
|
public function moveDown($id, $socid)
|
||||||
|
{
|
||||||
|
return $this->movePosition($id, $socid, 'down');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Move a favorite product position
|
||||||
|
*
|
||||||
|
* @param int $id Favorite ID to move
|
||||||
|
* @param int $socid Customer ID
|
||||||
|
* @param string $direction 'up' or 'down'
|
||||||
|
* @return int 1 if OK, <0 if KO
|
||||||
|
*/
|
||||||
|
private function movePosition($id, $socid, $direction)
|
||||||
|
{
|
||||||
|
global $conf;
|
||||||
|
|
||||||
|
// Get all favorites ordered by rang
|
||||||
|
$sql = "SELECT rowid, rang FROM ".MAIN_DB_PREFIX.$this->table_element;
|
||||||
|
$sql .= " WHERE fk_soc = ".((int) $socid);
|
||||||
|
$sql .= " AND entity = ".((int) $conf->entity);
|
||||||
|
$sql .= " ORDER BY rang ASC, rowid ASC";
|
||||||
|
|
||||||
|
$resql = $this->db->query($sql);
|
||||||
|
if (!$resql) {
|
||||||
|
$this->error = $this->db->lasterror();
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
$items = array();
|
||||||
|
$currentIndex = -1;
|
||||||
|
$i = 0;
|
||||||
|
while ($obj = $this->db->fetch_object($resql)) {
|
||||||
|
$items[$i] = array('id' => $obj->rowid, 'rang' => $i);
|
||||||
|
if ($obj->rowid == $id) {
|
||||||
|
$currentIndex = $i;
|
||||||
|
}
|
||||||
|
$i++;
|
||||||
|
}
|
||||||
|
$this->db->free($resql);
|
||||||
|
|
||||||
|
if ($currentIndex < 0) {
|
||||||
|
return 0; // Item not found
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate new positions
|
||||||
|
$swapIndex = -1;
|
||||||
|
if ($direction == 'up' && $currentIndex > 0) {
|
||||||
|
$swapIndex = $currentIndex - 1;
|
||||||
|
} elseif ($direction == 'down' && $currentIndex < count($items) - 1) {
|
||||||
|
$swapIndex = $currentIndex + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($swapIndex < 0) {
|
||||||
|
return 0; // Cannot move
|
||||||
|
}
|
||||||
|
|
||||||
|
// Swap positions
|
||||||
|
$this->db->begin();
|
||||||
|
|
||||||
|
$sql1 = "UPDATE ".MAIN_DB_PREFIX.$this->table_element." SET rang = ".((int) $swapIndex);
|
||||||
|
$sql1 .= " WHERE rowid = ".((int) $items[$currentIndex]['id']);
|
||||||
|
|
||||||
|
$sql2 = "UPDATE ".MAIN_DB_PREFIX.$this->table_element." SET rang = ".((int) $currentIndex);
|
||||||
|
$sql2 .= " WHERE rowid = ".((int) $items[$swapIndex]['id']);
|
||||||
|
|
||||||
|
if ($this->db->query($sql1) && $this->db->query($sql2)) {
|
||||||
|
$this->db->commit();
|
||||||
|
return 1;
|
||||||
|
} else {
|
||||||
|
$this->error = $this->db->lasterror();
|
||||||
|
$this->db->rollback();
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate an order from selected favorite products
|
||||||
|
*
|
||||||
|
* @param User $user User creating the order
|
||||||
|
* @param int $socid Customer ID
|
||||||
|
* @param array $selectedIds Array of favorite product IDs to include
|
||||||
|
* @param array $quantities Optional array of quantities (id => qty)
|
||||||
|
* @return int Order ID if OK, <0 if KO
|
||||||
|
*/
|
||||||
|
public function generateOrder($user, $socid, $selectedIds, $quantities = array())
|
||||||
|
{
|
||||||
|
global $conf, $langs, $mysoc;
|
||||||
|
|
||||||
|
require_once DOL_DOCUMENT_ROOT.'/commande/class/commande.class.php';
|
||||||
|
require_once DOL_DOCUMENT_ROOT.'/societe/class/societe.class.php';
|
||||||
|
require_once DOL_DOCUMENT_ROOT.'/product/class/product.class.php';
|
||||||
|
|
||||||
|
if (empty($selectedIds)) {
|
||||||
|
$this->error = 'NoProductsSelected';
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load thirdparty (required for VAT calculation)
|
||||||
|
$societe = new Societe($this->db);
|
||||||
|
if ($societe->fetch($socid) <= 0) {
|
||||||
|
$this->error = 'ErrorLoadingThirdparty';
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch selected favorites
|
||||||
|
$favorites = $this->fetchAllBySociete($socid);
|
||||||
|
if (!is_array($favorites)) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter to selected only
|
||||||
|
$toAdd = array();
|
||||||
|
foreach ($favorites as $fav) {
|
||||||
|
if (in_array($fav->id, $selectedIds)) {
|
||||||
|
$qty = isset($quantities[$fav->id]) ? (float) $quantities[$fav->id] : $fav->qty;
|
||||||
|
if ($qty > 0) {
|
||||||
|
$toAdd[] = array(
|
||||||
|
'product_id' => $fav->fk_product,
|
||||||
|
'qty' => $qty
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($toAdd)) {
|
||||||
|
$this->error = 'NoValidProductsToAdd';
|
||||||
|
return -2;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create order
|
||||||
|
$order = new Commande($this->db);
|
||||||
|
$order->socid = $socid;
|
||||||
|
$order->thirdparty = $societe; // Required for VAT calculation
|
||||||
|
$order->date = dol_now();
|
||||||
|
$order->ref_client = $societe->name.' - '.$langs->trans('FavoriteProducts'); // Ihr Zeichen
|
||||||
|
$order->note_private = $langs->trans('OrderGeneratedFromFavorites');
|
||||||
|
|
||||||
|
$this->db->begin();
|
||||||
|
|
||||||
|
// First create the order header
|
||||||
|
$result = $order->create($user);
|
||||||
|
|
||||||
|
if ($result <= 0) {
|
||||||
|
$this->error = $order->error;
|
||||||
|
$this->errors = $order->errors;
|
||||||
|
$this->db->rollback();
|
||||||
|
return -3;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now add products to the created order
|
||||||
|
foreach ($toAdd as $item) {
|
||||||
|
$product = new Product($this->db);
|
||||||
|
$product->fetch($item['product_id']);
|
||||||
|
|
||||||
|
// Get VAT rate for this product and customer (mysoc = seller, societe = buyer)
|
||||||
|
$tva_tx = get_default_tva($mysoc, $societe, $product->id);
|
||||||
|
$localtax1_tx = get_default_localtax($mysoc, $societe, 1, $product->id);
|
||||||
|
$localtax2_tx = get_default_localtax($mysoc, $societe, 2, $product->id);
|
||||||
|
|
||||||
|
$lineResult = $order->addline(
|
||||||
|
$product->label, // Description
|
||||||
|
$product->price, // Unit price HT
|
||||||
|
$item['qty'], // Quantity
|
||||||
|
$tva_tx, // VAT rate
|
||||||
|
$localtax1_tx, // Local tax 1
|
||||||
|
$localtax2_tx, // Local tax 2
|
||||||
|
$product->id, // Product ID
|
||||||
|
0, // Discount
|
||||||
|
0, // Info bits
|
||||||
|
0, // fk_remise_except
|
||||||
|
'HT', // Price base type
|
||||||
|
0, // Unit price TTC
|
||||||
|
'', // Date start
|
||||||
|
'', // Date end
|
||||||
|
0, // Type (0=product)
|
||||||
|
-1, // Rang
|
||||||
|
0, // Special code
|
||||||
|
0, // fk_parent_line
|
||||||
|
0, // fk_fournprice
|
||||||
|
0, // pa_ht
|
||||||
|
$product->label, // Label
|
||||||
|
array(), // Array options
|
||||||
|
$product->fk_unit // Unit
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($lineResult < 0) {
|
||||||
|
$this->error = $order->error;
|
||||||
|
$this->errors = $order->errors;
|
||||||
|
$this->db->rollback();
|
||||||
|
return -4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->db->commit();
|
||||||
|
return $order->id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all favorite products for a contact/address
|
||||||
|
*
|
||||||
|
* @param int $contactid Contact ID
|
||||||
|
* @param int $activeonly Only active favorites
|
||||||
|
* @return array|int Array of FavoriteProduct objects or -1 if error
|
||||||
|
*/
|
||||||
|
public function fetchAllByContact($contactid, $activeonly = 1)
|
||||||
|
{
|
||||||
|
global $conf;
|
||||||
|
|
||||||
|
$results = array();
|
||||||
|
|
||||||
|
$sql = "SELECT fp.rowid, fp.fk_soc, fp.fk_contact, fp.fk_product, fp.qty, fp.rang, fp.note, fp.active,";
|
||||||
|
$sql .= " p.ref as product_ref, p.label as product_label, p.price, p.price_ttc, p.tva_tx";
|
||||||
|
$sql .= " FROM ".MAIN_DB_PREFIX.$this->table_element." as fp";
|
||||||
|
$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."product as p ON fp.fk_product = p.rowid";
|
||||||
|
$sql .= " WHERE fp.fk_contact = ".((int) $contactid);
|
||||||
|
$sql .= " AND fp.entity = ".((int) $conf->entity);
|
||||||
|
if ($activeonly) {
|
||||||
|
$sql .= " AND fp.active = 1";
|
||||||
|
}
|
||||||
|
$sql .= " ORDER BY fp.rang ASC, p.label ASC";
|
||||||
|
|
||||||
|
$resql = $this->db->query($sql);
|
||||||
|
if ($resql) {
|
||||||
|
while ($obj = $this->db->fetch_object($resql)) {
|
||||||
|
$fav = new FavoriteProduct($this->db);
|
||||||
|
$fav->id = $obj->rowid;
|
||||||
|
$fav->fk_soc = $obj->fk_soc;
|
||||||
|
$fav->fk_contact = $obj->fk_contact;
|
||||||
|
$fav->fk_product = $obj->fk_product;
|
||||||
|
$fav->qty = $obj->qty;
|
||||||
|
$fav->rang = $obj->rang;
|
||||||
|
$fav->note = $obj->note;
|
||||||
|
$fav->active = $obj->active;
|
||||||
|
|
||||||
|
// Product info
|
||||||
|
$fav->product_ref = $obj->product_ref;
|
||||||
|
$fav->product_label = $obj->product_label;
|
||||||
|
$fav->product_price = $obj->price;
|
||||||
|
$fav->product_price_ttc = $obj->price_ttc;
|
||||||
|
$fav->product_tva_tx = $obj->tva_tx;
|
||||||
|
|
||||||
|
$results[] = $fav;
|
||||||
|
}
|
||||||
|
$this->db->free($resql);
|
||||||
|
return $results;
|
||||||
|
} else {
|
||||||
|
$this->error = $this->db->lasterror();
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Move a favorite product up in the list (for contact)
|
||||||
|
*
|
||||||
|
* @param int $id Favorite ID to move
|
||||||
|
* @param int $contactid Contact ID
|
||||||
|
* @return int 1 if OK, <0 if KO
|
||||||
|
*/
|
||||||
|
public function moveUpByContact($id, $contactid)
|
||||||
|
{
|
||||||
|
return $this->movePositionByContact($id, $contactid, 'up');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Move a favorite product down in the list (for contact)
|
||||||
|
*
|
||||||
|
* @param int $id Favorite ID to move
|
||||||
|
* @param int $contactid Contact ID
|
||||||
|
* @return int 1 if OK, <0 if KO
|
||||||
|
*/
|
||||||
|
public function moveDownByContact($id, $contactid)
|
||||||
|
{
|
||||||
|
return $this->movePositionByContact($id, $contactid, 'down');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Move a favorite product position (for contact)
|
||||||
|
*
|
||||||
|
* @param int $id Favorite ID to move
|
||||||
|
* @param int $contactid Contact ID
|
||||||
|
* @param string $direction 'up' or 'down'
|
||||||
|
* @return int 1 if OK, <0 if KO
|
||||||
|
*/
|
||||||
|
private function movePositionByContact($id, $contactid, $direction)
|
||||||
|
{
|
||||||
|
global $conf;
|
||||||
|
|
||||||
|
$sql = "SELECT rowid, rang FROM ".MAIN_DB_PREFIX.$this->table_element;
|
||||||
|
$sql .= " WHERE fk_contact = ".((int) $contactid);
|
||||||
|
$sql .= " AND entity = ".((int) $conf->entity);
|
||||||
|
$sql .= " ORDER BY rang ASC, rowid ASC";
|
||||||
|
|
||||||
|
$resql = $this->db->query($sql);
|
||||||
|
if (!$resql) {
|
||||||
|
$this->error = $this->db->lasterror();
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
$items = array();
|
||||||
|
$currentIndex = -1;
|
||||||
|
$i = 0;
|
||||||
|
while ($obj = $this->db->fetch_object($resql)) {
|
||||||
|
$items[$i] = array('id' => $obj->rowid, 'rang' => $i);
|
||||||
|
if ($obj->rowid == $id) {
|
||||||
|
$currentIndex = $i;
|
||||||
|
}
|
||||||
|
$i++;
|
||||||
|
}
|
||||||
|
$this->db->free($resql);
|
||||||
|
|
||||||
|
if ($currentIndex < 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$swapIndex = -1;
|
||||||
|
if ($direction == 'up' && $currentIndex > 0) {
|
||||||
|
$swapIndex = $currentIndex - 1;
|
||||||
|
} elseif ($direction == 'down' && $currentIndex < count($items) - 1) {
|
||||||
|
$swapIndex = $currentIndex + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($swapIndex < 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->db->begin();
|
||||||
|
|
||||||
|
$sql1 = "UPDATE ".MAIN_DB_PREFIX.$this->table_element." SET rang = ".((int) $swapIndex);
|
||||||
|
$sql1 .= " WHERE rowid = ".((int) $items[$currentIndex]['id']);
|
||||||
|
|
||||||
|
$sql2 = "UPDATE ".MAIN_DB_PREFIX.$this->table_element." SET rang = ".((int) $currentIndex);
|
||||||
|
$sql2 .= " WHERE rowid = ".((int) $items[$swapIndex]['id']);
|
||||||
|
|
||||||
|
if ($this->db->query($sql1) && $this->db->query($sql2)) {
|
||||||
|
$this->db->commit();
|
||||||
|
return 1;
|
||||||
|
} else {
|
||||||
|
$this->error = $this->db->lasterror();
|
||||||
|
$this->db->rollback();
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate an order from selected favorite products (for contact)
|
||||||
|
*
|
||||||
|
* @param User $user User creating the order
|
||||||
|
* @param int $socid Customer ID
|
||||||
|
* @param int $contactid Contact ID
|
||||||
|
* @param array $selectedIds Array of favorite product IDs to include
|
||||||
|
* @param array $quantities Optional array of quantities (id => qty)
|
||||||
|
* @return int Order ID if OK, <0 if KO
|
||||||
|
*/
|
||||||
|
public function generateOrderByContact($user, $socid, $contactid, $selectedIds, $quantities = array())
|
||||||
|
{
|
||||||
|
global $conf, $langs, $mysoc;
|
||||||
|
|
||||||
|
require_once DOL_DOCUMENT_ROOT.'/commande/class/commande.class.php';
|
||||||
|
require_once DOL_DOCUMENT_ROOT.'/societe/class/societe.class.php';
|
||||||
|
require_once DOL_DOCUMENT_ROOT.'/contact/class/contact.class.php';
|
||||||
|
require_once DOL_DOCUMENT_ROOT.'/product/class/product.class.php';
|
||||||
|
|
||||||
|
if (empty($selectedIds)) {
|
||||||
|
$this->error = 'NoProductsSelected';
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load thirdparty
|
||||||
|
$societe = new Societe($this->db);
|
||||||
|
if ($societe->fetch($socid) <= 0) {
|
||||||
|
$this->error = 'ErrorLoadingThirdparty';
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load contact
|
||||||
|
$contact = new Contact($this->db);
|
||||||
|
if ($contact->fetch($contactid) <= 0) {
|
||||||
|
$this->error = 'ErrorLoadingContact';
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch selected favorites
|
||||||
|
$favorites = $this->fetchAllByContact($contactid);
|
||||||
|
if (!is_array($favorites)) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter to selected only
|
||||||
|
$toAdd = array();
|
||||||
|
foreach ($favorites as $fav) {
|
||||||
|
if (in_array($fav->id, $selectedIds)) {
|
||||||
|
$qty = isset($quantities[$fav->id]) ? (float) $quantities[$fav->id] : $fav->qty;
|
||||||
|
if ($qty > 0) {
|
||||||
|
$toAdd[] = array(
|
||||||
|
'product_id' => $fav->fk_product,
|
||||||
|
'qty' => $qty
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($toAdd)) {
|
||||||
|
$this->error = 'NoValidProductsToAdd';
|
||||||
|
return -2;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create order
|
||||||
|
$order = new Commande($this->db);
|
||||||
|
$order->socid = $socid;
|
||||||
|
$order->thirdparty = $societe;
|
||||||
|
$order->date = dol_now();
|
||||||
|
// Ihr Zeichen: Kunde - Kontakt/Adresse - Favoriten
|
||||||
|
$order->ref_client = $societe->name.' - '.$contact->getFullName($langs).' - '.$langs->trans('FavoriteProducts');
|
||||||
|
$order->note_private = $langs->trans('OrderGeneratedFromFavorites');
|
||||||
|
|
||||||
|
$this->db->begin();
|
||||||
|
|
||||||
|
$result = $order->create($user);
|
||||||
|
|
||||||
|
if ($result <= 0) {
|
||||||
|
$this->error = $order->error;
|
||||||
|
$this->errors = $order->errors;
|
||||||
|
$this->db->rollback();
|
||||||
|
return -3;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add products
|
||||||
|
foreach ($toAdd as $item) {
|
||||||
|
$product = new Product($this->db);
|
||||||
|
$product->fetch($item['product_id']);
|
||||||
|
|
||||||
|
$tva_tx = get_default_tva($mysoc, $societe, $product->id);
|
||||||
|
$localtax1_tx = get_default_localtax($mysoc, $societe, 1, $product->id);
|
||||||
|
$localtax2_tx = get_default_localtax($mysoc, $societe, 2, $product->id);
|
||||||
|
|
||||||
|
$lineResult = $order->addline(
|
||||||
|
$product->label,
|
||||||
|
$product->price,
|
||||||
|
$item['qty'],
|
||||||
|
$tva_tx,
|
||||||
|
$localtax1_tx,
|
||||||
|
$localtax2_tx,
|
||||||
|
$product->id,
|
||||||
|
0, 0, 0, 'HT', 0, '', '', 0, -1, 0, 0, 0, 0,
|
||||||
|
$product->label,
|
||||||
|
array(),
|
||||||
|
$product->fk_unit
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($lineResult < 0) {
|
||||||
|
$this->error = $order->error;
|
||||||
|
$this->errors = $order->errors;
|
||||||
|
$this->db->rollback();
|
||||||
|
return -4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->db->commit();
|
||||||
|
return $order->id;
|
||||||
|
}
|
||||||
|
}
|
||||||
383
class/mediumtype.class.php
Executable file
383
class/mediumtype.class.php
Executable file
|
|
@ -0,0 +1,383 @@
|
||||||
|
<?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 MediumType
|
||||||
|
* Manages cable/wire types (Kabeltypen) for connections
|
||||||
|
*/
|
||||||
|
class MediumType extends CommonObject
|
||||||
|
{
|
||||||
|
public $element = 'mediumtype';
|
||||||
|
public $table_element = 'kundenkarte_medium_type';
|
||||||
|
|
||||||
|
public $ref;
|
||||||
|
public $label;
|
||||||
|
public $label_short;
|
||||||
|
public $description;
|
||||||
|
public $fk_system;
|
||||||
|
public $category;
|
||||||
|
public $default_spec;
|
||||||
|
public $available_specs; // JSON array
|
||||||
|
public $color;
|
||||||
|
public $picto;
|
||||||
|
public $fk_product;
|
||||||
|
public $is_system;
|
||||||
|
public $position;
|
||||||
|
public $active;
|
||||||
|
|
||||||
|
public $date_creation;
|
||||||
|
public $fk_user_creat;
|
||||||
|
public $fk_user_modif;
|
||||||
|
|
||||||
|
// Loaded properties
|
||||||
|
public $system_label;
|
||||||
|
public $system_code;
|
||||||
|
|
||||||
|
// Category constants
|
||||||
|
const CAT_STROMKABEL = 'stromkabel';
|
||||||
|
const CAT_NETZWERKKABEL = 'netzwerkkabel';
|
||||||
|
const CAT_LWL = 'lwl';
|
||||||
|
const CAT_KOAX = 'koax';
|
||||||
|
const CAT_SONSTIGES = 'sonstiges';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor
|
||||||
|
*
|
||||||
|
* @param DoliDB $db Database handler
|
||||||
|
*/
|
||||||
|
public function __construct($db)
|
||||||
|
{
|
||||||
|
$this->db = $db;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create object in database
|
||||||
|
*
|
||||||
|
* @param User $user User that creates
|
||||||
|
* @return int Return integer <0 if KO, Id of created object if OK
|
||||||
|
*/
|
||||||
|
public function create($user)
|
||||||
|
{
|
||||||
|
global $conf;
|
||||||
|
|
||||||
|
$error = 0;
|
||||||
|
$now = dol_now();
|
||||||
|
|
||||||
|
if (empty($this->ref) || empty($this->label)) {
|
||||||
|
$this->error = 'ErrorMissingParameters';
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->db->begin();
|
||||||
|
|
||||||
|
$sql = "INSERT INTO ".MAIN_DB_PREFIX.$this->table_element." (";
|
||||||
|
$sql .= "entity, ref, label, label_short, description, fk_system, category,";
|
||||||
|
$sql .= " default_spec, available_specs, color, picto, fk_product,";
|
||||||
|
$sql .= " is_system, position, active, date_creation, fk_user_creat";
|
||||||
|
$sql .= ") VALUES (";
|
||||||
|
$sql .= "0"; // entity 0 = global
|
||||||
|
$sql .= ", '".$this->db->escape($this->ref)."'";
|
||||||
|
$sql .= ", '".$this->db->escape($this->label)."'";
|
||||||
|
$sql .= ", ".($this->label_short ? "'".$this->db->escape($this->label_short)."'" : "NULL");
|
||||||
|
$sql .= ", ".($this->description ? "'".$this->db->escape($this->description)."'" : "NULL");
|
||||||
|
$sql .= ", ".((int) $this->fk_system);
|
||||||
|
$sql .= ", ".($this->category ? "'".$this->db->escape($this->category)."'" : "NULL");
|
||||||
|
$sql .= ", ".($this->default_spec ? "'".$this->db->escape($this->default_spec)."'" : "NULL");
|
||||||
|
$sql .= ", ".($this->available_specs ? "'".$this->db->escape($this->available_specs)."'" : "NULL");
|
||||||
|
$sql .= ", ".($this->color ? "'".$this->db->escape($this->color)."'" : "NULL");
|
||||||
|
$sql .= ", ".($this->picto ? "'".$this->db->escape($this->picto)."'" : "NULL");
|
||||||
|
$sql .= ", ".($this->fk_product > 0 ? ((int) $this->fk_product) : "NULL");
|
||||||
|
$sql .= ", 0"; // is_system = 0 for user-created
|
||||||
|
$sql .= ", ".((int) $this->position);
|
||||||
|
$sql .= ", ".((int) ($this->active !== null ? $this->active : 1));
|
||||||
|
$sql .= ", '".$this->db->idate($now)."'";
|
||||||
|
$sql .= ", ".((int) $user->id);
|
||||||
|
$sql .= ")";
|
||||||
|
|
||||||
|
$resql = $this->db->query($sql);
|
||||||
|
if (!$resql) {
|
||||||
|
$error++;
|
||||||
|
$this->errors[] = "Error ".$this->db->lasterror();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$error) {
|
||||||
|
$this->id = $this->db->last_insert_id(MAIN_DB_PREFIX.$this->table_element);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($error) {
|
||||||
|
$this->db->rollback();
|
||||||
|
return -1 * $error;
|
||||||
|
} else {
|
||||||
|
$this->db->commit();
|
||||||
|
return $this->id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load object from database
|
||||||
|
*
|
||||||
|
* @param int $id ID of record
|
||||||
|
* @return int Return integer <0 if KO, 0 if not found, >0 if OK
|
||||||
|
*/
|
||||||
|
public function fetch($id)
|
||||||
|
{
|
||||||
|
$sql = "SELECT t.*, s.label as system_label, s.code as system_code";
|
||||||
|
$sql .= " FROM ".MAIN_DB_PREFIX.$this->table_element." as t";
|
||||||
|
$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."c_kundenkarte_anlage_system as s ON t.fk_system = s.rowid";
|
||||||
|
$sql .= " WHERE t.rowid = ".((int) $id);
|
||||||
|
|
||||||
|
$resql = $this->db->query($sql);
|
||||||
|
if ($resql) {
|
||||||
|
if ($this->db->num_rows($resql)) {
|
||||||
|
$obj = $this->db->fetch_object($resql);
|
||||||
|
|
||||||
|
$this->id = $obj->rowid;
|
||||||
|
$this->entity = $obj->entity;
|
||||||
|
$this->ref = $obj->ref;
|
||||||
|
$this->label = $obj->label;
|
||||||
|
$this->label_short = $obj->label_short;
|
||||||
|
$this->description = $obj->description;
|
||||||
|
$this->fk_system = $obj->fk_system;
|
||||||
|
$this->category = $obj->category;
|
||||||
|
$this->default_spec = $obj->default_spec;
|
||||||
|
$this->available_specs = $obj->available_specs;
|
||||||
|
$this->color = $obj->color;
|
||||||
|
$this->picto = $obj->picto;
|
||||||
|
$this->fk_product = $obj->fk_product;
|
||||||
|
$this->is_system = $obj->is_system;
|
||||||
|
$this->position = $obj->position;
|
||||||
|
$this->active = $obj->active;
|
||||||
|
$this->date_creation = $this->db->jdate($obj->date_creation);
|
||||||
|
$this->fk_user_creat = $obj->fk_user_creat;
|
||||||
|
$this->fk_user_modif = $obj->fk_user_modif;
|
||||||
|
|
||||||
|
$this->system_label = $obj->system_label;
|
||||||
|
$this->system_code = $obj->system_code;
|
||||||
|
|
||||||
|
$this->db->free($resql);
|
||||||
|
return 1;
|
||||||
|
} else {
|
||||||
|
$this->db->free($resql);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$this->error = $this->db->lasterror();
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update object in database
|
||||||
|
*
|
||||||
|
* @param User $user User that modifies
|
||||||
|
* @return int Return integer <0 if KO, >0 if OK
|
||||||
|
*/
|
||||||
|
public function update($user)
|
||||||
|
{
|
||||||
|
$error = 0;
|
||||||
|
|
||||||
|
$this->db->begin();
|
||||||
|
|
||||||
|
$sql = "UPDATE ".MAIN_DB_PREFIX.$this->table_element." SET";
|
||||||
|
$sql .= " ref = '".$this->db->escape($this->ref)."'";
|
||||||
|
$sql .= ", label = '".$this->db->escape($this->label)."'";
|
||||||
|
$sql .= ", label_short = ".($this->label_short ? "'".$this->db->escape($this->label_short)."'" : "NULL");
|
||||||
|
$sql .= ", description = ".($this->description ? "'".$this->db->escape($this->description)."'" : "NULL");
|
||||||
|
$sql .= ", fk_system = ".((int) $this->fk_system);
|
||||||
|
$sql .= ", category = ".($this->category ? "'".$this->db->escape($this->category)."'" : "NULL");
|
||||||
|
$sql .= ", default_spec = ".($this->default_spec ? "'".$this->db->escape($this->default_spec)."'" : "NULL");
|
||||||
|
$sql .= ", available_specs = ".($this->available_specs ? "'".$this->db->escape($this->available_specs)."'" : "NULL");
|
||||||
|
$sql .= ", color = ".($this->color ? "'".$this->db->escape($this->color)."'" : "NULL");
|
||||||
|
$sql .= ", picto = ".($this->picto ? "'".$this->db->escape($this->picto)."'" : "NULL");
|
||||||
|
$sql .= ", fk_product = ".($this->fk_product > 0 ? ((int) $this->fk_product) : "NULL");
|
||||||
|
$sql .= ", position = ".((int) $this->position);
|
||||||
|
$sql .= ", active = ".((int) $this->active);
|
||||||
|
$sql .= ", fk_user_modif = ".((int) $user->id);
|
||||||
|
$sql .= " WHERE rowid = ".((int) $this->id);
|
||||||
|
|
||||||
|
$resql = $this->db->query($sql);
|
||||||
|
if (!$resql) {
|
||||||
|
$error++;
|
||||||
|
$this->errors[] = "Error ".$this->db->lasterror();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($error) {
|
||||||
|
$this->db->rollback();
|
||||||
|
return -1 * $error;
|
||||||
|
} else {
|
||||||
|
$this->db->commit();
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete object in database
|
||||||
|
*
|
||||||
|
* @param User $user User that deletes
|
||||||
|
* @return int Return integer <0 if KO, >0 if OK
|
||||||
|
*/
|
||||||
|
public function delete($user)
|
||||||
|
{
|
||||||
|
// Cannot delete system types
|
||||||
|
if ($this->is_system) {
|
||||||
|
$this->error = 'ErrorCannotDeleteSystemType';
|
||||||
|
return -2;
|
||||||
|
}
|
||||||
|
|
||||||
|
$error = 0;
|
||||||
|
$this->db->begin();
|
||||||
|
|
||||||
|
$sql = "DELETE FROM ".MAIN_DB_PREFIX.$this->table_element." WHERE rowid = ".((int) $this->id);
|
||||||
|
$resql = $this->db->query($sql);
|
||||||
|
if (!$resql) {
|
||||||
|
$error++;
|
||||||
|
$this->errors[] = "Error ".$this->db->lasterror();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($error) {
|
||||||
|
$this->db->rollback();
|
||||||
|
return -1 * $error;
|
||||||
|
} else {
|
||||||
|
$this->db->commit();
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch all medium types for a system
|
||||||
|
*
|
||||||
|
* @param int $systemId System ID (0 = all)
|
||||||
|
* @param int $activeOnly Only active types
|
||||||
|
* @return array Array of MediumType objects
|
||||||
|
*/
|
||||||
|
public function fetchAllBySystem($systemId = 0, $activeOnly = 1)
|
||||||
|
{
|
||||||
|
$results = array();
|
||||||
|
|
||||||
|
$sql = "SELECT t.*, s.label as system_label, s.code as system_code";
|
||||||
|
$sql .= " FROM ".MAIN_DB_PREFIX.$this->table_element." as t";
|
||||||
|
$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."c_kundenkarte_anlage_system as s ON t.fk_system = s.rowid";
|
||||||
|
$sql .= " WHERE 1 = 1";
|
||||||
|
if ($systemId > 0) {
|
||||||
|
// Show types for this system AND global types (fk_system = 0)
|
||||||
|
$sql .= " AND (t.fk_system = ".((int) $systemId)." OR t.fk_system = 0)";
|
||||||
|
}
|
||||||
|
if ($activeOnly) {
|
||||||
|
$sql .= " AND t.active = 1";
|
||||||
|
}
|
||||||
|
$sql .= " ORDER BY t.category ASC, t.position ASC, t.label ASC";
|
||||||
|
|
||||||
|
$resql = $this->db->query($sql);
|
||||||
|
if ($resql) {
|
||||||
|
while ($obj = $this->db->fetch_object($resql)) {
|
||||||
|
$type = new MediumType($this->db);
|
||||||
|
$type->id = $obj->rowid;
|
||||||
|
$type->ref = $obj->ref;
|
||||||
|
$type->label = $obj->label;
|
||||||
|
$type->label_short = $obj->label_short;
|
||||||
|
$type->description = $obj->description;
|
||||||
|
$type->fk_system = $obj->fk_system;
|
||||||
|
$type->category = $obj->category;
|
||||||
|
$type->default_spec = $obj->default_spec;
|
||||||
|
$type->available_specs = $obj->available_specs;
|
||||||
|
$type->color = $obj->color;
|
||||||
|
$type->picto = $obj->picto;
|
||||||
|
$type->fk_product = $obj->fk_product;
|
||||||
|
$type->is_system = $obj->is_system;
|
||||||
|
$type->position = $obj->position;
|
||||||
|
$type->active = $obj->active;
|
||||||
|
$type->system_label = $obj->system_label;
|
||||||
|
$type->system_code = $obj->system_code;
|
||||||
|
|
||||||
|
$results[] = $type;
|
||||||
|
}
|
||||||
|
$this->db->free($resql);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $results;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch all types grouped by category
|
||||||
|
*
|
||||||
|
* @param int $systemId System ID (0 = all)
|
||||||
|
* @return array Associative array: category => array of MediumType objects
|
||||||
|
*/
|
||||||
|
public function fetchGroupedByCategory($systemId = 0)
|
||||||
|
{
|
||||||
|
$all = $this->fetchAllBySystem($systemId, 1);
|
||||||
|
$grouped = array();
|
||||||
|
|
||||||
|
foreach ($all as $type) {
|
||||||
|
$cat = $type->category ?: 'sonstiges';
|
||||||
|
if (!isset($grouped[$cat])) {
|
||||||
|
$grouped[$cat] = array();
|
||||||
|
}
|
||||||
|
$grouped[$cat][] = $type;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $grouped;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get available specs as array
|
||||||
|
*
|
||||||
|
* @return array Array of specification strings
|
||||||
|
*/
|
||||||
|
public function getAvailableSpecsArray()
|
||||||
|
{
|
||||||
|
if (empty($this->available_specs)) {
|
||||||
|
return array();
|
||||||
|
}
|
||||||
|
$specs = json_decode($this->available_specs, true);
|
||||||
|
return is_array($specs) ? $specs : array();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get category label
|
||||||
|
*
|
||||||
|
* @return string Translated category label
|
||||||
|
*/
|
||||||
|
public function getCategoryLabel()
|
||||||
|
{
|
||||||
|
global $langs;
|
||||||
|
|
||||||
|
switch ($this->category) {
|
||||||
|
case self::CAT_STROMKABEL:
|
||||||
|
return $langs->trans('MediumCatStromkabel');
|
||||||
|
case self::CAT_NETZWERKKABEL:
|
||||||
|
return $langs->trans('MediumCatNetzwerkkabel');
|
||||||
|
case self::CAT_LWL:
|
||||||
|
return $langs->trans('MediumCatLWL');
|
||||||
|
case self::CAT_KOAX:
|
||||||
|
return $langs->trans('MediumCatKoax');
|
||||||
|
case self::CAT_SONSTIGES:
|
||||||
|
default:
|
||||||
|
return $langs->trans('MediumCatSonstiges');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all category options
|
||||||
|
*
|
||||||
|
* @return array category_code => translated_label
|
||||||
|
*/
|
||||||
|
public static function getCategoryOptions()
|
||||||
|
{
|
||||||
|
global $langs;
|
||||||
|
|
||||||
|
return array(
|
||||||
|
self::CAT_STROMKABEL => $langs->trans('MediumCatStromkabel'),
|
||||||
|
self::CAT_NETZWERKKABEL => $langs->trans('MediumCatNetzwerkkabel'),
|
||||||
|
self::CAT_LWL => $langs->trans('MediumCatLWL'),
|
||||||
|
self::CAT_KOAX => $langs->trans('MediumCatKoax'),
|
||||||
|
self::CAT_SONSTIGES => $langs->trans('MediumCatSonstiges')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
257
class/terminalbridge.class.php
Executable file
257
class/terminalbridge.class.php
Executable file
|
|
@ -0,0 +1,257 @@
|
||||||
|
<?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 TerminalBridge
|
||||||
|
* Manages bridges between terminal blocks (Brücken zwischen Reihenklemmen)
|
||||||
|
*/
|
||||||
|
class TerminalBridge extends CommonObject
|
||||||
|
{
|
||||||
|
public $element = 'terminalbridge';
|
||||||
|
public $table_element = 'kundenkarte_terminal_bridge';
|
||||||
|
|
||||||
|
public $fk_anlage;
|
||||||
|
public $fk_carrier;
|
||||||
|
public $start_te;
|
||||||
|
public $end_te;
|
||||||
|
public $terminal_side = 'top';
|
||||||
|
public $terminal_row = 0;
|
||||||
|
public $color = '#e74c3c';
|
||||||
|
public $bridge_type = 'standard';
|
||||||
|
public $label;
|
||||||
|
public $status = 1;
|
||||||
|
public $date_creation;
|
||||||
|
public $fk_user_creat;
|
||||||
|
public $fk_user_modif;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor
|
||||||
|
*
|
||||||
|
* @param DoliDB $db Database handler
|
||||||
|
*/
|
||||||
|
public function __construct($db)
|
||||||
|
{
|
||||||
|
$this->db = $db;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create object in database
|
||||||
|
*
|
||||||
|
* @param User $user User that creates
|
||||||
|
* @return int Return integer <0 if KO, Id of created object if OK
|
||||||
|
*/
|
||||||
|
public function create($user)
|
||||||
|
{
|
||||||
|
global $conf;
|
||||||
|
|
||||||
|
$error = 0;
|
||||||
|
$now = dol_now();
|
||||||
|
|
||||||
|
if (empty($this->fk_anlage) || empty($this->fk_carrier) || empty($this->start_te) || empty($this->end_te)) {
|
||||||
|
$this->error = 'ErrorMissingParameters';
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->db->begin();
|
||||||
|
|
||||||
|
$sql = "INSERT INTO ".MAIN_DB_PREFIX.$this->table_element." (";
|
||||||
|
$sql .= "entity, fk_anlage, fk_carrier, start_te, end_te,";
|
||||||
|
$sql .= " terminal_side, terminal_row, color, bridge_type, label,";
|
||||||
|
$sql .= " status, date_creation, fk_user_creat";
|
||||||
|
$sql .= ") VALUES (";
|
||||||
|
$sql .= ((int) ($conf->entity));
|
||||||
|
$sql .= ", ".((int) $this->fk_anlage);
|
||||||
|
$sql .= ", ".((int) $this->fk_carrier);
|
||||||
|
$sql .= ", ".((int) $this->start_te);
|
||||||
|
$sql .= ", ".((int) $this->end_te);
|
||||||
|
$sql .= ", '".$this->db->escape($this->terminal_side ?: 'top')."'";
|
||||||
|
$sql .= ", ".((int) $this->terminal_row);
|
||||||
|
$sql .= ", '".$this->db->escape($this->color ?: '#e74c3c')."'";
|
||||||
|
$sql .= ", '".$this->db->escape($this->bridge_type ?: 'standard')."'";
|
||||||
|
$sql .= ", ".($this->label ? "'".$this->db->escape($this->label)."'" : "NULL");
|
||||||
|
$sql .= ", ".((int) ($this->status !== null ? $this->status : 1));
|
||||||
|
$sql .= ", '".$this->db->idate($now)."'";
|
||||||
|
$sql .= ", ".((int) $user->id);
|
||||||
|
$sql .= ")";
|
||||||
|
|
||||||
|
$resql = $this->db->query($sql);
|
||||||
|
if (!$resql) {
|
||||||
|
$error++;
|
||||||
|
$this->errors[] = "Error ".$this->db->lasterror();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$error) {
|
||||||
|
$this->id = $this->db->last_insert_id(MAIN_DB_PREFIX.$this->table_element);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($error) {
|
||||||
|
$this->db->rollback();
|
||||||
|
return -1 * $error;
|
||||||
|
} else {
|
||||||
|
$this->db->commit();
|
||||||
|
return $this->id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load object from database
|
||||||
|
*
|
||||||
|
* @param int $id ID of record
|
||||||
|
* @return int Return integer <0 if KO, 0 if not found, >0 if OK
|
||||||
|
*/
|
||||||
|
public function fetch($id)
|
||||||
|
{
|
||||||
|
$sql = "SELECT * FROM ".MAIN_DB_PREFIX.$this->table_element;
|
||||||
|
$sql .= " WHERE rowid = ".((int) $id);
|
||||||
|
|
||||||
|
$resql = $this->db->query($sql);
|
||||||
|
if ($resql) {
|
||||||
|
if ($this->db->num_rows($resql)) {
|
||||||
|
$obj = $this->db->fetch_object($resql);
|
||||||
|
|
||||||
|
$this->id = $obj->rowid;
|
||||||
|
$this->entity = $obj->entity;
|
||||||
|
$this->fk_anlage = $obj->fk_anlage;
|
||||||
|
$this->fk_carrier = $obj->fk_carrier;
|
||||||
|
$this->start_te = $obj->start_te;
|
||||||
|
$this->end_te = $obj->end_te;
|
||||||
|
$this->terminal_side = $obj->terminal_side;
|
||||||
|
$this->terminal_row = $obj->terminal_row;
|
||||||
|
$this->color = $obj->color;
|
||||||
|
$this->bridge_type = $obj->bridge_type;
|
||||||
|
$this->label = $obj->label;
|
||||||
|
$this->status = $obj->status;
|
||||||
|
$this->date_creation = $this->db->jdate($obj->date_creation);
|
||||||
|
$this->fk_user_creat = $obj->fk_user_creat;
|
||||||
|
$this->fk_user_modif = $obj->fk_user_modif;
|
||||||
|
|
||||||
|
$this->db->free($resql);
|
||||||
|
return 1;
|
||||||
|
} else {
|
||||||
|
$this->db->free($resql);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$this->error = $this->db->lasterror();
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update object in database
|
||||||
|
*
|
||||||
|
* @param User $user User that modifies
|
||||||
|
* @return int Return integer <0 if KO, >0 if OK
|
||||||
|
*/
|
||||||
|
public function update($user)
|
||||||
|
{
|
||||||
|
$error = 0;
|
||||||
|
|
||||||
|
$this->db->begin();
|
||||||
|
|
||||||
|
$sql = "UPDATE ".MAIN_DB_PREFIX.$this->table_element." SET";
|
||||||
|
$sql .= " fk_anlage = ".((int) $this->fk_anlage);
|
||||||
|
$sql .= ", fk_carrier = ".((int) $this->fk_carrier);
|
||||||
|
$sql .= ", start_te = ".((int) $this->start_te);
|
||||||
|
$sql .= ", end_te = ".((int) $this->end_te);
|
||||||
|
$sql .= ", terminal_side = '".$this->db->escape($this->terminal_side ?: 'top')."'";
|
||||||
|
$sql .= ", terminal_row = ".((int) $this->terminal_row);
|
||||||
|
$sql .= ", color = '".$this->db->escape($this->color ?: '#e74c3c')."'";
|
||||||
|
$sql .= ", bridge_type = '".$this->db->escape($this->bridge_type ?: 'standard')."'";
|
||||||
|
$sql .= ", label = ".($this->label ? "'".$this->db->escape($this->label)."'" : "NULL");
|
||||||
|
$sql .= ", status = ".((int) $this->status);
|
||||||
|
$sql .= ", fk_user_modif = ".((int) $user->id);
|
||||||
|
$sql .= " WHERE rowid = ".((int) $this->id);
|
||||||
|
|
||||||
|
$resql = $this->db->query($sql);
|
||||||
|
if (!$resql) {
|
||||||
|
$error++;
|
||||||
|
$this->errors[] = "Error ".$this->db->lasterror();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($error) {
|
||||||
|
$this->db->rollback();
|
||||||
|
return -1 * $error;
|
||||||
|
} else {
|
||||||
|
$this->db->commit();
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete object in database
|
||||||
|
*
|
||||||
|
* @param User $user User that deletes
|
||||||
|
* @return int Return integer <0 if KO, >0 if OK
|
||||||
|
*/
|
||||||
|
public function delete($user)
|
||||||
|
{
|
||||||
|
$error = 0;
|
||||||
|
$this->db->begin();
|
||||||
|
|
||||||
|
$sql = "DELETE FROM ".MAIN_DB_PREFIX.$this->table_element." WHERE rowid = ".((int) $this->id);
|
||||||
|
$resql = $this->db->query($sql);
|
||||||
|
if (!$resql) {
|
||||||
|
$error++;
|
||||||
|
$this->errors[] = "Error ".$this->db->lasterror();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($error) {
|
||||||
|
$this->db->rollback();
|
||||||
|
return -1 * $error;
|
||||||
|
} else {
|
||||||
|
$this->db->commit();
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch all bridges for an installation
|
||||||
|
*
|
||||||
|
* @param int $anlageId Installation ID
|
||||||
|
* @param int $carrierId Optional carrier ID filter
|
||||||
|
* @return array Array of TerminalBridge objects
|
||||||
|
*/
|
||||||
|
public function fetchAllByAnlage($anlageId, $carrierId = 0)
|
||||||
|
{
|
||||||
|
$results = array();
|
||||||
|
|
||||||
|
$sql = "SELECT * FROM ".MAIN_DB_PREFIX.$this->table_element;
|
||||||
|
$sql .= " WHERE fk_anlage = ".((int) $anlageId);
|
||||||
|
$sql .= " AND status = 1";
|
||||||
|
if ($carrierId > 0) {
|
||||||
|
$sql .= " AND fk_carrier = ".((int) $carrierId);
|
||||||
|
}
|
||||||
|
$sql .= " ORDER BY fk_carrier ASC, terminal_side ASC, start_te ASC";
|
||||||
|
|
||||||
|
$resql = $this->db->query($sql);
|
||||||
|
if ($resql) {
|
||||||
|
while ($obj = $this->db->fetch_object($resql)) {
|
||||||
|
$bridge = new TerminalBridge($this->db);
|
||||||
|
$bridge->id = $obj->rowid;
|
||||||
|
$bridge->entity = $obj->entity;
|
||||||
|
$bridge->fk_anlage = $obj->fk_anlage;
|
||||||
|
$bridge->fk_carrier = $obj->fk_carrier;
|
||||||
|
$bridge->start_te = $obj->start_te;
|
||||||
|
$bridge->end_te = $obj->end_te;
|
||||||
|
$bridge->terminal_side = $obj->terminal_side;
|
||||||
|
$bridge->terminal_row = $obj->terminal_row;
|
||||||
|
$bridge->color = $obj->color;
|
||||||
|
$bridge->bridge_type = $obj->bridge_type;
|
||||||
|
$bridge->label = $obj->label;
|
||||||
|
$bridge->status = $obj->status;
|
||||||
|
|
||||||
|
$results[] = $bridge;
|
||||||
|
}
|
||||||
|
$this->db->free($resql);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $results;
|
||||||
|
}
|
||||||
|
}
|
||||||
776
core/modules/modKundenKarte.class.php
Executable file
776
core/modules/modKundenKarte.class.php
Executable file
|
|
@ -0,0 +1,776 @@
|
||||||
|
<?php
|
||||||
|
/* Copyright (C) 2004-2018 Laurent Destailleur <eldy@users.sourceforge.net>
|
||||||
|
* Copyright (C) 2018-2019 Nicolas ZABOURI <info@inovea-conseil.com>
|
||||||
|
* Copyright (C) 2019-2024 Frédéric France <frederic.france@free.fr>
|
||||||
|
* Copyright (C) 2026 Eduard Wisch <data@data-it-solution.de>
|
||||||
|
*
|
||||||
|
* This program is free software; you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation; either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* \defgroup kundenkarte Module KundenKarte
|
||||||
|
* \brief KundenKarte module descriptor.
|
||||||
|
*
|
||||||
|
* \file htdocs/kundenkarte/core/modules/modKundenKarte.class.php
|
||||||
|
* \ingroup kundenkarte
|
||||||
|
* \brief Description and activation file for module KundenKarte
|
||||||
|
*/
|
||||||
|
include_once DOL_DOCUMENT_ROOT.'/core/modules/DolibarrModules.class.php';
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Description and activation class for module KundenKarte
|
||||||
|
*/
|
||||||
|
class modKundenKarte extends DolibarrModules
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Constructor. Define names, constants, directories, boxes, permissions
|
||||||
|
*
|
||||||
|
* @param DoliDB $db Database handler
|
||||||
|
*/
|
||||||
|
public function __construct($db)
|
||||||
|
{
|
||||||
|
global $conf, $langs;
|
||||||
|
|
||||||
|
$this->db = $db;
|
||||||
|
|
||||||
|
// Id for module (must be unique).
|
||||||
|
// Use here a free id (See in Home -> System information -> Dolibarr for list of used modules id).
|
||||||
|
$this->numero = 500015; // TODO Go on page https://wiki.dolibarr.org/index.php/List_of_modules_id to reserve an id number for your module
|
||||||
|
|
||||||
|
// Key text used to identify module (for permissions, menus, etc...)
|
||||||
|
$this->rights_class = 'kundenkarte';
|
||||||
|
|
||||||
|
// Family can be 'base' (core modules),'crm','financial','hr','projects','products','ecm','technic' (transverse modules),'interface' (link with external tools),'other','...'
|
||||||
|
// It is used to group modules by family in module setup page
|
||||||
|
$this->family = "other";
|
||||||
|
|
||||||
|
// Module position in the family on 2 digits ('01', '10', '20', ...)
|
||||||
|
$this->module_position = '90';
|
||||||
|
|
||||||
|
// Gives the possibility for the module, to provide his own family info and position of this family (Overwrite $this->family and $this->module_position. Avoid this)
|
||||||
|
//$this->familyinfo = array('myownfamily' => array('position' => '01', 'label' => $langs->trans("MyOwnFamily")));
|
||||||
|
// Module label (no space allowed), used if translation string 'ModuleKundenKarteName' not found (KundenKarte is name of module).
|
||||||
|
$this->name = preg_replace('/^mod/i', '', get_class($this));
|
||||||
|
|
||||||
|
// DESCRIPTION_FLAG
|
||||||
|
// Module description, used if translation string 'ModuleKundenKarteDesc' not found (KundenKarte is name of module).
|
||||||
|
$this->description = "KundenKarteDescription";
|
||||||
|
// Used only if file README.md and README-LL.md not found.
|
||||||
|
$this->descriptionlong = "KundenKarteDescription";
|
||||||
|
|
||||||
|
// Author
|
||||||
|
$this->editor_name = 'Alles Watt läuft (Testsystem)';
|
||||||
|
$this->editor_url = ''; // Must be an external online web site
|
||||||
|
$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 = '5.2.0';
|
||||||
|
// Url to the file with your last numberversion of this module
|
||||||
|
//$this->url_last_version = 'http://www.example.com/versionmodule.txt';
|
||||||
|
|
||||||
|
// Key used in llx_const table to save module status enabled/disabled (where KUNDENKARTE is value of property name of module in uppercase)
|
||||||
|
$this->const_name = 'MAIN_MODULE_'.strtoupper($this->name);
|
||||||
|
|
||||||
|
// Name of image file used for this module.
|
||||||
|
// If file is in theme/yourtheme/img directory under name object_pictovalue.png, use this->picto='pictovalue'
|
||||||
|
// If file is in module/img directory under name object_pictovalue.png, use this->picto='pictovalue@module'
|
||||||
|
// To use a supported fa-xxx css style of font awesome, use this->picto='xxx'
|
||||||
|
$this->picto = 'fa-id-card';
|
||||||
|
|
||||||
|
// Define some features supported by module (triggers, login, substitutions, menus, css, etc...)
|
||||||
|
$this->module_parts = array(
|
||||||
|
// Set this to 1 if module has its own trigger directory (core/triggers)
|
||||||
|
'triggers' => 0,
|
||||||
|
// Set this to 1 if module has its own login method file (core/login)
|
||||||
|
'login' => 0,
|
||||||
|
// Set this to 1 if module has its own substitution function file (core/substitutions)
|
||||||
|
'substitutions' => 0,
|
||||||
|
// Set this to 1 if module has its own menus handler directory (core/menus)
|
||||||
|
'menus' => 0,
|
||||||
|
// Set this to 1 if module overwrite template dir (core/tpl)
|
||||||
|
'tpl' => 0,
|
||||||
|
// Set this to 1 if module has its own barcode directory (core/modules/barcode)
|
||||||
|
'barcode' => 0,
|
||||||
|
// Set this to 1 if module has its own models directory (core/modules/xxx)
|
||||||
|
'models' => 0,
|
||||||
|
// Set this to 1 if module has its own printing directory (core/modules/printing)
|
||||||
|
'printing' => 0,
|
||||||
|
// Set this to 1 if module has its own theme directory (theme)
|
||||||
|
'theme' => 0,
|
||||||
|
// Set this to relative path of css file if module has its own css file
|
||||||
|
'css' => array(
|
||||||
|
'/kundenkarte/css/kundenkarte.css',
|
||||||
|
),
|
||||||
|
// Set this to relative path of js file if module must load a js on all pages
|
||||||
|
// DISABLED: Loading 438KB JS globally causes conflicts with Dolibarr file upload
|
||||||
|
// The JS is now loaded only on kundenkarte pages via explicit include
|
||||||
|
'js' => array(
|
||||||
|
// '/kundenkarte/js/kundenkarte.js',
|
||||||
|
),
|
||||||
|
// Set here all hooks context managed by module. To find available hook context, make a "grep -r '>initHooks(' *" on source code. You can also set hook context to 'all'
|
||||||
|
/* BEGIN MODULEBUILDER HOOKSCONTEXTS */
|
||||||
|
'hooks' => array(
|
||||||
|
// 'data' => array(
|
||||||
|
// 'hookcontext1',
|
||||||
|
// 'hookcontext2',
|
||||||
|
// ),
|
||||||
|
// 'entity' => '0',
|
||||||
|
),
|
||||||
|
/* END MODULEBUILDER HOOKSCONTEXTS */
|
||||||
|
// Set this to 1 if features of module are opened to external users
|
||||||
|
'moduleforexternal' => 0,
|
||||||
|
// Set this to 1 if the module provides a website template into doctemplates/websites/website_template-mytemplate
|
||||||
|
'websitetemplates' => 0,
|
||||||
|
// Set this to 1 if the module provides a captcha driver
|
||||||
|
'captcha' => 0
|
||||||
|
);
|
||||||
|
|
||||||
|
// Data directories to create when module is enabled.
|
||||||
|
$this->dirs = array(
|
||||||
|
"/kundenkarte/temp",
|
||||||
|
"/kundenkarte/anlagen"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Config pages. Put here list of php page, stored into kundenkarte/admin directory, to use to setup module.
|
||||||
|
$this->config_page_url = array("setup.php@kundenkarte");
|
||||||
|
|
||||||
|
// Dependencies
|
||||||
|
// A condition to hide module
|
||||||
|
$this->hidden = getDolGlobalInt('MODULE_KUNDENKARTE_DISABLED'); // A condition to disable module;
|
||||||
|
// List of module class names that must be enabled if this module is enabled. Example: array('always'=>array('modModuleToEnable1','modModuleToEnable2'), 'FR'=>array('modModuleToEnableFR')...)
|
||||||
|
$this->depends = array();
|
||||||
|
// List of module class names to disable if this one is disabled. Example: array('modModuleToDisable1', ...)
|
||||||
|
$this->requiredby = array();
|
||||||
|
// List of module class names this module is in conflict with. Example: array('modModuleToDisable1', ...)
|
||||||
|
$this->conflictwith = array();
|
||||||
|
|
||||||
|
// The language file dedicated to your module
|
||||||
|
$this->langfiles = array("kundenkarte@kundenkarte");
|
||||||
|
|
||||||
|
// Prerequisites
|
||||||
|
$this->phpmin = array(7, 1); // Minimum version of PHP required by module
|
||||||
|
// $this->phpmax = array(8, 0); // Maximum version of PHP required by module
|
||||||
|
$this->need_dolibarr_version = array(19, -3); // Minimum version of Dolibarr required by module
|
||||||
|
// $this->max_dolibarr_version = array(19, -3); // Maximum version of Dolibarr required by module
|
||||||
|
$this->need_javascript_ajax = 0;
|
||||||
|
|
||||||
|
// Messages at activation
|
||||||
|
$this->warnings_activation = array(); // Warning to show when we activate module. array('always'='text') or array('FR'='textfr','MX'='textmx'...)
|
||||||
|
$this->warnings_activation_ext = array(); // Warning to show when we activate an external module. array('always'='text') or array('FR'='textfr','MX'='textmx'...)
|
||||||
|
//$this->automatic_activation = array('FR'=>'KundenKarteWasAutomaticallyActivatedBecauseOfYourCountryChoice');
|
||||||
|
//$this->always_enabled = true; // If true, can't be disabled
|
||||||
|
|
||||||
|
// Constants
|
||||||
|
// List of particular constants to add when module is enabled (key, 'chaine', value, desc, visible, 'current' or 'allentities', deleteonunactive)
|
||||||
|
// Example: $this->const=array(1 => array('KUNDENKARTE_MYNEWCONST1', 'chaine', 'myvalue', 'This is a constant to add', 1),
|
||||||
|
// 2 => array('KUNDENKARTE_MYNEWCONST2', 'chaine', 'myvalue', 'This is another constant to add', 0, 'current', 1)
|
||||||
|
// );
|
||||||
|
$this->const = array();
|
||||||
|
|
||||||
|
// Some keys to add into the overwriting translation tables
|
||||||
|
/*$this->overwrite_translation = array(
|
||||||
|
'en_US:ParentCompany'=>'Parent company or reseller',
|
||||||
|
'fr_FR:ParentCompany'=>'Maison mère ou revendeur'
|
||||||
|
)*/
|
||||||
|
|
||||||
|
if (!isModEnabled("kundenkarte")) {
|
||||||
|
$conf->kundenkarte = new stdClass();
|
||||||
|
$conf->kundenkarte->enabled = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Array to add new pages in new tabs
|
||||||
|
/* BEGIN MODULEBUILDER TABS */
|
||||||
|
$this->tabs = array(
|
||||||
|
// Add tab for favorite products on thirdparty card
|
||||||
|
array('data' => 'thirdparty:+favoriteproducts:FavoriteProducts:kundenkarte@kundenkarte:$user->hasRight(\'kundenkarte\', \'read\'):/kundenkarte/tabs/favoriteproducts.php?id=__ID__'),
|
||||||
|
// Add tab for technical installations on thirdparty card
|
||||||
|
array('data' => 'thirdparty:+anlagen:TechnicalInstallations:kundenkarte@kundenkarte:$user->hasRight(\'kundenkarte\', \'read\'):/kundenkarte/tabs/anlagen.php?id=__ID__'),
|
||||||
|
// Add tab for favorite products on contact card (for addresses/buildings)
|
||||||
|
array('data' => 'contact:+favoriteproducts:FavoriteProducts:kundenkarte@kundenkarte:$user->hasRight(\'kundenkarte\', \'read\'):/kundenkarte/tabs/contact_favoriteproducts.php?id=__ID__'),
|
||||||
|
// Add tab for technical installations on contact card (for addresses/buildings)
|
||||||
|
array('data' => 'contact:+anlagen:TechnicalInstallations:kundenkarte@kundenkarte:$user->hasRight(\'kundenkarte\', \'read\'):/kundenkarte/tabs/contact_anlagen.php?id=__ID__'),
|
||||||
|
);
|
||||||
|
/* END MODULEBUILDER TABS */
|
||||||
|
// Example:
|
||||||
|
// To add a new tab identified by code tabname1
|
||||||
|
// $this->tabs[] = array('data' => 'objecttype:+tabname1:Title1:mylangfile@kundenkarte:$user->hasRight(\'kundenkarte\', \'read\'):/kundenkarte/mynewtab1.php?id=__ID__');
|
||||||
|
// To add another new tab identified by code tabname2. Label will be result of calling all substitution functions on 'Title2' key.
|
||||||
|
// $this->tabs[] = array('data' => 'objecttype:+tabname2:SUBSTITUTION_Title2:mylangfile@kundenkarte:$user->hasRight(\'othermodule\', \'read\'):/kundenkarte/mynewtab2.php?id=__ID__',
|
||||||
|
// To remove an existing tab identified by code tabname
|
||||||
|
// $this->tabs[] = array('data' => 'objecttype:-tabname:NU:conditiontoremove');
|
||||||
|
//
|
||||||
|
// Where objecttype can be
|
||||||
|
// 'categories_x' to add a tab in category view (replace 'x' by type of category (0=product, 1=supplier, 2=customer, 3=member)
|
||||||
|
// 'contact' to add a tab in contact view
|
||||||
|
// 'contract' to add a tab in contract view
|
||||||
|
// 'delivery' to add a tab in delivery view
|
||||||
|
// 'group' to add a tab in group view
|
||||||
|
// 'intervention' to add a tab in intervention view
|
||||||
|
// 'invoice' to add a tab in customer invoice view
|
||||||
|
// 'supplier_invoice' to add a tab in supplier invoice view
|
||||||
|
// 'member' to add a tab in foundation member view
|
||||||
|
// 'opensurveypoll' to add a tab in opensurvey poll view
|
||||||
|
// 'order' to add a tab in sale order view
|
||||||
|
// 'supplier_order' to add a tab in supplier order view
|
||||||
|
// 'payment' to add a tab in payment view
|
||||||
|
// 'supplier_payment' to add a tab in supplier payment view
|
||||||
|
// 'product' to add a tab in product view
|
||||||
|
// 'propal' to add a tab in propal view
|
||||||
|
// 'project' to add a tab in project view
|
||||||
|
// 'stock' to add a tab in stock view
|
||||||
|
// 'thirdparty' to add a tab in third party view
|
||||||
|
// 'user' to add a tab in user view
|
||||||
|
|
||||||
|
|
||||||
|
// Dictionaries
|
||||||
|
/* Example:
|
||||||
|
$this->dictionaries=array(
|
||||||
|
'langs' => 'kundenkarte@kundenkarte',
|
||||||
|
// List of tables we want to see into dictionary editor
|
||||||
|
'tabname' => array("table1", "table2", "table3"),
|
||||||
|
// Label of tables
|
||||||
|
'tablib' => array("Table1", "Table2", "Table3"),
|
||||||
|
// Request to select fields
|
||||||
|
'tabsql' => array('SELECT f.rowid as rowid, f.code, f.label, f.active FROM '.$this->db->prefix().'table1 as f', 'SELECT f.rowid as rowid, f.code, f.label, f.active FROM '.$this->db->prefix().'table2 as f', 'SELECT f.rowid as rowid, f.code, f.label, f.active FROM '.$this->db->prefix().'table3 as f'),
|
||||||
|
// Sort order
|
||||||
|
'tabsqlsort' => array("label ASC", "label ASC", "label ASC"),
|
||||||
|
// List of fields (result of select to show dictionary)
|
||||||
|
'tabfield' => array("code,label", "code,label", "code,label"),
|
||||||
|
// List of fields (list of fields to edit a record)
|
||||||
|
'tabfieldvalue' => array("code,label", "code,label", "code,label"),
|
||||||
|
// List of fields (list of fields for insert)
|
||||||
|
'tabfieldinsert' => array("code,label", "code,label", "code,label"),
|
||||||
|
// Name of columns with primary key (try to always name it 'rowid')
|
||||||
|
'tabrowid' => array("rowid", "rowid", "rowid"),
|
||||||
|
// Condition to show each dictionary
|
||||||
|
'tabcond' => array(isModEnabled('kundenkarte'), isModEnabled('kundenkarte'), isModEnabled('kundenkarte')),
|
||||||
|
// Tooltip for every fields of dictionaries: DO NOT PUT AN EMPTY ARRAY
|
||||||
|
'tabhelp' => array(array('code' => $langs->trans('CodeTooltipHelp'), 'field2' => 'field2tooltip'), array('code' => $langs->trans('CodeTooltipHelp'), 'field2' => 'field2tooltip'), ...),
|
||||||
|
);
|
||||||
|
*/
|
||||||
|
/* BEGIN MODULEBUILDER DICTIONARIES */
|
||||||
|
$this->dictionaries = array();
|
||||||
|
/* END MODULEBUILDER DICTIONARIES */
|
||||||
|
|
||||||
|
// Boxes/Widgets
|
||||||
|
// Add here list of php file(s) stored in kundenkarte/core/boxes that contains a class to show a widget.
|
||||||
|
/* BEGIN MODULEBUILDER WIDGETS */
|
||||||
|
$this->boxes = array(
|
||||||
|
// 0 => array(
|
||||||
|
// 'file' => 'kundenkartewidget1.php@kundenkarte',
|
||||||
|
// 'note' => 'Widget provided by KundenKarte',
|
||||||
|
// 'enabledbydefaulton' => 'Home',
|
||||||
|
// ),
|
||||||
|
// ...
|
||||||
|
);
|
||||||
|
/* END MODULEBUILDER WIDGETS */
|
||||||
|
|
||||||
|
// Cronjobs (List of cron jobs entries to add when module is enabled)
|
||||||
|
// unit_frequency must be 60 for minute, 3600 for hour, 86400 for day, 604800 for week
|
||||||
|
/* BEGIN MODULEBUILDER CRON */
|
||||||
|
$this->cronjobs = array(
|
||||||
|
// 0 => array(
|
||||||
|
// 'label' => 'MyJob label',
|
||||||
|
// 'jobtype' => 'method',
|
||||||
|
// 'class' => '/kundenkarte/class/myobject.class.php',
|
||||||
|
// 'objectname' => 'MyObject',
|
||||||
|
// 'method' => 'doScheduledJob',
|
||||||
|
// 'parameters' => '',
|
||||||
|
// 'comment' => 'Comment',
|
||||||
|
// 'frequency' => 2,
|
||||||
|
// 'unitfrequency' => 3600,
|
||||||
|
// 'status' => 0,
|
||||||
|
// 'test' => 'isModEnabled("kundenkarte")',
|
||||||
|
// 'priority' => 50,
|
||||||
|
// ),
|
||||||
|
);
|
||||||
|
/* END MODULEBUILDER CRON */
|
||||||
|
// Example: $this->cronjobs=array(
|
||||||
|
// 0=>array('label'=>'My label', 'jobtype'=>'method', 'class'=>'/dir/class/file.class.php', 'objectname'=>'MyClass', 'method'=>'myMethod', 'parameters'=>'param1, param2', 'comment'=>'Comment', 'frequency'=>2, 'unitfrequency'=>3600, 'status'=>0, 'test'=>'isModEnabled("kundenkarte")', 'priority'=>50),
|
||||||
|
// 1=>array('label'=>'My label', 'jobtype'=>'command', 'command'=>'', 'parameters'=>'param1, param2', 'comment'=>'Comment', 'frequency'=>1, 'unitfrequency'=>3600*24, 'status'=>0, 'test'=>'isModEnabled("kundenkarte")', 'priority'=>50)
|
||||||
|
// );
|
||||||
|
|
||||||
|
// Permissions provided by this module
|
||||||
|
$this->rights = array();
|
||||||
|
$r = 0;
|
||||||
|
// Add here entries to declare new permissions
|
||||||
|
/* BEGIN MODULEBUILDER PERMISSIONS */
|
||||||
|
|
||||||
|
// Read permission
|
||||||
|
$this->rights[$r][0] = $this->numero . sprintf("%02d", 1);
|
||||||
|
$this->rights[$r][1] = 'Read KundenKarte data';
|
||||||
|
$this->rights[$r][4] = 'read';
|
||||||
|
$this->rights[$r][5] = '';
|
||||||
|
$r++;
|
||||||
|
|
||||||
|
// Write permission
|
||||||
|
$this->rights[$r][0] = $this->numero . sprintf("%02d", 2);
|
||||||
|
$this->rights[$r][1] = 'Create/Update KundenKarte data';
|
||||||
|
$this->rights[$r][4] = 'write';
|
||||||
|
$this->rights[$r][5] = '';
|
||||||
|
$r++;
|
||||||
|
|
||||||
|
// Delete permission
|
||||||
|
$this->rights[$r][0] = $this->numero . sprintf("%02d", 3);
|
||||||
|
$this->rights[$r][1] = 'Delete KundenKarte data';
|
||||||
|
$this->rights[$r][4] = 'delete';
|
||||||
|
$this->rights[$r][5] = '';
|
||||||
|
$r++;
|
||||||
|
|
||||||
|
// Admin permission for managing types and systems
|
||||||
|
$this->rights[$r][0] = $this->numero . sprintf("%02d", 4);
|
||||||
|
$this->rights[$r][1] = 'Administer KundenKarte settings';
|
||||||
|
$this->rights[$r][4] = 'admin';
|
||||||
|
$this->rights[$r][5] = '';
|
||||||
|
$r++;
|
||||||
|
|
||||||
|
/* END MODULEBUILDER PERMISSIONS */
|
||||||
|
|
||||||
|
|
||||||
|
// Main menu entries to add
|
||||||
|
$this->menu = array();
|
||||||
|
$r = 0;
|
||||||
|
// Add here entries to declare new menus
|
||||||
|
/* BEGIN MODULEBUILDER TOPMENU
|
||||||
|
$this->menu[$r++] = array(
|
||||||
|
'fk_menu' => '', // Will be stored into mainmenu + leftmenu. Use '' if this is a top menu. For left menu, use 'fk_mainmenu=xxx' or 'fk_mainmenu=xxx,fk_leftmenu=yyy' where xxx is mainmenucode and yyy is a leftmenucode
|
||||||
|
'type' => 'top', // This is a Top menu entry
|
||||||
|
'titre' => 'ModuleKundenKarteName',
|
||||||
|
'prefix' => img_picto('', $this->picto, 'class="pictofixedwidth valignmiddle"'),
|
||||||
|
'mainmenu' => 'kundenkarte',
|
||||||
|
'leftmenu' => '',
|
||||||
|
'url' => '/kundenkarte/kundenkarteindex.php',
|
||||||
|
'langs' => 'kundenkarte@kundenkarte', // Lang file to use (without .lang) by module. File must be in langs/code_CODE/ directory.
|
||||||
|
'position' => 1000 + $r,
|
||||||
|
'enabled' => 'isModEnabled("kundenkarte")', // Define condition to show or hide menu entry. Use 'isModEnabled("kundenkarte")' if entry must be visible if module is enabled.
|
||||||
|
'perms' => '1', // Use 'perms'=>'$user->hasRight("kundenkarte", "myobject", "read")' if you want your menu with a permission rules
|
||||||
|
'target' => '',
|
||||||
|
'user' => 2, // 0=Menu for internal users, 1=external users, 2=both
|
||||||
|
);*/
|
||||||
|
/* END MODULEBUILDER TOPMENU */
|
||||||
|
|
||||||
|
/* BEGIN MODULEBUILDER LEFTMENU */
|
||||||
|
// Admin submenu: Manage Systems
|
||||||
|
$this->menu[$r++] = array(
|
||||||
|
'fk_menu' => 'fk_mainmenu=kundenkarte',
|
||||||
|
'type' => 'left',
|
||||||
|
'titre' => 'AnlagenSystems',
|
||||||
|
'prefix' => img_picto('', 'fa-cogs', 'class="pictofixedwidth valignmiddle paddingright"'),
|
||||||
|
'mainmenu' => 'kundenkarte',
|
||||||
|
'leftmenu' => 'kundenkarte_systems',
|
||||||
|
'url' => '/kundenkarte/admin/anlage_systems.php',
|
||||||
|
'langs' => 'kundenkarte@kundenkarte',
|
||||||
|
'position' => 1000 + $r,
|
||||||
|
'enabled' => 'isModEnabled("kundenkarte")',
|
||||||
|
'perms' => '$user->hasRight("kundenkarte", "admin")',
|
||||||
|
'target' => '',
|
||||||
|
'user' => 0,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Admin submenu: Manage Element Types
|
||||||
|
$this->menu[$r++] = array(
|
||||||
|
'fk_menu' => 'fk_mainmenu=kundenkarte',
|
||||||
|
'type' => 'left',
|
||||||
|
'titre' => 'AnlagenTypes',
|
||||||
|
'prefix' => img_picto('', 'fa-list', 'class="pictofixedwidth valignmiddle paddingright"'),
|
||||||
|
'mainmenu' => 'kundenkarte',
|
||||||
|
'leftmenu' => 'kundenkarte_types',
|
||||||
|
'url' => '/kundenkarte/admin/anlage_types.php',
|
||||||
|
'langs' => 'kundenkarte@kundenkarte',
|
||||||
|
'position' => 1000 + $r,
|
||||||
|
'enabled' => 'isModEnabled("kundenkarte")',
|
||||||
|
'perms' => '$user->hasRight("kundenkarte", "admin")',
|
||||||
|
'target' => '',
|
||||||
|
'user' => 0,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Admin submenu: Manage Equipment Types
|
||||||
|
$this->menu[$r++] = array(
|
||||||
|
'fk_menu' => 'fk_mainmenu=kundenkarte',
|
||||||
|
'type' => 'left',
|
||||||
|
'titre' => 'EquipmentTypes',
|
||||||
|
'prefix' => img_picto('', 'fa-microchip', 'class="pictofixedwidth valignmiddle paddingright"'),
|
||||||
|
'mainmenu' => 'kundenkarte',
|
||||||
|
'leftmenu' => 'kundenkarte_equipment_types',
|
||||||
|
'url' => '/kundenkarte/admin/equipment_types.php',
|
||||||
|
'langs' => 'kundenkarte@kundenkarte',
|
||||||
|
'position' => 1000 + $r,
|
||||||
|
'enabled' => 'isModEnabled("kundenkarte")',
|
||||||
|
'perms' => '$user->hasRight("kundenkarte", "admin")',
|
||||||
|
'target' => '',
|
||||||
|
'user' => 0,
|
||||||
|
);
|
||||||
|
/* END MODULEBUILDER LEFTMENU */
|
||||||
|
|
||||||
|
|
||||||
|
// Exports profiles provided by this module
|
||||||
|
$r = 0;
|
||||||
|
/* BEGIN MODULEBUILDER EXPORT MYOBJECT */
|
||||||
|
/*
|
||||||
|
$langs->load("kundenkarte@kundenkarte");
|
||||||
|
$this->export_code[$r] = $this->rights_class.'_'.$r;
|
||||||
|
$this->export_label[$r] = 'MyObjectLines'; // Translation key (used only if key ExportDataset_xxx_z not found)
|
||||||
|
$this->export_icon[$r] = $this->picto;
|
||||||
|
// Define $this->export_fields_array, $this->export_TypeFields_array and $this->export_entities_array
|
||||||
|
$keyforclass = 'MyObject'; $keyforclassfile='/kundenkarte/class/myobject.class.php'; $keyforelement='myobject@kundenkarte';
|
||||||
|
include DOL_DOCUMENT_ROOT.'/core/commonfieldsinexport.inc.php';
|
||||||
|
//$this->export_fields_array[$r]['t.fieldtoadd']='FieldToAdd'; $this->export_TypeFields_array[$r]['t.fieldtoadd']='Text';
|
||||||
|
//unset($this->export_fields_array[$r]['t.fieldtoremove']);
|
||||||
|
//$keyforclass = 'MyObjectLine'; $keyforclassfile='/kundenkarte/class/myobject.class.php'; $keyforelement='myobjectline@kundenkarte'; $keyforalias='tl';
|
||||||
|
//include DOL_DOCUMENT_ROOT.'/core/commonfieldsinexport.inc.php';
|
||||||
|
$keyforselect='myobject'; $keyforaliasextra='extra'; $keyforelement='myobject@kundenkarte';
|
||||||
|
include DOL_DOCUMENT_ROOT.'/core/extrafieldsinexport.inc.php';
|
||||||
|
//$keyforselect='myobjectline'; $keyforaliasextra='extraline'; $keyforelement='myobjectline@kundenkarte';
|
||||||
|
//include DOL_DOCUMENT_ROOT.'/core/extrafieldsinexport.inc.php';
|
||||||
|
//$this->export_dependencies_array[$r] = array('myobjectline' => array('tl.rowid','tl.ref')); // To force to activate one or several fields if we select some fields that need same (like to select a unique key if we ask a field of a child to avoid the DISTINCT to discard them, or for computed field than need several other fields)
|
||||||
|
//$this->export_special_array[$r] = array('t.field' => '...');
|
||||||
|
//$this->export_examplevalues_array[$r] = array('t.field' => 'Example');
|
||||||
|
//$this->export_help_array[$r] = array('t.field' => 'FieldDescHelp');
|
||||||
|
$this->export_sql_start[$r]='SELECT DISTINCT ';
|
||||||
|
$this->export_sql_end[$r] =' FROM '.$this->db->prefix().'kundenkarte_myobject as t';
|
||||||
|
//$this->export_sql_end[$r] .=' LEFT JOIN '.$this->db->prefix().'kundenkarte_myobject_line as tl ON tl.fk_myobject = t.rowid';
|
||||||
|
$this->export_sql_end[$r] .=' WHERE 1 = 1';
|
||||||
|
$this->export_sql_end[$r] .=' AND t.entity IN ('.getEntity('myobject').')';
|
||||||
|
$r++; */
|
||||||
|
/* END MODULEBUILDER EXPORT MYOBJECT */
|
||||||
|
|
||||||
|
// Imports profiles provided by this module
|
||||||
|
$r = 0;
|
||||||
|
/* BEGIN MODULEBUILDER IMPORT MYOBJECT */
|
||||||
|
/*
|
||||||
|
$langs->load("kundenkarte@kundenkarte");
|
||||||
|
$this->import_code[$r] = $this->rights_class.'_'.$r;
|
||||||
|
$this->import_label[$r] = 'MyObjectLines'; // Translation key (used only if key ExportDataset_xxx_z not found)
|
||||||
|
$this->import_icon[$r] = $this->picto;
|
||||||
|
$this->import_tables_array[$r] = array('t' => $this->db->prefix().'kundenkarte_myobject', 'extra' => $this->db->prefix().'kundenkarte_myobject_extrafields');
|
||||||
|
$this->import_tables_creator_array[$r] = array('t' => 'fk_user_author'); // Fields to store import user id
|
||||||
|
$import_sample = array();
|
||||||
|
$keyforclass = 'MyObject'; $keyforclassfile='/kundenkarte/class/myobject.class.php'; $keyforelement='myobject@kundenkarte';
|
||||||
|
include DOL_DOCUMENT_ROOT.'/core/commonfieldsinimport.inc.php';
|
||||||
|
$import_extrafield_sample = array();
|
||||||
|
$keyforselect='myobject'; $keyforaliasextra='extra'; $keyforelement='myobject@kundenkarte';
|
||||||
|
include DOL_DOCUMENT_ROOT.'/core/extrafieldsinimport.inc.php';
|
||||||
|
$this->import_fieldshidden_array[$r] = array('extra.fk_object' => 'lastrowid-'.$this->db->prefix().'kundenkarte_myobject');
|
||||||
|
$this->import_regex_array[$r] = array();
|
||||||
|
$this->import_examplevalues_array[$r] = array_merge($import_sample, $import_extrafield_sample);
|
||||||
|
$this->import_updatekeys_array[$r] = array('t.ref' => 'Ref');
|
||||||
|
$this->import_convertvalue_array[$r] = array(
|
||||||
|
't.ref' => array(
|
||||||
|
'rule'=>'getrefifauto',
|
||||||
|
'class'=>(!getDolGlobalString('KUNDENKARTE_MYOBJECT_ADDON') ? 'mod_myobject_standard' : getDolGlobalString('KUNDENKARTE_MYOBJECT_ADDON')),
|
||||||
|
'path'=>"/core/modules/kundenkarte/".(!getDolGlobalString('KUNDENKARTE_MYOBJECT_ADDON') ? 'mod_myobject_standard' : getDolGlobalString('KUNDENKARTE_MYOBJECT_ADDON')).'.php',
|
||||||
|
'classobject'=>'MyObject',
|
||||||
|
'pathobject'=>'/kundenkarte/class/myobject.class.php',
|
||||||
|
),
|
||||||
|
't.fk_soc' => array('rule' => 'fetchidfromref', 'file' => '/societe/class/societe.class.php', 'class' => 'Societe', 'method' => 'fetch', 'element' => 'ThirdParty'),
|
||||||
|
't.fk_user_valid' => array('rule' => 'fetchidfromref', 'file' => '/user/class/user.class.php', 'class' => 'User', 'method' => 'fetch', 'element' => 'user'),
|
||||||
|
't.fk_mode_reglement' => array('rule' => 'fetchidfromcodeorlabel', 'file' => '/compta/paiement/class/cpaiement.class.php', 'class' => 'Cpaiement', 'method' => 'fetch', 'element' => 'cpayment'),
|
||||||
|
);
|
||||||
|
$this->import_run_sql_after_array[$r] = array();
|
||||||
|
$r++; */
|
||||||
|
/* END MODULEBUILDER IMPORT MYOBJECT */
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function called when module is enabled.
|
||||||
|
* The init function add constants, boxes, permissions and menus (defined in constructor) into Dolibarr database.
|
||||||
|
* It also creates data directories
|
||||||
|
*
|
||||||
|
* @param string $options Options when enabling module ('', 'noboxes')
|
||||||
|
* @return int<-1,1> 1 if OK, <=0 if KO
|
||||||
|
*/
|
||||||
|
public function init($options = '')
|
||||||
|
{
|
||||||
|
global $conf, $langs;
|
||||||
|
|
||||||
|
// Create tables of module at module activation
|
||||||
|
//$result = $this->_load_tables('/install/mysql/', 'kundenkarte');
|
||||||
|
$result = $this->_load_tables('/kundenkarte/sql/');
|
||||||
|
if ($result < 0) {
|
||||||
|
return -1; // Do not activate module if error 'not allowed' returned when loading module SQL queries (the _load_table run sql with run_sql with the error allowed parameter set to 'default')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create extrafields during init
|
||||||
|
//include_once DOL_DOCUMENT_ROOT.'/core/class/extrafields.class.php';
|
||||||
|
//$extrafields = new ExtraFields($this->db);
|
||||||
|
//$result0=$extrafields->addExtraField('kundenkarte_separator1', "Separator 1", 'separator', 1, 0, 'thirdparty', 0, 0, '', array('options'=>array(1=>1)), 1, '', 1, 0, '', '', 'kundenkarte@kundenkarte', 'isModEnabled("kundenkarte")');
|
||||||
|
//$result1=$extrafields->addExtraField('kundenkarte_myattr1', "New Attr 1 label", 'boolean', 1, 3, 'thirdparty', 0, 0, '', '', 1, '', -1, 0, '', '', 'kundenkarte@kundenkarte', 'isModEnabled("kundenkarte")');
|
||||||
|
//$result2=$extrafields->addExtraField('kundenkarte_myattr2', "New Attr 2 label", 'varchar', 1, 10, 'project', 0, 0, '', '', 1, '', -1, 0, '', '', 'kundenkarte@kundenkarte', 'isModEnabled("kundenkarte")');
|
||||||
|
//$result3=$extrafields->addExtraField('kundenkarte_myattr3', "New Attr 3 label", 'varchar', 1, 10, 'bank_account', 0, 0, '', '', 1, '', -1, 0, '', '', 'kundenkarte@kundenkarte', 'isModEnabled("kundenkarte")');
|
||||||
|
//$result4=$extrafields->addExtraField('kundenkarte_myattr4', "New Attr 4 label", 'select', 1, 3, 'thirdparty', 0, 1, '', array('options'=>array('code1'=>'Val1','code2'=>'Val2','code3'=>'Val3')), 1,'', -1, 0, '', '', 'kundenkarte@kundenkarte', 'isModEnabled("kundenkarte")');
|
||||||
|
//$result5=$extrafields->addExtraField('kundenkarte_myattr5', "New Attr 5 label", 'text', 1, 10, 'user', 0, 0, '', '', 1, '', -1, 0, '', '', 'kundenkarte@kundenkarte', 'isModEnabled("kundenkarte")');
|
||||||
|
|
||||||
|
// Migrationen: UNIQUE KEY uk_kundenkarte_societe_system um fk_contact erweitern
|
||||||
|
$this->_migrateSocieteSystemUniqueKey();
|
||||||
|
|
||||||
|
// Run all database migrations
|
||||||
|
$this->runMigrations();
|
||||||
|
|
||||||
|
// Permissions
|
||||||
|
$this->remove($options);
|
||||||
|
|
||||||
|
$sql = array();
|
||||||
|
|
||||||
|
// Document templates
|
||||||
|
$moduledir = dol_sanitizeFileName('kundenkarte');
|
||||||
|
$myTmpObjects = array();
|
||||||
|
$myTmpObjects['MyObject'] = array('includerefgeneration' => 0, 'includedocgeneration' => 0);
|
||||||
|
|
||||||
|
foreach ($myTmpObjects as $myTmpObjectKey => $myTmpObjectArray) {
|
||||||
|
if ($myTmpObjectArray['includerefgeneration']) {
|
||||||
|
$src = DOL_DOCUMENT_ROOT.'/install/doctemplates/'.$moduledir.'/template_myobjects.odt';
|
||||||
|
$dirodt = DOL_DATA_ROOT.($conf->entity > 1 ? '/'.$conf->entity : '').'/doctemplates/'.$moduledir;
|
||||||
|
$dest = $dirodt.'/template_myobjects.odt';
|
||||||
|
|
||||||
|
if (file_exists($src) && !file_exists($dest)) {
|
||||||
|
require_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php';
|
||||||
|
dol_mkdir($dirodt);
|
||||||
|
$result = dol_copy($src, $dest, '0', 0);
|
||||||
|
if ($result < 0) {
|
||||||
|
$langs->load("errors");
|
||||||
|
$this->error = $langs->trans('ErrorFailToCopyFile', $src, $dest);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$sql = array_merge($sql, array(
|
||||||
|
"DELETE FROM ".$this->db->prefix()."document_model WHERE nom = 'standard_".strtolower($myTmpObjectKey)."' AND type = '".$this->db->escape(strtolower($myTmpObjectKey))."' AND entity = ".((int) $conf->entity),
|
||||||
|
"INSERT INTO ".$this->db->prefix()."document_model (nom, type, entity) VALUES('standard_".strtolower($myTmpObjectKey)."', '".$this->db->escape(strtolower($myTmpObjectKey))."', ".((int) $conf->entity).")",
|
||||||
|
"DELETE FROM ".$this->db->prefix()."document_model WHERE nom = 'generic_".strtolower($myTmpObjectKey)."_odt' AND type = '".$this->db->escape(strtolower($myTmpObjectKey))."' AND entity = ".((int) $conf->entity),
|
||||||
|
"INSERT INTO ".$this->db->prefix()."document_model (nom, type, entity) VALUES('generic_".strtolower($myTmpObjectKey)."_odt', '".$this->db->escape(strtolower($myTmpObjectKey))."', ".((int) $conf->entity).")"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->_init($sql, $options);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Migration: UNIQUE KEY uk_kundenkarte_societe_system um fk_contact erweitern.
|
||||||
|
* Idempotent - kann mehrfach ausgeführt werden.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private function _migrateSocieteSystemUniqueKey()
|
||||||
|
{
|
||||||
|
$table = MAIN_DB_PREFIX.'kundenkarte_societe_system';
|
||||||
|
|
||||||
|
// Prüfen ob Tabelle existiert
|
||||||
|
$resql = $this->db->query("SHOW TABLES LIKE '".$this->db->escape($table)."'");
|
||||||
|
if (!$resql || $this->db->num_rows($resql) == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// NULL-Werte in fk_contact auf 0 normalisieren
|
||||||
|
$this->db->query("UPDATE ".$table." SET fk_contact = 0 WHERE fk_contact IS NULL");
|
||||||
|
|
||||||
|
// Spalte NOT NULL mit Default 0 setzen
|
||||||
|
$this->db->query("ALTER TABLE ".$table." MODIFY COLUMN fk_contact integer DEFAULT 0 NOT NULL");
|
||||||
|
|
||||||
|
// Prüfen ob der UNIQUE KEY fk_contact enthält
|
||||||
|
$resql = $this->db->query("SHOW INDEX FROM ".$table." WHERE Key_name = 'uk_kundenkarte_societe_system' AND Column_name = 'fk_contact'");
|
||||||
|
if ($resql && $this->db->num_rows($resql) > 0) {
|
||||||
|
return; // Bereits migriert
|
||||||
|
}
|
||||||
|
|
||||||
|
// Alten UNIQUE KEY entfernen (falls vorhanden)
|
||||||
|
$this->db->query("ALTER TABLE ".$table." DROP INDEX uk_kundenkarte_societe_system");
|
||||||
|
|
||||||
|
// Neuen UNIQUE KEY mit fk_contact anlegen
|
||||||
|
$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();
|
||||||
|
|
||||||
|
// v4.1.0: Add fk_building_node for room assignment
|
||||||
|
$this->migrate_v410_building_node();
|
||||||
|
|
||||||
|
// v4.1.0: Graph-Positionen speichern
|
||||||
|
$this->migrate_v410_graph_positions();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Migration v4.1.0: Spalte fk_building_node für Raumzuordnung
|
||||||
|
* Trennt Gebäude-/Raumzuordnung von technischem Parent (fk_parent)
|
||||||
|
*/
|
||||||
|
private function migrate_v410_building_node()
|
||||||
|
{
|
||||||
|
$table = MAIN_DB_PREFIX."kundenkarte_anlage";
|
||||||
|
|
||||||
|
// Prüfen ob Tabelle existiert
|
||||||
|
$resql = $this->db->query("SHOW TABLES LIKE '".$this->db->escape($table)."'");
|
||||||
|
if (!$resql || $this->db->num_rows($resql) == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prüfen ob Spalte bereits existiert
|
||||||
|
$resql = $this->db->query("SHOW COLUMNS FROM ".$table." LIKE 'fk_building_node'");
|
||||||
|
if ($resql && $this->db->num_rows($resql) > 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Spalte hinzufügen
|
||||||
|
$this->db->query("ALTER TABLE ".$table." ADD COLUMN fk_building_node integer DEFAULT 0 AFTER fk_system");
|
||||||
|
|
||||||
|
// Index für Performance
|
||||||
|
$this->db->query("ALTER TABLE ".$table." ADD INDEX idx_anlage_building_node (fk_building_node)");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Migration v4.1.0: Graph-Positionen (x/y) in Anlage-Tabelle
|
||||||
|
*/
|
||||||
|
private function migrate_v410_graph_positions()
|
||||||
|
{
|
||||||
|
$table = MAIN_DB_PREFIX."kundenkarte_anlage";
|
||||||
|
|
||||||
|
// Prüfen ob Tabelle existiert
|
||||||
|
$resql = $this->db->query("SHOW TABLES LIKE '".$this->db->escape($table)."'");
|
||||||
|
if (!$resql || $this->db->num_rows($resql) == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prüfen ob Spalte bereits existiert
|
||||||
|
$resql = $this->db->query("SHOW COLUMNS FROM ".$table." LIKE 'graph_x'");
|
||||||
|
if ($resql && $this->db->num_rows($resql) > 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Spalten hinzufügen
|
||||||
|
$this->db->query("ALTER TABLE ".$table." ADD COLUMN graph_x double DEFAULT NULL AFTER fk_building_node");
|
||||||
|
$this->db->query("ALTER TABLE ".$table." ADD COLUMN graph_y double DEFAULT NULL AFTER graph_x");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function called when module is disabled.
|
||||||
|
* Remove from database constants, boxes and permissions from Dolibarr database.
|
||||||
|
* Data directories are not deleted
|
||||||
|
*
|
||||||
|
* @param string $options Options when enabling module ('', 'noboxes')
|
||||||
|
* @return int<-1,1> 1 if OK, <=0 if KO
|
||||||
|
*/
|
||||||
|
public function remove($options = '')
|
||||||
|
{
|
||||||
|
$sql = array();
|
||||||
|
return $this->_remove($sql, $options);
|
||||||
|
}
|
||||||
|
}
|
||||||
2813
css/kundenkarte.css
Executable file
2813
css/kundenkarte.css
Executable file
File diff suppressed because it is too large
Load diff
402
css/kundenkarte_cytoscape.css
Executable file
402
css/kundenkarte_cytoscape.css
Executable file
|
|
@ -0,0 +1,402 @@
|
||||||
|
/**
|
||||||
|
* KundenKarte Graph-Ansicht Styles
|
||||||
|
* Nutzt Dolibarr CSS-Variablen für Theme-Kompatibilität
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* Graph Container */
|
||||||
|
.kundenkarte-graph-wrapper {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
margin-top: 10px;
|
||||||
|
overflow: visible;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
#kundenkarte-graph-container {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
height: 400px;
|
||||||
|
min-height: 300px;
|
||||||
|
max-height: 80vh;
|
||||||
|
border: 1px solid var(--inputbordercolor, #3a3a3a);
|
||||||
|
border-radius: 4px;
|
||||||
|
background: var(--colorbackbody, #1d1e20);
|
||||||
|
overflow: hidden;
|
||||||
|
box-sizing: border-box;
|
||||||
|
resize: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
#kundenkarte-graph-container {
|
||||||
|
height: 300px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Toolbar: Zweizeilig */
|
||||||
|
.kundenkarte-graph-toolbar {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kundenkarte-graph-toolbar-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Alle Buttons in der Graph-Toolbar einheitlich */
|
||||||
|
.kundenkarte-graph-toolbar .button,
|
||||||
|
.kundenkarte-graph-toolbar button.button {
|
||||||
|
white-space: nowrap;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center !important;
|
||||||
|
gap: 4px !important;
|
||||||
|
padding: 6px 12px !important;
|
||||||
|
font-size: 12px !important;
|
||||||
|
height: 30px !important;
|
||||||
|
box-sizing: border-box !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Spacer: schiebt Anordnen/Speichern/Abbrechen nach rechts */
|
||||||
|
.kundenkarte-graph-toolbar-spacer {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
#btn-graph-wheel-zoom.active {
|
||||||
|
background: var(--butactionbg, #4390dc) !important;
|
||||||
|
color: #fff !important;
|
||||||
|
border-color: var(--butactionbg, #4390dc) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Bearbeitungsmodus: Container-Rahmen */
|
||||||
|
#kundenkarte-graph-container.graph-edit-mode {
|
||||||
|
border-color: #5a9a6a;
|
||||||
|
box-shadow: 0 0 0 2px rgba(90, 154, 106, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Speichern-Button grün, Abbrechen-Button rot */
|
||||||
|
.btn-graph-save {
|
||||||
|
background: #2e7d32 !important;
|
||||||
|
border-color: #2e7d32 !important;
|
||||||
|
color: #fff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-graph-save:hover {
|
||||||
|
background: #388e3c !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-graph-cancel {
|
||||||
|
background: #c62828 !important;
|
||||||
|
border-color: #c62828 !important;
|
||||||
|
color: #fff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-graph-cancel:hover {
|
||||||
|
background: #d32f2f !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Legende */
|
||||||
|
.kundenkarte-graph-legend {
|
||||||
|
display: flex;
|
||||||
|
gap: 15px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
margin-top: 8px;
|
||||||
|
background: var(--colorbacktitle1, rgba(30, 30, 50, 0.8));
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--colortext, #aaa);
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kundenkarte-graph-legend-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kundenkarte-graph-legend-line {
|
||||||
|
width: 20px;
|
||||||
|
height: 2px;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kundenkarte-graph-legend-line.cable {
|
||||||
|
background: #5a8a5a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kundenkarte-graph-legend-line.passthrough {
|
||||||
|
background: none;
|
||||||
|
border-top: 2px dashed #505860;
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kundenkarte-graph-legend-line.hierarchy {
|
||||||
|
background: none;
|
||||||
|
border-top: 2px dotted #6a7a8a;
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kundenkarte-graph-legend-box {
|
||||||
|
width: 16px;
|
||||||
|
height: 12px;
|
||||||
|
border-radius: 2px;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kundenkarte-graph-legend-box.building {
|
||||||
|
background: #2a2b2d;
|
||||||
|
border: 1px dashed #4390dc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kundenkarte-graph-legend-box.device {
|
||||||
|
background: #2d4a3a;
|
||||||
|
border: 2px solid #5a9a6a;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Loading Overlay */
|
||||||
|
.kundenkarte-graph-loading {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
color: var(--colortext, #aaa);
|
||||||
|
font-size: 14px;
|
||||||
|
z-index: 10;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kundenkarte-graph-loading i {
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Graph Tooltip */
|
||||||
|
.kundenkarte-graph-tooltip {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 100;
|
||||||
|
background: var(--colorbacktabcard1, #1e2a3a);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 0;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--colortext, #ccc);
|
||||||
|
max-width: 320px;
|
||||||
|
min-width: 200px;
|
||||||
|
pointer-events: none;
|
||||||
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5), 0 2px 8px rgba(0, 0, 0, 0.3);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kundenkarte-graph-tooltip .tooltip-header {
|
||||||
|
padding: 10px 14px;
|
||||||
|
background: rgba(255, 255, 255, 0.03);
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kundenkarte-graph-tooltip .tooltip-title {
|
||||||
|
font-weight: bold;
|
||||||
|
color: var(--colortextlink, #7ab0d4);
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kundenkarte-graph-tooltip .tooltip-title i {
|
||||||
|
margin-right: 4px;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kundenkarte-graph-tooltip .tooltip-type {
|
||||||
|
color: var(--colortext, #999);
|
||||||
|
font-size: 11px;
|
||||||
|
margin-top: 3px;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kundenkarte-graph-tooltip .tooltip-system {
|
||||||
|
background: rgba(255, 255, 255, 0.08);
|
||||||
|
padding: 1px 6px;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-size: 10px;
|
||||||
|
margin-left: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kundenkarte-graph-tooltip .tooltip-fields {
|
||||||
|
padding: 6px 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kundenkarte-graph-tooltip .tooltip-field {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 3px 0;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
.kundenkarte-graph-tooltip .tooltip-field:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kundenkarte-graph-tooltip .tooltip-field-label {
|
||||||
|
color: var(--colortext, #888);
|
||||||
|
opacity: 0.6;
|
||||||
|
font-size: 11px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kundenkarte-graph-tooltip .tooltip-field-value {
|
||||||
|
color: var(--colortext, #eee);
|
||||||
|
text-align: right;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kundenkarte-graph-tooltip .tooltip-field-badge {
|
||||||
|
color: #fff;
|
||||||
|
text-align: right;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 11px;
|
||||||
|
padding: 1px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kundenkarte-graph-tooltip .tooltip-footer {
|
||||||
|
padding: 6px 14px;
|
||||||
|
border-top: 1px solid rgba(255, 255, 255, 0.06);
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--colortext, #888);
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kundenkarte-graph-tooltip .tooltip-file-badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kundenkarte-graph-tooltip .tooltip-file-badge i {
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Leer-Zustand */
|
||||||
|
.kundenkarte-graph-empty {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 300px;
|
||||||
|
color: var(--colortext, #666);
|
||||||
|
font-size: 14px;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kundenkarte-graph-empty i {
|
||||||
|
font-size: 48px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Kontextmenü (Rechtsklick auf Node) */
|
||||||
|
.kundenkarte-graph-contextmenu {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 200;
|
||||||
|
background: var(--colorbacktabcard1, #1e2a3a);
|
||||||
|
border: 1px solid var(--inputbordercolor, #3a6a8e);
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 4px 0;
|
||||||
|
min-width: 160px;
|
||||||
|
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.kundenkarte-graph-contextmenu .ctx-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 8px 14px;
|
||||||
|
color: var(--colortext, #ddd);
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 13px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kundenkarte-graph-contextmenu .ctx-item:hover {
|
||||||
|
background: var(--butactionbg, #4390dc);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kundenkarte-graph-contextmenu .ctx-item i {
|
||||||
|
width: 16px;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kundenkarte-graph-contextmenu .ctx-delete {
|
||||||
|
color: #e57373;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kundenkarte-graph-contextmenu .ctx-delete:hover {
|
||||||
|
background: #c62828;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Suchfeld als Overlay im Graph-Container */
|
||||||
|
.kundenkarte-graph-search-floating {
|
||||||
|
position: absolute;
|
||||||
|
top: 8px;
|
||||||
|
right: 8px;
|
||||||
|
z-index: 20;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kundenkarte-graph-search-floating input {
|
||||||
|
padding: 6px 10px 6px 28px;
|
||||||
|
border: 1px solid var(--inputbordercolor, #3a3a3a);
|
||||||
|
border-radius: 4px;
|
||||||
|
background: var(--colorbackbody, #1d1e20);
|
||||||
|
color: var(--colortext, #ddd);
|
||||||
|
font-size: 12px;
|
||||||
|
width: 180px;
|
||||||
|
opacity: 0.7;
|
||||||
|
transition: opacity 0.2s, width 0.2s;
|
||||||
|
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' viewBox='0 0 24 24' fill='none' stroke='%23888' stroke-width='2'%3E%3Ccircle cx='11' cy='11' r='8'/%3E%3Cline x1='21' y1='21' x2='16.65' y2='16.65'/%3E%3C/svg%3E");
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: 8px center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kundenkarte-graph-search-floating input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--butactionbg, #4390dc);
|
||||||
|
opacity: 1;
|
||||||
|
width: 220px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile Anpassungen */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.kundenkarte-graph-toolbar-row {
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kundenkarte-graph-toolbar .button,
|
||||||
|
.kundenkarte-graph-toolbar button.button {
|
||||||
|
padding: 8px 8px !important;
|
||||||
|
font-size: 11px !important;
|
||||||
|
height: 28px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kundenkarte-graph-search-floating input {
|
||||||
|
width: 140px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kundenkarte-graph-search-floating input:focus {
|
||||||
|
width: 160px;
|
||||||
|
}
|
||||||
|
}
|
||||||
982
css/pwa.css
Normal file
982
css/pwa.css
Normal file
|
|
@ -0,0 +1,982 @@
|
||||||
|
/**
|
||||||
|
* KundenKarte PWA Styles
|
||||||
|
* Mobile-First, Touch-optimiert, Dark Mode
|
||||||
|
*/
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--primary: #3498db;
|
||||||
|
--primary-dark: #2980b9;
|
||||||
|
--success: #27ae60;
|
||||||
|
--warning: #f39c12;
|
||||||
|
--danger: #e74c3c;
|
||||||
|
|
||||||
|
--bg-body: #1a1a2e;
|
||||||
|
--bg-card: #16213e;
|
||||||
|
--bg-input: #0f3460;
|
||||||
|
--bg-header: #0f3460;
|
||||||
|
|
||||||
|
--text: #eee;
|
||||||
|
--text-muted: #888;
|
||||||
|
--text-dim: #666;
|
||||||
|
|
||||||
|
--border: #2a2a4a;
|
||||||
|
--border-light: #3a3a5a;
|
||||||
|
|
||||||
|
--shadow: 0 4px 20px rgba(0,0,0,0.4);
|
||||||
|
--radius: 12px;
|
||||||
|
--radius-sm: 8px;
|
||||||
|
|
||||||
|
/* Safe areas für notches */
|
||||||
|
--safe-top: env(safe-area-inset-top, 0px);
|
||||||
|
--safe-bottom: env(safe-area-inset-bottom, 0px);
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
html, body {
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
|
||||||
|
background: var(--bg-body);
|
||||||
|
color: var(--text);
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================
|
||||||
|
APP CONTAINER
|
||||||
|
============================================ */
|
||||||
|
|
||||||
|
.app {
|
||||||
|
height: 100%;
|
||||||
|
height: 100dvh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================
|
||||||
|
SCREENS
|
||||||
|
============================================ */
|
||||||
|
|
||||||
|
.screen {
|
||||||
|
display: none;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.screen.active {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================
|
||||||
|
HEADER
|
||||||
|
============================================ */
|
||||||
|
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
padding-top: calc(12px + var(--safe-top));
|
||||||
|
background: var(--bg-header);
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
min-height: 60px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header h1 {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-spacer {
|
||||||
|
width: 44px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-icon {
|
||||||
|
width: 44px;
|
||||||
|
height: 44px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
border-radius: 50%;
|
||||||
|
color: var(--text);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.2s;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-icon:active {
|
||||||
|
background: rgba(255,255,255,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-icon svg {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
fill: currentColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sync-btn {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sync-badge {
|
||||||
|
position: absolute;
|
||||||
|
top: 4px;
|
||||||
|
right: 4px;
|
||||||
|
background: var(--danger);
|
||||||
|
color: #fff;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: bold;
|
||||||
|
min-width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
border-radius: 9px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 0 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sync-badge.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================
|
||||||
|
LOGIN SCREEN
|
||||||
|
============================================ */
|
||||||
|
|
||||||
|
.login-container {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-logo {
|
||||||
|
width: 80px;
|
||||||
|
height: 80px;
|
||||||
|
background: var(--primary);
|
||||||
|
border-radius: 20px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-logo svg {
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
fill: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-title {
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-subtitle {
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
margin-bottom: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-form {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 320px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================
|
||||||
|
FORMS
|
||||||
|
============================================ */
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group label {
|
||||||
|
display: block;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input,
|
||||||
|
.form-group select {
|
||||||
|
width: 100%;
|
||||||
|
padding: 14px 16px;
|
||||||
|
font-size: 16px;
|
||||||
|
background: var(--bg-input);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
color: var(--text);
|
||||||
|
outline: none;
|
||||||
|
transition: border-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input:focus,
|
||||||
|
.form-group select:focus {
|
||||||
|
border-color: var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input::placeholder {
|
||||||
|
color: var(--text-dim);
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-text {
|
||||||
|
color: var(--danger);
|
||||||
|
font-size: 13px;
|
||||||
|
margin-top: 12px;
|
||||||
|
text-align: center;
|
||||||
|
min-height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================
|
||||||
|
BUTTONS
|
||||||
|
============================================ */
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 14px 24px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
border: none;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:active {
|
||||||
|
transform: scale(0.98);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background: var(--primary);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:active {
|
||||||
|
background: var(--primary-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
background: var(--border);
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-success {
|
||||||
|
background: var(--success);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger {
|
||||||
|
background: var(--danger);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-large {
|
||||||
|
width: 100%;
|
||||||
|
padding: 16px;
|
||||||
|
font-size: 17px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================
|
||||||
|
SEARCH
|
||||||
|
============================================ */
|
||||||
|
|
||||||
|
.search-container {
|
||||||
|
padding: 16px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-box {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
background: var(--bg-input);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-box svg {
|
||||||
|
width: 22px;
|
||||||
|
height: 22px;
|
||||||
|
fill: var(--text-muted);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-box input {
|
||||||
|
flex: 1;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
color: var(--text);
|
||||||
|
font-size: 16px;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-box input::placeholder {
|
||||||
|
color: var(--text-dim);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================
|
||||||
|
LISTS
|
||||||
|
============================================ */
|
||||||
|
|
||||||
|
.list {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
padding: 0 16px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 14px;
|
||||||
|
padding: 16px;
|
||||||
|
background: var(--bg-card);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
margin-bottom: 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-item:active {
|
||||||
|
background: var(--bg-input);
|
||||||
|
transform: scale(0.99);
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-item-icon {
|
||||||
|
width: 44px;
|
||||||
|
height: 44px;
|
||||||
|
background: var(--primary);
|
||||||
|
border-radius: 10px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-item-icon svg {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
fill: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-item-content {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-item-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-item-subtitle {
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-item-arrow {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
fill: var(--text-dim);
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-empty {
|
||||||
|
text-align: center;
|
||||||
|
padding: 48px 24px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================
|
||||||
|
ANLAGEN GRID
|
||||||
|
============================================ */
|
||||||
|
|
||||||
|
.anlagen-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: 12px;
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.anlage-card {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
padding: 20px 16px;
|
||||||
|
background: var(--bg-card);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.anlage-card:active {
|
||||||
|
background: var(--bg-input);
|
||||||
|
transform: scale(0.98);
|
||||||
|
}
|
||||||
|
|
||||||
|
.anlage-card-icon {
|
||||||
|
width: 56px;
|
||||||
|
height: 56px;
|
||||||
|
background: var(--success);
|
||||||
|
border-radius: 14px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.anlage-card-icon svg {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
fill: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.anlage-card-title {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-align: center;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================
|
||||||
|
EDITOR
|
||||||
|
============================================ */
|
||||||
|
|
||||||
|
.editor-content {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
padding: 16px;
|
||||||
|
padding-bottom: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-card {
|
||||||
|
background: var(--bg-card);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
margin-bottom: 16px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 14px 16px;
|
||||||
|
background: var(--bg-header);
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-body {
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hutschiene */
|
||||||
|
.carrier-item {
|
||||||
|
background: var(--bg-input);
|
||||||
|
border: 1px solid var(--border-light);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
margin-bottom: 10px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.carrier-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 10px 12px;
|
||||||
|
background: rgba(255,255,255,0.03);
|
||||||
|
}
|
||||||
|
|
||||||
|
.carrier-label {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.carrier-te {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-dim);
|
||||||
|
}
|
||||||
|
|
||||||
|
.carrier-body {
|
||||||
|
padding: 10px;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 6px;
|
||||||
|
min-height: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Equipment Block */
|
||||||
|
.equipment-block {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-width: 44px;
|
||||||
|
height: 60px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
background: var(--primary);
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.equipment-block:active {
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
.equipment-block-type {
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.equipment-block-value {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.equipment-block-label {
|
||||||
|
font-size: 9px;
|
||||||
|
color: rgba(255,255,255,0.7);
|
||||||
|
max-width: 50px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Add Button in Carrier */
|
||||||
|
.btn-add-equipment {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-width: 44px;
|
||||||
|
height: 60px;
|
||||||
|
padding: 8px;
|
||||||
|
background: transparent;
|
||||||
|
border: 2px dashed var(--border-light);
|
||||||
|
border-radius: 6px;
|
||||||
|
color: var(--text-dim);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-add-equipment:active {
|
||||||
|
background: rgba(255,255,255,0.05);
|
||||||
|
border-color: var(--primary);
|
||||||
|
color: var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-add-equipment svg {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
fill: currentColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Add Carrier Button */
|
||||||
|
.btn-add-carrier {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 14px;
|
||||||
|
background: transparent;
|
||||||
|
border: 2px dashed var(--border);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-add-carrier:active {
|
||||||
|
border-color: var(--primary);
|
||||||
|
color: var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-add-carrier svg {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
fill: currentColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================
|
||||||
|
FAB (Floating Action Button)
|
||||||
|
============================================ */
|
||||||
|
|
||||||
|
.fab-container {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 24px;
|
||||||
|
right: 24px;
|
||||||
|
padding-bottom: var(--safe-bottom);
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fab {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 14px 20px;
|
||||||
|
background: var(--primary);
|
||||||
|
color: #fff;
|
||||||
|
border: none;
|
||||||
|
border-radius: 30px;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
box-shadow: var(--shadow);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fab:active {
|
||||||
|
transform: scale(0.95);
|
||||||
|
background: var(--primary-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
.fab svg {
|
||||||
|
width: 22px;
|
||||||
|
height: 22px;
|
||||||
|
fill: currentColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================
|
||||||
|
MODALS
|
||||||
|
============================================ */
|
||||||
|
|
||||||
|
.modal {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0,0,0,0.7);
|
||||||
|
display: none;
|
||||||
|
align-items: flex-end;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 1000;
|
||||||
|
padding: var(--safe-bottom);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal.active {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 500px;
|
||||||
|
max-height: 85vh;
|
||||||
|
background: var(--bg-card);
|
||||||
|
border-radius: var(--radius) var(--radius) 0 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
animation: slideUp 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-small {
|
||||||
|
max-height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideUp {
|
||||||
|
from {
|
||||||
|
transform: translateY(100%);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 16px 20px;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header h2 {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-close {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 28px;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-close:active {
|
||||||
|
background: rgba(255,255,255,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-body {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-footer {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 16px 20px;
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-footer .btn {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================
|
||||||
|
TYPE GRID
|
||||||
|
============================================ */
|
||||||
|
|
||||||
|
.step {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step.active {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-label {
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.type-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.type-btn {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 16px 8px;
|
||||||
|
background: var(--bg-input);
|
||||||
|
border: 2px solid var(--border);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
color: var(--text);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.type-btn:active,
|
||||||
|
.type-btn.selected {
|
||||||
|
border-color: var(--primary);
|
||||||
|
background: rgba(52, 152, 219, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.type-btn-icon {
|
||||||
|
font-size: 24px;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.type-btn-label {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.te-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.te-btn {
|
||||||
|
padding: 20px;
|
||||||
|
background: var(--bg-input);
|
||||||
|
border: 2px solid var(--border);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
color: var(--text);
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.te-btn:active,
|
||||||
|
.te-btn.selected {
|
||||||
|
border-color: var(--primary);
|
||||||
|
background: rgba(52, 152, 219, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Value Quick Select */
|
||||||
|
.value-quick {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value-chip {
|
||||||
|
padding: 10px 16px;
|
||||||
|
background: var(--bg-input);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 20px;
|
||||||
|
color: var(--text);
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value-chip:active,
|
||||||
|
.value-chip.selected {
|
||||||
|
background: var(--primary);
|
||||||
|
border-color: var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================
|
||||||
|
OFFLINE BAR
|
||||||
|
============================================ */
|
||||||
|
|
||||||
|
.offline-bar {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
padding: 10px;
|
||||||
|
padding-bottom: calc(10px + var(--safe-bottom));
|
||||||
|
background: var(--warning);
|
||||||
|
color: #000;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
z-index: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.offline-bar.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================
|
||||||
|
TOAST
|
||||||
|
============================================ */
|
||||||
|
|
||||||
|
.toast {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 80px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%) translateY(100px);
|
||||||
|
padding: 14px 24px;
|
||||||
|
background: var(--bg-card);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
color: var(--text);
|
||||||
|
font-size: 14px;
|
||||||
|
box-shadow: var(--shadow);
|
||||||
|
opacity: 0;
|
||||||
|
transition: all 0.3s;
|
||||||
|
z-index: 2000;
|
||||||
|
max-width: 90%;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast.visible {
|
||||||
|
transform: translateX(-50%) translateY(0);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast.success {
|
||||||
|
border-color: var(--success);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast.error {
|
||||||
|
border-color: var(--danger);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================
|
||||||
|
UTILITIES
|
||||||
|
============================================ */
|
||||||
|
|
||||||
|
.hidden {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-center {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-muted {
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Loading Spinner */
|
||||||
|
.spinner {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border: 3px solid var(--border);
|
||||||
|
border-top-color: var(--primary);
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 0.8s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
to { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-container {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
14
img/README.md
Executable file
14
img/README.md
Executable file
|
|
@ -0,0 +1,14 @@
|
||||||
|
|
||||||
|
Directory for module image files
|
||||||
|
--------------------------------
|
||||||
|
|
||||||
|
You can put here the .png files of your module:
|
||||||
|
|
||||||
|
|
||||||
|
If the picto of your module is an image (property $picto has been set to 'kundenkarte.png@kundenkarte', you can put into this
|
||||||
|
directory a .png file called *object_kundenkarte.png* (16x16 or 32x32 pixels)
|
||||||
|
|
||||||
|
|
||||||
|
If the picto of an object is an image (property $picto of the object.class.php has been set to 'myobject.png@kundenkarte', then you can put into this
|
||||||
|
directory a .png file called *object_myobject.png* (16x16 or 32x32 pixels)
|
||||||
|
|
||||||
BIN
img/pwa-icon-192.png
Normal file
BIN
img/pwa-icon-192.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.9 KiB |
BIN
img/pwa-icon-512.png
Normal file
BIN
img/pwa-icon-512.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 129 KiB |
3214
js/cose-base.js
Executable file
3214
js/cose-base.js
Executable file
File diff suppressed because it is too large
Load diff
458
js/cytoscape-cose-bilkent.js
Executable file
458
js/cytoscape-cose-bilkent.js
Executable file
|
|
@ -0,0 +1,458 @@
|
||||||
|
(function webpackUniversalModuleDefinition(root, factory) {
|
||||||
|
if(typeof exports === 'object' && typeof module === 'object')
|
||||||
|
module.exports = factory(require("cose-base"));
|
||||||
|
else if(typeof define === 'function' && define.amd)
|
||||||
|
define(["cose-base"], factory);
|
||||||
|
else if(typeof exports === 'object')
|
||||||
|
exports["cytoscapeCoseBilkent"] = factory(require("cose-base"));
|
||||||
|
else
|
||||||
|
root["cytoscapeCoseBilkent"] = factory(root["coseBase"]);
|
||||||
|
})(this, function(__WEBPACK_EXTERNAL_MODULE_0__) {
|
||||||
|
return /******/ (function(modules) { // webpackBootstrap
|
||||||
|
/******/ // The module cache
|
||||||
|
/******/ var installedModules = {};
|
||||||
|
/******/
|
||||||
|
/******/ // The require function
|
||||||
|
/******/ function __webpack_require__(moduleId) {
|
||||||
|
/******/
|
||||||
|
/******/ // Check if module is in cache
|
||||||
|
/******/ if(installedModules[moduleId]) {
|
||||||
|
/******/ return installedModules[moduleId].exports;
|
||||||
|
/******/ }
|
||||||
|
/******/ // Create a new module (and put it into the cache)
|
||||||
|
/******/ var module = installedModules[moduleId] = {
|
||||||
|
/******/ i: moduleId,
|
||||||
|
/******/ l: false,
|
||||||
|
/******/ exports: {}
|
||||||
|
/******/ };
|
||||||
|
/******/
|
||||||
|
/******/ // Execute the module function
|
||||||
|
/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
|
||||||
|
/******/
|
||||||
|
/******/ // Flag the module as loaded
|
||||||
|
/******/ module.l = true;
|
||||||
|
/******/
|
||||||
|
/******/ // Return the exports of the module
|
||||||
|
/******/ return module.exports;
|
||||||
|
/******/ }
|
||||||
|
/******/
|
||||||
|
/******/
|
||||||
|
/******/ // expose the modules object (__webpack_modules__)
|
||||||
|
/******/ __webpack_require__.m = modules;
|
||||||
|
/******/
|
||||||
|
/******/ // expose the module cache
|
||||||
|
/******/ __webpack_require__.c = installedModules;
|
||||||
|
/******/
|
||||||
|
/******/ // identity function for calling harmony imports with the correct context
|
||||||
|
/******/ __webpack_require__.i = function(value) { return value; };
|
||||||
|
/******/
|
||||||
|
/******/ // define getter function for harmony exports
|
||||||
|
/******/ __webpack_require__.d = function(exports, name, getter) {
|
||||||
|
/******/ if(!__webpack_require__.o(exports, name)) {
|
||||||
|
/******/ Object.defineProperty(exports, name, {
|
||||||
|
/******/ configurable: false,
|
||||||
|
/******/ enumerable: true,
|
||||||
|
/******/ get: getter
|
||||||
|
/******/ });
|
||||||
|
/******/ }
|
||||||
|
/******/ };
|
||||||
|
/******/
|
||||||
|
/******/ // getDefaultExport function for compatibility with non-harmony modules
|
||||||
|
/******/ __webpack_require__.n = function(module) {
|
||||||
|
/******/ var getter = module && module.__esModule ?
|
||||||
|
/******/ function getDefault() { return module['default']; } :
|
||||||
|
/******/ function getModuleExports() { return module; };
|
||||||
|
/******/ __webpack_require__.d(getter, 'a', getter);
|
||||||
|
/******/ return getter;
|
||||||
|
/******/ };
|
||||||
|
/******/
|
||||||
|
/******/ // Object.prototype.hasOwnProperty.call
|
||||||
|
/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
|
||||||
|
/******/
|
||||||
|
/******/ // __webpack_public_path__
|
||||||
|
/******/ __webpack_require__.p = "";
|
||||||
|
/******/
|
||||||
|
/******/ // Load entry module and return exports
|
||||||
|
/******/ return __webpack_require__(__webpack_require__.s = 1);
|
||||||
|
/******/ })
|
||||||
|
/************************************************************************/
|
||||||
|
/******/ ([
|
||||||
|
/* 0 */
|
||||||
|
/***/ (function(module, exports) {
|
||||||
|
|
||||||
|
module.exports = __WEBPACK_EXTERNAL_MODULE_0__;
|
||||||
|
|
||||||
|
/***/ }),
|
||||||
|
/* 1 */
|
||||||
|
/***/ (function(module, exports, __webpack_require__) {
|
||||||
|
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
|
||||||
|
var LayoutConstants = __webpack_require__(0).layoutBase.LayoutConstants;
|
||||||
|
var FDLayoutConstants = __webpack_require__(0).layoutBase.FDLayoutConstants;
|
||||||
|
var CoSEConstants = __webpack_require__(0).CoSEConstants;
|
||||||
|
var CoSELayout = __webpack_require__(0).CoSELayout;
|
||||||
|
var CoSENode = __webpack_require__(0).CoSENode;
|
||||||
|
var PointD = __webpack_require__(0).layoutBase.PointD;
|
||||||
|
var DimensionD = __webpack_require__(0).layoutBase.DimensionD;
|
||||||
|
|
||||||
|
var defaults = {
|
||||||
|
// Called on `layoutready`
|
||||||
|
ready: function ready() {},
|
||||||
|
// Called on `layoutstop`
|
||||||
|
stop: function stop() {},
|
||||||
|
// 'draft', 'default' or 'proof"
|
||||||
|
// - 'draft' fast cooling rate
|
||||||
|
// - 'default' moderate cooling rate
|
||||||
|
// - "proof" slow cooling rate
|
||||||
|
quality: 'default',
|
||||||
|
// include labels in node dimensions
|
||||||
|
nodeDimensionsIncludeLabels: false,
|
||||||
|
// number of ticks per frame; higher is faster but more jerky
|
||||||
|
refresh: 30,
|
||||||
|
// Whether to fit the network view after when done
|
||||||
|
fit: true,
|
||||||
|
// Padding on fit
|
||||||
|
padding: 10,
|
||||||
|
// Whether to enable incremental mode
|
||||||
|
randomize: true,
|
||||||
|
// Node repulsion (non overlapping) multiplier
|
||||||
|
nodeRepulsion: 4500,
|
||||||
|
// Ideal edge (non nested) length
|
||||||
|
idealEdgeLength: 50,
|
||||||
|
// Divisor to compute edge forces
|
||||||
|
edgeElasticity: 0.45,
|
||||||
|
// Nesting factor (multiplier) to compute ideal edge length for nested edges
|
||||||
|
nestingFactor: 0.1,
|
||||||
|
// Gravity force (constant)
|
||||||
|
gravity: 0.25,
|
||||||
|
// Maximum number of iterations to perform
|
||||||
|
numIter: 2500,
|
||||||
|
// For enabling tiling
|
||||||
|
tile: true,
|
||||||
|
// Type of layout animation. The option set is {'during', 'end', false}
|
||||||
|
animate: 'end',
|
||||||
|
// Duration for animate:end
|
||||||
|
animationDuration: 500,
|
||||||
|
// Represents the amount of the vertical space to put between the zero degree members during the tiling operation(can also be a function)
|
||||||
|
tilingPaddingVertical: 10,
|
||||||
|
// Represents the amount of the horizontal space to put between the zero degree members during the tiling operation(can also be a function)
|
||||||
|
tilingPaddingHorizontal: 10,
|
||||||
|
// Gravity range (constant) for compounds
|
||||||
|
gravityRangeCompound: 1.5,
|
||||||
|
// Gravity force (constant) for compounds
|
||||||
|
gravityCompound: 1.0,
|
||||||
|
// Gravity range (constant)
|
||||||
|
gravityRange: 3.8,
|
||||||
|
// Initial cooling factor for incremental layout
|
||||||
|
initialEnergyOnIncremental: 0.5
|
||||||
|
};
|
||||||
|
|
||||||
|
function extend(defaults, options) {
|
||||||
|
var obj = {};
|
||||||
|
|
||||||
|
for (var i in defaults) {
|
||||||
|
obj[i] = defaults[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var i in options) {
|
||||||
|
obj[i] = options[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
return obj;
|
||||||
|
};
|
||||||
|
|
||||||
|
function _CoSELayout(_options) {
|
||||||
|
this.options = extend(defaults, _options);
|
||||||
|
getUserOptions(this.options);
|
||||||
|
}
|
||||||
|
|
||||||
|
var getUserOptions = function getUserOptions(options) {
|
||||||
|
if (options.nodeRepulsion != null) CoSEConstants.DEFAULT_REPULSION_STRENGTH = FDLayoutConstants.DEFAULT_REPULSION_STRENGTH = options.nodeRepulsion;
|
||||||
|
if (options.idealEdgeLength != null) CoSEConstants.DEFAULT_EDGE_LENGTH = FDLayoutConstants.DEFAULT_EDGE_LENGTH = options.idealEdgeLength;
|
||||||
|
if (options.edgeElasticity != null) CoSEConstants.DEFAULT_SPRING_STRENGTH = FDLayoutConstants.DEFAULT_SPRING_STRENGTH = options.edgeElasticity;
|
||||||
|
if (options.nestingFactor != null) CoSEConstants.PER_LEVEL_IDEAL_EDGE_LENGTH_FACTOR = FDLayoutConstants.PER_LEVEL_IDEAL_EDGE_LENGTH_FACTOR = options.nestingFactor;
|
||||||
|
if (options.gravity != null) CoSEConstants.DEFAULT_GRAVITY_STRENGTH = FDLayoutConstants.DEFAULT_GRAVITY_STRENGTH = options.gravity;
|
||||||
|
if (options.numIter != null) CoSEConstants.MAX_ITERATIONS = FDLayoutConstants.MAX_ITERATIONS = options.numIter;
|
||||||
|
if (options.gravityRange != null) CoSEConstants.DEFAULT_GRAVITY_RANGE_FACTOR = FDLayoutConstants.DEFAULT_GRAVITY_RANGE_FACTOR = options.gravityRange;
|
||||||
|
if (options.gravityCompound != null) CoSEConstants.DEFAULT_COMPOUND_GRAVITY_STRENGTH = FDLayoutConstants.DEFAULT_COMPOUND_GRAVITY_STRENGTH = options.gravityCompound;
|
||||||
|
if (options.gravityRangeCompound != null) CoSEConstants.DEFAULT_COMPOUND_GRAVITY_RANGE_FACTOR = FDLayoutConstants.DEFAULT_COMPOUND_GRAVITY_RANGE_FACTOR = options.gravityRangeCompound;
|
||||||
|
if (options.initialEnergyOnIncremental != null) CoSEConstants.DEFAULT_COOLING_FACTOR_INCREMENTAL = FDLayoutConstants.DEFAULT_COOLING_FACTOR_INCREMENTAL = options.initialEnergyOnIncremental;
|
||||||
|
|
||||||
|
if (options.quality == 'draft') LayoutConstants.QUALITY = 0;else if (options.quality == 'proof') LayoutConstants.QUALITY = 2;else LayoutConstants.QUALITY = 1;
|
||||||
|
|
||||||
|
CoSEConstants.NODE_DIMENSIONS_INCLUDE_LABELS = FDLayoutConstants.NODE_DIMENSIONS_INCLUDE_LABELS = LayoutConstants.NODE_DIMENSIONS_INCLUDE_LABELS = options.nodeDimensionsIncludeLabels;
|
||||||
|
CoSEConstants.DEFAULT_INCREMENTAL = FDLayoutConstants.DEFAULT_INCREMENTAL = LayoutConstants.DEFAULT_INCREMENTAL = !options.randomize;
|
||||||
|
CoSEConstants.ANIMATE = FDLayoutConstants.ANIMATE = LayoutConstants.ANIMATE = options.animate;
|
||||||
|
CoSEConstants.TILE = options.tile;
|
||||||
|
CoSEConstants.TILING_PADDING_VERTICAL = typeof options.tilingPaddingVertical === 'function' ? options.tilingPaddingVertical.call() : options.tilingPaddingVertical;
|
||||||
|
CoSEConstants.TILING_PADDING_HORIZONTAL = typeof options.tilingPaddingHorizontal === 'function' ? options.tilingPaddingHorizontal.call() : options.tilingPaddingHorizontal;
|
||||||
|
};
|
||||||
|
|
||||||
|
_CoSELayout.prototype.run = function () {
|
||||||
|
var ready;
|
||||||
|
var frameId;
|
||||||
|
var options = this.options;
|
||||||
|
var idToLNode = this.idToLNode = {};
|
||||||
|
var layout = this.layout = new CoSELayout();
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
self.stopped = false;
|
||||||
|
|
||||||
|
this.cy = this.options.cy;
|
||||||
|
|
||||||
|
this.cy.trigger({ type: 'layoutstart', layout: this });
|
||||||
|
|
||||||
|
var gm = layout.newGraphManager();
|
||||||
|
this.gm = gm;
|
||||||
|
|
||||||
|
var nodes = this.options.eles.nodes();
|
||||||
|
var edges = this.options.eles.edges();
|
||||||
|
|
||||||
|
this.root = gm.addRoot();
|
||||||
|
this.processChildrenList(this.root, this.getTopMostNodes(nodes), layout);
|
||||||
|
|
||||||
|
for (var i = 0; i < edges.length; i++) {
|
||||||
|
var edge = edges[i];
|
||||||
|
var sourceNode = this.idToLNode[edge.data("source")];
|
||||||
|
var targetNode = this.idToLNode[edge.data("target")];
|
||||||
|
if (sourceNode !== targetNode && sourceNode.getEdgesBetween(targetNode).length == 0) {
|
||||||
|
var e1 = gm.add(layout.newEdge(), sourceNode, targetNode);
|
||||||
|
e1.id = edge.id();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var getPositions = function getPositions(ele, i) {
|
||||||
|
if (typeof ele === "number") {
|
||||||
|
ele = i;
|
||||||
|
}
|
||||||
|
var theId = ele.data('id');
|
||||||
|
var lNode = self.idToLNode[theId];
|
||||||
|
|
||||||
|
return {
|
||||||
|
x: lNode.getRect().getCenterX(),
|
||||||
|
y: lNode.getRect().getCenterY()
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Reposition nodes in iterations animatedly
|
||||||
|
*/
|
||||||
|
var iterateAnimated = function iterateAnimated() {
|
||||||
|
// Thigs to perform after nodes are repositioned on screen
|
||||||
|
var afterReposition = function afterReposition() {
|
||||||
|
if (options.fit) {
|
||||||
|
options.cy.fit(options.eles, options.padding);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ready) {
|
||||||
|
ready = true;
|
||||||
|
self.cy.one('layoutready', options.ready);
|
||||||
|
self.cy.trigger({ type: 'layoutready', layout: self });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var ticksPerFrame = self.options.refresh;
|
||||||
|
var isDone;
|
||||||
|
|
||||||
|
for (var i = 0; i < ticksPerFrame && !isDone; i++) {
|
||||||
|
isDone = self.stopped || self.layout.tick();
|
||||||
|
}
|
||||||
|
|
||||||
|
// If layout is done
|
||||||
|
if (isDone) {
|
||||||
|
// If the layout is not a sublayout and it is successful perform post layout.
|
||||||
|
if (layout.checkLayoutSuccess() && !layout.isSubLayout) {
|
||||||
|
layout.doPostLayout();
|
||||||
|
}
|
||||||
|
|
||||||
|
// If layout has a tilingPostLayout function property call it.
|
||||||
|
if (layout.tilingPostLayout) {
|
||||||
|
layout.tilingPostLayout();
|
||||||
|
}
|
||||||
|
|
||||||
|
layout.isLayoutFinished = true;
|
||||||
|
|
||||||
|
self.options.eles.nodes().positions(getPositions);
|
||||||
|
|
||||||
|
afterReposition();
|
||||||
|
|
||||||
|
// trigger layoutstop when the layout stops (e.g. finishes)
|
||||||
|
self.cy.one('layoutstop', self.options.stop);
|
||||||
|
self.cy.trigger({ type: 'layoutstop', layout: self });
|
||||||
|
|
||||||
|
if (frameId) {
|
||||||
|
cancelAnimationFrame(frameId);
|
||||||
|
}
|
||||||
|
|
||||||
|
ready = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var animationData = self.layout.getPositionsData(); // Get positions of layout nodes note that all nodes may not be layout nodes because of tiling
|
||||||
|
|
||||||
|
// Position nodes, for the nodes whose id does not included in data (because they are removed from their parents and included in dummy compounds)
|
||||||
|
// use position of their ancestors or dummy ancestors
|
||||||
|
options.eles.nodes().positions(function (ele, i) {
|
||||||
|
if (typeof ele === "number") {
|
||||||
|
ele = i;
|
||||||
|
}
|
||||||
|
// If ele is a compound node, then its position will be defined by its children
|
||||||
|
if (!ele.isParent()) {
|
||||||
|
var theId = ele.id();
|
||||||
|
var pNode = animationData[theId];
|
||||||
|
var temp = ele;
|
||||||
|
// If pNode is undefined search until finding position data of its first ancestor (It may be dummy as well)
|
||||||
|
while (pNode == null) {
|
||||||
|
pNode = animationData[temp.data('parent')] || animationData['DummyCompound_' + temp.data('parent')];
|
||||||
|
animationData[theId] = pNode;
|
||||||
|
temp = temp.parent()[0];
|
||||||
|
if (temp == undefined) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (pNode != null) {
|
||||||
|
return {
|
||||||
|
x: pNode.x,
|
||||||
|
y: pNode.y
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
x: ele.position('x'),
|
||||||
|
y: ele.position('y')
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
afterReposition();
|
||||||
|
|
||||||
|
frameId = requestAnimationFrame(iterateAnimated);
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Listen 'layoutstarted' event and start animated iteration if animate option is 'during'
|
||||||
|
*/
|
||||||
|
layout.addListener('layoutstarted', function () {
|
||||||
|
if (self.options.animate === 'during') {
|
||||||
|
frameId = requestAnimationFrame(iterateAnimated);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
layout.runLayout(); // Run cose layout
|
||||||
|
|
||||||
|
/*
|
||||||
|
* If animate option is not 'during' ('end' or false) perform these here (If it is 'during' similar things are already performed)
|
||||||
|
*/
|
||||||
|
if (this.options.animate !== "during") {
|
||||||
|
self.options.eles.nodes().not(":parent").layoutPositions(self, self.options, getPositions); // Use layout positions to reposition the nodes it considers the options parameter
|
||||||
|
ready = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this; // chaining
|
||||||
|
};
|
||||||
|
|
||||||
|
//Get the top most ones of a list of nodes
|
||||||
|
_CoSELayout.prototype.getTopMostNodes = function (nodes) {
|
||||||
|
var nodesMap = {};
|
||||||
|
for (var i = 0; i < nodes.length; i++) {
|
||||||
|
nodesMap[nodes[i].id()] = true;
|
||||||
|
}
|
||||||
|
var roots = nodes.filter(function (ele, i) {
|
||||||
|
if (typeof ele === "number") {
|
||||||
|
ele = i;
|
||||||
|
}
|
||||||
|
var parent = ele.parent()[0];
|
||||||
|
while (parent != null) {
|
||||||
|
if (nodesMap[parent.id()]) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
parent = parent.parent()[0];
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
return roots;
|
||||||
|
};
|
||||||
|
|
||||||
|
_CoSELayout.prototype.processChildrenList = function (parent, children, layout) {
|
||||||
|
var size = children.length;
|
||||||
|
for (var i = 0; i < size; i++) {
|
||||||
|
var theChild = children[i];
|
||||||
|
var children_of_children = theChild.children();
|
||||||
|
var theNode;
|
||||||
|
|
||||||
|
var dimensions = theChild.layoutDimensions({
|
||||||
|
nodeDimensionsIncludeLabels: this.options.nodeDimensionsIncludeLabels
|
||||||
|
});
|
||||||
|
|
||||||
|
if (theChild.outerWidth() != null && theChild.outerHeight() != null) {
|
||||||
|
theNode = parent.add(new CoSENode(layout.graphManager, new PointD(theChild.position('x') - dimensions.w / 2, theChild.position('y') - dimensions.h / 2), new DimensionD(parseFloat(dimensions.w), parseFloat(dimensions.h))));
|
||||||
|
} else {
|
||||||
|
theNode = parent.add(new CoSENode(this.graphManager));
|
||||||
|
}
|
||||||
|
// Attach id to the layout node
|
||||||
|
theNode.id = theChild.data("id");
|
||||||
|
// Attach the paddings of cy node to layout node
|
||||||
|
theNode.paddingLeft = parseInt(theChild.css('padding'));
|
||||||
|
theNode.paddingTop = parseInt(theChild.css('padding'));
|
||||||
|
theNode.paddingRight = parseInt(theChild.css('padding'));
|
||||||
|
theNode.paddingBottom = parseInt(theChild.css('padding'));
|
||||||
|
|
||||||
|
//Attach the label properties to compound if labels will be included in node dimensions
|
||||||
|
if (this.options.nodeDimensionsIncludeLabels) {
|
||||||
|
if (theChild.isParent()) {
|
||||||
|
var labelWidth = theChild.boundingBox({ includeLabels: true, includeNodes: false }).w;
|
||||||
|
var labelHeight = theChild.boundingBox({ includeLabels: true, includeNodes: false }).h;
|
||||||
|
var labelPos = theChild.css("text-halign");
|
||||||
|
theNode.labelWidth = labelWidth;
|
||||||
|
theNode.labelHeight = labelHeight;
|
||||||
|
theNode.labelPos = labelPos;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map the layout node
|
||||||
|
this.idToLNode[theChild.data("id")] = theNode;
|
||||||
|
|
||||||
|
if (isNaN(theNode.rect.x)) {
|
||||||
|
theNode.rect.x = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isNaN(theNode.rect.y)) {
|
||||||
|
theNode.rect.y = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (children_of_children != null && children_of_children.length > 0) {
|
||||||
|
var theNewGraph;
|
||||||
|
theNewGraph = layout.getGraphManager().add(layout.newGraph(), theNode);
|
||||||
|
this.processChildrenList(theNewGraph, children_of_children, layout);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief : called on continuous layouts to stop them before they finish
|
||||||
|
*/
|
||||||
|
_CoSELayout.prototype.stop = function () {
|
||||||
|
this.stopped = true;
|
||||||
|
|
||||||
|
return this; // chaining
|
||||||
|
};
|
||||||
|
|
||||||
|
var register = function register(cytoscape) {
|
||||||
|
// var Layout = getLayout( cytoscape );
|
||||||
|
|
||||||
|
cytoscape('layout', 'cose-bilkent', _CoSELayout);
|
||||||
|
};
|
||||||
|
|
||||||
|
// auto reg for globals
|
||||||
|
if (typeof cytoscape !== 'undefined') {
|
||||||
|
register(cytoscape);
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = register;
|
||||||
|
|
||||||
|
/***/ })
|
||||||
|
/******/ ]);
|
||||||
|
});
|
||||||
397
js/cytoscape-dagre.js
Executable file
397
js/cytoscape-dagre.js
Executable file
|
|
@ -0,0 +1,397 @@
|
||||||
|
(function webpackUniversalModuleDefinition(root, factory) {
|
||||||
|
if(typeof exports === 'object' && typeof module === 'object')
|
||||||
|
module.exports = factory(require("dagre"));
|
||||||
|
else if(typeof define === 'function' && define.amd)
|
||||||
|
define(["dagre"], factory);
|
||||||
|
else if(typeof exports === 'object')
|
||||||
|
exports["cytoscapeDagre"] = factory(require("dagre"));
|
||||||
|
else
|
||||||
|
root["cytoscapeDagre"] = factory(root["dagre"]);
|
||||||
|
})(this, function(__WEBPACK_EXTERNAL_MODULE__4__) {
|
||||||
|
return /******/ (function(modules) { // webpackBootstrap
|
||||||
|
/******/ // The module cache
|
||||||
|
/******/ var installedModules = {};
|
||||||
|
/******/
|
||||||
|
/******/ // The require function
|
||||||
|
/******/ function __webpack_require__(moduleId) {
|
||||||
|
/******/
|
||||||
|
/******/ // Check if module is in cache
|
||||||
|
/******/ if(installedModules[moduleId]) {
|
||||||
|
/******/ return installedModules[moduleId].exports;
|
||||||
|
/******/ }
|
||||||
|
/******/ // Create a new module (and put it into the cache)
|
||||||
|
/******/ var module = installedModules[moduleId] = {
|
||||||
|
/******/ i: moduleId,
|
||||||
|
/******/ l: false,
|
||||||
|
/******/ exports: {}
|
||||||
|
/******/ };
|
||||||
|
/******/
|
||||||
|
/******/ // Execute the module function
|
||||||
|
/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
|
||||||
|
/******/
|
||||||
|
/******/ // Flag the module as loaded
|
||||||
|
/******/ module.l = true;
|
||||||
|
/******/
|
||||||
|
/******/ // Return the exports of the module
|
||||||
|
/******/ return module.exports;
|
||||||
|
/******/ }
|
||||||
|
/******/
|
||||||
|
/******/
|
||||||
|
/******/ // expose the modules object (__webpack_modules__)
|
||||||
|
/******/ __webpack_require__.m = modules;
|
||||||
|
/******/
|
||||||
|
/******/ // expose the module cache
|
||||||
|
/******/ __webpack_require__.c = installedModules;
|
||||||
|
/******/
|
||||||
|
/******/ // define getter function for harmony exports
|
||||||
|
/******/ __webpack_require__.d = function(exports, name, getter) {
|
||||||
|
/******/ if(!__webpack_require__.o(exports, name)) {
|
||||||
|
/******/ Object.defineProperty(exports, name, { enumerable: true, get: getter });
|
||||||
|
/******/ }
|
||||||
|
/******/ };
|
||||||
|
/******/
|
||||||
|
/******/ // define __esModule on exports
|
||||||
|
/******/ __webpack_require__.r = function(exports) {
|
||||||
|
/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
|
||||||
|
/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
||||||
|
/******/ }
|
||||||
|
/******/ Object.defineProperty(exports, '__esModule', { value: true });
|
||||||
|
/******/ };
|
||||||
|
/******/
|
||||||
|
/******/ // create a fake namespace object
|
||||||
|
/******/ // mode & 1: value is a module id, require it
|
||||||
|
/******/ // mode & 2: merge all properties of value into the ns
|
||||||
|
/******/ // mode & 4: return value when already ns object
|
||||||
|
/******/ // mode & 8|1: behave like require
|
||||||
|
/******/ __webpack_require__.t = function(value, mode) {
|
||||||
|
/******/ if(mode & 1) value = __webpack_require__(value);
|
||||||
|
/******/ if(mode & 8) return value;
|
||||||
|
/******/ if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;
|
||||||
|
/******/ var ns = Object.create(null);
|
||||||
|
/******/ __webpack_require__.r(ns);
|
||||||
|
/******/ Object.defineProperty(ns, 'default', { enumerable: true, value: value });
|
||||||
|
/******/ if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));
|
||||||
|
/******/ return ns;
|
||||||
|
/******/ };
|
||||||
|
/******/
|
||||||
|
/******/ // getDefaultExport function for compatibility with non-harmony modules
|
||||||
|
/******/ __webpack_require__.n = function(module) {
|
||||||
|
/******/ var getter = module && module.__esModule ?
|
||||||
|
/******/ function getDefault() { return module['default']; } :
|
||||||
|
/******/ function getModuleExports() { return module; };
|
||||||
|
/******/ __webpack_require__.d(getter, 'a', getter);
|
||||||
|
/******/ return getter;
|
||||||
|
/******/ };
|
||||||
|
/******/
|
||||||
|
/******/ // Object.prototype.hasOwnProperty.call
|
||||||
|
/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
|
||||||
|
/******/
|
||||||
|
/******/ // __webpack_public_path__
|
||||||
|
/******/ __webpack_require__.p = "";
|
||||||
|
/******/
|
||||||
|
/******/
|
||||||
|
/******/ // Load entry module and return exports
|
||||||
|
/******/ return __webpack_require__(__webpack_require__.s = 0);
|
||||||
|
/******/ })
|
||||||
|
/************************************************************************/
|
||||||
|
/******/ ([
|
||||||
|
/* 0 */
|
||||||
|
/***/ (function(module, exports, __webpack_require__) {
|
||||||
|
|
||||||
|
var impl = __webpack_require__(1); // registers the extension on a cytoscape lib ref
|
||||||
|
|
||||||
|
|
||||||
|
var register = function register(cytoscape) {
|
||||||
|
if (!cytoscape) {
|
||||||
|
return;
|
||||||
|
} // can't register if cytoscape unspecified
|
||||||
|
|
||||||
|
|
||||||
|
cytoscape('layout', 'dagre', impl); // register with cytoscape.js
|
||||||
|
};
|
||||||
|
|
||||||
|
if (typeof cytoscape !== 'undefined') {
|
||||||
|
// expose to global cytoscape (i.e. window.cytoscape)
|
||||||
|
register(cytoscape);
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = register;
|
||||||
|
|
||||||
|
/***/ }),
|
||||||
|
/* 1 */
|
||||||
|
/***/ (function(module, exports, __webpack_require__) {
|
||||||
|
|
||||||
|
function _typeof(obj) { "@babel/helpers - typeof"; if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); }
|
||||||
|
|
||||||
|
var isFunction = function isFunction(o) {
|
||||||
|
return typeof o === 'function';
|
||||||
|
};
|
||||||
|
|
||||||
|
var defaults = __webpack_require__(2);
|
||||||
|
|
||||||
|
var assign = __webpack_require__(3);
|
||||||
|
|
||||||
|
var dagre = __webpack_require__(4); // constructor
|
||||||
|
// options : object containing layout options
|
||||||
|
|
||||||
|
|
||||||
|
function DagreLayout(options) {
|
||||||
|
this.options = assign({}, defaults, options);
|
||||||
|
} // runs the layout
|
||||||
|
|
||||||
|
|
||||||
|
DagreLayout.prototype.run = function () {
|
||||||
|
var options = this.options;
|
||||||
|
var layout = this;
|
||||||
|
var cy = options.cy; // cy is automatically populated for us in the constructor
|
||||||
|
|
||||||
|
var eles = options.eles;
|
||||||
|
|
||||||
|
var getVal = function getVal(ele, val) {
|
||||||
|
return isFunction(val) ? val.apply(ele, [ele]) : val;
|
||||||
|
};
|
||||||
|
|
||||||
|
var bb = options.boundingBox || {
|
||||||
|
x1: 0,
|
||||||
|
y1: 0,
|
||||||
|
w: cy.width(),
|
||||||
|
h: cy.height()
|
||||||
|
};
|
||||||
|
|
||||||
|
if (bb.x2 === undefined) {
|
||||||
|
bb.x2 = bb.x1 + bb.w;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bb.w === undefined) {
|
||||||
|
bb.w = bb.x2 - bb.x1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bb.y2 === undefined) {
|
||||||
|
bb.y2 = bb.y1 + bb.h;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bb.h === undefined) {
|
||||||
|
bb.h = bb.y2 - bb.y1;
|
||||||
|
}
|
||||||
|
|
||||||
|
var g = new dagre.graphlib.Graph({
|
||||||
|
multigraph: true,
|
||||||
|
compound: true
|
||||||
|
});
|
||||||
|
var gObj = {};
|
||||||
|
|
||||||
|
var setGObj = function setGObj(name, val) {
|
||||||
|
if (val != null) {
|
||||||
|
gObj[name] = val;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
setGObj('nodesep', options.nodeSep);
|
||||||
|
setGObj('edgesep', options.edgeSep);
|
||||||
|
setGObj('ranksep', options.rankSep);
|
||||||
|
setGObj('rankdir', options.rankDir);
|
||||||
|
setGObj('align', options.align);
|
||||||
|
setGObj('ranker', options.ranker);
|
||||||
|
setGObj('acyclicer', options.acyclicer);
|
||||||
|
g.setGraph(gObj);
|
||||||
|
g.setDefaultEdgeLabel(function () {
|
||||||
|
return {};
|
||||||
|
});
|
||||||
|
g.setDefaultNodeLabel(function () {
|
||||||
|
return {};
|
||||||
|
}); // add nodes to dagre
|
||||||
|
|
||||||
|
var nodes = eles.nodes();
|
||||||
|
|
||||||
|
if (isFunction(options.sort)) {
|
||||||
|
nodes = nodes.sort(options.sort);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var i = 0; i < nodes.length; i++) {
|
||||||
|
var node = nodes[i];
|
||||||
|
var nbb = node.layoutDimensions(options);
|
||||||
|
g.setNode(node.id(), {
|
||||||
|
width: nbb.w,
|
||||||
|
height: nbb.h,
|
||||||
|
name: node.id()
|
||||||
|
}); // console.log( g.node(node.id()) );
|
||||||
|
} // set compound parents
|
||||||
|
|
||||||
|
|
||||||
|
for (var _i = 0; _i < nodes.length; _i++) {
|
||||||
|
var _node = nodes[_i];
|
||||||
|
|
||||||
|
if (_node.isChild()) {
|
||||||
|
g.setParent(_node.id(), _node.parent().id());
|
||||||
|
}
|
||||||
|
} // add edges to dagre
|
||||||
|
|
||||||
|
|
||||||
|
var edges = eles.edges().stdFilter(function (edge) {
|
||||||
|
return !edge.source().isParent() && !edge.target().isParent(); // dagre can't handle edges on compound nodes
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isFunction(options.sort)) {
|
||||||
|
edges = edges.sort(options.sort);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var _i2 = 0; _i2 < edges.length; _i2++) {
|
||||||
|
var edge = edges[_i2];
|
||||||
|
g.setEdge(edge.source().id(), edge.target().id(), {
|
||||||
|
minlen: getVal(edge, options.minLen),
|
||||||
|
weight: getVal(edge, options.edgeWeight),
|
||||||
|
name: edge.id()
|
||||||
|
}, edge.id()); // console.log( g.edge(edge.source().id(), edge.target().id(), edge.id()) );
|
||||||
|
}
|
||||||
|
|
||||||
|
dagre.layout(g);
|
||||||
|
var gNodeIds = g.nodes();
|
||||||
|
|
||||||
|
for (var _i3 = 0; _i3 < gNodeIds.length; _i3++) {
|
||||||
|
var id = gNodeIds[_i3];
|
||||||
|
var n = g.node(id);
|
||||||
|
cy.getElementById(id).scratch().dagre = n;
|
||||||
|
}
|
||||||
|
|
||||||
|
var dagreBB;
|
||||||
|
|
||||||
|
if (options.boundingBox) {
|
||||||
|
dagreBB = {
|
||||||
|
x1: Infinity,
|
||||||
|
x2: -Infinity,
|
||||||
|
y1: Infinity,
|
||||||
|
y2: -Infinity
|
||||||
|
};
|
||||||
|
nodes.forEach(function (node) {
|
||||||
|
var dModel = node.scratch().dagre;
|
||||||
|
dagreBB.x1 = Math.min(dagreBB.x1, dModel.x);
|
||||||
|
dagreBB.x2 = Math.max(dagreBB.x2, dModel.x);
|
||||||
|
dagreBB.y1 = Math.min(dagreBB.y1, dModel.y);
|
||||||
|
dagreBB.y2 = Math.max(dagreBB.y2, dModel.y);
|
||||||
|
});
|
||||||
|
dagreBB.w = dagreBB.x2 - dagreBB.x1;
|
||||||
|
dagreBB.h = dagreBB.y2 - dagreBB.y1;
|
||||||
|
} else {
|
||||||
|
dagreBB = bb;
|
||||||
|
}
|
||||||
|
|
||||||
|
var constrainPos = function constrainPos(p) {
|
||||||
|
if (options.boundingBox) {
|
||||||
|
var xPct = dagreBB.w === 0 ? 0 : (p.x - dagreBB.x1) / dagreBB.w;
|
||||||
|
var yPct = dagreBB.h === 0 ? 0 : (p.y - dagreBB.y1) / dagreBB.h;
|
||||||
|
return {
|
||||||
|
x: bb.x1 + xPct * bb.w,
|
||||||
|
y: bb.y1 + yPct * bb.h
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
nodes.layoutPositions(layout, options, function (ele) {
|
||||||
|
ele = _typeof(ele) === "object" ? ele : this;
|
||||||
|
var dModel = ele.scratch().dagre;
|
||||||
|
return constrainPos({
|
||||||
|
x: dModel.x,
|
||||||
|
y: dModel.y
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return this; // chaining
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = DagreLayout;
|
||||||
|
|
||||||
|
/***/ }),
|
||||||
|
/* 2 */
|
||||||
|
/***/ (function(module, exports) {
|
||||||
|
|
||||||
|
var defaults = {
|
||||||
|
// dagre algo options, uses default value on undefined
|
||||||
|
nodeSep: undefined,
|
||||||
|
// the separation between adjacent nodes in the same rank
|
||||||
|
edgeSep: undefined,
|
||||||
|
// the separation between adjacent edges in the same rank
|
||||||
|
rankSep: undefined,
|
||||||
|
// the separation between adjacent nodes in the same rank
|
||||||
|
rankDir: undefined,
|
||||||
|
// 'TB' for top to bottom flow, 'LR' for left to right,
|
||||||
|
align: undefined,
|
||||||
|
// alignment for rank nodes. Can be 'UL', 'UR', 'DL', or 'DR', where U = up, D = down, L = left, and R = right
|
||||||
|
acyclicer: undefined,
|
||||||
|
// If set to 'greedy', uses a greedy heuristic for finding a feedback arc set for a graph.
|
||||||
|
// A feedback arc set is a set of edges that can be removed to make a graph acyclic.
|
||||||
|
ranker: undefined,
|
||||||
|
// Type of algorithm to assigns a rank to each node in the input graph.
|
||||||
|
// Possible values: network-simplex, tight-tree or longest-path
|
||||||
|
minLen: function minLen(edge) {
|
||||||
|
return 1;
|
||||||
|
},
|
||||||
|
// number of ranks to keep between the source and target of the edge
|
||||||
|
edgeWeight: function edgeWeight(edge) {
|
||||||
|
return 1;
|
||||||
|
},
|
||||||
|
// higher weight edges are generally made shorter and straighter than lower weight edges
|
||||||
|
// general layout options
|
||||||
|
fit: true,
|
||||||
|
// whether to fit to viewport
|
||||||
|
padding: 30,
|
||||||
|
// fit padding
|
||||||
|
spacingFactor: undefined,
|
||||||
|
// Applies a multiplicative factor (>0) to expand or compress the overall area that the nodes take up
|
||||||
|
nodeDimensionsIncludeLabels: false,
|
||||||
|
// whether labels should be included in determining the space used by a node
|
||||||
|
animate: false,
|
||||||
|
// whether to transition the node positions
|
||||||
|
animateFilter: function animateFilter(node, i) {
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
// whether to animate specific nodes when animation is on; non-animated nodes immediately go to their final positions
|
||||||
|
animationDuration: 500,
|
||||||
|
// duration of animation in ms if enabled
|
||||||
|
animationEasing: undefined,
|
||||||
|
// easing of animation if enabled
|
||||||
|
boundingBox: undefined,
|
||||||
|
// constrain layout bounds; { x1, y1, x2, y2 } or { x1, y1, w, h }
|
||||||
|
transform: function transform(node, pos) {
|
||||||
|
return pos;
|
||||||
|
},
|
||||||
|
// a function that applies a transform to the final node position
|
||||||
|
ready: function ready() {},
|
||||||
|
// on layoutready
|
||||||
|
sort: undefined,
|
||||||
|
// a sorting function to order the nodes and edges; e.g. function(a, b){ return a.data('weight') - b.data('weight') }
|
||||||
|
// because cytoscape dagre creates a directed graph, and directed graphs use the node order as a tie breaker when
|
||||||
|
// defining the topology of a graph, this sort function can help ensure the correct order of the nodes/edges.
|
||||||
|
// this feature is most useful when adding and removing the same nodes and edges multiple times in a graph.
|
||||||
|
stop: function stop() {} // on layoutstop
|
||||||
|
|
||||||
|
};
|
||||||
|
module.exports = defaults;
|
||||||
|
|
||||||
|
/***/ }),
|
||||||
|
/* 3 */
|
||||||
|
/***/ (function(module, exports) {
|
||||||
|
|
||||||
|
// Simple, internal Object.assign() polyfill for options objects etc.
|
||||||
|
module.exports = Object.assign != null ? Object.assign.bind(Object) : function (tgt) {
|
||||||
|
for (var _len = arguments.length, srcs = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
|
||||||
|
srcs[_key - 1] = arguments[_key];
|
||||||
|
}
|
||||||
|
|
||||||
|
srcs.forEach(function (src) {
|
||||||
|
Object.keys(src).forEach(function (k) {
|
||||||
|
return tgt[k] = src[k];
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return tgt;
|
||||||
|
};
|
||||||
|
|
||||||
|
/***/ }),
|
||||||
|
/* 4 */
|
||||||
|
/***/ (function(module, exports) {
|
||||||
|
|
||||||
|
module.exports = __WEBPACK_EXTERNAL_MODULE__4__;
|
||||||
|
|
||||||
|
/***/ })
|
||||||
|
/******/ ]);
|
||||||
|
});
|
||||||
32
js/cytoscape.min.js
vendored
Executable file
32
js/cytoscape.min.js
vendored
Executable file
File diff suppressed because one or more lines are too long
3809
js/dagre.min.js
vendored
Executable file
3809
js/dagre.min.js
vendored
Executable file
File diff suppressed because one or more lines are too long
11750
js/kundenkarte.js
Executable file
11750
js/kundenkarte.js
Executable file
File diff suppressed because it is too large
Load diff
1155
js/kundenkarte_cytoscape.js
Executable file
1155
js/kundenkarte_cytoscape.js
Executable file
File diff suppressed because it is too large
Load diff
5230
js/layout-base.js
Executable file
5230
js/layout-base.js
Executable file
File diff suppressed because it is too large
Load diff
1
js/pathfinding.min.js
vendored
Executable file
1
js/pathfinding.min.js
vendored
Executable file
File diff suppressed because one or more lines are too long
932
js/pwa.js
Normal file
932
js/pwa.js
Normal file
|
|
@ -0,0 +1,932 @@
|
||||||
|
/**
|
||||||
|
* KundenKarte PWA - Mobile Schaltschrank-Dokumentation
|
||||||
|
* Offline-First App für Elektriker
|
||||||
|
*/
|
||||||
|
|
||||||
|
(function() {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// APP STATE
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
const App = {
|
||||||
|
// Auth
|
||||||
|
token: null,
|
||||||
|
user: null,
|
||||||
|
|
||||||
|
// Current selection
|
||||||
|
customerId: null,
|
||||||
|
customerName: '',
|
||||||
|
anlageId: null,
|
||||||
|
anlageName: '',
|
||||||
|
|
||||||
|
// Data
|
||||||
|
panels: [],
|
||||||
|
carriers: [],
|
||||||
|
equipment: [],
|
||||||
|
equipmentTypes: [],
|
||||||
|
|
||||||
|
// Offline queue
|
||||||
|
offlineQueue: [],
|
||||||
|
isOnline: navigator.onLine,
|
||||||
|
|
||||||
|
// Current modal state
|
||||||
|
currentCarrierId: null,
|
||||||
|
selectedTypeId: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// INIT
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
// Register Service Worker
|
||||||
|
if ('serviceWorker' in navigator) {
|
||||||
|
navigator.serviceWorker.register('sw.js')
|
||||||
|
.then(reg => console.log('[PWA] Service Worker registered'))
|
||||||
|
.catch(err => console.error('[PWA] SW registration failed:', err));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check online status
|
||||||
|
window.addEventListener('online', () => {
|
||||||
|
App.isOnline = true;
|
||||||
|
hideOfflineBar();
|
||||||
|
syncOfflineChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
window.addEventListener('offline', () => {
|
||||||
|
App.isOnline = false;
|
||||||
|
showOfflineBar();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check stored auth
|
||||||
|
const storedToken = localStorage.getItem('kundenkarte_pwa_token');
|
||||||
|
const storedUser = localStorage.getItem('kundenkarte_pwa_user');
|
||||||
|
if (storedToken && storedUser) {
|
||||||
|
App.token = storedToken;
|
||||||
|
App.user = JSON.parse(storedUser);
|
||||||
|
showScreen('search');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load offline queue
|
||||||
|
const storedQueue = localStorage.getItem('kundenkarte_offline_queue');
|
||||||
|
if (storedQueue) {
|
||||||
|
App.offlineQueue = JSON.parse(storedQueue);
|
||||||
|
updateSyncBadge();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bind events
|
||||||
|
bindEvents();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// EVENTS
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
function bindEvents() {
|
||||||
|
// Login
|
||||||
|
$('#login-form').on('submit', handleLogin);
|
||||||
|
$('#btn-logout').on('click', handleLogout);
|
||||||
|
|
||||||
|
// Navigation
|
||||||
|
$('#btn-back-search').on('click', () => showScreen('search'));
|
||||||
|
$('#btn-back-anlagen').on('click', () => showScreen('anlagen'));
|
||||||
|
|
||||||
|
// Search
|
||||||
|
$('#search-customer').on('input', debounce(handleSearch, 300));
|
||||||
|
|
||||||
|
// Customer/Anlage selection
|
||||||
|
$('#customer-list').on('click', '.list-item', handleCustomerSelect);
|
||||||
|
$('#anlagen-list').on('click', '.anlage-card', handleAnlageSelect);
|
||||||
|
|
||||||
|
// Editor actions
|
||||||
|
$('#btn-add-panel').on('click', () => openModal('add-panel'));
|
||||||
|
$('#btn-save-panel').on('click', handleSavePanel);
|
||||||
|
|
||||||
|
$('#editor-content').on('click', '.btn-add-carrier', handleAddCarrier);
|
||||||
|
$('#btn-save-carrier').on('click', handleSaveCarrier);
|
||||||
|
|
||||||
|
$('#editor-content').on('click', '.btn-add-equipment', handleAddEquipment);
|
||||||
|
$('#editor-content').on('click', '.equipment-block', handleEquipmentClick);
|
||||||
|
|
||||||
|
// Equipment modal
|
||||||
|
$('#type-grid').on('click', '.type-btn', handleTypeSelect);
|
||||||
|
$('#btn-save-equipment').on('click', handleSaveEquipment);
|
||||||
|
$('#btn-cancel-equipment').on('click', () => closeModal('add-equipment'));
|
||||||
|
|
||||||
|
// TE buttons
|
||||||
|
$('.te-btn').on('click', function() {
|
||||||
|
$('.te-btn').removeClass('selected');
|
||||||
|
$(this).addClass('selected');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Modal close
|
||||||
|
$('.modal-close').on('click', function() {
|
||||||
|
$(this).closest('.modal').removeClass('active');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Sync button
|
||||||
|
$('#btn-sync').on('click', syncOfflineChanges);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// AUTH
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
async function handleLogin(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const user = $('#login-user').val().trim();
|
||||||
|
const pass = $('#login-pass').val();
|
||||||
|
|
||||||
|
if (!user || !pass) {
|
||||||
|
$('#login-error').text('Bitte Benutzername und Passwort eingeben');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$('#login-error').text('');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await apiCall('pwa_auth.php', {
|
||||||
|
action: 'login',
|
||||||
|
username: user,
|
||||||
|
password: pass
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
App.token = response.token;
|
||||||
|
App.user = response.user;
|
||||||
|
localStorage.setItem('kundenkarte_pwa_token', response.token);
|
||||||
|
localStorage.setItem('kundenkarte_pwa_user', JSON.stringify(response.user));
|
||||||
|
showScreen('search');
|
||||||
|
} else {
|
||||||
|
$('#login-error').text(response.error || 'Login fehlgeschlagen');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
$('#login-error').text('Verbindungsfehler');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleLogout() {
|
||||||
|
App.token = null;
|
||||||
|
App.user = null;
|
||||||
|
localStorage.removeItem('kundenkarte_pwa_token');
|
||||||
|
localStorage.removeItem('kundenkarte_pwa_user');
|
||||||
|
showScreen('login');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// SCREENS
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
function showScreen(name) {
|
||||||
|
$('.screen').removeClass('active');
|
||||||
|
$('#screen-' + name).addClass('active');
|
||||||
|
|
||||||
|
// Load data if needed
|
||||||
|
if (name === 'search') {
|
||||||
|
$('#search-customer').val('').focus();
|
||||||
|
$('#customer-list').html('<div class="list-empty">Suchbegriff eingeben...</div>');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// CUSTOMER SEARCH
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
async function handleSearch() {
|
||||||
|
const query = $('#search-customer').val().trim();
|
||||||
|
if (query.length < 2) {
|
||||||
|
$('#customer-list').html('<div class="list-empty">Mindestens 2 Zeichen eingeben...</div>');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$('#customer-list').html('<div class="loading-container"><div class="spinner"></div></div>');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await apiCall('ajax/pwa_api.php', {
|
||||||
|
action: 'search_customers',
|
||||||
|
query: query
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.success && response.customers) {
|
||||||
|
renderCustomerList(response.customers);
|
||||||
|
} else {
|
||||||
|
$('#customer-list').html('<div class="list-empty">Keine Kunden gefunden</div>');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
$('#customer-list').html('<div class="list-empty">Fehler bei der Suche</div>');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderCustomerList(customers) {
|
||||||
|
if (!customers.length) {
|
||||||
|
$('#customer-list').html('<div class="list-empty">Keine Kunden gefunden</div>');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let html = '';
|
||||||
|
customers.forEach(c => {
|
||||||
|
html += `
|
||||||
|
<div class="list-item" data-id="${c.id}">
|
||||||
|
<div class="list-item-icon">
|
||||||
|
<svg viewBox="0 0 24 24"><path d="M12 7V3H2v18h20V7H12zM6 19H4v-2h2v2zm0-4H4v-2h2v2zm0-4H4V9h2v2zm0-4H4V5h2v2zm4 12H8v-2h2v2zm0-4H8v-2h2v2zm0-4H8V9h2v2zm0-4H8V5h2v2zm10 12h-8v-2h2v-2h-2v-2h2v-2h-2V9h8v10zm-2-8h-2v2h2v-2zm0 4h-2v2h2v-2z"/></svg>
|
||||||
|
</div>
|
||||||
|
<div class="list-item-content">
|
||||||
|
<div class="list-item-title">${escapeHtml(c.name)}</div>
|
||||||
|
<div class="list-item-subtitle">${escapeHtml(c.town || '')}</div>
|
||||||
|
</div>
|
||||||
|
<svg class="list-item-arrow" viewBox="0 0 24 24"><path d="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z"/></svg>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#customer-list').html(html);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// CUSTOMER & ANLAGE SELECTION
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
async function handleCustomerSelect() {
|
||||||
|
const id = $(this).data('id');
|
||||||
|
const name = $(this).find('.list-item-title').text();
|
||||||
|
|
||||||
|
App.customerId = id;
|
||||||
|
App.customerName = name;
|
||||||
|
$('#customer-name').text(name);
|
||||||
|
|
||||||
|
showScreen('anlagen');
|
||||||
|
$('#anlagen-list').html('<div class="loading-container"><div class="spinner"></div></div>');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await apiCall('ajax/pwa_api.php', {
|
||||||
|
action: 'get_anlagen',
|
||||||
|
customer_id: id
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.success && response.anlagen) {
|
||||||
|
renderAnlagenList(response.anlagen);
|
||||||
|
// Cache for offline
|
||||||
|
localStorage.setItem('kundenkarte_anlagen_' + id, JSON.stringify(response.anlagen));
|
||||||
|
} else {
|
||||||
|
$('#anlagen-list').html('<div class="list-empty">Keine Anlagen gefunden</div>');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
// Try cached
|
||||||
|
const cached = localStorage.getItem('kundenkarte_anlagen_' + id);
|
||||||
|
if (cached) {
|
||||||
|
renderAnlagenList(JSON.parse(cached));
|
||||||
|
showToast('Offline - Zeige gecachte Daten', 'warning');
|
||||||
|
} else {
|
||||||
|
$('#anlagen-list').html('<div class="list-empty">Fehler beim Laden</div>');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderAnlagenList(anlagen) {
|
||||||
|
// Filter nur Anlagen mit Editor
|
||||||
|
const withEditor = anlagen.filter(a => a.has_editor);
|
||||||
|
|
||||||
|
if (!withEditor.length) {
|
||||||
|
$('#anlagen-list').html('<div class="list-empty">Keine Anlagen mit Schaltplan-Editor</div>');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let html = '';
|
||||||
|
withEditor.forEach(a => {
|
||||||
|
html += `
|
||||||
|
<div class="anlage-card" data-id="${a.id}">
|
||||||
|
<div class="anlage-card-icon">
|
||||||
|
<svg viewBox="0 0 24 24"><path d="M21 3H3c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h18c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 16H3V5h18v14zM9 7H7v2h2V7zm0 4H7v2h2v-2zm0 4H7v2h2v-2zm8-8h-6v2h6V7zm0 4h-6v2h6v-2zm0 4h-6v2h6v-2z"/></svg>
|
||||||
|
</div>
|
||||||
|
<div class="anlage-card-title">${escapeHtml(a.label || 'Anlage ' + a.id)}</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#anlagen-list').html(html);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleAnlageSelect() {
|
||||||
|
const id = $(this).data('id');
|
||||||
|
const name = $(this).find('.anlage-card-title').text();
|
||||||
|
|
||||||
|
App.anlageId = id;
|
||||||
|
App.anlageName = name;
|
||||||
|
$('#anlage-name').text(name);
|
||||||
|
|
||||||
|
showScreen('editor');
|
||||||
|
await loadEditorData();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// EDITOR
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
async function loadEditorData() {
|
||||||
|
$('#editor-content').html('<div class="loading-container"><div class="spinner"></div><div class="text-muted">Lade Daten...</div></div>');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await apiCall('ajax/pwa_api.php', {
|
||||||
|
action: 'get_anlage_data',
|
||||||
|
anlage_id: App.anlageId
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
App.panels = response.panels || [];
|
||||||
|
App.carriers = response.carriers || [];
|
||||||
|
App.equipment = response.equipment || [];
|
||||||
|
App.equipmentTypes = response.types || [];
|
||||||
|
|
||||||
|
// Cache for offline
|
||||||
|
localStorage.setItem('kundenkarte_data_' + App.anlageId, JSON.stringify({
|
||||||
|
panels: App.panels,
|
||||||
|
carriers: App.carriers,
|
||||||
|
equipment: App.equipment,
|
||||||
|
types: App.equipmentTypes
|
||||||
|
}));
|
||||||
|
|
||||||
|
renderEditor();
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
// Try cached
|
||||||
|
const cached = localStorage.getItem('kundenkarte_data_' + App.anlageId);
|
||||||
|
if (cached) {
|
||||||
|
const data = JSON.parse(cached);
|
||||||
|
App.panels = data.panels || [];
|
||||||
|
App.carriers = data.carriers || [];
|
||||||
|
App.equipment = data.equipment || [];
|
||||||
|
App.equipmentTypes = data.types || [];
|
||||||
|
renderEditor();
|
||||||
|
showToast('Offline - Zeige gecachte Daten', 'warning');
|
||||||
|
} else {
|
||||||
|
$('#editor-content').html('<div class="list-empty">Fehler beim Laden</div>');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderEditor() {
|
||||||
|
if (!App.panels.length) {
|
||||||
|
$('#editor-content').html('<div class="list-empty">Noch keine Felder angelegt.<br>Tippe auf "+ Feld" um zu beginnen.</div>');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let html = '';
|
||||||
|
|
||||||
|
App.panels.forEach(panel => {
|
||||||
|
const panelCarriers = App.carriers.filter(c => c.fk_panel == panel.id);
|
||||||
|
|
||||||
|
html += `
|
||||||
|
<div class="panel-card" data-panel-id="${panel.id}">
|
||||||
|
<div class="panel-header">
|
||||||
|
<div class="panel-title">${escapeHtml(panel.label || 'Feld ' + panel.id)}</div>
|
||||||
|
</div>
|
||||||
|
<div class="panel-body">
|
||||||
|
`;
|
||||||
|
|
||||||
|
panelCarriers.forEach(carrier => {
|
||||||
|
const carrierEquipment = App.equipment.filter(e => e.fk_carrier == carrier.id);
|
||||||
|
carrierEquipment.sort((a, b) => (a.position_te || 0) - (b.position_te || 0));
|
||||||
|
|
||||||
|
html += `
|
||||||
|
<div class="carrier-item" data-carrier-id="${carrier.id}">
|
||||||
|
<div class="carrier-header">
|
||||||
|
<span class="carrier-label">${escapeHtml(carrier.label || 'Hutschiene')}</span>
|
||||||
|
<span class="carrier-te">${carrier.total_te || 12} TE</span>
|
||||||
|
</div>
|
||||||
|
<div class="carrier-body">
|
||||||
|
`;
|
||||||
|
|
||||||
|
carrierEquipment.forEach(eq => {
|
||||||
|
const type = App.equipmentTypes.find(t => t.id == eq.fk_equipment_type);
|
||||||
|
const typeLabel = type ? (type.label_short || type.ref) : '?';
|
||||||
|
const fieldVals = eq.field_values ? (typeof eq.field_values === 'string' ? JSON.parse(eq.field_values) : eq.field_values) : {};
|
||||||
|
const value = fieldVals.ampere ? fieldVals.ampere + 'A' : (fieldVals.characteristic || '');
|
||||||
|
|
||||||
|
html += `
|
||||||
|
<div class="equipment-block" data-equipment-id="${eq.id}" style="background:${type?.color || '#3498db'}">
|
||||||
|
<div class="equipment-block-type">${escapeHtml(typeLabel)}</div>
|
||||||
|
<div class="equipment-block-value">${escapeHtml(value)}</div>
|
||||||
|
${eq.label ? '<div class="equipment-block-label">' + escapeHtml(eq.label) + '</div>' : ''}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
|
||||||
|
html += `
|
||||||
|
<button class="btn-add-equipment" data-carrier-id="${carrier.id}">
|
||||||
|
<svg viewBox="0 0 24 24"><path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/></svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
|
||||||
|
html += `
|
||||||
|
<button class="btn-add-carrier" data-panel-id="${panel.id}">
|
||||||
|
<svg viewBox="0 0 24 24"><path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/></svg>
|
||||||
|
Hutschiene hinzufügen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#editor-content').html(html);
|
||||||
|
|
||||||
|
// Load type grid
|
||||||
|
renderTypeGrid();
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderTypeGrid() {
|
||||||
|
let html = '';
|
||||||
|
App.equipmentTypes.forEach(type => {
|
||||||
|
html += `
|
||||||
|
<button class="type-btn" data-type-id="${type.id}" data-width="${type.width_te || 1}">
|
||||||
|
<div class="type-btn-icon" style="color:${type.color || '#3498db'}">⚡</div>
|
||||||
|
<div class="type-btn-label">${escapeHtml(type.label_short || type.ref || type.label)}</div>
|
||||||
|
</button>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
$('#type-grid').html(html);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// PANEL (FELD) ACTIONS
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
async function handleSavePanel() {
|
||||||
|
const label = $('#panel-label').val().trim() || 'Feld ' + (App.panels.length + 1);
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
action: 'create_panel',
|
||||||
|
anlage_id: App.anlageId,
|
||||||
|
label: label
|
||||||
|
};
|
||||||
|
|
||||||
|
closeModal('add-panel');
|
||||||
|
$('#panel-label').val('');
|
||||||
|
|
||||||
|
if (App.isOnline) {
|
||||||
|
try {
|
||||||
|
const response = await apiCall('ajax/pwa_api.php', data);
|
||||||
|
if (response.success) {
|
||||||
|
App.panels.push({ id: response.panel_id, label: label });
|
||||||
|
renderEditor();
|
||||||
|
showToast('Feld angelegt');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
queueOfflineAction(data);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
queueOfflineAction(data);
|
||||||
|
// Optimistic UI
|
||||||
|
App.panels.push({ id: 'temp_' + Date.now(), label: label });
|
||||||
|
renderEditor();
|
||||||
|
showToast('Feld wird synchronisiert...', 'warning');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// CARRIER (HUTSCHIENE) ACTIONS
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
function handleAddCarrier() {
|
||||||
|
const panelId = $(this).data('panel-id');
|
||||||
|
App.currentPanelId = panelId;
|
||||||
|
$('.te-btn').removeClass('selected');
|
||||||
|
$('#carrier-label').val('');
|
||||||
|
openModal('add-carrier');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSaveCarrier() {
|
||||||
|
const teBtn = $('.te-btn.selected');
|
||||||
|
if (!teBtn.length) {
|
||||||
|
showToast('Bitte Größe wählen', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalTe = parseInt(teBtn.data('te'));
|
||||||
|
const label = $('#carrier-label').val().trim() || 'Hutschiene';
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
action: 'create_carrier',
|
||||||
|
panel_id: App.currentPanelId,
|
||||||
|
total_te: totalTe,
|
||||||
|
label: label
|
||||||
|
};
|
||||||
|
|
||||||
|
closeModal('add-carrier');
|
||||||
|
|
||||||
|
if (App.isOnline) {
|
||||||
|
try {
|
||||||
|
const response = await apiCall('ajax/pwa_api.php', data);
|
||||||
|
if (response.success) {
|
||||||
|
App.carriers.push({
|
||||||
|
id: response.carrier_id,
|
||||||
|
fk_panel: App.currentPanelId,
|
||||||
|
total_te: totalTe,
|
||||||
|
label: label
|
||||||
|
});
|
||||||
|
renderEditor();
|
||||||
|
showToast('Hutschiene angelegt');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
queueOfflineAction(data);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
queueOfflineAction(data);
|
||||||
|
App.carriers.push({
|
||||||
|
id: 'temp_' + Date.now(),
|
||||||
|
fk_panel: App.currentPanelId,
|
||||||
|
total_te: totalTe,
|
||||||
|
label: label
|
||||||
|
});
|
||||||
|
renderEditor();
|
||||||
|
showToast('Wird synchronisiert...', 'warning');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// EQUIPMENT (AUTOMAT) ACTIONS
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
function handleAddEquipment() {
|
||||||
|
const carrierId = $(this).data('carrier-id');
|
||||||
|
App.currentCarrierId = carrierId;
|
||||||
|
App.selectedTypeId = null;
|
||||||
|
|
||||||
|
// Reset modal
|
||||||
|
$('.type-btn').removeClass('selected');
|
||||||
|
$('#step-type').addClass('active');
|
||||||
|
$('#step-values').removeClass('active');
|
||||||
|
$('#equipment-label').val('');
|
||||||
|
$('#value-fields').html('');
|
||||||
|
|
||||||
|
openModal('add-equipment');
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleTypeSelect() {
|
||||||
|
$('.type-btn').removeClass('selected');
|
||||||
|
$(this).addClass('selected');
|
||||||
|
|
||||||
|
App.selectedTypeId = $(this).data('type-id');
|
||||||
|
const type = App.equipmentTypes.find(t => t.id == App.selectedTypeId);
|
||||||
|
|
||||||
|
// Show value step
|
||||||
|
$('#step-type').removeClass('active');
|
||||||
|
$('#step-values').addClass('active');
|
||||||
|
|
||||||
|
// Build value fields based on type
|
||||||
|
let html = '';
|
||||||
|
|
||||||
|
// Quick select for common values
|
||||||
|
if (type && (type.ref?.includes('LS') || type.label?.includes('Leitungsschutz'))) {
|
||||||
|
html += '<p class="step-label">Kennlinie + Ampere:</p>';
|
||||||
|
html += '<div class="value-quick">';
|
||||||
|
['B6', 'B10', 'B13', 'B16', 'B20', 'B25', 'B32', 'C6', 'C10', 'C13', 'C16', 'C20', 'C25', 'C32'].forEach(v => {
|
||||||
|
html += `<button type="button" class="value-chip" data-char="${v[0]}" data-amp="${v.slice(1)}">${v}</button>`;
|
||||||
|
});
|
||||||
|
html += '</div>';
|
||||||
|
} else if (type && (type.ref?.includes('FI') || type.label?.includes('RCD'))) {
|
||||||
|
html += '<p class="step-label">Ampere:</p>';
|
||||||
|
html += '<div class="value-quick">';
|
||||||
|
['25', '40', '63', '80'].forEach(v => {
|
||||||
|
html += `<button type="button" class="value-chip" data-amp="${v}">${v}A</button>`;
|
||||||
|
});
|
||||||
|
html += '</div>';
|
||||||
|
html += '<p class="step-label" style="margin-top:12px;">Empfindlichkeit:</p>';
|
||||||
|
html += '<div class="value-quick">';
|
||||||
|
['30', '100', '300'].forEach(v => {
|
||||||
|
html += `<button type="button" class="value-chip chip-sens" data-sens="${v}">${v}mA</button>`;
|
||||||
|
});
|
||||||
|
html += '</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
$('#value-fields').html(html);
|
||||||
|
|
||||||
|
// Bind chip clicks
|
||||||
|
$('#value-fields .value-chip').on('click', function() {
|
||||||
|
if ($(this).hasClass('chip-sens')) {
|
||||||
|
$('.chip-sens').removeClass('selected');
|
||||||
|
} else {
|
||||||
|
$('.value-chip:not(.chip-sens)').removeClass('selected');
|
||||||
|
}
|
||||||
|
$(this).addClass('selected');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSaveEquipment() {
|
||||||
|
if (!App.selectedTypeId) {
|
||||||
|
showToast('Bitte Typ wählen', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const type = App.equipmentTypes.find(t => t.id == App.selectedTypeId);
|
||||||
|
const label = $('#equipment-label').val().trim();
|
||||||
|
|
||||||
|
// Collect field values
|
||||||
|
const fieldValues = {};
|
||||||
|
const selectedChip = $('.value-chip.selected:not(.chip-sens)');
|
||||||
|
const selectedSens = $('.chip-sens.selected');
|
||||||
|
|
||||||
|
if (selectedChip.length) {
|
||||||
|
if (selectedChip.data('char')) fieldValues.characteristic = selectedChip.data('char');
|
||||||
|
if (selectedChip.data('amp')) fieldValues.ampere = selectedChip.data('amp');
|
||||||
|
}
|
||||||
|
if (selectedSens.length) {
|
||||||
|
fieldValues.sensitivity = selectedSens.data('sens');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate position
|
||||||
|
const carrierEquipment = App.equipment.filter(e => e.fk_carrier == App.currentCarrierId);
|
||||||
|
let nextPos = 1;
|
||||||
|
carrierEquipment.forEach(e => {
|
||||||
|
const endPos = (parseInt(e.position_te) || 1) + (parseInt(e.width_te) || 1);
|
||||||
|
if (endPos > nextPos) nextPos = endPos;
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
action: 'create_equipment',
|
||||||
|
carrier_id: App.currentCarrierId,
|
||||||
|
type_id: App.selectedTypeId,
|
||||||
|
label: label,
|
||||||
|
position_te: nextPos,
|
||||||
|
field_values: JSON.stringify(fieldValues)
|
||||||
|
};
|
||||||
|
|
||||||
|
closeModal('add-equipment');
|
||||||
|
|
||||||
|
if (App.isOnline) {
|
||||||
|
try {
|
||||||
|
const response = await apiCall('ajax/pwa_api.php', data);
|
||||||
|
if (response.success) {
|
||||||
|
App.equipment.push({
|
||||||
|
id: response.equipment_id,
|
||||||
|
fk_carrier: App.currentCarrierId,
|
||||||
|
fk_equipment_type: App.selectedTypeId,
|
||||||
|
label: label,
|
||||||
|
position_te: nextPos,
|
||||||
|
width_te: type?.width_te || 1,
|
||||||
|
field_values: fieldValues
|
||||||
|
});
|
||||||
|
renderEditor();
|
||||||
|
showToast('Automat angelegt');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
queueOfflineAction(data);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
queueOfflineAction(data);
|
||||||
|
App.equipment.push({
|
||||||
|
id: 'temp_' + Date.now(),
|
||||||
|
fk_carrier: App.currentCarrierId,
|
||||||
|
fk_equipment_type: App.selectedTypeId,
|
||||||
|
label: label,
|
||||||
|
position_te: nextPos,
|
||||||
|
width_te: type?.width_te || 1,
|
||||||
|
field_values: fieldValues
|
||||||
|
});
|
||||||
|
renderEditor();
|
||||||
|
showToast('Wird synchronisiert...', 'warning');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleEquipmentClick() {
|
||||||
|
const eqId = $(this).data('equipment-id');
|
||||||
|
// TODO: Edit/Delete popup
|
||||||
|
showToast('Bearbeiten kommt noch...');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// OFFLINE SYNC
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
function queueOfflineAction(data) {
|
||||||
|
data._timestamp = Date.now();
|
||||||
|
App.offlineQueue.push(data);
|
||||||
|
localStorage.setItem('kundenkarte_offline_queue', JSON.stringify(App.offlineQueue));
|
||||||
|
updateSyncBadge();
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateSyncBadge() {
|
||||||
|
const count = App.offlineQueue.length;
|
||||||
|
const $badge = $('#sync-badge');
|
||||||
|
if (count > 0) {
|
||||||
|
$badge.text(count).removeClass('hidden');
|
||||||
|
} else {
|
||||||
|
$badge.addClass('hidden');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function syncOfflineChanges() {
|
||||||
|
if (!App.offlineQueue.length) {
|
||||||
|
showToast('Alles synchronisiert');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!App.isOnline) {
|
||||||
|
showToast('Offline - Sync nicht möglich', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
showToast('Synchronisiere...');
|
||||||
|
|
||||||
|
const queue = [...App.offlineQueue];
|
||||||
|
let successCount = 0;
|
||||||
|
|
||||||
|
for (const data of queue) {
|
||||||
|
try {
|
||||||
|
const response = await apiCall('ajax/pwa_api.php', data);
|
||||||
|
if (response.success) {
|
||||||
|
successCount++;
|
||||||
|
// Remove from queue
|
||||||
|
const idx = App.offlineQueue.findIndex(q => q._timestamp === data._timestamp);
|
||||||
|
if (idx > -1) App.offlineQueue.splice(idx, 1);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Sync failed for:', data, err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
localStorage.setItem('kundenkarte_offline_queue', JSON.stringify(App.offlineQueue));
|
||||||
|
updateSyncBadge();
|
||||||
|
|
||||||
|
if (successCount === queue.length) {
|
||||||
|
showToast('Alle Änderungen synchronisiert', 'success');
|
||||||
|
// Reload data
|
||||||
|
loadEditorData();
|
||||||
|
} else {
|
||||||
|
showToast(`${successCount}/${queue.length} synchronisiert`, 'warning');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// API HELPER
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
async function apiCall(endpoint, data = {}) {
|
||||||
|
const url = window.MODULE_URL + '/' + endpoint;
|
||||||
|
|
||||||
|
// Add token
|
||||||
|
if (App.token) {
|
||||||
|
data.token = App.token;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
},
|
||||||
|
body: new URLSearchParams(data)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Network error');
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// UI HELPERS
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
function openModal(name) {
|
||||||
|
$('#modal-' + name).addClass('active');
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeModal(name) {
|
||||||
|
$('#modal-' + name).removeClass('active');
|
||||||
|
}
|
||||||
|
|
||||||
|
function showToast(message, type = '') {
|
||||||
|
const $toast = $('#toast');
|
||||||
|
$toast.text(message).removeClass('success error warning visible').addClass(type);
|
||||||
|
setTimeout(() => $toast.addClass('visible'), 10);
|
||||||
|
setTimeout(() => $toast.removeClass('visible'), 3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function showOfflineBar() {
|
||||||
|
$('#offline-indicator').removeClass('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideOfflineBar() {
|
||||||
|
$('#offline-indicator').addClass('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeHtml(text) {
|
||||||
|
if (!text) return '';
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.textContent = text;
|
||||||
|
return div.innerHTML;
|
||||||
|
}
|
||||||
|
|
||||||
|
function debounce(func, wait) {
|
||||||
|
let timeout;
|
||||||
|
return function(...args) {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
timeout = setTimeout(() => func.apply(this, args), wait);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// jQuery shorthand
|
||||||
|
function $(selector) {
|
||||||
|
if (typeof selector === 'string') {
|
||||||
|
const elements = document.querySelectorAll(selector);
|
||||||
|
return new ElementCollection(elements);
|
||||||
|
}
|
||||||
|
return new ElementCollection([selector]);
|
||||||
|
}
|
||||||
|
|
||||||
|
class ElementCollection {
|
||||||
|
constructor(elements) {
|
||||||
|
this.elements = Array.from(elements);
|
||||||
|
this.length = this.elements.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
on(event, selectorOrHandler, handler) {
|
||||||
|
if (typeof selectorOrHandler === 'function') {
|
||||||
|
// Direct event
|
||||||
|
this.elements.forEach(el => el.addEventListener(event, selectorOrHandler));
|
||||||
|
} else {
|
||||||
|
// Delegated event
|
||||||
|
this.elements.forEach(el => {
|
||||||
|
el.addEventListener(event, function(e) {
|
||||||
|
const target = e.target.closest(selectorOrHandler);
|
||||||
|
if (target && el.contains(target)) {
|
||||||
|
handler.call(target, e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
addClass(className) {
|
||||||
|
this.elements.forEach(el => el.classList.add(className));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
removeClass(className) {
|
||||||
|
this.elements.forEach(el => el.classList.remove(className));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
hasClass(className) {
|
||||||
|
return this.elements[0]?.classList.contains(className);
|
||||||
|
}
|
||||||
|
|
||||||
|
html(content) {
|
||||||
|
if (content === undefined) {
|
||||||
|
return this.elements[0]?.innerHTML;
|
||||||
|
}
|
||||||
|
this.elements.forEach(el => el.innerHTML = content);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
text(content) {
|
||||||
|
if (content === undefined) {
|
||||||
|
return this.elements[0]?.textContent;
|
||||||
|
}
|
||||||
|
this.elements.forEach(el => el.textContent = content);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
val(value) {
|
||||||
|
if (value === undefined) {
|
||||||
|
return this.elements[0]?.value;
|
||||||
|
}
|
||||||
|
this.elements.forEach(el => el.value = value);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
data(key) {
|
||||||
|
return this.elements[0]?.dataset[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
find(selector) {
|
||||||
|
const found = [];
|
||||||
|
this.elements.forEach(el => {
|
||||||
|
found.push(...el.querySelectorAll(selector));
|
||||||
|
});
|
||||||
|
return new ElementCollection(found);
|
||||||
|
}
|
||||||
|
|
||||||
|
closest(selector) {
|
||||||
|
return new ElementCollection([this.elements[0]?.closest(selector)].filter(Boolean));
|
||||||
|
}
|
||||||
|
|
||||||
|
focus() {
|
||||||
|
this.elements[0]?.focus();
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// START
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', init);
|
||||||
|
|
||||||
|
})();
|
||||||
259
kundenkarteindex.php
Executable file
259
kundenkarteindex.php
Executable file
|
|
@ -0,0 +1,259 @@
|
||||||
|
<?php
|
||||||
|
/* Copyright (C) 2001-2005 Rodolphe Quiedeville <rodolphe@quiedeville.org>
|
||||||
|
* Copyright (C) 2004-2015 Laurent Destailleur <eldy@users.sourceforge.net>
|
||||||
|
* Copyright (C) 2005-2012 Regis Houssin <regis.houssin@inodbox.com>
|
||||||
|
* Copyright (C) 2015 Jean-François Ferry <jfefe@aternatik.fr>
|
||||||
|
* Copyright (C) 2024 Frédéric France <frederic.france@free.fr>
|
||||||
|
* Copyright (C) 2026 Eduard Wisch <data@data-it-solution.de>
|
||||||
|
*
|
||||||
|
* This program is free software; you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation; either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* \file kundenkarte/kundenkarteindex.php
|
||||||
|
* \ingroup kundenkarte
|
||||||
|
* \brief Home page of kundenkarte top menu
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Load Dolibarr environment
|
||||||
|
$res = 0;
|
||||||
|
// Try main.inc.php into web root known defined into CONTEXT_DOCUMENT_ROOT (not always defined)
|
||||||
|
if (!$res && !empty($_SERVER["CONTEXT_DOCUMENT_ROOT"])) {
|
||||||
|
$res = @include $_SERVER["CONTEXT_DOCUMENT_ROOT"]."/main.inc.php";
|
||||||
|
}
|
||||||
|
// Try main.inc.php into web root detected using web root calculated from SCRIPT_FILENAME
|
||||||
|
$tmp = empty($_SERVER['SCRIPT_FILENAME']) ? '' : $_SERVER['SCRIPT_FILENAME'];
|
||||||
|
$tmp2 = realpath(__FILE__);
|
||||||
|
$i = strlen($tmp) - 1;
|
||||||
|
$j = strlen($tmp2) - 1;
|
||||||
|
while ($i > 0 && $j > 0 && isset($tmp[$i]) && isset($tmp2[$j]) && $tmp[$i] == $tmp2[$j]) {
|
||||||
|
$i--;
|
||||||
|
$j--;
|
||||||
|
}
|
||||||
|
if (!$res && $i > 0 && file_exists(substr($tmp, 0, ($i + 1))."/main.inc.php")) {
|
||||||
|
$res = @include substr($tmp, 0, ($i + 1))."/main.inc.php";
|
||||||
|
}
|
||||||
|
if (!$res && $i > 0 && file_exists(dirname(substr($tmp, 0, ($i + 1)))."/main.inc.php")) {
|
||||||
|
$res = @include dirname(substr($tmp, 0, ($i + 1)))."/main.inc.php";
|
||||||
|
}
|
||||||
|
// Try main.inc.php using relative path
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
|
||||||
|
require_once DOL_DOCUMENT_ROOT.'/core/class/html.formfile.class.php';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var Conf $conf
|
||||||
|
* @var DoliDB $db
|
||||||
|
* @var HookManager $hookmanager
|
||||||
|
* @var Translate $langs
|
||||||
|
* @var User $user
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Load translation files required by the page
|
||||||
|
$langs->loadLangs(array("kundenkarte@kundenkarte"));
|
||||||
|
|
||||||
|
$action = GETPOST('action', 'aZ09');
|
||||||
|
|
||||||
|
$now = dol_now();
|
||||||
|
$max = getDolGlobalInt('MAIN_SIZE_SHORTLIST_LIMIT', 5);
|
||||||
|
|
||||||
|
// Security check - Protection if external user
|
||||||
|
$socid = GETPOSTINT('socid');
|
||||||
|
if (!empty($user->socid) && $user->socid > 0) {
|
||||||
|
$action = '';
|
||||||
|
$socid = $user->socid;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize a technical object to manage hooks. Note that conf->hooks_modules contains array
|
||||||
|
//$hookmanager->initHooks(array($object->element.'index'));
|
||||||
|
|
||||||
|
// Security check (enable the most restrictive one)
|
||||||
|
//if ($user->socid > 0) accessforbidden();
|
||||||
|
//if ($user->socid > 0) $socid = $user->socid;
|
||||||
|
//if (!isModEnabled('kundenkarte')) {
|
||||||
|
// accessforbidden('Module not enabled');
|
||||||
|
//}
|
||||||
|
//if (! $user->hasRight('kundenkarte', 'myobject', 'read')) {
|
||||||
|
// accessforbidden();
|
||||||
|
//}
|
||||||
|
//restrictedArea($user, 'kundenkarte', 0, 'kundenkarte_myobject', 'myobject', '', 'rowid');
|
||||||
|
//if (empty($user->admin)) {
|
||||||
|
// accessforbidden('Must be admin');
|
||||||
|
//}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Actions
|
||||||
|
*/
|
||||||
|
|
||||||
|
// None
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* View
|
||||||
|
*/
|
||||||
|
|
||||||
|
$form = new Form($db);
|
||||||
|
$formfile = new FormFile($db);
|
||||||
|
|
||||||
|
llxHeader("", $langs->trans("KundenKarteArea"), '', '', 0, 0, '', '', '', 'mod-kundenkarte page-index');
|
||||||
|
|
||||||
|
print load_fiche_titre($langs->trans("KundenKarteArea"), '', 'kundenkarte.png@kundenkarte');
|
||||||
|
|
||||||
|
print '<div class="fichecenter"><div class="fichethirdleft">';
|
||||||
|
|
||||||
|
|
||||||
|
/* BEGIN MODULEBUILDER DRAFT MYOBJECT
|
||||||
|
// Draft MyObject
|
||||||
|
if (isModEnabled('kundenkarte') && $user->hasRight('kundenkarte', 'read')) {
|
||||||
|
$langs->load("orders");
|
||||||
|
|
||||||
|
$sql = "SELECT c.rowid, c.ref, c.ref_client, c.total_ht, c.tva as total_tva, c.total_ttc, s.rowid as socid, s.nom as name, s.client, s.canvas";
|
||||||
|
$sql.= ", s.code_client";
|
||||||
|
$sql.= " FROM ".$db->prefix()."commande as c";
|
||||||
|
$sql.= ", ".$db->prefix()."societe as s";
|
||||||
|
$sql.= " WHERE c.fk_soc = s.rowid";
|
||||||
|
$sql.= " AND c.fk_statut = 0";
|
||||||
|
$sql.= " AND c.entity IN (".getEntity('commande').")";
|
||||||
|
if ($socid) $sql.= " AND c.fk_soc = ".((int) $socid);
|
||||||
|
|
||||||
|
$resql = $db->query($sql);
|
||||||
|
if ($resql)
|
||||||
|
{
|
||||||
|
$total = 0;
|
||||||
|
$num = $db->num_rows($resql);
|
||||||
|
|
||||||
|
print '<table class="noborder centpercent">';
|
||||||
|
print '<tr class="liste_titre">';
|
||||||
|
print '<th colspan="3">'.$langs->trans("DraftMyObjects").($num?'<span class="badge marginleftonlyshort">'.$num.'</span>':'').'</th></tr>';
|
||||||
|
|
||||||
|
$var = true;
|
||||||
|
if ($num > 0)
|
||||||
|
{
|
||||||
|
$i = 0;
|
||||||
|
while ($i < $num)
|
||||||
|
{
|
||||||
|
|
||||||
|
$obj = $db->fetch_object($resql);
|
||||||
|
print '<tr class="oddeven"><td class="nowrap">';
|
||||||
|
|
||||||
|
$myobjectstatic->id=$obj->rowid;
|
||||||
|
$myobjectstatic->ref=$obj->ref;
|
||||||
|
$myobjectstatic->ref_client=$obj->ref_client;
|
||||||
|
$myobjectstatic->total_ht = $obj->total_ht;
|
||||||
|
$myobjectstatic->total_tva = $obj->total_tva;
|
||||||
|
$myobjectstatic->total_ttc = $obj->total_ttc;
|
||||||
|
|
||||||
|
print $myobjectstatic->getNomUrl(1);
|
||||||
|
print '</td>';
|
||||||
|
print '<td class="nowrap">';
|
||||||
|
print '</td>';
|
||||||
|
print '<td class="right" class="nowrap">'.price($obj->total_ttc).'</td></tr>';
|
||||||
|
$i++;
|
||||||
|
$total += $obj->total_ttc;
|
||||||
|
}
|
||||||
|
if ($total>0)
|
||||||
|
{
|
||||||
|
|
||||||
|
print '<tr class="liste_total"><td>'.$langs->trans("Total").'</td><td colspan="2" class="right">'.price($total)."</td></tr>";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
|
||||||
|
print '<tr class="oddeven"><td colspan="3" class="opacitymedium">'.$langs->trans("NoOrder").'</td></tr>';
|
||||||
|
}
|
||||||
|
print "</table><br>";
|
||||||
|
|
||||||
|
$db->free($resql);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
dol_print_error($db);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
END MODULEBUILDER DRAFT MYOBJECT */
|
||||||
|
|
||||||
|
|
||||||
|
print '</div><div class="fichetwothirdright">';
|
||||||
|
|
||||||
|
|
||||||
|
/* BEGIN MODULEBUILDER LASTMODIFIED MYOBJECT
|
||||||
|
// Last modified myobject
|
||||||
|
if (isModEnabled('kundenkarte') && $user->hasRight('kundenkarte', 'read')) {
|
||||||
|
$sql = "SELECT s.rowid, s.ref, s.label, s.date_creation, s.tms";
|
||||||
|
$sql.= " FROM ".$db->prefix()."kundenkarte_myobject as s";
|
||||||
|
$sql.= " WHERE s.entity IN (".getEntity($myobjectstatic->element).")";
|
||||||
|
//if ($socid) $sql.= " AND s.rowid = $socid";
|
||||||
|
$sql .= " ORDER BY s.tms DESC";
|
||||||
|
$sql .= $db->plimit($max, 0);
|
||||||
|
|
||||||
|
$resql = $db->query($sql);
|
||||||
|
if ($resql)
|
||||||
|
{
|
||||||
|
$num = $db->num_rows($resql);
|
||||||
|
$i = 0;
|
||||||
|
|
||||||
|
print '<table class="noborder centpercent">';
|
||||||
|
print '<tr class="liste_titre">';
|
||||||
|
print '<th colspan="2">';
|
||||||
|
print $langs->trans("BoxTitleLatestModifiedMyObjects", $max);
|
||||||
|
print '</th>';
|
||||||
|
print '<th class="right">'.$langs->trans("DateModificationShort").'</th>';
|
||||||
|
print '</tr>';
|
||||||
|
if ($num)
|
||||||
|
{
|
||||||
|
while ($i < $num)
|
||||||
|
{
|
||||||
|
$objp = $db->fetch_object($resql);
|
||||||
|
|
||||||
|
$myobjectstatic->id=$objp->rowid;
|
||||||
|
$myobjectstatic->ref=$objp->ref;
|
||||||
|
$myobjectstatic->label=$objp->label;
|
||||||
|
$myobjectstatic->status = $objp->status;
|
||||||
|
|
||||||
|
print '<tr class="oddeven">';
|
||||||
|
print '<td class="nowrap">'.$myobjectstatic->getNomUrl(1).'</td>';
|
||||||
|
print '<td class="right nowrap">';
|
||||||
|
print "</td>";
|
||||||
|
print '<td class="right nowrap">'.dol_print_date($db->jdate($objp->tms), 'day')."</td>";
|
||||||
|
print '</tr>';
|
||||||
|
$i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
$db->free($resql);
|
||||||
|
} else {
|
||||||
|
print '<tr class="oddeven"><td colspan="3" class="opacitymedium">'.$langs->trans("None").'</td></tr>';
|
||||||
|
}
|
||||||
|
print "</table><br>";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
print '</div></div>';
|
||||||
|
|
||||||
|
// End of page
|
||||||
|
llxFooter();
|
||||||
|
$db->close();
|
||||||
547
langs/de_DE/kundenkarte.lang
Executable file
547
langs/de_DE/kundenkarte.lang
Executable file
|
|
@ -0,0 +1,547 @@
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
# Module
|
||||||
|
ModuleKundenKarteName = KundenKarte
|
||||||
|
ModuleKundenKarteDesc = Kundenkarte mit Favoriten und technischen Anlagen
|
||||||
|
KundenKarteSetupPage = Hier koennen Sie die Einstellungen fuer das Modul KundenKarte konfigurieren.
|
||||||
|
|
||||||
|
# Tabs
|
||||||
|
FavoriteProducts = Favoriten
|
||||||
|
TechnicalInstallations = Anlagen
|
||||||
|
|
||||||
|
# Favorite Products
|
||||||
|
FavoriteProductsList = Favorisierte Produkte
|
||||||
|
AddFavoriteProduct = Produkt hinzufuegen
|
||||||
|
NoFavoriteProducts = Keine favorisierten Produkte vorhanden
|
||||||
|
GenerateOrder = Bestellung generieren
|
||||||
|
SelectProducts = Produkte auswaehlen
|
||||||
|
DefaultQuantity = Standard-Menge
|
||||||
|
OrderGenerated = Bestellung %s wurde erstellt
|
||||||
|
ConfirmGenerateOrder = Moechten Sie eine Bestellung aus den ausgewaehlten Produkten erstellen?
|
||||||
|
OrderGeneratedFromFavorites = Generiert aus Favoriten-Produkten
|
||||||
|
NoProductsSelected = Keine Produkte ausgewaehlt
|
||||||
|
SelectAll = Alle auswaehlen
|
||||||
|
Up = Nach oben
|
||||||
|
Down = Nach unten
|
||||||
|
Qty = Menge
|
||||||
|
UnitPriceHT = Preis (netto)
|
||||||
|
Total = Gesamt
|
||||||
|
|
||||||
|
# Technical Installations
|
||||||
|
AnlagenSystems = Anlagen-Systeme
|
||||||
|
AnlagenTypes = Element-Typen
|
||||||
|
AnlagenTypeFields = Typ-Felder
|
||||||
|
AddSystem = System hinzufuegen
|
||||||
|
AddType = Typ hinzufuegen
|
||||||
|
AddField = Feld hinzufuegen
|
||||||
|
AddElement = Element hinzufuegen
|
||||||
|
EditElement = Element bearbeiten
|
||||||
|
DeleteElement = Element loeschen
|
||||||
|
NoInstallations = Keine Installationen vorhanden
|
||||||
|
SelectSystem = System auswaehlen
|
||||||
|
AllSystems = Alle Systeme
|
||||||
|
AllSystemsHint = Leer lassen fuer alle Systeme
|
||||||
|
Category = Kategorie
|
||||||
|
SelectCategory = Kategorie auswaehlen
|
||||||
|
SelectType = Typ auswaehlen
|
||||||
|
SelectParent = Uebergeordnetes Element
|
||||||
|
TechnicalElement = Element / Geraet
|
||||||
|
Root = Stamm
|
||||||
|
|
||||||
|
# Customer System Management
|
||||||
|
SystemAdded = System wurde hinzugefuegt
|
||||||
|
SystemRemoved = System wurde entfernt
|
||||||
|
RemoveSystem = System entfernen
|
||||||
|
ConfirmRemoveSystem = Moechten Sie dieses System wirklich vom Kunden entfernen?
|
||||||
|
NoSystemsConfigured = Keine Systeme konfiguriert
|
||||||
|
ClickAddSystemToStart = Klicken Sie auf "System hinzufuegen" um Systeme fuer diesen Kunden hinzuzufuegen
|
||||||
|
ContactAdminToAddSystems = Bitte erstellen Sie zuerst Systeme im Admin-Bereich
|
||||||
|
SelectSystemToAdd = System zum Hinzufuegen auswaehlen
|
||||||
|
ErrorSystemHasElements = System kann nicht entfernt werden, da noch Elemente vorhanden sind
|
||||||
|
NoTypesDefinedForSystem = Keine Typen fuer dieses System definiert
|
||||||
|
AvailableSystems = Verfuegbare Systeme
|
||||||
|
NoMoreSystemsAvailable = Alle verfuegbaren Systeme wurden bereits hinzugefuegt
|
||||||
|
|
||||||
|
# System Categories
|
||||||
|
SystemStrom = Strom
|
||||||
|
SystemInternet = Internet/Netzwerk
|
||||||
|
SystemKabel = Kabelfernsehen
|
||||||
|
SystemSat = Satelliten
|
||||||
|
|
||||||
|
# Element Types
|
||||||
|
TypeZaehlerschrank = Zaehlerschrank
|
||||||
|
TypeUnterverteilung = Unterverteilung
|
||||||
|
TypeWallbox = Wallbox
|
||||||
|
TypePVSpeicher = PV-Speicher
|
||||||
|
TypeWechselrichter = Wechselrichter
|
||||||
|
TypeWaermepumpe = Waermepumpe
|
||||||
|
TypeRouter = Router
|
||||||
|
TypeSwitch = Switch
|
||||||
|
TypeAccessPoint = Access Point
|
||||||
|
TypeNetzwerkschrank = Netzwerkschrank
|
||||||
|
|
||||||
|
# Fields
|
||||||
|
FieldSize = Groesse
|
||||||
|
FieldFieldCount = Feldanzahl
|
||||||
|
FieldManufacturer = Hersteller
|
||||||
|
FieldModel = Modell/Typ
|
||||||
|
FieldSerialNumber = Seriennummer
|
||||||
|
FieldPowerRating = Leistung
|
||||||
|
FieldLocation = Standort
|
||||||
|
FieldInstallationDate = Installationsdatum
|
||||||
|
FieldWarrantyUntil = Garantie bis
|
||||||
|
FieldNotes = Hinweise
|
||||||
|
|
||||||
|
# Tree
|
||||||
|
CanHaveChildren = Kann Unterelemente haben
|
||||||
|
CanBeNested = Kann verschachtelt werden
|
||||||
|
SameTypeUnderItself = Gleicher Typ unter sich selbst erlaubt
|
||||||
|
AllowedParentTypes = Erlaubte Eltern-Typen
|
||||||
|
AllowedParentTypesHelp = Leer = alle Typen erlaubt
|
||||||
|
CommaSeperatedRefs = Komma-getrennte Referenzen
|
||||||
|
ShowInTree = Im Baum anzeigen
|
||||||
|
ShowInHover = Im Hover anzeigen
|
||||||
|
IsRequired = Pflichtfeld
|
||||||
|
ExpandAll = Alles ausklappen
|
||||||
|
CollapseAll = Alles einklappen
|
||||||
|
|
||||||
|
# Files
|
||||||
|
UploadFiles = Dateien hochladen
|
||||||
|
AttachedFiles = Angehaengte Dateien
|
||||||
|
NoFiles = Keine Dateien vorhanden
|
||||||
|
SetAsCover = Als Vorschaubild setzen
|
||||||
|
ConfirmDeleteFile = Moechten Sie diese Datei wirklich loeschen?
|
||||||
|
FileDeleted = Datei wurde geloescht
|
||||||
|
FileUploaded = Datei wurde hochgeladen
|
||||||
|
|
||||||
|
# Admin
|
||||||
|
Settings = Einstellungen
|
||||||
|
About = Ueber
|
||||||
|
ManageSystems = Systeme verwalten
|
||||||
|
ManageTypes = Typen verwalten
|
||||||
|
ManageFields = Felder verwalten
|
||||||
|
SystemCode = System-Code
|
||||||
|
SystemLabel = Bezeichnung
|
||||||
|
SystemPicto = Icon
|
||||||
|
SystemColor = Farbe
|
||||||
|
TypeRef = Referenz
|
||||||
|
TypeLabel = Bezeichnung
|
||||||
|
TypeShortLabel = Kurzbezeichnung
|
||||||
|
FieldCode = Feld-Code
|
||||||
|
FieldLabel = Feld-Bezeichnung
|
||||||
|
FieldType = Feld-Typ
|
||||||
|
FieldOptions = Optionen (JSON)
|
||||||
|
FontAwesomeIcon = FontAwesome Icon
|
||||||
|
SelectIcon = Icon auswaehlen
|
||||||
|
Position = Position
|
||||||
|
Status = Status
|
||||||
|
Actions = Aktionen
|
||||||
|
System = System
|
||||||
|
Description = Beschreibung
|
||||||
|
FilterBySystem = Nach System filtern
|
||||||
|
All = Alle
|
||||||
|
NoRecords = Keine Eintraege vorhanden
|
||||||
|
NoFieldsDefined = Keine Felder definiert
|
||||||
|
EditFieldsInDatabase = Felder koennen in der Datenbank oder ueber SQL bearbeitet werden
|
||||||
|
FieldOptionsHelp = Bei Typ "Select": Optionen mit | trennen (z.B. Option1|Option2|Option3)
|
||||||
|
|
||||||
|
# Field Types
|
||||||
|
Text = Text
|
||||||
|
Number = Zahl
|
||||||
|
Date = Datum
|
||||||
|
Textarea = Textfeld (mehrzeilig)
|
||||||
|
Checkbox = Checkbox
|
||||||
|
|
||||||
|
# Confirmations
|
||||||
|
ConfirmDeleteSystem = Moechten Sie dieses System wirklich loeschen?
|
||||||
|
ConfirmDeleteType = Moechten Sie diesen Typ wirklich loeschen?
|
||||||
|
ConfirmDeleteElement = Moechten Sie dieses Element wirklich loeschen?
|
||||||
|
ConfirmDeleteField = Moechten Sie dieses Feld wirklich loeschen?
|
||||||
|
|
||||||
|
# Errors
|
||||||
|
ErrorSystemInUse = System kann nicht geloescht werden, da es noch verwendet wird
|
||||||
|
ErrorTypeInUse = Typ kann nicht geloescht werden, da er noch verwendet wird
|
||||||
|
ErrorParentNotAllowed = Dieses Element kann nicht unter dem ausgewaehlten Elternelement platziert werden
|
||||||
|
ErrorFieldRequired = Pflichtfeld nicht ausgefuellt
|
||||||
|
ErrorCircularReference = Zirkulaere Referenz: Das Element kann nicht unter sich selbst oder einem seiner Unterelemente platziert werden
|
||||||
|
ErrorMaxDepthExceeded = Maximale Verschachtelungstiefe ueberschritten
|
||||||
|
ErrorMissingParameters = Pflichtfelder fehlen. Bitte fuellen Sie alle erforderlichen Felder aus.
|
||||||
|
ErrorDatabaseConnection = Datenbankfehler. Bitte versuchen Sie es erneut.
|
||||||
|
ErrorFileUpload = Datei-Upload fehlgeschlagen. Bitte pruefen Sie Dateityp und Groesse.
|
||||||
|
ErrorFileTooLarge = Die Datei ist zu gross (max. %s MB)
|
||||||
|
ErrorInvalidFileType = Ungueltiger Dateityp. Erlaubt: %s
|
||||||
|
ErrorPermissionDenied = Sie haben keine Berechtigung fuer diese Aktion
|
||||||
|
ErrorRecordNotFound = Eintrag nicht gefunden
|
||||||
|
ErrorEquipmentNoSpace = Kein Platz auf der Hutschiene an dieser Position
|
||||||
|
ErrorCarrierFull = Die Hutschiene ist voll belegt
|
||||||
|
ErrorDuplicateRef = Diese Referenz existiert bereits
|
||||||
|
ErrorInvalidJson = Ungueltige JSON-Daten
|
||||||
|
ErrorSaveFailedDetails = Speichern fehlgeschlagen: %s
|
||||||
|
|
||||||
|
# Keyboard Shortcuts
|
||||||
|
KeyboardShortcuts = Tastenkuerzel
|
||||||
|
ShortcutSave = Speichern/Aktualisieren
|
||||||
|
ShortcutEscape = Abbrechen/Schliessen
|
||||||
|
ShortcutDelete = Loeschen
|
||||||
|
ShortcutZoomIn = Vergroessern
|
||||||
|
ShortcutZoomOut = Verkleinern
|
||||||
|
ShortcutZoomReset = Zoom zuruecksetzen
|
||||||
|
ShortcutZoomFit = An Fenster anpassen
|
||||||
|
ShortcutRefresh = Neu laden
|
||||||
|
|
||||||
|
# Setup Page
|
||||||
|
KundenKarteSetup = KundenKarte Einstellungen
|
||||||
|
Parameter = Parameter
|
||||||
|
Value = Wert
|
||||||
|
ShowFavoritesTab = Tab "Favoriten" anzeigen
|
||||||
|
ShowAnlagenTab = Tab "Anlagen" anzeigen
|
||||||
|
DefaultOrderType = Standard-Bestelltyp fuer Favoriten
|
||||||
|
OrderTypeOrder = Bestellung (Commande)
|
||||||
|
OrderTypeProposal = Angebot (Propal)
|
||||||
|
ConfigurationHelp = Konfigurationshinweise
|
||||||
|
ConfigHelpSystems = Systeme verwalten: Gehen Sie zum Tab "Anlagen-Systeme" um eigene System-Kategorien anzulegen
|
||||||
|
ConfigHelpTypes = Element-Typen verwalten: Gehen Sie zum Tab "Element-Typen" um Geraetetypen und Felder zu definieren
|
||||||
|
SetupSaved = Einstellungen gespeichert
|
||||||
|
|
||||||
|
# Equipment (Hutschienen-Komponenten)
|
||||||
|
EquipmentTypes = Equipment-Typen
|
||||||
|
AddEquipmentType = Equipment-Typ hinzufuegen
|
||||||
|
EquipmentTypeFields = Equipment-Felder
|
||||||
|
CanHaveEquipment = Kann Equipment haben
|
||||||
|
CanHaveEquipmentHelp = Hutschienen-Komponenten koennen unter diesem Typ platziert werden
|
||||||
|
WidthTE = Breite (TE)
|
||||||
|
WidthTEHelp = Breite in Teilungseinheiten (1 TE = 18mm)
|
||||||
|
ColorForSVG = Farbe fuer SVG-Darstellung
|
||||||
|
TerminalConfig = Anschlusspunkte
|
||||||
|
TerminalConfigHelp = JSON-Konfiguration der Anschlusspunkte (Terminals)
|
||||||
|
LinkedProduct = Verknuepftes Produkt
|
||||||
|
ShowOnBlock = Auf Block anzeigen
|
||||||
|
ShowInHover = Im Hover anzeigen
|
||||||
|
Carrier = Traeger
|
||||||
|
CarrierLabel = Hutschiene
|
||||||
|
AddCarrier = Traeger hinzufuegen
|
||||||
|
TotalTE = Gesamt TE
|
||||||
|
UsedTE = Belegt
|
||||||
|
FreeTE = Frei
|
||||||
|
Equipment = Equipment
|
||||||
|
AddEquipment = Equipment hinzufuegen
|
||||||
|
DuplicateEquipment = Equipment kopieren
|
||||||
|
DuplicatePrevious = Vorherigen Automaten kopieren
|
||||||
|
DuplicatePreviousCarrier = Vorherige Hutschiene kopieren
|
||||||
|
DuplicatePreviousPanel = Vorheriges Feld kopieren
|
||||||
|
NoSpaceOnCarrier = Kein Platz mehr auf dem Traeger
|
||||||
|
PositionTE = Position (TE)
|
||||||
|
Panels = Felder
|
||||||
|
Fields = Felder
|
||||||
|
AddPanel = Feld hinzufuegen
|
||||||
|
PanelLabel = Feld
|
||||||
|
NoCarriers = Keine Hutschienen
|
||||||
|
DirectCarriers = Direkte Hutschienen
|
||||||
|
NoPanelsOrCarriers = Keine Felder oder Hutschienen vorhanden
|
||||||
|
AddPanelOrCarrier = Feld oder Hutschiene hinzufuegen
|
||||||
|
Protection = Schutzeinrichtung
|
||||||
|
ProtectionLabel = Schutzbezeichnung
|
||||||
|
AssignedToProtection = Gehoert zu Schutzeinrichtung
|
||||||
|
Characteristic = Charakteristik
|
||||||
|
Ampere = Nennstrom
|
||||||
|
Pole = Polzahl
|
||||||
|
Circuit = Stromkreis
|
||||||
|
|
||||||
|
# Connections (Verbindungen - generisch fuer alle Systeme)
|
||||||
|
Connections = Verbindungen
|
||||||
|
Connection = Verbindung
|
||||||
|
AddConnection = Verbindung hinzufuegen
|
||||||
|
AddCableConnection = Kabelverbindung hinzufuegen
|
||||||
|
AddOutput = Abgang hinzufuegen
|
||||||
|
AddRail = Sammelschiene hinzufuegen
|
||||||
|
AddBusbar = Sammelschiene hinzufuegen
|
||||||
|
Busbar = Sammelschiene
|
||||||
|
BusbarTypes = Sammelschienen-Typen
|
||||||
|
NewBusbarType = Neuer Sammelschienen-Typ
|
||||||
|
DeleteBusbarType = Sammelschienen-Typ loeschen
|
||||||
|
ConfirmDeleteBusbarType = Moechten Sie den Sammelschienen-Typ "%s" wirklich loeschen?
|
||||||
|
Channels = Kanaele
|
||||||
|
ChannelsHint = Konfigurations-ID (z.B. A, B, AB, 1-2-3)
|
||||||
|
NumLines = Anzahl Linien
|
||||||
|
Colors = Farben
|
||||||
|
DefaultColor = Standardfarbe
|
||||||
|
LineHeight = Linienhoehe
|
||||||
|
LineSpacing = Linienabstand
|
||||||
|
DefaultPosition = Standard-Position
|
||||||
|
BlockImage = Block-Bild
|
||||||
|
ConnectionEditor = Verbindungseditor
|
||||||
|
ConnectionType = Verbindungstyp
|
||||||
|
Color = Farbe
|
||||||
|
OutputLabel = Ziel/Endpunkt
|
||||||
|
MediumType = Medientyp
|
||||||
|
MediumSpec = Spezifikation
|
||||||
|
MediumLength = Laenge
|
||||||
|
SourceEquipment = Von Equipment
|
||||||
|
TargetEquipment = Zu Equipment
|
||||||
|
SourceTerminal = Ausgang von
|
||||||
|
TargetTerminal = Eingang zu
|
||||||
|
InputTerminal = Eingang
|
||||||
|
OutputTerminal = Ausgang
|
||||||
|
RailStart = Schiene von TE
|
||||||
|
RailEnd = Schiene bis TE
|
||||||
|
NoConnections = Keine Verbindungen
|
||||||
|
DeleteConnection = Verbindung loeschen
|
||||||
|
ConfirmDeleteConnection = Moechten Sie diese Verbindung wirklich loeschen?
|
||||||
|
ExternalInput = Externe Einspeisung
|
||||||
|
|
||||||
|
# Beispiel-Verbindungstypen (flexibel, Benutzer kann eigene anlegen)
|
||||||
|
# Strom: L1, L2, L3, N, PE, L1N, 3P, 3P+N, 3P+N+PE
|
||||||
|
# Netzwerk: CAT5, CAT6, CAT7, LWL, Koax
|
||||||
|
# Sicherheit: 2-Draht, 4-Draht, BUS
|
||||||
|
|
||||||
|
# PDF Export
|
||||||
|
PDFExportTemplate = PDF Export Vorlage
|
||||||
|
PDFFontSettings = PDF Schriftgroessen
|
||||||
|
PDFFontHeader = Ueberschrift Schriftgroesse
|
||||||
|
PDFFontContent = Inhalt Schriftgroesse
|
||||||
|
PDFFontFields = Felder Schriftgroesse
|
||||||
|
PDFFontHeaderHelp = Schriftgroesse fuer Element-Namen (Standard: 9pt)
|
||||||
|
PDFFontContentHelp = Schriftgroesse fuer Feldwerte (Standard: 7pt)
|
||||||
|
PDFFontFieldsHelp = Schriftgroesse fuer Feld-Labels (Standard: 7pt)
|
||||||
|
PDFTemplate = PDF Vorlage
|
||||||
|
CurrentTemplate = Aktuelle Vorlage
|
||||||
|
NoTemplateUploaded = Keine Vorlage hochgeladen
|
||||||
|
UploadNewTemplate = Neue Vorlage hochladen
|
||||||
|
DeleteTemplate = Vorlage loeschen
|
||||||
|
ConfirmDeleteTemplate = Moechten Sie die Vorlage wirklich loeschen?
|
||||||
|
TemplateUploadSuccess = Vorlage wurde erfolgreich hochgeladen
|
||||||
|
TemplateDeleted = Vorlage wurde geloescht
|
||||||
|
ErrorOnlyPDFAllowed = Nur PDF-Dateien sind erlaubt
|
||||||
|
ErrorUploadFailed = Hochladen fehlgeschlagen
|
||||||
|
ErrorNoFileSelected = Keine Datei ausgewaehlt
|
||||||
|
PDFTemplateHelp = Laden Sie eine PDF-Datei als Hintergrund/Briefpapier fuer den Export hoch. Die erste Seite wird als Vorlage verwendet.
|
||||||
|
ExportTreeAsPDF = Als PDF exportieren
|
||||||
|
|
||||||
|
# View Mode
|
||||||
|
DefaultViewMode = Standard-Ansichtsmodus fuer Anlagen
|
||||||
|
ViewModeTree = Baumansicht (klassisch)
|
||||||
|
ViewModeGraph = Graph-Ansicht (Cytoscape)
|
||||||
|
SpatialView = Raeumlich
|
||||||
|
TechnicalView = Technisch
|
||||||
|
GraphLoading = Graph wird geladen...
|
||||||
|
GraphLegendRoom = Raum/Gebaeude
|
||||||
|
GraphLegendDevice = Geraet
|
||||||
|
GraphLegendCable = Kabel
|
||||||
|
GraphLegendPassthrough = Durchgeschleift
|
||||||
|
GraphLegendHierarchy = Hierarchie
|
||||||
|
|
||||||
|
# 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
|
||||||
|
Delete = Loeschen
|
||||||
|
Enabled = Aktiviert
|
||||||
|
Disabled = Deaktiviert
|
||||||
|
RecordSaved = Eintrag gespeichert
|
||||||
|
RecordDeleted = Eintrag geloescht
|
||||||
|
BackToModuleList = Zurueck zur Modulliste
|
||||||
|
Error = Fehler
|
||||||
|
Select = Auswaehlen
|
||||||
|
Add = Hinzufuegen
|
||||||
|
AddChild = Unterelement hinzufuegen
|
||||||
|
Label = Bezeichnung
|
||||||
|
Type = Typ
|
||||||
|
Upload = Hochladen
|
||||||
|
View = Ansehen
|
||||||
|
Back = Zurueck
|
||||||
|
Modify = Bearbeiten
|
||||||
|
Edit = Bearbeiten
|
||||||
|
Close = Schliessen
|
||||||
|
Confirm = Bestaetigen
|
||||||
|
Yes = Ja
|
||||||
|
No = Nein
|
||||||
|
|
||||||
|
# Bill of Materials (Stueckliste)
|
||||||
|
BillOfMaterials = Stueckliste
|
||||||
|
BOMFromSchematic = Stueckliste aus Schaltplan
|
||||||
|
GenerateBOM = Stueckliste generieren
|
||||||
|
BOMSummary = Zusammenfassung
|
||||||
|
BOMDetails = Detailliste
|
||||||
|
BOMReference = Referenz
|
||||||
|
BOMDescription = Beschreibung
|
||||||
|
BOMQuantity = Menge
|
||||||
|
BOMUnitPrice = Stueckpreis
|
||||||
|
BOMTotal = Gesamt
|
||||||
|
BOMTotalQuantity = Gesamtmenge
|
||||||
|
BOMEstimatedTotal = Geschaetzter Gesamtpreis
|
||||||
|
BOMNoProduct = Kein Produkt verknuepft
|
||||||
|
BOMCopyClipboard = In Zwischenablage kopieren
|
||||||
|
BOMCopied = Stueckliste in Zwischenablage kopiert
|
||||||
|
BOMCreateOrder = Bestellung erstellen
|
||||||
|
BOMNoItems = Keine Komponenten im Schaltplan gefunden
|
||||||
|
BOMLocation = Position
|
||||||
|
|
||||||
|
# Audit Log
|
||||||
|
AuditLog = Aenderungsprotokoll
|
||||||
|
AuditLogHistory = Aenderungsverlauf
|
||||||
|
AuditActionCreate = Erstellt
|
||||||
|
AuditActionUpdate = Geaendert
|
||||||
|
AuditActionDelete = Geloescht
|
||||||
|
AuditActionMove = Verschoben
|
||||||
|
AuditActionDuplicate = Kopiert
|
||||||
|
AuditActionStatus = Status geaendert
|
||||||
|
AuditFieldChanged = Geaendertes Feld
|
||||||
|
AuditOldValue = Alter Wert
|
||||||
|
AuditNewValue = Neuer Wert
|
||||||
|
AuditUser = Benutzer
|
||||||
|
AuditDate = Datum
|
||||||
|
AuditNoEntries = Keine Aenderungen protokolliert
|
||||||
|
AuditShowMore = Mehr anzeigen
|
||||||
|
Installation = Anlage
|
||||||
|
EquipmentType = Equipment-Typ
|
||||||
|
BusbarType = Sammelschienen-Typ
|
||||||
|
|
||||||
|
# Medium Types (Kabeltypen)
|
||||||
|
MediumTypes = Kabeltypen
|
||||||
|
MediumType = Kabeltyp
|
||||||
|
AddMediumType = Kabeltyp hinzufuegen
|
||||||
|
DeleteMediumType = Kabeltyp loeschen
|
||||||
|
ConfirmDeleteMediumType = Moechten Sie den Kabeltyp "%s" wirklich loeschen?
|
||||||
|
DefaultSpec = Standard-Spezifikation
|
||||||
|
DefaultSpecHelp = z.B. "3x1,5" fuer NYM
|
||||||
|
AvailableSpecs = Verfuegbare Spezifikationen
|
||||||
|
AvailableSpecsHelp = Kommagetrennt, z.B.: 3x1,5, 3x2,5, 5x1,5, 5x2,5
|
||||||
|
LabelShort = Kurzbezeichnung
|
||||||
|
MediumCatStromkabel = Stromkabel
|
||||||
|
MediumCatNetzwerkkabel = Netzwerkkabel
|
||||||
|
MediumCatLWL = Lichtwellenleiter
|
||||||
|
MediumCatKoax = Koaxialkabel
|
||||||
|
MediumCatSonstiges = Sonstiges
|
||||||
|
CableType = Kabeltyp
|
||||||
|
CableSpec = Querschnitt/Typ
|
||||||
|
CableLength = Laenge
|
||||||
|
CableLengthUnit = m
|
||||||
|
|
||||||
|
# Building Types (Gebaeudetypen)
|
||||||
|
BuildingTypes = Gebaeudetypen
|
||||||
|
BuildingType = Gebaeudetyp
|
||||||
|
AddBuildingType = Gebaeudetyp hinzufuegen
|
||||||
|
DeleteBuildingType = Gebaeudetyp loeschen
|
||||||
|
ConfirmDeleteBuildingType = Moechten Sie den Gebaeudetyp "%s" wirklich loeschen?
|
||||||
|
BuildingStructure = Gebaeudestruktur
|
||||||
|
IsGlobal = Global (alle Systeme)
|
||||||
|
AvailableForAllSystems = Verfuegbar fuer alle Systeme
|
||||||
|
BuildingTypesSetup = Gebaeudetypen
|
||||||
|
LevelType = Ebenen-Typ
|
||||||
|
FilterByLevel = Nach Ebene filtern
|
||||||
|
BuildingLevelBuilding = Gebaeude
|
||||||
|
BuildingLevelFloor = Etage/Geschoss
|
||||||
|
BuildingLevelWing = Gebaeudefluegel
|
||||||
|
BuildingLevelCorridor = Flur/Gang
|
||||||
|
BuildingLevelRoom = Raum
|
||||||
|
BuildingLevelArea = Bereich/Zone
|
||||||
|
CanHaveChildren = Kann Unterelemente haben
|
||||||
|
CannotDeleteTypeWithChildren = Kann nicht geloescht werden - wird als Eltern-Typ verwendet
|
||||||
|
CannotDeleteSystemType = System-Typen koennen nicht geloescht werden
|
||||||
|
|
||||||
|
# Tree Display Configuration
|
||||||
|
TreeDisplayConfig = Baum-Anzeige Konfiguration
|
||||||
|
TreeShowRef = Referenz anzeigen
|
||||||
|
TreeShowRefHelp = Zeigt die Referenznummer im Baum an
|
||||||
|
TreeShowLabel = Bezeichnung anzeigen
|
||||||
|
TreeShowLabelHelp = Zeigt die Bezeichnung/Namen im Baum an
|
||||||
|
TreeShowType = Typ anzeigen
|
||||||
|
TreeShowTypeHelp = Zeigt den Anlagentyp im Baum an
|
||||||
|
TreeShowIcon = Icon anzeigen
|
||||||
|
TreeShowIconHelp = Zeigt das Typ-Icon im Baum an
|
||||||
|
TreeShowStatus = Status anzeigen
|
||||||
|
TreeShowStatusHelp = Zeigt den Status (aktiv/inaktiv) im Baum an
|
||||||
|
TreeShowFields = Felder anzeigen
|
||||||
|
TreeShowFieldsHelp = Zeigt zusaetzliche Typ-Felder direkt im Baum an
|
||||||
|
TreeExpandDefault = Standardmaessig erweitert
|
||||||
|
TreeExpandDefaultHelp = Baum wird beim Laden automatisch erweitert
|
||||||
|
TreeIndentStyle = Einrueckungsstil
|
||||||
|
TreeIndentLines = Linien (Standard)
|
||||||
|
TreeIndentDots = Punkte
|
||||||
|
TreeIndentArrows = Pfeile
|
||||||
|
TreeIndentSimple = Einfach (nur Einrueckung)
|
||||||
|
|
||||||
|
# Anlagen-Verbindungen
|
||||||
|
AnlageConnections = Verbindungen
|
||||||
|
AnlageConnection = Verbindung
|
||||||
|
AddConnection = Verbindung hinzufuegen
|
||||||
|
EditConnection = Verbindung bearbeiten
|
||||||
|
DeleteConnection = Verbindung loeschen
|
||||||
|
ConfirmDeleteConnection = Moechten Sie diese Verbindung wirklich loeschen?
|
||||||
|
ConnectionFrom = Von
|
||||||
|
ConnectionTo = Nach
|
||||||
|
ConnectionSource = Quelle
|
||||||
|
ConnectionTarget = Ziel
|
||||||
|
ConnectionLabel = Bezeichnung
|
||||||
|
RouteDescription = Verlegungsweg
|
||||||
|
InstallationDate = Installationsdatum
|
||||||
|
SelectSource = Quelle waehlen...
|
||||||
|
SelectTarget = Ziel waehlen...
|
||||||
|
NoConnections = Keine Verbindungen vorhanden
|
||||||
|
ConnectionCreated = Verbindung erstellt
|
||||||
|
ConnectionUpdated = Verbindung aktualisiert
|
||||||
|
ConnectionDeleted = Verbindung geloescht
|
||||||
|
|
||||||
|
# Graph-Ansicht
|
||||||
|
TreeView = Baumansicht
|
||||||
|
GraphView = Graph-Ansicht
|
||||||
|
GraphLoading = Graph wird geladen...
|
||||||
|
SearchPlaceholder = Suchen...
|
||||||
295
langs/en_US/kundenkarte.lang
Executable file
295
langs/en_US/kundenkarte.lang
Executable file
|
|
@ -0,0 +1,295 @@
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
# Module
|
||||||
|
ModuleKundenKarteName = KundenKarte
|
||||||
|
ModuleKundenKarteDesc = Customer card with favorites and technical installations
|
||||||
|
KundenKarteSetupPage = Configure the settings for the KundenKarte module here.
|
||||||
|
|
||||||
|
# Tabs
|
||||||
|
FavoriteProducts = Favorites
|
||||||
|
TechnicalInstallations = Installations
|
||||||
|
|
||||||
|
# Favorite Products
|
||||||
|
FavoriteProductsList = Favorite Products
|
||||||
|
AddFavoriteProduct = Add product
|
||||||
|
NoFavoriteProducts = No favorite products
|
||||||
|
GenerateOrder = Generate Order
|
||||||
|
SelectProducts = Select products
|
||||||
|
DefaultQuantity = Default quantity
|
||||||
|
OrderGenerated = Order %s has been created
|
||||||
|
ConfirmGenerateOrder = Do you want to create an order from the selected products?
|
||||||
|
OrderGeneratedFromFavorites = Generated from favorite products
|
||||||
|
NoProductsSelected = No products selected
|
||||||
|
SelectAll = Select all
|
||||||
|
Up = Up
|
||||||
|
Down = Down
|
||||||
|
Qty = Qty
|
||||||
|
UnitPriceHT = Price (excl. tax)
|
||||||
|
Total = Total
|
||||||
|
|
||||||
|
# Technical Installations
|
||||||
|
AnlagenSystems = Installation Systems
|
||||||
|
AnlagenTypes = Element Types
|
||||||
|
AnlagenTypeFields = Type Fields
|
||||||
|
AddSystem = Add system
|
||||||
|
AddType = Add type
|
||||||
|
AddField = Add field
|
||||||
|
AddElement = Add element
|
||||||
|
EditElement = Edit element
|
||||||
|
DeleteElement = Delete element
|
||||||
|
NoInstallations = No installations
|
||||||
|
SelectSystem = Select system
|
||||||
|
Category = Category
|
||||||
|
SelectCategory = Select category
|
||||||
|
SelectType = Select type
|
||||||
|
SelectParent = Parent element
|
||||||
|
TechnicalElement = Element / Device
|
||||||
|
Root = Root
|
||||||
|
|
||||||
|
# Customer System Management
|
||||||
|
SystemAdded = System has been added
|
||||||
|
SystemRemoved = System has been removed
|
||||||
|
RemoveSystem = Remove system
|
||||||
|
ConfirmRemoveSystem = Are you sure you want to remove this system from this customer?
|
||||||
|
NoSystemsConfigured = No systems configured
|
||||||
|
ClickAddSystemToStart = Click "Add system" to add systems for this customer
|
||||||
|
ContactAdminToAddSystems = Please create systems in the admin area first
|
||||||
|
SelectSystemToAdd = Select system to add
|
||||||
|
ErrorSystemHasElements = System cannot be removed because it still has elements
|
||||||
|
NoTypesDefinedForSystem = No types defined for this system
|
||||||
|
AvailableSystems = Available systems
|
||||||
|
NoMoreSystemsAvailable = All available systems have already been added
|
||||||
|
|
||||||
|
# Fields
|
||||||
|
FieldSize = Size
|
||||||
|
FieldFieldCount = Field count
|
||||||
|
FieldManufacturer = Manufacturer
|
||||||
|
FieldModel = Model/Type
|
||||||
|
FieldSerialNumber = Serial number
|
||||||
|
FieldPowerRating = Power rating
|
||||||
|
FieldLocation = Location
|
||||||
|
FieldInstallationDate = Installation date
|
||||||
|
FieldWarrantyUntil = Warranty until
|
||||||
|
FieldNotes = Notes
|
||||||
|
|
||||||
|
# Tree
|
||||||
|
CanHaveChildren = Can have children
|
||||||
|
CanBeNested = Can be nested
|
||||||
|
SameTypeUnderItself = Same type under itself allowed
|
||||||
|
AllowedParentTypes = Allowed parent types
|
||||||
|
AllowedParentTypesHelp = Empty = all types allowed
|
||||||
|
CommaSeperatedRefs = Comma separated refs
|
||||||
|
ShowInTree = Show in tree
|
||||||
|
ShowInHover = Show on hover
|
||||||
|
IsRequired = Required
|
||||||
|
ExpandAll = Expand all
|
||||||
|
CollapseAll = Collapse all
|
||||||
|
|
||||||
|
# Files
|
||||||
|
UploadFiles = Upload files
|
||||||
|
AttachedFiles = Attached files
|
||||||
|
NoFiles = No files
|
||||||
|
SetAsCover = Set as cover
|
||||||
|
ConfirmDeleteFile = Are you sure you want to delete this file?
|
||||||
|
FileDeleted = File has been deleted
|
||||||
|
FileUploaded = File has been uploaded
|
||||||
|
|
||||||
|
# Admin
|
||||||
|
Settings = Settings
|
||||||
|
About = About
|
||||||
|
ManageSystems = Manage systems
|
||||||
|
ManageTypes = Manage types
|
||||||
|
ManageFields = Manage fields
|
||||||
|
SystemCode = System code
|
||||||
|
SystemLabel = Label
|
||||||
|
SystemPicto = Icon
|
||||||
|
SystemColor = Color
|
||||||
|
TypeRef = Reference
|
||||||
|
TypeLabel = Label
|
||||||
|
TypeShortLabel = Short label
|
||||||
|
FieldCode = Field code
|
||||||
|
FieldLabel = Field label
|
||||||
|
FieldType = Field type
|
||||||
|
FieldOptions = Options (JSON)
|
||||||
|
FontAwesomeIcon = FontAwesome icon
|
||||||
|
SelectIcon = Select icon
|
||||||
|
Position = Position
|
||||||
|
Status = Status
|
||||||
|
Actions = Actions
|
||||||
|
System = System
|
||||||
|
Description = Description
|
||||||
|
FilterBySystem = Filter by system
|
||||||
|
All = All
|
||||||
|
NoRecords = No records
|
||||||
|
NoFieldsDefined = No fields defined
|
||||||
|
EditFieldsInDatabase = Fields can be edited in database or via SQL
|
||||||
|
FieldOptionsHelp = For type "Select": separate options with | (e.g. Option1|Option2|Option3)
|
||||||
|
|
||||||
|
# Field Types
|
||||||
|
Text = Text
|
||||||
|
Number = Number
|
||||||
|
Date = Date
|
||||||
|
Textarea = Textarea (multiline)
|
||||||
|
Checkbox = Checkbox
|
||||||
|
|
||||||
|
# Setup Page
|
||||||
|
KundenKarteSetup = KundenKarte Settings
|
||||||
|
Parameter = Parameter
|
||||||
|
Value = Value
|
||||||
|
ShowFavoritesTab = Show "Favorites" tab
|
||||||
|
ShowAnlagenTab = Show "Installations" tab
|
||||||
|
DefaultOrderType = Default order type for favorites
|
||||||
|
OrderTypeOrder = Order (Commande)
|
||||||
|
OrderTypeProposal = Proposal (Propal)
|
||||||
|
ConfigurationHelp = Configuration tips
|
||||||
|
ConfigHelpSystems = Manage systems: Go to the "Installation Systems" tab to create custom system categories
|
||||||
|
ConfigHelpTypes = Manage element types: Go to the "Element Types" tab to define device types and fields
|
||||||
|
SetupSaved = Settings saved
|
||||||
|
|
||||||
|
# Confirmations
|
||||||
|
ConfirmDeleteSystem = Are you sure you want to delete this system?
|
||||||
|
ConfirmDeleteType = Are you sure you want to delete this type?
|
||||||
|
ConfirmDeleteElement = Are you sure you want to delete this element?
|
||||||
|
ConfirmDeleteField = Are you sure you want to delete this field?
|
||||||
|
|
||||||
|
# Errors
|
||||||
|
ErrorSystemInUse = System cannot be deleted because it is still in use
|
||||||
|
ErrorTypeInUse = Type cannot be deleted because it is still in use
|
||||||
|
ErrorParentNotAllowed = This element cannot be placed under the selected parent
|
||||||
|
ErrorCannotDeleteSystemType = System types cannot be deleted
|
||||||
|
ErrorFieldRequired = Required field not filled
|
||||||
|
|
||||||
|
# PDF Export
|
||||||
|
PDFExportTemplate = PDF Export Template
|
||||||
|
PDFFontSettings = PDF Font Sizes
|
||||||
|
PDFFontHeader = Header Font Size
|
||||||
|
PDFFontContent = Content Font Size
|
||||||
|
PDFFontFields = Field Label Font Size
|
||||||
|
PDFFontHeaderHelp = Font size for element names (Default: 9pt)
|
||||||
|
PDFFontContentHelp = Font size for field values (Default: 7pt)
|
||||||
|
PDFFontFieldsHelp = Font size for field labels (Default: 7pt)
|
||||||
|
PDFTemplate = PDF Template
|
||||||
|
CurrentTemplate = Current Template
|
||||||
|
NoTemplateUploaded = No template uploaded
|
||||||
|
UploadNewTemplate = Upload new template
|
||||||
|
DeleteTemplate = Delete template
|
||||||
|
ConfirmDeleteTemplate = Are you sure you want to delete this template?
|
||||||
|
TemplateUploadSuccess = Template has been uploaded successfully
|
||||||
|
TemplateDeleted = Template has been deleted
|
||||||
|
ErrorOnlyPDFAllowed = Only PDF files are allowed
|
||||||
|
ErrorUploadFailed = Upload failed
|
||||||
|
ErrorNoFileSelected = No file selected
|
||||||
|
PDFTemplateHelp = Upload a PDF file as background/letterhead for export. The first page will be used as template.
|
||||||
|
ExportTreeAsPDF = Export as PDF
|
||||||
|
|
||||||
|
# View Mode
|
||||||
|
DefaultViewMode = Default view mode for installations
|
||||||
|
ViewModeTree = Tree view (classic)
|
||||||
|
ViewModeGraph = Graph view (Cytoscape)
|
||||||
|
SpatialView = Spatial
|
||||||
|
TechnicalView = Technical
|
||||||
|
GraphLoading = Loading graph...
|
||||||
|
GraphLegendRoom = Room/Building
|
||||||
|
GraphLegendDevice = Device
|
||||||
|
GraphLegendCable = Cable
|
||||||
|
GraphLegendPassthrough = Passthrough
|
||||||
|
GraphLegendHierarchy = Hierarchy
|
||||||
|
|
||||||
|
# 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
|
||||||
|
Delete = Delete
|
||||||
|
Enabled = Enabled
|
||||||
|
Disabled = Disabled
|
||||||
|
RecordSaved = Record saved
|
||||||
|
RecordDeleted = Record deleted
|
||||||
|
BackToModuleList = Back to module list
|
||||||
|
Error = Error
|
||||||
|
Select = Select
|
||||||
|
Add = Add
|
||||||
|
AddChild = Add child element
|
||||||
|
Label = Label
|
||||||
|
Type = Type
|
||||||
|
Upload = Upload
|
||||||
|
View = View
|
||||||
|
Back = Back
|
||||||
|
Modify = Modify
|
||||||
|
Edit = Edit
|
||||||
|
Close = Close
|
||||||
|
Confirm = Confirm
|
||||||
|
Yes = Yes
|
||||||
|
No = No
|
||||||
|
|
||||||
|
# Building Types
|
||||||
|
BuildingStructure = Building / Location
|
||||||
|
BuildingLevelBuilding = Building
|
||||||
|
BuildingLevelFloor = Floor / Level
|
||||||
|
BuildingLevelWing = Wing
|
||||||
|
BuildingLevelCorridor = Corridor / Hallway
|
||||||
|
BuildingLevelRoom = Room
|
||||||
|
BuildingLevelArea = Area / Zone
|
||||||
|
|
||||||
|
# Graph View
|
||||||
|
TreeView = Tree View
|
||||||
|
GraphView = Graph View
|
||||||
|
GraphLoading = Loading graph...
|
||||||
|
SearchPlaceholder = Search...
|
||||||
160
lib/graph_view.lib.php
Executable file
160
lib/graph_view.lib.php
Executable file
|
|
@ -0,0 +1,160 @@
|
||||||
|
<?php
|
||||||
|
/* Copyright (C) 2026 Alles Watt lauft
|
||||||
|
*
|
||||||
|
* Gemeinsame Graph-Ansicht Funktionen für anlagen.php und contact_anlagen.php
|
||||||
|
* Vermeidet doppelten Code für Toolbar, Container, Kontextmenü, Legende
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Graph-spezifische JS/CSS-Dateien zu den Asset-Arrays hinzufügen
|
||||||
|
*
|
||||||
|
* @param array $jsFiles Referenz auf JS-Array
|
||||||
|
* @param array $cssFiles Referenz auf CSS-Array
|
||||||
|
* @param string $viewMode 'tree' oder 'graph'
|
||||||
|
*/
|
||||||
|
function kundenkarte_graph_add_assets(&$jsFiles, &$cssFiles, $viewMode)
|
||||||
|
{
|
||||||
|
if ($viewMode === 'graph') {
|
||||||
|
$jsFiles[] = '/kundenkarte/js/dagre.min.js';
|
||||||
|
$jsFiles[] = '/kundenkarte/js/cytoscape.min.js';
|
||||||
|
$jsFiles[] = '/kundenkarte/js/cytoscape-dagre.js';
|
||||||
|
$jsFiles[] = '/kundenkarte/js/kundenkarte_cytoscape.js?v='.time();
|
||||||
|
$cssFiles[] = '/kundenkarte/css/kundenkarte_cytoscape.css?v='.time();
|
||||||
|
} else {
|
||||||
|
array_unshift($jsFiles, '/kundenkarte/js/pathfinding.min.js');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Graph-Toolbar rendern (2 Zeilen: Aktionen + Steuerung)
|
||||||
|
* Wird UNTER der System-Tab-Borderlinie ausgegeben
|
||||||
|
*
|
||||||
|
* @param array $params Konfiguration:
|
||||||
|
* 'socid' int Kunden-ID
|
||||||
|
* 'contactid' int Kontakt-ID (0 für Kundenansicht)
|
||||||
|
* 'systemid' int Aktuelles System
|
||||||
|
* 'viewMode' string 'tree' oder 'graph'
|
||||||
|
* 'permissiontoadd' bool Schreibberechtigung
|
||||||
|
* 'pageUrl' string Aktuelle Seiten-URL ($_SERVER['PHP_SELF'])
|
||||||
|
*/
|
||||||
|
function kundenkarte_graph_print_toolbar($params)
|
||||||
|
{
|
||||||
|
global $langs;
|
||||||
|
|
||||||
|
$socId = (int) ($params['socid'] ?? 0);
|
||||||
|
$contactId = (int) ($params['contactid'] ?? 0);
|
||||||
|
$systemId = (int) ($params['systemid'] ?? 0);
|
||||||
|
$viewMode = $params['viewMode'] ?? 'tree';
|
||||||
|
$permissiontoadd = !empty($params['permissiontoadd']);
|
||||||
|
$pageUrl = $params['pageUrl'] ?? $_SERVER['PHP_SELF'];
|
||||||
|
|
||||||
|
// View-Toggle URL: ID-Parameter je nach Kontext
|
||||||
|
$idParam = ($contactId > 0) ? $contactId : $socId;
|
||||||
|
$toggleView = ($viewMode === 'graph') ? 'tree' : 'graph';
|
||||||
|
$toggleUrl = $pageUrl.'?id='.$idParam.'&system='.$systemId.'&view='.$toggleView;
|
||||||
|
$toggleIcon = ($viewMode === 'graph') ? 'fa-list' : 'fa-sitemap';
|
||||||
|
$toggleLabel = ($viewMode === 'graph') ? $langs->trans('TreeView') : $langs->trans('GraphView');
|
||||||
|
|
||||||
|
// Connection-URL: bei Kontakten wird socid + contactid übergeben
|
||||||
|
$connUrlParams = 'socid='.$socId;
|
||||||
|
if ($contactId > 0) {
|
||||||
|
$connUrlParams .= '&contactid='.$contactId;
|
||||||
|
}
|
||||||
|
|
||||||
|
print '<div class="kundenkarte-graph-toolbar">';
|
||||||
|
|
||||||
|
// Zeile 1: Ansicht-Wechsel + Aktionen
|
||||||
|
print '<div class="kundenkarte-graph-toolbar-row">';
|
||||||
|
print '<a class="button small" href="'.$toggleUrl.'" title="'.$toggleLabel.'"><i class="fa '.$toggleIcon.'"></i> '.$toggleLabel.'</a>';
|
||||||
|
if ($permissiontoadd) {
|
||||||
|
print '<a class="button small" href="'.$pageUrl.'?id='.$idParam.'&action=create&system='.$systemId.'"><i class="fa fa-plus"></i> '.$langs->trans('AddElement').'</a>';
|
||||||
|
print '<a class="button small" href="'.dol_buildpath('/kundenkarte/anlage_connection.php', 1).'?'.$connUrlParams.'&system_id='.$systemId.'&action=create"><i class="fa fa-plug"></i> '.$langs->trans('AddConnection').'</a>';
|
||||||
|
}
|
||||||
|
print '</div>';
|
||||||
|
|
||||||
|
// Zeile 2: Graph-Steuerung (Anordnen rechts)
|
||||||
|
print '<div class="kundenkarte-graph-toolbar-row">';
|
||||||
|
print '<button type="button" class="button small" id="btn-graph-reset-layout" title="Layout zurücksetzen"><i class="fa fa-refresh"></i> Layout</button>';
|
||||||
|
print '<button type="button" class="button small" id="btn-graph-wheel-zoom" title="Mausrad-Zoom"><i class="fa fa-arrows"></i> Scroll</button>';
|
||||||
|
print '<button type="button" class="button small" id="btn-graph-zoom-in" title="Vergrößern"><i class="fa fa-search-plus"></i></button>';
|
||||||
|
print '<button type="button" class="button small" id="btn-graph-zoom-out" title="Verkleinern"><i class="fa fa-search-minus"></i></button>';
|
||||||
|
print '<button type="button" class="button small" id="btn-graph-fit" title="Einpassen"><i class="fa fa-crosshairs"></i> Fit</button>';
|
||||||
|
print '<button type="button" class="button small" id="btn-graph-export-png" title="PNG exportieren"><i class="fa fa-download"></i> PNG</button>';
|
||||||
|
if ($permissiontoadd) {
|
||||||
|
print '<span class="kundenkarte-graph-toolbar-spacer"></span>';
|
||||||
|
print '<button type="button" class="button small" id="btn-graph-edit-mode" title="Elemente anordnen"><i class="fa fa-hand-paper-o"></i> Anordnen</button>';
|
||||||
|
print '<button type="button" class="button small btn-graph-save" id="btn-graph-save-positions" title="Positionen speichern" style="display:none;"><i class="fa fa-check"></i> Speichern</button>';
|
||||||
|
print '<button type="button" class="button small btn-graph-cancel" id="btn-graph-cancel-edit" title="Abbrechen" style="display:none;"><i class="fa fa-times"></i> Abbrechen</button>';
|
||||||
|
}
|
||||||
|
print '</div>';
|
||||||
|
|
||||||
|
print '</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Graph-Container, Kontextmenü und Legende rendern
|
||||||
|
*
|
||||||
|
* @param array $params Konfiguration:
|
||||||
|
* 'socid' int Kunden-ID
|
||||||
|
* 'contactid' int Kontakt-ID (0 für Kundenansicht)
|
||||||
|
* 'systemid' int Aktuelles System
|
||||||
|
* 'permissiontoadd' bool Schreibberechtigung
|
||||||
|
* 'permissiontodelete' bool Löschberechtigung
|
||||||
|
* 'pageUrl' string Aktuelle Seiten-URL
|
||||||
|
*/
|
||||||
|
function kundenkarte_graph_print_container($params)
|
||||||
|
{
|
||||||
|
global $langs;
|
||||||
|
|
||||||
|
$socId = (int) ($params['socid'] ?? 0);
|
||||||
|
$contactId = (int) ($params['contactid'] ?? 0);
|
||||||
|
$systemId = (int) ($params['systemid'] ?? 0);
|
||||||
|
$permissiontoadd = !empty($params['permissiontoadd']);
|
||||||
|
$permissiontodelete = !empty($params['permissiontodelete']);
|
||||||
|
$pageUrl = $params['pageUrl'] ?? $_SERVER['PHP_SELF'];
|
||||||
|
|
||||||
|
$graphAjaxUrl = dol_buildpath('/kundenkarte/ajax/graph_data.php', 1);
|
||||||
|
$graphSaveUrl = dol_buildpath('/kundenkarte/ajax/graph_save_positions.php', 1);
|
||||||
|
$graphModuleUrl = dol_buildpath('/kundenkarte', 1);
|
||||||
|
|
||||||
|
print '<div class="kundenkarte-graph-wrapper">';
|
||||||
|
|
||||||
|
// Suchfeld als Overlay
|
||||||
|
print '<div class="kundenkarte-graph-search-floating">';
|
||||||
|
print '<input type="text" id="kundenkarte-graph-search" placeholder="'.$langs->trans('SearchPlaceholder').'" autocomplete="off">';
|
||||||
|
print '</div>';
|
||||||
|
|
||||||
|
// Graph-Container mit Data-Attributen für JS-Initialisierung
|
||||||
|
print '<div id="kundenkarte-graph-container"';
|
||||||
|
print ' data-ajax-url="'.dol_escape_htmltag($graphAjaxUrl).'"';
|
||||||
|
print ' data-save-url="'.dol_escape_htmltag($graphSaveUrl).'"';
|
||||||
|
print ' data-module-url="'.dol_escape_htmltag($graphModuleUrl).'"';
|
||||||
|
print ' data-socid="'.$socId.'"';
|
||||||
|
if ($contactId > 0) {
|
||||||
|
print ' data-contactid="'.$contactId.'"';
|
||||||
|
}
|
||||||
|
print ' data-systemid="'.$systemId.'"';
|
||||||
|
print ' data-can-edit="'.($permissiontoadd ? '1' : '0').'"';
|
||||||
|
print ' data-can-delete="'.($permissiontodelete ? '1' : '0').'"';
|
||||||
|
print ' data-page-url="'.dol_escape_htmltag($pageUrl).'"';
|
||||||
|
print '>';
|
||||||
|
print '<div class="kundenkarte-graph-loading"><i class="fa fa-spinner fa-spin"></i> '.$langs->trans('GraphLoading').'</div>';
|
||||||
|
print '</div>';
|
||||||
|
|
||||||
|
// Kontextmenü (Rechtsklick auf Node)
|
||||||
|
print '<div id="kundenkarte-graph-contextmenu" class="kundenkarte-graph-contextmenu" style="display:none;">';
|
||||||
|
print '<a class="ctx-item ctx-view" data-action="view"><i class="fa fa-eye"></i> '.$langs->trans('View').'</a>';
|
||||||
|
if ($permissiontoadd) {
|
||||||
|
print '<a class="ctx-item ctx-add-child" data-action="add-child"><i class="fa fa-plus"></i> '.$langs->trans('AddChild').'</a>';
|
||||||
|
print '<a class="ctx-item ctx-edit" data-action="edit"><i class="fa fa-edit"></i> '.$langs->trans('Edit').'</a>';
|
||||||
|
print '<a class="ctx-item ctx-copy" data-action="copy"><i class="fa fa-copy"></i> '.$langs->trans('Copy').'</a>';
|
||||||
|
}
|
||||||
|
if ($permissiontodelete) {
|
||||||
|
print '<a class="ctx-item ctx-delete" data-action="delete"><i class="fa fa-trash"></i> '.$langs->trans('Delete').'</a>';
|
||||||
|
}
|
||||||
|
print '</div>';
|
||||||
|
|
||||||
|
// Legende - wird dynamisch vom JS befüllt (Kabeltypen mit Farben)
|
||||||
|
print '<div id="kundenkarte-graph-legend" class="kundenkarte-graph-legend"></div>';
|
||||||
|
print '</div>'; // End kundenkarte-graph-wrapper
|
||||||
|
}
|
||||||
248
lib/kundenkarte.lib.php
Executable file
248
lib/kundenkarte.lib.php
Executable file
|
|
@ -0,0 +1,248 @@
|
||||||
|
<?php
|
||||||
|
/* Copyright (C) 2026 Eduard Wisch <data@data-it-solution.de>
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* \file kundenkarte/lib/kundenkarte.lib.php
|
||||||
|
* \ingroup kundenkarte
|
||||||
|
* \brief Library files with common functions for KundenKarte
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prepare admin pages header
|
||||||
|
*
|
||||||
|
* @return array<array{string,string,string}>
|
||||||
|
*/
|
||||||
|
function kundenkarteAdminPrepareHead()
|
||||||
|
{
|
||||||
|
global $langs, $conf;
|
||||||
|
|
||||||
|
// global $db;
|
||||||
|
// $extrafields = new ExtraFields($db);
|
||||||
|
// $extrafields->fetch_name_optionals_label('myobject');
|
||||||
|
|
||||||
|
$langs->load("kundenkarte@kundenkarte");
|
||||||
|
|
||||||
|
$h = 0;
|
||||||
|
$head = array();
|
||||||
|
|
||||||
|
$head[$h][0] = dol_buildpath("/kundenkarte/admin/setup.php", 1);
|
||||||
|
$head[$h][1] = $langs->trans("Settings");
|
||||||
|
$head[$h][2] = 'settings';
|
||||||
|
$h++;
|
||||||
|
|
||||||
|
$head[$h][0] = dol_buildpath("/kundenkarte/admin/anlage_systems.php", 1);
|
||||||
|
$head[$h][1] = $langs->trans("AnlagenSystems");
|
||||||
|
$head[$h][2] = 'systems';
|
||||||
|
$h++;
|
||||||
|
|
||||||
|
$head[$h][0] = dol_buildpath("/kundenkarte/admin/anlage_types.php", 1);
|
||||||
|
$head[$h][1] = $langs->trans("AnlagenTypes");
|
||||||
|
$head[$h][2] = 'types';
|
||||||
|
$h++;
|
||||||
|
|
||||||
|
$head[$h][0] = dol_buildpath("/kundenkarte/admin/equipment_types.php", 1);
|
||||||
|
$head[$h][1] = $langs->trans("EquipmentTypes");
|
||||||
|
$head[$h][2] = 'equipment_types';
|
||||||
|
$h++;
|
||||||
|
|
||||||
|
$head[$h][0] = dol_buildpath("/kundenkarte/admin/busbar_types.php", 1);
|
||||||
|
$head[$h][1] = $langs->trans("BusbarTypes");
|
||||||
|
$head[$h][2] = 'busbar_types';
|
||||||
|
$h++;
|
||||||
|
|
||||||
|
$head[$h][0] = dol_buildpath("/kundenkarte/admin/medium_types.php", 1);
|
||||||
|
$head[$h][1] = $langs->trans("MediumTypes");
|
||||||
|
$head[$h][2] = 'medium_types';
|
||||||
|
$h++;
|
||||||
|
|
||||||
|
$head[$h][0] = dol_buildpath("/kundenkarte/admin/building_types.php", 1);
|
||||||
|
$head[$h][1] = $langs->trans("BuildingTypes");
|
||||||
|
$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");
|
||||||
|
$nbExtrafields = (isset($extrafields->attributes['myobject']['label']) && is_countable($extrafields->attributes['myobject']['label'])) ? count($extrafields->attributes['myobject']['label']) : 0;
|
||||||
|
if ($nbExtrafields > 0) {
|
||||||
|
$head[$h][1] .= '<span class="badge marginleftonlyshort">' . $nbExtrafields . '</span>';
|
||||||
|
}
|
||||||
|
$head[$h][2] = 'myobject_extrafields';
|
||||||
|
$h++;
|
||||||
|
|
||||||
|
$head[$h][0] = dol_buildpath("/kundenkarte/admin/myobjectline_extrafields.php", 1);
|
||||||
|
$head[$h][1] = $langs->trans("ExtraFieldsLines");
|
||||||
|
$nbExtrafields = (isset($extrafields->attributes['myobjectline']['label']) && is_countable($extrafields->attributes['myobjectline']['label'])) ? count($extrafields->attributes['myobject']['label']) : 0;
|
||||||
|
if ($nbExtrafields > 0) {
|
||||||
|
$head[$h][1] .= '<span class="badge marginleftonlyshort">' . $nbExtrafields . '</span>';
|
||||||
|
}
|
||||||
|
$head[$h][2] = 'myobject_extrafieldsline';
|
||||||
|
$h++;
|
||||||
|
*/
|
||||||
|
|
||||||
|
$head[$h][0] = dol_buildpath("/kundenkarte/admin/about.php", 1);
|
||||||
|
$head[$h][1] = $langs->trans("About");
|
||||||
|
$head[$h][2] = 'about';
|
||||||
|
$h++;
|
||||||
|
|
||||||
|
// Show more tabs from modules
|
||||||
|
// Entries must be declared in modules descriptor with line
|
||||||
|
//$this->tabs = array(
|
||||||
|
// 'entity:+tabname:Title:@kundenkarte:/kundenkarte/mypage.php?id=__ID__'
|
||||||
|
//); // to add new tab
|
||||||
|
//$this->tabs = array(
|
||||||
|
// 'entity:-tabname:Title:@kundenkarte:/kundenkarte/mypage.php?id=__ID__'
|
||||||
|
//); // to remove a tab
|
||||||
|
complete_head_from_modules($conf, $langs, null, $head, $h, 'kundenkarte@kundenkarte');
|
||||||
|
|
||||||
|
complete_head_from_modules($conf, $langs, null, $head, $h, 'kundenkarte@kundenkarte', 'remove');
|
||||||
|
|
||||||
|
return $head;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render an icon - either Font Awesome or custom image
|
||||||
|
*
|
||||||
|
* @param string $picto Icon identifier (fa-xxx or img:url)
|
||||||
|
* @param string $alt Alt text for images
|
||||||
|
* @param string $style Additional inline style
|
||||||
|
* @return string HTML for the icon
|
||||||
|
*/
|
||||||
|
function kundenkarte_render_icon($picto, $alt = '', $style = '')
|
||||||
|
{
|
||||||
|
if (empty($picto)) {
|
||||||
|
return '<i class="fa fa-cube"'.($style ? ' style="'.$style.'"' : '').'></i>';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if it's a custom image (starts with img:)
|
||||||
|
if (strpos($picto, 'img:') === 0) {
|
||||||
|
$url = substr($picto, 4); // Remove 'img:' prefix
|
||||||
|
$styleAttr = 'max-width:20px;max-height:20px;vertical-align:middle;'.($style ? $style : '');
|
||||||
|
return '<img src="'.dol_escape_htmltag($url).'" alt="'.dol_escape_htmltag($alt).'" style="'.$styleAttr.'">';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
27
manifest.json
Normal file
27
manifest.json
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
{
|
||||||
|
"name": "KundenKarte Dokumentation",
|
||||||
|
"short_name": "KundenKarte",
|
||||||
|
"description": "Schaltschrank-Dokumentation für Elektriker - Felder, Hutschienen, Automaten erfassen",
|
||||||
|
"start_url": "./pwa.php",
|
||||||
|
"scope": "./",
|
||||||
|
"display": "standalone",
|
||||||
|
"orientation": "portrait",
|
||||||
|
"background_color": "#1a1a2e",
|
||||||
|
"theme_color": "#3498db",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "img/pwa-icon-192.png",
|
||||||
|
"sizes": "192x192",
|
||||||
|
"type": "image/png",
|
||||||
|
"purpose": "any maskable"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "img/pwa-icon-512.png",
|
||||||
|
"sizes": "512x512",
|
||||||
|
"type": "image/png",
|
||||||
|
"purpose": "any maskable"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"categories": ["business", "productivity", "utilities"],
|
||||||
|
"lang": "de-DE"
|
||||||
|
}
|
||||||
3
modulebuilder.txt
Executable file
3
modulebuilder.txt
Executable file
|
|
@ -0,0 +1,3 @@
|
||||||
|
# DO NOT DELETE THIS FILE MANUALLY
|
||||||
|
# File to flag module built using official module template.
|
||||||
|
# When this file is present into a module directory, you can edit it with the module builder tool.
|
||||||
238
pwa.php
Normal file
238
pwa.php
Normal file
|
|
@ -0,0 +1,238 @@
|
||||||
|
<?php
|
||||||
|
/* Copyright (C) 2026 Alles Watt lauft
|
||||||
|
*
|
||||||
|
* KundenKarte PWA - Mobile Schaltschrank-Dokumentation
|
||||||
|
* Offline-fähig für Elektriker vor Ort
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Kein Dolibarr-Login erforderlich - eigenes Token-System
|
||||||
|
if (!defined('NOLOGIN')) {
|
||||||
|
define('NOLOGIN', '1');
|
||||||
|
}
|
||||||
|
if (!defined('NOREQUIREMENU')) {
|
||||||
|
define('NOREQUIREMENU', '1');
|
||||||
|
}
|
||||||
|
if (!defined('NOREQUIREHTML')) {
|
||||||
|
define('NOREQUIREHTML', '1');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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("Dolibarr konnte nicht geladen werden");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Theme color from Dolibarr
|
||||||
|
$themeColor = getDolGlobalString('THEME_ELDY_TOPMENU_BACK1', '#3498db');
|
||||||
|
|
||||||
|
?><!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
|
||||||
|
<meta name="theme-color" content="<?php echo $themeColor; ?>">
|
||||||
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||||
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||||||
|
<meta name="apple-mobile-web-app-title" content="KundenKarte">
|
||||||
|
<title>KundenKarte</title>
|
||||||
|
<link rel="manifest" href="manifest.json">
|
||||||
|
<link rel="apple-touch-icon" href="img/pwa-icon-192.png">
|
||||||
|
<link rel="stylesheet" href="css/pwa.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app" class="app">
|
||||||
|
|
||||||
|
<!-- Login Screen -->
|
||||||
|
<div id="screen-login" class="screen active">
|
||||||
|
<div class="login-container">
|
||||||
|
<div class="login-logo">
|
||||||
|
<svg viewBox="0 0 24 24" fill="currentColor">
|
||||||
|
<path d="M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm-7 14h-2v-4H6v-2h4V7h2v4h4v2h-4v4z"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h1 class="login-title">KundenKarte</h1>
|
||||||
|
<p class="login-subtitle">Schaltschrank-Dokumentation</p>
|
||||||
|
|
||||||
|
<form id="login-form" class="login-form">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Benutzername</label>
|
||||||
|
<input type="text" id="login-user" autocomplete="username" required>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Passwort</label>
|
||||||
|
<input type="password" id="login-pass" autocomplete="current-password" required>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary btn-large">
|
||||||
|
Anmelden
|
||||||
|
</button>
|
||||||
|
<p id="login-error" class="error-text"></p>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Kundensuche Screen -->
|
||||||
|
<div id="screen-search" class="screen">
|
||||||
|
<header class="header">
|
||||||
|
<h1>Kunde wählen</h1>
|
||||||
|
<button id="btn-logout" class="btn-icon" title="Abmelden">
|
||||||
|
<svg viewBox="0 0 24 24"><path d="M17 7l-1.41 1.41L18.17 11H8v2h10.17l-2.58 2.58L17 17l5-5zM4 5h8V3H4c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h8v-2H4V5z"/></svg>
|
||||||
|
</button>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="search-container">
|
||||||
|
<div class="search-box">
|
||||||
|
<svg viewBox="0 0 24 24"><path d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"/></svg>
|
||||||
|
<input type="text" id="search-customer" placeholder="Kunde suchen...">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="customer-list" class="list">
|
||||||
|
<!-- Kunden werden hier geladen -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="offline-indicator" class="offline-bar hidden">
|
||||||
|
⚡ Offline-Modus
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Anlagen Screen -->
|
||||||
|
<div id="screen-anlagen" class="screen">
|
||||||
|
<header class="header">
|
||||||
|
<button id="btn-back-search" class="btn-icon">
|
||||||
|
<svg viewBox="0 0 24 24"><path d="M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z"/></svg>
|
||||||
|
</button>
|
||||||
|
<h1 id="customer-name">Kunde</h1>
|
||||||
|
<div class="header-spacer"></div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div id="anlagen-list" class="list anlagen-grid">
|
||||||
|
<!-- Anlagen werden hier geladen -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Editor Screen -->
|
||||||
|
<div id="screen-editor" class="screen">
|
||||||
|
<header class="header">
|
||||||
|
<button id="btn-back-anlagen" class="btn-icon">
|
||||||
|
<svg viewBox="0 0 24 24"><path d="M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z"/></svg>
|
||||||
|
</button>
|
||||||
|
<h1 id="anlage-name">Anlage</h1>
|
||||||
|
<button id="btn-sync" class="btn-icon sync-btn">
|
||||||
|
<svg viewBox="0 0 24 24"><path d="M12 4V1L8 5l4 4V6c3.31 0 6 2.69 6 6 0 1.01-.25 1.97-.7 2.8l1.46 1.46C19.54 15.03 20 13.57 20 12c0-4.42-3.58-8-8-8zm0 14c-3.31 0-6-2.69-6-6 0-1.01.25-1.97.7-2.8L5.24 7.74C4.46 8.97 4 10.43 4 12c0 4.42 3.58 8 8 8v3l4-4-4-4v3z"/></svg>
|
||||||
|
<span id="sync-badge" class="sync-badge hidden">0</span>
|
||||||
|
</button>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div id="editor-content" class="editor-content">
|
||||||
|
<!-- Felder und Hutschienen -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="fab-container">
|
||||||
|
<button id="btn-add-panel" class="fab">
|
||||||
|
<svg viewBox="0 0 24 24"><path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/></svg>
|
||||||
|
<span>Feld</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Automat hinzufügen Modal -->
|
||||||
|
<div id="modal-add-equipment" class="modal">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h2>Automat hinzufügen</h2>
|
||||||
|
<button class="modal-close">×</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<!-- Step 1: Typ wählen -->
|
||||||
|
<div id="step-type" class="step active">
|
||||||
|
<p class="step-label">Typ wählen:</p>
|
||||||
|
<div id="type-grid" class="type-grid">
|
||||||
|
<!-- Typen werden geladen -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Step 2: Werte eingeben -->
|
||||||
|
<div id="step-values" class="step">
|
||||||
|
<p class="step-label">Werte:</p>
|
||||||
|
<div id="value-fields" class="value-fields">
|
||||||
|
<!-- Dynamische Felder -->
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Abgang / Beschriftung</label>
|
||||||
|
<input type="text" id="equipment-label" placeholder="z.B. Küche Licht">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button id="btn-cancel-equipment" class="btn btn-secondary">Abbrechen</button>
|
||||||
|
<button id="btn-save-equipment" class="btn btn-primary">Speichern</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Hutschiene hinzufügen Modal -->
|
||||||
|
<div id="modal-add-carrier" class="modal">
|
||||||
|
<div class="modal-content modal-small">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h2>Hutschiene</h2>
|
||||||
|
<button class="modal-close">×</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<p class="step-label">Größe wählen:</p>
|
||||||
|
<div class="te-grid">
|
||||||
|
<button class="te-btn" data-te="12">12 TE</button>
|
||||||
|
<button class="te-btn" data-te="18">18 TE</button>
|
||||||
|
<button class="te-btn" data-te="24">24 TE</button>
|
||||||
|
<button class="te-btn" data-te="36">36 TE</button>
|
||||||
|
</div>
|
||||||
|
<div class="form-group" style="margin-top:16px;">
|
||||||
|
<label>Bezeichnung (optional)</label>
|
||||||
|
<input type="text" id="carrier-label" placeholder="z.B. Hutschiene 1">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button class="btn btn-secondary modal-close">Abbrechen</button>
|
||||||
|
<button id="btn-save-carrier" class="btn btn-primary">Hinzufügen</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Feld hinzufügen Modal -->
|
||||||
|
<div id="modal-add-panel" class="modal">
|
||||||
|
<div class="modal-content modal-small">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h2>Neues Feld</h2>
|
||||||
|
<button class="modal-close">×</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Bezeichnung</label>
|
||||||
|
<input type="text" id="panel-label" placeholder="z.B. Feld 1">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button class="btn btn-secondary modal-close">Abbrechen</button>
|
||||||
|
<button id="btn-save-panel" class="btn btn-primary">Hinzufügen</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Toast Notifications -->
|
||||||
|
<div id="toast" class="toast"></div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Dolibarr URL für AJAX
|
||||||
|
window.DOLIBARR_URL = '<?php echo DOL_URL_ROOT; ?>';
|
||||||
|
window.MODULE_URL = '<?php echo DOL_URL_ROOT; ?>/custom/kundenkarte';
|
||||||
|
</script>
|
||||||
|
<script src="js/pwa.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
151
pwa_auth.php
Normal file
151
pwa_auth.php
Normal file
|
|
@ -0,0 +1,151 @@
|
||||||
|
<?php
|
||||||
|
/* Copyright (C) 2026 Alles Watt lauft
|
||||||
|
*
|
||||||
|
* PWA Authentication - Token-basierte Authentifizierung
|
||||||
|
* Für Offline-First Mobile App
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (!defined('NOLOGIN')) {
|
||||||
|
define('NOLOGIN', '1');
|
||||||
|
}
|
||||||
|
if (!defined('NOREQUIREMENU')) {
|
||||||
|
define('NOREQUIREMENU', '1');
|
||||||
|
}
|
||||||
|
if (!defined('NOREQUIREHTML')) {
|
||||||
|
define('NOREQUIREHTML', '1');
|
||||||
|
}
|
||||||
|
if (!defined('NOREQUIREAJAX')) {
|
||||||
|
define('NOREQUIREAJAX', '1');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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(json_encode(array('success' => false, 'error' => 'Dolibarr not loaded')));
|
||||||
|
}
|
||||||
|
|
||||||
|
header('Content-Type: application/json; charset=UTF-8');
|
||||||
|
|
||||||
|
$action = GETPOST('action', 'aZ09');
|
||||||
|
$response = array('success' => false);
|
||||||
|
|
||||||
|
switch ($action) {
|
||||||
|
case 'login':
|
||||||
|
$username = GETPOST('username', 'alphanohtml');
|
||||||
|
$password = GETPOST('password', 'none');
|
||||||
|
|
||||||
|
if (empty($username) || empty($password)) {
|
||||||
|
$response['error'] = 'Benutzername und Passwort erforderlich';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Brute force protection
|
||||||
|
usleep(100000); // 100ms delay
|
||||||
|
|
||||||
|
// Load user
|
||||||
|
require_once DOL_DOCUMENT_ROOT.'/user/class/user.class.php';
|
||||||
|
$userLogin = new User($db);
|
||||||
|
|
||||||
|
// Find user by login
|
||||||
|
$sql = "SELECT rowid FROM ".MAIN_DB_PREFIX."user WHERE login = '".$db->escape($username)."' AND statut = 1";
|
||||||
|
$result = $db->query($sql);
|
||||||
|
|
||||||
|
if ($result && $db->num_rows($result) > 0) {
|
||||||
|
$obj = $db->fetch_object($result);
|
||||||
|
$userLogin->fetch($obj->rowid);
|
||||||
|
|
||||||
|
// Check password
|
||||||
|
require_once DOL_DOCUMENT_ROOT.'/core/lib/security2.lib.php';
|
||||||
|
|
||||||
|
$passOk = false;
|
||||||
|
if (!empty($userLogin->pass_indatabase_crypted)) {
|
||||||
|
// Check crypted password
|
||||||
|
$passOk = dol_verifyHash($password, $userLogin->pass_indatabase_crypted);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($passOk) {
|
||||||
|
// Check kundenkarte permission
|
||||||
|
if ($userLogin->hasRight('kundenkarte', 'read')) {
|
||||||
|
// Generate token (valid for 15 days)
|
||||||
|
$tokenData = array(
|
||||||
|
'user_id' => $userLogin->id,
|
||||||
|
'login' => $userLogin->login,
|
||||||
|
'created' => time(),
|
||||||
|
'expires' => time() + (15 * 24 * 60 * 60),
|
||||||
|
'hash' => md5($userLogin->id . $userLogin->login . getDolGlobalString('MAIN_SECURITY_SALT', 'defaultsalt'))
|
||||||
|
);
|
||||||
|
$token = base64_encode(json_encode($tokenData));
|
||||||
|
|
||||||
|
$response['success'] = true;
|
||||||
|
$response['token'] = $token;
|
||||||
|
$response['user'] = array(
|
||||||
|
'id' => $userLogin->id,
|
||||||
|
'login' => $userLogin->login,
|
||||||
|
'name' => $userLogin->getFullName($langs)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
$response['error'] = 'Keine Berechtigung für KundenKarte';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$response['error'] = 'Falsches Passwort';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$response['error'] = 'Benutzer nicht gefunden';
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'verify':
|
||||||
|
$token = GETPOST('token', 'none');
|
||||||
|
|
||||||
|
if (empty($token)) {
|
||||||
|
$response['error'] = 'Kein Token';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
$tokenData = json_decode(base64_decode($token), true);
|
||||||
|
|
||||||
|
if (!$tokenData || empty($tokenData['user_id']) || empty($tokenData['expires'])) {
|
||||||
|
$response['error'] = 'Ungültiges Token';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check expiration
|
||||||
|
if ($tokenData['expires'] < time()) {
|
||||||
|
$response['error'] = 'Token abgelaufen';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify hash
|
||||||
|
$expectedHash = md5($tokenData['user_id'] . $tokenData['login'] . getDolGlobalString('MAIN_SECURITY_SALT', 'defaultsalt'));
|
||||||
|
if ($tokenData['hash'] !== $expectedHash) {
|
||||||
|
$response['error'] = 'Token manipuliert';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load user to verify still active
|
||||||
|
require_once DOL_DOCUMENT_ROOT.'/user/class/user.class.php';
|
||||||
|
$userCheck = new User($db);
|
||||||
|
if ($userCheck->fetch($tokenData['user_id']) > 0 && $userCheck->statut == 1) {
|
||||||
|
$response['success'] = true;
|
||||||
|
$response['user'] = array(
|
||||||
|
'id' => $userCheck->id,
|
||||||
|
'login' => $userCheck->login,
|
||||||
|
'name' => $userCheck->getFullName($langs)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
$response['error'] = 'Benutzer nicht mehr aktiv';
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
$response['error'] = 'Unbekannte Aktion';
|
||||||
|
}
|
||||||
|
|
||||||
|
echo json_encode($response);
|
||||||
|
$db->close();
|
||||||
29
sql/data.sql
Executable file
29
sql/data.sql
Executable file
|
|
@ -0,0 +1,29 @@
|
||||||
|
-- ============================================================================
|
||||||
|
-- Copyright (C) 2026 Alles Watt lauft
|
||||||
|
--
|
||||||
|
-- Initial data for KundenKarte module
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
-- ============================================================================
|
||||||
|
-- HINWEIS / NOTE:
|
||||||
|
-- ============================================================================
|
||||||
|
-- Diese Datei enthaelt KEINE vordefinierten Daten.
|
||||||
|
-- This file contains NO predefined data.
|
||||||
|
--
|
||||||
|
-- Alle Systeme, Element-Typen und Felder koennen ueber die Admin-Seiten
|
||||||
|
-- erstellt werden:
|
||||||
|
-- All systems, element types and fields can be created via the admin pages:
|
||||||
|
--
|
||||||
|
-- - Admin > Module > KundenKarte > Anlagen-Systeme (anlage_systems.php)
|
||||||
|
-- -> Neue System-Kategorien erstellen (z.B. Strom, Internet, Heizung...)
|
||||||
|
-- -> Create new system categories (e.g. Electrical, Network, Heating...)
|
||||||
|
--
|
||||||
|
-- - Admin > Module > KundenKarte > Anlagen-Typen (anlage_types.php)
|
||||||
|
-- -> Neue Element-Typen pro System erstellen
|
||||||
|
-- -> Create new element types per system
|
||||||
|
-- -> Hierarchie-Regeln definieren (Kann Kinder haben, Erlaubte Eltern)
|
||||||
|
-- -> Define hierarchy rules (Can have children, Allowed parents)
|
||||||
|
-- -> Felder pro Typ konfigurieren
|
||||||
|
-- -> Configure fields per type
|
||||||
|
--
|
||||||
|
-- ============================================================================
|
||||||
140
sql/data_building_types.sql
Executable file
140
sql/data_building_types.sql
Executable file
|
|
@ -0,0 +1,140 @@
|
||||||
|
-- ============================================================================
|
||||||
|
-- Copyright (C) 2026 Alles Watt lauft
|
||||||
|
--
|
||||||
|
-- Default Building Types (Gebäudetypen)
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
-- Clear existing system entries
|
||||||
|
DELETE FROM llx_kundenkarte_building_type WHERE is_system = 1;
|
||||||
|
|
||||||
|
-- ============================================================================
|
||||||
|
-- BUILDING LEVEL (Gebäude)
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
INSERT INTO llx_kundenkarte_building_type (entity, ref, label, label_short, level_type, icon, color, is_system, can_have_children, position, active, date_creation)
|
||||||
|
VALUES (0, 'HAUS', 'Haus', 'Haus', 'building', 'fa-home', '#3498db', 1, 1, 10, 1, NOW());
|
||||||
|
|
||||||
|
INSERT INTO llx_kundenkarte_building_type (entity, ref, label, label_short, level_type, icon, color, is_system, can_have_children, position, active, date_creation)
|
||||||
|
VALUES (0, 'HALLE', 'Halle', 'Halle', 'building', 'fa-warehouse', '#e67e22', 1, 1, 20, 1, NOW());
|
||||||
|
|
||||||
|
INSERT INTO llx_kundenkarte_building_type (entity, ref, label, label_short, level_type, icon, color, is_system, can_have_children, position, active, date_creation)
|
||||||
|
VALUES (0, 'STALL', 'Stall', 'Stall', 'building', 'fa-horse', '#8e44ad', 1, 1, 30, 1, NOW());
|
||||||
|
|
||||||
|
INSERT INTO llx_kundenkarte_building_type (entity, ref, label, label_short, level_type, icon, color, is_system, can_have_children, position, active, date_creation)
|
||||||
|
VALUES (0, 'GARAGE', 'Garage', 'Garage', 'building', 'fa-car', '#2c3e50', 1, 1, 40, 1, NOW());
|
||||||
|
|
||||||
|
INSERT INTO llx_kundenkarte_building_type (entity, ref, label, label_short, level_type, icon, color, is_system, can_have_children, position, active, date_creation)
|
||||||
|
VALUES (0, 'SCHUPPEN', 'Schuppen', 'Schuppen', 'building', 'fa-box', '#7f8c8d', 1, 1, 50, 1, NOW());
|
||||||
|
|
||||||
|
INSERT INTO llx_kundenkarte_building_type (entity, ref, label, label_short, level_type, icon, color, is_system, can_have_children, position, active, date_creation)
|
||||||
|
VALUES (0, 'BUEROGEBAEUDE', 'Bürogebäude', 'Büro', 'building', 'fa-building', '#1abc9c', 1, 1, 60, 1, NOW());
|
||||||
|
|
||||||
|
-- ============================================================================
|
||||||
|
-- FLOOR LEVEL (Etage/Geschoss)
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
INSERT INTO llx_kundenkarte_building_type (entity, ref, label, label_short, level_type, icon, color, is_system, can_have_children, position, active, date_creation)
|
||||||
|
VALUES (0, 'KELLER', 'Keller', 'KG', 'floor', 'fa-level-down-alt', '#34495e', 1, 1, 100, 1, NOW());
|
||||||
|
|
||||||
|
INSERT INTO llx_kundenkarte_building_type (entity, ref, label, label_short, level_type, icon, color, is_system, can_have_children, position, active, date_creation)
|
||||||
|
VALUES (0, 'ERDGESCHOSS', 'Erdgeschoss', 'EG', 'floor', 'fa-layer-group', '#27ae60', 1, 1, 110, 1, NOW());
|
||||||
|
|
||||||
|
INSERT INTO llx_kundenkarte_building_type (entity, ref, label, label_short, level_type, icon, color, is_system, can_have_children, position, active, date_creation)
|
||||||
|
VALUES (0, 'OBERGESCHOSS', 'Obergeschoss', 'OG', 'floor', 'fa-level-up-alt', '#2980b9', 1, 1, 120, 1, NOW());
|
||||||
|
|
||||||
|
INSERT INTO llx_kundenkarte_building_type (entity, ref, label, label_short, level_type, icon, color, is_system, can_have_children, position, active, date_creation)
|
||||||
|
VALUES (0, 'DACHGESCHOSS', 'Dachgeschoss', 'DG', 'floor', 'fa-home', '#9b59b6', 1, 1, 130, 1, NOW());
|
||||||
|
|
||||||
|
INSERT INTO llx_kundenkarte_building_type (entity, ref, label, label_short, level_type, icon, color, is_system, can_have_children, position, active, date_creation)
|
||||||
|
VALUES (0, 'SPITZBODEN', 'Spitzboden', 'SB', 'floor', 'fa-mountain', '#95a5a6', 1, 1, 140, 1, NOW());
|
||||||
|
|
||||||
|
-- ============================================================================
|
||||||
|
-- WING LEVEL (Gebäudeflügel/Trakt)
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
INSERT INTO llx_kundenkarte_building_type (entity, ref, label, label_short, level_type, icon, color, is_system, can_have_children, position, active, date_creation)
|
||||||
|
VALUES (0, 'NORDFLUEGL', 'Nordflügel', 'Nord', 'wing', 'fa-compass', '#3498db', 1, 1, 200, 1, NOW());
|
||||||
|
|
||||||
|
INSERT INTO llx_kundenkarte_building_type (entity, ref, label, label_short, level_type, icon, color, is_system, can_have_children, position, active, date_creation)
|
||||||
|
VALUES (0, 'SUEDFLUEGL', 'Südflügel', 'Süd', 'wing', 'fa-compass', '#e74c3c', 1, 1, 210, 1, NOW());
|
||||||
|
|
||||||
|
INSERT INTO llx_kundenkarte_building_type (entity, ref, label, label_short, level_type, icon, color, is_system, can_have_children, position, active, date_creation)
|
||||||
|
VALUES (0, 'OSTFLUEGL', 'Ostflügel', 'Ost', 'wing', 'fa-compass', '#f39c12', 1, 1, 220, 1, NOW());
|
||||||
|
|
||||||
|
INSERT INTO llx_kundenkarte_building_type (entity, ref, label, label_short, level_type, icon, color, is_system, can_have_children, position, active, date_creation)
|
||||||
|
VALUES (0, 'WESTFLUEGL', 'Westflügel', 'West', 'wing', 'fa-compass', '#2ecc71', 1, 1, 230, 1, NOW());
|
||||||
|
|
||||||
|
INSERT INTO llx_kundenkarte_building_type (entity, ref, label, label_short, level_type, icon, color, is_system, can_have_children, position, active, date_creation)
|
||||||
|
VALUES (0, 'ANBAU', 'Anbau', 'Anbau', 'wing', 'fa-plus-square', '#1abc9c', 1, 1, 240, 1, NOW());
|
||||||
|
|
||||||
|
-- ============================================================================
|
||||||
|
-- CORRIDOR LEVEL (Flur/Gang)
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
INSERT INTO llx_kundenkarte_building_type (entity, ref, label, label_short, level_type, icon, color, is_system, can_have_children, position, active, date_creation)
|
||||||
|
VALUES (0, 'FLUR', 'Flur', 'Flur', 'corridor', 'fa-arrows-alt-h', '#16a085', 1, 1, 300, 1, NOW());
|
||||||
|
|
||||||
|
INSERT INTO llx_kundenkarte_building_type (entity, ref, label, label_short, level_type, icon, color, is_system, can_have_children, position, active, date_creation)
|
||||||
|
VALUES (0, 'EINGANGSHALLE', 'Eingangshalle', 'Eingang', 'corridor', 'fa-door-open', '#2c3e50', 1, 1, 310, 1, NOW());
|
||||||
|
|
||||||
|
INSERT INTO llx_kundenkarte_building_type (entity, ref, label, label_short, level_type, icon, color, is_system, can_have_children, position, active, date_creation)
|
||||||
|
VALUES (0, 'TREPPENHAUS', 'Treppenhaus', 'Treppe', 'corridor', 'fa-stairs', '#8e44ad', 1, 1, 320, 1, NOW());
|
||||||
|
|
||||||
|
-- ============================================================================
|
||||||
|
-- ROOM LEVEL (Räume)
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
INSERT INTO llx_kundenkarte_building_type (entity, ref, label, label_short, level_type, icon, color, is_system, can_have_children, position, active, date_creation)
|
||||||
|
VALUES (0, 'ZIMMER', 'Zimmer', 'Zi', 'room', 'fa-door-closed', '#3498db', 1, 0, 400, 1, NOW());
|
||||||
|
|
||||||
|
INSERT INTO llx_kundenkarte_building_type (entity, ref, label, label_short, level_type, icon, color, is_system, can_have_children, position, active, date_creation)
|
||||||
|
VALUES (0, 'WOHNZIMMER', 'Wohnzimmer', 'WoZi', 'room', 'fa-couch', '#e67e22', 1, 0, 410, 1, NOW());
|
||||||
|
|
||||||
|
INSERT INTO llx_kundenkarte_building_type (entity, ref, label, label_short, level_type, icon, color, is_system, can_have_children, position, active, date_creation)
|
||||||
|
VALUES (0, 'SCHLAFZIMMER', 'Schlafzimmer', 'SchZi', 'room', 'fa-bed', '#9b59b6', 1, 0, 420, 1, NOW());
|
||||||
|
|
||||||
|
INSERT INTO llx_kundenkarte_building_type (entity, ref, label, label_short, level_type, icon, color, is_system, can_have_children, position, active, date_creation)
|
||||||
|
VALUES (0, 'KINDERZIMMER', 'Kinderzimmer', 'KiZi', 'room', 'fa-child', '#1abc9c', 1, 0, 430, 1, NOW());
|
||||||
|
|
||||||
|
INSERT INTO llx_kundenkarte_building_type (entity, ref, label, label_short, level_type, icon, color, is_system, can_have_children, position, active, date_creation)
|
||||||
|
VALUES (0, 'KUECHE', 'Küche', 'Kü', 'room', 'fa-utensils', '#27ae60', 1, 0, 440, 1, NOW());
|
||||||
|
|
||||||
|
INSERT INTO llx_kundenkarte_building_type (entity, ref, label, label_short, level_type, icon, color, is_system, can_have_children, position, active, date_creation)
|
||||||
|
VALUES (0, 'BAD', 'Badezimmer', 'Bad', 'room', 'fa-bath', '#3498db', 1, 0, 450, 1, NOW());
|
||||||
|
|
||||||
|
INSERT INTO llx_kundenkarte_building_type (entity, ref, label, label_short, level_type, icon, color, is_system, can_have_children, position, active, date_creation)
|
||||||
|
VALUES (0, 'WC', 'WC/Toilette', 'WC', 'room', 'fa-toilet', '#95a5a6', 1, 0, 460, 1, NOW());
|
||||||
|
|
||||||
|
INSERT INTO llx_kundenkarte_building_type (entity, ref, label, label_short, level_type, icon, color, is_system, can_have_children, position, active, date_creation)
|
||||||
|
VALUES (0, 'BUERO', 'Büro', 'Büro', 'room', 'fa-desktop', '#2c3e50', 1, 0, 470, 1, NOW());
|
||||||
|
|
||||||
|
INSERT INTO llx_kundenkarte_building_type (entity, ref, label, label_short, level_type, icon, color, is_system, can_have_children, position, active, date_creation)
|
||||||
|
VALUES (0, 'ABSTELLRAUM', 'Abstellraum', 'Abst', 'room', 'fa-box', '#7f8c8d', 1, 0, 480, 1, NOW());
|
||||||
|
|
||||||
|
INSERT INTO llx_kundenkarte_building_type (entity, ref, label, label_short, level_type, icon, color, is_system, can_have_children, position, active, date_creation)
|
||||||
|
VALUES (0, 'HAUSWIRTSCHAFT', 'Hauswirtschaftsraum', 'HWR', 'room', 'fa-tshirt', '#16a085', 1, 0, 490, 1, NOW());
|
||||||
|
|
||||||
|
INSERT INTO llx_kundenkarte_building_type (entity, ref, label, label_short, level_type, icon, color, is_system, can_have_children, position, active, date_creation)
|
||||||
|
VALUES (0, 'WERKSTATT', 'Werkstatt', 'Werkst', 'room', 'fa-tools', '#f39c12', 1, 0, 500, 1, NOW());
|
||||||
|
|
||||||
|
INSERT INTO llx_kundenkarte_building_type (entity, ref, label, label_short, level_type, icon, color, is_system, can_have_children, position, active, date_creation)
|
||||||
|
VALUES (0, 'TECHNIKRAUM', 'Technikraum', 'Tech', 'room', 'fa-cogs', '#e74c3c', 1, 1, 510, 1, NOW());
|
||||||
|
|
||||||
|
INSERT INTO llx_kundenkarte_building_type (entity, ref, label, label_short, level_type, icon, color, is_system, can_have_children, position, active, date_creation)
|
||||||
|
VALUES (0, 'SERVERRAUM', 'Serverraum', 'Server', 'room', 'fa-server', '#8e44ad', 1, 1, 520, 1, NOW());
|
||||||
|
|
||||||
|
-- ============================================================================
|
||||||
|
-- AREA LEVEL (Bereiche/Zonen)
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
INSERT INTO llx_kundenkarte_building_type (entity, ref, label, label_short, level_type, icon, color, is_system, can_have_children, position, active, date_creation)
|
||||||
|
VALUES (0, 'AUSSENBEREICH', 'Außenbereich', 'Außen', 'area', 'fa-tree', '#27ae60', 1, 1, 600, 1, NOW());
|
||||||
|
|
||||||
|
INSERT INTO llx_kundenkarte_building_type (entity, ref, label, label_short, level_type, icon, color, is_system, can_have_children, position, active, date_creation)
|
||||||
|
VALUES (0, 'TERRASSE', 'Terrasse', 'Terrasse', 'area', 'fa-sun', '#f1c40f', 1, 0, 610, 1, NOW());
|
||||||
|
|
||||||
|
INSERT INTO llx_kundenkarte_building_type (entity, ref, label, label_short, level_type, icon, color, is_system, can_have_children, position, active, date_creation)
|
||||||
|
VALUES (0, 'GARTEN', 'Garten', 'Garten', 'area', 'fa-leaf', '#2ecc71', 1, 1, 620, 1, NOW());
|
||||||
|
|
||||||
|
INSERT INTO llx_kundenkarte_building_type (entity, ref, label, label_short, level_type, icon, color, is_system, can_have_children, position, active, date_creation)
|
||||||
|
VALUES (0, 'CARPORT', 'Carport', 'Carport', 'area', 'fa-car-side', '#34495e', 1, 0, 630, 1, NOW());
|
||||||
49
sql/data_busbar_types.sql
Executable file
49
sql/data_busbar_types.sql
Executable file
|
|
@ -0,0 +1,49 @@
|
||||||
|
-- ============================================================================
|
||||||
|
-- Copyright (C) 2026 Alles Watt lauft
|
||||||
|
-- Standard busbar types for electrical installations
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
-- Get the electrical system ID (assuming it's 1, but using subquery to be safe)
|
||||||
|
-- Note: These are inserted for system_id = 1 (Elektroinstallation)
|
||||||
|
|
||||||
|
-- 1-phasige Phasenschienen
|
||||||
|
INSERT INTO llx_kundenkarte_busbar_type (entity, ref, label, label_short, fk_system, phases, num_lines, color, default_color, line_height, line_spacing, position_default, is_system, position, active, date_creation)
|
||||||
|
VALUES (0, 'PS_L1', 'Phasenschiene L1', 'L1', 1, 'L1', 1, '#e74c3c', '#e74c3c', 3, 4, 'below', 1, 10, 1, NOW());
|
||||||
|
|
||||||
|
INSERT INTO llx_kundenkarte_busbar_type (entity, ref, label, label_short, fk_system, phases, num_lines, color, default_color, line_height, line_spacing, position_default, is_system, position, active, date_creation)
|
||||||
|
VALUES (0, 'PS_L2', 'Phasenschiene L2', 'L2', 1, 'L2', 1, '#2ecc71', '#2ecc71', 3, 4, 'below', 1, 20, 1, NOW());
|
||||||
|
|
||||||
|
INSERT INTO llx_kundenkarte_busbar_type (entity, ref, label, label_short, fk_system, phases, num_lines, color, default_color, line_height, line_spacing, position_default, is_system, position, active, date_creation)
|
||||||
|
VALUES (0, 'PS_L3', 'Phasenschiene L3', 'L3', 1, 'L3', 1, '#9b59b6', '#9b59b6', 3, 4, 'below', 1, 30, 1, NOW());
|
||||||
|
|
||||||
|
INSERT INTO llx_kundenkarte_busbar_type (entity, ref, label, label_short, fk_system, phases, num_lines, color, default_color, line_height, line_spacing, position_default, is_system, position, active, date_creation)
|
||||||
|
VALUES (0, 'PS_N', 'Neutralleiter-Schiene', 'N', 1, 'N', 1, '#3498db', '#3498db', 3, 4, 'below', 1, 40, 1, NOW());
|
||||||
|
|
||||||
|
INSERT INTO llx_kundenkarte_busbar_type (entity, ref, label, label_short, fk_system, phases, num_lines, color, default_color, line_height, line_spacing, position_default, is_system, position, active, date_creation)
|
||||||
|
VALUES (0, 'PS_PE', 'Schutzleiter-Schiene', 'PE', 1, 'PE', 1, '#f1c40f', '#f1c40f', 3, 4, 'below', 1, 50, 1, NOW());
|
||||||
|
|
||||||
|
-- 1-phasig + N (Wechselstrom)
|
||||||
|
INSERT INTO llx_kundenkarte_busbar_type (entity, ref, label, label_short, fk_system, phases, num_lines, color, default_color, line_height, line_spacing, position_default, is_system, position, active, date_creation)
|
||||||
|
VALUES (0, 'PS_L1N', 'Phasenschiene L1+N', 'L1N', 1, 'L1N', 2, '#e74c3c,#3498db', '#e74c3c', 3, 4, 'below', 1, 100, 1, NOW());
|
||||||
|
|
||||||
|
-- 3-phasig (Drehstrom)
|
||||||
|
INSERT INTO llx_kundenkarte_busbar_type (entity, ref, label, label_short, fk_system, phases, num_lines, color, default_color, line_height, line_spacing, position_default, is_system, position, active, date_creation)
|
||||||
|
VALUES (0, 'PS_3P', 'Phasenschiene 3-phasig', '3P', 1, '3P', 3, '#e74c3c,#2ecc71,#9b59b6', '#e74c3c', 3, 4, 'below', 1, 200, 1, NOW());
|
||||||
|
|
||||||
|
-- 3-phasig + N
|
||||||
|
INSERT INTO llx_kundenkarte_busbar_type (entity, ref, label, label_short, fk_system, phases, num_lines, color, default_color, line_height, line_spacing, position_default, is_system, position, active, date_creation)
|
||||||
|
VALUES (0, 'PS_3PN', 'Phasenschiene 3P+N', '3P+N', 1, '3P+N', 4, '#e74c3c,#2ecc71,#9b59b6,#3498db', '#e74c3c', 3, 4, 'below', 1, 300, 1, NOW());
|
||||||
|
|
||||||
|
-- 3-phasig + N + PE (Vollausstattung)
|
||||||
|
INSERT INTO llx_kundenkarte_busbar_type (entity, ref, label, label_short, fk_system, phases, num_lines, color, default_color, line_height, line_spacing, position_default, is_system, position, active, date_creation)
|
||||||
|
VALUES (0, 'PS_3PNPE', 'Phasenschiene 3P+N+PE', '3P+N+PE', 1, '3P+N+PE', 5, '#e74c3c,#2ecc71,#9b59b6,#3498db,#f1c40f', '#e74c3c', 3, 4, 'below', 1, 400, 1, NOW());
|
||||||
|
|
||||||
|
-- Kammschiene (Gabelverschienung)
|
||||||
|
INSERT INTO llx_kundenkarte_busbar_type (entity, ref, label, label_short, fk_system, phases, num_lines, color, default_color, line_height, line_spacing, position_default, is_system, position, active, date_creation)
|
||||||
|
VALUES (0, 'KS_1P', 'Kammschiene 1-polig', 'KS1', 1, 'L1', 1, '#e74c3c', '#e74c3c', 4, 4, 'above', 1, 500, 1, NOW());
|
||||||
|
|
||||||
|
INSERT INTO llx_kundenkarte_busbar_type (entity, ref, label, label_short, fk_system, phases, num_lines, color, default_color, line_height, line_spacing, position_default, is_system, position, active, date_creation)
|
||||||
|
VALUES (0, 'KS_3P', 'Kammschiene 3-polig', 'KS3', 1, '3P', 3, '#e74c3c,#2ecc71,#9b59b6', '#e74c3c', 4, 4, 'above', 1, 510, 1, NOW());
|
||||||
|
|
||||||
|
INSERT INTO llx_kundenkarte_busbar_type (entity, ref, label, label_short, fk_system, phases, num_lines, color, default_color, line_height, line_spacing, position_default, is_system, position, active, date_creation)
|
||||||
|
VALUES (0, 'KS_3PN', 'Kammschiene 3P+N', 'KS3N', 1, '3P+N', 4, '#e74c3c,#2ecc71,#9b59b6,#3498db', '#e74c3c', 4, 4, 'above', 1, 520, 1, NOW());
|
||||||
92
sql/data_medium_types.sql
Executable file
92
sql/data_medium_types.sql
Executable file
|
|
@ -0,0 +1,92 @@
|
||||||
|
-- ============================================================================
|
||||||
|
-- Copyright (C) 2026 Alles Watt lauft
|
||||||
|
--
|
||||||
|
-- Default Medium Types (Kabeltypen)
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
-- Clear existing system entries
|
||||||
|
DELETE FROM llx_kundenkarte_medium_type WHERE is_system = 1;
|
||||||
|
|
||||||
|
-- ============================================================================
|
||||||
|
-- STROM - Elektrokabel (System: Strom, fk_system from c_kundenkarte_anlage_system)
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
-- NYM-J (Mantelleitung für Innenbereich)
|
||||||
|
INSERT INTO llx_kundenkarte_medium_type (entity, ref, label, label_short, category, fk_system, default_spec, available_specs, color, is_system, position, active, date_creation)
|
||||||
|
VALUES (0, 'NYM-J', 'NYM-J Mantelleitung', 'NYM', 'stromkabel', 1, '3x1,5', '["1,5", "2,5", "4", "6", "10", "16", "3x1,5", "3x2,5", "3x4", "3x6", "5x1,5", "5x2,5", "5x4", "5x6", "5x10", "5x16"]', '#666666', 1, 10, 1, NOW());
|
||||||
|
|
||||||
|
-- NYY-J (Erdkabel)
|
||||||
|
INSERT INTO llx_kundenkarte_medium_type (entity, ref, label, label_short, category, fk_system, default_spec, available_specs, color, is_system, position, active, date_creation)
|
||||||
|
VALUES (0, 'NYY-J', 'NYY-J Erdkabel', 'NYY', 'stromkabel', 1, '3x1,5', '["3x1,5", "3x2,5", "3x4", "3x6", "5x1,5", "5x2,5", "5x4", "5x6", "5x10", "5x16", "5x25"]', '#333333', 1, 20, 1, NOW());
|
||||||
|
|
||||||
|
-- H07V-U (Aderleitung starr)
|
||||||
|
INSERT INTO llx_kundenkarte_medium_type (entity, ref, label, label_short, category, fk_system, default_spec, available_specs, color, is_system, position, active, date_creation)
|
||||||
|
VALUES (0, 'H07V-U', 'H07V-U Aderleitung starr', 'H07V-U', 'stromkabel', 1, '1,5', '["1,5", "2,5", "4", "6", "10"]', '#0066cc', 1, 30, 1, NOW());
|
||||||
|
|
||||||
|
-- H07V-K (Aderleitung flexibel)
|
||||||
|
INSERT INTO llx_kundenkarte_medium_type (entity, ref, label, label_short, category, fk_system, default_spec, available_specs, color, is_system, position, active, date_creation)
|
||||||
|
VALUES (0, 'H07V-K', 'H07V-K Aderleitung flexibel', 'H07V-K', 'stromkabel', 1, '1,5', '["1,5", "2,5", "4", "6", "10", "16", "25"]', '#3498db', 1, 40, 1, NOW());
|
||||||
|
|
||||||
|
-- H05VV-F (Schlauchleitung)
|
||||||
|
INSERT INTO llx_kundenkarte_medium_type (entity, ref, label, label_short, category, fk_system, default_spec, available_specs, color, is_system, position, active, date_creation)
|
||||||
|
VALUES (0, 'H05VV-F', 'H05VV-F Schlauchleitung', 'H05VV-F', 'stromkabel', 1, '3x1,0', '["2x0,75", "2x1,0", "3x0,75", "3x1,0", "3x1,5", "5x1,0", "5x1,5"]', '#ffffff', 1, 50, 1, NOW());
|
||||||
|
|
||||||
|
-- ============================================================================
|
||||||
|
-- NETZWERK - Datenkabel (System: Internet/Netzwerk)
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
-- CAT5e
|
||||||
|
INSERT INTO llx_kundenkarte_medium_type (entity, ref, label, label_short, category, fk_system, default_spec, available_specs, color, is_system, position, active, date_creation)
|
||||||
|
VALUES (0, 'CAT5E', 'CAT5e Netzwerkkabel', 'CAT5e', 'netzwerkkabel', 2, 'U/UTP', '["U/UTP", "F/UTP", "S/FTP"]', '#f39c12', 1, 100, 1, NOW());
|
||||||
|
|
||||||
|
-- CAT6
|
||||||
|
INSERT INTO llx_kundenkarte_medium_type (entity, ref, label, label_short, category, fk_system, default_spec, available_specs, color, is_system, position, active, date_creation)
|
||||||
|
VALUES (0, 'CAT6', 'CAT6 Netzwerkkabel', 'CAT6', 'netzwerkkabel', 2, 'U/UTP', '["U/UTP", "F/UTP", "S/FTP", "S/STP"]', '#e67e22', 1, 110, 1, NOW());
|
||||||
|
|
||||||
|
-- CAT6a
|
||||||
|
INSERT INTO llx_kundenkarte_medium_type (entity, ref, label, label_short, category, fk_system, default_spec, available_specs, color, is_system, position, active, date_creation)
|
||||||
|
VALUES (0, 'CAT6A', 'CAT6a Netzwerkkabel', 'CAT6a', 'netzwerkkabel', 2, 'S/FTP', '["U/UTP", "F/UTP", "S/FTP", "S/STP"]', '#d35400', 1, 120, 1, NOW());
|
||||||
|
|
||||||
|
-- CAT7
|
||||||
|
INSERT INTO llx_kundenkarte_medium_type (entity, ref, label, label_short, category, fk_system, default_spec, available_specs, color, is_system, position, active, date_creation)
|
||||||
|
VALUES (0, 'CAT7', 'CAT7 Netzwerkkabel', 'CAT7', 'netzwerkkabel', 2, 'S/FTP', '["S/FTP", "S/STP"]', '#c0392b', 1, 130, 1, NOW());
|
||||||
|
|
||||||
|
-- CAT8
|
||||||
|
INSERT INTO llx_kundenkarte_medium_type (entity, ref, label, label_short, category, fk_system, default_spec, available_specs, color, is_system, position, active, date_creation)
|
||||||
|
VALUES (0, 'CAT8', 'CAT8 Netzwerkkabel', 'CAT8', 'netzwerkkabel', 2, 'S/FTP', '["S/FTP"]', '#8e44ad', 1, 140, 1, NOW());
|
||||||
|
|
||||||
|
-- LWL Singlemode
|
||||||
|
INSERT INTO llx_kundenkarte_medium_type (entity, ref, label, label_short, category, fk_system, default_spec, available_specs, color, is_system, position, active, date_creation)
|
||||||
|
VALUES (0, 'LWL-SM', 'LWL Singlemode', 'SM', 'lwl', 2, 'OS2 9/125', '["OS2 9/125", "2 Fasern", "4 Fasern", "8 Fasern", "12 Fasern", "24 Fasern"]', '#f1c40f', 1, 150, 1, NOW());
|
||||||
|
|
||||||
|
-- LWL Multimode
|
||||||
|
INSERT INTO llx_kundenkarte_medium_type (entity, ref, label, label_short, category, fk_system, default_spec, available_specs, color, is_system, position, active, date_creation)
|
||||||
|
VALUES (0, 'LWL-MM', 'LWL Multimode', 'MM', 'lwl', 2, 'OM3 50/125', '["OM1 62.5/125", "OM2 50/125", "OM3 50/125", "OM4 50/125", "OM5 50/125"]', '#2ecc71', 1, 160, 1, NOW());
|
||||||
|
|
||||||
|
-- ============================================================================
|
||||||
|
-- KOAX - Koaxialkabel (System: Kabelfernsehen/SAT)
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
-- RG6
|
||||||
|
INSERT INTO llx_kundenkarte_medium_type (entity, ref, label, label_short, category, fk_system, default_spec, available_specs, color, is_system, position, active, date_creation)
|
||||||
|
VALUES (0, 'RG6', 'RG6 Koaxialkabel', 'RG6', 'koax', 3, '75 Ohm', '["75 Ohm", "Quad-Shield"]', '#1abc9c', 1, 200, 1, NOW());
|
||||||
|
|
||||||
|
-- RG59
|
||||||
|
INSERT INTO llx_kundenkarte_medium_type (entity, ref, label, label_short, category, fk_system, default_spec, available_specs, color, is_system, position, active, date_creation)
|
||||||
|
VALUES (0, 'RG59', 'RG59 Koaxialkabel', 'RG59', 'koax', 3, '75 Ohm', '["75 Ohm"]', '#16a085', 1, 210, 1, NOW());
|
||||||
|
|
||||||
|
-- SAT-Kabel
|
||||||
|
INSERT INTO llx_kundenkarte_medium_type (entity, ref, label, label_short, category, fk_system, default_spec, available_specs, color, is_system, position, active, date_creation)
|
||||||
|
VALUES (0, 'SAT-KOAX', 'SAT Koaxialkabel', 'SAT', 'koax', 4, '120dB', '["90dB", "100dB", "110dB", "120dB", "130dB"]', '#9b59b6', 1, 220, 1, NOW());
|
||||||
|
|
||||||
|
-- ============================================================================
|
||||||
|
-- GLOBAL - Für alle Systeme
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
-- Leerrohr
|
||||||
|
INSERT INTO llx_kundenkarte_medium_type (entity, ref, label, label_short, category, fk_system, default_spec, available_specs, color, is_system, position, active, date_creation)
|
||||||
|
VALUES (0, 'LEERROHR', 'Leerrohr/Kabelkanal', 'Rohr', 'sonstiges', 0, 'M20', '["M16", "M20", "M25", "M32", "M40", "M50", "DN50", "DN75", "DN100"]', '#95a5a6', 1, 300, 1, NOW());
|
||||||
|
|
||||||
|
-- Kabelrinne
|
||||||
|
INSERT INTO llx_kundenkarte_medium_type (entity, ref, label, label_short, category, fk_system, default_spec, available_specs, color, is_system, position, active, date_creation)
|
||||||
|
VALUES (0, 'KABELRINNE', 'Kabelrinne/Kabeltrasse', 'Rinne', 'sonstiges', 0, '100x60', '["60x40", "100x60", "200x60", "300x60", "400x60", "500x100"]', '#7f8c8d', 1, 310, 1, NOW());
|
||||||
110
sql/data_terminal_types.sql
Executable file
110
sql/data_terminal_types.sql
Executable file
|
|
@ -0,0 +1,110 @@
|
||||||
|
-- ============================================================================
|
||||||
|
-- Copyright (C) 2026 Alles Watt lauft
|
||||||
|
--
|
||||||
|
-- Standard terminal block types (Reihenklemmen-Typen)
|
||||||
|
-- These are narrow components (0.5 TE) with stacked terminals
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
-- Note: Run this after the module is installed and you have created a system
|
||||||
|
-- with code 'STROM' for electrical installations.
|
||||||
|
-- Adjust fk_system values to match your actual system IDs.
|
||||||
|
|
||||||
|
-- Get the electrical system ID (adjust WHERE clause if needed)
|
||||||
|
-- SET @strom_system = (SELECT rowid FROM llx_c_kundenkarte_anlage_system WHERE code = 'STROM' LIMIT 1);
|
||||||
|
|
||||||
|
-- ============================================================================
|
||||||
|
-- Reihenklemmen / Terminal Blocks (0.5 TE breit)
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
-- Durchgangsklemme (Feed-through terminal) - 1 input, 1 output
|
||||||
|
-- Standard 2-point terminal block
|
||||||
|
INSERT INTO llx_kundenkarte_equipment_type
|
||||||
|
(entity, ref, label, label_short, description, fk_system, width_te, color, terminals_config, flow_direction, terminal_position, picto, is_system, position, active, date_creation)
|
||||||
|
SELECT 0, 'RK_DURCH', 'Durchgangsklemme', 'DK', 'Standard-Durchgangsklemme für Leiterverbindung',
|
||||||
|
s.rowid, 1, '#7f8c8d',
|
||||||
|
'{"terminals":[{"id":"t1","label":"1","pos":"top","col":0,"row":0},{"id":"t2","label":"2","pos":"bottom","col":0,"row":0}]}',
|
||||||
|
'top_to_bottom', 'both', 'fa-square', 0, 100, 1, NOW()
|
||||||
|
FROM llx_c_kundenkarte_anlage_system s WHERE s.code = 'STROM' LIMIT 1;
|
||||||
|
|
||||||
|
-- Dreistockklemme (3-level terminal) - 3 top, 2 bottom
|
||||||
|
-- Used for phase distribution with multiple connection points
|
||||||
|
INSERT INTO llx_kundenkarte_equipment_type
|
||||||
|
(entity, ref, label, label_short, description, fk_system, width_te, color, terminals_config, flow_direction, terminal_position, picto, is_system, position, active, date_creation)
|
||||||
|
SELECT 0, 'RK_3STOCK', 'Dreistockklemme', '3ST', 'Dreistöckige Klemme für mehrfache Verbindungen',
|
||||||
|
s.rowid, 1, '#27ae60',
|
||||||
|
'{"terminals":[{"id":"t1","label":"1","pos":"top","col":0,"row":0},{"id":"t3","label":"3","pos":"top","col":0,"row":1},{"id":"t5","label":"5","pos":"top","col":0,"row":2},{"id":"t2","label":"2","pos":"bottom","col":0,"row":0},{"id":"t4","label":"4","pos":"bottom","col":0,"row":1}]}',
|
||||||
|
NULL, 'both', 'fa-th-large', 0, 110, 1, NOW()
|
||||||
|
FROM llx_c_kundenkarte_anlage_system s WHERE s.code = 'STROM' LIMIT 1;
|
||||||
|
|
||||||
|
-- Doppelstockklemme (2-level terminal) - 2 top, 2 bottom
|
||||||
|
INSERT INTO llx_kundenkarte_equipment_type
|
||||||
|
(entity, ref, label, label_short, description, fk_system, width_te, color, terminals_config, flow_direction, terminal_position, picto, is_system, position, active, date_creation)
|
||||||
|
SELECT 0, 'RK_2STOCK', 'Doppelstockklemme', '2ST', 'Zweistöckige Klemme für doppelte Verbindungen',
|
||||||
|
s.rowid, 1, '#2980b9',
|
||||||
|
'{"terminals":[{"id":"t1","label":"1","pos":"top","col":0,"row":0},{"id":"t3","label":"3","pos":"top","col":0,"row":1},{"id":"t2","label":"2","pos":"bottom","col":0,"row":0},{"id":"t4","label":"4","pos":"bottom","col":0,"row":1}]}',
|
||||||
|
NULL, 'both', 'fa-th', 0, 105, 1, NOW()
|
||||||
|
FROM llx_c_kundenkarte_anlage_system s WHERE s.code = 'STROM' LIMIT 1;
|
||||||
|
|
||||||
|
-- N-Klemme (Neutral terminal) - blaue Farbe
|
||||||
|
INSERT INTO llx_kundenkarte_equipment_type
|
||||||
|
(entity, ref, label, label_short, description, fk_system, width_te, color, terminals_config, flow_direction, terminal_position, picto, is_system, position, active, date_creation)
|
||||||
|
SELECT 0, 'RK_N', 'N-Klemme', 'N', 'Neutralleiter-Klemme (blau)',
|
||||||
|
s.rowid, 1, '#3498db',
|
||||||
|
'{"terminals":[{"id":"t1","label":"N","pos":"top","col":0,"row":0},{"id":"t2","label":"N","pos":"bottom","col":0,"row":0}]}',
|
||||||
|
'top_to_bottom', 'both', 'fa-square', 0, 120, 1, NOW()
|
||||||
|
FROM llx_c_kundenkarte_anlage_system s WHERE s.code = 'STROM' LIMIT 1;
|
||||||
|
|
||||||
|
-- PE-Klemme (Protective Earth terminal) - grün-gelb
|
||||||
|
INSERT INTO llx_kundenkarte_equipment_type
|
||||||
|
(entity, ref, label, label_short, description, fk_system, width_te, color, terminals_config, flow_direction, terminal_position, picto, is_system, position, active, date_creation)
|
||||||
|
SELECT 0, 'RK_PE', 'PE-Klemme', 'PE', 'Schutzleiter-Klemme (grün-gelb)',
|
||||||
|
s.rowid, 1, '#27ae60',
|
||||||
|
'{"terminals":[{"id":"t1","label":"PE","pos":"top","col":0,"row":0},{"id":"t2","label":"PE","pos":"bottom","col":0,"row":0}]}',
|
||||||
|
'top_to_bottom', 'both', 'fa-square', 0, 125, 1, NOW()
|
||||||
|
FROM llx_c_kundenkarte_anlage_system s WHERE s.code = 'STROM' LIMIT 1;
|
||||||
|
|
||||||
|
-- Hauptabzweigklemme (Main junction terminal) - 4 Anschlusspunkte
|
||||||
|
-- Large terminal for main distribution with 2 inputs and 2 outputs
|
||||||
|
INSERT INTO llx_kundenkarte_equipment_type
|
||||||
|
(entity, ref, label, label_short, description, fk_system, width_te, color, terminals_config, flow_direction, terminal_position, picto, is_system, position, active, date_creation)
|
||||||
|
SELECT 0, 'RK_HAK', 'Hauptabzweigklemme', 'HAK', 'Hauptabzweigklemme für große Querschnitte mit 4 Anschlüssen',
|
||||||
|
s.rowid, 2, '#e74c3c',
|
||||||
|
'{"terminals":[{"id":"t1","label":"1","pos":"top","col":0,"row":0},{"id":"t3","label":"3","pos":"top","col":1,"row":0},{"id":"t2","label":"2","pos":"bottom","col":0,"row":0},{"id":"t4","label":"4","pos":"bottom","col":1,"row":0}]}',
|
||||||
|
NULL, 'both', 'fa-th-large', 0, 130, 1, NOW()
|
||||||
|
FROM llx_c_kundenkarte_anlage_system s WHERE s.code = 'STROM' LIMIT 1;
|
||||||
|
|
||||||
|
-- Trennklemme (Disconnect terminal) - mit Trennmöglichkeit
|
||||||
|
INSERT INTO llx_kundenkarte_equipment_type
|
||||||
|
(entity, ref, label, label_short, description, fk_system, width_te, color, terminals_config, flow_direction, terminal_position, picto, is_system, position, active, date_creation)
|
||||||
|
SELECT 0, 'RK_TRENN', 'Trennklemme', 'TK', 'Reihenklemme mit Trennmöglichkeit',
|
||||||
|
s.rowid, 1, '#f39c12',
|
||||||
|
'{"terminals":[{"id":"t1","label":"1","pos":"top","col":0,"row":0},{"id":"t2","label":"2","pos":"bottom","col":0,"row":0}]}',
|
||||||
|
'top_to_bottom', 'both', 'fa-minus-square', 0, 115, 1, NOW()
|
||||||
|
FROM llx_c_kundenkarte_anlage_system s WHERE s.code = 'STROM' LIMIT 1;
|
||||||
|
|
||||||
|
-- Sicherungsklemme (Fuse terminal)
|
||||||
|
INSERT INTO llx_kundenkarte_equipment_type
|
||||||
|
(entity, ref, label, label_short, description, fk_system, width_te, color, terminals_config, flow_direction, terminal_position, picto, is_system, position, active, date_creation)
|
||||||
|
SELECT 0, 'RK_SI', 'Sicherungsklemme', 'SI', 'Reihenklemme mit Sicherungseinsatz',
|
||||||
|
s.rowid, 1, '#c0392b',
|
||||||
|
'{"terminals":[{"id":"t1","label":"1","pos":"top","col":0,"row":0},{"id":"t2","label":"2","pos":"bottom","col":0,"row":0}]}',
|
||||||
|
'top_to_bottom', 'both', 'fa-bolt', 0, 135, 1, NOW()
|
||||||
|
FROM llx_c_kundenkarte_anlage_system s WHERE s.code = 'STROM' LIMIT 1;
|
||||||
|
|
||||||
|
-- LED-Klemme (Terminal with LED indicator)
|
||||||
|
INSERT INTO llx_kundenkarte_equipment_type
|
||||||
|
(entity, ref, label, label_short, description, fk_system, width_te, color, terminals_config, flow_direction, terminal_position, picto, is_system, position, active, date_creation)
|
||||||
|
SELECT 0, 'RK_LED', 'LED-Klemme', 'LED', 'Reihenklemme mit LED-Anzeige',
|
||||||
|
s.rowid, 1, '#8e44ad',
|
||||||
|
'{"terminals":[{"id":"t1","label":"1","pos":"top","col":0,"row":0},{"id":"t2","label":"2","pos":"bottom","col":0,"row":0}]}',
|
||||||
|
'top_to_bottom', 'both', 'fa-lightbulb-o', 0, 140, 1, NOW()
|
||||||
|
FROM llx_c_kundenkarte_anlage_system s WHERE s.code = 'STROM' LIMIT 1;
|
||||||
|
|
||||||
|
-- Vierfachklemme (4-level terminal) - für dichte Verdrahtung
|
||||||
|
INSERT INTO llx_kundenkarte_equipment_type
|
||||||
|
(entity, ref, label, label_short, description, fk_system, width_te, color, terminals_config, flow_direction, terminal_position, picto, is_system, position, active, date_creation)
|
||||||
|
SELECT 0, 'RK_4STOCK', 'Vierfachklemme', '4ST', 'Vierstöckige Klemme für sehr dichte Verdrahtung',
|
||||||
|
s.rowid, 1, '#1abc9c',
|
||||||
|
'{"terminals":[{"id":"t1","label":"1","pos":"top","col":0,"row":0},{"id":"t3","label":"3","pos":"top","col":0,"row":1},{"id":"t5","label":"5","pos":"top","col":0,"row":2},{"id":"t7","label":"7","pos":"top","col":0,"row":3},{"id":"t2","label":"2","pos":"bottom","col":0,"row":0},{"id":"t4","label":"4","pos":"bottom","col":0,"row":1},{"id":"t6","label":"6","pos":"bottom","col":0,"row":2}]}',
|
||||||
|
NULL, 'both', 'fa-th', 0, 112, 1, NOW()
|
||||||
|
FROM llx_c_kundenkarte_anlage_system s WHERE s.code = 'STROM' LIMIT 1;
|
||||||
52
sql/dolibarr_allversions.sql
Executable file
52
sql/dolibarr_allversions.sql
Executable file
|
|
@ -0,0 +1,52 @@
|
||||||
|
--
|
||||||
|
-- Script run when an upgrade of Dolibarr is done. Whatever is the Dolibarr version.
|
||||||
|
--
|
||||||
|
|
||||||
|
-- Add path_data column for manually drawn connection paths
|
||||||
|
ALTER TABLE llx_kundenkarte_equipment_connection ADD COLUMN IF NOT EXISTS path_data TEXT AFTER position_y;
|
||||||
|
|
||||||
|
-- Add icon_file column for custom SVG/PNG schematic symbols
|
||||||
|
ALTER TABLE llx_kundenkarte_equipment_type ADD COLUMN IF NOT EXISTS icon_file VARCHAR(255) AFTER picto;
|
||||||
|
|
||||||
|
-- Add terminals_config if not exists
|
||||||
|
ALTER TABLE llx_kundenkarte_equipment_type ADD COLUMN IF NOT EXISTS terminals_config TEXT AFTER fk_product;
|
||||||
|
|
||||||
|
-- Add flow_direction and terminal_position for equipment types
|
||||||
|
ALTER TABLE llx_kundenkarte_equipment_type ADD COLUMN IF NOT EXISTS flow_direction VARCHAR(16) DEFAULT NULL AFTER terminals_config;
|
||||||
|
ALTER TABLE llx_kundenkarte_equipment_type ADD COLUMN IF NOT EXISTS terminal_position VARCHAR(16) DEFAULT 'both' AFTER flow_direction;
|
||||||
|
|
||||||
|
-- Add block_image for SchematicEditor display
|
||||||
|
ALTER TABLE llx_kundenkarte_equipment_type ADD COLUMN IF NOT EXISTS block_image VARCHAR(255) AFTER icon_file;
|
||||||
|
|
||||||
|
-- Add busbar type reference to connections
|
||||||
|
ALTER TABLE llx_kundenkarte_equipment_connection ADD COLUMN IF NOT EXISTS fk_busbar_type INTEGER DEFAULT NULL AFTER is_rail;
|
||||||
|
|
||||||
|
-- Create busbar_type table if not exists
|
||||||
|
CREATE TABLE IF NOT EXISTS llx_kundenkarte_busbar_type
|
||||||
|
(
|
||||||
|
rowid integer AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
entity integer DEFAULT 0 NOT NULL,
|
||||||
|
ref varchar(64) NOT NULL,
|
||||||
|
label varchar(255) NOT NULL,
|
||||||
|
label_short varchar(32),
|
||||||
|
description text,
|
||||||
|
fk_system integer NOT NULL,
|
||||||
|
phases varchar(20) NOT NULL,
|
||||||
|
num_lines integer DEFAULT 1 NOT NULL,
|
||||||
|
color varchar(64),
|
||||||
|
default_color varchar(8),
|
||||||
|
line_height integer DEFAULT 3,
|
||||||
|
line_spacing integer DEFAULT 4,
|
||||||
|
position_default varchar(16) DEFAULT 'below',
|
||||||
|
fk_product integer DEFAULT NULL,
|
||||||
|
picto varchar(64),
|
||||||
|
icon_file varchar(255),
|
||||||
|
is_system tinyint DEFAULT 0 NOT NULL,
|
||||||
|
position integer DEFAULT 0,
|
||||||
|
active tinyint DEFAULT 1 NOT NULL,
|
||||||
|
date_creation datetime,
|
||||||
|
tms timestamp DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
fk_user_creat integer,
|
||||||
|
fk_user_modif integer,
|
||||||
|
import_key varchar(14)
|
||||||
|
) ENGINE=innodb;
|
||||||
6
sql/llx_c_kundenkarte_anlage_system.key.sql
Executable file
6
sql/llx_c_kundenkarte_anlage_system.key.sql
Executable file
|
|
@ -0,0 +1,6 @@
|
||||||
|
-- ============================================================================
|
||||||
|
-- Copyright (C) 2026 Alles Watt lauft
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
ALTER TABLE llx_c_kundenkarte_anlage_system ADD UNIQUE INDEX uk_c_kundenkarte_anlage_system_code (code, entity);
|
||||||
|
ALTER TABLE llx_c_kundenkarte_anlage_system ADD INDEX idx_c_kundenkarte_anlage_system_active (active);
|
||||||
24
sql/llx_c_kundenkarte_anlage_system.sql
Executable file
24
sql/llx_c_kundenkarte_anlage_system.sql
Executable file
|
|
@ -0,0 +1,24 @@
|
||||||
|
-- ============================================================================
|
||||||
|
-- Copyright (C) 2026 Alles Watt lauft
|
||||||
|
--
|
||||||
|
-- Dictionary table for installation system categories
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
CREATE TABLE llx_c_kundenkarte_anlage_system
|
||||||
|
(
|
||||||
|
rowid integer AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
entity integer DEFAULT 0 NOT NULL,
|
||||||
|
|
||||||
|
code varchar(32) NOT NULL,
|
||||||
|
label varchar(128) NOT NULL,
|
||||||
|
description text,
|
||||||
|
|
||||||
|
picto varchar(64),
|
||||||
|
color varchar(8),
|
||||||
|
|
||||||
|
-- Tree display configuration (JSON)
|
||||||
|
tree_display_config text COMMENT 'JSON config for tree display options',
|
||||||
|
|
||||||
|
position integer DEFAULT 0,
|
||||||
|
active tinyint DEFAULT 1 NOT NULL
|
||||||
|
) ENGINE=innodb;
|
||||||
16
sql/llx_kundenkarte_anlage.key.sql
Executable file
16
sql/llx_kundenkarte_anlage.key.sql
Executable file
|
|
@ -0,0 +1,16 @@
|
||||||
|
-- ============================================================================
|
||||||
|
-- Copyright (C) 2026 Alles Watt lauft
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
ALTER TABLE llx_kundenkarte_anlage ADD INDEX idx_kundenkarte_anlage_fk_soc (fk_soc);
|
||||||
|
ALTER TABLE llx_kundenkarte_anlage ADD INDEX idx_kundenkarte_anlage_fk_parent (fk_parent);
|
||||||
|
ALTER TABLE llx_kundenkarte_anlage ADD INDEX idx_kundenkarte_anlage_fk_type (fk_anlage_type);
|
||||||
|
ALTER TABLE llx_kundenkarte_anlage ADD INDEX idx_kundenkarte_anlage_fk_system (fk_system);
|
||||||
|
ALTER TABLE llx_kundenkarte_anlage ADD INDEX idx_kundenkarte_anlage_status (status);
|
||||||
|
|
||||||
|
ALTER TABLE llx_kundenkarte_anlage ADD INDEX idx_kundenkarte_anlage_tree (fk_soc, fk_parent, rang);
|
||||||
|
ALTER TABLE llx_kundenkarte_anlage ADD INDEX idx_kundenkarte_anlage_system_tree (fk_soc, fk_system, fk_parent);
|
||||||
|
|
||||||
|
ALTER TABLE llx_kundenkarte_anlage ADD CONSTRAINT fk_kundenkarte_anlage_fk_soc FOREIGN KEY (fk_soc) REFERENCES llx_societe (rowid);
|
||||||
|
ALTER TABLE llx_kundenkarte_anlage ADD CONSTRAINT fk_kundenkarte_anlage_fk_type FOREIGN KEY (fk_anlage_type) REFERENCES llx_kundenkarte_anlage_type (rowid);
|
||||||
|
ALTER TABLE llx_kundenkarte_anlage ADD CONSTRAINT fk_kundenkarte_anlage_fk_system FOREIGN KEY (fk_system) REFERENCES llx_c_kundenkarte_anlage_system (rowid);
|
||||||
47
sql/llx_kundenkarte_anlage.sql
Executable file
47
sql/llx_kundenkarte_anlage.sql
Executable file
|
|
@ -0,0 +1,47 @@
|
||||||
|
-- ============================================================================
|
||||||
|
-- Copyright (C) 2026 Alles Watt lauft
|
||||||
|
--
|
||||||
|
-- Table for customer-specific installation elements (tree structure)
|
||||||
|
-- Uses adjacency list pattern with fk_parent for hierarchy
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
CREATE TABLE llx_kundenkarte_anlage
|
||||||
|
(
|
||||||
|
rowid integer AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
entity integer DEFAULT 1 NOT NULL,
|
||||||
|
|
||||||
|
ref varchar(64),
|
||||||
|
label varchar(255) NOT NULL,
|
||||||
|
|
||||||
|
fk_soc integer NOT NULL,
|
||||||
|
fk_anlage_type integer NOT NULL,
|
||||||
|
fk_parent integer DEFAULT 0 NOT NULL,
|
||||||
|
|
||||||
|
fk_system integer NOT NULL,
|
||||||
|
fk_building_node integer DEFAULT 0,
|
||||||
|
|
||||||
|
manufacturer varchar(128),
|
||||||
|
model varchar(128),
|
||||||
|
serial_number varchar(64),
|
||||||
|
power_rating varchar(32),
|
||||||
|
|
||||||
|
field_values text,
|
||||||
|
|
||||||
|
location varchar(255),
|
||||||
|
installation_date date,
|
||||||
|
warranty_until date,
|
||||||
|
|
||||||
|
rang integer DEFAULT 0,
|
||||||
|
level integer DEFAULT 0,
|
||||||
|
|
||||||
|
note_private text,
|
||||||
|
note_public text,
|
||||||
|
|
||||||
|
status tinyint DEFAULT 1 NOT NULL,
|
||||||
|
|
||||||
|
date_creation datetime,
|
||||||
|
tms timestamp DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
fk_user_creat integer,
|
||||||
|
fk_user_modif integer,
|
||||||
|
import_key varchar(14)
|
||||||
|
) ENGINE=innodb;
|
||||||
7
sql/llx_kundenkarte_anlage_connection.key.sql
Executable file
7
sql/llx_kundenkarte_anlage_connection.key.sql
Executable file
|
|
@ -0,0 +1,7 @@
|
||||||
|
-- ============================================================================
|
||||||
|
-- Copyright (C) 2026 Alles Watt lauft
|
||||||
|
--
|
||||||
|
-- Keys for Anlage Connections table
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
-- Foreign keys already defined in main table
|
||||||
45
sql/llx_kundenkarte_anlage_connection.sql
Executable file
45
sql/llx_kundenkarte_anlage_connection.sql
Executable file
|
|
@ -0,0 +1,45 @@
|
||||||
|
-- ============================================================================
|
||||||
|
-- Copyright (C) 2026 Alles Watt lauft
|
||||||
|
--
|
||||||
|
-- Anlage Connections (Verbindungen zwischen Anlagen-Elementen im Baum)
|
||||||
|
-- Beschreibt Kabel/Leitungen zwischen Strukturelementen wie HAK → Zählerschrank
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS llx_kundenkarte_anlage_connection
|
||||||
|
(
|
||||||
|
rowid integer AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
entity integer DEFAULT 1 NOT NULL,
|
||||||
|
|
||||||
|
-- Source and target anlagen
|
||||||
|
fk_source integer NOT NULL COMMENT 'Source Anlage ID',
|
||||||
|
fk_target integer NOT NULL COMMENT 'Target Anlage ID',
|
||||||
|
|
||||||
|
-- Connection description
|
||||||
|
label varchar(255) COMMENT 'Connection label/description',
|
||||||
|
|
||||||
|
-- Medium/Cable info (references medium_type table or free text)
|
||||||
|
fk_medium_type integer COMMENT 'Reference to medium_type table',
|
||||||
|
medium_type_text varchar(100) COMMENT 'Free text if no type selected',
|
||||||
|
medium_spec varchar(100) COMMENT 'Specification (e.g., 5x16)',
|
||||||
|
medium_length varchar(50) COMMENT 'Length (e.g., 15m)',
|
||||||
|
medium_color varchar(50) COMMENT 'Wire/cable color',
|
||||||
|
|
||||||
|
-- Additional info
|
||||||
|
route_description text COMMENT 'Description of cable route',
|
||||||
|
installation_date date COMMENT 'When was this installed',
|
||||||
|
|
||||||
|
-- Status
|
||||||
|
status integer DEFAULT 1,
|
||||||
|
note_private text,
|
||||||
|
note_public text,
|
||||||
|
|
||||||
|
date_creation datetime,
|
||||||
|
tms timestamp DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
fk_user_creat integer,
|
||||||
|
fk_user_modif integer,
|
||||||
|
|
||||||
|
INDEX idx_anlage_conn_source (fk_source),
|
||||||
|
INDEX idx_anlage_conn_target (fk_target),
|
||||||
|
CONSTRAINT fk_anlage_conn_source FOREIGN KEY (fk_source) REFERENCES llx_kundenkarte_anlage(rowid) ON DELETE CASCADE,
|
||||||
|
CONSTRAINT fk_anlage_conn_target FOREIGN KEY (fk_target) REFERENCES llx_kundenkarte_anlage(rowid) ON DELETE CASCADE
|
||||||
|
) ENGINE=innodb;
|
||||||
8
sql/llx_kundenkarte_anlage_contact.sql
Executable file
8
sql/llx_kundenkarte_anlage_contact.sql
Executable file
|
|
@ -0,0 +1,8 @@
|
||||||
|
-- ============================================================================
|
||||||
|
-- Copyright (C) 2026 Alles Watt lauft
|
||||||
|
--
|
||||||
|
-- Add fk_contact column for contact/address specific installations
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
ALTER TABLE llx_kundenkarte_anlage ADD COLUMN fk_contact integer DEFAULT NULL AFTER fk_soc;
|
||||||
|
ALTER TABLE llx_kundenkarte_anlage ADD INDEX idx_kundenkarte_anlage_fk_contact (fk_contact);
|
||||||
11
sql/llx_kundenkarte_anlage_files.key.sql
Executable file
11
sql/llx_kundenkarte_anlage_files.key.sql
Executable file
|
|
@ -0,0 +1,11 @@
|
||||||
|
-- ============================================================================
|
||||||
|
-- Copyright (C) 2026 Alles Watt lauft
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
ALTER TABLE llx_kundenkarte_anlage_files ADD INDEX idx_kundenkarte_anlage_files_fk_anlage (fk_anlage);
|
||||||
|
ALTER TABLE llx_kundenkarte_anlage_files ADD INDEX idx_kundenkarte_anlage_files_type (file_type);
|
||||||
|
ALTER TABLE llx_kundenkarte_anlage_files ADD INDEX idx_kundenkarte_anlage_files_share (share);
|
||||||
|
|
||||||
|
ALTER TABLE llx_kundenkarte_anlage_files ADD UNIQUE INDEX uk_kundenkarte_anlage_files (fk_anlage, filename);
|
||||||
|
|
||||||
|
ALTER TABLE llx_kundenkarte_anlage_files ADD CONSTRAINT fk_kundenkarte_anlage_files_fk_anlage FOREIGN KEY (fk_anlage) REFERENCES llx_kundenkarte_anlage (rowid) ON DELETE CASCADE;
|
||||||
36
sql/llx_kundenkarte_anlage_files.sql
Executable file
36
sql/llx_kundenkarte_anlage_files.sql
Executable file
|
|
@ -0,0 +1,36 @@
|
||||||
|
-- ============================================================================
|
||||||
|
-- Copyright (C) 2026 Alles Watt lauft
|
||||||
|
--
|
||||||
|
-- Table for installation element file attachments
|
||||||
|
-- Files stored in: /documents/kundenkarte/anlagen/{socid}/{element_id}/
|
||||||
|
-- Separate from ECM system
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
CREATE TABLE llx_kundenkarte_anlage_files
|
||||||
|
(
|
||||||
|
rowid integer AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
entity integer DEFAULT 1 NOT NULL,
|
||||||
|
|
||||||
|
fk_anlage integer NOT NULL,
|
||||||
|
|
||||||
|
filename varchar(255) NOT NULL,
|
||||||
|
filepath varchar(512) NOT NULL,
|
||||||
|
filesize bigint DEFAULT 0,
|
||||||
|
mimetype varchar(128),
|
||||||
|
|
||||||
|
file_type varchar(32) DEFAULT 'document',
|
||||||
|
|
||||||
|
label varchar(255),
|
||||||
|
description text,
|
||||||
|
|
||||||
|
is_cover tinyint DEFAULT 0 NOT NULL,
|
||||||
|
is_pinned tinyint DEFAULT 0 NOT NULL,
|
||||||
|
position integer DEFAULT 0,
|
||||||
|
|
||||||
|
share varchar(128),
|
||||||
|
|
||||||
|
date_creation datetime,
|
||||||
|
tms timestamp DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
fk_user_creat integer,
|
||||||
|
fk_user_modif integer
|
||||||
|
) ENGINE=innodb;
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue