From 1b8bf9e8df2fb0cbdee4a899df9daf6fa330430c Mon Sep 17 00:00:00 2001 From: data Date: Sat, 31 Jan 2026 08:18:54 +0100 Subject: [PATCH] Stabile 1.1 Version --- COPYING | 621 ++++++++++++ ChangeLog.md | 47 + README.md | 86 ++ admin/about.php | 118 +++ admin/anlage_systems.php | 254 +++++ admin/anlage_types.php | 647 ++++++++++++ admin/setup.php | 160 +++ ajax/anlage_docs.php | 54 + ajax/anlage_images.php | 50 + ajax/anlage_tooltip.php | 102 ++ ajax/favorite_update.php | 75 ++ ajax/icon_upload.php | 145 +++ build/buildzip.php | 316 ++++++ build/makepack-kundenkarte.conf | 11 + class/anlage.class.php | 633 ++++++++++++ class/anlagefile.class.php | 417 ++++++++ class/anlagetype.class.php | 369 +++++++ class/favoriteproduct.class.php | 802 +++++++++++++++ core/modules/modKundenKarte.class.php | 548 ++++++++++ css/kundenkarte.css | 832 +++++++++++++++ img/README.md | 14 + js/kundenkarte.js | 950 ++++++++++++++++++ kundenkarteindex.php | 259 +++++ langs/de_DE/kundenkarte.lang | 204 ++++ langs/en_US/kundenkarte.lang | 187 ++++ lib/kundenkarte.lib.php | 120 +++ modulebuilder.txt | 3 + sql/data.sql | 29 + sql/dolibarr_allversions.sql | 3 + sql/llx_c_kundenkarte_anlage_system.key.sql | 6 + sql/llx_c_kundenkarte_anlage_system.sql | 21 + sql/llx_kundenkarte_anlage.key.sql | 16 + sql/llx_kundenkarte_anlage.sql | 46 + sql/llx_kundenkarte_anlage_contact.sql | 8 + sql/llx_kundenkarte_anlage_files.key.sql | 11 + sql/llx_kundenkarte_anlage_files.sql | 35 + sql/llx_kundenkarte_anlage_type.key.sql | 10 + sql/llx_kundenkarte_anlage_type.sql | 37 + sql/llx_kundenkarte_anlage_type_field.key.sql | 10 + sql/llx_kundenkarte_anlage_type_field.sql | 33 + sql/llx_kundenkarte_favorite_products.key.sql | 12 + sql/llx_kundenkarte_favorite_products.sql | 32 + ..._kundenkarte_favorite_products_contact.sql | 8 + sql/llx_kundenkarte_societe_system.key.sql | 12 + sql/llx_kundenkarte_societe_system.sql | 18 + ...llx_kundenkarte_societe_system_contact.sql | 8 + tabs/anlagen.php | 802 +++++++++++++++ tabs/contact_anlagen.php | 803 +++++++++++++++ tabs/contact_favoriteproducts.php | 363 +++++++ tabs/favoriteproducts.php | 363 +++++++ 50 files changed, 10710 insertions(+) create mode 100755 COPYING create mode 100755 ChangeLog.md create mode 100755 README.md create mode 100755 admin/about.php create mode 100755 admin/anlage_systems.php create mode 100755 admin/anlage_types.php create mode 100755 admin/setup.php create mode 100755 ajax/anlage_docs.php create mode 100755 ajax/anlage_images.php create mode 100755 ajax/anlage_tooltip.php create mode 100644 ajax/favorite_update.php create mode 100644 ajax/icon_upload.php create mode 100755 build/buildzip.php create mode 100755 build/makepack-kundenkarte.conf create mode 100755 class/anlage.class.php create mode 100755 class/anlagefile.class.php create mode 100755 class/anlagetype.class.php create mode 100755 class/favoriteproduct.class.php create mode 100755 core/modules/modKundenKarte.class.php create mode 100755 css/kundenkarte.css create mode 100755 img/README.md create mode 100755 js/kundenkarte.js create mode 100755 kundenkarteindex.php create mode 100755 langs/de_DE/kundenkarte.lang create mode 100755 langs/en_US/kundenkarte.lang create mode 100755 lib/kundenkarte.lib.php create mode 100755 modulebuilder.txt create mode 100755 sql/data.sql create mode 100755 sql/dolibarr_allversions.sql create mode 100755 sql/llx_c_kundenkarte_anlage_system.key.sql create mode 100755 sql/llx_c_kundenkarte_anlage_system.sql create mode 100755 sql/llx_kundenkarte_anlage.key.sql create mode 100755 sql/llx_kundenkarte_anlage.sql create mode 100644 sql/llx_kundenkarte_anlage_contact.sql create mode 100755 sql/llx_kundenkarte_anlage_files.key.sql create mode 100755 sql/llx_kundenkarte_anlage_files.sql create mode 100755 sql/llx_kundenkarte_anlage_type.key.sql create mode 100755 sql/llx_kundenkarte_anlage_type.sql create mode 100755 sql/llx_kundenkarte_anlage_type_field.key.sql create mode 100755 sql/llx_kundenkarte_anlage_type_field.sql create mode 100755 sql/llx_kundenkarte_favorite_products.key.sql create mode 100755 sql/llx_kundenkarte_favorite_products.sql create mode 100644 sql/llx_kundenkarte_favorite_products_contact.sql create mode 100755 sql/llx_kundenkarte_societe_system.key.sql create mode 100755 sql/llx_kundenkarte_societe_system.sql create mode 100644 sql/llx_kundenkarte_societe_system_contact.sql create mode 100755 tabs/anlagen.php create mode 100644 tabs/contact_anlagen.php create mode 100644 tabs/contact_favoriteproducts.php create mode 100755 tabs/favoriteproducts.php diff --git a/COPYING b/COPYING new file mode 100755 index 0000000..94a0453 --- /dev/null +++ b/COPYING @@ -0,0 +1,621 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + 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 diff --git a/ChangeLog.md b/ChangeLog.md new file mode 100755 index 0000000..35e1770 --- /dev/null +++ b/ChangeLog.md @@ -0,0 +1,47 @@ +# CHANGELOG MODULE KUNDENKARTE FOR [DOLIBARR ERP CRM](https://www.dolibarr.org) + +## 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 diff --git a/README.md b/README.md new file mode 100755 index 0000000..f58c355 --- /dev/null +++ b/README.md @@ -0,0 +1,86 @@ +# 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 +- Flexible Systemkategorien (z.B. Strom, Internet, Kabel, Sat) +- 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) + +### 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 + +## 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 diff --git a/admin/about.php b/admin/about.php new file mode 100755 index 0000000..6f2ab6d --- /dev/null +++ b/admin/about.php @@ -0,0 +1,118 @@ + + * Copyright (C) 2026 Eduard Wisch + * Copyright (C) 2024 Frédéric France + * + * 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 . + */ + +/** + * \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 = ''.$langs->trans("BackToModuleList").''; + +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(); diff --git a/admin/anlage_systems.php b/admin/anlage_systems.php new file mode 100755 index 0000000..eda3dbb --- /dev/null +++ b/admin/anlage_systems.php @@ -0,0 +1,254 @@ +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'); + + 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)"; + $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)"; + + $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'); + + $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 .= " 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'); +$morecss = array('/kundenkarte/css/kundenkarte.css'); + +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 '
'; + print ''; + print ''; + if ($action == 'edit') { + print ''; + } + + print ''; + + print ''; + print ''; + + print ''; + print ''; + + print ''; + print ''; + + print ''; + print ''; + + print ''; + print ''; + + print '
'.$langs->trans('SystemCode').'
'.$langs->trans('SystemLabel').'
'.$langs->trans('SystemPicto').'
'; + print ''; + if ($system && $system->picto) { + print kundenkarte_render_icon($system->picto); + } + print ''; + print ''; + print ''; + print '
'.$langs->trans('SystemColor').'
'.$langs->trans('Position').'
'; + + print '
'; + print ''; + print ' '.$langs->trans('Cancel').''; + print '
'; + + print '
'; + +} else { + // List + print ''; + + $sql = "SELECT * FROM ".MAIN_DB_PREFIX."c_kundenkarte_anlage_system ORDER BY position ASC, label ASC"; + $resql = $db->query($sql); + + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + + if ($resql) { + while ($obj = $db->fetch_object($resql)) { + print ''; + + print ''; + print ''; + print ''; + print ''; + + print ''; + + print ''; + + print ''; + } + } + + print '
'.$langs->trans('SystemCode').''.$langs->trans('SystemLabel').''.$langs->trans('SystemPicto').''.$langs->trans('Position').''.$langs->trans('Status').''.$langs->trans('Actions').'
'.dol_escape_htmltag($obj->code).''.dol_escape_htmltag($obj->label).''; + if ($obj->picto) { + print kundenkarte_render_icon($obj->picto, '', 'color:'.$obj->color.';').' '; + print dol_escape_htmltag($obj->picto); + } + print ''.$obj->position.''; + if ($obj->active) { + print ''.img_picto($langs->trans('Enabled'), 'switch_on').''; + } else { + print ''.img_picto($langs->trans('Disabled'), 'switch_off').''; + } + print ''; + print ''.img_edit().''; + print ' '.img_delete().''; + print '
'; +} + +print dol_get_fiche_end(); + +llxFooter(); +$db->close(); diff --git a/admin/anlage_types.php b/admin/anlage_types.php new file mode 100755 index 0000000..2027f67 --- /dev/null +++ b/admin/anlage_types.php @@ -0,0 +1,647 @@ +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'); +$systemFilter = GETPOSTINT('system'); + +$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 = GETPOST('allowed_parent_types', 'alphanohtml'); + $anlageType->picto = GETPOST('picto', 'alphanohtml'); + $anlageType->color = GETPOST('color', 'alphanohtml'); + $anlageType->position = GETPOSTINT('position'); + $anlageType->active = 1; + + if (empty($anlageType->ref) || empty($anlageType->label) || empty($anlageType->fk_system)) { + setEventMessages($langs->trans('ErrorFieldRequired'), null, 'errors'); + $action = 'create'; + } else { + $result = $anlageType->create($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 = '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 = GETPOST('allowed_parent_types', 'alphanohtml'); + $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 = ''; +} + +// 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', 'restricthtml'); + $showInTree = GETPOSTINT('show_in_tree'); + $showInHover = GETPOSTINT('show_in_hover'); + $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, show_in_hover, required, position, active)"; + $sql .= " VALUES (".((int) $typeId).", '".$db->escape($fieldCode)."', '".$db->escape($fieldLabel)."',"; + $sql .= " '".$db->escape($fieldType)."', '".$db->escape($fieldOptions)."',"; + $sql .= " ".((int) $showInTree).", ".((int) $showInHover).", ".((int) $isRequired).", ".((int) $fieldPosition).", 1)"; + + 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', 'restricthtml'); + $showInTree = GETPOSTINT('show_in_tree'); + $showInHover = GETPOSTINT('show_in_hover'); + $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 .= " show_in_hover = ".((int) $showInHover).","; + $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'); + } + $action = 'edit'; +} + +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'); +$morecss = array('/kundenkarte/css/kundenkarte.css'); + +llxHeader('', $title, '', '', 0, 0, $morejs, $morecss); + +$head = kundenkarteAdminPrepareHead(); +print dol_get_fiche_head($head, 'types', $langs->trans('ModuleKundenKarteName'), -1, 'fa-file'); + +// Confirmation +if ($action == 'delete') { + print $form->formconfirm( + $_SERVER['PHP_SELF'].'?typeid='.$typeId.'&system='.$systemFilter, + $langs->trans('Delete'), + $langs->trans('ConfirmDeleteType'), + 'confirm_delete', + '', + 'yes', + 1 + ); +} + +// Add/Edit form +if (in_array($action, array('create', 'edit'))) { + if ($action == 'edit' && $typeId > 0) { + $anlageType->fetch($typeId); + } + + // Get all types for parent selection + $allTypes = $anlageType->fetchAllBySystem(0, 0); + + print '
'; + print ''; + print ''; + if ($action == 'edit') { + print ''; + } + + print ''; + + // System + print ''; + print ''; + + // Reference + print ''; + print ''; + + // Label + print ''; + print ''; + + // Short label + print ''; + print ''; + + // Description + print ''; + print ''; + + // Can have children + print ''; + print ''; + + // Can be nested + print ''; + print ''; + + // Allowed parent types - with multi-select UI + print ''; + print ''; + + // Icon + print ''; + print ''; + + // Position + print ''; + print ''; + + print '
'.$langs->trans('System').'
'.$langs->trans('TypeRef').''; + print ' (UPPERCASE, no spaces)
'.$langs->trans('TypeLabel').'
'.$langs->trans('TypeShortLabel').'
'.$langs->trans('Description').'
'.$langs->trans('CanHaveChildren').'can_have_children ? ' checked' : '').'>
'.$langs->trans('CanBeNested').'can_be_nested ? ' checked' : '').'>'; + print ' ('.$langs->trans('SameTypeUnderItself').')
'.$langs->trans('AllowedParentTypes').''; + + // Hidden field to store the actual value + print ''; + + // Selection UI + print '
'; + + // Select dropdown with add button + print '
'; + print ''; + print ''; + print '
'; + + // List of selected parent types + print '
'; + // Will be filled by JavaScript + print '
'; + + print '
'; + + print '('.$langs->trans('AllowedParentTypesHelp').')'; + print '
'.$langs->trans('SystemPicto').'
'; + print ''; + if ($anlageType->picto) { + print kundenkarte_render_icon($anlageType->picto); + } + print ''; + print ''; + print ''; + print '
'.$langs->trans('Position').'
'; + + print '
'; + print ''; + print ' '.$langs->trans('Cancel').''; + print '
'; + + print '
'; + + // Fields management for existing type + if ($action == 'edit' && $typeId > 0) { + $editFieldId = GETPOSTINT('editfield'); + + // Confirmation for field deletion + if ($action == 'delete_field') { + print $form->formconfirm( + $_SERVER['PHP_SELF'].'?action=edit&typeid='.$typeId.'&fieldid='.$fieldId.'&system='.$systemFilter, + $langs->trans('Delete'), + $langs->trans('ConfirmDeleteField'), + 'confirm_delete_field', + '', + 'yes', + 1 + ); + } + + print '

'; + print '

'.$langs->trans('AnlagenTypeFields').'

'; + + $fields = $anlageType->fetchFields(0); + + // Field types available + $fieldTypes = array( + 'text' => 'Textfeld (einzeilig)', + 'textarea' => 'Textfeld (mehrzeilig)', + 'number' => 'Zahlenfeld', + 'select' => 'Dropdown-Auswahl', + 'date' => 'Datumsfeld', + 'checkbox' => 'Checkbox (Ja/Nein)', + ); + + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + + foreach ($fields as $field) { + // Check if we're editing this field + if ($editFieldId == $field->id) { + // Edit row + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + } else { + // Display row + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + } + } + + if (empty($fields)) { + print ''; + } + + // Add new field row + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + + print ''; + print ''; + + print '
'.$langs->trans('FieldCode').''.$langs->trans('FieldLabel').''.$langs->trans('FieldType').''.$langs->trans('FieldOptions').''.$langs->trans('ShowInTree').''.$langs->trans('ShowInHover').''.$langs->trans('IsRequired').''.$langs->trans('Position').''.$langs->trans('Status').''.$langs->trans('Actions').'
show_in_tree ? ' checked' : '').'>show_in_hover ? ' checked' : '').'>required ? ' checked' : '').'>'; + print ''; + print ''; + print '
'.dol_escape_htmltag($field->field_code).''.dol_escape_htmltag($field->field_label).''.dol_escape_htmltag($fieldTypes[$field->field_type] ?? $field->field_type).''.dol_escape_htmltag(dol_trunc($field->field_options, 20)).''.($field->show_in_tree ? img_picto('', 'tick') : '').''.($field->show_in_hover ? img_picto('', 'tick') : '').''.($field->required ? img_picto('', 'tick') : '').''.$field->position.''; + if ($field->active) { + print ''.img_picto($langs->trans('Enabled'), 'switch_on').''; + } else { + print ''.img_picto($langs->trans('Disabled'), 'switch_off').''; + } + print ''; + print ''.img_edit().''; + print ' '.img_delete().''; + print '
'.$langs->trans('NoFieldsDefined').'
'; + + // Help box for field options + print '
'; + print '

Hilfe: Feld-Optionen nach Feldtyp

'; + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + print '
Textfeld (einzeilig)Keine Optionen nötig
Textfeld (mehrzeilig)Keine Optionen nötig
ZahlenfeldOptional: min:0|max:100|step:0.1
Dropdown-AuswahlPflicht! Optionen mit | trennen, z.B.: Option A|Option B|Option C
DatumsfeldKeine Optionen nötig
Checkbox (Ja/Nein)Keine Optionen nötig
'; + print '
'; + } + +} else { + // System filter + print '
'; + print $langs->trans('FilterBySystem').': '; + print ''; + print '
'; + + // Add button + print ''; + + // List + $types = $anlageType->fetchAllBySystem($systemFilter, 0); + + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + + foreach ($types as $type) { + print ''; + + print ''; + + print ''; + + print ''; + print ''; + print ''; + + print ''; + + print ''; + + print ''; + } + + if (empty($types)) { + print ''; + } + + print '
'.$langs->trans('TypeRef').''.$langs->trans('TypeLabel').''.$langs->trans('System').''.$langs->trans('CanHaveChildren').''.$langs->trans('Position').''.$langs->trans('Status').''.$langs->trans('Actions').'
'; + if ($type->picto) { + print kundenkarte_render_icon($type->picto).' '; + } + print dol_escape_htmltag($type->ref).''.dol_escape_htmltag($type->label); + if ($type->label_short) { + print ' ('.$type->label_short.')'; + } + print ''.dol_escape_htmltag($type->system_label).''.($type->can_have_children ? img_picto('', 'tick') : '').''.$type->position.''; + if ($type->active) { + print ''.img_picto($langs->trans('Enabled'), 'switch_on').''; + } else { + print ''.img_picto($langs->trans('Disabled'), 'switch_off').''; + } + print ''; + print ''.img_edit().''; + if (!$type->is_system) { + print ' '.img_delete().''; + } + print '
'.$langs->trans('NoRecords').'
'; +} + +print dol_get_fiche_end(); + +// JavaScript for parent type selector +print ''; + +llxFooter(); +$db->close(); diff --git a/admin/setup.php b/admin/setup.php new file mode 100755 index 0000000..e5d012c --- /dev/null +++ b/admin/setup.php @@ -0,0 +1,160 @@ + + * + * 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 . + */ + +/** + * \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'); + +/* + * Actions + */ + +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++; + } + + 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 = ''.$langs->trans("BackToModuleList").''; +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 ''.$langs->trans("KundenKarteSetupPage").'

'; + +print '
'; +print ''; +print ''; + +print ''; + +// Header +print ''; +print ''; +print ''; +print ''; + +// Show Favorites Tab +print ''; +print ''; +print ''; +print ''; + +// Show Anlagen Tab +print ''; +print ''; +print ''; +print ''; + +// Default Order Type for Favorites +print ''; +print ''; +print ''; +print ''; + +print '
'.$langs->trans("Parameter").''.$langs->trans("Value").'
'.$langs->trans("ShowFavoritesTab").''; +print $form->selectyesno('KUNDENKARTE_SHOW_FAVORITES_TAB', getDolGlobalInt('KUNDENKARTE_SHOW_FAVORITES_TAB', 1), 1); +print '
'.$langs->trans("ShowAnlagenTab").''; +print $form->selectyesno('KUNDENKARTE_SHOW_ANLAGEN_TAB', getDolGlobalInt('KUNDENKARTE_SHOW_ANLAGEN_TAB', 1), 1); +print '
'.$langs->trans("DefaultOrderType").''; +$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 '
'; + +print '
'; +print '
'; +print ''; +print '
'; + +print '
'; + +// Info section +print '
'; +print '
'; +print ''.$langs->trans("ConfigurationHelp").':
'; +print '• '.$langs->trans("ConfigHelpSystems").'
'; +print '• '.$langs->trans("ConfigHelpTypes").'
'; +print '
'; + +print dol_get_fiche_end(); + +llxFooter(); +$db->close(); diff --git a/ajax/anlage_docs.php b/ajax/anlage_docs.php new file mode 100755 index 0000000..9a96612 --- /dev/null +++ b/ajax/anlage_docs.php @@ -0,0 +1,54 @@ + '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)); diff --git a/ajax/anlage_images.php b/ajax/anlage_images.php new file mode 100755 index 0000000..e3b4a95 --- /dev/null +++ b/ajax/anlage_images.php @@ -0,0 +1,50 @@ + '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)); diff --git a/ajax/anlage_tooltip.php b/ajax/anlage_tooltip.php new file mode 100755 index 0000000..665323a --- /dev/null +++ b/ajax/anlage_tooltip.php @@ -0,0 +1,102 @@ +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) { + $value = isset($fieldValues[$field->field_code]) ? $fieldValues[$field->field_code] : ''; + + // For select fields, get label + if ($field->field_type == 'select' && $value && $field->field_options) { + $options = json_decode($field->field_options, true); + if (isset($options['options'][$value])) { + $value = $options['options'][$value]; + } + } + + $fieldsData[$field->field_code] = array( + 'label' => $field->field_label, + 'value' => $value, + 'show_in_hover' => true + ); + } +} + +// 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, + 'manufacturer' => $anlage->manufacturer, + 'model' => $anlage->model, + 'serial_number' => $anlage->serial_number, + 'power_rating' => $anlage->power_rating, + 'location' => $anlage->location, + 'fields' => $fieldsData, + 'images' => $images +); + +echo json_encode(array('success' => true, 'data' => $data)); diff --git a/ajax/favorite_update.php b/ajax/favorite_update.php new file mode 100644 index 0000000..ce14ba4 --- /dev/null +++ b/ajax/favorite_update.php @@ -0,0 +1,75 @@ +hasRight('kundenkarte', 'write')) { + http_response_code(403); + echo json_encode(array('error' => 'Permission denied')); + exit; +} + +header('Content-Type: application/json'); + +$id = GETPOSTINT('id'); +$qty = GETPOSTFLOAT('qty'); + +if ($id <= 0) { + echo json_encode(array('error' => 'Invalid ID')); + exit; +} + +if ($qty <= 0) { + echo json_encode(array('error' => 'Invalid quantity')); + exit; +} + +$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)); +} diff --git a/ajax/icon_upload.php b/ajax/icon_upload.php new file mode 100644 index 0000000..2fc0ca2 --- /dev/null +++ b/ajax/icon_upload.php @@ -0,0 +1,145 @@ + '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'))); diff --git a/build/buildzip.php b/build/buildzip.php new file mode 100755 index 0000000..3508bbb --- /dev/null +++ b/build/buildzip.php @@ -0,0 +1,316 @@ +#!/usr/bin/env php -d memory_limit=256M + + * + * 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 . + */ + +/* + 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(?.*)\.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*'(?.*)'\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); +} diff --git a/build/makepack-kundenkarte.conf b/build/makepack-kundenkarte.conf new file mode 100755 index 0000000..16dc1e7 --- /dev/null +++ b/build/makepack-kundenkarte.conf @@ -0,0 +1,11 @@ +# Your module name here +# +# Goal: Goal of module +# Version: +# Author: Copyright - +# 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/ \ No newline at end of file diff --git a/class/anlage.class.php b/class/anlage.class.php new file mode 100755 index 0000000..73b30b3 --- /dev/null +++ b/class/anlage.class.php @@ -0,0 +1,633 @@ +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; + } + + // 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->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; + + // 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; + } + } + + /** + * 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; + } + + /** + * 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; + } +} diff --git a/class/anlagefile.class.php b/class/anlagefile.class.php new file mode 100755 index 0000000..213fe2c --- /dev/null +++ b/class/anlagefile.class.php @@ -0,0 +1,417 @@ +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, 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->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->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_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->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'; + } + + 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; + } + } + + /** + * 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); + } +} diff --git a/class/anlagetype.class.php b/class/anlagetype.class.php new file mode 100755 index 0000000..92feaae --- /dev/null +++ b/class/anlagetype.class.php @@ -0,0 +1,369 @@ +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,"; + $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 .= ", ".($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->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 .= ", 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 + * @return array Array of AnlageType 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 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->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); + } +} diff --git a/class/favoriteproduct.class.php b/class/favoriteproduct.class.php new file mode 100755 index 0000000..7cb2904 --- /dev/null +++ b/class/favoriteproduct.class.php @@ -0,0 +1,802 @@ +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; + } +} diff --git a/core/modules/modKundenKarte.class.php b/core/modules/modKundenKarte.class.php new file mode 100755 index 0000000..710578c --- /dev/null +++ b/core/modules/modKundenKarte.class.php @@ -0,0 +1,548 @@ + + * Copyright (C) 2018-2019 Nicolas ZABOURI + * Copyright (C) 2019-2024 Frédéric France + * Copyright (C) 2026 Eduard Wisch + * + * 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 . + */ + +/** + * \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 = '1.1'; + // 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 + '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, + ); + /* 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")'); + + // 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); + } + + /** + * 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); + } +} diff --git a/css/kundenkarte.css b/css/kundenkarte.css new file mode 100755 index 0000000..db3931e --- /dev/null +++ b/css/kundenkarte.css @@ -0,0 +1,832 @@ +/** + * KundenKarte Module Styles + * Copyright (C) 2026 Alles Watt lauft + */ + +/* ======================================== + TREE STRUCTURE + ======================================== */ + +.kundenkarte-tree { + font-family: inherit; + padding: 10px 0; +} + +.kundenkarte-tree-node { + position: relative; + padding-left: 20px; + margin: 2px 0; +} + +.kundenkarte-tree-node::before { + content: ''; + position: absolute; + left: 0; + top: 0; + height: 100%; + width: 1px; + background: #ddd; +} + +.kundenkarte-tree-node::after { + content: ''; + position: absolute; + left: 0; + top: 12px; + width: 15px; + height: 1px; + background: #ddd; +} + +.kundenkarte-tree-node:last-child::before { + height: 12px; +} + +.kundenkarte-tree-item { + display: flex; + align-items: center; + padding: 6px 10px; + border-radius: 4px; + background: #f9f9f9; + border: 1px solid #e0e0e0; + cursor: pointer; + transition: all 0.2s ease; +} + +.kundenkarte-tree-item:hover { + background: #f0f0f0; + border-color: #ccc; +} + +.kundenkarte-tree-item.selected { + background: #e3f2fd; + border-color: #2196f3; +} + +.kundenkarte-tree-toggle { + width: 20px; + height: 20px; + display: flex; + align-items: center; + justify-content: center; + margin-right: 5px; + cursor: pointer; + color: #666; +} + +.kundenkarte-tree-toggle:hover { + color: #333; + background-color: rgba(0, 0, 0, 0.1); + border-radius: 3px; +} + +.kundenkarte-tree-toggle .fa-chevron-down { + transition: transform 0.2s ease; +} + +.kundenkarte-tree-toggle.collapsed .fa-chevron-down { + transform: rotate(-90deg); +} + +.kundenkarte-tree-icon { + width: 24px; + height: 24px; + display: flex; + align-items: center; + justify-content: center; + margin-right: 8px; + font-size: 14px; +} + +.kundenkarte-tree-label { + flex: 1; + font-weight: 500; +} + +.kundenkarte-tree-label-info { + font-weight: normal; + color: #666; + font-size: 0.9em; +} + +.kundenkarte-tree-info { + color: #888; + font-size: 0.9em; + margin-left: 10px; +} + +.kundenkarte-tree-actions { + display: none; + margin-left: 10px; +} + +.kundenkarte-tree-item:hover .kundenkarte-tree-actions { + display: flex; + gap: 5px; +} + +.kundenkarte-tree-actions a { + padding: 2px 6px; + border-radius: 3px; + color: #666; +} + +.kundenkarte-tree-actions a:hover { + background: #ddd; + color: #333; +} + +.kundenkarte-tree-children { + margin-left: 10px; + border-left: 1px dashed #ccc; + padding-left: 10px; +} + +.kundenkarte-tree-children.collapsed { + display: none !important; +} + +/* Tree - Type Badge */ +.kundenkarte-tree-type { + font-size: 0.75em; + padding: 2px 8px; + border-radius: 10px; + background: #e0e0e0; + color: #555; + margin-left: 8px; + font-weight: normal; +} + +/* Tree - Location */ +.kundenkarte-tree-location { + font-size: 0.85em; + color: #666; + margin-left: 10px; + display: flex; + align-items: center; + gap: 4px; +} + +.kundenkarte-tree-location i { + color: #e74c3c; + font-size: 0.9em; +} + +/* Tree - File Indicators */ +.kundenkarte-tree-files { + display: inline-flex; + gap: 3px; + margin-left: 6px; + vertical-align: middle; +} + +.kundenkarte-tree-file-badge { + font-size: 0.8em; + padding: 2px 6px; + border-radius: 4px; + display: flex; + align-items: center; + gap: 3px; +} + +.kundenkarte-tree-file-images { + background: #e3f2fd; + color: #1976d2; +} + +.kundenkarte-tree-file-docs { + background: #fff3e0; + color: #f57c00; +} + +/* ======================================== + HOVER TOOLTIP + ======================================== */ + +.kundenkarte-tooltip { + position: absolute; + z-index: 1000; + background: #fff; + border: 1px solid #ccc; + border-radius: 6px; + box-shadow: 0 4px 12px rgba(0,0,0,0.15); + padding: 15px; + min-width: 300px; + max-width: 450px; + display: none; +} + +.kundenkarte-tooltip.visible { + display: block; +} + +.kundenkarte-tooltip-header { + display: flex; + align-items: center; + margin-bottom: 10px; + padding-bottom: 10px; + border-bottom: 1px solid #eee; +} + +.kundenkarte-tooltip-icon { + font-size: 24px; + margin-right: 12px; +} + +.kundenkarte-tooltip-title { + font-weight: 600; + font-size: 1.1em; +} + +.kundenkarte-tooltip-type { + color: #888; + font-size: 0.9em; +} + +.kundenkarte-tooltip-fields { + display: grid; + grid-template-columns: auto 1fr; + gap: 6px 12px; + font-size: 0.95em; +} + +.kundenkarte-tooltip-field-label { + color: #666; + font-weight: 500; +} + +.kundenkarte-tooltip-field-value { + color: #333; +} + +.kundenkarte-tooltip-images { + display: flex; + gap: 8px; + margin-top: 12px; + padding-top: 12px; + border-top: 1px solid #eee; + overflow-x: auto; +} + +.kundenkarte-tooltip-thumb { + width: 60px; + height: 60px; + object-fit: cover; + border-radius: 4px; + border: 1px solid #ddd; +} + +/* ======================================== + SYSTEM TABS + ======================================== */ + +.kundenkarte-system-tabs-wrapper { + display: flex; + justify-content: space-between; + align-items: flex-start; + margin-bottom: 15px; + padding-bottom: 10px; + border-bottom: 2px solid #e0e0e0; +} + +.kundenkarte-system-tabs { + display: flex; + flex-wrap: wrap; + gap: 5px; + flex: 1; + align-items: center; +} + +.kundenkarte-tree-controls { + display: flex; + gap: 5px; + flex-shrink: 0; +} + +.kundenkarte-tree-controls .button { + padding: 4px 10px; + font-size: 0.85em; +} + +.kundenkarte-system-tab { + padding: 8px 16px; + border: 1px solid #ddd; + border-radius: 4px 4px 0 0; + background: #f5f5f5; + cursor: pointer; + display: flex; + align-items: center; + gap: 8px; + transition: all 0.2s; +} + +.kundenkarte-system-tab:hover { + background: #e8e8e8; +} + +.kundenkarte-system-tab.active { + background: #fff; + border-bottom-color: #fff; + margin-bottom: -2px; + padding-bottom: 10px; +} + +.kundenkarte-system-tab-icon { + font-size: 16px; +} + +.kundenkarte-system-tab-add { + background: transparent; + border-style: dashed; + color: #888; +} + +.kundenkarte-system-tab-add:hover { + background: #f0f0f0; + color: #333; +} + +/* ======================================== + FAVORITE PRODUCTS + ======================================== */ + +.kundenkarte-favorites { + margin-top: 10px; +} + +.kundenkarte-favorites-add { + display: flex; + gap: 10px; + margin-bottom: 20px; + padding: 15px; + background: #f9f9f9; + border-radius: 6px; +} + +.kundenkarte-favorites-add .product-search { + flex: 1; +} + +.kundenkarte-favorites-table { + width: 100%; +} + +.kundenkarte-favorites-table th { + text-align: left; + padding: 10px; + background: #f5f5f5; + border-bottom: 2px solid #ddd; +} + +.kundenkarte-favorites-table td { + padding: 10px; + border-bottom: 1px solid #eee; + vertical-align: middle; +} + +.kundenkarte-favorites-table tr:hover { + background: #f9f9f9; +} + +.kundenkarte-favorites-qty { + width: 80px; + text-align: center; +} + +.kundenkarte-favorites-actions { + margin-top: 20px; + padding-top: 15px; + border-top: 1px solid #ddd; + display: flex; + justify-content: space-between; + align-items: center; +} + +/* ======================================== + ELEMENT FORM + ======================================== */ + +.kundenkarte-element-form { + max-width: 800px; +} + +.kundenkarte-element-form .field-row { + margin-bottom: 15px; +} + +.kundenkarte-element-form label { + display: block; + margin-bottom: 5px; + font-weight: 500; +} + +.kundenkarte-element-form .required::after { + content: ' *'; + color: #e74c3c; +} + +/* ======================================== + FILE GALLERY + ======================================== */ + +.kundenkarte-files { + margin-top: 20px; +} + +.kundenkarte-files-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); + gap: 15px; + margin-top: 15px; +} + +.kundenkarte-file-item { + border: 1px solid #ddd; + border-radius: 6px; + overflow: hidden; + background: #fff; +} + +.kundenkarte-file-preview { + height: 140px; + display: flex; + align-items: center; + justify-content: center; + background: #f5f5f5; + overflow: hidden; +} + +.kundenkarte-file-preview img { + max-width: 100%; + max-height: 100%; + object-fit: contain; +} + +.kundenkarte-file-preview embed { + width: 100%; + height: 100%; + border: none; +} + +.kundenkarte-file-preview .fa { + font-size: 48px; + color: #999; +} + +.kundenkarte-file-info { + padding: 10px; + font-size: 0.9em; +} + +.kundenkarte-file-name { + font-weight: 500; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.kundenkarte-file-size { + color: #888; + font-size: 0.85em; +} + +.kundenkarte-file-actions { + display: flex; + gap: 5px; + margin-top: 8px; + justify-content: center; +} + +.kundenkarte-file-btn { + display: inline-flex; + align-items: center; + justify-content: center; + width: 28px; + height: 28px; + border-radius: 4px; + background: #f0f0f0; + color: #555; + text-decoration: none; + transition: all 0.2s; + font-size: 14px; +} + +.kundenkarte-file-btn:hover { + background: #2196f3; + color: #fff; +} + +.kundenkarte-file-btn-delete { + color: #c00; +} + +.kundenkarte-file-btn-delete:hover { + background: #c00; + color: #fff; +} + +.kundenkarte-file-cover { + position: absolute; + top: 5px; + right: 5px; + background: #4caf50; + color: #fff; + padding: 2px 6px; + border-radius: 3px; + font-size: 0.75em; +} + +/* ======================================== + ICON PICKER MODAL + ======================================== */ + +.kundenkarte-modal { + display: none; + position: fixed; + z-index: 10000; + left: 0; + top: 0; + width: 100%; + height: 100%; + background-color: rgba(0,0,0,0.5); + align-items: center; + justify-content: center; +} + +.kundenkarte-modal.visible { + display: flex; +} + +.kundenkarte-modal-content { + background-color: #fff; + border-radius: 8px; + box-shadow: 0 8px 32px rgba(0,0,0,0.3); + max-width: 600px; + width: 90%; + max-height: 80vh; + display: flex; + flex-direction: column; +} + +.kundenkarte-modal-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 15px 20px; + border-bottom: 1px solid #e0e0e0; +} + +.kundenkarte-modal-header h3 { + margin: 0; + font-size: 1.2em; +} + +.kundenkarte-modal-close { + font-size: 28px; + font-weight: bold; + color: #888; + cursor: pointer; + line-height: 1; +} + +.kundenkarte-modal-close:hover { + color: #333; +} + +.kundenkarte-modal-body { + padding: 20px; + overflow-y: auto; +} + +.kundenkarte-icon-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(50px, 1fr)); + gap: 8px; + max-height: 400px; + overflow-y: auto; +} + +.kundenkarte-icon-item { + width: 50px; + height: 50px; + display: flex; + align-items: center; + justify-content: center; + border: 1px solid #e0e0e0; + border-radius: 6px; + cursor: pointer; + transition: all 0.2s; + font-size: 20px; + color: #555; +} + +.kundenkarte-icon-item:hover { + background: #e3f2fd; + border-color: #2196f3; + color: #2196f3; + transform: scale(1.1); +} + +.kundenkarte-icon-item:hover .kundenkarte-icon-delete { + display: block !important; +} + +.kundenkarte-icon-delete:hover { + background: #900 !important; +} + +/* Icon picker button in forms */ +.kundenkarte-icon-picker-wrapper { + display: flex; + align-items: center; + gap: 10px; +} + +.kundenkarte-icon-picker-wrapper input { + flex: 1; +} + +.kundenkarte-icon-picker-btn { + padding: 5px 12px; + border: 1px solid #ccc; + border-radius: 4px; + background: #f5f5f5; + cursor: pointer; + display: flex; + align-items: center; + gap: 5px; +} + +.kundenkarte-icon-picker-btn:hover { + background: #e8e8e8; +} + +.kundenkarte-icon-preview { + width: 30px; + height: 30px; + display: flex; + align-items: center; + justify-content: center; + border: 1px solid #ddd; + border-radius: 4px; + background: #fff; + font-size: 16px; +} + +/* ======================================== + TAG SELECTOR (Admin) + ======================================== */ + +.kundenkarte-selected-items { + display: flex; + flex-wrap: wrap; + gap: 8px; + min-height: 30px; +} + +.kundenkarte-tag { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 4px 10px; + background: #e3f2fd; + border: 1px solid #90caf9; + border-radius: 16px; + font-size: 0.9em; + color: #1565c0; +} + +.kundenkarte-tag-remove { + cursor: pointer; + font-weight: bold; + font-size: 1.2em; + line-height: 1; + color: #1976d2; + margin-left: 2px; +} + +.kundenkarte-tag-remove:hover { + color: #c62828; +} + +/* ======================================== + IMAGES POPUP + ======================================== */ + +.kundenkarte-images-popup { + padding: 5px; +} + +.kundenkarte-images-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(80px, 1fr)); + gap: 8px; + max-width: 350px; +} + +.kundenkarte-images-thumb { + display: block; + width: 80px; + height: 80px; + border-radius: 4px; + overflow: hidden; + border: 1px solid #ddd; + transition: transform 0.2s, box-shadow 0.2s; +} + +.kundenkarte-images-thumb:hover { + transform: scale(1.05); + box-shadow: 0 2px 8px rgba(0,0,0,0.2); + border-color: #2196f3; +} + +.kundenkarte-images-thumb img { + width: 100%; + height: 100%; + object-fit: cover; +} + +/* ======================================== + DOCUMENTS POPUP - Visual cards with icons + ======================================== */ + +.kundenkarte-docs-popup { + padding: 5px; +} + +.kundenkarte-docs-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(80px, 1fr)); + gap: 8px; + max-width: 350px; +} + +.kundenkarte-docs-card { + display: flex; + flex-direction: column; + align-items: center; + padding: 10px 5px; + border-radius: 6px; + border: 1px solid #e0e0e0; + background: #f9f9f9; + text-decoration: none; + transition: all 0.2s ease; + width: 80px; +} + +.kundenkarte-docs-card:hover { + transform: scale(1.05); + box-shadow: 0 2px 8px rgba(0,0,0,0.15); + border-color: #2196f3; + background: #fff; +} + +.kundenkarte-docs-card-icon { + font-size: 36px; + margin-bottom: 6px; +} + +.kundenkarte-docs-card-name { + font-size: 0.75em; + color: #333; + text-align: center; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + max-width: 70px; +} + +/* Tree icon clickable */ +.kundenkarte-tree-icon.kundenkarte-tooltip-trigger { + cursor: help; +} + +/* File badges as links */ +a.kundenkarte-tree-file-badge { + text-decoration: none; + cursor: pointer; +} + +a.kundenkarte-tree-file-badge:hover { + transform: scale(1.1); +} + +/* ======================================== + PDF PREVIEW (smaller, no toolbar) + ======================================== */ + +.kundenkarte-pdf-preview-wrapper { + width: 100%; + height: 100%; + overflow: hidden; + position: relative; +} + +.kundenkarte-pdf-preview-frame { + width: 200%; + height: 200%; + border: none; + transform: scale(0.5); + transform-origin: top left; + pointer-events: none; +} diff --git a/img/README.md b/img/README.md new file mode 100755 index 0000000..eff8424 --- /dev/null +++ b/img/README.md @@ -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) + diff --git a/js/kundenkarte.js b/js/kundenkarte.js new file mode 100755 index 0000000..fc61913 --- /dev/null +++ b/js/kundenkarte.js @@ -0,0 +1,950 @@ +/** + * KundenKarte Module JavaScript + * Copyright (C) 2026 Alles Watt lauft + */ + +(function() { + 'use strict'; + + // Namespace + window.KundenKarte = window.KundenKarte || {}; + + // Get base URL for AJAX calls + var baseUrl = (typeof DOL_URL_ROOT !== 'undefined') ? DOL_URL_ROOT : ''; + if (!baseUrl) { + // Try to detect from script src + var scripts = document.getElementsByTagName('script'); + for (var i = 0; i < scripts.length; i++) { + var src = scripts[i].src; + if (src && src.indexOf('/kundenkarte/js/kundenkarte.js') > -1) { + baseUrl = src.replace('/custom/kundenkarte/js/kundenkarte.js', '').replace(/\?.*$/, ''); + break; + } + } + } + + /** + * Tree Component + */ + KundenKarte.Tree = { + tooltipTimeout: null, + hideTimeout: null, + currentTooltip: null, + currentItem: null, + + init: function() { + this.bindEvents(); + }, + + bindEvents: function() { + var self = this; + + // Toggle tree nodes - MUST use stopImmediatePropagation for delegated handlers on same element + $(document).on('click', '.kundenkarte-tree-toggle', function(e) { + e.preventDefault(); + e.stopPropagation(); + e.stopImmediatePropagation(); + + var $toggle = $(this); + var $node = $toggle.closest('.kundenkarte-tree-node'); + var $children = $node.children('.kundenkarte-tree-children'); + + $toggle.toggleClass('collapsed'); + $children.toggleClass('collapsed'); + }); + + // Expand all nodes + $(document).on('click', '#btn-expand-all', function(e) { + e.preventDefault(); + self.expandAll(); + }); + + // Collapse all nodes + $(document).on('click', '#btn-collapse-all', function(e) { + e.preventDefault(); + self.collapseAll(); + }); + + // Hover tooltip on ICON only - show after delay + $(document).on('mouseenter', '.kundenkarte-tooltip-trigger', function(e) { + var $trigger = $(this); + var anlageId = $trigger.data('anlage-id'); + + if (!anlageId) return; + + // Cancel any pending hide + clearTimeout(self.hideTimeout); + self.hideTimeout = null; + + self.currentItem = $trigger; + + self.tooltipTimeout = setTimeout(function() { + self.showTooltip($trigger, anlageId); + }, 300); + }); + + // Hide tooltip when leaving icon + $(document).on('mouseleave', '.kundenkarte-tooltip-trigger', function() { + clearTimeout(self.tooltipTimeout); + self.tooltipTimeout = null; + self.currentItem = null; + + // Hide after short delay (allows moving to tooltip) + self.hideTimeout = setTimeout(function() { + self.hideTooltip(); + }, 100); + }); + + // Images tooltip on hover + $(document).on('mouseenter', '.kundenkarte-images-trigger', function(e) { + var $trigger = $(this); + var anlageId = $trigger.data('anlage-id'); + + if (!anlageId) return; + + clearTimeout(self.hideTimeout); + self.hideTimeout = null; + + self.tooltipTimeout = setTimeout(function() { + self.showImagesPopup($trigger, anlageId); + }, 300); + }); + + $(document).on('mouseleave', '.kundenkarte-images-trigger', function() { + clearTimeout(self.tooltipTimeout); + self.tooltipTimeout = null; + + self.hideTimeout = setTimeout(function() { + self.hideTooltip(); + }, 100); + }); + + // Documents tooltip on hover + $(document).on('mouseenter', '.kundenkarte-docs-trigger', function(e) { + var $trigger = $(this); + var anlageId = $trigger.data('anlage-id'); + + if (!anlageId) return; + + clearTimeout(self.hideTimeout); + self.hideTimeout = null; + + self.tooltipTimeout = setTimeout(function() { + self.showDocsPopup($trigger, anlageId); + }, 300); + }); + + $(document).on('mouseleave', '.kundenkarte-docs-trigger', function() { + clearTimeout(self.tooltipTimeout); + self.tooltipTimeout = null; + + self.hideTimeout = setTimeout(function() { + self.hideTooltip(); + }, 100); + }); + + // Keep tooltip visible when hovering over it + $(document).on('mouseenter', '#kundenkarte-tooltip', function() { + clearTimeout(self.hideTimeout); + self.hideTimeout = null; + }); + + // Hide when leaving tooltip + $(document).on('mouseleave', '#kundenkarte-tooltip', function() { + self.hideTooltip(); + }); + + // Select item + $(document).on('click', '.kundenkarte-tree-item', function(e) { + if ($(e.target).closest('.kundenkarte-tree-toggle, .kundenkarte-tree-actions, .kundenkarte-tree-files').length) { + return; + } + + $('.kundenkarte-tree-item').removeClass('selected'); + $(this).addClass('selected'); + + var anlageId = $(this).data('anlage-id'); + if (anlageId) { + $(document).trigger('kundenkarte:element:selected', [anlageId]); + } + }); + }, + + showTooltip: function($item, anlageId) { + var self = this; + + // Get tooltip data from data attribute (faster than AJAX) + var tooltipDataStr = $item.attr('data-tooltip'); + if (!tooltipDataStr) { + console.log('No tooltip data for anlage', anlageId); + return; + } + + var data; + try { + data = JSON.parse(tooltipDataStr); + } catch(e) { + console.error('Failed to parse tooltip JSON:', e, tooltipDataStr); + return; + } + + var html = self.buildTooltipHtml(data); + var $tooltip = $('#kundenkarte-tooltip'); + + if (!$tooltip.length) { + $tooltip = $('
'); + $('body').append($tooltip); + } + + $tooltip.html(html); + + // Position tooltip + var offset = $item.offset(); + var itemWidth = $item.outerWidth(); + var windowWidth = $(window).width(); + var scrollTop = $(window).scrollTop(); + + // First show to calculate width + $tooltip.css({ visibility: 'hidden', display: 'block' }); + var tooltipWidth = $tooltip.outerWidth(); + var tooltipHeight = $tooltip.outerHeight(); + $tooltip.css({ visibility: '', display: '' }); + + var left = offset.left + itemWidth + 10; + if (left + tooltipWidth > windowWidth - 20) { + left = offset.left - tooltipWidth - 10; + } + if (left < 10) { + left = 10; + } + + var top = offset.top; + // Prevent tooltip from going below viewport + if (top + tooltipHeight > scrollTop + $(window).height() - 20) { + top = scrollTop + $(window).height() - tooltipHeight - 20; + } + + $tooltip.css({ + top: top, + left: left + }).addClass('visible').show(); + + self.currentTooltip = $tooltip; + }, + + hideTooltip: function() { + clearTimeout(this.hideTimeout); + this.hideTimeout = null; + var $tooltip = $('#kundenkarte-tooltip'); + if ($tooltip.length) { + $tooltip.removeClass('visible').hide(); + } + this.currentTooltip = null; + }, + + showImagesPopup: function($trigger, anlageId) { + var self = this; + + // Load images via AJAX - use absolute path + $.ajax({ + url: baseUrl + '/custom/kundenkarte/ajax/anlage_images.php', + data: { anlage_id: anlageId }, + dataType: 'json', + success: function(response) { + if (!response.images || response.images.length === 0) { + return; + } + + var html = '
'; + html += '
'; + for (var i = 0; i < response.images.length; i++) { + var img = response.images[i]; + html += ''; + html += '' + self.escapeHtml(img.name) + ''; + html += ''; + } + html += '
'; + html += '
'; + + var $tooltip = $('#kundenkarte-tooltip'); + if (!$tooltip.length) { + $tooltip = $('
'); + $('body').append($tooltip); + } + + $tooltip.html(html); + + // Position tooltip + var offset = $trigger.offset(); + var windowWidth = $(window).width(); + var scrollTop = $(window).scrollTop(); + + $tooltip.css({ visibility: 'hidden', display: 'block' }); + var tooltipWidth = $tooltip.outerWidth(); + var tooltipHeight = $tooltip.outerHeight(); + $tooltip.css({ visibility: '', display: '' }); + + var left = offset.left + $trigger.outerWidth() + 10; + if (left + tooltipWidth > windowWidth - 20) { + left = offset.left - tooltipWidth - 10; + } + if (left < 10) left = 10; + + var top = offset.top; + if (top + tooltipHeight > scrollTop + $(window).height() - 20) { + top = scrollTop + $(window).height() - tooltipHeight - 20; + } + + $tooltip.css({ top: top, left: left }).addClass('visible').show(); + self.currentTooltip = $tooltip; + } + }); + }, + + showDocsPopup: function($trigger, anlageId) { + var self = this; + + // Load documents via AJAX + $.ajax({ + url: baseUrl + '/custom/kundenkarte/ajax/anlage_docs.php', + data: { anlage_id: anlageId }, + dataType: 'json', + success: function(response) { + if (!response.docs || response.docs.length === 0) { + return; + } + + // Visual document cards with icons + var html = '
'; + html += '
'; + for (var i = 0; i < response.docs.length; i++) { + var doc = response.docs[i]; + var iconClass = doc.type === 'pdf' ? 'fa-file-pdf-o' : 'fa-file-text-o'; + var iconColor = doc.type === 'pdf' ? '#e74c3c' : '#f39c12'; + html += ''; + html += '
'; + html += ''; + html += '
'; + html += '
' + self.escapeHtml(doc.name) + '
'; + html += '
'; + } + html += '
'; + html += '
'; + + var $tooltip = $('#kundenkarte-tooltip'); + if (!$tooltip.length) { + $tooltip = $('
'); + $('body').append($tooltip); + } + + $tooltip.html(html); + + // Position tooltip + var offset = $trigger.offset(); + var windowWidth = $(window).width(); + var scrollTop = $(window).scrollTop(); + + $tooltip.css({ visibility: 'hidden', display: 'block' }); + var tooltipWidth = $tooltip.outerWidth(); + var tooltipHeight = $tooltip.outerHeight(); + $tooltip.css({ visibility: '', display: '' }); + + var left = offset.left + $trigger.outerWidth() + 10; + if (left + tooltipWidth > windowWidth - 20) { + left = offset.left - tooltipWidth - 10; + } + if (left < 10) left = 10; + + var top = offset.top; + if (top + tooltipHeight > scrollTop + $(window).height() - 20) { + top = scrollTop + $(window).height() - tooltipHeight - 20; + } + + $tooltip.css({ top: top, left: left }).addClass('visible').show(); + self.currentTooltip = $tooltip; + } + }); + }, + + buildTooltipHtml: function(data) { + var html = '
'; + html += ''; + html += '
'; + html += '
' + this.escapeHtml(data.label || '') + '
'; + html += '
' + this.escapeHtml(data.type || data.type_label || '') + '
'; + html += '
'; + + html += '
'; + + if (data.location) { + html += ' Standort:'; + html += '' + this.escapeHtml(data.location) + ''; + } + + if (data.manufacturer) { + html += 'Hersteller:'; + html += '' + this.escapeHtml(data.manufacturer) + ''; + } + + if (data.model) { + html += 'Modell:'; + html += '' + this.escapeHtml(data.model) + ''; + } + + if (data.serial_number) { + html += 'Seriennummer:'; + html += '' + this.escapeHtml(data.serial_number) + ''; + } + + if (data.power_rating) { + html += 'Leistung:'; + html += '' + this.escapeHtml(data.power_rating) + ''; + } + + if (data.installation_date) { + html += 'Installiert:'; + html += '' + this.escapeHtml(data.installation_date) + ''; + } + + // Dynamic fields (from AJAX) + if (data.fields) { + for (var key in data.fields) { + if (data.fields.hasOwnProperty(key) && data.fields[key].show_in_hover) { + html += '' + this.escapeHtml(data.fields[key].label) + ':'; + html += '' + this.escapeHtml(data.fields[key].value) + ''; + } + } + } + + html += '
'; + + // Notes + if (data.note) { + html += '
'; + html += ' ' + this.escapeHtml(data.note); + html += '
'; + } + + // Images (from AJAX) + if (data.images && data.images.length > 0) { + html += '
'; + for (var i = 0; i < Math.min(data.images.length, 4); i++) { + html += ''; + } + html += '
'; + } + + return html; + }, + + escapeHtml: function(text) { + if (!text) return ''; + var div = document.createElement('div'); + div.textContent = text; + return div.innerHTML; + }, + + refresh: function(socId, systemId) { + var $container = $('.kundenkarte-tree[data-system="' + systemId + '"]'); + if (!$container.length) return; + + $.ajax({ + url: baseUrl + '/custom/kundenkarte/ajax/anlage_tree.php', + data: { socid: socId, system: systemId }, + success: function(html) { + $container.html(html); + } + }); + }, + + expandAll: function() { + $('.kundenkarte-tree-toggle').removeClass('collapsed'); + $('.kundenkarte-tree-children').removeClass('collapsed'); + }, + + collapseAll: function() { + $('.kundenkarte-tree-toggle').addClass('collapsed'); + $('.kundenkarte-tree-children').addClass('collapsed'); + } + }; + + /** + * Favorite Products Component + */ + KundenKarte.Favorites = { + init: function() { + this.bindEvents(); + }, + + bindEvents: function() { + // Select all checkbox + $(document).on('change', '#kundenkarte-select-all', function() { + var checked = $(this).prop('checked'); + $('.kundenkarte-favorites-table input[type="checkbox"][name="selected_products[]"]').prop('checked', checked); + KundenKarte.Favorites.updateGenerateButton(); + }); + + // Individual checkbox + $(document).on('change', '.kundenkarte-favorites-table input[type="checkbox"][name="selected_products[]"]', function() { + KundenKarte.Favorites.updateGenerateButton(); + }); + + // Save button click + $(document).on('click', '.kundenkarte-qty-save', function(e) { + e.preventDefault(); + var $btn = $(this); + var favId = $btn.data('fav-id'); + var $input = $('input.kundenkarte-favorites-qty[data-fav-id="' + favId + '"]'); + var qtyStr = $input.val().replace(',', '.'); + var qty = parseFloat(qtyStr); + + if (!isNaN(qty) && qty > 0) { + // Limit to 2 decimal places + qty = Math.round(qty * 100) / 100; + + // Format nicely + var display = (qty % 1 === 0) ? qty.toString() : qty.toFixed(2).replace(/\.?0+$/, ''); + $input.val(display); + + // Visual feedback + $btn.prop('disabled', true); + $btn.find('i').removeClass('fa-save').addClass('fa-spinner fa-spin'); + + $.ajax({ + url: baseUrl + '/custom/kundenkarte/ajax/favorite_update.php', + method: 'POST', + data: { id: favId, qty: qty, token: $('input[name="token"]').val() }, + success: function() { + $btn.find('i').removeClass('fa-spinner fa-spin').addClass('fa-check').css('color', '#0a0'); + setTimeout(function() { + $btn.find('i').removeClass('fa-check').addClass('fa-save').css('color', ''); + $btn.prop('disabled', false); + }, 1500); + }, + error: function() { + $btn.find('i').removeClass('fa-spinner fa-spin').addClass('fa-exclamation-triangle').css('color', '#c00'); + setTimeout(function() { + $btn.find('i').removeClass('fa-exclamation-triangle').addClass('fa-save').css('color', ''); + $btn.prop('disabled', false); + }, 2000); + } + }); + } + }); + }, + + updateGenerateButton: function() { + var count = $('.kundenkarte-favorites-table input[type="checkbox"][name="selected_products[]"]:checked').length; + var $btn = $('#btn-generate-order'); + + if (count > 0) { + $btn.prop('disabled', false).text($btn.data('text').replace('%d', count)); + } else { + $btn.prop('disabled', true).text($btn.data('text-none')); + } + } + }; + + /** + * System Tabs Component + */ + KundenKarte.SystemTabs = { + init: function() { + this.bindEvents(); + }, + + bindEvents: function() { + $(document).on('click', '.kundenkarte-system-tab:not(.active):not(.kundenkarte-system-tab-add)', function() { + var systemId = $(this).data('system'); + KundenKarte.SystemTabs.switchTo(systemId); + }); + }, + + switchTo: function(systemId) { + $('.kundenkarte-system-tab').removeClass('active'); + $('.kundenkarte-system-tab[data-system="' + systemId + '"]').addClass('active'); + + $('.kundenkarte-system-content').hide(); + $('.kundenkarte-system-content[data-system="' + systemId + '"]').show(); + + // Update URL without reload + var url = new URL(window.location.href); + url.searchParams.set('system', systemId); + window.history.replaceState({}, '', url); + } + }; + + /** + * Icon Picker Component with Custom Icon Upload + */ + KundenKarte.IconPicker = { + // Common FontAwesome icons for installations/technical use + icons: [ + // Electrical + 'fa-bolt', 'fa-plug', 'fa-power-off', 'fa-charging-station', 'fa-battery-full', 'fa-battery-half', + 'fa-car-battery', 'fa-solar-panel', 'fa-sun', 'fa-lightbulb', 'fa-toggle-on', 'fa-toggle-off', + // Network/Internet + 'fa-wifi', 'fa-network-wired', 'fa-server', 'fa-database', 'fa-hdd', 'fa-ethernet', + 'fa-broadcast-tower', 'fa-satellite-dish', 'fa-satellite', 'fa-signal', 'fa-rss', + // TV/Media + 'fa-tv', 'fa-play-circle', 'fa-video', 'fa-film', 'fa-podcast', 'fa-music', + // Temperature/Climate + 'fa-thermometer-half', 'fa-temperature-high', 'fa-temperature-low', 'fa-fire', 'fa-fire-alt', + 'fa-snowflake', 'fa-wind', 'fa-fan', 'fa-air-freshener', + // Building/Structure + 'fa-home', 'fa-building', 'fa-warehouse', 'fa-door-open', 'fa-door-closed', 'fa-archway', + // Devices/Hardware + 'fa-microchip', 'fa-memory', 'fa-sim-card', 'fa-sd-card', 'fa-usb', 'fa-desktop', 'fa-laptop', + 'fa-mobile-alt', 'fa-tablet-alt', 'fa-keyboard', 'fa-print', 'fa-fax', + // Security + 'fa-shield-alt', 'fa-lock', 'fa-unlock', 'fa-key', 'fa-fingerprint', 'fa-eye', 'fa-video', + 'fa-bell', 'fa-exclamation-triangle', 'fa-user-shield', + // Objects + 'fa-cube', 'fa-cubes', 'fa-box', 'fa-boxes', 'fa-archive', 'fa-toolbox', 'fa-tools', 'fa-wrench', + 'fa-cog', 'fa-cogs', 'fa-sliders-h', + // Layout + 'fa-th', 'fa-th-large', 'fa-th-list', 'fa-grip-horizontal', 'fa-grip-vertical', 'fa-bars', + 'fa-stream', 'fa-layer-group', 'fa-project-diagram', 'fa-share-alt', 'fa-sitemap', + // Arrows/Direction + 'fa-exchange-alt', 'fa-arrows-alt', 'fa-expand', 'fa-compress', 'fa-random', + // Misc + 'fa-tachometer-alt', 'fa-chart-line', 'fa-chart-bar', 'fa-chart-pie', 'fa-chart-area', + 'fa-clock', 'fa-calendar', 'fa-tag', 'fa-tags', 'fa-bookmark', 'fa-star', 'fa-heart', + 'fa-check', 'fa-times', 'fa-plus', 'fa-minus', 'fa-info-circle', 'fa-question-circle', + 'fa-dot-circle', 'fa-circle', 'fa-square', 'fa-adjust' + ], + + customIcons: [], + currentInput: null, + currentTab: 'fontawesome', + + init: function() { + this.createModal(); + this.bindEvents(); + }, + + createModal: function() { + if ($('#kundenkarte-icon-picker-modal').length) return; + + var self = this; + var html = '
'; + html += '
'; + html += '
'; + html += '

Icon auswählen

'; + html += '×'; + html += '
'; + html += '
'; + + // Tabs + html += '
'; + html += ''; + html += ''; + html += '
'; + + // Font Awesome Tab Content + html += '
'; + html += ''; + html += '
'; + for (var i = 0; i < this.icons.length; i++) { + html += '
'; + html += ''; + html += '
'; + } + html += '
'; + html += '
'; + + // Custom Icons Tab Content + html += ''; + + html += '
'; + html += '
'; + html += '
'; + + $('body').append(html); + }, + + bindEvents: function() { + var self = this; + + // Open picker + $(document).on('click', '.kundenkarte-icon-picker-btn', function(e) { + e.preventDefault(); + var inputName = $(this).data('input'); + self.currentInput = $('input[name="' + inputName + '"]'); + self.open(); + }); + + // Close modal + $(document).on('click', '.kundenkarte-modal-close, #kundenkarte-icon-picker-modal', function(e) { + if (e.target === this || $(e.target).hasClass('kundenkarte-modal-close')) { + self.close(); + } + }); + + // Tab switching + $(document).on('click', '.kundenkarte-icon-tab', function() { + var tab = $(this).data('tab'); + self.switchTab(tab); + }); + + // Select FA icon + $(document).on('click', '.kundenkarte-icon-item[data-type="fa"]', function() { + var icon = $(this).data('icon'); + self.selectIcon(icon, 'fa'); + }); + + // Select custom icon + $(document).on('click', '.kundenkarte-icon-item[data-type="custom"]', function() { + var iconUrl = $(this).data('icon'); + self.selectIcon(iconUrl, 'custom'); + }); + + // Search filter (FA only) + $(document).on('input', '#kundenkarte-icon-search', function() { + var search = $(this).val().toLowerCase(); + $('#kundenkarte-fa-icons .kundenkarte-icon-item').each(function() { + var icon = $(this).data('icon').toLowerCase(); + $(this).toggle(icon.indexOf(search) > -1); + }); + }); + + // Upload button click + $(document).on('click', '#kundenkarte-icon-upload-btn', function() { + $('#kundenkarte-icon-upload').click(); + }); + + // File selected + $(document).on('change', '#kundenkarte-icon-upload', function() { + var file = this.files[0]; + if (file) { + self.uploadIcon(file); + } + }); + + // Delete custom icon + $(document).on('click', '.kundenkarte-icon-delete', function(e) { + e.stopPropagation(); + var filename = $(this).data('filename'); + self.showDeleteConfirm(filename); + }); + + // ESC key to close + $(document).on('keydown', function(e) { + if (e.key === 'Escape') { + self.close(); + } + }); + }, + + switchTab: function(tab) { + this.currentTab = tab; + $('.kundenkarte-icon-tab').removeClass('active'); + $('.kundenkarte-icon-tab[data-tab="' + tab + '"]').addClass('active'); + $('.kundenkarte-icon-tab-content').hide(); + $('.kundenkarte-icon-tab-content[data-tab="' + tab + '"]').show(); + + if (tab === 'custom') { + this.loadCustomIcons(); + } + }, + + loadCustomIcons: function() { + var self = this; + var $grid = $('#kundenkarte-custom-icons'); + + $.ajax({ + url: baseUrl + '/custom/kundenkarte/ajax/icon_upload.php', + data: { action: 'list' }, + dataType: 'json', + success: function(response) { + $grid.empty(); + + if (response.icons && response.icons.length > 0) { + self.customIcons = response.icons; + for (var i = 0; i < response.icons.length; i++) { + var icon = response.icons[i]; + var html = '
'; + html += '' + icon.name + ''; + html += ''; + html += '
'; + $grid.append(html); + } + } else { + $grid.html('

Noch keine eigenen Icons hochgeladen.

'); + } + }, + error: function() { + $grid.html('

Fehler beim Laden der Icons.

'); + } + }); + }, + + uploadIcon: function(file) { + var self = this; + var formData = new FormData(); + formData.append('action', 'upload'); + formData.append('icon', file); + + $.ajax({ + url: baseUrl + '/custom/kundenkarte/ajax/icon_upload.php', + method: 'POST', + data: formData, + processData: false, + contentType: false, + dataType: 'json', + success: function(response) { + if (response.success) { + self.loadCustomIcons(); + // Auto-select uploaded icon + setTimeout(function() { + self.selectIcon(response.icon.url, 'custom'); + }, 500); + } else { + alert('Fehler: ' + (response.error || 'Unbekannter Fehler')); + } + }, + error: function(xhr) { + var msg = 'Upload fehlgeschlagen'; + try { + var resp = JSON.parse(xhr.responseText); + msg = resp.error || msg; + } catch(e) {} + alert(msg); + } + }); + + // Reset input + $('#kundenkarte-icon-upload').val(''); + }, + + deleteIcon: function(filename) { + var self = this; + + $.ajax({ + url: baseUrl + '/custom/kundenkarte/ajax/icon_upload.php', + method: 'POST', + data: { action: 'delete', filename: filename }, + dataType: 'json', + success: function(response) { + if (response.success) { + self.loadCustomIcons(); + } else { + alert('Fehler: ' + (response.error || 'Löschen fehlgeschlagen')); + } + } + }); + }, + + showDeleteConfirm: function(filename) { + var self = this; + + // Remove any existing confirm dialog + $('#kundenkarte-delete-confirm').remove(); + + // Create Dolibarr-style confirmation dialog + var html = '
'; + html += '
'; + + // Header (Dolibarr style) + html += '
'; + html += 'Bestätigung'; + html += '
'; + + // Body + html += '
'; + html += '

Möchten Sie dieses Icon wirklich löschen?

'; + html += '

' + this.escapeHtml(filename) + '

'; + html += '
'; + + // Footer with buttons (Dolibarr style) + html += ''; + + html += '
'; + html += '
'; + + $('body').append(html); + + // Bind events + $('#kundenkarte-confirm-yes').on('click', function() { + $('#kundenkarte-delete-confirm').remove(); + self.deleteIcon(filename); + }); + + $('#kundenkarte-confirm-no, #kundenkarte-delete-confirm').on('click', function(e) { + if (e.target === this) { + $('#kundenkarte-delete-confirm').remove(); + } + }); + + // ESC to close + $(document).one('keydown.confirmDialog', function(e) { + if (e.key === 'Escape') { + $('#kundenkarte-delete-confirm').remove(); + } + }); + }, + + escapeHtml: function(text) { + if (!text) return ''; + var div = document.createElement('div'); + div.textContent = text; + return div.innerHTML; + }, + + selectIcon: function(icon, type) { + if (this.currentInput) { + // For custom icons, store the URL with a prefix to distinguish + var value = (type === 'custom') ? 'img:' + icon : icon; + this.currentInput.val(value); + + // Update preview + var $wrapper = this.currentInput.closest('.kundenkarte-icon-picker-wrapper'); + var $preview = $wrapper.find('.kundenkarte-icon-preview'); + if ($preview.length) { + if (type === 'custom') { + $preview.html(''); + } else { + $preview.html(''); + } + } + } + this.close(); + }, + + open: function() { + $('#kundenkarte-icon-search').val(''); + $('#kundenkarte-fa-icons .kundenkarte-icon-item').show(); + this.switchTab('fontawesome'); + $('#kundenkarte-icon-picker-modal').addClass('visible'); + }, + + close: function() { + $('#kundenkarte-icon-picker-modal').removeClass('visible'); + this.currentInput = null; + } + }; + + // Initialize on DOM ready + $(document).ready(function() { + KundenKarte.Tree.init(); + KundenKarte.Favorites.init(); + KundenKarte.SystemTabs.init(); + KundenKarte.IconPicker.init(); + }); + +})(); diff --git a/kundenkarteindex.php b/kundenkarteindex.php new file mode 100755 index 0000000..8d935c1 --- /dev/null +++ b/kundenkarteindex.php @@ -0,0 +1,259 @@ + + * Copyright (C) 2004-2015 Laurent Destailleur + * Copyright (C) 2005-2012 Regis Houssin + * Copyright (C) 2015 Jean-François Ferry + * Copyright (C) 2024 Frédéric France + * Copyright (C) 2026 Eduard Wisch + * + * 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 . + */ + +/** + * \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 '
'; + + +/* 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 ''; + print ''; + print ''; + + $var = true; + if ($num > 0) + { + $i = 0; + while ($i < $num) + { + + $obj = $db->fetch_object($resql); + print ''; + print ''; + print ''; + $i++; + $total += $obj->total_ttc; + } + if ($total>0) + { + + print '"; + } + } + else + { + + print ''; + } + print "
'.$langs->trans("DraftMyObjects").($num?''.$num.'':'').'
'; + + $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 ''; + print ''.price($obj->total_ttc).'
'.$langs->trans("Total").''.price($total)."
'.$langs->trans("NoOrder").'

"; + + $db->free($resql); + } + else + { + dol_print_error($db); + } +} +END MODULEBUILDER DRAFT MYOBJECT */ + + +print '
'; + + +/* 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 ''; + print ''; + print ''; + print ''; + print ''; + 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 ''; + print ''; + print '"; + print '"; + print ''; + $i++; + } + + $db->free($resql); + } else { + print ''; + } + print "
'; + print $langs->trans("BoxTitleLatestModifiedMyObjects", $max); + print ''.$langs->trans("DateModificationShort").'
'.$myobjectstatic->getNomUrl(1).''; + print "'.dol_print_date($db->jdate($objp->tms), 'day')."
'.$langs->trans("None").'

"; + } +} +*/ + +print '
'; + +// End of page +llxFooter(); +$db->close(); diff --git a/langs/de_DE/kundenkarte.lang b/langs/de_DE/kundenkarte.lang new file mode 100755 index 0000000..f7c77ec --- /dev/null +++ b/langs/de_DE/kundenkarte.lang @@ -0,0 +1,204 @@ +# 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 +SelectType = Typ auswaehlen +SelectParent = Uebergeordnetes Element +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 + +# 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 + +# 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 diff --git a/langs/en_US/kundenkarte.lang b/langs/en_US/kundenkarte.lang new file mode 100755 index 0000000..a16a624 --- /dev/null +++ b/langs/en_US/kundenkarte.lang @@ -0,0 +1,187 @@ +# 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 +SelectType = Select type +SelectParent = Parent element +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 + +# 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 diff --git a/lib/kundenkarte.lib.php b/lib/kundenkarte.lib.php new file mode 100755 index 0000000..d2661f5 --- /dev/null +++ b/lib/kundenkarte.lib.php @@ -0,0 +1,120 @@ + + * + * 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 . + */ + +/** + * \file kundenkarte/lib/kundenkarte.lib.php + * \ingroup kundenkarte + * \brief Library files with common functions for KundenKarte + */ + +/** + * Prepare admin pages header + * + * @return array + */ +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/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] .= '' . $nbExtrafields . ''; + } + $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] .= '' . $nbExtrafields . ''; + } + $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 ''; + } + + // 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 ''.dol_escape_htmltag($alt).''; + } + + // Font Awesome icon + return ''; +} diff --git a/modulebuilder.txt b/modulebuilder.txt new file mode 100755 index 0000000..670a177 --- /dev/null +++ b/modulebuilder.txt @@ -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. \ No newline at end of file diff --git a/sql/data.sql b/sql/data.sql new file mode 100755 index 0000000..423da99 --- /dev/null +++ b/sql/data.sql @@ -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 +-- +-- ============================================================================ diff --git a/sql/dolibarr_allversions.sql b/sql/dolibarr_allversions.sql new file mode 100755 index 0000000..5026bb4 --- /dev/null +++ b/sql/dolibarr_allversions.sql @@ -0,0 +1,3 @@ +-- +-- Script run when an upgrade of Dolibarr is done. Whatever is the Dolibarr version. +-- diff --git a/sql/llx_c_kundenkarte_anlage_system.key.sql b/sql/llx_c_kundenkarte_anlage_system.key.sql new file mode 100755 index 0000000..02d996e --- /dev/null +++ b/sql/llx_c_kundenkarte_anlage_system.key.sql @@ -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); diff --git a/sql/llx_c_kundenkarte_anlage_system.sql b/sql/llx_c_kundenkarte_anlage_system.sql new file mode 100755 index 0000000..30ba6a6 --- /dev/null +++ b/sql/llx_c_kundenkarte_anlage_system.sql @@ -0,0 +1,21 @@ +-- ============================================================================ +-- 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), + + position integer DEFAULT 0, + active tinyint DEFAULT 1 NOT NULL +) ENGINE=innodb; diff --git a/sql/llx_kundenkarte_anlage.key.sql b/sql/llx_kundenkarte_anlage.key.sql new file mode 100755 index 0000000..4b3360c --- /dev/null +++ b/sql/llx_kundenkarte_anlage.key.sql @@ -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); diff --git a/sql/llx_kundenkarte_anlage.sql b/sql/llx_kundenkarte_anlage.sql new file mode 100755 index 0000000..a66af9f --- /dev/null +++ b/sql/llx_kundenkarte_anlage.sql @@ -0,0 +1,46 @@ +-- ============================================================================ +-- 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, + + 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; diff --git a/sql/llx_kundenkarte_anlage_contact.sql b/sql/llx_kundenkarte_anlage_contact.sql new file mode 100644 index 0000000..04824f1 --- /dev/null +++ b/sql/llx_kundenkarte_anlage_contact.sql @@ -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); diff --git a/sql/llx_kundenkarte_anlage_files.key.sql b/sql/llx_kundenkarte_anlage_files.key.sql new file mode 100755 index 0000000..2f486d4 --- /dev/null +++ b/sql/llx_kundenkarte_anlage_files.key.sql @@ -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; diff --git a/sql/llx_kundenkarte_anlage_files.sql b/sql/llx_kundenkarte_anlage_files.sql new file mode 100755 index 0000000..bcc208a --- /dev/null +++ b/sql/llx_kundenkarte_anlage_files.sql @@ -0,0 +1,35 @@ +-- ============================================================================ +-- 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, + 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; diff --git a/sql/llx_kundenkarte_anlage_type.key.sql b/sql/llx_kundenkarte_anlage_type.key.sql new file mode 100755 index 0000000..7f209e1 --- /dev/null +++ b/sql/llx_kundenkarte_anlage_type.key.sql @@ -0,0 +1,10 @@ +-- ============================================================================ +-- Copyright (C) 2026 Alles Watt lauft +-- ============================================================================ + +ALTER TABLE llx_kundenkarte_anlage_type ADD UNIQUE INDEX uk_kundenkarte_anlage_type_ref (ref, entity); + +ALTER TABLE llx_kundenkarte_anlage_type ADD INDEX idx_kundenkarte_anlage_type_fk_system (fk_system); +ALTER TABLE llx_kundenkarte_anlage_type ADD INDEX idx_kundenkarte_anlage_type_active (active); + +ALTER TABLE llx_kundenkarte_anlage_type ADD CONSTRAINT fk_kundenkarte_anlage_type_fk_system FOREIGN KEY (fk_system) REFERENCES llx_c_kundenkarte_anlage_system (rowid); diff --git a/sql/llx_kundenkarte_anlage_type.sql b/sql/llx_kundenkarte_anlage_type.sql new file mode 100755 index 0000000..04fe468 --- /dev/null +++ b/sql/llx_kundenkarte_anlage_type.sql @@ -0,0 +1,37 @@ +-- ============================================================================ +-- Copyright (C) 2026 Alles Watt lauft +-- +-- Table for installation element type templates (globally reusable) +-- Examples: Zaehlerschrank, Unterverteilung, Wallbox, Router, etc. +-- ============================================================================ + +CREATE TABLE llx_kundenkarte_anlage_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(64), + description text, + + fk_system integer NOT NULL, + + can_have_children tinyint DEFAULT 0 NOT NULL, + can_be_nested tinyint DEFAULT 0 NOT NULL, + allowed_parent_types varchar(255), + + picto varchar(64), + color varchar(8), + + 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; diff --git a/sql/llx_kundenkarte_anlage_type_field.key.sql b/sql/llx_kundenkarte_anlage_type_field.key.sql new file mode 100755 index 0000000..f420d03 --- /dev/null +++ b/sql/llx_kundenkarte_anlage_type_field.key.sql @@ -0,0 +1,10 @@ +-- ============================================================================ +-- Copyright (C) 2026 Alles Watt lauft +-- ============================================================================ + +ALTER TABLE llx_kundenkarte_anlage_type_field ADD UNIQUE INDEX uk_kundenkarte_anlage_type_field (fk_anlage_type, field_code); + +ALTER TABLE llx_kundenkarte_anlage_type_field ADD INDEX idx_kundenkarte_anlage_type_field_fk_type (fk_anlage_type); +ALTER TABLE llx_kundenkarte_anlage_type_field ADD INDEX idx_kundenkarte_anlage_type_field_active (active); + +ALTER TABLE llx_kundenkarte_anlage_type_field ADD CONSTRAINT fk_kundenkarte_anlage_type_field_fk_type FOREIGN KEY (fk_anlage_type) REFERENCES llx_kundenkarte_anlage_type (rowid) ON DELETE CASCADE; diff --git a/sql/llx_kundenkarte_anlage_type_field.sql b/sql/llx_kundenkarte_anlage_type_field.sql new file mode 100755 index 0000000..506194c --- /dev/null +++ b/sql/llx_kundenkarte_anlage_type_field.sql @@ -0,0 +1,33 @@ +-- ============================================================================ +-- Copyright (C) 2026 Alles Watt lauft +-- +-- Table for defining fields per element type (EAV pattern for flexibility) +-- ============================================================================ + +CREATE TABLE llx_kundenkarte_anlage_type_field +( + rowid integer AUTO_INCREMENT PRIMARY KEY, + entity integer DEFAULT 0 NOT NULL, + + fk_anlage_type integer NOT NULL, + + field_code varchar(64) NOT NULL, + field_label varchar(255) NOT NULL, + field_type varchar(32) NOT NULL, + field_options text, + + field_size integer DEFAULT 255, + field_default varchar(255), + + required tinyint DEFAULT 0 NOT NULL, + show_in_tree tinyint DEFAULT 0 NOT NULL, + show_in_hover tinyint DEFAULT 1 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 +) ENGINE=innodb; diff --git a/sql/llx_kundenkarte_favorite_products.key.sql b/sql/llx_kundenkarte_favorite_products.key.sql new file mode 100755 index 0000000..654b965 --- /dev/null +++ b/sql/llx_kundenkarte_favorite_products.key.sql @@ -0,0 +1,12 @@ +-- ============================================================================ +-- Copyright (C) 2026 Alles Watt lauft +-- ============================================================================ + +ALTER TABLE llx_kundenkarte_favorite_products ADD UNIQUE INDEX uk_kundenkarte_favprod_soc_product (fk_soc, fk_product, entity); + +ALTER TABLE llx_kundenkarte_favorite_products ADD INDEX idx_kundenkarte_favprod_fk_soc (fk_soc); +ALTER TABLE llx_kundenkarte_favorite_products ADD INDEX idx_kundenkarte_favprod_fk_product (fk_product); +ALTER TABLE llx_kundenkarte_favorite_products ADD INDEX idx_kundenkarte_favprod_active (active); + +ALTER TABLE llx_kundenkarte_favorite_products ADD CONSTRAINT fk_kundenkarte_favprod_fk_soc FOREIGN KEY (fk_soc) REFERENCES llx_societe (rowid); +ALTER TABLE llx_kundenkarte_favorite_products ADD CONSTRAINT fk_kundenkarte_favprod_fk_product FOREIGN KEY (fk_product) REFERENCES llx_product (rowid); diff --git a/sql/llx_kundenkarte_favorite_products.sql b/sql/llx_kundenkarte_favorite_products.sql new file mode 100755 index 0000000..36ad3c5 --- /dev/null +++ b/sql/llx_kundenkarte_favorite_products.sql @@ -0,0 +1,32 @@ +-- ============================================================================ +-- 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. +-- +-- Table for storing customer favorite products +-- ============================================================================ + +CREATE TABLE llx_kundenkarte_favorite_products +( + rowid integer AUTO_INCREMENT PRIMARY KEY, + entity integer DEFAULT 1 NOT NULL, + + fk_soc integer NOT NULL, + fk_product integer NOT NULL, + + qty double(24,8) DEFAULT 1 NOT NULL, + + rang integer DEFAULT 0, + note text, + + 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; diff --git a/sql/llx_kundenkarte_favorite_products_contact.sql b/sql/llx_kundenkarte_favorite_products_contact.sql new file mode 100644 index 0000000..0fd49f4 --- /dev/null +++ b/sql/llx_kundenkarte_favorite_products_contact.sql @@ -0,0 +1,8 @@ +-- ============================================================================ +-- Copyright (C) 2026 Alles Watt lauft +-- +-- Add fk_contact column for contact/address specific favorites +-- ============================================================================ + +ALTER TABLE llx_kundenkarte_favorite_products ADD COLUMN fk_contact integer DEFAULT NULL AFTER fk_soc; +ALTER TABLE llx_kundenkarte_favorite_products ADD INDEX idx_kundenkarte_favorite_products_fk_contact (fk_contact); diff --git a/sql/llx_kundenkarte_societe_system.key.sql b/sql/llx_kundenkarte_societe_system.key.sql new file mode 100755 index 0000000..40e3104 --- /dev/null +++ b/sql/llx_kundenkarte_societe_system.key.sql @@ -0,0 +1,12 @@ +-- ============================================================================ +-- Copyright (C) 2026 Alles Watt lauft +-- +-- Keys for llx_kundenkarte_societe_system +-- ============================================================================ + +ALTER TABLE llx_kundenkarte_societe_system ADD INDEX idx_kundenkarte_societe_system_fk_soc (fk_soc); +ALTER TABLE llx_kundenkarte_societe_system ADD INDEX idx_kundenkarte_societe_system_fk_system (fk_system); +ALTER TABLE llx_kundenkarte_societe_system ADD UNIQUE INDEX uk_kundenkarte_societe_system (fk_soc, fk_system); + +ALTER TABLE llx_kundenkarte_societe_system ADD CONSTRAINT fk_kundenkarte_societe_system_soc FOREIGN KEY (fk_soc) REFERENCES llx_societe(rowid); +ALTER TABLE llx_kundenkarte_societe_system ADD CONSTRAINT fk_kundenkarte_societe_system_system FOREIGN KEY (fk_system) REFERENCES llx_c_kundenkarte_anlage_system(rowid); diff --git a/sql/llx_kundenkarte_societe_system.sql b/sql/llx_kundenkarte_societe_system.sql new file mode 100755 index 0000000..aea1aa3 --- /dev/null +++ b/sql/llx_kundenkarte_societe_system.sql @@ -0,0 +1,18 @@ +-- ============================================================================ +-- Copyright (C) 2026 Alles Watt lauft +-- +-- Links customers (societe) to installation systems +-- Defines which systems are present at a specific customer location +-- ============================================================================ + +CREATE TABLE llx_kundenkarte_societe_system ( + rowid integer AUTO_INCREMENT PRIMARY KEY, + entity integer DEFAULT 1 NOT NULL, + fk_soc integer NOT NULL, -- Customer (llx_societe) + fk_system integer NOT NULL, -- System category (llx_c_kundenkarte_anlage_system) + note text, -- Optional notes + date_creation datetime NOT NULL, + tms timestamp DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + fk_user_creat integer, + active tinyint DEFAULT 1 NOT NULL +) ENGINE=innodb; diff --git a/sql/llx_kundenkarte_societe_system_contact.sql b/sql/llx_kundenkarte_societe_system_contact.sql new file mode 100644 index 0000000..9690403 --- /dev/null +++ b/sql/llx_kundenkarte_societe_system_contact.sql @@ -0,0 +1,8 @@ +-- ============================================================================ +-- Copyright (C) 2026 Alles Watt lauft +-- +-- Add fk_contact column for contact/address specific system assignments +-- ============================================================================ + +ALTER TABLE llx_kundenkarte_societe_system ADD COLUMN fk_contact integer DEFAULT NULL AFTER fk_soc; +ALTER TABLE llx_kundenkarte_societe_system ADD INDEX idx_kundenkarte_societe_system_fk_contact (fk_contact); diff --git a/tabs/anlagen.php b/tabs/anlagen.php new file mode 100755 index 0000000..1298d00 --- /dev/null +++ b/tabs/anlagen.php @@ -0,0 +1,802 @@ +loadLangs(array('companies', 'kundenkarte@kundenkarte')); + +// Get parameters +$id = GETPOSTINT('id'); +$action = GETPOST('action', 'aZ09'); +$confirm = GETPOST('confirm', 'alpha'); +$systemId = GETPOSTINT('system'); +$anlageId = GETPOSTINT('anlage_id'); +$parentId = GETPOSTINT('parent_id'); + +// Security check +if (!$user->hasRight('kundenkarte', 'read')) { + accessforbidden(); +} + +// Initialize objects +$object = new Societe($db); +$form = new Form($db); +$anlage = new Anlage($db); +$anlageType = new AnlageType($db); + +// Load thirdparty +if ($id > 0) { + $result = $object->fetch($id); + if ($result <= 0) { + dol_print_error($db, $object->error); + exit; + } +} + +$permissiontoread = $user->hasRight('kundenkarte', 'read'); +$permissiontoadd = $user->hasRight('kundenkarte', 'write'); +$permissiontodelete = $user->hasRight('kundenkarte', 'delete'); + +// Load ALL available systems (from dictionary) +$allSystems = array(); +$sql = "SELECT rowid, code, label, picto, color FROM ".MAIN_DB_PREFIX."c_kundenkarte_anlage_system WHERE active = 1 ORDER BY position ASC"; +$resql = $db->query($sql); +if ($resql) { + while ($obj = $db->fetch_object($resql)) { + $allSystems[$obj->rowid] = $obj; + } +} + +// Load systems ENABLED for this customer (only thirdparty-level, not contact-specific) +$customerSystems = array(); +$sql = "SELECT ss.rowid, ss.fk_system, s.code, s.label, s.picto, s.color + FROM ".MAIN_DB_PREFIX."kundenkarte_societe_system ss + JOIN ".MAIN_DB_PREFIX."c_kundenkarte_anlage_system s ON s.rowid = ss.fk_system + WHERE ss.fk_soc = ".((int) $id)." AND (ss.fk_contact IS NULL OR ss.fk_contact = 0) AND ss.active = 1 AND s.active = 1 + ORDER BY s.position ASC"; +$resql = $db->query($sql); +if ($resql) { + while ($obj = $db->fetch_object($resql)) { + $customerSystems[$obj->fk_system] = $obj; + } +} + +// Default to first enabled system if not specified +if (empty($systemId) && !empty($customerSystems)) { + $systemId = array_key_first($customerSystems); +} + +/* + * Actions + */ + +// Add system to customer +if ($action == 'add_system' && $permissiontoadd) { + $newSystemId = GETPOSTINT('new_system_id'); + if ($newSystemId > 0 && !isset($customerSystems[$newSystemId])) { + $sql = "INSERT INTO ".MAIN_DB_PREFIX."kundenkarte_societe_system"; + $sql .= " (entity, fk_soc, fk_system, date_creation, fk_user_creat, active)"; + $sql .= " VALUES (".$conf->entity.", ".((int) $id).", ".((int) $newSystemId).", NOW(), ".((int) $user->id).", 1)"; + $result = $db->query($sql); + if ($result) { + setEventMessages($langs->trans('SystemAdded'), null, 'mesgs'); + header('Location: '.$_SERVER['PHP_SELF'].'?id='.$id.'&system='.$newSystemId); + exit; + } else { + setEventMessages($db->lasterror(), null, 'errors'); + } + } + $action = ''; +} + +// Remove system from customer +if ($action == 'confirm_remove_system' && $confirm == 'yes' && $permissiontodelete) { + $removeSystemId = GETPOSTINT('remove_system_id'); + if ($removeSystemId > 0) { + // Check if system has any elements (only thirdparty-level, not contact-specific) + $sql = "SELECT COUNT(*) as cnt FROM ".MAIN_DB_PREFIX."kundenkarte_anlage WHERE fk_soc = ".((int) $id)." AND (fk_contact IS NULL OR fk_contact = 0) AND fk_system = ".((int) $removeSystemId); + $resql = $db->query($sql); + $obj = $db->fetch_object($resql); + + if ($obj->cnt > 0) { + setEventMessages($langs->trans('ErrorSystemHasElements'), null, 'errors'); + } else { + $sql = "DELETE FROM ".MAIN_DB_PREFIX."kundenkarte_societe_system WHERE fk_soc = ".((int) $id)." AND (fk_contact IS NULL OR fk_contact = 0) AND fk_system = ".((int) $removeSystemId); + $db->query($sql); + setEventMessages($langs->trans('SystemRemoved'), null, 'mesgs'); + + // Switch to another system or none + unset($customerSystems[$removeSystemId]); + if (!empty($customerSystems)) { + $systemId = array_key_first($customerSystems); + } else { + $systemId = 0; + } + } + } + header('Location: '.$_SERVER['PHP_SELF'].'?id='.$id.($systemId ? '&system='.$systemId : '')); + exit; +} + +if ($action == 'add' && $permissiontoadd) { + $anlage->label = GETPOST('label', 'alphanohtml'); + $anlage->fk_soc = $id; + $anlage->fk_anlage_type = GETPOSTINT('fk_anlage_type'); + $anlage->fk_parent = GETPOSTINT('fk_parent'); + $anlage->fk_system = $systemId; + $anlage->manufacturer = GETPOST('manufacturer', 'alphanohtml'); + $anlage->model = GETPOST('model', 'alphanohtml'); + $anlage->serial_number = GETPOST('serial_number', 'alphanohtml'); + $anlage->power_rating = GETPOST('power_rating', 'alphanohtml'); + $anlage->location = GETPOST('location', 'alphanohtml'); + $anlage->installation_date = dol_mktime(0, 0, 0, GETPOSTINT('installation_datemonth'), GETPOSTINT('installation_dateday'), GETPOSTINT('installation_dateyear')); + $anlage->note_private = GETPOST('note_private', 'restricthtml'); + $anlage->status = 1; + + // Get type for system ID + $type = new AnlageType($db); + if ($type->fetch($anlage->fk_anlage_type) > 0) { + $anlage->fk_system = $type->fk_system; + } + + // Dynamic fields + $fieldValues = array(); + $fields = $type->fetchFields(); + foreach ($fields as $field) { + $value = GETPOST('field_'.$field->field_code, 'alphanohtml'); + if ($value !== '') { + $fieldValues[$field->field_code] = $value; + } + } + $anlage->setFieldValues($fieldValues); + + $result = $anlage->create($user); + if ($result > 0) { + setEventMessages($langs->trans('RecordSaved'), null, 'mesgs'); + header('Location: '.$_SERVER['PHP_SELF'].'?id='.$id.'&system='.$anlage->fk_system); + exit; + } else { + setEventMessages($anlage->error, $anlage->errors, 'errors'); + $action = 'create'; + } +} + +if ($action == 'update' && $permissiontoadd) { + $anlage->fetch($anlageId); + $anlage->label = GETPOST('label', 'alphanohtml'); + $anlage->fk_anlage_type = GETPOSTINT('fk_anlage_type'); + $anlage->fk_parent = GETPOSTINT('fk_parent'); + $anlage->manufacturer = GETPOST('manufacturer', 'alphanohtml'); + $anlage->model = GETPOST('model', 'alphanohtml'); + $anlage->serial_number = GETPOST('serial_number', 'alphanohtml'); + $anlage->power_rating = GETPOST('power_rating', 'alphanohtml'); + $anlage->location = GETPOST('location', 'alphanohtml'); + $anlage->installation_date = dol_mktime(0, 0, 0, GETPOSTINT('installation_datemonth'), GETPOSTINT('installation_dateday'), GETPOSTINT('installation_dateyear')); + $anlage->note_private = GETPOST('note_private', 'restricthtml'); + + // Get type for system ID + $type = new AnlageType($db); + if ($type->fetch($anlage->fk_anlage_type) > 0) { + $anlage->fk_system = $type->fk_system; + } + + // Dynamic fields + $fieldValues = array(); + $fields = $type->fetchFields(); + foreach ($fields as $field) { + $value = GETPOST('field_'.$field->field_code, 'alphanohtml'); + if ($value !== '') { + $fieldValues[$field->field_code] = $value; + } + } + $anlage->setFieldValues($fieldValues); + + $result = $anlage->update($user); + if ($result > 0) { + setEventMessages($langs->trans('RecordSaved'), null, 'mesgs'); + header('Location: '.$_SERVER['PHP_SELF'].'?id='.$id.'&system='.$anlage->fk_system); + exit; + } else { + setEventMessages($anlage->error, $anlage->errors, 'errors'); + $action = 'edit'; + } +} + +if ($action == 'confirm_delete' && $confirm == 'yes' && $permissiontodelete) { + $anlage->fetch($anlageId); + $systemIdBefore = $anlage->fk_system; + $result = $anlage->delete($user); + if ($result > 0) { + setEventMessages($langs->trans('RecordDeleted'), null, 'mesgs'); + } else { + setEventMessages($anlage->error, $anlage->errors, 'errors'); + } + header('Location: '.$_SERVER['PHP_SELF'].'?id='.$id.'&system='.$systemIdBefore); + exit; +} + +// File upload +if ($action == 'uploadfile' && $permissiontoadd) { + $anlage->fetch($anlageId); + $upload_dir = $anlage->getFileDirectory(); + + // Create directory if not exists + if (!is_dir($upload_dir)) { + dol_mkdir($upload_dir); + } + + if (!empty($_FILES['userfile']['name'])) { + $result = dol_add_file_process($upload_dir, 0, 1, 'userfile', '', null, '', 1); + if ($result > 0) { + // Add to database + $anlagefile = new AnlageFile($db); + $anlagefile->fk_anlage = $anlageId; + $anlagefile->filename = dol_sanitizeFileName($_FILES['userfile']['name']); + // IMPORTANT: Store ONLY relative path (anlagen/socid/anlageid/filename) - never full path! + $anlagefile->filepath = 'anlagen/'.$anlage->fk_soc.'/'.$anlage->id.'/'.$anlagefile->filename; + $anlagefile->filesize = $_FILES['userfile']['size']; + $anlagefile->mimetype = dol_mimetype($anlagefile->filename); + $anlagefile->create($user); + + // Generate thumbnail + $anlagefile->generateThumbnail(); + + setEventMessages($langs->trans('FileUploaded'), null, 'mesgs'); + } + } + header('Location: '.$_SERVER['PHP_SELF'].'?id='.$id.'&system='.$systemId.'&action=view&anlage_id='.$anlageId); + exit; +} + +// File delete (after confirmation) +if ($action == 'confirm_deletefile' && $confirm == 'yes' && $permissiontodelete) { + $fileId = GETPOSTINT('fileid'); + if ($fileId > 0) { + $anlagefile = new AnlageFile($db); + if ($anlagefile->fetch($fileId) > 0) { + // Delete method handles physical file and database + if ($anlagefile->delete($user) > 0) { + setEventMessages($langs->trans('FileDeleted'), null, 'mesgs'); + } else { + setEventMessages($langs->trans('Error'), null, 'errors'); + } + } + } + header('Location: '.$_SERVER['PHP_SELF'].'?id='.$id.'&system='.$systemId.'&action=view&anlage_id='.$anlageId); + exit; +} + +/* + * View + */ + +$title = $langs->trans('TechnicalInstallations').' - '.$object->name; +llxHeader('', $title, '', '', 0, 0, array('/kundenkarte/js/kundenkarte.js'), array('/kundenkarte/css/kundenkarte.css')); + +// Prepare tabs +$head = societe_prepare_head($object); + +print dol_get_fiche_head($head, 'anlagen', $langs->trans("ThirdParty"), -1, 'company'); + +// Thirdparty card +$linkback = ''.$langs->trans("BackToList").''; +dol_banner_tab($object, 'socid', $linkback, ($user->socid ? 0 : 1), 'rowid', 'nom'); + +print '
'; + +// Confirmation dialogs +if ($action == 'delete') { + print $form->formconfirm( + $_SERVER['PHP_SELF'].'?id='.$id.'&anlage_id='.$anlageId.'&system='.$systemId, + $langs->trans('DeleteElement'), + $langs->trans('ConfirmDeleteElement'), + 'confirm_delete', + '', + 'yes', + 1 + ); +} + +if ($action == 'remove_system') { + $removeSystemId = GETPOSTINT('remove_system_id'); + $sysLabel = isset($customerSystems[$removeSystemId]) ? $customerSystems[$removeSystemId]->label : ''; + print $form->formconfirm( + $_SERVER['PHP_SELF'].'?id='.$id.'&remove_system_id='.$removeSystemId, + $langs->trans('RemoveSystem'), + $langs->trans('ConfirmRemoveSystem', $sysLabel), + 'confirm_remove_system', + '', + 'yes', + 1 + ); +} + +if ($action == 'askdeletefile') { + $fileId = GETPOSTINT('fileid'); + print $form->formconfirm( + $_SERVER['PHP_SELF'].'?id='.$id.'&system='.$systemId.'&anlage_id='.$anlageId.'&fileid='.$fileId, + $langs->trans('Delete'), + $langs->trans('ConfirmDeleteFile'), + 'confirm_deletefile', + '', + 'yes', + 1 + ); +} + +// System tabs (only show enabled systems for this customer) +print '
'; +print '
'; +foreach ($customerSystems as $sysId => $sys) { + $activeClass = ($sysId == $systemId) ? ' active' : ''; + print '
'; + print ''; + if ($sys->picto) { + print ''.kundenkarte_render_icon($sys->picto).''; + } + print ''.dol_escape_htmltag($sys->label).''; + print ''; + // Remove button (only if no elements) + if ($permissiontodelete && $sysId == $systemId) { + print ' '; + } + print '
'; +} + +// Add system button (always on the right) +if ($permissiontoadd) { + // Get systems not yet enabled for this customer + $availableSystems = array_diff_key($allSystems, $customerSystems); + if (!empty($availableSystems)) { + print ''; + } +} +print '
'; + +// Expand/Collapse buttons (only in tree view, not in create/edit/view) +$isTreeView = !in_array($action, array('create', 'edit', 'view')); +if ($isTreeView) { + print '
'; + print ''; + print ''; + print '
'; +} + +print '
'; // End kundenkarte-system-tabs-wrapper + +// Add system form (hidden by default) +if ($permissiontoadd && !empty($availableSystems)) { + print ''; +} + +// Check if customer has any systems +if (empty($customerSystems)) { + print '
'; + print '
'; + print $langs->trans('NoSystemsConfigured').'

'; + if ($permissiontoadd && !empty($allSystems)) { + print $langs->trans('ClickAddSystemToStart'); + } else { + print $langs->trans('ContactAdminToAddSystems'); + } + print '
'; +} elseif ($systemId > 0) { + // Show form or tree for selected system + if (in_array($action, array('create', 'edit', 'view'))) { + // Load element for edit/view + if ($action != 'create' && $anlageId > 0) { + $anlage->fetch($anlageId); + $type = new AnlageType($db); + $type->fetch($anlage->fk_anlage_type); + $type->fetchFields(); + } + + // Load types for select + $types = $anlageType->fetchAllBySystem($systemId); + + print '
'; + + if ($action == 'view') { + // View mode + print '

'.dol_escape_htmltag($anlage->label).'

'; + + print ''; + + print ''; + print ''; + + if ($anlage->manufacturer) { + print ''; + print ''; + } + if ($anlage->model) { + print ''; + print ''; + } + if ($anlage->serial_number) { + print ''; + print ''; + } + if ($anlage->power_rating) { + print ''; + print ''; + } + if ($anlage->location) { + print ''; + print ''; + } + if ($anlage->installation_date) { + print ''; + print ''; + } + + // Dynamic fields + $fieldValues = $anlage->getFieldValues(); + foreach ($type->fields as $field) { + if (isset($fieldValues[$field->field_code]) && $fieldValues[$field->field_code] !== '') { + print ''; + $value = $fieldValues[$field->field_code]; + // For select fields, show the label + if ($field->field_type == 'select' && $field->field_options) { + $options = json_decode($field->field_options, true); + if (isset($options['options'][$value])) { + $value = $options['options'][$value]; + } + } + print ''; + } + } + + if ($anlage->note_private) { + print ''; + print ''; + } + + print '
'.$langs->trans('Type').''.dol_escape_htmltag($anlage->type_label).'
'.$langs->trans('FieldManufacturer').''.dol_escape_htmltag($anlage->manufacturer).'
'.$langs->trans('FieldModel').''.dol_escape_htmltag($anlage->model).'
'.$langs->trans('FieldSerialNumber').''.dol_escape_htmltag($anlage->serial_number).'
'.$langs->trans('FieldPowerRating').''.dol_escape_htmltag($anlage->power_rating).'
'.$langs->trans('FieldLocation').''.dol_escape_htmltag($anlage->location).'
'.$langs->trans('FieldInstallationDate').''.dol_print_date($anlage->installation_date, 'day').'
'.dol_escape_htmltag($field->field_label).''.dol_escape_htmltag($value).'
'.$langs->trans('FieldNotes').''.dol_htmlentitiesbr($anlage->note_private).'
'; + + // Files section + $anlagefile = new AnlageFile($db); + $files = $anlagefile->fetchAllByAnlage($anlageId); + + print '

'.$langs->trans('AttachedFiles').'

'; + + if ($permissiontoadd) { + print '
'; + print ''; + print ''; + print ' '; + print '

'; + } + + if (!empty($files)) { + print '
'; + foreach ($files as $file) { + print '
'; + print '
'; + if ($file->file_type == 'image') { + $thumbUrl = $file->getThumbUrl(); + if ($thumbUrl) { + print ''; + } else { + print ''; + } + } elseif ($file->file_type == 'pdf') { + // PDF preview using iframe - 50% smaller, no toolbar + print '
'; + print ''; + print '
'; + } else { + print ''; + } + print '
'; + print '
'; + print '
'.dol_escape_htmltag(dol_trunc($file->filename, 20)).'
'; + print '
'.dol_print_size($file->filesize).'
'; + print '
'; + print ''; + if ($permissiontodelete) { + print ''; + } + print '
'; + print '
'; + print '
'; + } + print '
'; + } else { + print '

'.$langs->trans('NoFiles').'

'; + } + + // Action buttons + print '
'; + if ($permissiontoadd) { + print ''.$langs->trans('Modify').''; + } + if ($permissiontodelete) { + print ''.$langs->trans('Delete').''; + } + print ''.$langs->trans('Back').''; + print '
'; + + } else { + // Create/Edit form + $formAction = ($action == 'create') ? 'add' : 'update'; + print '
'; + print ''; + print ''; + if ($action == 'edit') { + print ''; + } + + print ''; + + // Label + print ''; + print ''; + + // Type + print ''; + print ''; + + // Parent + $tree = $anlage->fetchTree($id, $systemId); + print ''; + print ''; + + // Manufacturer + print ''; + print ''; + + // Model + print ''; + print ''; + + // Serial number + print ''; + print ''; + + // Power rating + print ''; + print ''; + + // Location + print ''; + print ''; + + // Installation date + print ''; + print ''; + + // Notes + print ''; + print ''; + + print '
'.$langs->trans('Label').'
'.$langs->trans('Type').''; + if (empty($types)) { + print '
'.$langs->trans('NoTypesDefinedForSystem').''; + } + print '
'.$langs->trans('SelectParent').'
'.$langs->trans('FieldManufacturer').'
'.$langs->trans('FieldModel').'
'.$langs->trans('FieldSerialNumber').'
'.$langs->trans('FieldPowerRating').'
'.$langs->trans('FieldLocation').'
'.$langs->trans('FieldInstallationDate').''.$form->selectDate($action == 'edit' ? $anlage->installation_date : '', 'installation_date', 0, 0, 1, '', 1, 1).'
'.$langs->trans('FieldNotes').'
'; + + // Dynamic fields will be loaded via JavaScript based on type selection + print '
'; + + print '
'; + print ''; + print ' '.$langs->trans('Cancel').''; + print '
'; + + print '
'; + } + + print '
'; + + } else { + // Tree view + if ($permissiontoadd) { + print ''; + } + + // Load tree + $tree = $anlage->fetchTree($id, $systemId); + + if (!empty($tree)) { + print '
'; + printTree($tree, $id, $systemId, $permissiontoadd, $permissiontodelete, $langs); + print '
'; + } else { + print '
'.$langs->trans('NoInstallations').'
'; + } + } +} + +print '
'; + +print dol_get_fiche_end(); + +// Tooltip container +print '
'; + +llxFooter(); +$db->close(); + +/** + * Print tree recursively + */ +function printTree($nodes, $socid, $systemId, $canEdit, $canDelete, $langs, $level = 0) +{ + foreach ($nodes as $node) { + $hasChildren = !empty($node->children); + + // Build tooltip data for hover on icon + $tooltipData = array( + 'label' => $node->label, + 'type' => $node->type_label, + 'location' => $node->location, + 'manufacturer' => $node->manufacturer, + 'model' => $node->model, + 'serial_number' => $node->serial_number, + 'power_rating' => $node->power_rating, + 'installation_date' => $node->installation_date ? dol_print_date($node->installation_date, 'day') : '', + 'note' => $node->note_private, + ); + + print '
'; + print '
'; + + // Toggle + if ($hasChildren) { + print ''; + } else { + print ''; + } + + // Icon with tooltip data + $picto = $node->type_picto ? $node->type_picto : 'fa-cube'; + print ''.kundenkarte_render_icon($picto).''; + + // Label with manufacturer/power in parentheses + file indicators + $viewUrl = $_SERVER['PHP_SELF'].'?id='.$socid.'&system='.$systemId.'&action=view&anlage_id='.$node->id; + print ''.dol_escape_htmltag($node->label); + $labelInfo = array(); + if ($node->manufacturer) { + $labelInfo[] = $node->manufacturer; + } + if ($node->power_rating) { + $labelInfo[] = $node->power_rating; + } + if (!empty($labelInfo)) { + print ' ('.dol_escape_htmltag(implode(', ', $labelInfo)).')'; + } + // File indicators - directly after parentheses + if ($node->image_count > 0 || $node->doc_count > 0) { + print ' '; + if ($node->image_count > 0) { + print ''; + print ''; + if ($node->image_count > 1) { + print ' '.$node->image_count; + } + print ''; + } + if ($node->doc_count > 0) { + print ''; + print ''; + if ($node->doc_count > 1) { + print ' '.$node->doc_count; + } + print ''; + } + print ''; + } + print ''; + + // Type badge + if ($node->type_short || $node->type_label) { + $typeDisplay = $node->type_short ? $node->type_short : $node->type_label; + print ''.dol_escape_htmltag($typeDisplay).''; + } + + // Location with icon + if ($node->location) { + print ' '.dol_escape_htmltag($node->location).''; + } + + // Actions + print ''; + print ''; + if ($canEdit) { + print ''; + print ''; + } + if ($canDelete) { + print ''; + } + print ''; + + print '
'; + + // Children + if ($hasChildren) { + print '
'; + printTree($node->children, $socid, $systemId, $canEdit, $canDelete, $langs, $level + 1); + print '
'; + } + + print '
'; + } +} + +/** + * Print tree options for select + */ +function printTreeOptions($nodes, $selected = 0, $excludeId = 0, $prefix = '') +{ + foreach ($nodes as $node) { + if ($node->id == $excludeId) continue; + + $sel = ($node->id == $selected) ? ' selected' : ''; + print ''; + + if (!empty($node->children)) { + printTreeOptions($node->children, $selected, $excludeId, $prefix.'   '); + } + } +} diff --git a/tabs/contact_anlagen.php b/tabs/contact_anlagen.php new file mode 100644 index 0000000..4cddb91 --- /dev/null +++ b/tabs/contact_anlagen.php @@ -0,0 +1,803 @@ +loadLangs(array('companies', 'kundenkarte@kundenkarte')); + +// Get parameters +$id = GETPOSTINT('id'); +$action = GETPOST('action', 'aZ09'); +$confirm = GETPOST('confirm', 'alpha'); +$systemId = GETPOSTINT('system'); +$anlageId = GETPOSTINT('anlage_id'); +$parentId = GETPOSTINT('parent_id'); + +// Security check +if (!$user->hasRight('kundenkarte', 'read')) { + accessforbidden(); +} + +// Initialize objects +$object = new Contact($db); +$form = new Form($db); +$anlage = new Anlage($db); +$anlageType = new AnlageType($db); + +// Load contact +if ($id > 0) { + $result = $object->fetch($id); + if ($result <= 0) { + dol_print_error($db, $object->error); + exit; + } +} + +$permissiontoread = $user->hasRight('kundenkarte', 'read'); +$permissiontoadd = $user->hasRight('kundenkarte', 'write'); +$permissiontodelete = $user->hasRight('kundenkarte', 'delete'); + +// Load ALL available systems (from dictionary) +$allSystems = array(); +$sql = "SELECT rowid, code, label, picto, color FROM ".MAIN_DB_PREFIX."c_kundenkarte_anlage_system WHERE active = 1 ORDER BY position ASC"; +$resql = $db->query($sql); +if ($resql) { + while ($obj = $db->fetch_object($resql)) { + $allSystems[$obj->rowid] = $obj; + } +} + +// Load systems ENABLED for this contact +$customerSystems = array(); +$sql = "SELECT ss.rowid, ss.fk_system, s.code, s.label, s.picto, s.color + FROM ".MAIN_DB_PREFIX."kundenkarte_societe_system ss + JOIN ".MAIN_DB_PREFIX."c_kundenkarte_anlage_system s ON s.rowid = ss.fk_system + WHERE ss.fk_soc = ".((int) $object->socid)." AND ss.fk_contact = ".((int) $id)." AND ss.active = 1 AND s.active = 1 + ORDER BY s.position ASC"; +$resql = $db->query($sql); +if ($resql) { + while ($obj = $db->fetch_object($resql)) { + $customerSystems[$obj->fk_system] = $obj; + } +} + +// Default to first enabled system if not specified +if (empty($systemId) && !empty($customerSystems)) { + $systemId = array_key_first($customerSystems); +} + +/* + * Actions + */ + +// Add system to contact +if ($action == 'add_system' && $permissiontoadd) { + $newSystemId = GETPOSTINT('new_system_id'); + if ($newSystemId > 0 && !isset($customerSystems[$newSystemId])) { + $sql = "INSERT INTO ".MAIN_DB_PREFIX."kundenkarte_societe_system"; + $sql .= " (entity, fk_soc, fk_contact, fk_system, date_creation, fk_user_creat, active)"; + $sql .= " VALUES (".$conf->entity.", ".((int) $object->socid).", ".((int) $id).", ".((int) $newSystemId).", NOW(), ".((int) $user->id).", 1)"; + $result = $db->query($sql); + if ($result) { + setEventMessages($langs->trans('SystemAdded'), null, 'mesgs'); + header('Location: '.$_SERVER['PHP_SELF'].'?id='.$id.'&system='.$newSystemId); + exit; + } else { + setEventMessages($db->lasterror(), null, 'errors'); + } + } + $action = ''; +} + +// Remove system from contact +if ($action == 'confirm_remove_system' && $confirm == 'yes' && $permissiontodelete) { + $removeSystemId = GETPOSTINT('remove_system_id'); + if ($removeSystemId > 0) { + // Check if system has any elements for this contact + $sql = "SELECT COUNT(*) as cnt FROM ".MAIN_DB_PREFIX."kundenkarte_anlage WHERE fk_soc = ".((int) $object->socid)." AND fk_contact = ".((int) $id)." AND fk_system = ".((int) $removeSystemId); + $resql = $db->query($sql); + $obj = $db->fetch_object($resql); + + if ($obj->cnt > 0) { + setEventMessages($langs->trans('ErrorSystemHasElements'), null, 'errors'); + } else { + $sql = "DELETE FROM ".MAIN_DB_PREFIX."kundenkarte_societe_system WHERE fk_soc = ".((int) $object->socid)." AND fk_contact = ".((int) $id)." AND fk_system = ".((int) $removeSystemId); + $db->query($sql); + setEventMessages($langs->trans('SystemRemoved'), null, 'mesgs'); + + // Switch to another system or none + unset($customerSystems[$removeSystemId]); + if (!empty($customerSystems)) { + $systemId = array_key_first($customerSystems); + } else { + $systemId = 0; + } + } + } + header('Location: '.$_SERVER['PHP_SELF'].'?id='.$id.($systemId ? '&system='.$systemId : '')); + exit; +} + +if ($action == 'add' && $permissiontoadd) { + $anlage->label = GETPOST('label', 'alphanohtml'); + $anlage->fk_soc = $object->socid; + $anlage->fk_contact = $id; + $anlage->fk_anlage_type = GETPOSTINT('fk_anlage_type'); + $anlage->fk_parent = GETPOSTINT('fk_parent'); + $anlage->fk_system = $systemId; + $anlage->manufacturer = GETPOST('manufacturer', 'alphanohtml'); + $anlage->model = GETPOST('model', 'alphanohtml'); + $anlage->serial_number = GETPOST('serial_number', 'alphanohtml'); + $anlage->power_rating = GETPOST('power_rating', 'alphanohtml'); + $anlage->location = GETPOST('location', 'alphanohtml'); + $anlage->installation_date = dol_mktime(0, 0, 0, GETPOSTINT('installation_datemonth'), GETPOSTINT('installation_dateday'), GETPOSTINT('installation_dateyear')); + $anlage->note_private = GETPOST('note_private', 'restricthtml'); + $anlage->status = 1; + + // Get type for system ID + $type = new AnlageType($db); + if ($type->fetch($anlage->fk_anlage_type) > 0) { + $anlage->fk_system = $type->fk_system; + } + + // Dynamic fields + $fieldValues = array(); + $fields = $type->fetchFields(); + foreach ($fields as $field) { + $value = GETPOST('field_'.$field->field_code, 'alphanohtml'); + if ($value !== '') { + $fieldValues[$field->field_code] = $value; + } + } + $anlage->setFieldValues($fieldValues); + + $result = $anlage->create($user); + if ($result > 0) { + setEventMessages($langs->trans('RecordSaved'), null, 'mesgs'); + header('Location: '.$_SERVER['PHP_SELF'].'?id='.$id.'&system='.$anlage->fk_system); + exit; + } else { + setEventMessages($anlage->error, $anlage->errors, 'errors'); + $action = 'create'; + } +} + +if ($action == 'update' && $permissiontoadd) { + $anlage->fetch($anlageId); + $anlage->label = GETPOST('label', 'alphanohtml'); + $anlage->fk_anlage_type = GETPOSTINT('fk_anlage_type'); + $anlage->fk_parent = GETPOSTINT('fk_parent'); + $anlage->manufacturer = GETPOST('manufacturer', 'alphanohtml'); + $anlage->model = GETPOST('model', 'alphanohtml'); + $anlage->serial_number = GETPOST('serial_number', 'alphanohtml'); + $anlage->power_rating = GETPOST('power_rating', 'alphanohtml'); + $anlage->location = GETPOST('location', 'alphanohtml'); + $anlage->installation_date = dol_mktime(0, 0, 0, GETPOSTINT('installation_datemonth'), GETPOSTINT('installation_dateday'), GETPOSTINT('installation_dateyear')); + $anlage->note_private = GETPOST('note_private', 'restricthtml'); + + // Get type for system ID + $type = new AnlageType($db); + if ($type->fetch($anlage->fk_anlage_type) > 0) { + $anlage->fk_system = $type->fk_system; + } + + // Dynamic fields + $fieldValues = array(); + $fields = $type->fetchFields(); + foreach ($fields as $field) { + $value = GETPOST('field_'.$field->field_code, 'alphanohtml'); + if ($value !== '') { + $fieldValues[$field->field_code] = $value; + } + } + $anlage->setFieldValues($fieldValues); + + $result = $anlage->update($user); + if ($result > 0) { + setEventMessages($langs->trans('RecordSaved'), null, 'mesgs'); + header('Location: '.$_SERVER['PHP_SELF'].'?id='.$id.'&system='.$anlage->fk_system); + exit; + } else { + setEventMessages($anlage->error, $anlage->errors, 'errors'); + $action = 'edit'; + } +} + +if ($action == 'confirm_delete' && $confirm == 'yes' && $permissiontodelete) { + $anlage->fetch($anlageId); + $systemIdBefore = $anlage->fk_system; + $result = $anlage->delete($user); + if ($result > 0) { + setEventMessages($langs->trans('RecordDeleted'), null, 'mesgs'); + } else { + setEventMessages($anlage->error, $anlage->errors, 'errors'); + } + header('Location: '.$_SERVER['PHP_SELF'].'?id='.$id.'&system='.$systemIdBefore); + exit; +} + +// File upload +if ($action == 'uploadfile' && $permissiontoadd) { + $anlage->fetch($anlageId); + $upload_dir = $anlage->getFileDirectory(); + + // Create directory if not exists + if (!is_dir($upload_dir)) { + dol_mkdir($upload_dir); + } + + if (!empty($_FILES['userfile']['name'])) { + $result = dol_add_file_process($upload_dir, 0, 1, 'userfile', '', null, '', 1); + if ($result > 0) { + // Add to database + $anlagefile = new AnlageFile($db); + $anlagefile->fk_anlage = $anlageId; + $anlagefile->filename = dol_sanitizeFileName($_FILES['userfile']['name']); + // IMPORTANT: Store ONLY relative path (anlagen/socid/anlageid/filename) - never full path! + $anlagefile->filepath = 'anlagen/'.$anlage->fk_soc.'/'.$anlage->id.'/'.$anlagefile->filename; + $anlagefile->filesize = $_FILES['userfile']['size']; + $anlagefile->mimetype = dol_mimetype($anlagefile->filename); + $anlagefile->create($user); + + // Generate thumbnail + $anlagefile->generateThumbnail(); + + setEventMessages($langs->trans('FileUploaded'), null, 'mesgs'); + } + } + header('Location: '.$_SERVER['PHP_SELF'].'?id='.$id.'&system='.$systemId.'&action=view&anlage_id='.$anlageId); + exit; +} + +// File delete (after confirmation) +if ($action == 'confirm_deletefile' && $confirm == 'yes' && $permissiontodelete) { + $fileId = GETPOSTINT('fileid'); + if ($fileId > 0) { + $anlagefile = new AnlageFile($db); + if ($anlagefile->fetch($fileId) > 0) { + // Delete method handles physical file and database + if ($anlagefile->delete($user) > 0) { + setEventMessages($langs->trans('FileDeleted'), null, 'mesgs'); + } else { + setEventMessages($langs->trans('Error'), null, 'errors'); + } + } + } + header('Location: '.$_SERVER['PHP_SELF'].'?id='.$id.'&system='.$systemId.'&action=view&anlage_id='.$anlageId); + exit; +} + +/* + * View + */ + +$title = $langs->trans('TechnicalInstallations').' - '.$object->getFullName($langs); +llxHeader('', $title, '', '', 0, 0, array('/kundenkarte/js/kundenkarte.js'), array('/kundenkarte/css/kundenkarte.css')); + +// Prepare tabs +$head = contact_prepare_head($object); + +print dol_get_fiche_head($head, 'anlagen', $langs->trans("ContactAddress"), -1, 'contact'); + +// Contact card +$linkback = ''.$langs->trans("BackToList").''; +dol_banner_tab($object, 'id', $linkback, 1, 'rowid', 'nom'); + +print '
'; + +// Confirmation dialogs +if ($action == 'delete') { + print $form->formconfirm( + $_SERVER['PHP_SELF'].'?id='.$id.'&anlage_id='.$anlageId.'&system='.$systemId, + $langs->trans('DeleteElement'), + $langs->trans('ConfirmDeleteElement'), + 'confirm_delete', + '', + 'yes', + 1 + ); +} + +if ($action == 'remove_system') { + $removeSystemId = GETPOSTINT('remove_system_id'); + $sysLabel = isset($customerSystems[$removeSystemId]) ? $customerSystems[$removeSystemId]->label : ''; + print $form->formconfirm( + $_SERVER['PHP_SELF'].'?id='.$id.'&remove_system_id='.$removeSystemId, + $langs->trans('RemoveSystem'), + $langs->trans('ConfirmRemoveSystem', $sysLabel), + 'confirm_remove_system', + '', + 'yes', + 1 + ); +} + +if ($action == 'askdeletefile') { + $fileId = GETPOSTINT('fileid'); + print $form->formconfirm( + $_SERVER['PHP_SELF'].'?id='.$id.'&system='.$systemId.'&anlage_id='.$anlageId.'&fileid='.$fileId, + $langs->trans('Delete'), + $langs->trans('ConfirmDeleteFile'), + 'confirm_deletefile', + '', + 'yes', + 1 + ); +} + +// System tabs (only show enabled systems for this contact) +print '
'; +print '
'; +foreach ($customerSystems as $sysId => $sys) { + $activeClass = ($sysId == $systemId) ? ' active' : ''; + print '
'; + print ''; + if ($sys->picto) { + print ''.kundenkarte_render_icon($sys->picto).''; + } + print ''.dol_escape_htmltag($sys->label).''; + print ''; + // Remove button (only if no elements) + if ($permissiontodelete && $sysId == $systemId) { + print ' '; + } + print '
'; +} + +// Add system button (always on the right) +if ($permissiontoadd) { + // Get systems not yet enabled for this contact + $availableSystems = array_diff_key($allSystems, $customerSystems); + if (!empty($availableSystems)) { + print ''; + } +} +print '
'; + +// Expand/Collapse buttons (only in tree view, not in create/edit/view) +$isTreeView = !in_array($action, array('create', 'edit', 'view')); +if ($isTreeView) { + print '
'; + print ''; + print ''; + print '
'; +} + +print '
'; // End kundenkarte-system-tabs-wrapper + +// Add system form (hidden by default) +if ($permissiontoadd && !empty($availableSystems)) { + print ''; +} + +// Check if contact has any systems +if (empty($customerSystems)) { + print '
'; + print '
'; + print $langs->trans('NoSystemsConfigured').'

'; + if ($permissiontoadd && !empty($allSystems)) { + print $langs->trans('ClickAddSystemToStart'); + } else { + print $langs->trans('ContactAdminToAddSystems'); + } + print '
'; +} elseif ($systemId > 0) { + // Show form or tree for selected system + if (in_array($action, array('create', 'edit', 'view'))) { + // Load element for edit/view + if ($action != 'create' && $anlageId > 0) { + $anlage->fetch($anlageId); + $type = new AnlageType($db); + $type->fetch($anlage->fk_anlage_type); + $type->fetchFields(); + } + + // Load types for select + $types = $anlageType->fetchAllBySystem($systemId); + + print '
'; + + if ($action == 'view') { + // View mode + print '

'.dol_escape_htmltag($anlage->label).'

'; + + print ''; + + print ''; + print ''; + + if ($anlage->manufacturer) { + print ''; + print ''; + } + if ($anlage->model) { + print ''; + print ''; + } + if ($anlage->serial_number) { + print ''; + print ''; + } + if ($anlage->power_rating) { + print ''; + print ''; + } + if ($anlage->location) { + print ''; + print ''; + } + if ($anlage->installation_date) { + print ''; + print ''; + } + + // Dynamic fields + $fieldValues = $anlage->getFieldValues(); + foreach ($type->fields as $field) { + if (isset($fieldValues[$field->field_code]) && $fieldValues[$field->field_code] !== '') { + print ''; + $value = $fieldValues[$field->field_code]; + // For select fields, show the label + if ($field->field_type == 'select' && $field->field_options) { + $options = json_decode($field->field_options, true); + if (isset($options['options'][$value])) { + $value = $options['options'][$value]; + } + } + print ''; + } + } + + if ($anlage->note_private) { + print ''; + print ''; + } + + print '
'.$langs->trans('Type').''.dol_escape_htmltag($anlage->type_label).'
'.$langs->trans('FieldManufacturer').''.dol_escape_htmltag($anlage->manufacturer).'
'.$langs->trans('FieldModel').''.dol_escape_htmltag($anlage->model).'
'.$langs->trans('FieldSerialNumber').''.dol_escape_htmltag($anlage->serial_number).'
'.$langs->trans('FieldPowerRating').''.dol_escape_htmltag($anlage->power_rating).'
'.$langs->trans('FieldLocation').''.dol_escape_htmltag($anlage->location).'
'.$langs->trans('FieldInstallationDate').''.dol_print_date($anlage->installation_date, 'day').'
'.dol_escape_htmltag($field->field_label).''.dol_escape_htmltag($value).'
'.$langs->trans('FieldNotes').''.dol_htmlentitiesbr($anlage->note_private).'
'; + + // Files section + $anlagefile = new AnlageFile($db); + $files = $anlagefile->fetchAllByAnlage($anlageId); + + print '

'.$langs->trans('AttachedFiles').'

'; + + if ($permissiontoadd) { + print '
'; + print ''; + print ''; + print ' '; + print '

'; + } + + if (!empty($files)) { + print '
'; + foreach ($files as $file) { + print '
'; + print '
'; + if ($file->file_type == 'image') { + $thumbUrl = $file->getThumbUrl(); + if ($thumbUrl) { + print ''; + } else { + print ''; + } + } elseif ($file->file_type == 'pdf') { + // PDF preview using iframe - 50% smaller, no toolbar + print '
'; + print ''; + print '
'; + } else { + print ''; + } + print '
'; + print '
'; + print '
'.dol_escape_htmltag(dol_trunc($file->filename, 20)).'
'; + print '
'.dol_print_size($file->filesize).'
'; + print '
'; + print ''; + if ($permissiontodelete) { + print ''; + } + print '
'; + print '
'; + print '
'; + } + print '
'; + } else { + print '

'.$langs->trans('NoFiles').'

'; + } + + // Action buttons + print '
'; + if ($permissiontoadd) { + print ''.$langs->trans('Modify').''; + } + if ($permissiontodelete) { + print ''.$langs->trans('Delete').''; + } + print ''.$langs->trans('Back').''; + print '
'; + + } else { + // Create/Edit form + $formAction = ($action == 'create') ? 'add' : 'update'; + print '
'; + print ''; + print ''; + if ($action == 'edit') { + print ''; + } + + print ''; + + // Label + print ''; + print ''; + + // Type + print ''; + print ''; + + // Parent (uses contact-specific tree) + $tree = $anlage->fetchTreeByContact($object->socid, $id, $systemId); + print ''; + print ''; + + // Manufacturer + print ''; + print ''; + + // Model + print ''; + print ''; + + // Serial number + print ''; + print ''; + + // Power rating + print ''; + print ''; + + // Location + print ''; + print ''; + + // Installation date + print ''; + print ''; + + // Notes + print ''; + print ''; + + print '
'.$langs->trans('Label').'
'.$langs->trans('Type').''; + if (empty($types)) { + print '
'.$langs->trans('NoTypesDefinedForSystem').''; + } + print '
'.$langs->trans('SelectParent').'
'.$langs->trans('FieldManufacturer').'
'.$langs->trans('FieldModel').'
'.$langs->trans('FieldSerialNumber').'
'.$langs->trans('FieldPowerRating').'
'.$langs->trans('FieldLocation').'
'.$langs->trans('FieldInstallationDate').''.$form->selectDate($action == 'edit' ? $anlage->installation_date : '', 'installation_date', 0, 0, 1, '', 1, 1).'
'.$langs->trans('FieldNotes').'
'; + + // Dynamic fields will be loaded via JavaScript based on type selection + print '
'; + + print '
'; + print ''; + print ' '.$langs->trans('Cancel').''; + print '
'; + + print '
'; + } + + print '
'; + + } else { + // Tree view + if ($permissiontoadd) { + print ''; + } + + // Load tree for this contact + $tree = $anlage->fetchTreeByContact($object->socid, $id, $systemId); + + if (!empty($tree)) { + print '
'; + printTree($tree, $id, $systemId, $permissiontoadd, $permissiontodelete, $langs); + print '
'; + } else { + print '
'.$langs->trans('NoInstallations').'
'; + } + } +} + +print '
'; + +print dol_get_fiche_end(); + +// Tooltip container +print '
'; + +llxFooter(); +$db->close(); + +/** + * Print tree recursively + */ +function printTree($nodes, $contactid, $systemId, $canEdit, $canDelete, $langs, $level = 0) +{ + foreach ($nodes as $node) { + $hasChildren = !empty($node->children); + + // Build tooltip data for hover on icon + $tooltipData = array( + 'label' => $node->label, + 'type' => $node->type_label, + 'location' => $node->location, + 'manufacturer' => $node->manufacturer, + 'model' => $node->model, + 'serial_number' => $node->serial_number, + 'power_rating' => $node->power_rating, + 'installation_date' => $node->installation_date ? dol_print_date($node->installation_date, 'day') : '', + 'note' => $node->note_private, + ); + + print '
'; + print '
'; + + // Toggle + if ($hasChildren) { + print ''; + } else { + print ''; + } + + // Icon with tooltip data + $picto = $node->type_picto ? $node->type_picto : 'fa-cube'; + print ''.kundenkarte_render_icon($picto).''; + + // Label with manufacturer/power in parentheses + file indicators + $viewUrl = $_SERVER['PHP_SELF'].'?id='.$contactid.'&system='.$systemId.'&action=view&anlage_id='.$node->id; + print ''.dol_escape_htmltag($node->label); + $labelInfo = array(); + if ($node->manufacturer) { + $labelInfo[] = $node->manufacturer; + } + if ($node->power_rating) { + $labelInfo[] = $node->power_rating; + } + if (!empty($labelInfo)) { + print ' ('.dol_escape_htmltag(implode(', ', $labelInfo)).')'; + } + // File indicators - directly after parentheses + if ($node->image_count > 0 || $node->doc_count > 0) { + print ' '; + if ($node->image_count > 0) { + print ''; + print ''; + if ($node->image_count > 1) { + print ' '.$node->image_count; + } + print ''; + } + if ($node->doc_count > 0) { + print ''; + print ''; + if ($node->doc_count > 1) { + print ' '.$node->doc_count; + } + print ''; + } + print ''; + } + print ''; + + // Type badge + if ($node->type_short || $node->type_label) { + $typeDisplay = $node->type_short ? $node->type_short : $node->type_label; + print ''.dol_escape_htmltag($typeDisplay).''; + } + + // Location with icon + if ($node->location) { + print ' '.dol_escape_htmltag($node->location).''; + } + + // Actions + print ''; + print ''; + if ($canEdit) { + print ''; + print ''; + } + if ($canDelete) { + print ''; + } + print ''; + + print '
'; + + // Children + if ($hasChildren) { + print '
'; + printTree($node->children, $contactid, $systemId, $canEdit, $canDelete, $langs, $level + 1); + print '
'; + } + + print '
'; + } +} + +/** + * Print tree options for select + */ +function printTreeOptions($nodes, $selected = 0, $excludeId = 0, $prefix = '') +{ + foreach ($nodes as $node) { + if ($node->id == $excludeId) continue; + + $sel = ($node->id == $selected) ? ' selected' : ''; + print ''; + + if (!empty($node->children)) { + printTreeOptions($node->children, $selected, $excludeId, $prefix.'   '); + } + } +} diff --git a/tabs/contact_favoriteproducts.php b/tabs/contact_favoriteproducts.php new file mode 100644 index 0000000..5d6fab3 --- /dev/null +++ b/tabs/contact_favoriteproducts.php @@ -0,0 +1,363 @@ +loadLangs(array('companies', 'products', 'orders', 'kundenkarte@kundenkarte')); + +// Get parameters +$id = GETPOSTINT('id'); +$action = GETPOST('action', 'aZ09'); +$confirm = GETPOST('confirm', 'alpha'); + +// Security check +if (!$user->hasRight('kundenkarte', 'read')) { + accessforbidden(); +} + +// Initialize objects +$object = new Contact($db); +$form = new Form($db); +$favoriteProduct = new FavoriteProduct($db); + +// Load contact +if ($id > 0) { + $result = $object->fetch($id); + if ($result <= 0) { + dol_print_error($db, $object->error); + exit; + } +} + +$permissiontoread = $user->hasRight('kundenkarte', 'read'); +$permissiontoadd = $user->hasRight('kundenkarte', 'write'); +$permissiontodelete = $user->hasRight('kundenkarte', 'delete'); + +/* + * Actions + */ + +if ($action == 'add' && $permissiontoadd) { + $productid = GETPOSTINT('productid'); + $qty = GETPOSTFLOAT('qty'); + + if ($productid > 0) { + $favoriteProduct->fk_soc = $object->socid; + $favoriteProduct->fk_contact = $id; + $favoriteProduct->fk_product = $productid; + $favoriteProduct->qty = $qty > 0 ? $qty : 1; + $favoriteProduct->active = 1; + + $result = $favoriteProduct->create($user); + if ($result > 0) { + setEventMessages($langs->trans('RecordSaved'), null, 'mesgs'); + } else { + setEventMessages($favoriteProduct->error, $favoriteProduct->errors, 'errors'); + } + } + header('Location: '.$_SERVER['PHP_SELF'].'?id='.$id); + exit; +} + +if ($action == 'delete' && $permissiontodelete) { + $favid = GETPOSTINT('favid'); + if ($favid > 0) { + $favoriteProduct->fetch($favid); + $result = $favoriteProduct->delete($user); + if ($result > 0) { + setEventMessages($langs->trans('RecordDeleted'), null, 'mesgs'); + } else { + setEventMessages($favoriteProduct->error, $favoriteProduct->errors, 'errors'); + } + } + header('Location: '.$_SERVER['PHP_SELF'].'?id='.$id); + exit; +} + +if ($action == 'updateqty' && $permissiontoadd) { + $favid = GETPOSTINT('favid'); + $qty = GETPOSTFLOAT('qty'); + + if ($favid > 0 && $qty > 0) { + $favoriteProduct->fetch($favid); + $favoriteProduct->qty = $qty; + $result = $favoriteProduct->update($user); + } + header('Location: '.$_SERVER['PHP_SELF'].'?id='.$id); + exit; +} + +if ($action == 'moveup' && $permissiontoadd) { + $favid = GETPOSTINT('favid'); + if ($favid > 0) { + $favoriteProduct->moveUpByContact($favid, $id); + } + header('Location: '.$_SERVER['PHP_SELF'].'?id='.$id); + exit; +} + +if ($action == 'movedown' && $permissiontoadd) { + $favid = GETPOSTINT('favid'); + if ($favid > 0) { + $favoriteProduct->moveDownByContact($favid, $id); + } + header('Location: '.$_SERVER['PHP_SELF'].'?id='.$id); + exit; +} + +if ($action == 'confirm_generateorder' && $confirm == 'yes' && $permissiontoadd) { + $selected = GETPOST('selected_products', 'array'); + $quantities = GETPOST('quantities', 'array'); + + if (!empty($selected)) { + $result = $favoriteProduct->generateOrderByContact($user, $object->socid, $id, $selected, $quantities); + if ($result > 0) { + setEventMessages($langs->trans('OrderGenerated', $result), null, 'mesgs'); + header('Location: '.DOL_URL_ROOT.'/commande/card.php?id='.$result); + exit; + } else { + setEventMessages($favoriteProduct->error, $favoriteProduct->errors, 'errors'); + } + } else { + setEventMessages($langs->trans('NoProductsSelected'), null, 'warnings'); + } +} + +/* + * View + */ + +$title = $langs->trans('FavoriteProducts').' - '.$object->getFullName($langs); +llxHeader('', $title); + +// Fetch favorites for this contact +$favorites = $favoriteProduct->fetchAllByContact($id); + +// Prepare tabs +$head = contact_prepare_head($object); + +print dol_get_fiche_head($head, 'favoriteproducts', $langs->trans("ContactAddress"), -1, 'contact'); + +// Contact card +$linkback = ''.$langs->trans("BackToList").''; + +dol_banner_tab($object, 'id', $linkback, 1, 'rowid', 'nom'); + +print '
'; + +// Confirmation dialog for order generation +if ($action == 'generateorder') { + $formquestion = array(); + print $form->formconfirm( + $_SERVER['PHP_SELF'].'?id='.$id, + $langs->trans('GenerateOrder'), + $langs->trans('ConfirmGenerateOrder'), + 'confirm_generateorder', + $formquestion, + 'yes', + 1 + ); +} + +print '
'; + +// Add product form +if ($permissiontoadd) { + print '
'; + print '
'; + print ''; + print ''; + + print ''; + + print '
'; + print ''; + print ''; + print ''; + print '
'; + + print '
'; + print '
'; +} + +// List of favorites +print '
'; +print ''; +print ''; +print ''; + +if (is_array($favorites) && count($favorites) > 0) { + print ''; + print ''; + print ''; + print ''; + if ($permissiontoadd) { + print ''; + } + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + + $totalHT = 0; + $totalItems = count($favorites); + $currentIndex = 0; + foreach ($favorites as $fav) { + $currentIndex++; + $lineTotal = $fav->qty * $fav->product_price; + $totalHT += $lineTotal; + + // Format quantity + $qtyDisplay = (floor($fav->qty) == $fav->qty) ? (int)$fav->qty : round($fav->qty, 2); + + print ''; + + // Checkbox + print ''; + + // Position (up/down buttons) + if ($permissiontoadd) { + print ''; + } + + // Product + print ''; + + // Quantity + print ''; + + // Unit price + print ''; + + // Total + print ''; + + // Actions + print ''; + + print ''; + } + + print ''; + print ''; + print ''; + $colspan = $permissiontoadd ? 5 : 4; + print ''; + print ''; + print ''; + print ''; + print ''; + print '
'; + print ''; + print ''.$langs->trans('Position').''.$langs->trans('Product').''.$langs->trans('DefaultQuantity').''.$langs->trans('UnitPriceHT').''.$langs->trans('Total').''.$langs->trans('Actions').'
'; + print ''; + print ''; + if ($currentIndex > 1) { + print ''; + print ''; + print ' '; + } else { + print ' '; + } + if ($currentIndex < $totalItems) { + print ''; + print ''; + print ''; + } else { + print ''; + } + print ''; + print ''; + print img_object('', 'product', 'class="pictofixedwidth"'); + print dol_escape_htmltag($fav->product_ref).' - '.dol_escape_htmltag($fav->product_label); + print ''; + print ''; + print '
'; + print ''; + print ''; + print '
'; + print '
'.price($fav->product_price).''.price($lineTotal).''; + if ($permissiontodelete) { + print ''; + print img_delete(); + print ''; + } + print '
'.$langs->trans('Total').''.price($totalHT).'
'; + + // Actions bar + print '
'; + print '
'; + print ' '; + print ''; + print '
'; + + if ($permissiontoadd) { + print ''; + } + print '
'; +} else { + print '
'.$langs->trans('NoFavoriteProducts').'
'; +} + +print '
'; +print '
'; + +print dol_get_fiche_end(); + +// JavaScript for checkbox handling +print ''; + +llxFooter(); +$db->close(); diff --git a/tabs/favoriteproducts.php b/tabs/favoriteproducts.php new file mode 100755 index 0000000..07ced6a --- /dev/null +++ b/tabs/favoriteproducts.php @@ -0,0 +1,363 @@ +loadLangs(array('companies', 'products', 'orders', 'kundenkarte@kundenkarte')); + +// Get parameters +$id = GETPOSTINT('id'); +$action = GETPOST('action', 'aZ09'); +$confirm = GETPOST('confirm', 'alpha'); + +// Security check +if (!$user->hasRight('kundenkarte', 'read')) { + accessforbidden(); +} + +// Initialize objects +$object = new Societe($db); +$form = new Form($db); +$favoriteProduct = new FavoriteProduct($db); + +// Load thirdparty +if ($id > 0) { + $result = $object->fetch($id); + if ($result <= 0) { + dol_print_error($db, $object->error); + exit; + } +} + +$permissiontoread = $user->hasRight('kundenkarte', 'read'); +$permissiontoadd = $user->hasRight('kundenkarte', 'write'); +$permissiontodelete = $user->hasRight('kundenkarte', 'delete'); + +/* + * Actions + */ + +if ($action == 'add' && $permissiontoadd) { + $productid = GETPOSTINT('productid'); + $qty = GETPOSTFLOAT('qty'); + + if ($productid > 0) { + $favoriteProduct->fk_soc = $id; + $favoriteProduct->fk_product = $productid; + $favoriteProduct->qty = $qty > 0 ? $qty : 1; + $favoriteProduct->active = 1; + + $result = $favoriteProduct->create($user); + if ($result > 0) { + setEventMessages($langs->trans('RecordSaved'), null, 'mesgs'); + } else { + setEventMessages($favoriteProduct->error, $favoriteProduct->errors, 'errors'); + } + } + header('Location: '.$_SERVER['PHP_SELF'].'?id='.$id); + exit; +} + +if ($action == 'delete' && $permissiontodelete) { + $favid = GETPOSTINT('favid'); + if ($favid > 0) { + $favoriteProduct->fetch($favid); + $result = $favoriteProduct->delete($user); + if ($result > 0) { + setEventMessages($langs->trans('RecordDeleted'), null, 'mesgs'); + } else { + setEventMessages($favoriteProduct->error, $favoriteProduct->errors, 'errors'); + } + } + header('Location: '.$_SERVER['PHP_SELF'].'?id='.$id); + exit; +} + +if ($action == 'updateqty' && $permissiontoadd) { + $favid = GETPOSTINT('favid'); + $qty = GETPOSTFLOAT('qty'); + + if ($favid > 0 && $qty > 0) { + $favoriteProduct->fetch($favid); + $favoriteProduct->qty = $qty; + $result = $favoriteProduct->update($user); + } + header('Location: '.$_SERVER['PHP_SELF'].'?id='.$id); + exit; +} + +if ($action == 'moveup' && $permissiontoadd) { + $favid = GETPOSTINT('favid'); + if ($favid > 0) { + $favoriteProduct->moveUp($favid, $id); + } + header('Location: '.$_SERVER['PHP_SELF'].'?id='.$id); + exit; +} + +if ($action == 'movedown' && $permissiontoadd) { + $favid = GETPOSTINT('favid'); + if ($favid > 0) { + $favoriteProduct->moveDown($favid, $id); + } + header('Location: '.$_SERVER['PHP_SELF'].'?id='.$id); + exit; +} + +if ($action == 'confirm_generateorder' && $confirm == 'yes' && $permissiontoadd) { + $selected = GETPOST('selected_products', 'array'); + $quantities = GETPOST('quantities', 'array'); + + if (!empty($selected)) { + $result = $favoriteProduct->generateOrder($user, $id, $selected, $quantities); + if ($result > 0) { + setEventMessages($langs->trans('OrderGenerated', $result), null, 'mesgs'); + header('Location: '.DOL_URL_ROOT.'/commande/card.php?id='.$result); + exit; + } else { + setEventMessages($favoriteProduct->error, $favoriteProduct->errors, 'errors'); + } + } else { + setEventMessages($langs->trans('NoProductsSelected'), null, 'warnings'); + } +} + +/* + * View + */ + +$title = $langs->trans('FavoriteProducts').' - '.$object->name; +llxHeader('', $title); + +// Fetch favorites +$favorites = $favoriteProduct->fetchAllBySociete($id); + +// Prepare tabs +$head = societe_prepare_head($object); + +print dol_get_fiche_head($head, 'favoriteproducts', $langs->trans("ThirdParty"), -1, 'company'); + +// Thirdparty card +$linkback = ''.$langs->trans("BackToList").''; + +dol_banner_tab($object, 'socid', $linkback, ($user->socid ? 0 : 1), 'rowid', 'nom'); + +print '
'; + +// Confirmation dialog for order generation +if ($action == 'generateorder') { + $formquestion = array(); + print $form->formconfirm( + $_SERVER['PHP_SELF'].'?id='.$id, + $langs->trans('GenerateOrder'), + $langs->trans('ConfirmGenerateOrder'), + 'confirm_generateorder', + $formquestion, + 'yes', + 1 + ); +} + +print '
'; + +// Add product form +if ($permissiontoadd) { + print '
'; + print '
'; + print ''; + print ''; + + print ''; + + print '
'; + print ''; + print ''; + print ''; + print '
'; + + print '
'; + print '
'; +} + +// List of favorites +print '
'; +print ''; +print ''; +print ''; + +if (is_array($favorites) && count($favorites) > 0) { + print ''; + print ''; + print ''; + print ''; + if ($permissiontoadd) { + print ''; + } + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + + $totalHT = 0; + $totalItems = count($favorites); + $currentIndex = 0; + foreach ($favorites as $fav) { + $currentIndex++; + $lineTotal = $fav->qty * $fav->product_price; + $totalHT += $lineTotal; + + // Format quantity: show as integer if whole number, otherwise 2 decimals + // Use '.' as decimal separator for HTML number input compatibility + $qtyDisplay = (floor($fav->qty) == $fav->qty) ? (int)$fav->qty : round($fav->qty, 2); + + print ''; + + // Checkbox + print ''; + + // Position (up/down buttons) + if ($permissiontoadd) { + print ''; + } + + // Product + print ''; + + // Quantity + print ''; + + // Unit price + print ''; + + // Total + print ''; + + // Actions + print ''; + + print ''; + } + + print ''; + print ''; + print ''; + $colspan = $permissiontoadd ? 5 : 4; + print ''; + print ''; + print ''; + print ''; + print ''; + print '
'; + print ''; + print ''.$langs->trans('Position').''.$langs->trans('Product').''.$langs->trans('DefaultQuantity').''.$langs->trans('UnitPriceHT').''.$langs->trans('Total').''.$langs->trans('Actions').'
'; + print ''; + print ''; + if ($currentIndex > 1) { + print ''; + print ''; + print ' '; + } else { + print ' '; + } + if ($currentIndex < $totalItems) { + print ''; + print ''; + print ''; + } else { + print ''; + } + print ''; + print ''; + print img_object('', 'product', 'class="pictofixedwidth"'); + print dol_escape_htmltag($fav->product_ref).' - '.dol_escape_htmltag($fav->product_label); + print ''; + print ''; + print '
'; + print ''; + print ''; + print '
'; + print '
'.price($fav->product_price).''.price($lineTotal).''; + if ($permissiontodelete) { + print ''; + print img_delete(); + print ''; + } + print '
'.$langs->trans('Total').''.price($totalHT).'
'; + + // Actions bar + print '
'; + print '
'; + print ' '; + print ''; + print '
'; + + if ($permissiontoadd) { + print ''; + } + print '
'; +} else { + print '
'.$langs->trans('NoFavoriteProducts').'
'; +} + +print '
'; +print '
'; + +print dol_get_fiche_end(); + +// JavaScript for checkbox handling +print ''; + +llxFooter(); +$db->close();