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