From 85ccdae2cbbe51bf55c053343c176d9fc3235059 Mon Sep 17 00:00:00 2001 From: Claude Agent Date: Tue, 5 May 2026 13:02:16 +0000 Subject: [PATCH] sync efactura-generator -> 0.9-beta-14 Mirror sincronizat cu repo canonic /workspace/efactura-generator. CLAUDE.md: documentat workflow sync + exclude config.json din rsync deploy. Co-Authored-By: Claude Opus 4.7 (1M context) --- CLAUDE.md | 22 +- efactura-generator/.gitattributes | 2 + efactura-generator/.htaccess | 13 +- efactura-generator/CHANGELOG.md | 150 +- efactura-generator/LICENSE.md | 1326 +-- efactura-generator/README.md | 233 +- efactura-generator/index.html | 536 +- efactura-generator/js/anaf.js | 129 + efactura-generator/js/catalog.js | 129 + efactura-generator/js/formatter.js | 142 +- efactura-generator/js/numeric.js | 273 + efactura-generator/js/print.js | 500 +- efactura-generator/js/script.js | 8536 ++++++++++------- efactura-generator/js/server.js | 83 +- efactura-generator/js/storage.js | 185 + efactura-generator/js/validation/br-ro.js | 481 + efactura-generator/js/validation/cif.js | 71 + efactura-generator/js/validation/iban.js | 63 + efactura-generator/js/vendor/big.mjs | 1027 ++ .../js/vendor/html2pdf.bundle.min.js | 3 + efactura-generator/js/vendor/html2pdf.mjs | 44 + efactura-generator/js/vendor/jszip.mjs | 7 + efactura-generator/receiver.php | 398 +- efactura-generator/styles/main.css | 2179 ++++- .../templates/print-compact.html | 706 +- efactura-generator/templates/print.html | 822 +- 26 files changed, 12188 insertions(+), 5872 deletions(-) create mode 100644 efactura-generator/.gitattributes create mode 100644 efactura-generator/js/anaf.js create mode 100644 efactura-generator/js/catalog.js create mode 100644 efactura-generator/js/numeric.js create mode 100644 efactura-generator/js/storage.js create mode 100644 efactura-generator/js/validation/br-ro.js create mode 100644 efactura-generator/js/validation/cif.js create mode 100644 efactura-generator/js/validation/iban.js create mode 100644 efactura-generator/js/vendor/big.mjs create mode 100644 efactura-generator/js/vendor/html2pdf.bundle.min.js create mode 100644 efactura-generator/js/vendor/html2pdf.mjs create mode 100644 efactura-generator/js/vendor/jszip.mjs diff --git a/CLAUDE.md b/CLAUDE.md index e857211..2aaa572 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -9,7 +9,10 @@ Romfast company website (2025 version) for a Romanian ERP software company. Prom ## Deploy ```bash -rsync -avz --exclude='.git' --exclude='.superdesign' --exclude='.claude' --exclude='verify-cleanup.sh' \ +rsync -avz \ + --exclude='.git' --exclude='.superdesign' --exclude='.claude' \ + --exclude='verify-cleanup.sh' \ + --exclude='efactura-generator/config.json' \ -e "ssh -p 7822" \ /home/moltbot/workspace/romfast-website/ \ romfastr@nl1-ss18.a2hosting.com:~/public_html/ @@ -17,6 +20,19 @@ rsync -avz --exclude='.git' --exclude='.superdesign' --exclude='.claude' --exclu - **Host:** nl1-ss18.a2hosting.com | **Port:** 7822 | **User:** romfastr - **Document root:** `~/public_html/` — SSH key auth (no password) +- **Important:** rsync-ul NU folosește `--delete`, deci nu șterge nimic pe prod. `efactura-generator/config.json` e exclus explicit ca să nu poată fi suprascris niciodată din repo (conține `api_key` și e gestionat doar pe server). + +## Sub-proiectul `efactura-generator/` + +Directorul `efactura-generator/` din acest repo este o **oglindă** a proiectului canonic `/workspace/efactura-generator/` (repo separat: `git@gitea.romfast.ro:romfast/efactura-generator.git`). Sursa de adevăr e acolo. + +**NU edita direct fișierele din `efactura-generator/` aici** — modificările se pierd la următorul sync. În schimb: + +1. Editează în `/workspace/efactura-generator/` (repo canonic). +2. Rulează `/workspace/efactura-generator/sync-to-website.sh` care propagă schimbările în `efactura-generator/` din acest repo cu excluderile potrivite (fără `config.json`, fără Dockerfile, fără docs interne etc.). +3. Commit aici (`romfast-website`) și deploy cu rsync-ul de mai sus. + +`config.json` de pe server (`~/public_html/efactura-generator/config.json`) conține `api_key` și nu e nici în repo, nici în sync — se gestionează manual pe a2hosting. ## Development @@ -27,10 +43,10 @@ python3 -m http.server 8000 # Access at http://localhost:8000 ``` -The `efactura-generator/` sub-app has its own Node.js server: +The `efactura-generator/` sub-app has its own Node.js server (rulează din repo-ul canonic, nu de aici): ```bash -cd efactura-generator && node js/server.js +cd /workspace/efactura-generator && node js/server.js # Access at http://localhost:3000 ``` diff --git a/efactura-generator/.gitattributes b/efactura-generator/.gitattributes new file mode 100644 index 0000000..dfe0770 --- /dev/null +++ b/efactura-generator/.gitattributes @@ -0,0 +1,2 @@ +# Auto detect text files and perform LF normalization +* text=auto diff --git a/efactura-generator/.htaccess b/efactura-generator/.htaccess index 010c610..b96b9c0 100644 --- a/efactura-generator/.htaccess +++ b/efactura-generator/.htaccess @@ -1,4 +1,9 @@ - - Order Allow,Deny - Allow from all - + + Order Allow,Deny + Allow from all + + +# Native ESM (.mjs) require JavaScript MIME type pe Apache. + + AddType text/javascript .mjs + diff --git a/efactura-generator/CHANGELOG.md b/efactura-generator/CHANGELOG.md index 54028cb..3b5b123 100644 --- a/efactura-generator/CHANGELOG.md +++ b/efactura-generator/CHANGELOG.md @@ -1,35 +1,115 @@ -# Changelog - -## 0.9-beta-4 - 07.02.2025 - -### Bugfixes -- Fixed: Al doilea tag TaxTotal se generează doar când valuta documentului diferă de valuta TVA. - -## 0.9-beta-3 - 14.01.2025 - -### Bugfixes -- Fixed: Discounturile de pe articole nu se mai cumulează în discount global pe factură. - -### New Features -- Added: Se completează valoare reducere și cod reducere pe fiecare articol. - -## 0.9-beta-2 - 07.01.2025 - -### Bugfixes -- Fixed: Selecția judet București și afișare oraș. - -### New Features -- Added: Se completează codul și motivul scutirii în secțiunea "Defalcare TVA" pentru articole cu Tip TVA "E" Neimpozabil. -- Added: S-a tratat cazul în care furnizorul nu este plătitor de TVA (codul fiscal nu are atributul fiscal "RO"). -- Added: Citire din xml coduri identificare articole (vânzător, cumpărător, cod de bare, CPV, NC8, vamal). -- Added: Editare, adăugare, ștergere coduri identificare articole. - -### Modifications -- Modified: Afișare responsive pentru ecrane de diferite dimensiuni. - -### TODO -- Implement: citire și editare referință factura originală pentru factura storno. -- Implement: citire și editare modalități de plată. - -## 0.9-beta-1 - 06.01.2025 -- Initial beta release. +# Changelog + +## 0.9-beta-14 - 05.05.2026 + +### Bugfixes +- Fixed: pe găzduirea cu PHP 7.3 (a2hosting), `receiver.php` returna 500 Internal Server Error la upload XML din cauza unei sintaxe noi (arrow function `fn() =>`) introduse în beta-13. Înlocuită cu funcție anonimă clasică, compatibilă cu PHP 7.3+. + +## 0.9-beta-13 - 04.05.2026 + +### Bugfixes +- Fixed: la încărcarea unui XML cu furnizor sau client neplătitor de TVA, codul fiscal apărea în câmpul „Nr. înregistrare" iar „Cod TVA" rămânea gol. Acum, dacă firma nu e plătitoare TVA, CIF-ul se completează în „Cod TVA" și numărul de la Registrul Comerțului în „Nr. înregistrare" (simetric cu modul în care se salvează XML-ul). +- Fixed: deploy în Docker/Dokploy returna „Acces interzis" la căutare CIF și validare ANAF, deoarece request-urile veneau prin reverse proxy și apăreau ca venind dintr-un IP intern, nu de la utilizator. Lista de IP-uri permise se poate dezactiva acum (gol sau `*`). + +### Modifications +- Configurare receiver prin variabile de mediu (`ANAF_API_KEY`, `ANAF_ALLOWED_IPS`, `ANAF_TOKEN`, `ANAF_TEMP_LIFETIME`) — suprascriu valorile din `config.json`. Util pentru deploy în container fără rebuild la schimbare configurație. +- Dockerfile: setează implicit `ANAF_ALLOWED_IPS=*` (verificare IP dezactivată), potrivit pentru deploy behind reverse proxy unde same-origin asigură deja protecția. + +## 0.9-beta-12 - 04.05.2026 + +### Bugfixes +- Fixed: eroare BR-CO-15 falsă după click pe „Recalculează Totaluri" — defalcarea TVA părea să nu corespundă cu Total TVA, deși matematica era corectă. +- Fixed: eroare BR-16 falsă pe linii cu cantitate fracționară (ex. 1,000 buc) — totalul liniei era marcat greșit ca incorect. +- Fixed: la salvare apărea „completați toate câmpurile obligatorii" chiar când nu se referențiază altă factură. Câmpul „Data factură referită" e opțional și nu mai blochează salvarea când e gol. +- Fixed: „Factură Nouă" — eroare XML declaration duplicată la parsare (`XMLSerializer` include deja ``, codul o prefixa din nou). Strip declaration înainte de concatenare. + +### Modifications +- ANAF lookup CIF: completare automată oraș, județ, telefon și prefix `RO` la CIF-ul plătitorilor TVA. Toast indică acum statusul `Plătitor/Neplătitor TVA · Înregistrat eFactura`. Câmpurile `CountrySubentity` (SELECT RO-XX) și `Country` se populează automat. +- Număr factură: pre-populare din secvența localStorage la deschiderea aplicației (fără incrementare contor). +- Număr factură: format configurabil — serie + spațiu + an opțional + contor cu N cifre (1–8). Exemple: `RFT 20260001` (cu an) sau `RFT 0001` (fără an). Modal „Factură Nouă" extins cu checkbox „Include an în număr" și input „Cifre contor". + +## 0.9-beta-11 - 04.05.2026 + +### Bugfixes +- Fixed: Lookup CIF ANAF nu mai funcționa — migrat la API v9 (PlatitorTvaRest) care a înlocuit v8-ul async. + +### Modifications +- Added: Documentație rulare locală și Docker în README. +- Added: Script `start.sh` pentru pornire dev (Node :3000 + PHP :8000) cu auto-stop al proceselor existente pe aceste porturi. Banner-ul indică explicit `:8000` pentru testare cu ANAF/receiver, `:3000` pentru testare statică. + +## 0.9-beta-10 - 30.04.2026 + +### New Features +- Added: Import bulk — se pot încărca mai multe fișiere XML (sau ZIP) simultan. Un sidebar afișează lista fișierelor deschise; fișierele modificate sunt marcate vizual. Limită 50 fișiere. + +## 0.9-beta-9 - 30.04.2026 + +### New Features +- Added: Catalog produse local — articolele pot fi salvate în catalogul browserului și refolosite prin autocomplete la câmpul „Denumire" pe orice linie factură. + +## 0.9-beta-8 - 30.04.2026 + +### New Features +- Added: Numerotare automată facturi — buton „Factură Nouă" generează numărul următor din serie configurată (ex. `RFT2026-0042`). La trecerea anului, aplicația întreabă dacă se continuă seria sau se resetează contorul la 1. + +## 0.9-beta-7 - 30.04.2026 + +### New Features +- Added: Descărcare PDF direct din browser, fără server — buton „Descarcă PDF" în header și în paginile de printare. + +## 0.9-beta-6 - 30.04.2026 + +### New Features +- Added: Validare XML la ANAF — buton „Validare ANAF" trimite factura la serverul ANAF și afișează erorile returnate (disponibil doar când receiver.php este activ). +- Added: Lookup CIF ANAF — buton „Caută CIF" lângă câmpurile cod TVA completează automat numele, adresa și numărul de înregistrare al firmei. + +## 0.9-beta-5 - 30.04.2026 + +### New Features +- Added: Încărcare ZIP — fișierele ZIP cu XML eFactura pot fi încărcate direct (prin buton sau drag-and-drop); primul XML din arhivă este extras automat. +- Added: Redesign vizual complet — font Geist, paletă warm-paper cu header slate, spațiere și contrast îmbunătățite. +- Added: Profil furnizor — datele furnizorului pot fi salvate în browser și refolosite la facturi noi cu un singur click. +- Added: Tip factură — câmp nou pentru tipul documentului: factură comercială, notă de credit, factură corectată sau autofactură. +- Added: Modalități de plată — secțiune nouă cu unul sau mai multe rânduri cod plată + IBAN. +- Added: Referință factură originală — câmpuri pentru numărul și data facturii la care se referă storno-ul; completate automat la apăsarea „Stornează". +- Added: Validare matematică inline — badge verde/roșu pe fiecare linie și la total, arată dacă valorile sunt consistente. La salvare apare un avertisment dacă există diferențe. +- Added: Validare CIF/CUI pe blur — eroare inline dacă cifra de control nu este corectă. +- Added: Validare IBAN pe blur — eroare inline dacă IBAN-ul are lungime sau check digits incorecte. +- Added: Panel reguli CIUS-RO — panou flotant cu lista erorilor de conformitate față de standardul eFactura, actualizat în timp real. Click pe o eroare navighează la câmpul problematic. + +### Bugfixes +- Fixed: Funcția „Stornează" lăsa valorile interne inconsistente față de ce era afișat în formular. + +## 0.9-beta-4 - 07.02.2025 + +### Bugfixes +- Fixed: Al doilea tag TaxTotal se generează doar când valuta documentului diferă de valuta TVA. + +## 0.9-beta-3 - 14.01.2025 + +### Bugfixes +- Fixed: Discounturile de pe articole nu se mai cumulează în discount global pe factură. + +### New Features +- Added: Se completează valoare reducere și cod reducere pe fiecare articol. + +## 0.9-beta-2 - 07.01.2025 + +### Bugfixes +- Fixed: Selecția judet București și afișare oraș. + +### New Features +- Added: Se completează codul și motivul scutirii în secțiunea "Defalcare TVA" pentru articole cu Tip TVA "E" Neimpozabil. +- Added: S-a tratat cazul în care furnizorul nu este plătitor de TVA (codul fiscal nu are atributul fiscal "RO"). +- Added: Citire din xml coduri identificare articole (vânzător, cumpărător, cod de bare, CPV, NC8, vamal). +- Added: Editare, adăugare, ștergere coduri identificare articole. + +### Modifications +- Modified: Afișare responsive pentru ecrane de diferite dimensiuni. + +### TODO +- Implement: citire și editare referință factura originală pentru factura storno. +- Implement: citire și editare modalități de plată. + +## 0.9-beta-1 - 06.01.2025 +- Initial beta release. diff --git a/efactura-generator/LICENSE.md b/efactura-generator/LICENSE.md index 46ac578..e6c6eef 100644 --- a/efactura-generator/LICENSE.md +++ b/efactura-generator/LICENSE.md @@ -1,663 +1,663 @@ - GNU AFFERO GENERAL PUBLIC LICENSE - Version 3, 19 November 2007 - -Copyright (C) 2025 Romfast SRL - -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 Affero General Public License is a free, copyleft license for -software and other kinds of works, specifically designed to ensure -cooperation with the community in the case of network server software. - - The licenses for most software and other practical works are designed -to take away your freedom to share and change the works. By contrast, -our General Public Licenses are 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. - - 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. - - Developers that use our General Public Licenses protect your rights -with two steps: (1) assert copyright on the software, and (2) offer -you this License which gives you legal permission to copy, distribute -and/or modify the software. - - A secondary benefit of defending all users' freedom is that -improvements made in alternate versions of the program, if they -receive widespread use, become available for other developers to -incorporate. Many developers of free software are heartened and -encouraged by the resulting cooperation. However, in the case of -software used on network servers, this result may fail to come about. -The GNU General Public License permits making a modified version and -letting the public access it on a server without ever releasing its -source code to the public. - - The GNU Affero General Public License is designed specifically to -ensure that, in such cases, the modified source code becomes available -to the community. It requires the operator of a network server to -provide the source code of the modified version running there to the -users of that server. Therefore, public use of a modified version, on -a publicly accessible server, gives the public access to the source -code of the modified version. - - An older license, called the Affero General Public License and -published by Affero, was designed to accomplish similar goals. This is -a different license, not a version of the Affero GPL, but Affero has -released a new version of the Affero GPL which permits relicensing under -this license. - - 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 Affero 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. Remote Network Interaction; Use with the GNU General Public License. - - Notwithstanding any other provision of this License, if you modify the -Program, your modified version must prominently offer all users -interacting with it remotely through a computer network (if your version -supports such interaction) an opportunity to receive the Corresponding -Source of your version by providing access to the Corresponding Source -from a network server at no charge, through some standard or customary -means of facilitating copying of software. This Corresponding Source -shall include the Corresponding Source for any work covered by version 3 -of the GNU General Public License that is incorporated pursuant to the -following paragraph. - - 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 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 work with which it is combined will remain governed by version -3 of the GNU General Public License. - - 14. Revised Versions of this License. - - The Free Software Foundation may publish revised and/or new versions of -the GNU Affero 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 Affero 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 Affero 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 Affero 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 - - How to Apply These Terms to Your New Programs - - If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these terms. - - To do so, attach the following notices to the program. It is safest -to attach them to the start of each source file to most effectively -state the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. - - - Copyright (C) - - 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 . - -Also add information on how to contact you by electronic and paper mail. - - If your software can interact with users remotely through a computer -network, you should also make sure that it provides a way for users to -get its source. For example, if your program is a web application, its -interface could display a "Source" link that leads users to an archive -of the code. There are many ways you could offer source, and different -solutions will be better for different programs; see section 13 for the -specific requirements. - - You should also get your employer (if you work as a programmer) or school, -if any, to sign a "copyright disclaimer" for the program, if necessary. -For more information on this, and how to apply and follow the GNU AGPL, see -. + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + +Copyright (C) 2025 Romfast SRL + +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 Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are 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. + + 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. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + 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 Affero 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. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + 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 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 work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero 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 Affero 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 Affero 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 Affero 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 + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + 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 . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. diff --git a/efactura-generator/README.md b/efactura-generator/README.md index 5abf75f..67382d2 100644 --- a/efactura-generator/README.md +++ b/efactura-generator/README.md @@ -1,61 +1,172 @@ -# eFactura Editor / Editor Facturi Electronice - -## Demo -https://romfast.github.io/efactura-generator/ - -## Overview / Prezentare -Romanian electronic invoice (eFactura) editor - loads XML, allows editing, printing invoices and generates new XML files. - -Editor pentru facturi electronice (eFactura) - încarcă fișiere XML, permite editarea, printarea facturilor și generează fișiere XML noi. - -![Factura 1](screenshot1.jpg) -![Factura 2](screenshot2.jpg) -![Factura 3](screenshot3.jpg) - -## Installation & Usage / Instalare & Utilizare - -### Option 1: Web Server / Opțiunea 1: Server Web -- Copy all project files to your web server maintaining the directory structure. -- Access through your web server URL. -- Use "Printează" button to print the invoice. - -- Copiați toate fișierele pe server păstrând structura directoarelor. -- Accesați prin URL-ul serverului. -- Folosiți butonul "Printează" pentru a printa factura. - -### Option 2: Local Development / Opțiunea 2: Dezvoltare Locală -1. Install Node.js / Instalați Node.js -2. Clone/download repository / Clonați/descărcați repository-ul -3. Run / Rulați: `node server.js` -4. Open / Deschideți: http://localhost:3000 - -## Project Structure / Structura Proiect -``` -project/ -├── index.html -├── styles/ -│ ├── main.css -├── js/ -│ ├── script.js -│ ├── formatter.js -│ └── print.js -├── templates/ -│ └── print.html -└── server.js -``` - -## License / Licență -AGPL-3.0-or-later - -If you use this software, even as a web service, you must: -1. Give credit to the original project -2. Share all your modifications -3. Use the same AGPL-3 license - -Dacă folosiți acest software, chiar și ca serviciu web, trebuie să: -1. Menționați proiectul original -2. Partajați toate modificările făcute -3. Folosiți aceeași licență AGPL-3 - -## Changelog -[Istoric modificări](CHANGELOG.md) \ No newline at end of file +# Editor eFactura + +Editor static în browser pentru fișiere XML eFactura (UBL 2.1) conforme cu standardul ANAF. + +## Demo + +- GitHub Pages: https://romfast.github.io/efactura-generator/ + +## Scop + +Aplicația este intenționat **statică** (HTML + CSS + JavaScript, fără build, fără backend obligatoriu) astfel încât să poată fi găzduită direct pe GitHub Pages sau pe orice server web simplu. + +Cazurile principale de utilizare: +- **Corecții minore** într-un XML eFactura existent (date factură, furnizor, client, articole, TVA). +- **Generare factură storno** pornind de la o factură existentă, prin butonul „Stornează”. + +Nu este un sistem complet de facturare — lucrează cu câte un singur XML, încărcat și salvat local. + +![Factura 1](screenshot1.jpg) +![Factura 2](screenshot2.jpg) +![Factura 3](screenshot3.jpg) + +## Funcționalități + +### Încărcare XML +- Încărcare fișier XML local prin butonul „Alege Fișier XML”. +- Încărcare automată dintr-un fișier temporar prin parametru URL (`index.html?xml=`), folosit împreună cu `receiver.php` pentru integrare cu sisteme externe. + +### Detalii factură +- Număr factură, data emiterii, data scadentă (cu calendar Pikaday și format `dd.mm.yyyy`). +- Monedă document și monedă TVA opțională (cu generarea automată a celui de-al doilea tag `TaxTotal` când monedele diferă). +- Câmp text adițional (notă) cu limită de 900 de caractere și împărțire automată. + +### Furnizor și client +- Nume, cod TVA, număr de înregistrare, adresă, oraș, județ, țară, telefon, persoană de contact, email. +- Selector de țară conform ISO 3166-1 și selector de județ conform codurilor `RO-XX`. +- Tratare specială pentru București și pentru furnizori neplătitori de TVA (cod fiscal fără atributul `RO`). + +### Articole factură +- Adăugare, editare și ștergere articole. +- Cantitate, preț, cotă TVA, unitate de măsură (EA, XPP, H87, KGM, MTR, LTR, MTQ). +- Reduceri pe linie cu cod și motiv reducere. +- Coduri de identificare articole multiple per linie: cod vânzător, cod cumpărător, cod de bare, CPV, NC8, cod vamal. + +### TVA +- Tipuri TVA suportate: `S` (Standard), `AE` (Taxare Inversă), `O` (Neplătitor TVA), `Z` (Cotă 0%), `E` (Neimpozabil). +- Coduri de scutire `VATEX-EU-*` cu motiv corespunzător, completate automat în funcție de tipul TVA și editabile manual. +- Defalcare TVA cu mai multe cote, editabilă inline (bază impozabilă și valoare TVA). + +### Reduceri și taxe la nivel de factură +- Adăugare/editare/ștergere reduceri și taxe suplimentare la nivel de document. +- Coduri motiv pentru reduceri (95, 41, 42, 60, 62 etc.) și pentru taxe (TV, FC, ZZZ). + +### Totaluri +- Recalculare automată: subtotal, total reduceri, total taxe, valoare netă, TVA, total. +- Editare inline a totalurilor (click pe valoare) cu păstrarea totalurilor originale din XML-ul încărcat. +- Buton „Recalculează Totaluri” pentru regenerare din articole. + +### Storno +- Buton „Stornează” care convertește factura curentă într-o factură storno (cantități și valori negative) gata de salvat. + +### Salvare și printare +- Buton „Salvează XML” care generează și descarcă un XML UBL conform. +- Vizualizare printabilă în două formate (standard și compact) prin șabloanele din `templates/`. + +### Formatare +- Numere, cantități și sume formatate conform locale-ului browserului, cu conversie automată la punct decimal pentru XML. + +## Instalare și utilizare + +### Opțiunea 1: Server web static +Copiați toate fișierele pe serverul web păstrând structura directoarelor și accesați `index.html` prin URL-ul serverului. Funcționează pe orice server static (Apache, nginx, GitHub Pages etc.). + +### Opțiunea 2: Dezvoltare locală cu Node.js +```bash +node js/server.js +``` +Apoi deschideți http://localhost:3000 + +Serverul Node este minimal (fără dependințe) și servește doar fișierele statice. + +### Opțiunea 3: Script `start.sh` (Node + PHP simultan) + +Pentru testare vizuală manuală cu receiver-ul PHP activ: + +```bash +./start.sh +``` + +Pornește în paralel: +- Node static server pe `http://localhost:3000` (frontend, fără PHP — simulează GitHub Pages) +- PHP built-in server pe `http://localhost:8000` (servește atât frontend cât și `receiver.php`, `test-config.php`) + +**Pentru testarea funcționalităților ANAF (lookup CIF, validare, PDF) deschideți `http://localhost:8000/` — frontend-ul apelează `./receiver.php` pe același origin, deci pe `:3000` (Node nu execută PHP) veți primi 404.** Folosiți `:3000` doar pentru a verifica comportamentul fără PHP. + +Dacă porturile sunt deja ocupate, scriptul oprește procesele existente înainte de pornire. `Ctrl+C` oprește ambele servere. Loguri în `logs/dev-node.log` și `logs/dev-php.log`. Override porturi: `NODE_PORT=4000 PHP_PORT=9000 ./start.sh`. + +### Opțiunea 4: Docker (local sau CI) + +Imaginea include Apache + PHP 8.2 cu `mod_rewrite` și `mod_headers`, identic cu un hosting de producție. + +**Build și run:** +```bash +docker build -t efactura-generator . +docker run --rm -p 8080:80 efactura-generator +``` +Apoi deschideți http://localhost:8080 + +**Cu PHP receiver activ** (opțional — doar dacă integrați cu un sistem extern): +```bash +# Copiați și editați config.json înainte de build +cp config.json config.local.json +# Editați config.local.json: api_key, allowed_ips + +docker build -t efactura-generator . +docker run --rm -p 8080:80 \ + -v "$(pwd)/config.local.json:/var/www/html/config.json:ro" \ + efactura-generator +``` + +**Testare rapidă (fără PHP):** +```bash +# Nici o dependință — server Node minimal, doar fișiere statice +node js/server.js # http://localhost:3000 +``` + +> **Notă:** Directorul `temp/` din container este volatil — se șterge la `docker run --rm`. Dacă vreți persistență, montați un volum: `-v "$(pwd)/temp:/var/www/html/temp"`. + +### Opțiunea 5: Integrare cu sistem extern (PHP) +Pentru a primi un XML dintr-o aplicație externă și a-l deschide direct în editor: +1. Configurați `config.json` (cheie API, IP-uri permise, durată de viață fișiere temporare). +2. Sistemul extern face POST cu conținutul XML către `receiver.php`, transmițând antetul `X-Api-Key`. +3. `receiver.php` validează XML-ul și namespace-urile UBL, salvează în `temp/` și returnează numele fișierului. +4. Utilizatorul este redirecționat către `index.html?xml=`, care încarcă XML-ul automat. +5. Fișierul temporar este șters după încărcare; fișierele mai vechi decât `temp_file_lifetime` ore sunt curățate automat. + +Pentru diagnosticare există pagina `test-config.php`. + +## Structura proiectului + +``` +efactura-generator/ +├── index.html # Pagina principală +├── styles/main.css +├── js/ +│ ├── script.js # Logica completă: parsare/generare XML, formular, totaluri +│ ├── formatter.js # Formatare numere/sume/cantități +│ ├── print.js # Generare vizualizare printabilă +│ └── server.js # Server static minimal pentru dezvoltare locală +├── templates/ +│ ├── print.html # Șablon printare standard +│ └── print-compact.html # Șablon printare compact +├── receiver.php # Endpoint opțional pentru încărcare XML din sisteme externe +├── test-config.php # Pagina de diagnosticare pentru receiver +├── config.json # Configurație receiver (API key, IP-uri permise) +└── .htaccess.template # Configurație Apache pentru hosting în producție +``` + +## Licență + +[AGPL-3.0-or-later](LICENSE.md) + +Dacă folosiți acest software, chiar și ca serviciu web, trebuie să: +1. Menționați proiectul original. +2. Partajați toate modificările făcute. +3. Folosiți aceeași licență AGPL-3. + +## Linkuri + +- [Istoric modificări](CHANGELOG.md) +- [De făcut](TODO.md) +- [www.romfast.ro](https://www.romfast.ro) diff --git a/efactura-generator/index.html b/efactura-generator/index.html index 4eedc24..9281fa4 100644 --- a/efactura-generator/index.html +++ b/efactura-generator/index.html @@ -1,242 +1,294 @@ - - - - - - Editor Factură Electronică - - - - - - -
-
-
-

Editor eFactura

-
Romfast SRL
-
-
- - - - -
-
- -
- -
- -
-

Detalii Factură

-
-
- - -
-
- -
- - -
-
-
- -
- - -
-
-
- - -
-
- - -
-
-
- - -
-

Detalii Furnizor

-
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
-
- - -
-

Detalii Client

-
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
-
-
- - -
-

Text Adițional

-
- -
0/900 caractere
-
-
- - -
-

- Reduceri și Taxe Suplimentare - -

-
- - -
-

- Articole Factură - -

-
- - -
-
- Subtotal: - 0.00 -
-
- Total Reduceri: - 0.00 -
-
- Total Taxe: - 0.00 -
-
- Valoare Netă: - 0.00 -
- - -
-

Defalcare TVA

-
- -
-
- Total TVA: - 0.00 -
-
-
- -
- Total: - 0.00 -
-
- -
-
-
- - -
- - - - - - - + + + + + + Editor Factură Electronică + + + + + + + +
+
+
+

Editor eFactura

+
Romfast SRL
+
+
+ + + + + + + +
+
+ +
+ +
+ +
+

Detalii Factură

+
+
+ + +
+
+ + +
+
+ +
+ + +
+
+
+ +
+ + +
+
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ + +
+
+
+
+ + +
+

+ Detalii Furnizor + + + + + +

+
+
+ + +
+
+ +
+ + +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+

Detalii Client

+
+
+ + +
+
+ +
+ + +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+
+ + +
+

Text Adițional

+
+ +
0/900 caractere
+
+
+ + +
+

+ Modalități de Plată + +

+
+
+ + +
+

+ Reduceri și Taxe Suplimentare + +

+
+ + +
+

+ Articole Factură + +

+
+ + +
+
+ Subtotal: + 0.00 +
+
+ Total Reduceri: + 0.00 +
+
+ Total Taxe: + 0.00 +
+
+ Valoare Netă: + 0.00 +
+ + +
+

Defalcare TVA

+
+ +
+
+ Total TVA: + 0.00 +
+
+
+ +
+ Total: + + 0.00 + + +
+
+ +
+
+
+ + +
+ + + + + + + diff --git a/efactura-generator/js/anaf.js b/efactura-generator/js/anaf.js new file mode 100644 index 0000000..1c348f7 --- /dev/null +++ b/efactura-generator/js/anaf.js @@ -0,0 +1,129 @@ +/** + * js/anaf.js — Proxy ANAF APIs prin receiver.php + * + * Toate apelurile merg prin receiver.php (CORS proxy server-side). + * Pe hosting static (GitHub Pages, fără PHP), apelurile vor eșua cu eroare + * "receiver indisponibil" — verificați cu probeReceiver() la inițializare. + * + * Configurare necesară în config.json (server-side): + * "anaf_token": "" — necesar pentru validate + pdf + * + * Endpoints ANAF (proxied): + * Validate : POST https://api.anaf.ro/prod/FCTEL/rest/validare/FACT1 + * PDF/HTML : POST https://api.anaf.ro/prod/FCTEL/rest/transformare/FACT1/DA + * CIF info : POST https://webservicesp.anaf.ro/api/PlatitorTvaRest/v9/tva + */ + +const RECEIVER = './receiver.php'; + +/** + * Verifică dacă receiver.php este disponibil pe server. + * Returnează true dacă poate răspunde la ?action=ping. + * @returns {Promise} + */ +export async function probeReceiver() { + try { + const res = await fetch(`${RECEIVER}?action=ping`, { method: 'GET' }); + if (!res.ok) return false; + const json = await res.json().catch(() => null); + return json?.pong === true; + } catch { + return false; + } +} + +/** + * Validează un XML eFactura prin API-ul ANAF (necesită Bearer token în config.json). + * @param {string} xmlContent - XML ca string UTF-8 + * @returns {Promise<{valid: boolean, messages: Array<{message:string, severity:string, xpathLocation?:string}>}>} + */ +export async function anafValidate(xmlContent) { + let res; + try { + res = await fetch(`${RECEIVER}?action=validate`, { + method: 'POST', + headers: { 'Content-Type': 'text/plain; charset=utf-8' }, + body: xmlContent + }); + } catch (e) { + throw new Error('Receiver.php indisponibil — ' + e.message); + } + if (!res.ok) { + let msg = `ANAF validare: HTTP ${res.status}`; + try { const t = await res.text(); if (t) msg += ' — ' + t.slice(0, 200); } catch { /* ok */ } + throw new Error(msg); + } + const data = await res.json(); + // Normalizare răspuns ANAF: { Messages: [{message, severity, xpathLocation}] } + const messages = (data.Messages || data.messages || []).map(m => ({ + message: m.message || m.Message || String(m), + severity: (m.severity || m.Severity || 'ERROR').toUpperCase(), + xpathLocation: m.xpathLocation || m.XpathLocation || '' + })); + const valid = messages.filter(m => m.severity === 'ERROR' || m.severity === 'FATAL').length === 0; + return { valid, messages }; +} + +/** + * Obține vizualizarea ANAF a facturii (ZIP cu HTML). + * Notă: ANAF /transformare returnează ZIP+HTML, nu PDF direct. + * PDF-ul real este generat client-side prin PR-PDF / html2pdf.js. + * @param {string} xmlContent - XML ca string UTF-8 + * @returns {Promise} ZIP blob + */ +export async function anafPdf(xmlContent) { + let res; + try { + res = await fetch(`${RECEIVER}?action=pdf`, { + method: 'POST', + headers: { 'Content-Type': 'text/plain; charset=utf-8' }, + body: xmlContent + }); + } catch (e) { + throw new Error('Receiver.php indisponibil — ' + e.message); + } + if (!res.ok) { + throw new Error(`ANAF vizualizare: HTTP ${res.status}`); + } + return res.blob(); +} + +/** + * Caută informații contribuabil după CIF prin ANAF. + * Folosește API-ul sincron ANAF v9 (webservicesp.anaf.ro/api/PlatitorTvaRest/v9/tva). + * Nu necesită token OAuth. + * @param {string|number} cif - CIF/CUI (cu sau fără prefix RO) + * @returns {Promise<{ + * found: boolean, + * denumire?: string, + * adresa?: string, + * nrRegCom?: string, + * cui?: number, + * tvaActiv?: boolean, + * strada?: string, + * oras?: string, + * judetCod?: string, + * codPostal?: string, + * telefon?: string, + * statusEFactura?: boolean + * }>} + * @property {string} strada - Strada + număr din adresa_sediu_social ANAF + * @property {string} oras - Localitatea (fără prefix MUN./ORS./COM.) + * @property {string} judetCod - Cod județ ISO format RO-XX (ex: RO-B, RO-CJ) + * @property {string} codPostal - Cod poștal + * @property {string} telefon - Număr telefon din date_generale ANAF + * @property {boolean} statusEFactura - Înregistrat în sistemul eFactura + */ +export async function anafCifLookup(cif) { + const cifNum = String(cif).replace(/^RO\s*/i, '').trim(); + let res; + try { + res = await fetch(`${RECEIVER}?action=cif&cif=${encodeURIComponent(cifNum)}`); + } catch (e) { + throw new Error('Receiver.php indisponibil — ' + e.message); + } + if (!res.ok) { + throw new Error(`ANAF CIF lookup: HTTP ${res.status}`); + } + return res.json(); +} diff --git a/efactura-generator/js/catalog.js b/efactura-generator/js/catalog.js new file mode 100644 index 0000000..eb8aa69 --- /dev/null +++ b/efactura-generator/js/catalog.js @@ -0,0 +1,129 @@ +/** + * js/catalog.js — Catalog produse/servicii în IndexedDB (PR-A13) + * + * Folosește `openCatalog()` din storage.js (DB `efactura` v1, store `products`, + * indexes: `name`, `sellerItemID`, `cpvCode`). + * + * Schema produs (v1): + * id: string (UUID v4) + * name: string — denumire produs/serviciu (indexed, searched by prefix) + * unit: string — cod UM (EA, KGM, etc.) + * price: string — preț unitar canonical decimal + * vatType: string — cod tip TVA (S, AE, O, Z, E) + * vatRate: string — cotă TVA (19, 9, 5, 0) + * description: string — descriere detaliată (opțional) + * sellerItemID: string — cod articol furnizor (opțional, indexed) + * cpvCode: string — cod CPV (opțional, indexed) + */ + +import { openCatalog } from './storage.js'; + +/** + * Adaugă sau actualizează un produs în catalog. + * Dacă `product.id` lipsește, generează UUID nou. + * @param {Object} product + * @returns {Promise} ID-ul produsului salvat + */ +export async function catalogAdd(product) { + const db = await openCatalog(); + const entry = { + id: product.id || _uuid(), + name: (product.name || '').trim(), + unit: (product.unit || 'EA').trim(), + price: (product.price || '0').trim(), + vatType: (product.vatType || 'S').trim(), + vatRate: (product.vatRate || '19').trim(), + description: (product.description || '').trim(), + sellerItemID: (product.sellerItemID || '').trim(), + cpvCode: (product.cpvCode || '').trim(), + }; + return new Promise((resolve, reject) => { + const tx = db.transaction('products', 'readwrite'); + const store = tx.objectStore('products'); + const req = store.put(entry); + req.onsuccess = () => resolve(entry.id); + req.onerror = () => reject(req.error); + }); +} + +/** + * Caută produse după prefix de denumire (case-insensitive prefix match). + * Returnează max `limit` rezultate, sortate alfabetic. + * @param {string} prefix + * @param {number} limit + * @returns {Promise} + */ +export async function catalogSearch(prefix, limit = 8) { + if (!prefix || !prefix.trim()) return []; + const db = await openCatalog(); + const lower = prefix.trim().toLowerCase(); + const results = []; + + return new Promise((resolve, reject) => { + const tx = db.transaction('products', 'readonly'); + const store = tx.objectStore('products'); + const index = store.index('name'); + + // Interval IDB: [lower, lower + '￿') pentru prefix match + const range = IDBKeyRange.bound(lower, lower + '￿', false, false); + + // Scanăm cu cursor pe index name (lowercase nu e direct în IDB — + // folosim open cursor pe tot și filtrăm client-side pentru robustețe) + const allReq = index.openCursor(); + allReq.onsuccess = (e) => { + const cursor = e.target.result; + if (!cursor || results.length >= limit) { + resolve(results); + return; + } + const name = (cursor.value.name || '').toLowerCase(); + if (name.startsWith(lower)) { + results.push(cursor.value); + } + cursor.continue(); + }; + allReq.onerror = () => reject(allReq.error); + }); +} + +/** + * Șterge un produs din catalog după ID. + * @param {string} id + * @returns {Promise} + */ +export async function catalogDelete(id) { + const db = await openCatalog(); + return new Promise((resolve, reject) => { + const tx = db.transaction('products', 'readwrite'); + const store = tx.objectStore('products'); + const req = store.delete(id); + req.onsuccess = () => resolve(); + req.onerror = () => reject(req.error); + }); +} + +/** + * Listează toate produsele (pentru management catalog). + * @returns {Promise} + */ +export async function catalogList() { + const db = await openCatalog(); + return new Promise((resolve, reject) => { + const tx = db.transaction('products', 'readonly'); + const store = tx.objectStore('products'); + const req = store.getAll(); + req.onsuccess = () => resolve(req.result || []); + req.onerror = () => reject(req.error); + }); +} + +/** Generează un UUID v4 simplu (crypto.randomUUID dacă disponibil, fallback manual). */ +function _uuid() { + if (typeof crypto !== 'undefined' && crypto.randomUUID) { + return crypto.randomUUID(); + } + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => { + const r = (Math.random() * 16) | 0; + return (c === 'x' ? r : (r & 0x3) | 0x8).toString(16); + }); +} diff --git a/efactura-generator/js/formatter.js b/efactura-generator/js/formatter.js index bd15bc7..7916214 100644 --- a/efactura-generator/js/formatter.js +++ b/efactura-generator/js/formatter.js @@ -1,71 +1,71 @@ -export class InvoiceFormatter { - constructor() { - this.locale = navigator.language; - - this.currencyFormatter = new Intl.NumberFormat(this.locale, { - minimumFractionDigits: 2, - maximumFractionDigits: 2, - useGrouping: true - }); - - this.quantityFormatter = new Intl.NumberFormat(this.locale, { - minimumFractionDigits: 3, - maximumFractionDigits: 3, - useGrouping: true - }); - - this.numberFormatter = new Intl.NumberFormat(this.locale, { - minimumFractionDigits: 4, - maximumFractionDigits: 4, - useGrouping: true - }); - } - - formatCurrency(value) { - const numValue = parseFloat(value); - return isNaN(numValue) ? '0,00' : this.currencyFormatter.format(numValue); - } - - formatQuantity(value) { - const numValue = parseFloat(value); - return isNaN(numValue) ? '0,000' : this.quantityFormatter.format(numValue); - } - - formatNumber(value) { - const numValue = parseFloat(value); - return isNaN(numValue) ? '0,0000' : this.numberFormatter.format(numValue); - } - - parseCurrency(value) { - if (typeof value !== 'string') { - value = value.toString(); - } - // Remove all non-digit characters except decimal and minus - const normalized = value.replace(/[^\d\-.,]/g, '') - // Replace thousands separator - .replace(/[.,](?=.*[.,])/g, '') - // Last dot/comma is decimal separator - .replace(/[.,]/, '.'); - return parseFloat(normalized) || 0; - } - - parseQuantity(value) { - if (typeof value !== 'string') { - value = value.toString(); - } - const normalized = value.replace(/[^\d\-.,]/g, '') - .replace(/[.,](?=.*[.,])/g, '') - .replace(/[.,]/, '.'); - return parseFloat(normalized) || 0; - } - - parseNumber(value) { - if (typeof value !== 'string') { - value = value.toString(); - } - const normalized = value.replace(/[^\d\-.,]/g, '') - .replace(/[.,](?=.*[.,])/g, '') - .replace(/[.,]/, '.'); - return parseFloat(normalized) || 0; - } -} \ No newline at end of file +// js/formatter.js +// +// Compatibility-layer formatter folosit de print template + script.js +// pentru afișare. Internal delegate la js/numeric.js (PR-E E1+E3+E4). +// +// E2: locale hardcoded "ro-RO" (înlocuit `navigator.language`). Audiența +// țintă e RO; print PDF / display formular trebuie să fie consistent +// între browsere și OS-uri. + +import { RO_LOCALE, parseStrict, parseStrictOr, format2, format3, format4 } from './numeric.js'; + +export class InvoiceFormatter { + constructor() { + this.locale = RO_LOCALE; + + this.currencyFormatter = new Intl.NumberFormat(this.locale, { + minimumFractionDigits: 2, + maximumFractionDigits: 2, + useGrouping: true + }); + + this.quantityFormatter = new Intl.NumberFormat(this.locale, { + minimumFractionDigits: 3, + maximumFractionDigits: 3, + useGrouping: true + }); + + this.numberFormatter = new Intl.NumberFormat(this.locale, { + minimumFractionDigits: 4, + maximumFractionDigits: 4, + useGrouping: true + }); + } + + formatCurrency(value) { + const big = (value === '' || value === null || value === undefined) + ? null + : parseStrict(value); + return big === null ? '0,00' : format2(big); + } + + formatQuantity(value) { + const big = (value === '' || value === null || value === undefined) + ? null + : parseStrict(value); + return big === null ? '0,000' : format3(big); + } + + formatNumber(value) { + const big = (value === '' || value === null || value === undefined) + ? null + : parseStrict(value); + return big === null ? '0,0000' : format4(big); + } + + /** + * Strict-but-pragmatic parsing → number (pentru consumatorii vechi). + * Pentru cod nou, preferă `parseStrict` din numeric.js (returnează Big). + */ + parseCurrency(value) { + return Number(parseStrictOr(value, '0').toString()); + } + + parseQuantity(value) { + return Number(parseStrictOr(value, '0').toString()); + } + + parseNumber(value) { + return Number(parseStrictOr(value, '0').toString()); + } +} diff --git a/efactura-generator/js/numeric.js b/efactura-generator/js/numeric.js new file mode 100644 index 0000000..1bc59da --- /dev/null +++ b/efactura-generator/js/numeric.js @@ -0,0 +1,273 @@ +// js/numeric.js +// +// Numeric pipeline canonică pentru editor eFactura (PR-E / Track 1). +// +// Trei reguli fundamentale: +// +// 1. `input.dataset.raw` este unica sursă de adevăr numerică (canonical +// decimal-dot string). `input.value` este display-only — locale +// "ro-RO" cu virgulă decimală. +// +// 2. Toate calculele folosesc Big.js (precizie arbitrară), niciodată +// Number. Rounding mode: HALF_UP (standard fiscal RO). +// +// 3. Parserul este strict-but-pragmatic: acceptă atât canonicul XML +// ("1234.56") cât și displayul RO ("1234,56" / "1.234,56"). Refuză +// formele EN ambigue ("1,234.56"). +// +// Prefix module exports: +// - `Big` re-export pentru consumeri (single source of truth pentru +// pin-ul vendored). +// - `parseStrict(value)` → Big | null. null pentru NaN / empty / format +// ambiguu. +// - `parseStrictOr(value, fallback)` → Big. Fallback la "0" dacă invalid. +// - `format2`, `format3`, `format4` → string ro-RO display cu zecimale fix. +// - `formatRaw(big, decimals)` → string canonical decimal-dot pentru XML. +// - `setRaw(input, value)` → setează dataset.raw + input.value formatted. +// - `getRaw(input)` → Big citit din dataset.raw, fallback la parseStrict +// pe input.value. +// - `lineTotal(qty, price, discount, vatRate)` → { net, vat, gross } cu Big. + +import Big from './vendor/big.mjs'; + +// HALF_UP = 1 în big.js. (HALF_EVEN = 2, HALF_DOWN = 3 — vezi big.mjs). +Big.RM = 1; +// Default decimal places pentru division (suficient pentru calcul intermediar). +Big.DP = 20; + +export { Big }; + +// Locale hardcoded pentru proiectul RO. NU folosim navigator.language — +// vezi DESIGN.md / E2. +export const RO_LOCALE = 'ro-RO'; + +const _displayFmt = { + 2: new Intl.NumberFormat(RO_LOCALE, { minimumFractionDigits: 2, maximumFractionDigits: 2, useGrouping: true }), + 3: new Intl.NumberFormat(RO_LOCALE, { minimumFractionDigits: 3, maximumFractionDigits: 3, useGrouping: true }), + 4: new Intl.NumberFormat(RO_LOCALE, { minimumFractionDigits: 4, maximumFractionDigits: 4, useGrouping: true }), +}; + +/** + * Parser strict-but-pragmatic. + * + * Acceptă: + * - canonical XML / număr cu zecimală pe punct: "1234.56", "0.001" + * - RO display cu zecimală pe virgulă: "1234,56", "1.234,56", "1.234.567,89" + * - integer: "0", "-12", " 42 " + * - Big sau Number: returnate direct (Number → Big via toString). + * + * Refuză (returnează null): + * - empty string / null / undefined + * - NaN (după ce s-a încercat normalizarea) + * - format EN cu thousands separator pe virgulă: "1,234.56" (ambiguu pentru RO) + * - alte caractere non-numerice: "abc", "1.2.3" cu mai multe puncte și fără virgulă + * + * @param {string|number|Big|null|undefined} value + * @returns {Big|null} + */ +export function parseStrict(value) { + if (value === null || value === undefined) return null; + if (value instanceof Big) return value; + if (typeof value === 'number') { + if (!Number.isFinite(value)) return null; + return new Big(value.toString()); + } + if (typeof value !== 'string') return null; + + let s = value.trim(); + if (s === '') return null; + + // Optional leading minus. + let sign = ''; + if (s.startsWith('-')) { sign = '-'; s = s.slice(1); } + else if (s.startsWith('+')) { s = s.slice(1); } + if (s === '') return null; + + const dotCount = (s.match(/\./g) || []).length; + const commaCount = (s.match(/,/g) || []).length; + + let canonical; + if (commaCount === 0 && dotCount === 0) { + // integer + if (!/^\d+$/.test(s)) return null; + canonical = s; + } else if (commaCount === 0 && dotCount === 1) { + // canonical decimal-dot: "1234.56" + if (!/^\d+\.\d+$/.test(s)) return null; + canonical = s; + } else if (commaCount === 0 && dotCount > 1) { + // ambigu: "1.2.3" — refuz + return null; + } else if (commaCount === 1) { + // RO: virgula = decimală; punctele = thousands. + // Forma așteptată: cifre[.cifre[.cifre]]*,cifre+ + if (!/^\d{1,3}(?:\.\d{3})*,\d+$/.test(s) && !/^\d+,\d+$/.test(s)) { + return null; + } + canonical = s.replace(/\./g, '').replace(',', '.'); + } else { + // commaCount > 1 — nu e RO valid. Refuz (ar putea fi EN "1,234,567.89" + // dar asta e ambiguu pentru audiența RO). + return null; + } + + try { + return new Big(sign + canonical); + } catch (_) { + return null; + } +} + +/** + * Variantă "or fallback" pentru cazurile unde un fallback la zero e + * acceptabil (display, sumare). NU folosi pentru validare. + * + * @param {*} value + * @param {string|number|Big} fallback + * @returns {Big} + */ +export function parseStrictOr(value, fallback = '0') { + const parsed = parseStrict(value); + if (parsed !== null) return parsed; + if (fallback instanceof Big) return fallback; + return new Big(fallback); +} + +/** Format Big → string display ro-RO cu N zecimale fixe. */ +function _format(value, decimals) { + const big = (value instanceof Big) ? value : parseStrictOr(value); + const fmt = _displayFmt[decimals] || _displayFmt[2]; + // Big.toFixed(decimals) → canonical decimal-dot. Convert la Number + // doar pentru Intl format (number passes through cu precizie suficientă + // pentru valori fiscale practice). + return fmt.format(Number(big.toFixed(decimals))); +} + +export function format2(value) { return _format(value, 2); } +export function format3(value) { return _format(value, 3); } +export function format4(value) { return _format(value, 4); } + +/** + * Format pentru ieșirea XML: canonical decimal-dot, fix N zecimale, + * fără thousands separator. Folosit la serializare UBL. + * + * @param {*} value + * @param {number} decimals + * @returns {string} + */ +export function formatRaw(value, decimals = 2) { + const big = (value instanceof Big) ? value : parseStrictOr(value); + return big.toFixed(decimals); +} + +/** + * Setează valoarea unui input numeric: + * - `dataset.raw` ← canonical decimal-dot (sursa de adevăr) + * - `input.value` ← display ro-RO cu N zecimale + * + * Folosit la populare din XML și la commit-ul user-editat (post-blur). + * + * @param {HTMLInputElement} input + * @param {*} value Big | string | number + * @param {number} decimals decimale display (2 = currency, 3 = qty, 4 = price) + */ +export function setRaw(input, value, decimals = 2) { + const big = (value instanceof Big) ? value : parseStrictOr(value); + input.dataset.raw = big.toFixed(decimals); + // type="number" acceptă doar punct decimal; type="text" primește display ro-RO + input.value = (input.type === 'number') ? big.toFixed(decimals) : _format(big, decimals); +} + +/** + * Citește valoarea numerică canonică a unui input. + * - Preferă `dataset.raw` (set de noi pe populate / blur). + * - Fallback la `parseStrict(input.value)` dacă raw absent. + * - Fallback final la Big("0"). + * + * @param {HTMLInputElement} input + * @returns {Big} + */ +export function getRaw(input) { + if (!input) return new Big('0'); + if (input.dataset && input.dataset.raw !== undefined && input.dataset.raw !== '') { + const parsed = parseStrict(input.dataset.raw); + if (parsed !== null) return parsed; + } + return parseStrictOr(input.value, '0'); +} + +/** + * Marchează un input ca dirty (editat de user). PR-A11 va folosi acest + * flag pentru tolerance switching (zero pe row dirty, ±0.01 RON pe row + * loaded). + */ +export function markDirty(input) { + if (input && input.dataset) input.dataset.dirty = '1'; +} + +/** + * Atașează handler-ul de blur care: + * 1. parseStrict pe input.value + * 2. setRaw cu valoarea normalizată (sau lasă raw existent dacă parse eșuează + * și marchează vizual ca invalid). + * 3. markDirty. + * + * @param {HTMLInputElement} input + * @param {number} decimals + */ +export function wireDatasetRaw(input, decimals = 2) { + if (!input || input.dataset.rawWired === '1') return; + input.addEventListener('blur', () => { + const parsed = parseStrict(input.value); + if (parsed === null && input.value.trim() !== '') { + input.classList.add('invalid'); + return; + } + input.classList.remove('invalid'); + if (parsed !== null) { + setRaw(input, parsed, decimals); + markDirty(input); + } + }); + // La input change, marchează dirty (dar nu reformatează — lasă user să tasteze). + input.addEventListener('input', () => markDirty(input)); + input.dataset.rawWired = '1'; +} + +/** + * Calculează totalul pe linia de factură. + * + * net = (qty * price) - lineDiscount + * vat = round2(net * vatRate / 100) + * gross = net + vat + * + * @param {*} qty + * @param {*} price + * @param {*} discount + * @param {*} vatRate procent (ex. 19 pentru 19%) + * @returns {{net: Big, vat: Big, gross: Big}} + */ +export function lineTotal(qty, price, discount, vatRate) { + const q = parseStrictOr(qty, '0'); + const p = parseStrictOr(price, '0'); + const d = parseStrictOr(discount, '0'); + const r = parseStrictOr(vatRate, '0'); + + const gross = q.times(p); + const net = gross.minus(d); + const vat = net.times(r).div(100).round(2, 1); // HALF_UP + const total = net.plus(vat); + + return { net, vat, gross: total }; +} + +/** + * Helper: a.eq(b) cu toleranță. Returnează true dacă |a - b| ≤ epsilon. + * Pentru A11 reconciliation legacy: ±0.01 RON. + */ +export function withinTolerance(a, b, epsilon) { + const aB = (a instanceof Big) ? a : parseStrictOr(a); + const bB = (b instanceof Big) ? b : parseStrictOr(b); + const eB = (epsilon instanceof Big) ? epsilon : parseStrictOr(epsilon); + return aB.minus(bB).abs().lte(eB); +} diff --git a/efactura-generator/js/print.js b/efactura-generator/js/print.js index c33e8b2..88c916d 100644 --- a/efactura-generator/js/print.js +++ b/efactura-generator/js/print.js @@ -1,251 +1,251 @@ -import { InvoiceFormatter } from './formatter.js'; - -export class InvoicePrintHandler { - constructor() { - this.printWindow = null; - this.formatter = new InvoiceFormatter(); - this.templates = { - standard: './templates/print.html', - compact: './templates/print-compact.html' - }; - this.currentTemplate = 'standard'; - } - - setTemplate(templateName) { - if (this.templates[templateName]) { - this.currentTemplate = templateName; - } - } - - collectInvoiceData() { - return { - // Basic details - invoiceNumber: document.querySelector('[name="invoiceNumber"]').value, - issueDate: document.querySelector('[name="issueDate"]').value, - dueDate: document.querySelector('[name="dueDate"]').value, - documentCurrencyCode: document.querySelector('[name="documentCurrencyCode"]').value.toUpperCase() || 'RON', - taxCurrencyCode: document.querySelector('[name="taxCurrencyCode"]').value.toUpperCase(), - exchangeRate: parseFloat(document.querySelector('[name="exchangeRate"]')?.value || 1), - - // Supplier details - supplier: { - name: document.querySelector('[name="supplierName"]').value, - vat: document.querySelector('[name="supplierVAT"]').value, - companyId: document.querySelector('[name="supplierCompanyId"]').value, - address: document.querySelector('[name="supplierAddress"]').value, - city: document.querySelector('[name="supplierCity"]').value, - county: document.querySelector('[name="supplierCountrySubentity"]').value, - country: document.querySelector('[name="supplierCountry"]').value, - phone: document.querySelector('[name="supplierPhone"]').value, - contactName: document.querySelector('[name="supplierContactName"]').value, - email: document.querySelector('[name="supplierEmail"]').value - }, - - // Customer details - customer: { - name: document.querySelector('[name="customerName"]').value, - vat: document.querySelector('[name="customerVAT"]').value, - companyId: document.querySelector('[name="customerCompanyId"]').value, - address: document.querySelector('[name="customerAddress"]').value, - city: document.querySelector('[name="customerCity"]').value, - county: document.querySelector('[name="customerCountrySubentity"]').value, - country: document.querySelector('[name="customerCountry"]').value, - phone: document.querySelector('[name="customerPhone"]').value, - contactName: document.querySelector('[name="customerContactName"]').value, - email: document.querySelector('[name="customerEmail"]').value - }, - - // Line items with formatted values - items: Array.from(document.querySelectorAll('.line-item')).map((item, index) => ({ - number: index + 1, - description: item.querySelector('[name^="description"]').value, - quantity: this.formatter.formatQuantity(item.querySelector('[name^="quantity"]').value), - unit: item.querySelector('[name^="unit"]').value, - price: this.formatter.formatCurrency(item.querySelector('[name^="price"]').value), - vatRate: this.formatter.formatCurrency(item.querySelector('[name^="vatRate"]').value), - totalAmount: this.formatter.formatCurrency( - this.formatter.parseQuantity(item.querySelector('[name^="quantity"]').value) * - this.formatter.parseCurrency(item.querySelector('[name^="price"]').value) - ) - })), - - // Note - note: document.querySelector('[name="invoiceNote"]')?.value, - - // Get totals directly from the display elements - totals: { - subtotal: document.getElementById('subtotal').textContent, - allowances: document.getElementById('totalAllowances').textContent, - charges: document.getElementById('totalCharges').textContent, - netAmount: document.getElementById('netAmount').textContent, - vat: document.getElementById('vat').textContent, - total: document.getElementById('total').textContent - }, - - // VAT Breakdown - vatBreakdown: Array.from(document.querySelectorAll('.vat-row')).map(row => ({ - type: row.querySelector('.vat-type').value, - rate: row.querySelector('.vat-rate').value, - base: row.querySelector('.vat-base').value, - amount: row.querySelector('.vat-amount').value - })) - }; - } - - createPartyHTML(party) { - return ` -

${party.name}

-

CUI: ${party.vat}

-

Nr. Reg. Com.: ${party.companyId}

-

${party.address}

-

${party.city}${party.county ? ', ' + party.county : ''}

-

${party.country}

- ${party.phone ? `

Tel: ${party.phone}

` : ''} - ${party.contactName ? `

Contact: ${party.contactName}

` : ''} - ${party.email ? `

Email: ${party.email}

` : ''} - `; - } - - getVATTypeLabel(type) { - const labels = { - 'S': 'Standard', - 'AE': 'Taxare Inversă', - 'O': 'Neplătitor TVA', - 'Z': 'Cotă 0%', - 'E': 'Scutit' - }; - return labels[type] || type; - } - - async print() { - try { - // Collect all the data - const invoiceData = this.collectInvoiceData(); - - // Open new window and load the selected print template - this.printWindow = window.open( - this.templates[this.currentTemplate], - '_blank', - 'width=800,height=600' - ); - - // Wait for the window to load - await new Promise(resolve => { - this.printWindow.onload = resolve; - }); - - // Generate QR code - const qrData = { - invoiceNumber: invoiceData.invoiceNumber, - issueDate: invoiceData.issueDate, - supplier: invoiceData.supplier.name, - customer: invoiceData.customer.name, - total: this.formatter.parseCurrency(invoiceData.totals.total) - }; - - const qrElement = this.printWindow.document.getElementById('qrcode'); - if (qrElement) { - new this.printWindow.QRCode(qrElement, { - text: JSON.stringify(qrData), - width: 100, - height: 100, - colorDark: "#2563eb", - colorLight: "#ffffff", - correctLevel: this.printWindow.QRCode.CorrectLevel.L - }); - } - - // Populate the template with data - this.populatePrintWindow(invoiceData); - - // Print the window - this.printWindow.print(); - - // Clean up - this.printWindow.onafterprint = () => { - this.printWindow.close(); - this.printWindow = null; - }; - - } catch (error) { - console.error('Print failed:', error); - if (this.printWindow) { - this.printWindow.close(); - this.printWindow = null; - } - alert('A apărut o eroare la printare. Vă rugăm să încercați din nou.'); - } - } - - populatePrintWindow(data) { - if (!this.printWindow) return; - - const doc = this.printWindow.document; - - // Basic details - doc.getElementById('print-invoice-number').textContent = data.invoiceNumber; - doc.getElementById('print-issue-date').textContent = data.issueDate; - doc.getElementById('print-due-date').textContent = data.dueDate; - doc.getElementById('print-document-currency').textContent = data.documentCurrencyCode; - - // Currency information - const taxCurrencyContainer = doc.getElementById('print-tax-currency-container'); - if (data.taxCurrencyCode && data.taxCurrencyCode !== data.documentCurrencyCode) { - taxCurrencyContainer.style.display = 'block'; - doc.getElementById('print-tax-currency').textContent = data.taxCurrencyCode; - doc.getElementById('print-exchange-rate').textContent = this.formatter.formatNumber(data.exchangeRate); - } - - // Party details - doc.getElementById('print-supplier-details').innerHTML = this.createPartyHTML(data.supplier); - doc.getElementById('print-customer-details').innerHTML = this.createPartyHTML(data.customer); - - // Note - if (data.note) { - const noteSection = doc.getElementById('print-note'); - noteSection.style.display = 'block'; - noteSection.querySelector('div').textContent = data.note; - } - - // Line items - use formatted values from data - doc.getElementById('print-items').innerHTML = data.items.map(item => ` - - ${item.number} - ${item.description} - ${item.unit} - ${item.quantity} - ${item.price} - ${item.vatRate}% - ${item.totalAmount} - - `).join(''); - - // Totals - use values directly from display - doc.getElementById('print-subtotal').textContent = data.totals.subtotal; - doc.getElementById('print-allowances').textContent = data.totals.allowances; - doc.getElementById('print-charges').textContent = data.totals.charges; - doc.getElementById('print-net-amount').textContent = data.totals.netAmount; - doc.getElementById('print-total').textContent = data.totals.total; - - // VAT Breakdown - use values directly from display - doc.getElementById('print-vat-breakdown').innerHTML = data.vatBreakdown.map(vat => ` -
${this.getVATTypeLabel(vat.type)}
-
${vat.rate}%
-
${vat.base}
-
${vat.amount}
- `).join(''); - - // VAT totals - doc.getElementById('print-vat-currency-main').textContent = data.documentCurrencyCode; - doc.getElementById('print-vat-main').textContent = data.totals.vat; - - const secondaryVatRow = doc.getElementById('print-vat-secondary'); - if (data.taxCurrencyCode && data.taxCurrencyCode !== data.documentCurrencyCode) { - secondaryVatRow.style.display = 'flex'; - doc.getElementById('print-vat-currency-secondary').textContent = data.taxCurrencyCode; - const vatInTaxCurrency = this.formatter.parseCurrency(data.totals.vat) * data.exchangeRate; - doc.getElementById('print-vat-secondary-amount').textContent = - this.formatter.formatCurrency(vatInTaxCurrency); - } - } +import { InvoiceFormatter } from './formatter.js'; + +export class InvoicePrintHandler { + constructor() { + this.printWindow = null; + this.formatter = new InvoiceFormatter(); + this.templates = { + standard: './templates/print.html', + compact: './templates/print-compact.html' + }; + this.currentTemplate = 'standard'; + } + + setTemplate(templateName) { + if (this.templates[templateName]) { + this.currentTemplate = templateName; + } + } + + collectInvoiceData() { + return { + // Basic details + invoiceNumber: document.querySelector('[name="invoiceNumber"]').value, + issueDate: document.querySelector('[name="issueDate"]').value, + dueDate: document.querySelector('[name="dueDate"]').value, + documentCurrencyCode: document.querySelector('[name="documentCurrencyCode"]').value.toUpperCase() || 'RON', + taxCurrencyCode: document.querySelector('[name="taxCurrencyCode"]').value.toUpperCase(), + exchangeRate: parseFloat(document.querySelector('[name="exchangeRate"]')?.value || 1), + + // Supplier details + supplier: { + name: document.querySelector('[name="supplierName"]').value, + vat: document.querySelector('[name="supplierVAT"]').value, + companyId: document.querySelector('[name="supplierCompanyId"]').value, + address: document.querySelector('[name="supplierAddress"]').value, + city: document.querySelector('[name="supplierCity"]').value, + county: document.querySelector('[name="supplierCountrySubentity"]').value, + country: document.querySelector('[name="supplierCountry"]').value, + phone: document.querySelector('[name="supplierPhone"]').value, + contactName: document.querySelector('[name="supplierContactName"]').value, + email: document.querySelector('[name="supplierEmail"]').value + }, + + // Customer details + customer: { + name: document.querySelector('[name="customerName"]').value, + vat: document.querySelector('[name="customerVAT"]').value, + companyId: document.querySelector('[name="customerCompanyId"]').value, + address: document.querySelector('[name="customerAddress"]').value, + city: document.querySelector('[name="customerCity"]').value, + county: document.querySelector('[name="customerCountrySubentity"]').value, + country: document.querySelector('[name="customerCountry"]').value, + phone: document.querySelector('[name="customerPhone"]').value, + contactName: document.querySelector('[name="customerContactName"]').value, + email: document.querySelector('[name="customerEmail"]').value + }, + + // Line items with formatted values + items: Array.from(document.querySelectorAll('.line-item')).map((item, index) => ({ + number: index + 1, + description: item.querySelector('[name^="description"]').value, + quantity: this.formatter.formatQuantity(item.querySelector('[name^="quantity"]').value), + unit: item.querySelector('[name^="unit"]').value, + price: this.formatter.formatCurrency(item.querySelector('[name^="price"]').value), + vatRate: this.formatter.formatCurrency(item.querySelector('[name^="vatRate"]').value), + totalAmount: this.formatter.formatCurrency( + this.formatter.parseQuantity(item.querySelector('[name^="quantity"]').value) * + this.formatter.parseCurrency(item.querySelector('[name^="price"]').value) + ) + })), + + // Note + note: document.querySelector('[name="invoiceNote"]')?.value, + + // Get totals directly from the display elements + totals: { + subtotal: document.getElementById('subtotal').textContent, + allowances: document.getElementById('totalAllowances').textContent, + charges: document.getElementById('totalCharges').textContent, + netAmount: document.getElementById('netAmount').textContent, + vat: document.getElementById('vat').textContent, + total: document.getElementById('total').textContent + }, + + // VAT Breakdown + vatBreakdown: Array.from(document.querySelectorAll('.vat-row')).map(row => ({ + type: row.querySelector('.vat-type').value, + rate: row.querySelector('.vat-rate').value, + base: row.querySelector('.vat-base').value, + amount: row.querySelector('.vat-amount').value + })) + }; + } + + createPartyHTML(party) { + return ` +

${party.name}

+

CUI: ${party.vat}

+

Nr. Reg. Com.: ${party.companyId}

+

${party.address}

+

${party.city}${party.county ? ', ' + party.county : ''}

+

${party.country}

+ ${party.phone ? `

Tel: ${party.phone}

` : ''} + ${party.contactName ? `

Contact: ${party.contactName}

` : ''} + ${party.email ? `

Email: ${party.email}

` : ''} + `; + } + + getVATTypeLabel(type) { + const labels = { + 'S': 'Standard', + 'AE': 'Taxare Inversă', + 'O': 'Neplătitor TVA', + 'Z': 'Cotă 0%', + 'E': 'Scutit' + }; + return labels[type] || type; + } + + async print() { + try { + // Collect all the data + const invoiceData = this.collectInvoiceData(); + + // Open new window and load the selected print template + this.printWindow = window.open( + this.templates[this.currentTemplate], + '_blank', + 'width=800,height=600' + ); + + // Wait for the window to load + await new Promise(resolve => { + this.printWindow.onload = resolve; + }); + + // Generate QR code + const qrData = { + invoiceNumber: invoiceData.invoiceNumber, + issueDate: invoiceData.issueDate, + supplier: invoiceData.supplier.name, + customer: invoiceData.customer.name, + total: this.formatter.parseCurrency(invoiceData.totals.total) + }; + + const qrElement = this.printWindow.document.getElementById('qrcode'); + if (qrElement) { + new this.printWindow.QRCode(qrElement, { + text: JSON.stringify(qrData), + width: 100, + height: 100, + colorDark: "#2563eb", + colorLight: "#ffffff", + correctLevel: this.printWindow.QRCode.CorrectLevel.L + }); + } + + // Populate the template with data + this.populatePrintWindow(invoiceData); + + // Print the window + this.printWindow.print(); + + // Clean up + this.printWindow.onafterprint = () => { + this.printWindow.close(); + this.printWindow = null; + }; + + } catch (error) { + console.error('Print failed:', error); + if (this.printWindow) { + this.printWindow.close(); + this.printWindow = null; + } + alert('A apărut o eroare la printare. Vă rugăm să încercați din nou.'); + } + } + + populatePrintWindow(data) { + if (!this.printWindow) return; + + const doc = this.printWindow.document; + + // Basic details + doc.getElementById('print-invoice-number').textContent = data.invoiceNumber; + doc.getElementById('print-issue-date').textContent = data.issueDate; + doc.getElementById('print-due-date').textContent = data.dueDate; + doc.getElementById('print-document-currency').textContent = data.documentCurrencyCode; + + // Currency information + const taxCurrencyContainer = doc.getElementById('print-tax-currency-container'); + if (data.taxCurrencyCode && data.taxCurrencyCode !== data.documentCurrencyCode) { + taxCurrencyContainer.style.display = 'block'; + doc.getElementById('print-tax-currency').textContent = data.taxCurrencyCode; + doc.getElementById('print-exchange-rate').textContent = this.formatter.formatNumber(data.exchangeRate); + } + + // Party details + doc.getElementById('print-supplier-details').innerHTML = this.createPartyHTML(data.supplier); + doc.getElementById('print-customer-details').innerHTML = this.createPartyHTML(data.customer); + + // Note + if (data.note) { + const noteSection = doc.getElementById('print-note'); + noteSection.style.display = 'block'; + noteSection.querySelector('div').textContent = data.note; + } + + // Line items - use formatted values from data + doc.getElementById('print-items').innerHTML = data.items.map(item => ` + + ${item.number} + ${item.description} + ${item.unit} + ${item.quantity} + ${item.price} + ${item.vatRate}% + ${item.totalAmount} + + `).join(''); + + // Totals - use values directly from display + doc.getElementById('print-subtotal').textContent = data.totals.subtotal; + doc.getElementById('print-allowances').textContent = data.totals.allowances; + doc.getElementById('print-charges').textContent = data.totals.charges; + doc.getElementById('print-net-amount').textContent = data.totals.netAmount; + doc.getElementById('print-total').textContent = data.totals.total; + + // VAT Breakdown - use values directly from display + doc.getElementById('print-vat-breakdown').innerHTML = data.vatBreakdown.map(vat => ` +
${this.getVATTypeLabel(vat.type)}
+
${vat.rate}%
+
${vat.base}
+
${vat.amount}
+ `).join(''); + + // VAT totals + doc.getElementById('print-vat-currency-main').textContent = data.documentCurrencyCode; + doc.getElementById('print-vat-main').textContent = data.totals.vat; + + const secondaryVatRow = doc.getElementById('print-vat-secondary'); + if (data.taxCurrencyCode && data.taxCurrencyCode !== data.documentCurrencyCode) { + secondaryVatRow.style.display = 'flex'; + doc.getElementById('print-vat-currency-secondary').textContent = data.taxCurrencyCode; + const vatInTaxCurrency = this.formatter.parseCurrency(data.totals.vat) * data.exchangeRate; + doc.getElementById('print-vat-secondary-amount').textContent = + this.formatter.formatCurrency(vatInTaxCurrency); + } + } } \ No newline at end of file diff --git a/efactura-generator/js/script.js b/efactura-generator/js/script.js index 5e778ca..1d3542e 100644 --- a/efactura-generator/js/script.js +++ b/efactura-generator/js/script.js @@ -1,3239 +1,5299 @@ -import { InvoiceFormatter } from './formatter.js'; - -// Constants -const XML_NAMESPACES = { - ubl: "urn:oasis:names:specification:ubl:schema:xsd:Invoice-2", - cbc: "urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2", - cac: "urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2" -}; - -const VAT_TYPES = { - "S": "Cotă Standard", - "AE": "Taxare Inversă", - "O": "Neplătitor TVA", - "Z": "Cotă 0% TVA", - "E": "Neimpozabil" -}; - -const VAT_EXEMPTION_CODES = { - 'AE': { - code: 'VATEX-EU-AE', - reason: 'Taxare inversa' - }, - 'K': { - code: 'VATEX-EU-IC', - reason: 'Livrare intracomunitara' - }, - 'O': { - code: 'VATEX-EU-O', - reason: 'Neplatitor TVA' - }, - 'E': [ - { - code: '', - reason: 'Scutit' - }, - { - code: 'VATEX-EU-F', - reason: 'Bunuri second hand' - }, - { - code: 'VATEX-EU-D', - reason: 'Regim special agentii de turism' - } - ] -}; - -const UNIT_CODES = new Map([ - ['EA', 'Bucată (EA)'], - ['XPP', 'Bucată (XPP)'], - ['KGM', 'Kilogram (KGM)'], - ['MTR', 'Metri (MTR)'], - ['LTR', 'Litru (LTR)'], - ['H87', 'Bucată (H87)'], - ['MTQ', 'Metri cubi (MTQ)'] -]); - -const ISO_3166_1_CODES = new Set([ - 'AD', 'AE', 'AF', 'AG', 'AI', 'AL', 'AM', 'AO', 'AQ', 'AR', 'AS', 'AT', 'AU', 'AW', 'AX', - 'AZ', 'BA', 'BB', 'BD', 'BE', 'BF', 'BG', 'BH', 'BI', 'BJ', 'BL', 'BM', 'BN', 'BO', 'BQ', - 'BR', 'BS', 'BT', 'BV', 'BW', 'BY', 'BZ', 'CA', 'CC', 'CD', 'CF', 'CG', 'CH', 'CI', 'CK', - 'CL', 'CM', 'CN', 'CO', 'CR', 'CU', 'CV', 'CW', 'CX', 'CY', 'CZ', 'DE', 'DJ', 'DK', 'DM', - 'DO', 'DZ', 'EC', 'EE', 'EG', 'EH', 'ER', 'ES', 'ET', 'FI', 'FJ', 'FK', 'FM', 'FO', 'FR', - 'GA', 'GB', 'GD', 'GE', 'GF', 'GG', 'GH', 'GI', 'GL', 'GM', 'GN', 'GP', 'GQ', 'GR', 'GS', - 'GT', 'GU', 'GW', 'GY', 'HK', 'HM', 'HN', 'HR', 'HT', 'HU', 'ID', 'IE', 'IL', 'IM', 'IN', - 'IO', 'IQ', 'IR', 'IS', 'IT', 'JE', 'JM', 'JO', 'JP', 'KE', 'KG', 'KH', 'KI', 'KM', 'KN', - 'KP', 'KR', 'KW', 'KY', 'KZ', 'LA', 'LB', 'LC', 'LI', 'LK', 'LR', 'LS', 'LT', 'LU', 'LV', - 'LY', 'MA', 'MC', 'MD', 'ME', 'MF', 'MG', 'MH', 'MK', 'ML', 'MM', 'MN', 'MO', 'MP', 'MQ', - 'MR', 'MS', 'MT', 'MU', 'MV', 'MW', 'MX', 'MY', 'MZ', 'NA', 'NC', 'NE', 'NF', 'NG', 'NI', - 'NL', 'NO', 'NP', 'NR', 'NU', 'NZ', 'OM', 'PA', 'PE', 'PF', 'PG', 'PH', 'PK', 'PL', 'PM', - 'PN', 'PR', 'PS', 'PT', 'PW', 'PY', 'QA', 'RE', 'RO', 'RS', 'RU', 'RW', 'SA', 'SB', 'SC', - 'SD', 'SE', 'SG', 'SH', 'SI', 'SJ', 'SK', 'SL', 'SM', 'SN', 'SO', 'SR', 'SS', 'ST', 'SV', - 'SX', 'SY', 'SZ', 'TC', 'TD', 'TF', 'TG', 'TH', 'TJ', 'TK', 'TL', 'TM', 'TN', 'TO', 'TR', - 'TT', 'TV', 'TW', 'TZ', 'UA', 'UG', 'UM', 'US', 'UY', 'UZ', 'VA', 'VC', 'VE', 'VG', 'VI', - 'VN', 'VU', 'WF', 'WS', 'XI', 'YE', 'YT', 'ZA', 'ZM', 'ZW' -]); - -const ROMANIAN_COUNTY_CODES = new Set([ - 'RO-AB', 'RO-AG', 'RO-AR', 'RO-B', 'RO-BC', 'RO-BH', 'RO-BN', 'RO-BR', 'RO-BT', 'RO-BV', - 'RO-BZ', 'RO-CJ', 'RO-CL', 'RO-CS', 'RO-CT', 'RO-CV', 'RO-DB', 'RO-DJ', 'RO-GJ', 'RO-GL', - 'RO-GR', 'RO-HD', 'RO-HR', 'RO-IF', 'RO-IL', 'RO-IS', 'RO-MH', 'RO-MM', 'RO-MS', 'RO-NT', - 'RO-OT', 'RO-PH', 'RO-SB', 'RO-SJ', 'RO-SM', 'RO-SV', 'RO-TL', 'RO-TM', 'RO-TR', 'RO-VL', - 'RO-VN', 'RO-VS' -]); - -const CHARGE_REASON_CODES = { - 'TV': 'Cheltuieli de transport', - 'FC': 'Taxe transport', - 'ZZZ': 'Definite reciproc' -}; - -const ALLOWANCE_REASON_CODES = { - '95': 'Reducere', - '41': 'Bonus lucrări în avans', - '42': 'Alt bonus', - '60': 'Reducere volum', - '62': 'Alte reduceri', - '63': 'Reducere producător', - '64': 'Din cauza războiului', - '65': 'Reducere outlet nou', - '66': 'Reducere mostre', - '67': 'Reducere end-of-range', - '68': 'Cost ambalaj returnabil', - '70': 'Reducere Incoterm', - '71': 'Prag vânzări', - '88': 'Suprataxă/deducere materiale', - '100': 'Reducere specială', - '102': 'Termen lung fix', - '103': 'Temporar', - '104': 'Standard', - '105': 'Cifră de afaceri anuală' -}; - -// Structure for item identifications -const IDENTIFICATION_TYPES = { - SELLERS: { - type: 'sellers', - label: 'Cod Furnizor', - xmlTag: 'SellersItemIdentification' - }, - BUYERS: { - type: 'buyers', - label: 'Cod Client', - xmlTag: 'BuyersItemIdentification' - }, - STANDARD: { - type: 'standard', - label: 'Cod Bare', - xmlTag: 'StandardItemIdentification', - schemeID: '0160' - }, - COMMODITY: { - type: 'commodity', - label: 'Cod Clasificare', - xmlTag: 'CommodityClassification', - schemes: [ - { id: 'CV', name: 'Cod Vamal' }, - { id: 'TSP', name: 'Cod CPV' }, - { id: 'STI', name: 'Cod NC8' } - ] - } -}; - -const formatter = new InvoiceFormatter() - -const resolver = { - lookupNamespaceURI: prefix => { - const ns = { - 'cbc': 'urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2', - 'cac': 'urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2', - 'ubl': 'urn:oasis:names:specification:ubl:schema:xsd:Invoice-2' - }; - return ns[prefix] || null; - } -}; - -// Global variables -let currentInvoice = null; -let originalTotals = null; -let vatRates = new Map(); -let manuallyEditedVatRows = new Set(); - -// Initialize event listeners -document.addEventListener('DOMContentLoaded', async function() { - document.getElementById('fileInput').addEventListener('change', handleFileSelect); - initializeUI(); - - if (!currentInvoice) { - currentInvoice = createEmptyInvoice(); - } - - const totalElements = [ - 'subtotal', 'totalAllowances', 'totalCharges', - 'netAmount', 'vat', 'total' - ]; - - totalElements.forEach(elementId => { - const element = document.getElementById(elementId); - setupInlineEditing(element); - }); - - // Add currency code validation - const currencyInputs = document.querySelectorAll('[name="documentCurrencyCode"], [name="taxCurrencyCode"]'); - currencyInputs.forEach(input => { - input.addEventListener('input', function(e) { - // Convert to uppercase - this.value = this.value.toUpperCase(); - // Remove any non-letter characters - this.value = this.value.replace(/[^A-Z]/g, ''); - // Limit to 3 characters - if (this.value.length > 3) { - this.value = this.value.slice(0, 3); - } - }); - }); - - // Make document currency code required - const documentCurrencyInput = document.querySelector('[name="documentCurrencyCode"]'); - if (documentCurrencyInput) { - documentCurrencyInput.required = true; - } - - const supplierVATInput = document.querySelector('[name="supplierVAT"]'); - if (supplierVATInput) { - supplierVATInput.addEventListener('change', function() { - updateAllVATTypes(); - }); - } - - addExchangeRateField(); - - initializeLocationSelectors(); - - // Verifică dacă avem parametru XML în URL - const urlParams = new URLSearchParams(window.location.search); - const xmlFileName = urlParams.get('xml'); - - if (xmlFileName) { - try { - // Încarcă XML-ul din fișierul temporar - const response = await fetch('temp/' + xmlFileName); - if (response.ok) { - const xmlContent = await response.text(); - parseXML(xmlContent); - - // Curăță fișierul temporar - fetch('receiver.php?cleanup=' + xmlFileName) - .catch(error => console.error('Eroare la ștergerea fișierului temporar:', error)); - } - } catch (error) { - console.error('Eroare la încărcarea XML:', error); - } - } -}); - -// Initialize event listeners for existing line items -document.querySelectorAll('.line-item').forEach((item, index) => { - handleLineItemChange(index); -}); - -// Inline editing setup function -function setupInlineEditing(element) { - let originalValue; - - element.addEventListener('click', function() { - this.setAttribute('contenteditable', 'true'); - // Store exact displayed value - originalValue = this.textContent; - this.focus(); - }); - - element.addEventListener('blur', function() { - this.setAttribute('contenteditable', 'false'); - if (this.textContent !== originalValue) { - updateTotals(); - } - }); - - element.addEventListener('keydown', function(event) { - if (event.key === 'Enter') { - event.preventDefault(); - this.blur(); - } - }); -} - -function updateTotalDisplay(elementId, value) { - const element = document.getElementById(elementId); - if (element) { - // Convertim la număr folosind parseFloat pentru a evita probleme de truncare - const numValue = parseFloat(value); - element.textContent = formatter.formatCurrency(numValue); - } -} - -function displayTotals(totals) { - // Actualizează toate totalurile cu formatare - updateTotalDisplay('subtotal', totals.subtotal); - updateTotalDisplay('totalAllowances', totals.allowances); - updateTotalDisplay('totalCharges', totals.charges); - updateTotalDisplay('netAmount', totals.netAmount); - updateTotalDisplay('vat', totals.totalVat); - updateTotalDisplay('total', totals.total); - - // Actualizează defalcarea TVA dacă există - const container = document.getElementById('vatBreakdownRows'); - if (container) { - container.innerHTML = ''; - if (totals.vatBreakdown) { - totals.vatBreakdown.forEach((data, key) => { - const [rate, type] = key.split('-'); - addVATBreakdownRow( - parseFloat(rate), - data.baseAmount, - data.vatAmount, - type - ); - }); - } - } -} - -function updateVATDisplay(row, amount, type = 'amount') { - const input = row.querySelector(`.vat-${type}`); - if (input) { - input.value = formatter.formatCurrency(amount); - } -} - -function validateVATExemption() { - const vatRows = document.querySelectorAll('.vat-row'); - let isValid = true; - - vatRows.forEach(row => { - const vatType = row.querySelector('.vat-type').value; - if (['E', 'K', 'AE', 'O'].includes(vatType)) { - const exemptionCodeInput = row.querySelector('.vat-exemption-code'); - const exemptionReasonInput = row.querySelector('.vat-exemption-reason'); - const exemptionCode = exemptionCodeInput?.value; - const exemptionReason = exemptionReasonInput?.value; - - // Pentru neplătitori de TVA (tip O), verificăm să aibă valorile corecte - if (vatType === 'O') { - if (exemptionCode !== 'VATEX-EU-O') { - isValid = false; - exemptionCodeInput?.classList.add('invalid'); - } - } - // Pentru celelalte tipuri de scutire, trebuie să aibă cel puțin unul dintre câmpuri completat - else if (!exemptionCode && !exemptionReason) { - isValid = false; - exemptionCodeInput?.classList.add('invalid'); - exemptionReasonInput?.classList.add('invalid'); - } else { - exemptionCodeInput?.classList.remove('invalid'); - exemptionReasonInput?.classList.remove('invalid'); - } - } - }); - - return isValid; -} - -function addDynamicVatExemptionCode(vatType, exemptionCode, exemptionReason) { - if (!vatType || !exemptionCode) return; - - if (vatType === 'E') { - // Verifică dacă codul există deja - const exists = VAT_EXEMPTION_CODES.E.some(e => e.code === exemptionCode); - if (!exists) { - VAT_EXEMPTION_CODES.E.push({ - code: exemptionCode, - reason: exemptionReason || exemptionCode - }); - } - } else if (!VAT_EXEMPTION_CODES[vatType]) { - // Adaugă un nou tip de TVA dacă nu există - VAT_EXEMPTION_CODES[vatType] = { - code: exemptionCode, - reason: exemptionReason || exemptionCode - }; - } -} - -function getDisplayValue(elementId) { - const element = document.getElementById(elementId); - return element ? formatter.parseCurrency(element.textContent) : 0; -} - -// Event delegation for dynamic elements -document.addEventListener('click', function(event) { - const target = event.target; - - if (target.matches('.delete-line-item')) { - const lineItem = target.closest('.line-item'); - if (lineItem) { - removeLineItem(parseInt(lineItem.dataset.index)); - } - } - - if (target.matches('.delete-allowance-charge')) { - const charge = target.closest('.allowance-charge'); - if (charge) { - removeAllowanceCharge(parseInt(charge.dataset.index)); - } - } -}); - -// File handling functions -function handleFileSelect(event) { - const file = event.target.files[0]; - if (file) { - const reader = new FileReader(); - reader.onload = function(e) { - const xmlContent = e.target.result; - parseXML(xmlContent); - }; - reader.readAsText(file); - } -} - -function parseXML(xmlContent) { - try { - const parser = new DOMParser(); - const xmlDoc = parser.parseFromString(xmlContent, "text/xml"); - - const parserError = xmlDoc.querySelector('parsererror'); - if (parserError) { - throw new Error('Eroare la parsarea XML: ' + parserError.textContent); - } - - // Extrage și adaugă codurile de scutire din TaxTotal - const taxSubtotals = xmlDoc.querySelectorAll('cac\\:TaxSubtotal, TaxSubtotal'); - taxSubtotals.forEach(subtotal => { - const taxCategory = subtotal.querySelector('cac\\:TaxCategory, TaxCategory'); - if (taxCategory) { - const vatType = getXMLValue(taxCategory, 'cbc\\:ID, ID'); - const exemptionCode = getXMLValue(taxCategory, 'cbc\\:TaxExemptionReasonCode, TaxExemptionReasonCode'); - const exemptionReason = getXMLValue(taxCategory, 'cbc\\:TaxExemptionReason, TaxExemptionReason'); - - if (vatType && exemptionCode) { - addDynamicVatExemptionCode(vatType, exemptionCode, exemptionReason); - } - } - }); - - currentInvoice = xmlDoc; - manuallyEditedVatRows.clear(); - - populateBasicDetails(xmlDoc); - populatePartyDetails(xmlDoc); - populateAllowanceCharges(xmlDoc); - populateLineItems(xmlDoc); - storeOriginalTotals(xmlDoc); - restoreOriginalTotals(); - displayVATBreakdown(xmlDoc); - - } catch (error) { - handleError(error, 'Eroare la parsarea fișierului XML'); - } -} - -// Create allowance charge HTML -function createAllowanceChargeHTML(index, charge) { - return ` -
-
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- -
- `; -} - -function createReasonCodeOptions(isCharge, selectedCode = '') { - const codes = isCharge ? CHARGE_REASON_CODES : ALLOWANCE_REASON_CODES; - return Object.entries(codes) - .map(([code, description]) => - `` - ).join(''); -} - -window.updateReasonCodeOptions = function(index) { - const chargeTypeSelect = document.querySelector(`[name="chargeType${index}"]`); - const reasonCodeSelect = document.querySelector(`[name="chargeReasonCode${index}"]`); - const reasonInput = document.querySelector(`[name="chargeReason${index}"]`); - - const isCharge = chargeTypeSelect.value === 'true'; - reasonCodeSelect.innerHTML = createReasonCodeOptions(isCharge); - - // Update reason text based on selected code - const selectedCode = reasonCodeSelect.value; - const codes = isCharge ? CHARGE_REASON_CODES : ALLOWANCE_REASON_CODES; - reasonInput.value = codes[selectedCode] || ''; -} - -// Create line item HTML -function createLineItemHTML(index, description = '', quantity = '1', price = '0', vatRate = '19', - unitCode = 'EA', vatTypeId = 'S', itemDescription = '', lineDiscount = '0', discountReasonCode = '') { - - return ` -
-
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- -
- -
- - - - -
- `; -} - -function createLineDiscountReasonOptions(selectedCode = '') { - return Object.entries(ALLOWANCE_REASON_CODES) - .map(([code, description]) => - `` - ).join(''); -} - -// Add VAT breakdown row -function addVATBreakdownRow(rate, baseAmount, vatAmount, vatType = 'S', existingRowId = null, exemptionCode = '', exemptionReason = '') { - const container = document.getElementById('vatBreakdownRows'); - const rowId = existingRowId || `vat-row-${Date.now()}`; - - const rowHtml = ` -
-
-
- - - - % - - - - -
-
-
- - -
-
- - -
-
- -
-
- `; - - container.insertAdjacentHTML('beforeend', rowHtml); - - // Add event listener for VAT type changes - const vatTypeSelect = document.querySelector(`#${rowId} .vat-type`); - vatTypeSelect.addEventListener('change', () => { - const exemptionContainer = document.querySelector(`#${rowId} .vat-exemption`); - const newVatType = vatTypeSelect.value; - - if (['E', 'K', 'O', 'AE'].includes(newVatType)) { - exemptionContainer.classList.remove('hidden'); - - // Update exemption code options - const codeSelect = document.querySelector(`#${rowId} .vat-exemption-code`); - codeSelect.innerHTML = generateExemptionCodeOptions(newVatType); - - // Set default values - const defaultExemption = getDefaultExemption(newVatType); - if (defaultExemption) { - codeSelect.value = defaultExemption.code; - document.querySelector(`#${rowId} .vat-exemption-reason`).value = defaultExemption.reason; - } - } else { - exemptionContainer.classList.add('hidden'); - } - }); -} - -function generateExemptionCodeOptions(vatType, selectedCode = '') { - if (vatType === 'E') { - return VAT_EXEMPTION_CODES.E.map(exemption => - `` - ).join(''); - } else if (vatType === 'K' || vatType === 'AE' || vatType === 'O') { - const exemption = VAT_EXEMPTION_CODES[vatType]; - return ``; - } - return ''; -} - -function getDefaultExemption(vatType) { - if (vatType === 'E') { - return VAT_EXEMPTION_CODES.E[0]; - } - return VAT_EXEMPTION_CODES[vatType]; -} - -// Add this to window object -window.updateExemptionReason = function(rowId) { - const row = document.getElementById(rowId); - if (!row) return; - - const vatType = row.querySelector('.vat-type').value; - const codeSelect = row.querySelector('.vat-exemption-code'); - const reasonInput = row.querySelector('.vat-exemption-reason'); - - if (vatType === 'E') { - const selectedExemption = VAT_EXEMPTION_CODES.E.find(e => e.code === codeSelect.value); - if (selectedExemption) { - reasonInput.value = selectedExemption.reason; - } - } else if (vatType === 'K' || vatType === 'AE') { - reasonInput.value = VAT_EXEMPTION_CODES[vatType].reason; - } -}; - -// Toggle optional details -window.toggleOptionalDetails = function(index) { - const optionalDetails = document.getElementById(`optionalDetails${index}`); - const button = optionalDetails.previousElementSibling.querySelector('button'); - - if (optionalDetails.style.display === 'none') { - optionalDetails.style.display = 'block'; - button.innerHTML = '▲ Detalii Suplimentare'; - } else { - optionalDetails.style.display = 'none'; - button.innerHTML = '▼ Detalii Suplimentare'; - } -} - -// Form validation -function validateForm(silent = false) { - let isValid = true; - let firstInvalidField = null; - - // Validare câmpuri existente... - const requiredFields = [ - 'invoiceNumber', - 'issueDate', - 'dueDate', - 'supplierName', - 'supplierVAT', - 'customerName', - ]; - - requiredFields.forEach(fieldName => { - const field = document.querySelector(`[name="${fieldName}"]`); - if (!field || !field.value.trim()) { - field.classList.add('invalid'); - isValid = false; - if (!firstInvalidField) - firstInvalidField = field; - } else { - field.classList.remove('invalid'); - } - }); - - // Adaugă validarea TVA - const vatExemptionValid = validateVATExemption(); - if (!vatExemptionValid) { - isValid = false; - if (!silent) { - alert('Vă rugăm să completați codul și/sau motivul scutirii de TVA pentru toate categoriile care necesită această informație.'); - } - } - - // Restul validărilor existente... - const lineItems = document.querySelectorAll('.line-item'); - if (lineItems.length === 0) { - isValid = false; - if (!silent) { - alert('Este necesară cel puțin o linie în factură'); - } - return false; - } - - lineItems.forEach((item, index) => { - const quantity = parseFloat(document.querySelector(`[name="quantity${index}"]`).value); - const price = parseFloat(document.querySelector(`[name="price${index}"]`).value); - const description = document.querySelector(`[name="description${index}"]`).value; - - if (!description.trim()) { - document.querySelector(`[name="description${index}"]`).classList.add('invalid'); - isValid = false; - } - if (isNaN(quantity)) { - document.querySelector(`[name="quantity${index}"]`).classList.add('invalid'); - isValid = false; - } - if (isNaN(price)) { - document.querySelector(`[name="price${index}"]`).classList.add('invalid'); - isValid = false; - } - }); - - const dateInputs = document.querySelectorAll('.date-input'); - dateInputs.forEach(input => { - if (!validateDateInput(input)) { - isValid = false; - if (!firstInvalidField) firstInvalidField = input; - } - }); - - // Validare curs valutar dacă este cazul - const taxCurrencyCode = document.querySelector('[name="taxCurrencyCode"]').value.trim(); - const documentCurrencyCode = document.querySelector('[name="documentCurrencyCode"]').value.trim(); - - if (taxCurrencyCode && taxCurrencyCode !== documentCurrencyCode) { - const exchangeRate = document.querySelector('[name="exchangeRate"]'); - if (!exchangeRate || !exchangeRate.value || parseFloat(exchangeRate.value) <= 0) { - exchangeRate.classList.add('invalid'); - isValid = false; - if (!firstInvalidField) { - firstInvalidField = exchangeRate; - } - } else { - exchangeRate.classList.remove('invalid'); - } - } - - if (!isValid && !silent) { - if (firstInvalidField) { - firstInvalidField.focus(); - } - alert('Vă rugăm să completați toate câmpurile obligatorii'); - } - - return isValid; -} - -function handleError(error, message) { - console.error(message, error); - alert(`${message}\nVă rugăm să verificați consola pentru detalii.`); -} - -function formatDateToRomanian(date) { - const day = String(date.getDate()).padStart(2, '0'); - const month = String(date.getMonth() + 1).padStart(2, '0'); - const year = date.getFullYear(); - return `${day}.${month}.${year}`; -} - -function parseRomanianDate(dateStr) { - const [day, month, year] = dateStr.split('.'); - return `${year}-${month}-${day}`; -} - -function createDatePicker(input, button) { - const picker = new Pikaday({ - field: input, - trigger: button, - format: 'DD.MM.YYYY', - i18n: { - previousMonth: 'Luna anterioară', - nextMonth: 'Luna următoare', - months: ['Ianuarie', 'Februarie', 'Martie', 'Aprilie', 'Mai', 'Iunie', 'Iulie', 'August', 'Septembrie', 'Octombrie', 'Noiembrie', 'Decembrie'], - weekdays: ['Duminică', 'Luni', 'Marți', 'Miercuri', 'Joi', 'Vineri', 'Sâmbătă'], - weekdaysShort: ['Dum', 'Lun', 'Mar', 'Mie', 'Joi', 'Vin', 'Sâm'] - }, - firstDay: 1, - onSelect: function(date) { - input.value = formatDateToRomanian(date); - validateDateInput(input); - } - }); - return picker; -} - -function validateDateInput(input) { - const value = input.value; - const regex = /^(\d{2})\.(\d{2})\.(\d{4})$/; - const match = value.match(regex); - - if (match) { - const day = parseInt(match[1]); - const month = parseInt(match[2]); - const year = parseInt(match[3]); - - // Create date object and verify if it's valid - const date = new Date(year, month - 1, day); - if (date.getFullYear() === year && date.getMonth() === month - 1 && date.getDate() === day) { - input.classList.remove('invalid'); - return true; - } - } - - if (value !== '') { - input.classList.add('invalid'); - } - return false; -} - -function restrictDateInput(input) { - input.addEventListener('input', function(e) { - let value = e.target.value; - - // Remove any non-digit characters except dots - value = value.replace(/[^\d.]/g, ''); - - // Auto-add dots after day and month - if (value.length >= 2 && value.charAt(2) !== '.') { - value = value.slice(0, 2) + '.' + value.slice(2); - } - if (value.length >= 5 && value.charAt(5) !== '.') { - value = value.slice(0, 5) + '.' + value.slice(5); - } - - // Restrict to exactly 10 characters (dd.mm.yyyy) - value = value.slice(0, 10); - - e.target.value = value; - }); - - input.addEventListener('blur', function() { - validateDateInput(input); - }); -} - -function createCountryOptions() { - return Array.from(ISO_3166_1_CODES).map(code => - `` - ).join(''); -} - -function createCountyOptions() { - return Array.from(ROMANIAN_COUNTY_CODES).map(code => { - const label = code.replace('RO-', ''); - const counties = { - 'AB': 'Alba', 'AR': 'Arad', 'AG': 'Argeș', 'BC': 'Bacău', 'BH': 'Bihor', - 'BN': 'Bistrița-Năsăud', 'BT': 'Botoșani', 'BV': 'Brașov', 'BR': 'Brăila', - 'B': 'București', 'BZ': 'Buzău', 'CS': 'Caraș-Severin', 'CL': 'Călărași', - 'CJ': 'Cluj', 'CT': 'Constanța', 'CV': 'Covasna', 'DB': 'Dâmbovița', - 'DJ': 'Dolj', 'GL': 'Galați', 'GR': 'Giurgiu', 'GJ': 'Gorj', 'HR': 'Harghita', - 'HD': 'Hunedoara', 'IL': 'Ialomița', 'IS': 'Iași', 'IF': 'Ilfov', - 'MM': 'Maramureș', 'MH': 'Mehedinți', 'MS': 'Mureș', 'NT': 'Neamț', - 'OT': 'Olt', 'PH': 'Prahova', 'SM': 'Satu Mare', 'SJ': 'Sălaj', - 'SB': 'Sibiu', 'SV': 'Suceava', 'TR': 'Teleorman', 'TM': 'Timiș', - 'TL': 'Tulcea', 'VS': 'Vaslui', 'VL': 'Vâlcea', 'VN': 'Vrancea' - }; - return ``; - }).join(''); -} - -function initializeLocationSelectors() { - // Replace country inputs with selects for both supplier and customer - ['supplier', 'customer'].forEach(party => { - const countryInput = document.querySelector(`[name="${party}Country"]`); - if (countryInput) { - const select = document.createElement('select'); - select.className = 'form-input'; - select.name = countryInput.name; - select.innerHTML = createCountryOptions(); - select.value = countryInput.dataset.xmlValue || countryInput.value || 'RO'; - countryInput.parentNode?.replaceChild(select, countryInput); - } - - const countyInput = document.querySelector(`[name="${party}CountrySubentity"]`); - if (countyInput) { - const select = document.createElement('select'); - select.className = 'form-input'; - select.name = countyInput.name; - select.innerHTML = createCountyOptions(); - select.value = countyInput.dataset.xmlValue || countyInput.value || ''; - countyInput.parentNode?.replaceChild(select, countyInput); - } - }); - - // Initialize location handlers for both parties - setupPartyLocationHandlers('supplier'); - setupPartyLocationHandlers('customer'); -} - -function updateCountyVisibility(countrySelect, countySelect) { - if (!countrySelect || !countySelect) return; - const showCounty = countrySelect.value === 'RO'; - countySelect.style.display = showCounty ? 'block' : 'none'; - countySelect.required = showCounty; -} - -function setupPartyLocationHandlers(party) { - const countrySelect = document.querySelector(`[name="${party}Country"]`); - const countySelect = document.querySelector(`[name="${party}CountrySubentity"]`); - const cityContainer = document.querySelector(`[name="${party}City"]`)?.parentNode; - - if (!countrySelect || !countySelect || !cityContainer) return; - - const handleLocationChange = () => { - const isBucharest = countrySelect.value === 'RO' && countySelect.value === 'RO-B'; - const currentElement = cityContainer.querySelector('input, select'); - const isCurrentlySector = currentElement.tagName.toLowerCase() === 'select'; - - if (isBucharest && !isCurrentlySector) { - const sectorSelect = document.createElement('select'); - sectorSelect.className = 'form-input'; - sectorSelect.name = `${party}City`; - sectorSelect.innerHTML = ` - - - - - - - - `; - - // Try to preserve any existing sector value - const currentValue = currentElement.value || ''; - if (currentValue.toUpperCase().includes('SECTOR')) { - sectorSelect.value = currentValue.toUpperCase().replace(/\s+/g, ''); - } - - cityContainer.replaceChild(sectorSelect, currentElement); - } else if (!isBucharest && isCurrentlySector) { - const cityInput = document.createElement('input'); - cityInput.type = 'text'; - cityInput.className = 'form-input'; - cityInput.name = `${party}City`; - cityInput.value = ''; - cityContainer.replaceChild(cityInput, currentElement); - } - }; - - // Set up event listeners - countrySelect.addEventListener('change', () => { - updateCountyVisibility(countrySelect, countySelect); - handleLocationChange(); - }); - - countySelect.addEventListener('change', handleLocationChange); - - // Initial setup - updateCountyVisibility(countrySelect, countySelect); - handleLocationChange(); -} - -function initializeUI() { - document.querySelectorAll('.form-input').forEach(input => { - input.addEventListener('input', function() { - this.classList.remove('invalid'); - updateTotals(); - }); - }); - - document.addEventListener('keydown', function(event) { - if (event.ctrlKey || event.metaKey) { - switch (event.key.toLowerCase()) { - case 's': - event.preventDefault(); - saveXML(); - break; - case 'o': - event.preventDefault(); - document.getElementById('fileInput').click(); - break; - case 'n': - event.preventDefault(); - addLineItem(); - break; - } - } - }); - - // Initialize date pickers - const dateInputs = document.querySelectorAll('.date-input'); - dateInputs.forEach(input => { - const button = input.parentElement.querySelector('.calendar-button'); - createDatePicker(input, button); - restrictDateInput(input); - }); - - if (!currentInvoice) { - const today = new Date(); - const dueDate = new Date(); - dueDate.setDate(today.getDate() + 30); - - document.querySelector('[name="issueDate"]').value = formatDateToRomanian(today); - document.querySelector('[name="dueDate"]').value = formatDateToRomanian(dueDate); - } - - // Add note counter event listener - const noteInput = document.querySelector('[name="invoiceNote"]'); - if (noteInput) { - noteInput.addEventListener('input', updateNoteCounter); - updateNoteCounter(); // Initial count - } - - // Initialize location selectors - initializeLocationSelectors(); - - window.addLineItem = addLineItem; - window.removeLineItem = removeLineItem; - window.addAllowanceCharge = addAllowanceCharge; - window.removeAllowanceCharge = removeAllowanceCharge; - window.handleStorno = handleStorno; - window.updateTotals = updateTotals; - window.saveXML = saveXML; - window.refreshTotals = refreshTotals; - window.displayVATBreakdown = displayVATBreakdown; -} - -// Handling VAT type changes -window.handleVatTypeChange = function(index) { - const vatTypeSelect = document.querySelector(`[name="vatType${index}"]`); - const vatRateInput = document.querySelector(`[name="vatRate${index}"]`); - - // Verifică dacă furnizorul este neplătitor TVA - if (!isVATRegistered()) { - vatTypeSelect.value = 'O'; - vatTypeSelect.disabled = true; // Dezactivează selectul pentru neplătitori - vatRateInput.value = '0'; - vatRateInput.disabled = true; - - // Setează codul și motivul scutirii pentru neplătitori - const row = document.querySelector(`.vat-row`); - if (row) { - const exemptionCode = row.querySelector('.vat-exemption-code'); - const exemptionReason = row.querySelector('.vat-exemption-reason'); - if (exemptionCode) exemptionCode.value = 'VATEX-EU-O'; - if (exemptionReason) exemptionReason.value = 'Operațiune efectuată de neplătitor de TVA'; - } - } else { - vatTypeSelect.disabled = false; // Activează selectul pentru plătitori - - switch(vatTypeSelect.value) { - case 'O': // Pentru neplătitori - vatRateInput.value = '0'; - vatRateInput.disabled = true; - break; - case 'AE': // Taxare inversă - case 'Z': // Cotă 0% - case 'E': // Scutit - vatRateInput.value = '0'; - vatRateInput.disabled = true; - break; - case 'S': // Standard - vatRateInput.value = '19'; - vatRateInput.disabled = false; - break; - } - } - - updateTotals(); -} - -function isVATRegistered() { - const supplierVAT = document.querySelector('[name="supplierVAT"]').value.trim().toUpperCase(); - return supplierVAT.startsWith('RO'); -} - -function handleChargeVatTypeChange(index) { - const vatTypeSelect = document.querySelector(`[name="chargeVatType${index}"]`); - const vatRateInput = document.querySelector(`[name="chargeVatRate${index}"]`); - - if (!vatTypeSelect || !vatRateInput) return; - - // Verifică dacă furnizorul este neplătitor TVA - if (!isVATRegistered()) { - vatTypeSelect.value = 'O'; - vatTypeSelect.disabled = true; // Dezactivează selectul pentru neplătitori - vatRateInput.value = '0'; - vatRateInput.disabled = true; - - // Setează codul și motivul scutirii pentru neplătitori - const row = document.querySelector(`.vat-row`); - if (row) { - const exemptionCode = row.querySelector('.vat-exemption-code'); - const exemptionReason = row.querySelector('.vat-exemption-reason'); - if (exemptionCode) exemptionCode.value = 'VATEX-EU-O'; - if (exemptionReason) exemptionReason.value = 'Operațiune efectuată de neplătitor de TVA'; - } - } else { - vatTypeSelect.disabled = false; // Activează selectul pentru plătitori - - switch(vatTypeSelect.value) { - case 'O': // Pentru neplătitori - vatRateInput.value = '0'; - vatRateInput.disabled = true; - break; - case 'AE': // Taxare inversă - case 'Z': // Cotă 0% - case 'E': // Scutit - vatRateInput.value = '0'; - vatRateInput.disabled = true; - break; - case 'S': // Standard - vatRateInput.value = '19'; - vatRateInput.disabled = false; - break; - } - } - - // Clear manual edits and refresh - manuallyEditedVatRows.clear(); - refreshTotals(); -} - -// Funcție pentru actualizarea tuturor categoriilor TVA când se modifică codul fiscal -function updateAllVATTypes() { - const isNotVATRegistered = !isVATRegistered(); - - // Actualizează toate liniile de articole - document.querySelectorAll('.line-item').forEach((item, index) => { - const vatTypeSelect = item.querySelector(`[name="vatType${index}"]`); - const vatRateInput = item.querySelector(`[name="vatRate${index}"]`); - - if (isNotVATRegistered) { - vatTypeSelect.value = 'O'; - vatTypeSelect.disabled = true; - vatRateInput.value = '0'; - vatRateInput.disabled = true; - } else { - vatTypeSelect.disabled = false; - // Restabilește valorile implicite pentru plătitori - if (vatTypeSelect.value === 'O') { - vatTypeSelect.value = 'S'; - vatRateInput.value = '19'; - vatRateInput.disabled = false; - } - } - }); - - // Actualizează breakdown-ul TVA - updateVATBreakdown(); - refreshTotals(); -} - -// XML modifications -function addUnitCode(code) { - if (!UNIT_CODES.has(code)) { - UNIT_CODES.set(code, `${code} (${code})`); - } -} - -function createUnitCodeOptionsHTML(selectedCode = 'EA') { - return Array.from(UNIT_CODES.entries()) - .map(([code, description]) => - `` - ) - .join(''); -} - -function storeOriginalTotals(xmlDoc) { - const taxTotal = xmlDoc.querySelector('cac\\:TaxTotal, TaxTotal'); - const monetaryTotal = xmlDoc.querySelector('cac\\:LegalMonetaryTotal, LegalMonetaryTotal'); - - originalTotals = { - subtotal: getXMLValue(monetaryTotal, 'cbc\\:LineExtensionAmount, LineExtensionAmount'), - allowances: getXMLValue(monetaryTotal, 'cbc\\:AllowanceTotalAmount, AllowanceTotalAmount', '0'), - charges: getXMLValue(monetaryTotal, 'cbc\\:ChargeTotalAmount, ChargeTotalAmount', '0'), - netAmount: getXMLValue(monetaryTotal, 'cbc\\:TaxExclusiveAmount, TaxExclusiveAmount'), - totalVat: getXMLValue(taxTotal, 'cbc\\:TaxAmount, TaxAmount'), - total: getXMLValue(monetaryTotal, 'cbc\\:TaxInclusiveAmount, TaxInclusiveAmount') - }; - - // console.log('Original totals from XML:', originalTotals); - - const vatBreakdown = []; - const taxSubtotals = xmlDoc.querySelectorAll('cac\\:TaxSubtotal, TaxSubtotal'); - taxSubtotals.forEach(subtotal => { - const taxCategory = subtotal.querySelector('cac\\:TaxCategory, TaxCategory'); - vatBreakdown.push({ - taxableAmount: getXMLValue(subtotal, 'cbc\\:TaxableAmount, TaxableAmount'), - taxAmount: getXMLValue(subtotal, 'cbc\\:TaxAmount, TaxAmount'), - percent: getXMLValue(taxCategory, 'cbc\\:Percent, Percent'), - type: getXMLValue(taxCategory, 'cbc\\:ID, ID', 'S'), - exemptionCode: getXMLValue(taxCategory, 'cbc\\:TaxExemptionReasonCode, TaxExemptionReasonCode'), - exemptionReason: getXMLValue(taxCategory, 'cbc\\:TaxExemptionReason, TaxExemptionReason') - }); - }); - originalTotals.vatBreakdown = vatBreakdown; -} - -function restoreOriginalTotals() { - if (!originalTotals) return; - - // Display exact values from XML with formatting - document.getElementById('subtotal').textContent = formatter.formatCurrency(originalTotals.subtotal); - document.getElementById('totalAllowances').textContent = formatter.formatCurrency(originalTotals.allowances); - document.getElementById('totalCharges').textContent = formatter.formatCurrency(originalTotals.charges); - document.getElementById('netAmount').textContent = formatter.formatCurrency(originalTotals.netAmount); - document.getElementById('vat').textContent = formatter.formatCurrency(originalTotals.totalVat); - - // Bypass formatting for total - document.getElementById('total').textContent = originalTotals.total; - - const container = document.getElementById('vatBreakdownRows'); - if (container) { - container.innerHTML = ''; - - if (originalTotals.vatBreakdown && originalTotals.vatBreakdown.length > 0) { - originalTotals.vatBreakdown.forEach(vat => { - addVATBreakdownRow( - vat.percent, - vat.taxableAmount, - vat.taxAmount, - vat.type, - null, - vat.exemptionCode, - vat.exemptionReason - ); - }); - } - } -} - -function updateNoteCounter() { - const noteInput = document.querySelector('[name="invoiceNote"]'); - const counter = document.querySelector('.note-counter'); - if (noteInput && counter) { - const length = noteInput.value.length; - counter.textContent = `${length}/900 caractere`; - } -} - -function splitNoteIntoChunks(text, maxLength) { - if (!text) return []; - const chunks = []; - let remainingText = text; - - while (remainingText.length > 0) { - if (remainingText.length <= maxLength) { - chunks.push(remainingText); - break; - } - - let splitPoint = remainingText.substr(0, maxLength).lastIndexOf('\n'); - if (splitPoint === -1) { - splitPoint = remainingText.substr(0, maxLength).lastIndexOf(' '); - } - if (splitPoint === -1) splitPoint = maxLength; - - chunks.push(remainingText.substr(0, splitPoint)); - remainingText = remainingText.substr(splitPoint + 1); - } - - return chunks.filter(chunk => chunk.trim().length > 0); -} - -function populateBasicDetails(xmlDoc) { - document.querySelector('[name="invoiceNumber"]').value = getXMLValue(xmlDoc, 'cbc\\:ID, ID'); - - // Get and combine all Note elements - const notes = xmlDoc.querySelectorAll('cbc\\:Note, Note'); - const combinedNotes = Array.from(notes).map(note => note.textContent).join('\n'); - document.querySelector('[name="invoiceNote"]').value = combinedNotes; - updateNoteCounter(); - - const issueDate = getXMLValue(xmlDoc, 'cbc\\:IssueDate, IssueDate'); - const dueDate = getXMLValue(xmlDoc, 'cbc\\:DueDate, DueDate'); - - if (issueDate) { - const [year, month, day] = issueDate.split('-'); - document.querySelector('[name="issueDate"]').value = `${day}.${month}.${year}`; - } - - if (dueDate) { - const [year, month, day] = dueDate.split('-'); - document.querySelector('[name="dueDate"]').value = `${day}.${month}.${year}`; - } - - // Add currency code handling - const documentCurrencyCode = getXMLValue(xmlDoc, 'cbc\\:DocumentCurrencyCode, DocumentCurrencyCode', 'RON'); - const taxCurrencyCode = getXMLValue(xmlDoc, 'cbc\\:TaxCurrencyCode, TaxCurrencyCode', ''); - - document.querySelector('[name="documentCurrencyCode"]').value = documentCurrencyCode; - document.querySelector('[name="taxCurrencyCode"]').value = taxCurrencyCode; - - // Store original totals and display them - storeOriginalTotals(xmlDoc); - restoreOriginalTotals(); -} - -function populatePartyDetails(xmlDoc) { - function extractPartyDetails(party, prefix) { - // Extract contact information - const contact = party.querySelector('cac\\:Contact, Contact'); - const phone = contact?.querySelector('cbc\\:Telephone, Telephone')?.textContent || ''; - const contactName = contact?.querySelector('cbc\\:Name, Name')?.textContent || ''; - const email = contact?.querySelector('cbc\\:ElectronicMail, ElectronicMail')?.textContent || ''; - - // Country Code Extraction - const countryCodeElement = party.querySelector('cac\\:Country cbc\\:IdentificationCode, Country IdentificationCode'); - const countryCode = countryCodeElement ? countryCodeElement.textContent.trim() : 'RO'; - - // Postal Address Details - const postalAddress = party.querySelector('cac\\:PostalAddress, PostalAddress'); - const streetName = postalAddress ? - getXMLValue(postalAddress, 'cbc\\:StreetName, StreetName') : ''; - const cityName = postalAddress ? - getXMLValue(postalAddress, 'cbc\\:CityName, CityName') : ''; - const countyCode = postalAddress ? - getXMLValue(postalAddress, 'cbc\\:CountrySubentity, CountrySubentity') : ''; - - // Set inputs - document.querySelector(`[name="${prefix}Name"]`).value = - getXMLValue(party, 'cac\\:PartyLegalEntity cbc\\:RegistrationName, PartyLegalEntity RegistrationName'); - document.querySelector(`[name="${prefix}VAT"]`).value = - getXMLValue(party, 'cac\\:PartyTaxScheme cbc\\:CompanyID, PartyTaxScheme CompanyID'); - document.querySelector(`[name="${prefix}CompanyId"]`).value = - getXMLValue(party, 'cac\\:PartyLegalEntity cbc\\:CompanyID, PartyLegalEntity CompanyID'); - document.querySelector(`[name="${prefix}Address"]`).value = streetName; - document.querySelector(`[name="${prefix}City"]`).value = cityName; - document.querySelector(`[name="${prefix}Phone"]`).value = phone; - document.querySelector(`[name="${prefix}ContactName"]`).value = contactName; - document.querySelector(`[name="${prefix}Email"]`).value = email; - - // Country Select - const countrySelect = document.querySelector(`[name="${prefix}Country"]`); - if (countrySelect) { - countrySelect.value = countryCode; - countrySelect.dataset.xmlValue = countryCode; - } - - // County Select - const countySelect = document.querySelector(`[name="${prefix}CountrySubentity"]`); - if (countySelect) { - countySelect.value = countyCode; - countySelect.dataset.xmlValue = countyCode; - } - } - - const supplierParty = xmlDoc.querySelector('cac\\:AccountingSupplierParty, AccountingSupplierParty'); - if (supplierParty) { - const supplierPartyDetails = supplierParty.querySelector('cac\\:Party, Party'); - if (supplierPartyDetails) { - extractPartyDetails(supplierPartyDetails, 'supplier'); - } - } - - const customerParty = xmlDoc.querySelector('cac\\:AccountingCustomerParty, AccountingCustomerParty'); - if (customerParty) { - const customerPartyDetails = customerParty.querySelector('cac\\:Party, Party'); - if (customerPartyDetails) { - extractPartyDetails(customerPartyDetails, 'customer'); - } - } - - initializeLocationSelectors(); - } - - function populateAllowanceCharges(xmlDoc) { - const charges = parseAllowanceCharges(xmlDoc); - displayAllowanceCharges(charges); - - charges.forEach((_, index) => { - setupAllowanceChargeListeners(index); - addChargeVatTypeChangeListener(index); - }); -} - -function populateLineItems(xmlDoc) { - const lineItems = xmlDoc.querySelectorAll('cac\\:InvoiceLine, InvoiceLine'); - const lineItemsContainer = document.getElementById('lineItems'); - lineItemsContainer.innerHTML = '

Articole Factură

'; - - lineItems.forEach((item, index) => { - const quantity = getXMLValue(item, 'cbc\\:InvoicedQuantity, InvoicedQuantity', '0'); - const unitCode = item.querySelector('cbc\\:InvoicedQuantity, InvoicedQuantity')?.getAttribute('unitCode') || 'EA'; - const price = getXMLValue(item, 'cac\\:Price cbc\\:PriceAmount, Price PriceAmount', '0'); - const itemElement = item.querySelector('cac\\:Item, Item'); - const description = getXMLValue(itemElement, 'cbc\\:Name, Name', ''); - const itemDescription = getXMLValue(itemElement, 'cbc\\:Description, Description', ''); - - const taxCategory = itemElement.querySelector('cac\\:ClassifiedTaxCategory, ClassifiedTaxCategory'); - const vatType = getXMLValue(taxCategory, 'cbc\\:ID, ID') || 'S'; - const vatRate = getXMLValue(taxCategory, 'cbc\\:Percent, Percent') || '19'; - - // Extragem discountul și codul de motiv de pe linie dacă există - let lineDiscount = 0; - let discountReasonCode = ''; - const allowanceCharge = item.querySelector('cac\\:AllowanceCharge, AllowanceCharge'); - if (allowanceCharge) { - const chargeIndicator = getXMLValue(allowanceCharge, 'cbc\\:ChargeIndicator, ChargeIndicator'); - if (chargeIndicator === 'false') { - lineDiscount = parseFloat(getXMLValue(allowanceCharge, 'cbc\\:Amount, Amount', '0')); - discountReasonCode = getXMLValue(allowanceCharge, 'cbc\\:AllowanceChargeReasonCode, AllowanceChargeReasonCode', ''); - } - } - - addUnitCode(unitCode); - const lineItemHtml = createLineItemHTML( - index, description, quantity, price, vatRate, unitCode, vatType, - itemDescription, lineDiscount, discountReasonCode - ); - lineItemsContainer.insertAdjacentHTML('beforeend', lineItemHtml); - - // Parse identifications after adding the line item HTML - if (itemElement) { - parseIdentifications(itemElement, index); - } - - if (!isVATRegistered()) { - const vatTypeSelect = document.querySelector(`[name="vatType${index}"]`); - const vatRateInput = document.querySelector(`[name="vatRate${index}"]`); - if (vatTypeSelect && vatRateInput) { - vatTypeSelect.value = 'O'; - vatTypeSelect.disabled = true; - vatRateInput.value = '0'; - vatRateInput.disabled = true; - } - } - - // Enable/disable discount reason code field based on discount value - const discountInput = document.querySelector(`[name="lineDiscount${index}"]`); - const reasonCodeSelect = document.querySelector(`[name="discountReasonCode${index}"]`); - if (discountInput && reasonCodeSelect) { - reasonCodeSelect.disabled = lineDiscount <= 0; - } - - handleLineItemChange(index); - }); - -} - -function setupAllowanceChargeListeners(index) { - const chargeAmountInput = document.querySelector(`[name="chargeAmount${index}"]`); - const chargeTypeInput = document.querySelector(`[name="chargeType${index}"]`); - const chargeVatTypeInput = document.querySelector(`[name="chargeVatType${index}"]`); - const chargeVatRateInput = document.querySelector(`[name="chargeVatRate${index}"]`); - const reasonCodeSelect = document.querySelector(`[name="chargeReasonCode${index}"]`); - const reasonInput = document.querySelector(`[name="chargeReason${index}"]`); - - // Add change listeners to all inputs - [chargeAmountInput, chargeTypeInput, chargeVatTypeInput, chargeVatRateInput].forEach(input => { - if (input) { - input.addEventListener('change', () => { - manuallyEditedVatRows.clear(); - refreshTotals(); - }); - } - }); - - // Add reason code change listener - if (reasonCodeSelect) { - reasonCodeSelect.addEventListener('change', () => { - const isCharge = chargeTypeInput.value === 'true'; - const codes = isCharge ? CHARGE_REASON_CODES : ALLOWANCE_REASON_CODES; - reasonInput.value = codes[reasonCodeSelect.value] || ''; - }); - } - - // Special handling for VAT type changes - if (chargeVatTypeInput) { - chargeVatTypeInput.addEventListener('change', () => handleChargeVatTypeChange(index)); - } -} - -function addChargeVatTypeChangeListener(index) { - const vatTypeSelect = document.querySelector(`[name="chargeVatType${index}"]`); - if (vatTypeSelect) { - vatTypeSelect.addEventListener('change', function() { - handleChargeVatTypeChange(index); - // Force refresh of VAT breakdown - displayVATBreakdown(); - updateTotals(); - }); - } -} - -function parseAllowanceCharges(xmlDoc) { - const charges = []; - // Folosim Set pentru a preveni dublarea elementelor din cauza namespace-urilor - const processedIds = new Set(); - - // Selectăm toate elementele AllowanceCharge care sunt copii direcți ai Invoice - const allowanceCharges = xmlDoc.querySelectorAll('cac\\:AllowanceCharge, AllowanceCharge'); - - allowanceCharges.forEach(ac => { - // Verificăm dacă elementul este copil direct al Invoice și nu a fost deja procesat - if (ac.parentElement === xmlDoc.documentElement) { - const amount = getXMLValue(ac, 'cbc\\:Amount, Amount'); - const reasonCode = getXMLValue(ac, 'cbc\\:AllowanceChargeReasonCode, AllowanceChargeReasonCode'); - - // Creăm un ID unic bazat pe cod și valoare pentru a evita duplicatele - const uniqueId = `${reasonCode}-${amount}`; - - if (!processedIds.has(uniqueId)) { - processedIds.add(uniqueId); - - const charge = { - isCharge: getXMLValue(ac, 'cbc\\:ChargeIndicator, ChargeIndicator') === 'true', - reasonCode: reasonCode, - reason: getXMLValue(ac, 'cbc\\:AllowanceChargeReason, AllowanceChargeReason'), - amount: parseFloat(amount) || 0, - baseAmount: parseFloat(getXMLValue(ac, 'cbc\\:BaseAmount, BaseAmount')) || 0, - vatRate: parseFloat(getXMLValue(ac, 'cac\\:TaxCategory cbc\\:Percent, TaxCategory Percent')) || 0, - vatTypeId: getXMLValue(ac, 'cac\\:TaxCategory cbc\\:ID, TaxCategory ID', 'S'), - multiplierFactorNumeric: parseFloat(getXMLValue(ac, 'cbc\\:MultiplierFactorNumeric, MultiplierFactorNumeric')) || 0 - }; - charges.push(charge); - } - } - }); - - return charges; -} - -function displayAllowanceCharges(charges) { - const container = document.getElementById('allowanceCharges'); - container.innerHTML = '

Reduceri și Taxe Suplimentare

'; - - charges.forEach((charge, index) => { - const html = createAllowanceChargeHTML(index, charge); - container.insertAdjacentHTML('beforeend', html); - }); -} - -function addAllowanceCharge() { - const container = document.getElementById('allowanceCharges'); - const index = document.querySelectorAll('.allowance-charge').length; - - // Create new charge with default values - const newCharge = { - isCharge: true, - reasonCode: 'TV', - reason: 'Cheltuieli transport', - amount: 0, - vatRate: 19.0, - vatTypeId: 'S' - }; - - // Add the HTML - const html = createAllowanceChargeHTML(index, newCharge); - container.insertAdjacentHTML('beforeend', html); - - // Setup event listeners - setupAllowanceChargeListeners(index); - - // Force refresh of totals and VAT - refreshTotals(); -} - -function removeAllowanceCharge(index) { - const charge = document.querySelector(`.allowance-charge[data-index="${index}"]`); - if (charge) { - // Remove the element - charge.remove(); - - // Renumber remaining charges - renumberAllowanceCharges(); - - // Clear manual edits and refresh totals - manuallyEditedVatRows.clear(); - refreshTotals(); - } -} - -function renumberAllowanceCharges() { - document.querySelectorAll('.allowance-charge').forEach((charge, newIndex) => { - // Update data-index - charge.dataset.index = newIndex; - - // Update all input names - charge.querySelectorAll('input, select').forEach(input => { - const name = input.getAttribute('name'); - if (name) { - const baseName = name.replace(/\d+$/, ''); - input.setAttribute('name', baseName + newIndex); - } - }); - }); -} - - -function addLineItem() { - const container = document.getElementById('lineItems'); - const index = document.querySelectorAll('.line-item').length; - - // Determină tipul implicit de TVA bazat pe statusul furnizorului - const defaultVatType = isVATRegistered() ? 'S' : 'O'; - const defaultVatRate = isVATRegistered() ? '19' : '0'; - - const lineItemHtml = createLineItemHTML(index, '', '1', '0', defaultVatRate, 'EA', defaultVatType); - container.insertAdjacentHTML('beforeend', lineItemHtml); - - const newItem = container.lastElementChild; - const vatTypeSelect = newItem.querySelector(`[name="vatType${index}"]`); - - if (!isVATRegistered()) { - vatTypeSelect.disabled = true; - newItem.querySelector(`[name="vatRate${index}"]`).disabled = true; - } - - // Adăugăm event handlers pentru noua linie - handleLineItemChange(index); - - manuallyEditedVatRows.clear(); - refreshTotals(); -} - -function removeLineItem(index) { - const lineItem = document.querySelector(`.line-item[data-index="${index}"]`); - if (lineItem) { - lineItem.remove(); - renumberLineItems(); - manuallyEditedVatRows.clear(); // Clear manual edits - refreshTotals(); // Recalculate all totals - } -} - -function handleLineItemChange(index) { - const quantityInput = document.querySelector(`[name="quantity${index}"]`); - const priceInput = document.querySelector(`[name="price${index}"]`); - const discountInput = document.querySelector(`[name="lineDiscount${index}"]`); - const reasonCodeSelect = document.querySelector(`[name="discountReasonCode${index}"]`); - - // Handler pentru schimbarea discount-ului - discountInput?.addEventListener('input', function() { - const discountValue = parseFloat(this.value) || 0; - if (reasonCodeSelect) { - reasonCodeSelect.disabled = discountValue == 0; // activat pentru orice valoare nenulă - // Dacă discount != 0 și nu e selectat niciun cod, selectăm codul implicit - if (discountValue != 0 && !reasonCodeSelect.value) { - reasonCodeSelect.value = '95'; // Cod implicit pentru reducere - } - } - updateTotals(); - }); - - // Handlers pentru cantitate și preț - quantityInput?.addEventListener('change', updateTotals); - priceInput?.addEventListener('change', updateTotals); - - // Force refresh - updateTotals(); -} - -function handleStorno() { - document.querySelectorAll('.line-item').forEach((item, index) => { - const quantityInput = document.querySelector(`[name="quantity${index}"]`); - const currentValue = parseFloat(quantityInput.value); - quantityInput.value = -currentValue; - }); - - document.querySelectorAll('.allowance-charge').forEach((item, index) => { - const amountInput = document.querySelector(`[name="chargeAmount${index}"]`); - const currentAmount = parseFloat(amountInput.value); - amountInput.value = -currentAmount; - }); - - document.querySelectorAll('.vat-row').forEach(row => { - const baseInput = row.querySelector('.vat-base'); - const amountInput = row.querySelector('.vat-amount'); - - if (baseInput) { - const currentBase = parseFloat(baseInput.value) || 0; - baseInput.value = (-currentBase).toFixed(2); - } - - if (amountInput) { - const currentAmount = parseFloat(amountInput.value) || 0; - amountInput.value = (-currentAmount).toFixed(2); - } - }); - - manuallyEditedVatRows.clear(); - refreshTotals(); - - if (currentInvoice) { - updateTaxTotals(currentInvoice); - } -} - -function updateTotals() { - // Calculăm totalurile liniilor (deja nete, după discount) - const lineItemTotals = calculateLineItemTotals(); - const chargeTotals = calculateChargeTotals(); - - const subtotal = lineItemTotals.subtotal; - const allowances = chargeTotals.allowances; - const charges = chargeTotals.charges; - const netAmount = subtotal - allowances + charges; - - // Calculate VAT breakdown - const { vatBreakdown } = calculateVATBreakdown(); - let totalVat = 0; - - vatBreakdown.forEach((entry) => { - if (entry.type === 'S') { - totalVat += entry.vatAmount; - } - }); - - // Display totals with 2 decimal places - displayTotals({ - subtotal: roundNumber(subtotal, 2), - allowances: roundNumber(allowances, 2), - charges: roundNumber(charges, 2), - netAmount: roundNumber(netAmount, 2), - totalVat: roundNumber(totalVat, 2), - total: roundNumber(netAmount + totalVat, 2), - vatBreakdown - }); -} - -function refreshTotals() { - // Calculate line items first - const lineItemTotals = calculateLineItemTotals(); - const chargeTotals = calculateChargeTotals(); - - const subtotal = lineItemTotals.subtotal; - const allowances = chargeTotals.allowances; - const charges = chargeTotals.charges; - const netAmount = subtotal - allowances + charges; - - // Calculate VAT breakdown - const { vatBreakdown } = calculateVATBreakdown(); - let totalVat = 0; - if (vatBreakdown) { - vatBreakdown.forEach(entry => { - totalVat += entry.vatAmount; - }); - } - - // Display totals - displayTotals({ - subtotal, - allowances, - charges, - netAmount, - totalVat, - total: netAmount + totalVat, - vatBreakdown - }); -} - -function calculateLineItemTotals() { - let subtotal = 0; - - document.querySelectorAll('.line-item').forEach((item, index) => { - const quantity = parseFloat(document.querySelector(`[name="quantity${index}"]`).value) || 0; - const price = parseFloat(document.querySelector(`[name="price${index}"]`).value) || 0; - const lineDiscount = parseFloat(document.querySelector(`[name="lineDiscount${index}"]`).value) || 0; - - // LineExtensionAmount = cantitate * preț - discount - const lineAmount = (quantity * price) - lineDiscount; - subtotal += lineAmount; - }); - - return { - subtotal: roundNumber(subtotal, 2) - }; -} - -function calculateChargeTotals() { - let allowances = 0; - let charges = 0; - - document.querySelectorAll('.allowance-charge').forEach((item, index) => { - const isCharge = document.querySelector(`[name="chargeType${index}"]`).value === 'true'; - const amount = parseFloat(document.querySelector(`[name="chargeAmount${index}"]`).value) || 0; - if (isCharge) { - charges += amount; - } else { - allowances += amount; - } - }); - - return { - allowances: roundNumber(allowances, 2), - charges: roundNumber(charges, 2) - }; -} - -function calculateTotals() { - let subtotal = 0; - let vatBreakdown = new Map(); - - // Calculăm totalurile liniilor - document.querySelectorAll('.line-item').forEach((item, index) => { - const quantity = parseFloat(document.querySelector(`[name="quantity${index}"]`).value) || 0; - const price = parseFloat(document.querySelector(`[name="price${index}"]`).value) || 0; - const lineDiscount = parseFloat(document.querySelector(`[name="lineDiscount${index}"]`).value) || 0; - const vatType = document.querySelector(`[name="vatType${index}"]`).value; - const vatRate = parseFloat(document.querySelector(`[name="vatRate${index}"]`).value) || 0; - - // LineExtensionAmount pentru linie = (cantitate * preț) - discount linie - const lineTotal = (quantity * price) - lineDiscount; - subtotal += lineTotal; - - // Actualizăm baza de TVA pentru această linie - const key = `${vatRate}-${vatType}`; - if (!vatBreakdown.has(key)) { - vatBreakdown.set(key, { - baseAmount: 0, - vatAmount: 0, - rate: vatRate, - type: vatType - }); - } - - const entry = vatBreakdown.get(key); - // Adăugăm la baza de TVA valoarea netă a liniei (după discount) - entry.baseAmount += lineTotal; - }); - - // Calculăm discounturile și taxele globale - const { allowances, charges } = calculateChargeTotals(); - - // Calculăm suma netă după aplicarea discount-urilor și taxelor globale - const netAmount = subtotal - allowances + charges; - - // Pentru fiecare discount/taxă globală, ajustăm baza de TVA corespunzătoare - document.querySelectorAll('.allowance-charge').forEach((item, index) => { - const amount = parseFloat(document.querySelector(`[name="chargeAmount${index}"]`).value) || 0; - const vatType = document.querySelector(`[name="chargeVatType${index}"]`).value; - const vatRate = parseFloat(document.querySelector(`[name="chargeVatRate${index}"]`).value) || 0; - const isCharge = document.querySelector(`[name="chargeType${index}"]`).value === 'true'; - - const key = `${vatRate}-${vatType}`; - if (!vatBreakdown.has(key)) { - vatBreakdown.set(key, { - baseAmount: 0, - vatAmount: 0, - rate: vatRate, - type: vatType - }); - } - - const entry = vatBreakdown.get(key); - // Ajustăm baza de TVA în funcție de tipul operațiunii (discount/taxă) - const adjustmentAmount = isCharge ? amount : -amount; - entry.baseAmount += adjustmentAmount; - }); - - // Calculăm TVA-ul pentru fiecare rată - let totalVat = 0; - vatBreakdown.forEach((entry, key) => { - if (entry.type === 'S') { - entry.vatAmount = roundNumber(entry.baseAmount * entry.rate / 100, 2); - totalVat += entry.vatAmount; - } - }); - - return { - subtotal: roundNumber(subtotal), - allowances: roundNumber(allowances), - charges: roundNumber(charges), - netAmount: roundNumber(netAmount), - totalVat: roundNumber(totalVat), - total: roundNumber(netAmount + totalVat), - vatBreakdown - }; -} - -function calculateVATBases(lines, globalAllowances, globalCharges) { - // Initialize VAT bases - const vatBases = {}; - - // First pass: calculate base amounts per VAT rate from lines - lines.forEach(line => { - const vatRate = line.vatRate; - if (!vatBases[vatRate]) { - vatBases[vatRate] = { - base: 0, - allowances: 0, - charges: 0 - }; - } - - const lineTotal = line.quantity * line.price; - const lineAfterDiscount = lineTotal - line.lineDiscount; - vatBases[vatRate].base += lineAfterDiscount; - }); - - // Second pass: apply global allowances and charges per VAT rate - globalAllowances.forEach(allowance => { - const vatRate = allowance.vatRate; - if (vatBases[vatRate]) { - vatBases[vatRate].allowances += allowance.amount; - } - }); - - globalCharges.forEach(charge => { - const vatRate = charge.vatRate; - if (vatBases[vatRate]) { - vatBases[vatRate].charges += charge.amount; - } - }); - - // Calculate final VAT amounts - Object.keys(vatBases).forEach(rate => { - const rateData = vatBases[rate]; - const netBase = rateData.base - rateData.allowances + rateData.charges; - rateData.vatAmount = netBase * (parseFloat(rate) / 100); - }); - - return vatBases; -} - -function calculateTotalVAT() { - let totalVat = Array.from(document.querySelectorAll('.vat-amount')) - .reduce((sum, input) => sum + formatter.parseCurrency(input.value), 0); - return roundNumber(totalVat, 2); -} - -function calculateVATBreakdown() { - let vatBreakdown = new Map(); - let totalVat = 0; - - // Process line items - document.querySelectorAll('.line-item').forEach((item, index) => { - const quantity = parseFloat(document.querySelector(`[name="quantity${index}"]`).value) || 0; - const price = parseFloat(document.querySelector(`[name="price${index}"]`).value) || 0; - const lineDiscount = parseFloat(document.querySelector(`[name="lineDiscount${index}"]`).value) || 0; - const vatType = document.querySelector(`[name="vatType${index}"]`).value; - const vatRate = parseFloat(document.querySelector(`[name="vatRate${index}"]`).value) || 0; - - const lineAmount = (quantity * price) - lineDiscount; - const key = `${vatRate}-${vatType}`; - - if (!vatBreakdown.has(key)) { - vatBreakdown.set(key, { - baseAmount: 0, - vatAmount: 0, - rate: vatRate, - type: vatType - }); - } - - const entry = vatBreakdown.get(key); - entry.baseAmount += lineAmount; - }); - - // Process allowances and charges - document.querySelectorAll('.allowance-charge').forEach((charge, index) => { - const amount = parseFloat(document.querySelector(`[name="chargeAmount${index}"]`).value) || 0; - const vatType = document.querySelector(`[name="chargeVatType${index}"]`).value; - const vatRate = parseFloat(document.querySelector(`[name="chargeVatRate${index}"]`).value) || 0; - const isCharge = document.querySelector(`[name="chargeType${index}"]`).value === 'true'; - - if (amount === 0) return; - - const key = `${vatRate}-${vatType}`; - - if (!vatBreakdown.has(key)) { - vatBreakdown.set(key, { - baseAmount: 0, - vatAmount: 0, - rate: vatRate, - type: vatType - }); - } - - const entry = vatBreakdown.get(key); - const adjustedAmount = isCharge ? amount : -amount; - entry.baseAmount += adjustedAmount; - }); - - // Calculate VAT for each rate - vatBreakdown.forEach((entry) => { - if (entry.type === 'S') { - entry.vatAmount = roundNumber(entry.baseAmount * entry.rate / 100, 2); - totalVat += entry.vatAmount; - } - }); - - return { vatBreakdown, totalVat: roundNumber(totalVat, 2) }; -} - -window.updateVATRow = function(rowId, source) { - const row = document.getElementById(rowId); - if (!row) return; - - const typeSelect = row.querySelector('.vat-type'); - const rateInput = row.querySelector('.vat-rate'); - const baseInput = row.querySelector('.vat-base'); - const amountInput = row.querySelector('.vat-amount'); - - if (source === 'manual') { - manuallyEditedVatRows.add(rowId); - // Keep existing values, just update totals - updateTotalVAT(); - refreshTotals(); - return; - } - - // Only calculate VAT amount for non-manual updates - if (!manuallyEditedVatRows.has(rowId)) { - const type = typeSelect.value; - const rate = parseFloat(rateInput.value) || 0; - const base = formatter.parseCurrency(baseInput.value) || 0; - - const calculatedAmount = type === 'S' ? roundNumber(base * rate / 100, 2) : 0; - amountInput.value = formatter.formatCurrency(calculatedAmount); - - updateTotalVAT(); - refreshTotals(); - } -}; - -window.updateVATRowFromAmount = function(rowId) { - const row = document.getElementById(rowId); - if (!row) return; - - // Just mark as manually edited and update the totals - // Do not recalculate base amount - manuallyEditedVatRows.add(rowId); - - const amountInput = row.querySelector('.vat-amount'); - if (amountInput) { - const value = formatter.parseCurrency(amountInput.value); - amountInput.value = formatter.formatCurrency(value); - } - - let totalVat = 0; - document.querySelectorAll('.vat-row').forEach(vatRow => { - const vatAmount = formatter.parseCurrency(vatRow.querySelector('.vat-amount').value) || 0; - totalVat += vatAmount; - }); - - // Update just total VAT and final total - const netAmount = formatter.parseCurrency(document.getElementById('netAmount').textContent); - document.getElementById('vat').textContent = formatter.formatCurrency(totalVat); - document.getElementById('total').textContent = formatter.formatCurrency(netAmount + totalVat); -}; - -window.removeVATRow = function(rowId) { - const row = document.getElementById(rowId); - if (row) { - manuallyEditedVatRows.delete(rowId); - row.remove(); - updateTotalVAT(); - refreshTotals(); - } -}; - -window.addVATRate = function() { - const container = document.getElementById('vatBreakdownRows'); - addVATBreakdownRow(19, 0, 0); - refreshTotals(); -}; - -function updateTotalVAT() { - const totalVat = Array.from(document.querySelectorAll('.vat-amount')) - .reduce((sum, input) => sum + (parseFloat(input.value) || 0), 0); - - document.getElementById('vat').textContent = totalVat.toFixed(2); - - const netAmount = parseFloat(document.getElementById('netAmount').textContent) || 0; - const total = netAmount + totalVat; - document.getElementById('total').textContent = total.toFixed(2); -} - -function updateVATBreakdown() { - // Șterge și reconstruiește rândurile TVA - const container = document.getElementById('vatBreakdownRows'); - if (!container) return; - - container.innerHTML = ''; - const { vatBreakdown } = calculateVATBreakdown(); - - vatBreakdown.forEach((data, key) => { - const [rate, type] = key.split('-'); - addVATBreakdownRow( - parseFloat(rate), - data.baseAmount, - data.vatAmount, - type - ); - }); -} - -function displayVATBreakdown(xmlDoc = null) { - const container = document.getElementById('vatBreakdownRows'); - if (!container) return; - - // Clear container - container.innerHTML = ''; - - // If XML is provided, use its VAT breakdown - if (xmlDoc && originalTotals && originalTotals.vatBreakdown) { - originalTotals.vatBreakdown.forEach((vat, index) => { - addVATBreakdownRow( - vat.percent, - vat.taxableAmount, - vat.taxAmount, - vat.type, - `vat-row-${index}`, - vat.exemptionCode, - vat.exemptionReason - ); - }); - } else { - // Calculate current VAT breakdown - const { vatBreakdown } = calculateVATBreakdown(); - vatBreakdown.forEach((data, key) => { - const [rate, type] = key.split('-'); - addVATBreakdownRow( - parseFloat(rate), - data.baseAmount, - data.vatAmount, - type - ); - }); - } -} - -function createEmptyInvoice() { - const parser = new DOMParser(); - const xmlString = ` - - urn:cen.eu:en16931:2017#compliant#urn:efactura.mfinante.ro:CIUS-RO:1.0.1 - - - - 380 - RON - - - - - - - - - - - RO - - - - RO - - VAT - - - - - - - - - - - - - - - - - - - - - - RO - - - - - - VAT - - - - - - - - - - - -`; - return parser.parseFromString(xmlString, "text/xml"); -} - -function saveXML() { - if (!validateForm()) return; - - try { - if (!currentInvoice) { - currentInvoice = createEmptyInvoice(); - } - - const xmlDoc = currentInvoice; - - // Update all the data - updateBasicDetails(xmlDoc); - updatePartyDetails(xmlDoc); - updateAllowanceCharges(xmlDoc); - - // Remove existing TaxTotal and LegalMonetaryTotal elements - const existingTaxTotals = xmlDoc.querySelectorAll('cac\\:TaxTotal, TaxTotal'); - existingTaxTotals.forEach(el => el.remove()); - - const existingMonetaryTotal = xmlDoc.querySelector('cac\\:LegalMonetaryTotal, LegalMonetaryTotal'); - if (existingMonetaryTotal) { - existingMonetaryTotal.remove(); - } - - // Remove existing InvoiceLine elements - const existingLines = xmlDoc.querySelectorAll('cac\\:InvoiceLine, InvoiceLine'); - existingLines.forEach(el => el.remove()); - - // Add elements in the correct order - updateTaxTotals(xmlDoc); - updateMonetaryTotals(xmlDoc); - updateLineItems(xmlDoc); - - downloadXML(xmlDoc); - } catch (error) { - handleError(error, 'Eroare la salvarea fișierului XML'); - } -} - -function updateBasicDetails(xmlDoc) { - setXMLValue(xmlDoc, 'cbc\\:ID, ID', document.querySelector('[name="invoiceNumber"]').value); - - // Remove existing Note elements - const existingNotes = xmlDoc.querySelectorAll('cbc\\:Note, Note'); - existingNotes.forEach(note => note.remove()); - - // Split note text and create new Note elements - const noteText = document.querySelector('[name="invoiceNote"]').value; - if (noteText) { - const insertAfter = xmlDoc.querySelector('cbc\\:InvoiceTypeCode, InvoiceTypeCode'); - const chunks = splitNoteIntoChunks(noteText, 300); - chunks.forEach(chunk => { - const noteElement = createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:Note", chunk); - if (insertAfter && insertAfter.parentNode) { - insertAfter.parentNode.insertBefore(noteElement, insertAfter.nextSibling); - } - }); - } - - const issueDateValue = document.querySelector('[name="issueDate"]').value; - const dueDateValue = document.querySelector('[name="dueDate"]').value; - - setXMLValue(xmlDoc, 'cbc\\:IssueDate, IssueDate', parseRomanianDate(issueDateValue)); - setXMLValue(xmlDoc, 'cbc\\:DueDate, DueDate', parseRomanianDate(dueDateValue)); - - // Update currency codes - const documentCurrencyCode = document.querySelector('[name="documentCurrencyCode"]').value.toUpperCase() || 'RON'; - setXMLValue(xmlDoc, 'cbc\\:DocumentCurrencyCode, DocumentCurrencyCode', documentCurrencyCode); - - const taxCurrencyCode = document.querySelector('[name="taxCurrencyCode"]').value.toUpperCase(); - if (taxCurrencyCode) { - let taxCurrencyElement = xmlDoc.querySelector('cbc\\:TaxCurrencyCode, TaxCurrencyCode'); - if (!taxCurrencyElement) { - taxCurrencyElement = createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:TaxCurrencyCode"); - const insertAfter = xmlDoc.querySelector('cbc\\:DocumentCurrencyCode, DocumentCurrencyCode'); - if (insertAfter && insertAfter.parentNode) { - insertAfter.parentNode.insertBefore(taxCurrencyElement, insertAfter.nextSibling); - } - } - taxCurrencyElement.textContent = taxCurrencyCode; - } else { - // Remove TaxCurrencyCode if it exists and is empty - const taxCurrencyElement = xmlDoc.querySelector('cbc\\:TaxCurrencyCode, TaxCurrencyCode'); - if (taxCurrencyElement) { - taxCurrencyElement.parentNode.removeChild(taxCurrencyElement); - } - } -} - -function createPartyElement(xmlDoc, isSupplier, partyData) { - const party = createXMLElement(xmlDoc, XML_NAMESPACES.cac, "cac:Party"); - const hasVatPrefix = /^[A-Z]{2}/.test(partyData.vat?.trim() || ''); - const IsNeplatitor = vatHasO(); - - // Add PartyIdentification - if (partyData.companyId) { - const partyIdentification = createXMLElement(xmlDoc, XML_NAMESPACES.cac, "cac:PartyIdentification"); - partyIdentification.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:ID", partyData.companyId)); - party.appendChild(partyIdentification); - } - - function validateCountryCode(countryCode) { - const code = countryCode?.trim().toUpperCase() || 'RO'; - return ISO_3166_1_CODES.has(code) ? code : 'RO'; - } - - function validateCountyCode(countryCode, countyCode) { - if (countryCode === 'RO') { - return ROMANIAN_COUNTY_CODES.has(countyCode) ? countyCode : 'RO-B'; - } - return countyCode; - } - - const postalAddress = createXMLElement(xmlDoc, XML_NAMESPACES.cac, "cac:PostalAddress"); - postalAddress.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:StreetName", partyData.address)); - postalAddress.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:CityName", partyData.city)); - postalAddress.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:CountrySubentity", partyData.county)); - - const country = createXMLElement(xmlDoc, XML_NAMESPACES.cac, "cac:Country"); - // Ensure country code is valid ISO 3166-1 format (2 uppercase letters) - const countryCode = partyData.country?.trim().toUpperCase() || 'RO'; - country.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:IdentificationCode", countryCode)); - postalAddress.appendChild(country); - party.appendChild(postalAddress); - - const vatTypesInUse = new Set(); - document.querySelectorAll('.line-item').forEach((item, index) => { - const vatType = document.querySelector(`[name="vatType${index}"]`).value; - vatTypesInUse.add(vatType); - }); - - // Add PartyTaxScheme for all suppliers VAT types except 'O' or all customers with VAT prefix - if (hasVatPrefix ) { - const partyTaxScheme = createXMLElement(xmlDoc, XML_NAMESPACES.cac, "cac:PartyTaxScheme"); - partyTaxScheme.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:CompanyID", partyData.vat.toUpperCase())); - const taxScheme = createXMLElement(xmlDoc, XML_NAMESPACES.cac, "cac:TaxScheme"); - if (!IsNeplatitor) { - taxScheme.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:ID", "VAT")); - } - else { - // console.log('not adding VAT ID'); - } - partyTaxScheme.appendChild(taxScheme); - - party.appendChild(partyTaxScheme); - } - - // Funcție pentru detectarea dacă există un VAT cu tipul 'O' Neplatitor de TVA - // În cazul în care există, nu se va adăuga un element TaxScheme cu ID-ul 'VAT' - function vatHasO() { - const vatTypesInUse = new Set(); - document.querySelectorAll('.line-item').forEach((item, index) => { - const vatType = document.querySelector(`[name="vatType${index}"]`).value; - vatTypesInUse.add(vatType); - }); - return vatTypesInUse.size > 0 && vatTypesInUse.has('O'); - } - - // Add PartyLegalEntity - const partyLegalEntity = createXMLElement(xmlDoc, XML_NAMESPACES.cac, "cac:PartyLegalEntity"); - partyLegalEntity.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:RegistrationName", partyData.name)); - - // Add CompanyID - if (!hasVatPrefix) { - // For non-VAT registered supplier, use VAT number in CompanyID - partyLegalEntity.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:CompanyID", partyData.vat)); - } else { - // For others, use companyId - partyLegalEntity.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:CompanyID", partyData.companyId)); - } - - party.appendChild(partyLegalEntity); - - // Add Contact if phone, email or contactName exists - if (partyData.phone || partyData.email || partyData.contactName) { - const contact = createXMLElement(xmlDoc, XML_NAMESPACES.cac, "cac:Contact"); - - if (partyData.contactName) { - contact.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:Name", partyData.contactName)); - } - if (partyData.phone) { - contact.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:Telephone", partyData.phone)); - } - if (partyData.email) { - contact.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:ElectronicMail", partyData.email)); - } - party.appendChild(contact); - } - return party; -} - -function updatePartyDetails(xmlDoc) { - // Update supplier details - const supplierData = { - name: document.querySelector('[name="supplierName"]').value, - vat: document.querySelector('[name="supplierVAT"]').value, - companyId: document.querySelector('[name="supplierCompanyId"]').value, - address: document.querySelector('[name="supplierAddress"]').value, - city: document.querySelector('[name="supplierCity"]').value, - county: document.querySelector('[name="supplierCountrySubentity"]').value, - country: document.querySelector('[name="supplierCountry"]').value, - phone: document.querySelector('[name="supplierPhone"]').value, - contactName: document.querySelector('[name="supplierContactName"]').value, - email: document.querySelector('[name="supplierEmail"]').value - }; - - updatePartyXML(xmlDoc, true, supplierData); - - // Update customer details - const customerData = { - name: document.querySelector('[name="customerName"]').value, - vat: document.querySelector('[name="customerVAT"]').value, - companyId: document.querySelector('[name="customerCompanyId"]').value, - address: document.querySelector('[name="customerAddress"]').value, - city: document.querySelector('[name="customerCity"]').value, - county: document.querySelector('[name="customerCountrySubentity"]').value, - country: document.querySelector('[name="customerCountry"]').value, - phone: document.querySelector('[name="customerPhone"]').value, - contactName: document.querySelector('[name="customerContactName"]').value, - email: document.querySelector('[name="customerEmail"]').value - }; - - updatePartyXML(xmlDoc, false, customerData); -} - -function updatePartyXML(xmlDoc, isSupplier, partyData) { - const partyElement = createPartyElement(xmlDoc, isSupplier, partyData); - const parentTag = isSupplier ? 'AccountingSupplierParty' : 'AccountingCustomerParty'; - let parentElement = xmlDoc.querySelector(`cac\\:${parentTag}, ${parentTag}`); - - if (!parentElement) { - parentElement = createXMLElement(xmlDoc, XML_NAMESPACES.cac, `cac:${parentTag}`); - const insertPoint = isSupplier ? - xmlDoc.querySelector('cbc\\:DocumentCurrencyCode, DocumentCurrencyCode') : - xmlDoc.querySelector('cac\\:AccountingSupplierParty, AccountingSupplierParty'); - - if (insertPoint && insertPoint.parentNode) { - insertPoint.parentNode.insertBefore(parentElement, insertPoint.nextSibling); - } else { - xmlDoc.documentElement.appendChild(parentElement); - } - } - - // Remove existing Party element if it exists - const existingParty = parentElement.querySelector('cac\\:Party, Party'); - if (existingParty) { - existingParty.remove(); - } - - parentElement.appendChild(partyElement); -} - -function getAllowanceCharges() { - const charges = []; - document.querySelectorAll('.allowance-charge').forEach((item, index) => { - charges.push({ - isCharge: document.querySelector(`[name="chargeType${index}"]`).value === 'true', - reasonCode: document.querySelector(`[name="chargeReasonCode${index}"]`).value, - reason: document.querySelector(`[name="chargeReason${index}"]`).value, - amount: parseFloat(document.querySelector(`[name="chargeAmount${index}"]`).value) || 0, - vatRate: parseFloat(document.querySelector(`[name="chargeVatRate${index}"]`).value) || 19.0, - vatTypeId: document.querySelector(`[name="chargeVatType${index}"]`).value || 'S' - }); - }); - return charges; -} - -function updateAllowanceCharges(xmlDoc) { - const currencyID = document.querySelector('[name="documentCurrencyCode"]').value.toUpperCase() || 'RON'; - - // Remove existing global allowances/charges - const existingCharges = xmlDoc.querySelectorAll('Invoice > cac\\:AllowanceCharge, Invoice > AllowanceCharge'); - existingCharges.forEach(charge => charge.remove()); - - // Add global allowances - document.querySelectorAll('.allowance-charge').forEach((item, index) => { - const allowanceElement = createXMLElement(xmlDoc, XML_NAMESPACES.cac, "cac:AllowanceCharge"); - - const isCharge = document.querySelector(`[name="chargeType${index}"]`).value === 'true'; - const amount = parseFloat(document.querySelector(`[name="chargeAmount${index}"]`).value) || 0; - const baseAmount = parseFloat(document.querySelector(`[name="chargeBaseAmount${index}"]`).value) || 0; - const reasonCode = document.querySelector(`[name="chargeReasonCode${index}"]`).value; - const reason = document.querySelector(`[name="chargeReason${index}"]`).value; - const vatTypeId = document.querySelector(`[name="chargeVatType${index}"]`).value; - const vatRate = parseFloat(document.querySelector(`[name="chargeVatRate${index}"]`).value) || 0; - - allowanceElement.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:ChargeIndicator", - isCharge.toString())); - - if (reasonCode) { - allowanceElement.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, - "cbc:AllowanceChargeReasonCode", reasonCode)); - } - - if (reason) { - allowanceElement.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, - "cbc:AllowanceChargeReason", reason)); - } - - // Add multiplier if exists - if (baseAmount > 0) { - const multiplier = (amount / baseAmount) * 100; - allowanceElement.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, - "cbc:MultiplierFactorNumeric", multiplier.toFixed(2))); - } - - allowanceElement.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:Amount", - amount.toFixed(2), { currencyID })); - - if (baseAmount > 0) { - allowanceElement.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:BaseAmount", - baseAmount.toFixed(2), { currencyID })); - } - - // Add tax category - const taxCategory = createXMLElement(xmlDoc, XML_NAMESPACES.cac, "cac:TaxCategory"); - taxCategory.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:ID", vatTypeId)); - taxCategory.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:Percent", - vatRate.toString())); - - const taxScheme = createXMLElement(xmlDoc, XML_NAMESPACES.cac, "cac:TaxScheme"); - taxScheme.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:ID", "VAT")); - taxCategory.appendChild(taxScheme); - - allowanceElement.appendChild(taxCategory); - - // Insert before TaxTotal - const taxTotal = xmlDoc.querySelector('cac\\:TaxTotal, TaxTotal'); - if (taxTotal) { - xmlDoc.documentElement.insertBefore(allowanceElement, taxTotal); - } else { - xmlDoc.documentElement.appendChild(allowanceElement); - } - }); -} - -function updateLineItems(xmlDoc) { - const currencyID = xmlDoc.querySelector('cbc\\:DocumentCurrencyCode, DocumentCurrencyCode').textContent; - - // Șterge liniile existente - const existingLines = xmlDoc.querySelectorAll('cac\\:InvoiceLine, InvoiceLine'); - existingLines.forEach(line => line.remove()); - - document.querySelectorAll('.line-item').forEach((item, index) => { - const invoiceLine = createXMLElement(xmlDoc, XML_NAMESPACES.cac, "cac:InvoiceLine"); - - // Obține valorile liniei - const quantity = document.querySelector(`[name="quantity${index}"]`)?.value || '0'; - const unitCode = document.querySelector(`[name="unit${index}"]`)?.value || 'EA'; - const price = document.querySelector(`[name="price${index}"]`)?.value || '0'; - const description = document.querySelector(`[name="description${index}"]`)?.value || ''; - const itemDescription = document.querySelector(`[name="itemDescription${index}"]`)?.value || ''; - const vatType = document.querySelector(`[name="vatType${index}"]`)?.value || 'S'; - const vatRate = vatType === 'AE' ? '0.00' : - (document.querySelector(`[name="vatRate${index}"]`)?.value || '0'); - const lineDiscount = parseFloat(document.querySelector(`[name="lineDiscount${index}"]`)?.value) || 0; - - // Calculează valorile liniei - const baseLineAmount = quantity * price; - const lineAmount = roundNumber(baseLineAmount - lineDiscount); - - // Adaugă elementele liniei - invoiceLine.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:ID", (index + 1).toString())); - - const quantityElement = createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:InvoicedQuantity", quantity); - quantityElement.setAttribute('unitCode', unitCode); - invoiceLine.appendChild(quantityElement); - - // Adaugă LineExtensionAmount (valoarea după discount) - invoiceLine.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:LineExtensionAmount", - lineAmount.toFixed(2), { currencyID })); - - // Adaugă discount pe linie dacă există - if (lineDiscount != 0) { - const allowanceCharge = createXMLElement(xmlDoc, XML_NAMESPACES.cac, "cac:AllowanceCharge"); - allowanceCharge.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:ChargeIndicator", - lineDiscount < 0 ? "true" : "false")); - - const reasonCode = document.querySelector(`[name="discountReasonCode${index}"]`)?.value || '95'; - allowanceCharge.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, - "cbc:AllowanceChargeReasonCode", reasonCode)); - allowanceCharge.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, - "cbc:AllowanceChargeReason", ALLOWANCE_REASON_CODES[reasonCode] || 'Reducere')); - - // BaseAmount trebuie să fie valoarea netă înainte de discount - const baseAmount = Math.abs(lineAmount); - const discountAmount = Math.abs(lineDiscount); - const multiplierFactor = (discountAmount / baseAmount * 100).toFixed(2); - - allowanceCharge.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, - "cbc:MultiplierFactorNumeric", multiplierFactor)); - - allowanceCharge.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:Amount", - discountAmount.toFixed(2), { currencyID })); - - allowanceCharge.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:BaseAmount", - baseAmount.toFixed(2), { currencyID })); - - invoiceLine.appendChild(allowanceCharge); - } - - // Adaugă detaliile articolului - const itemElement = createXMLElement(xmlDoc, XML_NAMESPACES.cac, "cac:Item"); - - if (itemDescription) { - itemElement.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:Description", - itemDescription)); - } - - itemElement.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:Name", description)); - - // Adaugă identificările - const identificationsContainer = document.querySelector(`#identifications${index}`); - if (identificationsContainer) { - saveIdentificationsToXML(xmlDoc, itemElement, index); - } - - // Adaugă categoria de taxă - const taxCategory = createXMLElement(xmlDoc, XML_NAMESPACES.cac, "cac:ClassifiedTaxCategory"); - taxCategory.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:ID", vatType)); - if (vatType !== 'O') { - taxCategory.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:Percent", vatRate)); - } - - const taxScheme = createXMLElement(xmlDoc, XML_NAMESPACES.cac, "cac:TaxScheme"); - taxScheme.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:ID", "VAT")); - taxCategory.appendChild(taxScheme); - itemElement.appendChild(taxCategory); - - invoiceLine.appendChild(itemElement); - - // Adaugă prețul - const priceElement = createXMLElement(xmlDoc, XML_NAMESPACES.cac, "cac:Price"); - priceElement.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:PriceAmount", - price, { currencyID })); - invoiceLine.appendChild(priceElement); - - xmlDoc.documentElement.appendChild(invoiceLine); - }); -} - -function updateTaxTotals(xmlDoc) { - const currencyID = document.querySelector('[name="documentCurrencyCode"]').value.toUpperCase() || 'RON'; - const taxCurrencyCode = document.querySelector('[name="taxCurrencyCode"]').value.toUpperCase(); - - // Remove existing TaxTotal elements - const existingTaxTotals = xmlDoc.querySelectorAll('cac\\:TaxTotal, TaxTotal'); - existingTaxTotals.forEach(element => element.parentNode.removeChild(element)); - - // Create main TaxTotal for document currency - const taxTotal = createXMLElement(xmlDoc, XML_NAMESPACES.cac, "cac:TaxTotal"); - - // Use precise currency parsing for total VAT - const uiTotalVat = formatter.parseCurrency(document.getElementById('vat').textContent); - const taxAmountElement = createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:TaxAmount", - uiTotalVat.toFixed(2), { currencyID }); - taxTotal.appendChild(taxAmountElement); - - // Add TaxSubtotal elements to the main TaxTotal - const vatRows = document.querySelectorAll('.vat-row'); - vatRows.forEach(row => { - const taxSubtotal = createXMLElement(xmlDoc, XML_NAMESPACES.cac, "cac:TaxSubtotal"); - - const baseAmount = formatter.parseCurrency(row.querySelector('.vat-base').value) || 0; - const vatAmount = formatter.parseCurrency(row.querySelector('.vat-amount').value) || 0; - const vatType = row.querySelector('.vat-type').value; - const vatRate = parseFloat(row.querySelector('.vat-rate').value) || 0; - - taxSubtotal.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:TaxableAmount", - baseAmount.toFixed(2), { currencyID })); - taxSubtotal.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:TaxAmount", - vatAmount.toFixed(2), { currencyID })); - - const taxCategory = createXMLElement(xmlDoc, XML_NAMESPACES.cac, "cac:TaxCategory"); - taxCategory.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:ID", vatType)); - - const percent = vatType === 'AE' ? '0.00' : vatRate.toFixed(2); - taxCategory.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:Percent", percent)); - - // Add exemption code and reason for special VAT types - if (['E', 'K', 'O', 'AE'].includes(vatType)) { - const exemptionCode = row.querySelector('.vat-exemption-code')?.value; - const exemptionReason = row.querySelector('.vat-exemption-reason')?.value; - - if (exemptionCode) { - taxCategory.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, - "cbc:TaxExemptionReasonCode", exemptionCode)); - } - if (exemptionReason) { - taxCategory.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, - "cbc:TaxExemptionReason", exemptionReason)); - } - } - - const taxScheme = createXMLElement(xmlDoc, XML_NAMESPACES.cac, "cac:TaxScheme"); - taxScheme.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:ID", "VAT")); - taxCategory.appendChild(taxScheme); - - taxSubtotal.appendChild(taxCategory); - taxTotal.appendChild(taxSubtotal); - }); - - // Insert TaxTotal in the correct position - let insertionPoint = xmlDoc.querySelector('cac\\:AllowanceCharge, AllowanceCharge'); - if (insertionPoint) { - while (insertionPoint.nextElementSibling && - (insertionPoint.nextElementSibling.localName === 'AllowanceCharge' || - insertionPoint.nextElementSibling.localName === 'TaxTotal')) { - insertionPoint = insertionPoint.nextElementSibling; - } - insertionPoint.parentNode.insertBefore(taxTotal, insertionPoint.nextSibling); - } else { - const monetaryTotal = xmlDoc.querySelector('cac\\:LegalMonetaryTotal, LegalMonetaryTotal'); - if (monetaryTotal) { - monetaryTotal.parentNode.insertBefore(taxTotal, monetaryTotal); - } else { - xmlDoc.documentElement.appendChild(taxTotal); - } - } - - // If tax currency is specified, add another TaxTotal element - if (taxCurrencyCode && taxCurrencyCode !== currencyID) { - const taxCurrencyTotal = createXMLElement(xmlDoc, XML_NAMESPACES.cac, "cac:TaxTotal"); - - const exchangeRateInput = document.querySelector('[name="exchangeRate"]'); - const exchangeRate = exchangeRateInput ? parseFloat(exchangeRateInput.value) || 1 : 1; - const taxCurrencyVAT = uiTotalVat * exchangeRate; - - const taxCurrencyAmountElement = createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:TaxAmount", - taxCurrencyVAT.toFixed(2), { currencyID: taxCurrencyCode }); - taxCurrencyTotal.appendChild(taxCurrencyAmountElement); - - // Insert after the main TaxTotal - taxTotal.parentNode.insertBefore(taxCurrencyTotal, taxTotal.nextSibling); - } -} - -function updateMonetaryTotals(xmlDoc) { - const currencyID = document.querySelector('[name="documentCurrencyCode"]').value.toUpperCase() || 'RON'; - - // Use direct parsing to preserve full precision and manual edits - const subtotal = formatter.parseCurrency(document.getElementById('subtotal').textContent); - const allowances = formatter.parseCurrency(document.getElementById('totalAllowances').textContent); - const charges = formatter.parseCurrency(document.getElementById('totalCharges').textContent); - const netAmount = formatter.parseCurrency(document.getElementById('netAmount').textContent); - const totalVat = formatter.parseCurrency(document.getElementById('vat').textContent); - const total = formatter.parseCurrency(document.getElementById('total').textContent); - - // Rest of the function remains the same... - const existingMonetaryTotal = xmlDoc.querySelector('cac\\:LegalMonetaryTotal, LegalMonetaryTotal'); - if (existingMonetaryTotal) { - existingMonetaryTotal.parentNode.removeChild(existingMonetaryTotal); - } - - const monetaryTotal = createXMLElement(xmlDoc, XML_NAMESPACES.cac, "cac:LegalMonetaryTotal"); - - const amounts = { - "LineExtensionAmount": subtotal, - "TaxExclusiveAmount": netAmount, - "TaxInclusiveAmount": total, - "AllowanceTotalAmount": allowances, - "ChargeTotalAmount": charges, - "PayableAmount": total - }; - - Object.entries(amounts).forEach(([elementName, value]) => { - monetaryTotal.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, - `cbc:${elementName}`, value.toFixed(2), { currencyID })); - }); - - // Insertion logic remains the same - let insertionPoint = xmlDoc.querySelector('cac\\:TaxTotal, TaxTotal'); - if (insertionPoint) { - while (insertionPoint.nextElementSibling && - insertionPoint.nextElementSibling.localName === 'TaxTotal') { - insertionPoint = insertionPoint.nextElementSibling; - } - insertionPoint.parentNode.insertBefore(monetaryTotal, insertionPoint.nextSibling); - } else { - const firstInvoiceLine = xmlDoc.querySelector('cac\\:InvoiceLine, InvoiceLine'); - if (firstInvoiceLine) { - firstInvoiceLine.parentNode.insertBefore(monetaryTotal, firstInvoiceLine); - } else { - xmlDoc.documentElement.appendChild(monetaryTotal); - } - } -} - -function getExchangeRate(fromCurrency, toCurrency) { - // For now, return 1 as default exchange rate - // TODO: Implement proper exchange rate handling - return 1; -} - -// Update the invoice form to include exchange rate when tax currency is different -function addExchangeRateField() { - const taxCurrencyInput = document.querySelector('[name="taxCurrencyCode"]'); - const documentCurrencyInput = document.querySelector('[name="documentCurrencyCode"]'); - - function updateExchangeRateVisibility() { - const taxCurrency = taxCurrencyInput.value.toUpperCase(); - const documentCurrency = documentCurrencyInput.value.toUpperCase(); - - let exchangeRateContainer = document.getElementById('exchangeRateContainer'); - if (taxCurrency && taxCurrency !== documentCurrency) { - if (!exchangeRateContainer) { - const container = document.createElement('div'); - container.id = 'exchangeRateContainer'; - container.className = 'form-group'; - container.innerHTML = ` - - - `; - taxCurrencyInput.parentNode.after(container); - } else { - // Update label if currencies changed - const label = exchangeRateContainer.querySelector('label'); - label.textContent = `Curs Valutar ${documentCurrency}/${taxCurrency}`; - } - } else if (exchangeRateContainer) { - exchangeRateContainer.remove(); - } - } - - taxCurrencyInput.addEventListener('input', updateExchangeRateVisibility); - documentCurrencyInput.addEventListener('input', updateExchangeRateVisibility); - - // Initial check - updateExchangeRateVisibility(); -} - - -// Utility functions -function getXMLValue(xmlDoc, selector, defaultValue = '') { - if (!xmlDoc) return defaultValue; - try { - const element = xmlDoc.querySelector(selector); - const value = element ? element.textContent : defaultValue; - // console.log(`getXMLValue: Selector: ${selector}, Value: ${value}`); - return value; - } catch (error) { - console.warn(`Eroare la obținerea valorii pentru selectorul ${selector}:`, error); - return defaultValue; - } -} - -function setXMLValue(xmlDoc, selector, value) { - try { - const element = xmlDoc.querySelector(selector); - if (element) { - element.textContent = value; - return true; - } - return false; - } catch (error) { - console.warn(`Eroare la setarea valorii pentru selectorul ${selector}:`, error); - return false; - } -} - -function createXMLElement(xmlDoc, namespace, elementName, value = '', attributes = {}) { - const element = xmlDoc.createElementNS(namespace, elementName); - if (value) { - element.textContent = value; - } - Object.entries(attributes).forEach(([key, value]) => { - element.setAttribute(key, value); - }); - return element; -} - -function formatXML(xmlString) { - let formatted = ''; - let indent = ''; - const tab = ' '; - - xmlString.split(/>\s* { - if (node.match(/^\/\w/)) { - indent = indent.substring(tab.length); - } - formatted += indent + '<' + node + '>\r\n'; - if (node.match(/^]*[^\/]$/) && !node.startsWith("?")) { - indent += tab; - } - }); - - return formatted.substring(1, formatted.length - 3); -} - -function downloadXML(xmlDoc) { - const serializer = new XMLSerializer(); - let xmlString = serializer.serializeToString(xmlDoc); - - if (!xmlString.startsWith('\n' + xmlString; - } - - xmlString = formatXML(xmlString); - - const blob = new Blob([xmlString], { type: 'application/xml' }); - const url = URL.createObjectURL(blob); - const a = document.createElement('a'); - a.href = url; - a.download = 'factura_' + document.querySelector('[name="invoiceNumber"]').value + '.xml'; - document.body.appendChild(a); - a.click(); - document.body.removeChild(a); - URL.revokeObjectURL(url); -} - -function roundNumber(number, decimals = 2) { - return Math.round(number * Math.pow(10, decimals)) / Math.pow(10, decimals); -} - -function renumberLineItems() { - document.querySelectorAll('.line-item').forEach((item, newIndex) => { - item.dataset.index = newIndex; - - item.querySelectorAll('input, select').forEach(input => { - const name = input.getAttribute('name'); - if (name) { - const baseName = name.replace(/\d+$/, ''); - input.setAttribute('name', baseName + newIndex); - } - }); - }); -} - - -function createIdentificationHTML(index, type, value = '', schemeId = '') { - const typeInfo = IDENTIFICATION_TYPES[type]; - const id = `${type.toLowerCase()}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; - - if (type === 'COMMODITY') { - return ` -
-
- - - -
-
`; - } - - return ` -
-
- ${type === 'STANDARD' ? `` : ''} - - -
-
`; -} - -window.addIdentification = function(lineItemIndex, type) { - const container = document.querySelector(`#identifications${lineItemIndex}`); - if (container) { - container.insertAdjacentHTML('beforeend', createIdentificationHTML(lineItemIndex, type)); - validateIdentifications(lineItemIndex); - } -} - -window.removeIdentification = function(id) { - const element = document.querySelector(`[data-id="${id}"]`); - if (element) { - const lineItem = element.closest('.line-item'); - element.remove(); - if (lineItem) { - validateIdentifications(parseInt(lineItem.dataset.index)); - } - } -}; - - -// Update XML parsing -function parseIdentifications(itemElement, lineItemIndex) { - // console.log("Parsing identifications for line", lineItemIndex); - - const container = document.querySelector(`#identifications${lineItemIndex}`); - if (!container) return; - - const listContainer = container.querySelector('.identifications-list'); - listContainer.innerHTML = ''; - - // Parse BuyersItemIdentification - const buyersId = itemElement.querySelector('cac\\:BuyersItemIdentification cbc\\:ID, BuyersItemIdentification ID'); - if (buyersId) { - listContainer.insertAdjacentHTML('beforeend', - createIdentificationHTML(lineItemIndex, 'BUYERS', buyersId.textContent) - ); - } - - // Parse SellersItemIdentification - const sellersId = itemElement.querySelector('cac\\:SellersItemIdentification cbc\\:ID, SellersItemIdentification ID'); - if (sellersId) { - listContainer.insertAdjacentHTML('beforeend', - createIdentificationHTML(lineItemIndex, 'SELLERS', sellersId.textContent) - ); - } - - // Parse StandardItemIdentification - const standardId = itemElement.querySelector('cac\\:StandardItemIdentification cbc\\:ID, StandardItemIdentification ID'); - if (standardId) { - listContainer.insertAdjacentHTML('beforeend', - createIdentificationHTML(lineItemIndex, 'STANDARD', standardId.textContent) - ); - } - - // Parse CommodityClassifications - const commodityClassifications = itemElement.querySelectorAll('cac\\:CommodityClassification cbc\\:ItemClassificationCode, CommodityClassification ItemClassificationCode'); - commodityClassifications.forEach(classification => { - const listId = classification.getAttribute('listID') || 'CV'; - const code = classification.textContent; - if (code && listId) { - listContainer.insertAdjacentHTML('beforeend', - createIdentificationHTML(lineItemIndex, 'COMMODITY', code, listId) - ); - } - }); -} - -// Update XML saving -function saveIdentificationsToXML(xmlDoc, itemElement, lineItemIndex) { - const container = document.querySelector(`#identifications${lineItemIndex}`); - if (!container) return; - - container.querySelectorAll('.identification-row').forEach(row => { - const type = row.dataset.type; - const id = row.dataset.id; - const schemeInput = document.querySelector(`[name="scheme_${id}"]`); - const valueInput = document.querySelector(`[name="value_${id}"]`); - - if (!valueInput?.value) return; - - if (type === 'COMMODITY') { - const schemeSelect = document.querySelector(`[name="scheme_${id}"]`); - if (!schemeSelect) return; - - const identificationElement = createXMLElement(xmlDoc, XML_NAMESPACES.cac, 'cac:CommodityClassification'); - const idElement = createXMLElement(xmlDoc, XML_NAMESPACES.cbc, 'cbc:ItemClassificationCode', valueInput.value); - idElement.setAttribute('listID', schemeSelect.value); - identificationElement.appendChild(idElement); - itemElement.appendChild(identificationElement); - } else { - const typeInfo = IDENTIFICATION_TYPES[type]; - const identificationElement = createXMLElement(xmlDoc, XML_NAMESPACES.cac, `cac:${typeInfo.xmlTag}`); - const idElement = createXMLElement(xmlDoc, XML_NAMESPACES.cbc, 'cbc:ID', valueInput.value); - - if (type === 'STANDARD') { - idElement.setAttribute('schemeID', '0160'); - } - - identificationElement.appendChild(idElement); - itemElement.appendChild(identificationElement); - } - }); -} - - -// Add validation -function validateIdentifications(lineItemIndex) { - const container = document.querySelector(`#identifications${lineItemIndex}`); - if (!container) return true; - - let isValid = true; - container.querySelectorAll('.identification-row').forEach(row => { - const type = row.dataset.type; - const id = row.dataset.id; - const input = row.querySelector(`[name="value_${id}"]`); - - if (type === 'COMMODITY') { - const scheme = row.querySelector('.scheme-select').value; - if (scheme === 'TSP' && !/^\d{8}-\d$/.test(input.value)) { - input.classList.add('invalid'); - isValid = false; - } else if (scheme === 'STI' && !/^\d{8}$/.test(input.value)) { - input.classList.add('invalid'); - isValid = false; - } else { - input.classList.remove('invalid'); - } - } - }); - - return isValid; -} - -// Export for testing if needed -if (typeof module !== 'undefined' && module.exports) { - module.exports = { - calculateTotals, - validateForm, - roundNumber, - formatXML, - createXMLElement, - getXMLValue, - setXMLValue, - formatter, - setupInlineEditing, - updateTotalDisplay, - displayTotals, - updateVATDisplay, - getDisplayValue - }; -} - -import { InvoicePrintHandler } from './print.js'; - -// Create print handler instance -const printHandler = new InvoicePrintHandler(); - -// Initialize when the document is ready -document.addEventListener('DOMContentLoaded', () => { - // Add print controls to the UI - const headerButtonGroup = document.querySelector('.button-group'); - if (headerButtonGroup) { - const printControls = document.createElement('div'); - printControls.className = 'print-controls'; - printControls.style.display = 'flex'; - printControls.style.gap = '8px'; - printControls.style.alignItems = 'center'; - - // Create template selector - const templateSelect = document.createElement('select'); - templateSelect.className = 'form-input'; - templateSelect.style.width = 'auto'; - templateSelect.innerHTML = ` - - - `; - templateSelect.addEventListener('change', (e) => { - printHandler.setTemplate(e.target.value); - }); - - // Create print button - const printButton = document.createElement('button'); - printButton.className = 'button'; - printButton.onclick = () => printHandler.print(); - printButton.innerHTML = 'Printează'; - - // Add elements to controls - printControls.appendChild(templateSelect); - printControls.appendChild(printButton); - - // Add controls to header - headerButtonGroup.appendChild(printControls); - } +import { InvoiceFormatter } from './formatter.js'; +import { + Big, parseStrict, parseStrictOr, formatRaw, format2, + setRaw, getRaw, markDirty, lineTotal as numericLineTotal, wireDatasetRaw, withinTolerance +} from './numeric.js'; +import { getJSON, setJSON, cacheGet, cacheSet } from './storage.js'; +import JSZip from './vendor/jszip.mjs'; +import { validateCIF } from './validation/cif.js'; +import { validateIBAN } from './validation/iban.js'; +import { runBRRules } from './validation/br-ro.js'; +import { probeReceiver, anafValidate, anafCifLookup } from './anaf.js'; +import { catalogAdd, catalogSearch, catalogDelete } from './catalog.js'; + +// Constants +const XML_NAMESPACES = { + ubl: "urn:oasis:names:specification:ubl:schema:xsd:Invoice-2", + cbc: "urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2", + cac: "urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2" +}; + +const VAT_TYPES = { + "S": "Cotă Standard", + "AE": "Taxare Inversă", + "O": "Neplătitor TVA", + "Z": "Cotă 0% TVA", + "E": "Neimpozabil" +}; + +// PR-TIPURI (A4): UN/CEFACT 1001 InvoiceTypeCode — subset folosit în RO. +// 380 = Factură comercială (default) +// 381 = Notă de credit (storno) +// 384 = Factură corectată (rectificare) +// 389 = Autofactură (self-billed) +const INVOICE_TYPES = { + '380': 'Factură comercială', + '381': 'Notă de credit', + '384': 'Factură corectată', + '389': 'Autofactură' +}; + +// PR-TODO (A5): cod-uri UN/CEFACT 4461 PaymentMeansCode folosite frecvent în RO. +const PAYMENT_MEANS_CODES = { + '30': 'Transfer bancar', + '10': 'Numerar', + '48': 'Card', + '58': 'Transfer SEPA', + '42': 'Plată în cont furnizor', + '49': 'Debit direct', + '97': 'Compensare', + '1': 'Instrument neidentificat' +}; + +const VAT_EXEMPTION_CODES = { + 'AE': { + code: 'VATEX-EU-AE', + reason: 'Taxare inversa' + }, + 'K': { + code: 'VATEX-EU-IC', + reason: 'Livrare intracomunitara' + }, + 'O': { + code: 'VATEX-EU-O', + reason: 'Neplatitor TVA' + }, + 'E': [ + { + code: '', + reason: 'Scutit' + }, + { + code: 'VATEX-EU-F', + reason: 'Bunuri second hand' + }, + { + code: 'VATEX-EU-D', + reason: 'Regim special agentii de turism' + } + ] +}; + +const UNIT_CODES = new Map([ + ['EA', 'Bucată (EA)'], + ['XPP', 'Bucată (XPP)'], + ['KGM', 'Kilogram (KGM)'], + ['MTR', 'Metri (MTR)'], + ['LTR', 'Litru (LTR)'], + ['H87', 'Bucată (H87)'], + ['MTQ', 'Metri cubi (MTQ)'] +]); + +const ISO_3166_1_CODES = new Set([ + 'AD', 'AE', 'AF', 'AG', 'AI', 'AL', 'AM', 'AO', 'AQ', 'AR', 'AS', 'AT', 'AU', 'AW', 'AX', + 'AZ', 'BA', 'BB', 'BD', 'BE', 'BF', 'BG', 'BH', 'BI', 'BJ', 'BL', 'BM', 'BN', 'BO', 'BQ', + 'BR', 'BS', 'BT', 'BV', 'BW', 'BY', 'BZ', 'CA', 'CC', 'CD', 'CF', 'CG', 'CH', 'CI', 'CK', + 'CL', 'CM', 'CN', 'CO', 'CR', 'CU', 'CV', 'CW', 'CX', 'CY', 'CZ', 'DE', 'DJ', 'DK', 'DM', + 'DO', 'DZ', 'EC', 'EE', 'EG', 'EH', 'ER', 'ES', 'ET', 'FI', 'FJ', 'FK', 'FM', 'FO', 'FR', + 'GA', 'GB', 'GD', 'GE', 'GF', 'GG', 'GH', 'GI', 'GL', 'GM', 'GN', 'GP', 'GQ', 'GR', 'GS', + 'GT', 'GU', 'GW', 'GY', 'HK', 'HM', 'HN', 'HR', 'HT', 'HU', 'ID', 'IE', 'IL', 'IM', 'IN', + 'IO', 'IQ', 'IR', 'IS', 'IT', 'JE', 'JM', 'JO', 'JP', 'KE', 'KG', 'KH', 'KI', 'KM', 'KN', + 'KP', 'KR', 'KW', 'KY', 'KZ', 'LA', 'LB', 'LC', 'LI', 'LK', 'LR', 'LS', 'LT', 'LU', 'LV', + 'LY', 'MA', 'MC', 'MD', 'ME', 'MF', 'MG', 'MH', 'MK', 'ML', 'MM', 'MN', 'MO', 'MP', 'MQ', + 'MR', 'MS', 'MT', 'MU', 'MV', 'MW', 'MX', 'MY', 'MZ', 'NA', 'NC', 'NE', 'NF', 'NG', 'NI', + 'NL', 'NO', 'NP', 'NR', 'NU', 'NZ', 'OM', 'PA', 'PE', 'PF', 'PG', 'PH', 'PK', 'PL', 'PM', + 'PN', 'PR', 'PS', 'PT', 'PW', 'PY', 'QA', 'RE', 'RO', 'RS', 'RU', 'RW', 'SA', 'SB', 'SC', + 'SD', 'SE', 'SG', 'SH', 'SI', 'SJ', 'SK', 'SL', 'SM', 'SN', 'SO', 'SR', 'SS', 'ST', 'SV', + 'SX', 'SY', 'SZ', 'TC', 'TD', 'TF', 'TG', 'TH', 'TJ', 'TK', 'TL', 'TM', 'TN', 'TO', 'TR', + 'TT', 'TV', 'TW', 'TZ', 'UA', 'UG', 'UM', 'US', 'UY', 'UZ', 'VA', 'VC', 'VE', 'VG', 'VI', + 'VN', 'VU', 'WF', 'WS', 'XI', 'YE', 'YT', 'ZA', 'ZM', 'ZW' +]); + +const ROMANIAN_COUNTY_CODES = new Set([ + 'RO-AB', 'RO-AG', 'RO-AR', 'RO-B', 'RO-BC', 'RO-BH', 'RO-BN', 'RO-BR', 'RO-BT', 'RO-BV', + 'RO-BZ', 'RO-CJ', 'RO-CL', 'RO-CS', 'RO-CT', 'RO-CV', 'RO-DB', 'RO-DJ', 'RO-GJ', 'RO-GL', + 'RO-GR', 'RO-HD', 'RO-HR', 'RO-IF', 'RO-IL', 'RO-IS', 'RO-MH', 'RO-MM', 'RO-MS', 'RO-NT', + 'RO-OT', 'RO-PH', 'RO-SB', 'RO-SJ', 'RO-SM', 'RO-SV', 'RO-TL', 'RO-TM', 'RO-TR', 'RO-VL', + 'RO-VN', 'RO-VS' +]); + +const CHARGE_REASON_CODES = { + 'TV': 'Cheltuieli de transport', + 'FC': 'Taxe transport', + 'ZZZ': 'Definite reciproc' +}; + +const ALLOWANCE_REASON_CODES = { + '95': 'Reducere', + '41': 'Bonus lucrări în avans', + '42': 'Alt bonus', + '60': 'Reducere volum', + '62': 'Alte reduceri', + '63': 'Reducere producător', + '64': 'Din cauza războiului', + '65': 'Reducere outlet nou', + '66': 'Reducere mostre', + '67': 'Reducere end-of-range', + '68': 'Cost ambalaj returnabil', + '70': 'Reducere Incoterm', + '71': 'Prag vânzări', + '88': 'Suprataxă/deducere materiale', + '100': 'Reducere specială', + '102': 'Termen lung fix', + '103': 'Temporar', + '104': 'Standard', + '105': 'Cifră de afaceri anuală' +}; + +// Structure for item identifications +const IDENTIFICATION_TYPES = { + SELLERS: { + type: 'sellers', + label: 'Cod Furnizor', + xmlTag: 'SellersItemIdentification' + }, + BUYERS: { + type: 'buyers', + label: 'Cod Client', + xmlTag: 'BuyersItemIdentification' + }, + STANDARD: { + type: 'standard', + label: 'Cod Bare', + xmlTag: 'StandardItemIdentification', + schemeID: '0160' + }, + COMMODITY: { + type: 'commodity', + label: 'Cod Clasificare', + xmlTag: 'CommodityClassification', + schemes: [ + { id: 'CV', name: 'Cod Vamal' }, + { id: 'TSP', name: 'Cod CPV' }, + { id: 'STI', name: 'Cod NC8' } + ] + } +}; + +const formatter = new InvoiceFormatter() + +const resolver = { + lookupNamespaceURI: prefix => { + const ns = { + 'cbc': 'urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2', + 'cac': 'urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2', + 'ubl': 'urn:oasis:names:specification:ubl:schema:xsd:Invoice-2' + }; + return ns[prefix] || null; + } +}; + +// Global variables +let currentInvoice = null; +let originalTotals = null; +let vatRates = new Map(); +let manuallyEditedVatRows = new Set(); + +// PR-A14: Multi-XML bulk state +let _multiXmlFiles = []; // [{name, content, dirty}] +let _activeFileIdx = -1; // index în _multiXmlFiles (-1 = single-file mode) +let _loadingFile = false; // suprimă dirty events cât timp parseXML populează formularul + +// Initialize event listeners +document.addEventListener('DOMContentLoaded', async function() { + document.getElementById('fileInput').addEventListener('change', handleFileSelect); + setupDragAndDrop(); + initializeUI(); + + if (!currentInvoice) { + currentInvoice = createEmptyInvoice(); + } + + const totalElements = [ + 'subtotal', 'totalAllowances', 'totalCharges', + 'netAmount', 'vat', 'total' + ]; + + totalElements.forEach(elementId => { + const element = document.getElementById(elementId); + setupInlineEditing(element); + }); + + // Add currency code validation + const currencyInputs = document.querySelectorAll('[name="documentCurrencyCode"], [name="taxCurrencyCode"]'); + currencyInputs.forEach(input => { + input.addEventListener('input', function(e) { + // Convert to uppercase + this.value = this.value.toUpperCase(); + // Remove any non-letter characters + this.value = this.value.replace(/[^A-Z]/g, ''); + // Limit to 3 characters + if (this.value.length > 3) { + this.value = this.value.slice(0, 3); + } + }); + }); + + // Make document currency code required + const documentCurrencyInput = document.querySelector('[name="documentCurrencyCode"]'); + if (documentCurrencyInput) { + documentCurrencyInput.required = true; + } + + const supplierVATInput = document.querySelector('[name="supplierVAT"]'); + if (supplierVATInput) { + supplierVATInput.addEventListener('change', function() { + updateAllVATTypes(); + }); + } + + addExchangeRateField(); + + initializeLocationSelectors(); + + // Verifică dacă avem parametru XML în URL + const urlParams = new URLSearchParams(window.location.search); + const xmlFileName = urlParams.get('xml'); + + if (xmlFileName) { + try { + // Încarcă XML-ul din fișierul temporar + const response = await fetch('temp/' + xmlFileName); + if (response.ok) { + const xmlContent = await response.text(); + parseXML(xmlContent); + + // Curăță fișierul temporar + fetch('receiver.php?cleanup=' + xmlFileName) + .catch(error => console.error('Eroare la ștergerea fișierului temporar:', error)); + } + } catch (error) { + console.error('Eroare la încărcarea XML:', error); + } + } + + // Pre-populare număr factură din secvență dacă câmpul e gol + const numEl = document.querySelector('[name="invoiceNumber"]'); + if (numEl && !numEl.value) { + const seq = _seqRead(); + numEl.value = _seqFormat(seq); + } +}); + +// Initialize event listeners for existing line items +document.querySelectorAll('.line-item').forEach((item, index) => { + handleLineItemChange(index); +}); + +// Inline editing setup function +function setupInlineEditing(element) { + let originalValue; + + element.addEventListener('click', function() { + this.setAttribute('contenteditable', 'true'); + // Store exact displayed value + originalValue = this.textContent; + this.focus(); + }); + + element.addEventListener('blur', function() { + this.setAttribute('contenteditable', 'false'); + if (this.textContent !== originalValue) { + updateTotals(); + } + }); + + element.addEventListener('keydown', function(event) { + if (event.key === 'Enter') { + event.preventDefault(); + this.blur(); + } + }); +} + +function updateTotalDisplay(elementId, value) { + const element = document.getElementById(elementId); + if (element) { + // Convertim la număr folosind parseFloat pentru a evita probleme de truncare + const numValue = parseFloat(value); + element.textContent = formatter.formatCurrency(numValue); + } +} + +function displayTotals(totals) { + // Actualizează toate totalurile cu formatare + updateTotalDisplay('subtotal', totals.subtotal); + updateTotalDisplay('totalAllowances', totals.allowances); + updateTotalDisplay('totalCharges', totals.charges); + updateTotalDisplay('netAmount', totals.netAmount); + updateTotalDisplay('vat', totals.totalVat); + updateTotalDisplay('total', totals.total); + + // Actualizează defalcarea TVA dacă există + const container = document.getElementById('vatBreakdownRows'); + if (container) { + container.innerHTML = ''; + if (totals.vatBreakdown) { + totals.vatBreakdown.forEach((data, key) => { + const [rate, type] = key.split('-'); + addVATBreakdownRow( + parseFloat(rate), + data.baseAmount, + data.vatAmount, + type + ); + }); + } + } +} + +function updateVATDisplay(row, amount, type = 'amount') { + const input = row.querySelector(`.vat-${type}`); + if (input) { + input.value = formatter.formatCurrency(amount); + } +} + +function validateVATExemption() { + const vatRows = document.querySelectorAll('.vat-row'); + let isValid = true; + + vatRows.forEach(row => { + const vatType = row.querySelector('.vat-type').value; + if (['E', 'K', 'AE', 'O'].includes(vatType)) { + const exemptionCodeInput = row.querySelector('.vat-exemption-code'); + const exemptionReasonInput = row.querySelector('.vat-exemption-reason'); + const exemptionCode = exemptionCodeInput?.value; + const exemptionReason = exemptionReasonInput?.value; + + // Pentru neplătitori de TVA (tip O), verificăm să aibă valorile corecte + if (vatType === 'O') { + if (exemptionCode !== 'VATEX-EU-O') { + isValid = false; + exemptionCodeInput?.classList.add('invalid'); + } + } + // Pentru celelalte tipuri de scutire, trebuie să aibă cel puțin unul dintre câmpuri completat + else if (!exemptionCode && !exemptionReason) { + isValid = false; + exemptionCodeInput?.classList.add('invalid'); + exemptionReasonInput?.classList.add('invalid'); + } else { + exemptionCodeInput?.classList.remove('invalid'); + exemptionReasonInput?.classList.remove('invalid'); + } + } + }); + + return isValid; +} + +function addDynamicVatExemptionCode(vatType, exemptionCode, exemptionReason) { + if (!vatType || !exemptionCode) return; + + if (vatType === 'E') { + // Verifică dacă codul există deja + const exists = VAT_EXEMPTION_CODES.E.some(e => e.code === exemptionCode); + if (!exists) { + VAT_EXEMPTION_CODES.E.push({ + code: exemptionCode, + reason: exemptionReason || exemptionCode + }); + } + } else if (!VAT_EXEMPTION_CODES[vatType]) { + // Adaugă un nou tip de TVA dacă nu există + VAT_EXEMPTION_CODES[vatType] = { + code: exemptionCode, + reason: exemptionReason || exemptionCode + }; + } +} + +function getDisplayValue(elementId) { + const element = document.getElementById(elementId); + return element ? formatter.parseCurrency(element.textContent) : 0; +} + +// Event delegation for dynamic elements +document.addEventListener('click', function(event) { + const target = event.target; + + if (target.matches('.delete-line-item')) { + const lineItem = target.closest('.line-item'); + if (lineItem) { + removeLineItem(parseInt(lineItem.dataset.index)); + } + } + + if (target.matches('.delete-allowance-charge')) { + const charge = target.closest('.allowance-charge'); + if (charge) { + removeAllowanceCharge(parseInt(charge.dataset.index)); + } + } +}); + +// File handling functions + +// ZIP magic bytes: "PK\x03\x04" — local file header signature. +async function isZipFile(file) { + if (!file || file.size < 4) return false; + const head = await file.slice(0, 4).arrayBuffer(); + const b = new Uint8Array(head); + return b[0] === 0x50 && b[1] === 0x4B && b[2] === 0x03 && b[3] === 0x04; +} + +// Încarcă un fișier (XML simplu sau ZIP cu un XML eFactura înăuntru). +// Detectează ZIP pe magic bytes (PK\x03\x04). Fallback la text() pentru XML. +async function loadInvoiceFile(file) { + if (!file) return; + try { + if (await isZipFile(file)) { + const buf = await file.arrayBuffer(); + const zip = await JSZip.loadAsync(buf); + const xmlEntries = Object.values(zip.files).filter(f => !f.dir && f.name.toLowerCase().endsWith('.xml')); + if (xmlEntries.length === 0) { + alert('Arhiva ZIP nu conține niciun fișier .xml.'); + return; + } + // Primul .xml din arhivă (sortat după nume pentru determinism). + xmlEntries.sort((a, b) => a.name.localeCompare(b.name)); + const xmlContent = await xmlEntries[0].async('string'); + parseXML(xmlContent); + } else { + const xmlContent = await file.text(); + parseXML(xmlContent); + } + } catch (err) { + console.error('Eroare la încărcarea fișierului:', err); + alert('Nu s-a putut încărca fișierul: ' + (err && err.message ? err.message : err)); + } +} + +function handleFileSelect(event) { + const files = event.target.files; + if (!files || files.length === 0) return; + if (files.length === 1) { + loadInvoiceFile(files[0]); + } else { + loadMultipleFiles(Array.from(files)); + } + // Reset input so same file(s) can be re-selected + event.target.value = ''; +} + +// ───────────────────────────────────────────────────────── +// PR-A14: Multi-XML bulk mode +// ───────────────────────────────────────────────────────── + +/** + * Citește conținutul text al unui fișier XML sau al primului XML dintr-un ZIP. + * @param {File} file + * @returns {Promise} conținut XML sau null la eroare + */ +async function _readFileContent(file) { + try { + if (await isZipFile(file)) { + const buf = await file.arrayBuffer(); + const zip = await JSZip.loadAsync(buf); + const xmlEntries = Object.values(zip.files) + .filter(f => !f.dir && f.name.toLowerCase().endsWith('.xml')); + if (xmlEntries.length === 0) return null; + xmlEntries.sort((a, b) => a.name.localeCompare(b.name)); + return await xmlEntries[0].async('string'); + } else { + return await file.text(); + } + } catch (err) { + console.error('_readFileContent:', file.name, err); + return null; + } +} + +/** + * Încarcă mai multe fișiere XML/ZIP în modul bulk. + * - Citește fișierele async cu yield între ele (responsivitate UI). + * - Aplică limitarea la 50 de fișiere, cu toast de avertizare. + * - Afișează sidebar-ul și încarcă primul fișier în formular. + * @param {File[]} files + */ +async function loadMultipleFiles(files) { + const MAX = 50; + const existing = _multiXmlFiles.length; + const available = Math.max(0, MAX - existing); + let dropped = 0; + + if (files.length > available) { + dropped = files.length - available; + files = files.slice(0, available); + } + + if (files.length === 0) { + if (dropped > 0) { + showToast(`Limita de ${MAX} fișiere atinsă — ${dropped} fișier(e) ignorat(e).`, 'warning'); + } + return; + } + + // Citire async cu yield între iterații + const newEntries = []; + for (const file of files) { + const content = await _readFileContent(file); + if (content !== null) { + newEntries.push({ name: file.name, content, dirty: false }); + } + // yield pentru ca UI să rămână responsiv + await new Promise(r => setTimeout(r, 0)); + } + + if (newEntries.length === 0) { + showToast('Niciun fișier XML valid găsit.', 'error'); + return; + } + + const wasEmpty = _multiXmlFiles.length === 0; + _multiXmlFiles.push(...newEntries); + + if (dropped > 0) { + showToast(`Limita de ${MAX} fișiere: ${dropped} fișier(e) ignorat(e).`, 'warning'); + } + + // Prima activare: dacă nu era niciun fișier activ sau era single-file mode + if (wasEmpty) { + _activeFileIdx = 0; + } + // Altfel păstrăm indexul activ curent + + _renderXmlSidebar(); + _activateFile(_activeFileIdx, false /* nu salva — tocmai am intrat în bulk mode */); +} + +/** + * Salvează starea curentă a formularului înapoi în _multiXmlFiles[idx].content. + * Apelat înaintea switch-ului de fișier. + */ +function _saveCurrentFileState() { + if (_activeFileIdx < 0 || _activeFileIdx >= _multiXmlFiles.length) return; + const xml = _currentXMLString(); + if (xml) { + _multiXmlFiles[_activeFileIdx].content = xml; + } +} + +/** + * Activează fișierul la indexul dat: parsează XML-ul în formular. + * @param {number} idx + * @param {boolean} [saveFirst=true] dacă să salveze starea fișierului activ înainte de switch + */ +function _activateFile(idx, saveFirst = true) { + if (idx < 0 || idx >= _multiXmlFiles.length) return; + + if (saveFirst && _activeFileIdx >= 0 && _activeFileIdx !== idx) { + _saveCurrentFileState(); + } + + _activeFileIdx = idx; + const entry = _multiXmlFiles[idx]; + + _loadingFile = true; + parseXML(entry.content); + // Suprimă dirty events generate de popularea formularului + queueMicrotask(() => { _loadingFile = false; }); + + _renderXmlSidebar(); +} + +/** + * Marchează fișierul activ ca dirty (modificat). + * Apelat din event delegation pe formularul de editare. + */ +function _markActiveDirty() { + if (_loadingFile) return; + if (_activeFileIdx < 0 || _activeFileIdx >= _multiXmlFiles.length) return; + if (_multiXmlFiles[_activeFileIdx].dirty) return; // deja marcat + _multiXmlFiles[_activeFileIdx].dirty = true; + _updateSidebarItem(_activeFileIdx); +} + +/** Redă întregul sidebar cu lista de fișiere. */ +function _renderXmlSidebar() { + const hasBulk = _multiXmlFiles.length >= 2; + document.body.classList.toggle('has-xml-sidebar', hasBulk); + + if (!hasBulk) { + const old = document.getElementById('xml-sidebar'); + if (old) old.remove(); + return; + } + + let sidebar = document.getElementById('xml-sidebar'); + if (!sidebar) { + sidebar = document.createElement('div'); + sidebar.id = 'xml-sidebar'; + sidebar.setAttribute('role', 'navigation'); + sidebar.setAttribute('aria-label', 'Fișiere XML'); + document.body.insertBefore(sidebar, document.body.firstChild); + } + + sidebar.innerHTML = ''; + + // Header + const hdr = document.createElement('div'); + hdr.className = 'xml-sidebar-header'; + hdr.textContent = `Fișiere (${_multiXmlFiles.length})`; + sidebar.appendChild(hdr); + + // Items + const list = document.createElement('ul'); + list.className = 'xml-sidebar-list'; + _multiXmlFiles.forEach((entry, idx) => { + const li = _buildSidebarItem(entry, idx); + list.appendChild(li); + }); + sidebar.appendChild(list); +} + +/** Construiește un
  • pentru un fișier din sidebar. */ +function _buildSidebarItem(entry, idx) { + const li = document.createElement('li'); + li.className = 'xml-sidebar-item' + (idx === _activeFileIdx ? ' is-active' : ''); + li.dataset.idx = idx; + li.title = entry.name; + + const nameSpan = document.createElement('span'); + nameSpan.className = 'xml-sidebar-item-name'; + nameSpan.textContent = entry.name; + + const dirtyDot = document.createElement('span'); + dirtyDot.className = 'xml-sidebar-dirty' + (entry.dirty ? ' is-dirty' : ''); + dirtyDot.title = entry.dirty ? 'Modificat (nesalvat)' : ''; + dirtyDot.setAttribute('aria-hidden', 'true'); + + li.appendChild(nameSpan); + li.appendChild(dirtyDot); + + li.addEventListener('click', () => { + if (idx === _activeFileIdx) return; + _activateFile(idx, true); + }); + + return li; +} + +/** Actualizează doar un item din sidebar (fără re-render complet). */ +function _updateSidebarItem(idx) { + const sidebar = document.getElementById('xml-sidebar'); + if (!sidebar) return; + const list = sidebar.querySelector('.xml-sidebar-list'); + if (!list) return; + const items = list.querySelectorAll('.xml-sidebar-item'); + if (!items[idx]) return; + const entry = _multiXmlFiles[idx]; + const old = items[idx]; + const replacement = _buildSidebarItem(entry, idx); + list.replaceChild(replacement, old); +} + +// Expune pe window pentru butonul "Salvează XML" în bulk mode +window.saveCurrentFileAndMark = function() { + if (_activeFileIdx < 0) return; + _saveCurrentFileState(); + _multiXmlFiles[_activeFileIdx].dirty = false; + _updateSidebarItem(_activeFileIdx); +}; + +// ───────────────────────────────────────────────────────── + +// Drag-and-drop: acceptă XML sau ZIP la drop pe oriunde în pagină. +function setupDragAndDrop() { + const target = document.body; + const prevent = (e) => { e.preventDefault(); e.stopPropagation(); }; + ['dragenter', 'dragover'].forEach(ev => { + target.addEventListener(ev, (e) => { + if (e.dataTransfer && Array.from(e.dataTransfer.types || []).includes('Files')) { + prevent(e); + e.dataTransfer.dropEffect = 'copy'; + target.classList.add('drag-over'); + } + }); + }); + ['dragleave', 'dragend'].forEach(ev => { + target.addEventListener(ev, (e) => { + if (e.target === target) target.classList.remove('drag-over'); + }); + }); + target.addEventListener('drop', (e) => { + if (!e.dataTransfer || !e.dataTransfer.files || e.dataTransfer.files.length === 0) return; + prevent(e); + target.classList.remove('drag-over'); + const files = Array.from(e.dataTransfer.files); + if (files.length === 1) { + loadInvoiceFile(files[0]); + } else { + loadMultipleFiles(files); + } + }); +} + +function parseXML(xmlContent) { + try { + const parser = new DOMParser(); + const xmlDoc = parser.parseFromString(xmlContent, "text/xml"); + + const parserError = xmlDoc.querySelector('parsererror'); + if (parserError) { + throw new Error('Eroare la parsarea XML: ' + parserError.textContent); + } + + // Extrage și adaugă codurile de scutire din TaxTotal + const taxSubtotals = xmlDoc.querySelectorAll('cac\\:TaxSubtotal, TaxSubtotal'); + taxSubtotals.forEach(subtotal => { + const taxCategory = subtotal.querySelector('cac\\:TaxCategory, TaxCategory'); + if (taxCategory) { + const vatType = getXMLValue(taxCategory, 'cbc\\:ID, ID'); + const exemptionCode = getXMLValue(taxCategory, 'cbc\\:TaxExemptionReasonCode, TaxExemptionReasonCode'); + const exemptionReason = getXMLValue(taxCategory, 'cbc\\:TaxExemptionReason, TaxExemptionReason'); + + if (vatType && exemptionCode) { + addDynamicVatExemptionCode(vatType, exemptionCode, exemptionReason); + } + } + }); + + currentInvoice = xmlDoc; + manuallyEditedVatRows.clear(); + + populateBasicDetails(xmlDoc); + populatePartyDetails(xmlDoc); + populateBillingReference(xmlDoc); + populatePaymentMeans(xmlDoc); + populateAllowanceCharges(xmlDoc); + populateLineItems(xmlDoc); + storeOriginalTotals(xmlDoc); + restoreOriginalTotals(); + displayVATBreakdown(xmlDoc); + + // PR-A11: după ce toate elementele sunt populate, rulează math + // validation pentru a popula badge-urile (per-line + per-VAT-row + footer). + if (typeof validateMath === 'function') validateMath(); + + // PR-BR: inițializează panelul de validare după load XML. + _updateBRPanel(); + } catch (error) { + handleError(error, 'Eroare la parsarea fișierului XML'); + } +} + +// Create allowance charge HTML +function createAllowanceChargeHTML(index, charge) { + return ` +
    +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + +
    + `; +} + +function createReasonCodeOptions(isCharge, selectedCode = '') { + const codes = isCharge ? CHARGE_REASON_CODES : ALLOWANCE_REASON_CODES; + return Object.entries(codes) + .map(([code, description]) => + `` + ).join(''); +} + +window.updateReasonCodeOptions = function(index) { + const chargeTypeSelect = document.querySelector(`[name="chargeType${index}"]`); + const reasonCodeSelect = document.querySelector(`[name="chargeReasonCode${index}"]`); + const reasonInput = document.querySelector(`[name="chargeReason${index}"]`); + + const isCharge = chargeTypeSelect.value === 'true'; + reasonCodeSelect.innerHTML = createReasonCodeOptions(isCharge); + + // Update reason text based on selected code + const selectedCode = reasonCodeSelect.value; + const codes = isCharge ? CHARGE_REASON_CODES : ALLOWANCE_REASON_CODES; + reasonInput.value = codes[selectedCode] || ''; +} + +// Create line item HTML +function createLineItemHTML(index, description = '', quantity = '1', price = '0', vatRate = '19', + unitCode = 'EA', vatTypeId = 'S', itemDescription = '', lineDiscount = '0', discountReasonCode = '') { + + return ` +
    +
    +
    + +
    + +
    +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + +
    + Total linie: + 0,00 + +
    + +
    + +
    + + + + +
    + `; +} + +function createLineDiscountReasonOptions(selectedCode = '') { + return Object.entries(ALLOWANCE_REASON_CODES) + .map(([code, description]) => + `` + ).join(''); +} + +// Add VAT breakdown row +let _vatRowCounter = 0; +function addVATBreakdownRow(rate, baseAmount, vatAmount, vatType = 'S', existingRowId = null, exemptionCode = '', exemptionReason = '') { + const container = document.getElementById('vatBreakdownRows'); + // ID unic chiar dacă mai multe rânduri sunt create în același ms (Date.now() + // singur cauza coliziuni → querySelector(#id) pică pe primul match și + // dataset.raw al rândurilor noi era fie suprascris, fie nesetat → BR-CO-15 + // false positive după Recalculează Totaluri. + const rowId = existingRowId || `vat-row-${Date.now()}-${++_vatRowCounter}`; + + const rowHtml = ` +
    +
    +
    + + + + % + + + + + +
    +
    +
    + + +
    +
    + + +
    +
    + +
    +
    + `; + + container.insertAdjacentHTML('beforeend', rowHtml); + + // PR-E E1: seed dataset.raw pentru rate/base/amount + wire blur listeners. + const rateEl = document.querySelector(`#${rowId} .vat-rate`); + const baseEl = document.querySelector(`#${rowId} .vat-base`); + const amountEl = document.querySelector(`#${rowId} .vat-amount`); + [ + { el: rateEl, val: rate, decimals: 2 }, + { el: baseEl, val: baseAmount, decimals: 2 }, + { el: amountEl, val: vatAmount, decimals: 2 } + ].forEach(f => { + if (!f.el) return; + const big = parseStrict(f.val); + if (big !== null) f.el.dataset.raw = big.toFixed(f.decimals); + wireDatasetRaw(f.el, f.decimals); + }); + + // Add event listener for VAT type changes + const vatTypeSelect = document.querySelector(`#${rowId} .vat-type`); + vatTypeSelect.addEventListener('change', () => { + const exemptionContainer = document.querySelector(`#${rowId} .vat-exemption`); + const newVatType = vatTypeSelect.value; + + if (['E', 'K', 'O', 'AE'].includes(newVatType)) { + exemptionContainer.classList.remove('hidden'); + + // Update exemption code options + const codeSelect = document.querySelector(`#${rowId} .vat-exemption-code`); + codeSelect.innerHTML = generateExemptionCodeOptions(newVatType); + + // Set default values + const defaultExemption = getDefaultExemption(newVatType); + if (defaultExemption) { + codeSelect.value = defaultExemption.code; + document.querySelector(`#${rowId} .vat-exemption-reason`).value = defaultExemption.reason; + } + } else { + exemptionContainer.classList.add('hidden'); + } + }); +} + +function generateExemptionCodeOptions(vatType, selectedCode = '') { + if (vatType === 'E') { + return VAT_EXEMPTION_CODES.E.map(exemption => + `` + ).join(''); + } else if (vatType === 'K' || vatType === 'AE' || vatType === 'O') { + const exemption = VAT_EXEMPTION_CODES[vatType]; + return ``; + } + return ''; +} + +function getDefaultExemption(vatType) { + if (vatType === 'E') { + return VAT_EXEMPTION_CODES.E[0]; + } + return VAT_EXEMPTION_CODES[vatType]; +} + +// Add this to window object +window.updateExemptionReason = function(rowId) { + const row = document.getElementById(rowId); + if (!row) return; + + const vatType = row.querySelector('.vat-type').value; + const codeSelect = row.querySelector('.vat-exemption-code'); + const reasonInput = row.querySelector('.vat-exemption-reason'); + + if (vatType === 'E') { + const selectedExemption = VAT_EXEMPTION_CODES.E.find(e => e.code === codeSelect.value); + if (selectedExemption) { + reasonInput.value = selectedExemption.reason; + } + } else if (vatType === 'K' || vatType === 'AE') { + reasonInput.value = VAT_EXEMPTION_CODES[vatType].reason; + } +}; + +// Toggle optional details +window.toggleOptionalDetails = function(index) { + const optionalDetails = document.getElementById(`optionalDetails${index}`); + const button = optionalDetails.previousElementSibling.querySelector('button'); + + if (optionalDetails.style.display === 'none') { + optionalDetails.style.display = 'block'; + button.innerHTML = '▲ Detalii Suplimentare'; + } else { + optionalDetails.style.display = 'none'; + button.innerHTML = '▼ Detalii Suplimentare'; + } +} + +// Form validation +function validateForm(silent = false) { + let isValid = true; + let firstInvalidField = null; + + // Validare câmpuri existente... + const requiredFields = [ + 'invoiceNumber', + 'issueDate', + 'dueDate', + 'supplierName', + 'supplierVAT', + 'customerName', + ]; + + requiredFields.forEach(fieldName => { + const field = document.querySelector(`[name="${fieldName}"]`); + if (!field || !field.value.trim()) { + field.classList.add('invalid'); + isValid = false; + if (!firstInvalidField) + firstInvalidField = field; + } else { + field.classList.remove('invalid'); + } + }); + + // Adaugă validarea TVA + const vatExemptionValid = validateVATExemption(); + if (!vatExemptionValid) { + isValid = false; + if (!silent) { + alert('Vă rugăm să completați codul și/sau motivul scutirii de TVA pentru toate categoriile care necesită această informație.'); + } + } + + // Restul validărilor existente... + const lineItems = document.querySelectorAll('.line-item'); + if (lineItems.length === 0) { + isValid = false; + if (!silent) { + alert('Este necesară cel puțin o linie în factură'); + } + return false; + } + + lineItems.forEach((item, index) => { + const quantity = parseFloat(document.querySelector(`[name="quantity${index}"]`).value); + const price = parseFloat(document.querySelector(`[name="price${index}"]`).value); + const description = document.querySelector(`[name="description${index}"]`).value; + + if (!description.trim()) { + document.querySelector(`[name="description${index}"]`).classList.add('invalid'); + isValid = false; + } + if (isNaN(quantity)) { + document.querySelector(`[name="quantity${index}"]`).classList.add('invalid'); + isValid = false; + } + if (isNaN(price)) { + document.querySelector(`[name="price${index}"]`).classList.add('invalid'); + isValid = false; + } + }); + + const dateInputs = document.querySelectorAll('.date-input'); + dateInputs.forEach(input => { + if (!validateDateInput(input)) { + isValid = false; + if (!firstInvalidField) firstInvalidField = input; + } + }); + + // Validare curs valutar dacă este cazul + const taxCurrencyCode = document.querySelector('[name="taxCurrencyCode"]').value.trim(); + const documentCurrencyCode = document.querySelector('[name="documentCurrencyCode"]').value.trim(); + + if (taxCurrencyCode && taxCurrencyCode !== documentCurrencyCode) { + const exchangeRate = document.querySelector('[name="exchangeRate"]'); + if (!exchangeRate || !exchangeRate.value || parseFloat(exchangeRate.value) <= 0) { + exchangeRate.classList.add('invalid'); + isValid = false; + if (!firstInvalidField) { + firstInvalidField = exchangeRate; + } + } else { + exchangeRate.classList.remove('invalid'); + } + } + + if (!isValid && !silent) { + if (firstInvalidField) { + firstInvalidField.focus(); + } + alert('Vă rugăm să completați toate câmpurile obligatorii'); + } + + return isValid; +} + +function handleError(error, message) { + console.error(message, error); + alert(`${message}\nVă rugăm să verificați consola pentru detalii.`); +} + +function formatDateToRomanian(date) { + const day = String(date.getDate()).padStart(2, '0'); + const month = String(date.getMonth() + 1).padStart(2, '0'); + const year = date.getFullYear(); + return `${day}.${month}.${year}`; +} + +function parseRomanianDate(dateStr) { + const [day, month, year] = dateStr.split('.'); + return `${year}-${month}-${day}`; +} + +function createDatePicker(input, button) { + const picker = new Pikaday({ + field: input, + trigger: button, + format: 'DD.MM.YYYY', + i18n: { + previousMonth: 'Luna anterioară', + nextMonth: 'Luna următoare', + months: ['Ianuarie', 'Februarie', 'Martie', 'Aprilie', 'Mai', 'Iunie', 'Iulie', 'August', 'Septembrie', 'Octombrie', 'Noiembrie', 'Decembrie'], + weekdays: ['Duminică', 'Luni', 'Marți', 'Miercuri', 'Joi', 'Vineri', 'Sâmbătă'], + weekdaysShort: ['Dum', 'Lun', 'Mar', 'Mie', 'Joi', 'Vin', 'Sâm'] + }, + firstDay: 1, + onSelect: function(date) { + input.value = formatDateToRomanian(date); + validateDateInput(input); + } + }); + return picker; +} + +function validateDateInput(input) { + const value = input.value; + + // Câmp gol = valid (regula "required" e tratată separat în validateForm). + if (value === '') { + input.classList.remove('invalid'); + return true; + } + + const regex = /^(\d{2})\.(\d{2})\.(\d{4})$/; + const match = value.match(regex); + + if (match) { + const day = parseInt(match[1]); + const month = parseInt(match[2]); + const year = parseInt(match[3]); + + const date = new Date(year, month - 1, day); + if (date.getFullYear() === year && date.getMonth() === month - 1 && date.getDate() === day) { + input.classList.remove('invalid'); + return true; + } + } + + input.classList.add('invalid'); + return false; +} + +function restrictDateInput(input) { + input.addEventListener('input', function(e) { + let value = e.target.value; + + // Remove any non-digit characters except dots + value = value.replace(/[^\d.]/g, ''); + + // Auto-add dots after day and month + if (value.length >= 2 && value.charAt(2) !== '.') { + value = value.slice(0, 2) + '.' + value.slice(2); + } + if (value.length >= 5 && value.charAt(5) !== '.') { + value = value.slice(0, 5) + '.' + value.slice(5); + } + + // Restrict to exactly 10 characters (dd.mm.yyyy) + value = value.slice(0, 10); + + e.target.value = value; + }); + + input.addEventListener('blur', function() { + validateDateInput(input); + }); +} + +function createCountryOptions() { + return Array.from(ISO_3166_1_CODES).map(code => + `` + ).join(''); +} + +function createCountyOptions() { + return Array.from(ROMANIAN_COUNTY_CODES).map(code => { + const label = code.replace('RO-', ''); + const counties = { + 'AB': 'Alba', 'AR': 'Arad', 'AG': 'Argeș', 'BC': 'Bacău', 'BH': 'Bihor', + 'BN': 'Bistrița-Năsăud', 'BT': 'Botoșani', 'BV': 'Brașov', 'BR': 'Brăila', + 'B': 'București', 'BZ': 'Buzău', 'CS': 'Caraș-Severin', 'CL': 'Călărași', + 'CJ': 'Cluj', 'CT': 'Constanța', 'CV': 'Covasna', 'DB': 'Dâmbovița', + 'DJ': 'Dolj', 'GL': 'Galați', 'GR': 'Giurgiu', 'GJ': 'Gorj', 'HR': 'Harghita', + 'HD': 'Hunedoara', 'IL': 'Ialomița', 'IS': 'Iași', 'IF': 'Ilfov', + 'MM': 'Maramureș', 'MH': 'Mehedinți', 'MS': 'Mureș', 'NT': 'Neamț', + 'OT': 'Olt', 'PH': 'Prahova', 'SM': 'Satu Mare', 'SJ': 'Sălaj', + 'SB': 'Sibiu', 'SV': 'Suceava', 'TR': 'Teleorman', 'TM': 'Timiș', + 'TL': 'Tulcea', 'VS': 'Vaslui', 'VL': 'Vâlcea', 'VN': 'Vrancea' + }; + return ``; + }).join(''); +} + +function initializeLocationSelectors() { + // Replace country inputs with selects for both supplier and customer + ['supplier', 'customer'].forEach(party => { + const countryInput = document.querySelector(`[name="${party}Country"]`); + if (countryInput) { + const select = document.createElement('select'); + select.className = 'form-input'; + select.name = countryInput.name; + select.innerHTML = createCountryOptions(); + select.value = countryInput.dataset.xmlValue || countryInput.value || 'RO'; + countryInput.parentNode?.replaceChild(select, countryInput); + } + + const countyInput = document.querySelector(`[name="${party}CountrySubentity"]`); + if (countyInput) { + const select = document.createElement('select'); + select.className = 'form-input'; + select.name = countyInput.name; + select.innerHTML = createCountyOptions(); + select.value = countyInput.dataset.xmlValue || countyInput.value || ''; + countyInput.parentNode?.replaceChild(select, countyInput); + } + }); + + // Initialize location handlers for both parties + setupPartyLocationHandlers('supplier'); + setupPartyLocationHandlers('customer'); +} + +function updateCountyVisibility(countrySelect, countySelect) { + if (!countrySelect || !countySelect) return; + const showCounty = countrySelect.value === 'RO'; + countySelect.style.display = showCounty ? 'block' : 'none'; + countySelect.required = showCounty; +} + +function setupPartyLocationHandlers(party) { + const countrySelect = document.querySelector(`[name="${party}Country"]`); + const countySelect = document.querySelector(`[name="${party}CountrySubentity"]`); + const cityContainer = document.querySelector(`[name="${party}City"]`)?.parentNode; + + if (!countrySelect || !countySelect || !cityContainer) return; + + const handleLocationChange = () => { + const isBucharest = countrySelect.value === 'RO' && countySelect.value === 'RO-B'; + const currentElement = cityContainer.querySelector('input, select'); + const isCurrentlySector = currentElement.tagName.toLowerCase() === 'select'; + + if (isBucharest && !isCurrentlySector) { + const sectorSelect = document.createElement('select'); + sectorSelect.className = 'form-input'; + sectorSelect.name = `${party}City`; + sectorSelect.innerHTML = ` + + + + + + + + `; + + // Try to preserve any existing sector value + const currentValue = currentElement.value || ''; + if (currentValue.toUpperCase().includes('SECTOR')) { + sectorSelect.value = currentValue.toUpperCase().replace(/\s+/g, ''); + } + + cityContainer.replaceChild(sectorSelect, currentElement); + } else if (!isBucharest && isCurrentlySector) { + const cityInput = document.createElement('input'); + cityInput.type = 'text'; + cityInput.className = 'form-input'; + cityInput.name = `${party}City`; + cityInput.value = ''; + cityContainer.replaceChild(cityInput, currentElement); + } + }; + + // Set up event listeners + countrySelect.addEventListener('change', () => { + updateCountyVisibility(countrySelect, countySelect); + handleLocationChange(); + }); + + countySelect.addEventListener('change', handleLocationChange); + + // Initial setup + updateCountyVisibility(countrySelect, countySelect); + handleLocationChange(); +} + +// ============================================================================ +// PR-VALID-IDS (A9 + A10): Helpers validare CIF și IBAN pe blur. +// Afișează eroare inline sub câmp: border --danger + bg --danger-soft + +// mesaj 11px. Curăță eroarea la re-focus sau la câmp valid. +// ============================================================================ + +/** + * Afișează sau actualizează mesajul de eroare sub un câmp de input. + * @param {HTMLInputElement} input + * @param {string} errorId — ID-ul elementului helper text (ex: "supplierVAT-error") + * @param {string} message — mesajul de afișat + */ +function _setFieldError(input, errorId, message) { + input.style.borderColor = 'var(--danger)'; + input.style.backgroundColor = 'var(--danger-soft)'; + input.setAttribute('aria-invalid', 'true'); + input.setAttribute('aria-describedby', errorId); + + let helper = document.getElementById(errorId); + if (!helper) { + helper = document.createElement('span'); + helper.id = errorId; + helper.style.cssText = 'display:block;font-size:11px;color:var(--danger);margin-top:3px;'; + input.parentNode.appendChild(helper); + } + helper.textContent = message; +} + +/** + * Curăță eroarea de validare de pe un câmp. + * @param {HTMLInputElement} input + * @param {string} errorId + */ +function _clearFieldError(input, errorId) { + input.style.borderColor = ''; + input.style.backgroundColor = ''; + input.removeAttribute('aria-invalid'); + input.removeAttribute('aria-describedby'); + + const helper = document.getElementById(errorId); + if (helper) helper.remove(); +} + +/** + * Wire blur + validare CIF pe un câmp dat. + * @param {HTMLInputElement} input + * @param {string} errorId + */ +function _wireCIFValidation(input, errorId) { + if (!input) return; + input.addEventListener('blur', function() { + const result = validateCIF(this.value); + if (!result.valid) { + _setFieldError(this, errorId, result.message); + } else { + _clearFieldError(this, errorId); + } + }); + // Curăță eroarea la tastare (nu re-validează — doar curăță indicatorul vizual) + input.addEventListener('input', function() { + _clearFieldError(this, errorId); + }); +} + +/** + * Wire blur + validare IBAN pe un câmp dat. + * @param {HTMLInputElement} input + * @param {string} errorId + */ +function _wireIBANValidation(input, errorId) { + if (!input) return; + input.addEventListener('blur', function() { + const result = validateIBAN(this.value); + if (!result.valid) { + _setFieldError(this, errorId, result.message); + } else { + _clearFieldError(this, errorId); + } + }); + input.addEventListener('input', function() { + _clearFieldError(this, errorId); + }); +} + +function initializeUI() { + document.querySelectorAll('.form-input').forEach(input => { + input.addEventListener('input', function() { + this.classList.remove('invalid'); + updateTotals(); + }); + }); + + document.addEventListener('keydown', function(event) { + if (event.ctrlKey || event.metaKey) { + switch (event.key.toLowerCase()) { + case 's': + event.preventDefault(); + saveXML(); + break; + case 'o': + event.preventDefault(); + document.getElementById('fileInput').click(); + break; + case 'n': + event.preventDefault(); + addLineItem(); + break; + } + } + }); + + // Initialize date pickers + const dateInputs = document.querySelectorAll('.date-input'); + dateInputs.forEach(input => { + const button = input.parentElement.querySelector('.calendar-button'); + createDatePicker(input, button); + restrictDateInput(input); + }); + + if (!currentInvoice) { + const today = new Date(); + const dueDate = new Date(); + dueDate.setDate(today.getDate() + 30); + + document.querySelector('[name="issueDate"]').value = formatDateToRomanian(today); + document.querySelector('[name="dueDate"]').value = formatDateToRomanian(dueDate); + } + + // Add note counter event listener + const noteInput = document.querySelector('[name="invoiceNote"]'); + if (noteInput) { + noteInput.addEventListener('input', updateNoteCounter); + updateNoteCounter(); // Initial count + } + + // Initialize location selectors + initializeLocationSelectors(); + + window.addLineItem = addLineItem; + window.removeLineItem = removeLineItem; + window.addAllowanceCharge = addAllowanceCharge; + window.removeAllowanceCharge = removeAllowanceCharge; + window.addPaymentMeansRow = addPaymentMeansRow; + window.removePaymentMeansRow = removePaymentMeansRow; + window.handleStorno = handleStorno; + window.updateTotals = updateTotals; + window.saveXML = saveXML; + window.refreshTotals = refreshTotals; + window.displayVATBreakdown = displayVATBreakdown; + + // A12: expune butoane profil furnizor și inițializează starea. + window.saveSupplierProfile = saveSupplierProfile; + window.useSupplierProfile = useSupplierProfile; + window.deleteSupplierProfile = deleteSupplierProfile; + _updateProfileButtons(); + + // PR-VALID-IDS (A9 + A10): wire CIF și IBAN pe blur. + _wireCIFValidation(document.querySelector('[name="supplierVAT"]'), 'supplierVAT-error'); + _wireCIFValidation(document.querySelector('[name="customerVAT"]'), 'customerVAT-error'); + + // IBAN: delegare pe containerul de Payment Means (câmpuri dinamice). + const pmContainer = document.getElementById('paymentMeansRows'); + if (pmContainer) { + pmContainer.addEventListener('blur', function(e) { + const input = e.target; + if (!input || !input.name || !input.name.startsWith('paymentMeansIBAN')) return; + const errorId = input.name + '-error'; + const result = validateIBAN(input.value); + if (!result.valid) { + _setFieldError(input, errorId, result.message); + } else { + _clearFieldError(input, errorId); + } + }, true); // capture=true pentru blur care nu bubblează + pmContainer.addEventListener('input', function(e) { + const input = e.target; + if (!input || !input.name || !input.name.startsWith('paymentMeansIBAN')) return; + _clearFieldError(input, input.name + '-error'); + }); + } + + // PR-A14: dirty tracking — marcăm fișierul activ la orice editare a formularului + const invoiceForm = document.getElementById('invoiceForm'); + if (invoiceForm) { + invoiceForm.addEventListener('input', () => _markActiveDirty()); + invoiceForm.addEventListener('change', () => _markActiveDirty()); + } +} + +// ============================================================================ +// A12: Profil furnizor (PR-PROFIL) +// Salvează / aplică / șterge datele furnizorului în localStorage. +// Cheia: efactura.profil.v1 (prefix enforced de storage.js). +// ============================================================================ + +const SUPPLIER_PROFILE_KEY = 'efactura.profil.v1'; + +const SUPPLIER_FIELDS = [ + 'supplierName', 'supplierVAT', 'supplierCompanyId', + 'supplierAddress', 'supplierCity', 'supplierCountrySubentity', + 'supplierCountry', 'supplierPhone', 'supplierContactName', 'supplierEmail' +]; + +/** Actualizează vizibilitatea butoanelor "Folosește / Șterge profil". */ +function _updateProfileButtons() { + const profile = getJSON(SUPPLIER_PROFILE_KEY); + const hasProfile = profile !== null; + const btnUse = document.getElementById('btnUseProfile'); + const btnDelete = document.getElementById('btnDeleteProfile'); + if (btnUse) btnUse.style.display = hasProfile ? '' : 'none'; + if (btnDelete) btnDelete.style.display = hasProfile ? '' : 'none'; +} + +/** Salvează câmpurile furnizorului în localStorage. */ +function saveSupplierProfile() { + const profile = {}; + SUPPLIER_FIELDS.forEach(f => { + const el = document.querySelector(`[name="${f}"]`); + if (el) profile[f] = el.value; + }); + setJSON(SUPPLIER_PROFILE_KEY, profile); + _updateProfileButtons(); + showToast('Profil furnizor salvat.', 'success'); +} + +/** Populează câmpurile furnizorului din profilul salvat. */ +function useSupplierProfile() { + const profile = getJSON(SUPPLIER_PROFILE_KEY); + if (!profile) { + showToast('Nu există profil salvat.', 'info'); + return; + } + SUPPLIER_FIELDS.forEach(f => { + const el = document.querySelector(`[name="${f}"]`); + if (el && profile[f] !== undefined) el.value = profile[f]; + }); + showToast('Profil furnizor aplicat.', 'success'); +} + +/** Șterge profilul furnizorului din localStorage. */ +function deleteSupplierProfile() { + localStorage.removeItem(SUPPLIER_PROFILE_KEY); + _updateProfileButtons(); + showToast('Profil furnizor șters.', 'info'); +} + +// Handling VAT type changes +window.handleVatTypeChange = function(index) { + const vatTypeSelect = document.querySelector(`[name="vatType${index}"]`); + const vatRateInput = document.querySelector(`[name="vatRate${index}"]`); + + // Verifică dacă furnizorul este neplătitor TVA + if (!isVATRegistered()) { + vatTypeSelect.value = 'O'; + vatTypeSelect.disabled = true; // Dezactivează selectul pentru neplătitori + vatRateInput.value = '0'; + vatRateInput.disabled = true; + + // Setează codul și motivul scutirii pentru neplătitori + const row = document.querySelector(`.vat-row`); + if (row) { + const exemptionCode = row.querySelector('.vat-exemption-code'); + const exemptionReason = row.querySelector('.vat-exemption-reason'); + if (exemptionCode) exemptionCode.value = 'VATEX-EU-O'; + if (exemptionReason) exemptionReason.value = 'Operațiune efectuată de neplătitor de TVA'; + } + } else { + vatTypeSelect.disabled = false; // Activează selectul pentru plătitori + + switch(vatTypeSelect.value) { + case 'O': // Pentru neplătitori + vatRateInput.value = '0'; + vatRateInput.disabled = true; + break; + case 'AE': // Taxare inversă + case 'Z': // Cotă 0% + case 'E': // Scutit + vatRateInput.value = '0'; + vatRateInput.disabled = true; + break; + case 'S': // Standard + vatRateInput.value = '19'; + vatRateInput.disabled = false; + break; + } + } + + updateTotals(); +} + +function isVATRegistered() { + const supplierVAT = document.querySelector('[name="supplierVAT"]').value.trim().toUpperCase(); + return supplierVAT.startsWith('RO'); +} + +function handleChargeVatTypeChange(index) { + const vatTypeSelect = document.querySelector(`[name="chargeVatType${index}"]`); + const vatRateInput = document.querySelector(`[name="chargeVatRate${index}"]`); + + if (!vatTypeSelect || !vatRateInput) return; + + // Verifică dacă furnizorul este neplătitor TVA + if (!isVATRegistered()) { + vatTypeSelect.value = 'O'; + vatTypeSelect.disabled = true; // Dezactivează selectul pentru neplătitori + vatRateInput.value = '0'; + vatRateInput.disabled = true; + + // Setează codul și motivul scutirii pentru neplătitori + const row = document.querySelector(`.vat-row`); + if (row) { + const exemptionCode = row.querySelector('.vat-exemption-code'); + const exemptionReason = row.querySelector('.vat-exemption-reason'); + if (exemptionCode) exemptionCode.value = 'VATEX-EU-O'; + if (exemptionReason) exemptionReason.value = 'Operațiune efectuată de neplătitor de TVA'; + } + } else { + vatTypeSelect.disabled = false; // Activează selectul pentru plătitori + + switch(vatTypeSelect.value) { + case 'O': // Pentru neplătitori + vatRateInput.value = '0'; + vatRateInput.disabled = true; + break; + case 'AE': // Taxare inversă + case 'Z': // Cotă 0% + case 'E': // Scutit + vatRateInput.value = '0'; + vatRateInput.disabled = true; + break; + case 'S': // Standard + vatRateInput.value = '19'; + vatRateInput.disabled = false; + break; + } + } + + // Clear manual edits and refresh + manuallyEditedVatRows.clear(); + refreshTotals(); +} + +// Funcție pentru actualizarea tuturor categoriilor TVA când se modifică codul fiscal +function updateAllVATTypes() { + const isNotVATRegistered = !isVATRegistered(); + + // Actualizează toate liniile de articole + document.querySelectorAll('.line-item').forEach((item, index) => { + const vatTypeSelect = item.querySelector(`[name="vatType${index}"]`); + const vatRateInput = item.querySelector(`[name="vatRate${index}"]`); + + if (isNotVATRegistered) { + vatTypeSelect.value = 'O'; + vatTypeSelect.disabled = true; + vatRateInput.value = '0'; + vatRateInput.disabled = true; + } else { + vatTypeSelect.disabled = false; + // Restabilește valorile implicite pentru plătitori + if (vatTypeSelect.value === 'O') { + vatTypeSelect.value = 'S'; + vatRateInput.value = '19'; + vatRateInput.disabled = false; + } + } + }); + + // Actualizează breakdown-ul TVA + updateVATBreakdown(); + refreshTotals(); +} + +// XML modifications +function addUnitCode(code) { + if (!UNIT_CODES.has(code)) { + UNIT_CODES.set(code, `${code} (${code})`); + } +} + +function createUnitCodeOptionsHTML(selectedCode = 'EA') { + return Array.from(UNIT_CODES.entries()) + .map(([code, description]) => + `` + ) + .join(''); +} + +function storeOriginalTotals(xmlDoc) { + const taxTotal = xmlDoc.querySelector('cac\\:TaxTotal, TaxTotal'); + const monetaryTotal = xmlDoc.querySelector('cac\\:LegalMonetaryTotal, LegalMonetaryTotal'); + + originalTotals = { + subtotal: getXMLValue(monetaryTotal, 'cbc\\:LineExtensionAmount, LineExtensionAmount'), + allowances: getXMLValue(monetaryTotal, 'cbc\\:AllowanceTotalAmount, AllowanceTotalAmount', '0'), + charges: getXMLValue(monetaryTotal, 'cbc\\:ChargeTotalAmount, ChargeTotalAmount', '0'), + netAmount: getXMLValue(monetaryTotal, 'cbc\\:TaxExclusiveAmount, TaxExclusiveAmount'), + totalVat: getXMLValue(taxTotal, 'cbc\\:TaxAmount, TaxAmount'), + total: getXMLValue(monetaryTotal, 'cbc\\:TaxInclusiveAmount, TaxInclusiveAmount') + }; + + // console.log('Original totals from XML:', originalTotals); + + const vatBreakdown = []; + const taxSubtotals = xmlDoc.querySelectorAll('cac\\:TaxSubtotal, TaxSubtotal'); + taxSubtotals.forEach(subtotal => { + const taxCategory = subtotal.querySelector('cac\\:TaxCategory, TaxCategory'); + vatBreakdown.push({ + taxableAmount: getXMLValue(subtotal, 'cbc\\:TaxableAmount, TaxableAmount'), + taxAmount: getXMLValue(subtotal, 'cbc\\:TaxAmount, TaxAmount'), + percent: getXMLValue(taxCategory, 'cbc\\:Percent, Percent'), + type: getXMLValue(taxCategory, 'cbc\\:ID, ID', 'S'), + exemptionCode: getXMLValue(taxCategory, 'cbc\\:TaxExemptionReasonCode, TaxExemptionReasonCode'), + exemptionReason: getXMLValue(taxCategory, 'cbc\\:TaxExemptionReason, TaxExemptionReason') + }); + }); + originalTotals.vatBreakdown = vatBreakdown; +} + +function restoreOriginalTotals() { + if (!originalTotals) return; + + // Display exact values from XML with formatting + document.getElementById('subtotal').textContent = formatter.formatCurrency(originalTotals.subtotal); + document.getElementById('totalAllowances').textContent = formatter.formatCurrency(originalTotals.allowances); + document.getElementById('totalCharges').textContent = formatter.formatCurrency(originalTotals.charges); + document.getElementById('netAmount').textContent = formatter.formatCurrency(originalTotals.netAmount); + document.getElementById('vat').textContent = formatter.formatCurrency(originalTotals.totalVat); + + // Bypass formatting for total + document.getElementById('total').textContent = originalTotals.total; + + const container = document.getElementById('vatBreakdownRows'); + if (container) { + container.innerHTML = ''; + + if (originalTotals.vatBreakdown && originalTotals.vatBreakdown.length > 0) { + originalTotals.vatBreakdown.forEach(vat => { + addVATBreakdownRow( + vat.percent, + vat.taxableAmount, + vat.taxAmount, + vat.type, + null, + vat.exemptionCode, + vat.exemptionReason + ); + }); + } + } + + // PR-A11: run math validation după ce displayed-urile sunt setate la + // valorile XML loaded (înainte ca user să editeze ceva). + if (typeof validateMath === 'function') validateMath(); +} + +function updateNoteCounter() { + const noteInput = document.querySelector('[name="invoiceNote"]'); + const counter = document.querySelector('.note-counter'); + if (noteInput && counter) { + const length = noteInput.value.length; + counter.textContent = `${length}/900 caractere`; + } +} + +function splitNoteIntoChunks(text, maxLength) { + if (!text) return []; + const chunks = []; + let remainingText = text; + + while (remainingText.length > 0) { + if (remainingText.length <= maxLength) { + chunks.push(remainingText); + break; + } + + let splitPoint = remainingText.substr(0, maxLength).lastIndexOf('\n'); + if (splitPoint === -1) { + splitPoint = remainingText.substr(0, maxLength).lastIndexOf(' '); + } + if (splitPoint === -1) splitPoint = maxLength; + + chunks.push(remainingText.substr(0, splitPoint)); + remainingText = remainingText.substr(splitPoint + 1); + } + + return chunks.filter(chunk => chunk.trim().length > 0); +} + +function populateBasicDetails(xmlDoc) { + document.querySelector('[name="invoiceNumber"]').value = getXMLValue(xmlDoc, 'cbc\\:ID, ID'); + + // PR-TIPURI (A4): citește cbc:InvoiceTypeCode (default 380 dacă absent). + const invoiceTypeCode = getXMLValue(xmlDoc, 'cbc\\:InvoiceTypeCode, InvoiceTypeCode', '380'); + const typeSelect = document.querySelector('[name="invoiceTypeCode"]'); + if (typeSelect) { + // Dacă XML conține un cod necunoscut, păstrează default-ul 380. + typeSelect.value = INVOICE_TYPES[invoiceTypeCode] ? invoiceTypeCode : '380'; + } + + // Get and combine all Note elements + const notes = xmlDoc.querySelectorAll('cbc\\:Note, Note'); + const combinedNotes = Array.from(notes).map(note => note.textContent).join('\n'); + document.querySelector('[name="invoiceNote"]').value = combinedNotes; + updateNoteCounter(); + + const issueDate = getXMLValue(xmlDoc, 'cbc\\:IssueDate, IssueDate'); + const dueDate = getXMLValue(xmlDoc, 'cbc\\:DueDate, DueDate'); + + if (issueDate) { + const [year, month, day] = issueDate.split('-'); + document.querySelector('[name="issueDate"]').value = `${day}.${month}.${year}`; + } + + if (dueDate) { + const [year, month, day] = dueDate.split('-'); + document.querySelector('[name="dueDate"]').value = `${day}.${month}.${year}`; + } + + // Add currency code handling + const documentCurrencyCode = getXMLValue(xmlDoc, 'cbc\\:DocumentCurrencyCode, DocumentCurrencyCode', 'RON'); + const taxCurrencyCode = getXMLValue(xmlDoc, 'cbc\\:TaxCurrencyCode, TaxCurrencyCode', ''); + + document.querySelector('[name="documentCurrencyCode"]').value = documentCurrencyCode; + document.querySelector('[name="taxCurrencyCode"]').value = taxCurrencyCode; + + // Store original totals and display them + storeOriginalTotals(xmlDoc); + restoreOriginalTotals(); +} + +function populatePartyDetails(xmlDoc) { + function extractPartyDetails(party, prefix) { + // Extract contact information + const contact = party.querySelector('cac\\:Contact, Contact'); + const phone = contact?.querySelector('cbc\\:Telephone, Telephone')?.textContent || ''; + const contactName = contact?.querySelector('cbc\\:Name, Name')?.textContent || ''; + const email = contact?.querySelector('cbc\\:ElectronicMail, ElectronicMail')?.textContent || ''; + + // Country Code Extraction + const countryCodeElement = party.querySelector('cac\\:Country cbc\\:IdentificationCode, Country IdentificationCode'); + const countryCode = countryCodeElement ? countryCodeElement.textContent.trim() : 'RO'; + + // Postal Address Details + const postalAddress = party.querySelector('cac\\:PostalAddress, PostalAddress'); + const streetName = postalAddress ? + getXMLValue(postalAddress, 'cbc\\:StreetName, StreetName') : ''; + const cityName = postalAddress ? + getXMLValue(postalAddress, 'cbc\\:CityName, CityName') : ''; + const countyCode = postalAddress ? + getXMLValue(postalAddress, 'cbc\\:CountrySubentity, CountrySubentity') : ''; + + // Set inputs + document.querySelector(`[name="${prefix}Name"]`).value = + getXMLValue(party, 'cac\\:PartyLegalEntity cbc\\:RegistrationName, PartyLegalEntity RegistrationName'); + + // Cod TVA / Nr. înregistrare — derivă în funcție de prezența PartyTaxScheme. + // Plătitor TVA: PartyTaxScheme/CompanyID e codul TVA, PartyLegalEntity/CompanyID e nr. registru. + // Neplătitor TVA: PartyTaxScheme lipsește; CIF-ul stă în PartyLegalEntity/CompanyID, + // iar nr. registru în PartyIdentification/ID. Trebuie inversate la încărcare ca să apară + // în câmpurile corecte din formular (simetric cu createPartyElement la salvare). + const taxSchemeCompanyId = getXMLValue(party, 'cac\\:PartyTaxScheme cbc\\:CompanyID, PartyTaxScheme CompanyID'); + const legalEntityCompanyId = getXMLValue(party, 'cac\\:PartyLegalEntity cbc\\:CompanyID, PartyLegalEntity CompanyID'); + const partyIdentificationId = getXMLValue(party, 'cac\\:PartyIdentification cbc\\:ID, PartyIdentification ID'); + let vatValue, companyIdValue; + if (taxSchemeCompanyId) { + vatValue = taxSchemeCompanyId; + companyIdValue = legalEntityCompanyId || partyIdentificationId; + } else { + vatValue = legalEntityCompanyId; + companyIdValue = partyIdentificationId; + } + document.querySelector(`[name="${prefix}VAT"]`).value = vatValue; + document.querySelector(`[name="${prefix}CompanyId"]`).value = companyIdValue; + document.querySelector(`[name="${prefix}Address"]`).value = streetName; + document.querySelector(`[name="${prefix}City"]`).value = cityName; + document.querySelector(`[name="${prefix}Phone"]`).value = phone; + document.querySelector(`[name="${prefix}ContactName"]`).value = contactName; + document.querySelector(`[name="${prefix}Email"]`).value = email; + + // Country Select + const countrySelect = document.querySelector(`[name="${prefix}Country"]`); + if (countrySelect) { + countrySelect.value = countryCode; + countrySelect.dataset.xmlValue = countryCode; + } + + // County Select + const countySelect = document.querySelector(`[name="${prefix}CountrySubentity"]`); + if (countySelect) { + countySelect.value = countyCode; + countySelect.dataset.xmlValue = countyCode; + } + } + + const supplierParty = xmlDoc.querySelector('cac\\:AccountingSupplierParty, AccountingSupplierParty'); + if (supplierParty) { + const supplierPartyDetails = supplierParty.querySelector('cac\\:Party, Party'); + if (supplierPartyDetails) { + extractPartyDetails(supplierPartyDetails, 'supplier'); + } + } + + const customerParty = xmlDoc.querySelector('cac\\:AccountingCustomerParty, AccountingCustomerParty'); + if (customerParty) { + const customerPartyDetails = customerParty.querySelector('cac\\:Party, Party'); + if (customerPartyDetails) { + extractPartyDetails(customerPartyDetails, 'customer'); + } + } + + initializeLocationSelectors(); + } + + function populateAllowanceCharges(xmlDoc) { + const charges = parseAllowanceCharges(xmlDoc); + displayAllowanceCharges(charges); + + charges.forEach((_, index) => { + setupAllowanceChargeListeners(index); + addChargeVatTypeChangeListener(index); + }); +} + +// ============================================================================ +// A6: BillingReference (cac:BillingReference / cac:InvoiceDocumentReference) +// Populate + update helpers. UI: câmpuri billingRefId + billingRefDate în +// Detalii Factură. La Stornare, handleStorno() auto-populează cu ID + dată +// facturii originale. +// ============================================================================ + +function populateBillingReference(xmlDoc) { + const refEl = xmlDoc.querySelector('cac\\:BillingReference cac\\:InvoiceDocumentReference, BillingReference InvoiceDocumentReference'); + const idInput = document.querySelector('[name="billingRefId"]'); + const dateInput = document.querySelector('[name="billingRefDate"]'); + if (!idInput || !dateInput) return; + + if (refEl) { + idInput.value = getXMLValue(refEl, 'cbc\\:ID, ID') || ''; + const rawDate = getXMLValue(refEl, 'cbc\\:IssueDate, IssueDate') || ''; + if (rawDate) { + const [year, month, day] = rawDate.split('-'); + dateInput.value = `${day}.${month}.${year}`; + } else { + dateInput.value = ''; + } + } else { + idInput.value = ''; + dateInput.value = ''; + } +} + +function updateBillingReference(xmlDoc) { + // Remove existing BillingReference elements. + xmlDoc.querySelectorAll('cac\\:BillingReference, BillingReference').forEach(el => el.remove()); + + const id = (document.querySelector('[name="billingRefId"]')?.value || '').trim(); + if (!id) return; // Nu scriem BillingReference dacă câmpul e gol. + + const dateRaw = (document.querySelector('[name="billingRefDate"]')?.value || '').trim(); + let isoDate = ''; + if (dateRaw && /^\d{2}\.\d{2}\.\d{4}$/.test(dateRaw)) { + const [dd, mm, yyyy] = dateRaw.split('.'); + isoDate = `${yyyy}-${mm}-${dd}`; + } + + const billingRef = createXMLElement(xmlDoc, XML_NAMESPACES.cac, 'cac:BillingReference'); + const docRef = createXMLElement(xmlDoc, XML_NAMESPACES.cac, 'cac:InvoiceDocumentReference'); + docRef.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, 'cbc:ID', id)); + if (isoDate) { + docRef.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, 'cbc:IssueDate', isoDate)); + } + billingRef.appendChild(docRef); + + // Insert before cac:AccountingSupplierParty (per UBL 2.1 ordering). + const supplierParty = xmlDoc.querySelector('cac\\:AccountingSupplierParty, AccountingSupplierParty'); + if (supplierParty) { + xmlDoc.documentElement.insertBefore(billingRef, supplierParty); + } else { + xmlDoc.documentElement.appendChild(billingRef); + } +} + +// ============================================================================ +// A5: PaymentMeans (cac:PaymentMeans multiple) +// UI: secțiune "Modalități de Plată" cu rânduri dinamice (cod + IBAN). +// PAYMENT_MEANS_CODES definit la top lângă VAT_TYPES (nu duplicat). +// ============================================================================ + +let _paymentMeansCount = 0; + +function createPaymentMeansRowHTML(index, code = '30', iban = '') { + const options = Object.entries(PAYMENT_MEANS_CODES) + .map(([val, label]) => + ``) + .join(''); + + return ` +
    +
    +
    + + +
    +
    + + +
    + +
    +
    + `; +} + +function addPaymentMeansRow(code = '30', iban = '') { + const container = document.getElementById('paymentMeansRows'); + if (!container) return; + const index = _paymentMeansCount++; + container.insertAdjacentHTML('beforeend', createPaymentMeansRowHTML(index, code, iban)); +} + +function removePaymentMeansRow(index) { + const row = document.querySelector(`.payment-means-row[data-pm-index="${index}"]`); + if (row) row.remove(); +} + +function populatePaymentMeans(xmlDoc) { + const container = document.getElementById('paymentMeansRows'); + if (!container) return; + container.innerHTML = ''; + _paymentMeansCount = 0; + + const pmElements = xmlDoc.querySelectorAll('cac\\:PaymentMeans, PaymentMeans'); + pmElements.forEach(pmEl => { + const code = getXMLValue(pmEl, 'cbc\\:PaymentMeansCode, PaymentMeansCode') || '30'; + const iban = getXMLValue(pmEl, 'cac\\:PayeeFinancialAccount cbc\\:ID, PayeeFinancialAccount ID') || ''; + addPaymentMeansRow(code, iban); + }); +} + +function updatePaymentMeans(xmlDoc) { + // Remove existing PaymentMeans elements. + xmlDoc.querySelectorAll('cac\\:PaymentMeans, PaymentMeans').forEach(el => el.remove()); + + const rows = document.querySelectorAll('.payment-means-row'); + if (rows.length === 0) return; + + // Insert before cac:AllowanceCharge (sau TaxTotal dacă nu există AllowanceCharge). + const refEl = xmlDoc.querySelector('cac\\:AllowanceCharge, AllowanceCharge') || + xmlDoc.querySelector('cac\\:TaxTotal, TaxTotal'); + + rows.forEach(row => { + const idx = row.dataset.pmIndex; + const code = document.querySelector(`[name="paymentMeansCode${idx}"]`)?.value || '30'; + const iban = (document.querySelector(`[name="paymentMeansIBAN${idx}"]`)?.value || '').trim(); + + const pmEl = createXMLElement(xmlDoc, XML_NAMESPACES.cac, 'cac:PaymentMeans'); + pmEl.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, 'cbc:PaymentMeansCode', code)); + + if (iban) { + const account = createXMLElement(xmlDoc, XML_NAMESPACES.cac, 'cac:PayeeFinancialAccount'); + account.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, 'cbc:ID', iban)); + pmEl.appendChild(account); + } + + if (refEl) { + xmlDoc.documentElement.insertBefore(pmEl, refEl); + } else { + xmlDoc.documentElement.appendChild(pmEl); + } + }); +} + +function populateLineItems(xmlDoc) { + const lineItems = xmlDoc.querySelectorAll('cac\\:InvoiceLine, InvoiceLine'); + const lineItemsContainer = document.getElementById('lineItems'); + lineItemsContainer.innerHTML = '

    Articole Factură

    '; + + lineItems.forEach((item, index) => { + const quantity = getXMLValue(item, 'cbc\\:InvoicedQuantity, InvoicedQuantity', '0'); + const unitCode = item.querySelector('cbc\\:InvoicedQuantity, InvoicedQuantity')?.getAttribute('unitCode') || 'EA'; + const price = getXMLValue(item, 'cac\\:Price cbc\\:PriceAmount, Price PriceAmount', '0'); + // PR-A11: capture original LineExtensionAmount from XML for math validation badge. + const xmlLineAmount = getXMLValue(item, 'cbc\\:LineExtensionAmount, LineExtensionAmount', ''); + const itemElement = item.querySelector('cac\\:Item, Item'); + const description = getXMLValue(itemElement, 'cbc\\:Name, Name', ''); + const itemDescription = getXMLValue(itemElement, 'cbc\\:Description, Description', ''); + + const taxCategory = itemElement.querySelector('cac\\:ClassifiedTaxCategory, ClassifiedTaxCategory'); + const vatType = getXMLValue(taxCategory, 'cbc\\:ID, ID') || 'S'; + const vatRate = getXMLValue(taxCategory, 'cbc\\:Percent, Percent') || '19'; + + // Extragem discountul și codul de motiv de pe linie dacă există + let lineDiscount = 0; + let discountReasonCode = ''; + const allowanceCharge = item.querySelector('cac\\:AllowanceCharge, AllowanceCharge'); + if (allowanceCharge) { + const chargeIndicator = getXMLValue(allowanceCharge, 'cbc\\:ChargeIndicator, ChargeIndicator'); + if (chargeIndicator === 'false') { + lineDiscount = parseFloat(getXMLValue(allowanceCharge, 'cbc\\:Amount, Amount', '0')); + discountReasonCode = getXMLValue(allowanceCharge, 'cbc\\:AllowanceChargeReasonCode, AllowanceChargeReasonCode', ''); + } + } + + addUnitCode(unitCode); + const lineItemHtml = createLineItemHTML( + index, description, quantity, price, vatRate, unitCode, vatType, + itemDescription, lineDiscount, discountReasonCode + ); + lineItemsContainer.insertAdjacentHTML('beforeend', lineItemHtml); + + // PR-A11: stash original XML LineExtensionAmount on the line-item element + // for math validation badge (compare computed vs XML loaded with ±0.01 RON + // tolerance când row-ul e clean — toate input-urile dataset.dirty='0'). + const lineItemEl = lineItemsContainer.querySelector(`.line-item[data-index="${index}"]`); + if (lineItemEl && xmlLineAmount !== '') { + const parsedXmlAmount = parseStrict(xmlLineAmount); + if (parsedXmlAmount !== null) { + lineItemEl.dataset.xmlLineAmount = parsedXmlAmount.toFixed(2); + } + } + + // PR-E E1: dataset.raw seeded din XML (canonical source of truth). + // Wire blur listeners care normalizează la commit (parseStrict + setRaw). + seedNumericRawForLineItem(index, { quantity, price, lineDiscount, vatRate }); + + // Parse identifications after adding the line item HTML + if (itemElement) { + parseIdentifications(itemElement, index); + } + + if (!isVATRegistered()) { + const vatTypeSelect = document.querySelector(`[name="vatType${index}"]`); + const vatRateInput = document.querySelector(`[name="vatRate${index}"]`); + if (vatTypeSelect && vatRateInput) { + vatTypeSelect.value = 'O'; + vatTypeSelect.disabled = true; + vatRateInput.value = '0'; + vatRateInput.disabled = true; + } + } + + // Enable/disable discount reason code field based on discount value + const discountInput = document.querySelector(`[name="lineDiscount${index}"]`); + const reasonCodeSelect = document.querySelector(`[name="discountReasonCode${index}"]`); + if (discountInput && reasonCodeSelect) { + reasonCodeSelect.disabled = lineDiscount <= 0; + } + + handleLineItemChange(index); + }); + +} + +// PR-E E1: helper care setează dataset.raw + wire blur normalize pe toate +// input-urile numerice ale unei linii de factură. Se apelează după +// insertAdjacentHTML, când DOM-ul există. +function seedNumericRawForLineItem(index, raw) { + const fields = [ + { name: 'quantity', decimals: 3, value: raw.quantity }, + { name: 'price', decimals: 4, value: raw.price }, + { name: 'lineDiscount', decimals: 2, value: raw.lineDiscount }, + { name: 'vatRate', decimals: 2, value: raw.vatRate }, + ]; + fields.forEach(f => { + const input = document.querySelector(`[name="${f.name}${index}"]`); + if (!input) return; + const big = parseStrict(f.value); + if (big !== null) { + // Stocăm raw canonical decimal-dot. NU schimbăm input.value + // (lăsăm display ca în markup-ul existent — type=number e + // display-locale-agnostic în input field și are nevoie de '.'). + input.dataset.raw = big.toFixed(f.decimals); + } + wireDatasetRaw(input, f.decimals); + }); +} + +// PR-E E1: idem pentru allowance/charge. +function seedNumericRawForCharge(index, raw) { + const fields = [ + { name: 'chargeAmount', decimals: 2, value: raw.amount }, + { name: 'chargeBaseAmount', decimals: 2, value: raw.baseAmount }, + { name: 'chargeVatRate', decimals: 2, value: raw.vatRate }, + ]; + fields.forEach(f => { + const input = document.querySelector(`[name="${f.name}${index}"]`); + if (!input) return; + const big = parseStrict(f.value); + if (big !== null) { + input.dataset.raw = big.toFixed(f.decimals); + } + wireDatasetRaw(input, f.decimals); + }); +} + +function setupAllowanceChargeListeners(index) { + const chargeAmountInput = document.querySelector(`[name="chargeAmount${index}"]`); + const chargeTypeInput = document.querySelector(`[name="chargeType${index}"]`); + const chargeVatTypeInput = document.querySelector(`[name="chargeVatType${index}"]`); + const chargeVatRateInput = document.querySelector(`[name="chargeVatRate${index}"]`); + const reasonCodeSelect = document.querySelector(`[name="chargeReasonCode${index}"]`); + const reasonInput = document.querySelector(`[name="chargeReason${index}"]`); + + // Add change listeners to all inputs + [chargeAmountInput, chargeTypeInput, chargeVatTypeInput, chargeVatRateInput].forEach(input => { + if (input) { + input.addEventListener('change', () => { + manuallyEditedVatRows.clear(); + refreshTotals(); + }); + } + }); + + // Add reason code change listener + if (reasonCodeSelect) { + reasonCodeSelect.addEventListener('change', () => { + const isCharge = chargeTypeInput.value === 'true'; + const codes = isCharge ? CHARGE_REASON_CODES : ALLOWANCE_REASON_CODES; + reasonInput.value = codes[reasonCodeSelect.value] || ''; + }); + } + + // Special handling for VAT type changes + if (chargeVatTypeInput) { + chargeVatTypeInput.addEventListener('change', () => handleChargeVatTypeChange(index)); + } +} + +function addChargeVatTypeChangeListener(index) { + const vatTypeSelect = document.querySelector(`[name="chargeVatType${index}"]`); + if (vatTypeSelect) { + vatTypeSelect.addEventListener('change', function() { + handleChargeVatTypeChange(index); + // Force refresh of VAT breakdown + displayVATBreakdown(); + updateTotals(); + }); + } +} + +function parseAllowanceCharges(xmlDoc) { + const charges = []; + // Folosim Set pentru a preveni dublarea elementelor din cauza namespace-urilor + const processedIds = new Set(); + + // Selectăm toate elementele AllowanceCharge care sunt copii direcți ai Invoice + const allowanceCharges = xmlDoc.querySelectorAll('cac\\:AllowanceCharge, AllowanceCharge'); + + allowanceCharges.forEach(ac => { + // Verificăm dacă elementul este copil direct al Invoice și nu a fost deja procesat + if (ac.parentElement === xmlDoc.documentElement) { + const amount = getXMLValue(ac, 'cbc\\:Amount, Amount'); + const reasonCode = getXMLValue(ac, 'cbc\\:AllowanceChargeReasonCode, AllowanceChargeReasonCode'); + + // Creăm un ID unic bazat pe cod și valoare pentru a evita duplicatele + const uniqueId = `${reasonCode}-${amount}`; + + if (!processedIds.has(uniqueId)) { + processedIds.add(uniqueId); + + const charge = { + isCharge: getXMLValue(ac, 'cbc\\:ChargeIndicator, ChargeIndicator') === 'true', + reasonCode: reasonCode, + reason: getXMLValue(ac, 'cbc\\:AllowanceChargeReason, AllowanceChargeReason'), + amount: parseFloat(amount) || 0, + baseAmount: parseFloat(getXMLValue(ac, 'cbc\\:BaseAmount, BaseAmount')) || 0, + vatRate: parseFloat(getXMLValue(ac, 'cac\\:TaxCategory cbc\\:Percent, TaxCategory Percent')) || 0, + vatTypeId: getXMLValue(ac, 'cac\\:TaxCategory cbc\\:ID, TaxCategory ID', 'S'), + multiplierFactorNumeric: parseFloat(getXMLValue(ac, 'cbc\\:MultiplierFactorNumeric, MultiplierFactorNumeric')) || 0 + }; + charges.push(charge); + } + } + }); + + return charges; +} + +function displayAllowanceCharges(charges) { + const container = document.getElementById('allowanceCharges'); + container.innerHTML = '

    Reduceri și Taxe Suplimentare

    '; + + charges.forEach((charge, index) => { + const html = createAllowanceChargeHTML(index, charge); + container.insertAdjacentHTML('beforeend', html); + // PR-E E1: seed dataset.raw + wire blur listeners. + seedNumericRawForCharge(index, { + amount: charge.amount, + baseAmount: charge.baseAmount, + vatRate: charge.vatRate + }); + }); +} + +function addAllowanceCharge() { + const container = document.getElementById('allowanceCharges'); + const index = document.querySelectorAll('.allowance-charge').length; + + // Create new charge with default values + const newCharge = { + isCharge: true, + reasonCode: 'TV', + reason: 'Cheltuieli transport', + amount: 0, + vatRate: 19.0, + vatTypeId: 'S' + }; + + // Add the HTML + const html = createAllowanceChargeHTML(index, newCharge); + container.insertAdjacentHTML('beforeend', html); + + // PR-E E1: seed dataset.raw + wire blur listeners. + seedNumericRawForCharge(index, { + amount: newCharge.amount, + baseAmount: newCharge.baseAmount, + vatRate: newCharge.vatRate + }); + + // Setup event listeners + setupAllowanceChargeListeners(index); + + // Force refresh of totals and VAT + refreshTotals(); +} + +function removeAllowanceCharge(index) { + const charge = document.querySelector(`.allowance-charge[data-index="${index}"]`); + if (charge) { + // Remove the element + charge.remove(); + + // Renumber remaining charges + renumberAllowanceCharges(); + + // Clear manual edits and refresh totals + manuallyEditedVatRows.clear(); + refreshTotals(); + } +} + +function renumberAllowanceCharges() { + document.querySelectorAll('.allowance-charge').forEach((charge, newIndex) => { + // Update data-index + charge.dataset.index = newIndex; + + // Update all input names + charge.querySelectorAll('input, select').forEach(input => { + const name = input.getAttribute('name'); + if (name) { + const baseName = name.replace(/\d+$/, ''); + input.setAttribute('name', baseName + newIndex); + } + }); + }); +} + + +function addLineItem() { + const container = document.getElementById('lineItems'); + const index = document.querySelectorAll('.line-item').length; + + // Determină tipul implicit de TVA bazat pe statusul furnizorului + const defaultVatType = isVATRegistered() ? 'S' : 'O'; + const defaultVatRate = isVATRegistered() ? '19' : '0'; + + const lineItemHtml = createLineItemHTML(index, '', '1', '0', defaultVatRate, 'EA', defaultVatType); + container.insertAdjacentHTML('beforeend', lineItemHtml); + + const newItem = container.lastElementChild; + const vatTypeSelect = newItem.querySelector(`[name="vatType${index}"]`); + + // PR-E E1: seed dataset.raw + wire blur listeners. + seedNumericRawForLineItem(index, { + quantity: '1', price: '0', lineDiscount: '0', vatRate: defaultVatRate + }); + + if (!isVATRegistered()) { + vatTypeSelect.disabled = true; + newItem.querySelector(`[name="vatRate${index}"]`).disabled = true; + } + + // Adăugăm event handlers pentru noua linie + handleLineItemChange(index); + + manuallyEditedVatRows.clear(); + refreshTotals(); +} + +function removeLineItem(index) { + const lineItem = document.querySelector(`.line-item[data-index="${index}"]`); + if (lineItem) { + lineItem.remove(); + renumberLineItems(); + manuallyEditedVatRows.clear(); // Clear manual edits + refreshTotals(); // Recalculate all totals + } +} + +function handleLineItemChange(index) { + const quantityInput = document.querySelector(`[name="quantity${index}"]`); + const priceInput = document.querySelector(`[name="price${index}"]`); + const discountInput = document.querySelector(`[name="lineDiscount${index}"]`); + const reasonCodeSelect = document.querySelector(`[name="discountReasonCode${index}"]`); + + // Handler pentru schimbarea discount-ului + discountInput?.addEventListener('input', function() { + const discountValue = parseFloat(this.value) || 0; + if (reasonCodeSelect) { + reasonCodeSelect.disabled = discountValue == 0; // activat pentru orice valoare nenulă + // Dacă discount != 0 și nu e selectat niciun cod, selectăm codul implicit + if (discountValue != 0 && !reasonCodeSelect.value) { + reasonCodeSelect.value = '95'; // Cod implicit pentru reducere + } + } + updateTotals(); + }); + + // Handlers pentru cantitate și preț + quantityInput?.addEventListener('change', updateTotals); + priceInput?.addEventListener('change', updateTotals); + + // Force refresh + updateTotals(); +} + +function handleStorno() { + // A6: populează câmpurile BillingReference cu datele facturii curente + // înainte de negare (referința facturii originale pentru nota de credit). + const currentInvoiceNumber = document.querySelector('[name="invoiceNumber"]')?.value || ''; + const currentIssueDate = document.querySelector('[name="issueDate"]')?.value || ''; + const billingRefIdInput = document.querySelector('[name="billingRefId"]'); + const billingRefDateInput = document.querySelector('[name="billingRefDate"]'); + if (billingRefIdInput && !billingRefIdInput.value && currentInvoiceNumber) { + billingRefIdInput.value = currentInvoiceNumber; + } + if (billingRefDateInput && !billingRefDateInput.value && currentIssueDate) { + billingRefDateInput.value = currentIssueDate; + } + + // PR-TIPURI (A4): la storno setăm cbc:InvoiceTypeCode = 381 (Notă de + // credit). User poate suprascrie din dropdown dacă vrea alt cod. + const typeSelect = document.querySelector('[name="invoiceTypeCode"]'); + if (typeSelect) typeSelect.value = '381'; + + // PR-A11: folosim getRaw/setRaw/markDirty în loc de parseFloat + // pentru a păstra dataset.raw consistent cu input.value după negare. + document.querySelectorAll('.line-item').forEach((item, index) => { + const quantityInput = document.querySelector(`[name="quantity${index}"]`); + if (!quantityInput) return; + setRaw(quantityInput, getRaw(quantityInput).times(-1), 3); + markDirty(quantityInput); + }); + + document.querySelectorAll('.allowance-charge').forEach((item, index) => { + const amountInput = document.querySelector(`[name="chargeAmount${index}"]`); + if (!amountInput) return; + setRaw(amountInput, getRaw(amountInput).times(-1), 2); + markDirty(amountInput); + }); + + document.querySelectorAll('.vat-row').forEach(row => { + const baseInput = row.querySelector('.vat-base'); + const amountInput = row.querySelector('.vat-amount'); + + if (baseInput) { + setRaw(baseInput, getRaw(baseInput).times(-1), 2); + markDirty(baseInput); + } + + if (amountInput) { + setRaw(amountInput, getRaw(amountInput).times(-1), 2); + markDirty(amountInput); + } + }); + + manuallyEditedVatRows.clear(); + refreshTotals(); + + if (currentInvoice) { + updateTaxTotals(currentInvoice); + } +} + +function updateTotals() { + // Calculăm totalurile liniilor (deja nete, după discount) + const lineItemTotals = calculateLineItemTotals(); + const chargeTotals = calculateChargeTotals(); + + const subtotal = lineItemTotals.subtotal; + const allowances = chargeTotals.allowances; + const charges = chargeTotals.charges; + const netAmount = subtotal - allowances + charges; + + // Calculate VAT breakdown + const { vatBreakdown } = calculateVATBreakdown(); + let totalVat = 0; + + vatBreakdown.forEach((entry) => { + if (entry.type === 'S') { + totalVat += entry.vatAmount; + } + }); + + // Display totals with 2 decimal places + displayTotals({ + subtotal: roundNumber(subtotal, 2), + allowances: roundNumber(allowances, 2), + charges: roundNumber(charges, 2), + netAmount: roundNumber(netAmount, 2), + totalVat: roundNumber(totalVat, 2), + total: roundNumber(netAmount + totalVat, 2), + vatBreakdown + }); + + // PR-A11: refresh badge-uri math validation după displayTotals. + if (typeof validateMath === 'function') validateMath(); + + // PR-BR: actualizează panelul de validare BR live pe orice editare. + _updateBRPanel(); +} + +function refreshTotals() { + // Calculate line items first + const lineItemTotals = calculateLineItemTotals(); + const chargeTotals = calculateChargeTotals(); + + const subtotal = lineItemTotals.subtotal; + const allowances = chargeTotals.allowances; + const charges = chargeTotals.charges; + const netAmount = subtotal - allowances + charges; + + // Calculate VAT breakdown + const { vatBreakdown } = calculateVATBreakdown(); + let totalVat = 0; + if (vatBreakdown) { + vatBreakdown.forEach(entry => { + totalVat += entry.vatAmount; + }); + } + + // Display totals + displayTotals({ + subtotal, + allowances, + charges, + netAmount, + totalVat, + total: netAmount + totalVat, + vatBreakdown + }); + + // PR-A11: refresh badge-uri math validation după displayTotals. + if (typeof validateMath === 'function') validateMath(); + + // PR-BR: actualizează panelul de validare BR live. + _updateBRPanel(); +} + +function calculateLineItemTotals() { + // PR-E E4: pipeline Big.js. Citește din dataset.raw (canonical), fallback + // la input.value via getRaw. Niciun parseFloat. + let subtotal = new Big('0'); + + document.querySelectorAll('.line-item').forEach((item, index) => { + const quantity = getRaw(document.querySelector(`[name="quantity${index}"]`)); + const price = getRaw(document.querySelector(`[name="price${index}"]`)); + const lineDiscount = getRaw(document.querySelector(`[name="lineDiscount${index}"]`)); + + // LineExtensionAmount = cantitate * preț - discount + const lineAmount = quantity.times(price).minus(lineDiscount); + subtotal = subtotal.plus(lineAmount); + }); + + return { + subtotal: Number(subtotal.round(2, 1).toFixed(2)) + }; +} + +function calculateChargeTotals() { + // PR-E E4: pipeline Big.js. + let allowances = new Big('0'); + let charges = new Big('0'); + + document.querySelectorAll('.allowance-charge').forEach((item, index) => { + const isCharge = document.querySelector(`[name="chargeType${index}"]`).value === 'true'; + const amount = getRaw(document.querySelector(`[name="chargeAmount${index}"]`)); + if (isCharge) { + charges = charges.plus(amount); + } else { + allowances = allowances.plus(amount); + } + }); + + return { + allowances: Number(allowances.round(2, 1).toFixed(2)), + charges: Number(charges.round(2, 1).toFixed(2)) + }; +} + +function calculateTotals() { + // PR-E E4: pipeline Big.js end-to-end. Toate citirile via getRaw → Big. + let subtotalBig = new Big('0'); + let vatBreakdown = new Map(); + + // Calculăm totalurile liniilor + document.querySelectorAll('.line-item').forEach((item, index) => { + const quantity = getRaw(document.querySelector(`[name="quantity${index}"]`)); + const price = getRaw(document.querySelector(`[name="price${index}"]`)); + const lineDiscount = getRaw(document.querySelector(`[name="lineDiscount${index}"]`)); + const vatType = document.querySelector(`[name="vatType${index}"]`).value; + const vatRate = getRaw(document.querySelector(`[name="vatRate${index}"]`)); + + // LineExtensionAmount = (cantitate * preț) - discount linie + const lineNet = quantity.times(price).minus(lineDiscount); + subtotalBig = subtotalBig.plus(lineNet); + + const rateKey = vatRate.toFixed(2); + const key = `${rateKey}-${vatType}`; + if (!vatBreakdown.has(key)) { + vatBreakdown.set(key, { + baseAmount: 0, + vatAmount: 0, + rate: Number(vatRate.toString()), + type: vatType, + _baseBig: new Big('0') + }); + } + const entry = vatBreakdown.get(key); + entry._baseBig = entry._baseBig.plus(lineNet); + }); + + // Calculăm discounturile și taxele globale (returnate ca Number — convertim la Big intern) + const { allowances, charges } = calculateChargeTotals(); + const allowancesBig = new Big(String(allowances)); + const chargesBig = new Big(String(charges)); + + // Net = subtotal - allowances + charges + const netAmountBig = subtotalBig.minus(allowancesBig).plus(chargesBig); + + // Ajustăm baza de TVA cu discount/taxe globale + document.querySelectorAll('.allowance-charge').forEach((item, index) => { + const amount = getRaw(document.querySelector(`[name="chargeAmount${index}"]`)); + const vatType = document.querySelector(`[name="chargeVatType${index}"]`).value; + const vatRate = getRaw(document.querySelector(`[name="chargeVatRate${index}"]`)); + const isCharge = document.querySelector(`[name="chargeType${index}"]`).value === 'true'; + + const rateKey = vatRate.toFixed(2); + const key = `${rateKey}-${vatType}`; + if (!vatBreakdown.has(key)) { + vatBreakdown.set(key, { + baseAmount: 0, + vatAmount: 0, + rate: Number(vatRate.toString()), + type: vatType, + _baseBig: new Big('0') + }); + } + const entry = vatBreakdown.get(key); + entry._baseBig = isCharge ? entry._baseBig.plus(amount) : entry._baseBig.minus(amount); + }); + + // VAT pe rată — round HALF_UP la 2 zecimale + let totalVatBig = new Big('0'); + vatBreakdown.forEach((entry) => { + const baseRounded = entry._baseBig.round(2, 1); + entry.baseAmount = Number(baseRounded.toFixed(2)); + if (entry.type === 'S') { + const vatBig = entry._baseBig.times(entry.rate).div(100).round(2, 1); + entry.vatAmount = Number(vatBig.toFixed(2)); + totalVatBig = totalVatBig.plus(vatBig); + } else { + entry.vatAmount = 0; + } + delete entry._baseBig; + }); + + const subtotal = Number(subtotalBig.round(2, 1).toFixed(2)); + const netAmount = Number(netAmountBig.round(2, 1).toFixed(2)); + const totalVat = Number(totalVatBig.round(2, 1).toFixed(2)); + + return { + subtotal, + allowances, + charges, + netAmount, + totalVat, + total: Number(netAmountBig.plus(totalVatBig).round(2, 1).toFixed(2)), + vatBreakdown + }; +} + +function calculateVATBases(lines, globalAllowances, globalCharges) { + // Initialize VAT bases + const vatBases = {}; + + // First pass: calculate base amounts per VAT rate from lines + lines.forEach(line => { + const vatRate = line.vatRate; + if (!vatBases[vatRate]) { + vatBases[vatRate] = { + base: 0, + allowances: 0, + charges: 0 + }; + } + + const lineTotal = line.quantity * line.price; + const lineAfterDiscount = lineTotal - line.lineDiscount; + vatBases[vatRate].base += lineAfterDiscount; + }); + + // Second pass: apply global allowances and charges per VAT rate + globalAllowances.forEach(allowance => { + const vatRate = allowance.vatRate; + if (vatBases[vatRate]) { + vatBases[vatRate].allowances += allowance.amount; + } + }); + + globalCharges.forEach(charge => { + const vatRate = charge.vatRate; + if (vatBases[vatRate]) { + vatBases[vatRate].charges += charge.amount; + } + }); + + // Calculate final VAT amounts + Object.keys(vatBases).forEach(rate => { + const rateData = vatBases[rate]; + const netBase = rateData.base - rateData.allowances + rateData.charges; + rateData.vatAmount = netBase * (parseFloat(rate) / 100); + }); + + return vatBases; +} + +function calculateTotalVAT() { + let totalVat = Array.from(document.querySelectorAll('.vat-amount')) + .reduce((sum, input) => sum + formatter.parseCurrency(input.value), 0); + return roundNumber(totalVat, 2); +} + +function calculateVATBreakdown() { + // PR-E E4: pipeline Big.js. Aceeași semantică ca calculateTotals dar + // doar partea de breakdown (folosit de displayVATBreakdown). + let vatBreakdown = new Map(); + let totalVatBig = new Big('0'); + + // Process line items + document.querySelectorAll('.line-item').forEach((item, index) => { + const quantity = getRaw(document.querySelector(`[name="quantity${index}"]`)); + const price = getRaw(document.querySelector(`[name="price${index}"]`)); + const lineDiscount = getRaw(document.querySelector(`[name="lineDiscount${index}"]`)); + const vatType = document.querySelector(`[name="vatType${index}"]`).value; + const vatRate = getRaw(document.querySelector(`[name="vatRate${index}"]`)); + + const lineAmount = quantity.times(price).minus(lineDiscount); + const rateKey = vatRate.toFixed(2); + const key = `${rateKey}-${vatType}`; + + if (!vatBreakdown.has(key)) { + vatBreakdown.set(key, { + baseAmount: 0, + vatAmount: 0, + rate: Number(vatRate.toString()), + type: vatType, + _baseBig: new Big('0') + }); + } + const entry = vatBreakdown.get(key); + entry._baseBig = entry._baseBig.plus(lineAmount); + }); + + // Process allowances and charges + document.querySelectorAll('.allowance-charge').forEach((charge, index) => { + const amount = getRaw(document.querySelector(`[name="chargeAmount${index}"]`)); + const vatType = document.querySelector(`[name="chargeVatType${index}"]`).value; + const vatRate = getRaw(document.querySelector(`[name="chargeVatRate${index}"]`)); + const isCharge = document.querySelector(`[name="chargeType${index}"]`).value === 'true'; + + if (amount.eq(0)) return; + + const rateKey = vatRate.toFixed(2); + const key = `${rateKey}-${vatType}`; + + if (!vatBreakdown.has(key)) { + vatBreakdown.set(key, { + baseAmount: 0, + vatAmount: 0, + rate: Number(vatRate.toString()), + type: vatType, + _baseBig: new Big('0') + }); + } + const entry = vatBreakdown.get(key); + entry._baseBig = isCharge ? entry._baseBig.plus(amount) : entry._baseBig.minus(amount); + }); + + // Calculate VAT for each rate + vatBreakdown.forEach((entry) => { + entry.baseAmount = Number(entry._baseBig.round(2, 1).toFixed(2)); + if (entry.type === 'S') { + const vatBig = entry._baseBig.times(entry.rate).div(100).round(2, 1); + entry.vatAmount = Number(vatBig.toFixed(2)); + totalVatBig = totalVatBig.plus(vatBig); + } else { + entry.vatAmount = 0; + } + delete entry._baseBig; + }); + + return { vatBreakdown, totalVat: Number(totalVatBig.round(2, 1).toFixed(2)) }; +} + +window.updateVATRow = function(rowId, source) { + const row = document.getElementById(rowId); + if (!row) return; + + const typeSelect = row.querySelector('.vat-type'); + const rateInput = row.querySelector('.vat-rate'); + const baseInput = row.querySelector('.vat-base'); + const amountInput = row.querySelector('.vat-amount'); + + if (source === 'manual') { + manuallyEditedVatRows.add(rowId); + // Keep existing values, just update totals + updateTotalVAT(); + refreshTotals(); + return; + } + + // Only calculate VAT amount for non-manual updates + if (!manuallyEditedVatRows.has(rowId)) { + // PR-E E4: Big.js pentru calcul VAT. + const type = typeSelect.value; + const rate = getRaw(rateInput); + const base = getRaw(baseInput); + + let calculatedAmountBig; + if (type === 'S') { + calculatedAmountBig = base.times(rate).div(100).round(2, 1); + } else { + calculatedAmountBig = new Big('0'); + } + const calculatedAmount = Number(calculatedAmountBig.toFixed(2)); + amountInput.value = formatter.formatCurrency(calculatedAmount); + amountInput.dataset.raw = calculatedAmountBig.toFixed(2); + + updateTotalVAT(); + refreshTotals(); + } +}; + +window.updateVATRowFromAmount = function(rowId) { + const row = document.getElementById(rowId); + if (!row) return; + + // Just mark as manually edited and update the totals + // Do not recalculate base amount + manuallyEditedVatRows.add(rowId); + + const amountInput = row.querySelector('.vat-amount'); + if (amountInput) { + // PR-E E1+E4: parse strict, sync dataset.raw, format display. + const valueBig = parseStrictOr(amountInput.value, '0'); + amountInput.value = formatter.formatCurrency(Number(valueBig.toString())); + amountInput.dataset.raw = valueBig.toFixed(2); + } + + let totalVatBig = new Big('0'); + document.querySelectorAll('.vat-row').forEach(vatRow => { + totalVatBig = totalVatBig.plus(getRaw(vatRow.querySelector('.vat-amount'))); + }); + + // Update just total VAT and final total + const netAmountBig = parseStrictOr(document.getElementById('netAmount').textContent, '0'); + document.getElementById('vat').textContent = formatter.formatCurrency(Number(totalVatBig.toFixed(2))); + document.getElementById('total').textContent = formatter.formatCurrency(Number(netAmountBig.plus(totalVatBig).toFixed(2))); + + // PR-A11: refresh badge-uri math validation după edit manual de vat-amount. + if (typeof validateMath === 'function') validateMath(); +}; + +window.removeVATRow = function(rowId) { + const row = document.getElementById(rowId); + if (row) { + manuallyEditedVatRows.delete(rowId); + row.remove(); + updateTotalVAT(); + refreshTotals(); + } +}; + +window.addVATRate = function() { + const container = document.getElementById('vatBreakdownRows'); + addVATBreakdownRow(19, 0, 0); + refreshTotals(); +}; + +function updateTotalVAT() { + // PR-E E4: Big.js. Citește din dataset.raw → fallback parseStrict pe value. + const totalVatBig = Array.from(document.querySelectorAll('.vat-amount')) + .reduce((sum, input) => sum.plus(getRaw(input)), new Big('0')); + + document.getElementById('vat').textContent = totalVatBig.toFixed(2); + + const netAmountBig = parseStrictOr(document.getElementById('netAmount').textContent, '0'); + const total = netAmountBig.plus(totalVatBig); + document.getElementById('total').textContent = total.toFixed(2); +} + +function updateVATBreakdown() { + // Șterge și reconstruiește rândurile TVA + const container = document.getElementById('vatBreakdownRows'); + if (!container) return; + + container.innerHTML = ''; + const { vatBreakdown } = calculateVATBreakdown(); + + vatBreakdown.forEach((data, key) => { + const [rate, type] = key.split('-'); + addVATBreakdownRow( + parseFloat(rate), + data.baseAmount, + data.vatAmount, + type + ); + }); +} + +function displayVATBreakdown(xmlDoc = null) { + const container = document.getElementById('vatBreakdownRows'); + if (!container) return; + + // Clear container + container.innerHTML = ''; + + // If XML is provided, use its VAT breakdown + if (xmlDoc && originalTotals && originalTotals.vatBreakdown) { + originalTotals.vatBreakdown.forEach((vat, index) => { + addVATBreakdownRow( + vat.percent, + vat.taxableAmount, + vat.taxAmount, + vat.type, + `vat-row-${index}`, + vat.exemptionCode, + vat.exemptionReason + ); + }); + } else { + // Calculate current VAT breakdown + const { vatBreakdown } = calculateVATBreakdown(); + vatBreakdown.forEach((data, key) => { + const [rate, type] = key.split('-'); + addVATBreakdownRow( + parseFloat(rate), + data.baseAmount, + data.vatAmount, + type + ); + }); + } +} + +function createEmptyInvoice() { + const parser = new DOMParser(); + const xmlString = ` + + urn:cen.eu:en16931:2017#compliant#urn:efactura.mfinante.ro:CIUS-RO:1.0.1 + + + + 380 + RON + + + + + + + + + + + RO + + + + RO + + VAT + + + + + + + + + + + + + + + + + + + + + + RO + + + + + + VAT + + + + + + + + + + + +`; + return parser.parseFromString(xmlString, "text/xml"); +} + +function saveXML() { + if (!validateForm()) return; + + // PR-A11: math validation pre-save. Ruleaza validateMath și dacă footer + // diff > tolerance, afișează toast warning ORANGE — DAR NU bloca save-ul. + try { + if (typeof validateMath === 'function') { + const { footerDiff, footerOver } = validateMath(); + if (footerOver) { + showToast( + `Atenție: totalul afișat diferă cu ${footerDiff.toFixed(2)} RON față de calculul intern.`, + 'warning', + 'Verifică liniile și defalcarea TVA. Salvarea continuă.' + ); + } + } + } catch (_) { /* validation must never block save */ } + + try { + if (!currentInvoice) { + currentInvoice = createEmptyInvoice(); + } + + const xmlDoc = currentInvoice; + + // Update all the data + updateBasicDetails(xmlDoc); + updatePartyDetails(xmlDoc); + updateBillingReference(xmlDoc); + updatePaymentMeans(xmlDoc); + updateAllowanceCharges(xmlDoc); + + // Remove existing TaxTotal and LegalMonetaryTotal elements + const existingTaxTotals = xmlDoc.querySelectorAll('cac\\:TaxTotal, TaxTotal'); + existingTaxTotals.forEach(el => el.remove()); + + const existingMonetaryTotal = xmlDoc.querySelector('cac\\:LegalMonetaryTotal, LegalMonetaryTotal'); + if (existingMonetaryTotal) { + existingMonetaryTotal.remove(); + } + + // Remove existing InvoiceLine elements + const existingLines = xmlDoc.querySelectorAll('cac\\:InvoiceLine, InvoiceLine'); + existingLines.forEach(el => el.remove()); + + // Add elements in the correct order + updateTaxTotals(xmlDoc); + updateMonetaryTotals(xmlDoc); + updateLineItems(xmlDoc); + + downloadXML(xmlDoc); + } catch (error) { + handleError(error, 'Eroare la salvarea fișierului XML'); + } +} + +function updateBasicDetails(xmlDoc) { + setXMLValue(xmlDoc, 'cbc\\:ID, ID', document.querySelector('[name="invoiceNumber"]').value); + + // PR-TIPURI (A4): scrie cbc:InvoiceTypeCode din dropdown. Dacă elementul + // lipsește (factură creată cu createEmptyInvoice care îl include deja, sau + // XML legacy fără el), îl creăm și inserăm imediat după cbc:DueDate. + const typeSelect = document.querySelector('[name="invoiceTypeCode"]'); + const typeCode = (typeSelect && INVOICE_TYPES[typeSelect.value]) ? typeSelect.value : '380'; + let typeEl = xmlDoc.querySelector('cbc\\:InvoiceTypeCode, InvoiceTypeCode'); + if (typeEl) { + typeEl.textContent = typeCode; + } else { + typeEl = createXMLElement(xmlDoc, XML_NAMESPACES.cbc, 'cbc:InvoiceTypeCode', typeCode); + const after = xmlDoc.querySelector('cbc\\:DueDate, DueDate') || + xmlDoc.querySelector('cbc\\:IssueDate, IssueDate'); + if (after && after.parentNode) { + after.parentNode.insertBefore(typeEl, after.nextSibling); + } else { + xmlDoc.documentElement.appendChild(typeEl); + } + } + + // Remove existing Note elements + const existingNotes = xmlDoc.querySelectorAll('cbc\\:Note, Note'); + existingNotes.forEach(note => note.remove()); + + // Split note text and create new Note elements + const noteText = document.querySelector('[name="invoiceNote"]').value; + if (noteText) { + const insertAfter = xmlDoc.querySelector('cbc\\:InvoiceTypeCode, InvoiceTypeCode'); + const chunks = splitNoteIntoChunks(noteText, 300); + chunks.forEach(chunk => { + const noteElement = createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:Note", chunk); + if (insertAfter && insertAfter.parentNode) { + insertAfter.parentNode.insertBefore(noteElement, insertAfter.nextSibling); + } + }); + } + + const issueDateValue = document.querySelector('[name="issueDate"]').value; + const dueDateValue = document.querySelector('[name="dueDate"]').value; + + setXMLValue(xmlDoc, 'cbc\\:IssueDate, IssueDate', parseRomanianDate(issueDateValue)); + setXMLValue(xmlDoc, 'cbc\\:DueDate, DueDate', parseRomanianDate(dueDateValue)); + + // Update currency codes + const documentCurrencyCode = document.querySelector('[name="documentCurrencyCode"]').value.toUpperCase() || 'RON'; + setXMLValue(xmlDoc, 'cbc\\:DocumentCurrencyCode, DocumentCurrencyCode', documentCurrencyCode); + + const taxCurrencyCode = document.querySelector('[name="taxCurrencyCode"]').value.toUpperCase(); + if (taxCurrencyCode) { + let taxCurrencyElement = xmlDoc.querySelector('cbc\\:TaxCurrencyCode, TaxCurrencyCode'); + if (!taxCurrencyElement) { + taxCurrencyElement = createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:TaxCurrencyCode"); + const insertAfter = xmlDoc.querySelector('cbc\\:DocumentCurrencyCode, DocumentCurrencyCode'); + if (insertAfter && insertAfter.parentNode) { + insertAfter.parentNode.insertBefore(taxCurrencyElement, insertAfter.nextSibling); + } + } + taxCurrencyElement.textContent = taxCurrencyCode; + } else { + // Remove TaxCurrencyCode if it exists and is empty + const taxCurrencyElement = xmlDoc.querySelector('cbc\\:TaxCurrencyCode, TaxCurrencyCode'); + if (taxCurrencyElement) { + taxCurrencyElement.parentNode.removeChild(taxCurrencyElement); + } + } +} + +function createPartyElement(xmlDoc, isSupplier, partyData) { + const party = createXMLElement(xmlDoc, XML_NAMESPACES.cac, "cac:Party"); + const hasVatPrefix = /^[A-Z]{2}/.test(partyData.vat?.trim() || ''); + const IsNeplatitor = vatHasO(); + + // Add PartyIdentification + if (partyData.companyId) { + const partyIdentification = createXMLElement(xmlDoc, XML_NAMESPACES.cac, "cac:PartyIdentification"); + partyIdentification.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:ID", partyData.companyId)); + party.appendChild(partyIdentification); + } + + function validateCountryCode(countryCode) { + const code = countryCode?.trim().toUpperCase() || 'RO'; + return ISO_3166_1_CODES.has(code) ? code : 'RO'; + } + + function validateCountyCode(countryCode, countyCode) { + if (countryCode === 'RO') { + return ROMANIAN_COUNTY_CODES.has(countyCode) ? countyCode : 'RO-B'; + } + return countyCode; + } + + const postalAddress = createXMLElement(xmlDoc, XML_NAMESPACES.cac, "cac:PostalAddress"); + postalAddress.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:StreetName", partyData.address)); + postalAddress.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:CityName", partyData.city)); + postalAddress.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:CountrySubentity", partyData.county)); + + const country = createXMLElement(xmlDoc, XML_NAMESPACES.cac, "cac:Country"); + // Ensure country code is valid ISO 3166-1 format (2 uppercase letters) + const countryCode = partyData.country?.trim().toUpperCase() || 'RO'; + country.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:IdentificationCode", countryCode)); + postalAddress.appendChild(country); + party.appendChild(postalAddress); + + const vatTypesInUse = new Set(); + document.querySelectorAll('.line-item').forEach((item, index) => { + const vatType = document.querySelector(`[name="vatType${index}"]`).value; + vatTypesInUse.add(vatType); + }); + + // Add PartyTaxScheme for all suppliers VAT types except 'O' or all customers with VAT prefix + if (hasVatPrefix ) { + const partyTaxScheme = createXMLElement(xmlDoc, XML_NAMESPACES.cac, "cac:PartyTaxScheme"); + partyTaxScheme.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:CompanyID", partyData.vat.toUpperCase())); + const taxScheme = createXMLElement(xmlDoc, XML_NAMESPACES.cac, "cac:TaxScheme"); + if (!IsNeplatitor) { + taxScheme.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:ID", "VAT")); + } + else { + // console.log('not adding VAT ID'); + } + partyTaxScheme.appendChild(taxScheme); + + party.appendChild(partyTaxScheme); + } + + // Funcție pentru detectarea dacă există un VAT cu tipul 'O' Neplatitor de TVA + // În cazul în care există, nu se va adăuga un element TaxScheme cu ID-ul 'VAT' + function vatHasO() { + const vatTypesInUse = new Set(); + document.querySelectorAll('.line-item').forEach((item, index) => { + const vatType = document.querySelector(`[name="vatType${index}"]`).value; + vatTypesInUse.add(vatType); + }); + return vatTypesInUse.size > 0 && vatTypesInUse.has('O'); + } + + // Add PartyLegalEntity + const partyLegalEntity = createXMLElement(xmlDoc, XML_NAMESPACES.cac, "cac:PartyLegalEntity"); + partyLegalEntity.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:RegistrationName", partyData.name)); + + // Add CompanyID + if (!hasVatPrefix) { + // For non-VAT registered supplier, use VAT number in CompanyID + partyLegalEntity.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:CompanyID", partyData.vat)); + } else { + // For others, use companyId + partyLegalEntity.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:CompanyID", partyData.companyId)); + } + + party.appendChild(partyLegalEntity); + + // Add Contact if phone, email or contactName exists + if (partyData.phone || partyData.email || partyData.contactName) { + const contact = createXMLElement(xmlDoc, XML_NAMESPACES.cac, "cac:Contact"); + + if (partyData.contactName) { + contact.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:Name", partyData.contactName)); + } + if (partyData.phone) { + contact.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:Telephone", partyData.phone)); + } + if (partyData.email) { + contact.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:ElectronicMail", partyData.email)); + } + party.appendChild(contact); + } + return party; +} + +function updatePartyDetails(xmlDoc) { + // Update supplier details + const supplierData = { + name: document.querySelector('[name="supplierName"]').value, + vat: document.querySelector('[name="supplierVAT"]').value, + companyId: document.querySelector('[name="supplierCompanyId"]').value, + address: document.querySelector('[name="supplierAddress"]').value, + city: document.querySelector('[name="supplierCity"]').value, + county: document.querySelector('[name="supplierCountrySubentity"]').value, + country: document.querySelector('[name="supplierCountry"]').value, + phone: document.querySelector('[name="supplierPhone"]').value, + contactName: document.querySelector('[name="supplierContactName"]').value, + email: document.querySelector('[name="supplierEmail"]').value + }; + + updatePartyXML(xmlDoc, true, supplierData); + + // Update customer details + const customerData = { + name: document.querySelector('[name="customerName"]').value, + vat: document.querySelector('[name="customerVAT"]').value, + companyId: document.querySelector('[name="customerCompanyId"]').value, + address: document.querySelector('[name="customerAddress"]').value, + city: document.querySelector('[name="customerCity"]').value, + county: document.querySelector('[name="customerCountrySubentity"]').value, + country: document.querySelector('[name="customerCountry"]').value, + phone: document.querySelector('[name="customerPhone"]').value, + contactName: document.querySelector('[name="customerContactName"]').value, + email: document.querySelector('[name="customerEmail"]').value + }; + + updatePartyXML(xmlDoc, false, customerData); +} + +function updatePartyXML(xmlDoc, isSupplier, partyData) { + const partyElement = createPartyElement(xmlDoc, isSupplier, partyData); + const parentTag = isSupplier ? 'AccountingSupplierParty' : 'AccountingCustomerParty'; + let parentElement = xmlDoc.querySelector(`cac\\:${parentTag}, ${parentTag}`); + + if (!parentElement) { + parentElement = createXMLElement(xmlDoc, XML_NAMESPACES.cac, `cac:${parentTag}`); + const insertPoint = isSupplier ? + xmlDoc.querySelector('cbc\\:DocumentCurrencyCode, DocumentCurrencyCode') : + xmlDoc.querySelector('cac\\:AccountingSupplierParty, AccountingSupplierParty'); + + if (insertPoint && insertPoint.parentNode) { + insertPoint.parentNode.insertBefore(parentElement, insertPoint.nextSibling); + } else { + xmlDoc.documentElement.appendChild(parentElement); + } + } + + // Remove existing Party element if it exists + const existingParty = parentElement.querySelector('cac\\:Party, Party'); + if (existingParty) { + existingParty.remove(); + } + + parentElement.appendChild(partyElement); +} + +function getAllowanceCharges() { + const charges = []; + document.querySelectorAll('.allowance-charge').forEach((item, index) => { + charges.push({ + isCharge: document.querySelector(`[name="chargeType${index}"]`).value === 'true', + reasonCode: document.querySelector(`[name="chargeReasonCode${index}"]`).value, + reason: document.querySelector(`[name="chargeReason${index}"]`).value, + amount: parseFloat(document.querySelector(`[name="chargeAmount${index}"]`).value) || 0, + vatRate: parseFloat(document.querySelector(`[name="chargeVatRate${index}"]`).value) || 19.0, + vatTypeId: document.querySelector(`[name="chargeVatType${index}"]`).value || 'S' + }); + }); + return charges; +} + +function updateAllowanceCharges(xmlDoc) { + const currencyID = document.querySelector('[name="documentCurrencyCode"]').value.toUpperCase() || 'RON'; + + // Remove existing global allowances/charges + const existingCharges = xmlDoc.querySelectorAll('Invoice > cac\\:AllowanceCharge, Invoice > AllowanceCharge'); + existingCharges.forEach(charge => charge.remove()); + + // Add global allowances + document.querySelectorAll('.allowance-charge').forEach((item, index) => { + const allowanceElement = createXMLElement(xmlDoc, XML_NAMESPACES.cac, "cac:AllowanceCharge"); + + const isCharge = document.querySelector(`[name="chargeType${index}"]`).value === 'true'; + const amount = parseFloat(document.querySelector(`[name="chargeAmount${index}"]`).value) || 0; + const baseAmount = parseFloat(document.querySelector(`[name="chargeBaseAmount${index}"]`).value) || 0; + const reasonCode = document.querySelector(`[name="chargeReasonCode${index}"]`).value; + const reason = document.querySelector(`[name="chargeReason${index}"]`).value; + const vatTypeId = document.querySelector(`[name="chargeVatType${index}"]`).value; + const vatRate = parseFloat(document.querySelector(`[name="chargeVatRate${index}"]`).value) || 0; + + allowanceElement.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:ChargeIndicator", + isCharge.toString())); + + if (reasonCode) { + allowanceElement.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, + "cbc:AllowanceChargeReasonCode", reasonCode)); + } + + if (reason) { + allowanceElement.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, + "cbc:AllowanceChargeReason", reason)); + } + + // Add multiplier if exists + if (baseAmount > 0) { + const multiplier = (amount / baseAmount) * 100; + allowanceElement.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, + "cbc:MultiplierFactorNumeric", multiplier.toFixed(2))); + } + + allowanceElement.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:Amount", + amount.toFixed(2), { currencyID })); + + if (baseAmount > 0) { + allowanceElement.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:BaseAmount", + baseAmount.toFixed(2), { currencyID })); + } + + // Add tax category + const taxCategory = createXMLElement(xmlDoc, XML_NAMESPACES.cac, "cac:TaxCategory"); + taxCategory.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:ID", vatTypeId)); + taxCategory.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:Percent", + vatRate.toString())); + + const taxScheme = createXMLElement(xmlDoc, XML_NAMESPACES.cac, "cac:TaxScheme"); + taxScheme.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:ID", "VAT")); + taxCategory.appendChild(taxScheme); + + allowanceElement.appendChild(taxCategory); + + // Insert before TaxTotal + const taxTotal = xmlDoc.querySelector('cac\\:TaxTotal, TaxTotal'); + if (taxTotal) { + xmlDoc.documentElement.insertBefore(allowanceElement, taxTotal); + } else { + xmlDoc.documentElement.appendChild(allowanceElement); + } + }); +} + +function updateLineItems(xmlDoc) { + const currencyID = xmlDoc.querySelector('cbc\\:DocumentCurrencyCode, DocumentCurrencyCode').textContent; + + // Șterge liniile existente + const existingLines = xmlDoc.querySelectorAll('cac\\:InvoiceLine, InvoiceLine'); + existingLines.forEach(line => line.remove()); + + document.querySelectorAll('.line-item').forEach((item, index) => { + const invoiceLine = createXMLElement(xmlDoc, XML_NAMESPACES.cac, "cac:InvoiceLine"); + + // Obține valorile liniei + const quantity = document.querySelector(`[name="quantity${index}"]`)?.value || '0'; + const unitCode = document.querySelector(`[name="unit${index}"]`)?.value || 'EA'; + const price = document.querySelector(`[name="price${index}"]`)?.value || '0'; + const description = document.querySelector(`[name="description${index}"]`)?.value || ''; + const itemDescription = document.querySelector(`[name="itemDescription${index}"]`)?.value || ''; + const vatType = document.querySelector(`[name="vatType${index}"]`)?.value || 'S'; + const vatRate = vatType === 'AE' ? '0.00' : + (document.querySelector(`[name="vatRate${index}"]`)?.value || '0'); + const lineDiscount = parseFloat(document.querySelector(`[name="lineDiscount${index}"]`)?.value) || 0; + + // Calculează valorile liniei + const baseLineAmount = quantity * price; + const lineAmount = roundNumber(baseLineAmount - lineDiscount); + + // Adaugă elementele liniei + invoiceLine.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:ID", (index + 1).toString())); + + const quantityElement = createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:InvoicedQuantity", quantity); + quantityElement.setAttribute('unitCode', unitCode); + invoiceLine.appendChild(quantityElement); + + // Adaugă LineExtensionAmount (valoarea după discount) + invoiceLine.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:LineExtensionAmount", + lineAmount.toFixed(2), { currencyID })); + + // Adaugă discount pe linie dacă există + if (lineDiscount != 0) { + const allowanceCharge = createXMLElement(xmlDoc, XML_NAMESPACES.cac, "cac:AllowanceCharge"); + allowanceCharge.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:ChargeIndicator", + lineDiscount < 0 ? "true" : "false")); + + const reasonCode = document.querySelector(`[name="discountReasonCode${index}"]`)?.value || '95'; + allowanceCharge.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, + "cbc:AllowanceChargeReasonCode", reasonCode)); + allowanceCharge.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, + "cbc:AllowanceChargeReason", ALLOWANCE_REASON_CODES[reasonCode] || 'Reducere')); + + // BaseAmount trebuie să fie valoarea netă înainte de discount + const baseAmount = Math.abs(lineAmount); + const discountAmount = Math.abs(lineDiscount); + const multiplierFactor = (discountAmount / baseAmount * 100).toFixed(2); + + allowanceCharge.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, + "cbc:MultiplierFactorNumeric", multiplierFactor)); + + allowanceCharge.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:Amount", + discountAmount.toFixed(2), { currencyID })); + + allowanceCharge.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:BaseAmount", + baseAmount.toFixed(2), { currencyID })); + + invoiceLine.appendChild(allowanceCharge); + } + + // Adaugă detaliile articolului + const itemElement = createXMLElement(xmlDoc, XML_NAMESPACES.cac, "cac:Item"); + + if (itemDescription) { + itemElement.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:Description", + itemDescription)); + } + + itemElement.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:Name", description)); + + // Adaugă identificările + const identificationsContainer = document.querySelector(`#identifications${index}`); + if (identificationsContainer) { + saveIdentificationsToXML(xmlDoc, itemElement, index); + } + + // Adaugă categoria de taxă + const taxCategory = createXMLElement(xmlDoc, XML_NAMESPACES.cac, "cac:ClassifiedTaxCategory"); + taxCategory.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:ID", vatType)); + if (vatType !== 'O') { + taxCategory.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:Percent", vatRate)); + } + + const taxScheme = createXMLElement(xmlDoc, XML_NAMESPACES.cac, "cac:TaxScheme"); + taxScheme.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:ID", "VAT")); + taxCategory.appendChild(taxScheme); + itemElement.appendChild(taxCategory); + + invoiceLine.appendChild(itemElement); + + // Adaugă prețul + const priceElement = createXMLElement(xmlDoc, XML_NAMESPACES.cac, "cac:Price"); + priceElement.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:PriceAmount", + price, { currencyID })); + invoiceLine.appendChild(priceElement); + + xmlDoc.documentElement.appendChild(invoiceLine); + }); +} + +function updateTaxTotals(xmlDoc) { + const currencyID = document.querySelector('[name="documentCurrencyCode"]').value.toUpperCase() || 'RON'; + const taxCurrencyCode = document.querySelector('[name="taxCurrencyCode"]').value.toUpperCase(); + + // Remove existing TaxTotal elements + const existingTaxTotals = xmlDoc.querySelectorAll('cac\\:TaxTotal, TaxTotal'); + existingTaxTotals.forEach(element => element.parentNode.removeChild(element)); + + // Create main TaxTotal for document currency + const taxTotal = createXMLElement(xmlDoc, XML_NAMESPACES.cac, "cac:TaxTotal"); + + // Use precise currency parsing for total VAT + const uiTotalVat = formatter.parseCurrency(document.getElementById('vat').textContent); + const taxAmountElement = createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:TaxAmount", + uiTotalVat.toFixed(2), { currencyID }); + taxTotal.appendChild(taxAmountElement); + + // Add TaxSubtotal elements to the main TaxTotal + const vatRows = document.querySelectorAll('.vat-row'); + vatRows.forEach(row => { + const taxSubtotal = createXMLElement(xmlDoc, XML_NAMESPACES.cac, "cac:TaxSubtotal"); + + const baseAmount = formatter.parseCurrency(row.querySelector('.vat-base').value) || 0; + const vatAmount = formatter.parseCurrency(row.querySelector('.vat-amount').value) || 0; + const vatType = row.querySelector('.vat-type').value; + const vatRate = parseFloat(row.querySelector('.vat-rate').value) || 0; + + taxSubtotal.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:TaxableAmount", + baseAmount.toFixed(2), { currencyID })); + taxSubtotal.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:TaxAmount", + vatAmount.toFixed(2), { currencyID })); + + const taxCategory = createXMLElement(xmlDoc, XML_NAMESPACES.cac, "cac:TaxCategory"); + taxCategory.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:ID", vatType)); + + const percent = vatType === 'AE' ? '0.00' : vatRate.toFixed(2); + taxCategory.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:Percent", percent)); + + // Add exemption code and reason for special VAT types + if (['E', 'K', 'O', 'AE'].includes(vatType)) { + const exemptionCode = row.querySelector('.vat-exemption-code')?.value; + const exemptionReason = row.querySelector('.vat-exemption-reason')?.value; + + if (exemptionCode) { + taxCategory.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, + "cbc:TaxExemptionReasonCode", exemptionCode)); + } + if (exemptionReason) { + taxCategory.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, + "cbc:TaxExemptionReason", exemptionReason)); + } + } + + const taxScheme = createXMLElement(xmlDoc, XML_NAMESPACES.cac, "cac:TaxScheme"); + taxScheme.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:ID", "VAT")); + taxCategory.appendChild(taxScheme); + + taxSubtotal.appendChild(taxCategory); + taxTotal.appendChild(taxSubtotal); + }); + + // Insert TaxTotal in the correct position + let insertionPoint = xmlDoc.querySelector('cac\\:AllowanceCharge, AllowanceCharge'); + if (insertionPoint) { + while (insertionPoint.nextElementSibling && + (insertionPoint.nextElementSibling.localName === 'AllowanceCharge' || + insertionPoint.nextElementSibling.localName === 'TaxTotal')) { + insertionPoint = insertionPoint.nextElementSibling; + } + insertionPoint.parentNode.insertBefore(taxTotal, insertionPoint.nextSibling); + } else { + const monetaryTotal = xmlDoc.querySelector('cac\\:LegalMonetaryTotal, LegalMonetaryTotal'); + if (monetaryTotal) { + monetaryTotal.parentNode.insertBefore(taxTotal, monetaryTotal); + } else { + xmlDoc.documentElement.appendChild(taxTotal); + } + } + + // If tax currency is specified, add another TaxTotal element + if (taxCurrencyCode && taxCurrencyCode !== currencyID) { + const taxCurrencyTotal = createXMLElement(xmlDoc, XML_NAMESPACES.cac, "cac:TaxTotal"); + + const exchangeRateInput = document.querySelector('[name="exchangeRate"]'); + const exchangeRate = exchangeRateInput ? parseFloat(exchangeRateInput.value) || 1 : 1; + const taxCurrencyVAT = uiTotalVat * exchangeRate; + + const taxCurrencyAmountElement = createXMLElement(xmlDoc, XML_NAMESPACES.cbc, "cbc:TaxAmount", + taxCurrencyVAT.toFixed(2), { currencyID: taxCurrencyCode }); + taxCurrencyTotal.appendChild(taxCurrencyAmountElement); + + // Insert after the main TaxTotal + taxTotal.parentNode.insertBefore(taxCurrencyTotal, taxTotal.nextSibling); + } +} + +function updateMonetaryTotals(xmlDoc) { + const currencyID = document.querySelector('[name="documentCurrencyCode"]').value.toUpperCase() || 'RON'; + + // Use direct parsing to preserve full precision and manual edits + const subtotal = formatter.parseCurrency(document.getElementById('subtotal').textContent); + const allowances = formatter.parseCurrency(document.getElementById('totalAllowances').textContent); + const charges = formatter.parseCurrency(document.getElementById('totalCharges').textContent); + const netAmount = formatter.parseCurrency(document.getElementById('netAmount').textContent); + const totalVat = formatter.parseCurrency(document.getElementById('vat').textContent); + const total = formatter.parseCurrency(document.getElementById('total').textContent); + + // Rest of the function remains the same... + const existingMonetaryTotal = xmlDoc.querySelector('cac\\:LegalMonetaryTotal, LegalMonetaryTotal'); + if (existingMonetaryTotal) { + existingMonetaryTotal.parentNode.removeChild(existingMonetaryTotal); + } + + const monetaryTotal = createXMLElement(xmlDoc, XML_NAMESPACES.cac, "cac:LegalMonetaryTotal"); + + const amounts = { + "LineExtensionAmount": subtotal, + "TaxExclusiveAmount": netAmount, + "TaxInclusiveAmount": total, + "AllowanceTotalAmount": allowances, + "ChargeTotalAmount": charges, + "PayableAmount": total + }; + + Object.entries(amounts).forEach(([elementName, value]) => { + monetaryTotal.appendChild(createXMLElement(xmlDoc, XML_NAMESPACES.cbc, + `cbc:${elementName}`, value.toFixed(2), { currencyID })); + }); + + // Insertion logic remains the same + let insertionPoint = xmlDoc.querySelector('cac\\:TaxTotal, TaxTotal'); + if (insertionPoint) { + while (insertionPoint.nextElementSibling && + insertionPoint.nextElementSibling.localName === 'TaxTotal') { + insertionPoint = insertionPoint.nextElementSibling; + } + insertionPoint.parentNode.insertBefore(monetaryTotal, insertionPoint.nextSibling); + } else { + const firstInvoiceLine = xmlDoc.querySelector('cac\\:InvoiceLine, InvoiceLine'); + if (firstInvoiceLine) { + firstInvoiceLine.parentNode.insertBefore(monetaryTotal, firstInvoiceLine); + } else { + xmlDoc.documentElement.appendChild(monetaryTotal); + } + } +} + +function getExchangeRate(fromCurrency, toCurrency) { + // For now, return 1 as default exchange rate + // TODO: Implement proper exchange rate handling + return 1; +} + +// Update the invoice form to include exchange rate when tax currency is different +function addExchangeRateField() { + const taxCurrencyInput = document.querySelector('[name="taxCurrencyCode"]'); + const documentCurrencyInput = document.querySelector('[name="documentCurrencyCode"]'); + + function updateExchangeRateVisibility() { + const taxCurrency = taxCurrencyInput.value.toUpperCase(); + const documentCurrency = documentCurrencyInput.value.toUpperCase(); + + let exchangeRateContainer = document.getElementById('exchangeRateContainer'); + if (taxCurrency && taxCurrency !== documentCurrency) { + if (!exchangeRateContainer) { + const container = document.createElement('div'); + container.id = 'exchangeRateContainer'; + container.className = 'form-group'; + container.innerHTML = ` + + + `; + taxCurrencyInput.parentNode.after(container); + } else { + // Update label if currencies changed + const label = exchangeRateContainer.querySelector('label'); + label.textContent = `Curs Valutar ${documentCurrency}/${taxCurrency}`; + } + } else if (exchangeRateContainer) { + exchangeRateContainer.remove(); + } + } + + taxCurrencyInput.addEventListener('input', updateExchangeRateVisibility); + documentCurrencyInput.addEventListener('input', updateExchangeRateVisibility); + + // Initial check + updateExchangeRateVisibility(); +} + + +// Utility functions +function getXMLValue(xmlDoc, selector, defaultValue = '') { + if (!xmlDoc) return defaultValue; + try { + const element = xmlDoc.querySelector(selector); + const value = element ? element.textContent : defaultValue; + // console.log(`getXMLValue: Selector: ${selector}, Value: ${value}`); + return value; + } catch (error) { + console.warn(`Eroare la obținerea valorii pentru selectorul ${selector}:`, error); + return defaultValue; + } +} + +function setXMLValue(xmlDoc, selector, value) { + try { + const element = xmlDoc.querySelector(selector); + if (element) { + element.textContent = value; + return true; + } + return false; + } catch (error) { + console.warn(`Eroare la setarea valorii pentru selectorul ${selector}:`, error); + return false; + } +} + +function createXMLElement(xmlDoc, namespace, elementName, value = '', attributes = {}) { + const element = xmlDoc.createElementNS(namespace, elementName); + if (value) { + element.textContent = value; + } + Object.entries(attributes).forEach(([key, value]) => { + element.setAttribute(key, value); + }); + return element; +} + +function formatXML(xmlString) { + let formatted = ''; + let indent = ''; + const tab = ' '; + + xmlString.split(/>\s* { + if (node.match(/^\/\w/)) { + indent = indent.substring(tab.length); + } + formatted += indent + '<' + node + '>\r\n'; + if (node.match(/^]*[^\/]$/) && !node.startsWith("?")) { + indent += tab; + } + }); + + return formatted.substring(1, formatted.length - 3); +} + +function downloadXML(xmlDoc) { + const serializer = new XMLSerializer(); + let xmlString = serializer.serializeToString(xmlDoc); + + if (!xmlString.startsWith('\n' + xmlString; + } + + xmlString = formatXML(xmlString); + + const blob = new Blob([xmlString], { type: 'application/xml' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = 'factura_' + document.querySelector('[name="invoiceNumber"]').value + '.xml'; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); +} + +function roundNumber(number, decimals = 2) { + return Math.round(number * Math.pow(10, decimals)) / Math.pow(10, decimals); +} + +function renumberLineItems() { + document.querySelectorAll('.line-item').forEach((item, newIndex) => { + item.dataset.index = newIndex; + + item.querySelectorAll('input, select').forEach(input => { + const name = input.getAttribute('name'); + if (name) { + const baseName = name.replace(/\d+$/, ''); + input.setAttribute('name', baseName + newIndex); + } + }); + }); +} + + +function createIdentificationHTML(index, type, value = '', schemeId = '') { + const typeInfo = IDENTIFICATION_TYPES[type]; + const id = `${type.toLowerCase()}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; + + if (type === 'COMMODITY') { + return ` +
    +
    + + + +
    +
    `; + } + + return ` +
    +
    + ${type === 'STANDARD' ? `` : ''} + + +
    +
    `; +} + +window.addIdentification = function(lineItemIndex, type) { + const container = document.querySelector(`#identifications${lineItemIndex}`); + if (container) { + container.insertAdjacentHTML('beforeend', createIdentificationHTML(lineItemIndex, type)); + validateIdentifications(lineItemIndex); + } +} + +window.removeIdentification = function(id) { + const element = document.querySelector(`[data-id="${id}"]`); + if (element) { + const lineItem = element.closest('.line-item'); + element.remove(); + if (lineItem) { + validateIdentifications(parseInt(lineItem.dataset.index)); + } + } +}; + + +// Update XML parsing +function parseIdentifications(itemElement, lineItemIndex) { + // console.log("Parsing identifications for line", lineItemIndex); + + const container = document.querySelector(`#identifications${lineItemIndex}`); + if (!container) return; + + const listContainer = container.querySelector('.identifications-list'); + listContainer.innerHTML = ''; + + // Parse BuyersItemIdentification + const buyersId = itemElement.querySelector('cac\\:BuyersItemIdentification cbc\\:ID, BuyersItemIdentification ID'); + if (buyersId) { + listContainer.insertAdjacentHTML('beforeend', + createIdentificationHTML(lineItemIndex, 'BUYERS', buyersId.textContent) + ); + } + + // Parse SellersItemIdentification + const sellersId = itemElement.querySelector('cac\\:SellersItemIdentification cbc\\:ID, SellersItemIdentification ID'); + if (sellersId) { + listContainer.insertAdjacentHTML('beforeend', + createIdentificationHTML(lineItemIndex, 'SELLERS', sellersId.textContent) + ); + } + + // Parse StandardItemIdentification + const standardId = itemElement.querySelector('cac\\:StandardItemIdentification cbc\\:ID, StandardItemIdentification ID'); + if (standardId) { + listContainer.insertAdjacentHTML('beforeend', + createIdentificationHTML(lineItemIndex, 'STANDARD', standardId.textContent) + ); + } + + // Parse CommodityClassifications + const commodityClassifications = itemElement.querySelectorAll('cac\\:CommodityClassification cbc\\:ItemClassificationCode, CommodityClassification ItemClassificationCode'); + commodityClassifications.forEach(classification => { + const listId = classification.getAttribute('listID') || 'CV'; + const code = classification.textContent; + if (code && listId) { + listContainer.insertAdjacentHTML('beforeend', + createIdentificationHTML(lineItemIndex, 'COMMODITY', code, listId) + ); + } + }); +} + +// Update XML saving +function saveIdentificationsToXML(xmlDoc, itemElement, lineItemIndex) { + const container = document.querySelector(`#identifications${lineItemIndex}`); + if (!container) return; + + container.querySelectorAll('.identification-row').forEach(row => { + const type = row.dataset.type; + const id = row.dataset.id; + const schemeInput = document.querySelector(`[name="scheme_${id}"]`); + const valueInput = document.querySelector(`[name="value_${id}"]`); + + if (!valueInput?.value) return; + + if (type === 'COMMODITY') { + const schemeSelect = document.querySelector(`[name="scheme_${id}"]`); + if (!schemeSelect) return; + + const identificationElement = createXMLElement(xmlDoc, XML_NAMESPACES.cac, 'cac:CommodityClassification'); + const idElement = createXMLElement(xmlDoc, XML_NAMESPACES.cbc, 'cbc:ItemClassificationCode', valueInput.value); + idElement.setAttribute('listID', schemeSelect.value); + identificationElement.appendChild(idElement); + itemElement.appendChild(identificationElement); + } else { + const typeInfo = IDENTIFICATION_TYPES[type]; + const identificationElement = createXMLElement(xmlDoc, XML_NAMESPACES.cac, `cac:${typeInfo.xmlTag}`); + const idElement = createXMLElement(xmlDoc, XML_NAMESPACES.cbc, 'cbc:ID', valueInput.value); + + if (type === 'STANDARD') { + idElement.setAttribute('schemeID', '0160'); + } + + identificationElement.appendChild(idElement); + itemElement.appendChild(identificationElement); + } + }); +} + + +// Add validation +function validateIdentifications(lineItemIndex) { + const container = document.querySelector(`#identifications${lineItemIndex}`); + if (!container) return true; + + let isValid = true; + container.querySelectorAll('.identification-row').forEach(row => { + const type = row.dataset.type; + const id = row.dataset.id; + const input = row.querySelector(`[name="value_${id}"]`); + + if (type === 'COMMODITY') { + const scheme = row.querySelector('.scheme-select').value; + if (scheme === 'TSP' && !/^\d{8}-\d$/.test(input.value)) { + input.classList.add('invalid'); + isValid = false; + } else if (scheme === 'STI' && !/^\d{8}$/.test(input.value)) { + input.classList.add('invalid'); + isValid = false; + } else { + input.classList.remove('invalid'); + } + } + }); + + return isValid; +} + +// ============================================================================ +// PR-A11: math validation inline (badge per-line / per-VAT-row / footer total) +// ============================================================================ + +const A11_TOLERANCE_LEGACY = new Big('0.01'); // ±0.01 RON pe rows complet clean +const A11_TOLERANCE_DIRTY = new Big('0'); // zero pe rows cu dirty='1' + +function _a11RowDirty(rowEl) { + if (!rowEl) return false; + return Array.from(rowEl.querySelectorAll('input')).some( + i => i.dataset && i.dataset.dirty === '1' + ); +} + +function _a11Tolerance(rowEl) { + return _a11RowDirty(rowEl) ? A11_TOLERANCE_DIRTY : A11_TOLERANCE_LEGACY; +} + +function _a11SetBadge(badgeEl, computedBig, displayedBig, epsBig, opts = {}) { + if (!badgeEl) return; + const { footer = false } = opts; + const diff = computedBig.minus(displayedBig); + const absDiff = diff.abs(); + badgeEl.classList.remove('badge-ok', 'badge-warn', 'badge-error'); + if (absDiff.lte(epsBig)) { + badgeEl.classList.add('badge-ok'); + badgeEl.textContent = '✓'; + return false; + } + if (footer) { + badgeEl.classList.add('badge-warn'); + badgeEl.textContent = `diferență ${absDiff.toFixed(2)} RON`; + } else { + badgeEl.classList.add('badge-error'); + const sign = diff.gte(0) ? '+' : '−'; + badgeEl.textContent = `${sign}${absDiff.toFixed(2)} RON`; + } + return true; +} + +/** + * PR-A11: validează consistența matematică a formularului și actualizează + * badge-urile (per-line, per-VAT-row, footer total). + * + * Lect: input.dataset.raw (canonical decimal-dot, source of truth E1). + * Compară: + * - line item: computed (qty*price-discount) vs XML loaded LineExtensionAmount. + * - VAT row: computed (base*rate/100, doar pentru type='S') vs vat-amount input. + * - footer total: computed (subtotal-allow+charges+totalVat) vs #total displayed. + * + * Tolerance switching pe row: dacă orice input din row are dataset.dirty='1' → + * zero (newly computed must match exact). Altfel ±0.01 RON (legacy float + * reconciliation pe XML încărcat). + * + * @returns {{footerDiff: Big, footerOver: boolean}} info pentru save toast. + */ +function validateMath() { + // Per-line: computed line net vs XML loaded LineExtensionAmount. + document.querySelectorAll('.line-item').forEach(item => { + const idx = item.dataset.index; + const qtyInput = document.querySelector(`[name="quantity${idx}"]`); + const priceInput = document.querySelector(`[name="price${idx}"]`); + const discountInput = document.querySelector(`[name="lineDiscount${idx}"]`); + if (!qtyInput || !priceInput) return; + + const qty = getRaw(qtyInput); + const price = getRaw(priceInput); + const discount = discountInput ? getRaw(discountInput) : new Big('0'); + const computed = qty.times(price).minus(discount); + + // Update vizibilul "Total linie" la valoarea recalculată. + const totalEl = item.querySelector(`[data-line-total-index="${idx}"]`); + if (totalEl) totalEl.textContent = format2(computed.round(2, 1)); + + const badgeEl = item.querySelector(`[data-line-badge-index="${idx}"]`); + if (!badgeEl) return; + + // Linii nou-adăugate (fără XML referință) → badge ✓ trivial. + const xmlAmt = item.dataset.xmlLineAmount; + if (xmlAmt === undefined || xmlAmt === '') { + badgeEl.textContent = ''; + badgeEl.classList.remove('badge-ok', 'badge-warn', 'badge-error'); + return; + } + const displayed = parseStrictOr(xmlAmt, '0'); + const eps = _a11Tolerance(item); + _a11SetBadge(badgeEl, computed.round(2, 1), displayed, eps); + }); + + // Per-VAT-row: computed (base*rate/100) vs vat-amount input. + document.querySelectorAll('.vat-row').forEach(row => { + const typeSelect = row.querySelector('.vat-type'); + const baseInput = row.querySelector('.vat-base'); + const rateInput = row.querySelector('.vat-rate'); + const amountInput = row.querySelector('.vat-amount'); + const badgeEl = row.querySelector('.vat-amount-badge'); + if (!badgeEl || !typeSelect || !baseInput || !rateInput || !amountInput) return; + + const type = typeSelect.value; + const base = getRaw(baseInput); + const rate = getRaw(rateInput); + const displayed = getRaw(amountInput); + + const computed = (type === 'S') + ? base.times(rate).div(100).round(2, 1) + : new Big('0'); + const eps = _a11Tolerance(row); + _a11SetBadge(badgeEl, computed, displayed, eps); + }); + + // Footer total: computed (subtotal-allow+charges+totalVat) vs #total displayed. + const totalBadge = document.getElementById('total-badge'); + let footerDiff = new Big('0'); + let footerOver = false; + if (totalBadge) { + const subtotalBig = parseStrictOr(document.getElementById('subtotal').textContent, '0'); + const allowancesBig = parseStrictOr(document.getElementById('totalAllowances').textContent, '0'); + const chargesBig = parseStrictOr(document.getElementById('totalCharges').textContent, '0'); + const vatBig = parseStrictOr(document.getElementById('vat').textContent, '0'); + const displayed = parseStrictOr(document.getElementById('total').textContent, '0'); + + const computed = subtotalBig.minus(allowancesBig).plus(chargesBig).plus(vatBig).round(2, 1); + // Footer tolerance: orice input dirty în întreaga formă → zero, altfel ±0.01. + const anyDirty = Array.from( + document.querySelectorAll('.line-item, .vat-row, .allowance-charge') + ).some(_a11RowDirty); + const eps = anyDirty ? A11_TOLERANCE_DIRTY : A11_TOLERANCE_LEGACY; + footerDiff = computed.minus(displayed).abs(); + footerOver = _a11SetBadge(totalBadge, computed, displayed, eps, { footer: true }) === true; + } + return { footerDiff, footerOver }; +} + +/** + * PR-A11: minimal toast helper. Compatibil cu DESIGN.md spec D14 (border-left + * 3px semantic, slide-in 150ms, auto-dismiss success 4s / info 6s / warning 6s + * / error persistent). NU bloca save — doar avertizează. + */ +function showToast(message, variant = 'info', subtext = '') { + let container = document.getElementById('toast-container'); + if (!container) { + container = document.createElement('div'); + container.id = 'toast-container'; + container.className = 'toast-container'; + container.setAttribute('aria-live', 'polite'); + document.body.appendChild(container); + } + const toast = document.createElement('div'); + toast.className = `toast toast-${variant}`; + toast.setAttribute('role', (variant === 'warning' || variant === 'error') ? 'alert' : 'status'); + + const msgEl = document.createElement('div'); + msgEl.className = 'toast-message'; + msgEl.textContent = message; + if (subtext) { + const subEl = document.createElement('span'); + subEl.className = 'toast-sub'; + subEl.textContent = subtext; + msgEl.appendChild(subEl); + } + toast.appendChild(msgEl); + + const dismiss = document.createElement('button'); + dismiss.className = 'toast-dismiss'; + dismiss.type = 'button'; + dismiss.setAttribute('aria-label', 'Închide'); + dismiss.textContent = '×'; + dismiss.onclick = () => toast.remove(); + toast.appendChild(dismiss); + + container.appendChild(toast); + + const timeouts = { success: 4000, info: 6000, warning: 6000, error: 0 }; + const ms = timeouts[variant] ?? 6000; + if (ms > 0) { + setTimeout(() => { if (toast.isConnected) toast.remove(); }, ms); + } +} + +// Expose pentru consumeri externi / debugging. +window.validateMath = validateMath; +window.showToast = showToast; + +// ============================================================================ +// PR-BR (A2): Floating BR Validation Panel (D5) +// Panel sticky bottom-right — colecteaza snapshot form, rulează cele 30 +// reguli BR/CIUS-RO și afișează violările cu link la câmp + highlight 2s. +// ============================================================================ + +/** + * Colectează snapshot-ul datelor din formular pentru validarea BR. + * Citește valorile afișate (sau dataset.raw pentru numerice). + */ +function collectInvoiceDataForBR() { + const lineItemEls = Array.from(document.querySelectorAll('.line-item')); + + const lineItems = lineItemEls.map(el => { + const idx = el.dataset.index; + const qEl = document.querySelector(`[name="quantity${idx}"]`); + const pEl = document.querySelector(`[name="price${idx}"]`); + const dEl = document.querySelector(`[name="lineDiscount${idx}"]`); + const tEl = document.querySelector(`[data-line-total-index="${idx}"]`); + return { + index: parseInt(idx, 10), + description: (document.querySelector(`[name="description${idx}"]`)?.value || '').trim(), + quantity: qEl ? (qEl.dataset.raw || qEl.value) : '', + unitPrice: pEl ? (pEl.dataset.raw || pEl.value) : '', + discount: dEl ? (dEl.dataset.raw || dEl.value || '0') : '0', + vatType: (document.querySelector(`[name="vatType${idx}"]`)?.value || ''), + vatRate: (document.querySelector(`[name="vatRate${idx}"]`)?.value || '0'), + lineTotal: tEl ? tEl.textContent.replace(/[^\d,.-]/g, '') : '0', + }; + }); + + const vatRows = Array.from(document.querySelectorAll('.vat-row')).map(row => { + const rEl = row.querySelector('.vat-rate'); + const bEl = row.querySelector('.vat-base'); + const aEl = row.querySelector('.vat-amount'); + return { + type: (row.querySelector('.vat-type')?.value || ''), + rate: rEl ? (rEl.dataset.raw || rEl.value) : '0', + base: bEl ? (bEl.dataset.raw || bEl.value) : '0', + amount: aEl ? (aEl.dataset.raw || aEl.value) : '0', + }; + }); + + // IBAN-uri din rândurile Payment Means dinamice + const ibans = Array.from(document.querySelectorAll('[name^="paymentMeansIBAN"]')) + .map(el => el.value.trim()); + + const parseDisplay = (id) => { + const el = document.getElementById(id); + if (!el) return '0'; + return el.textContent.replace(/[^\d,.-]/g, ''); + }; + + return { + invoiceNumber: (document.querySelector('[name="invoiceNumber"]')?.value || '').trim(), + issueDate: (document.querySelector('[name="issueDate"]')?.value || '').trim(), + dueDate: (document.querySelector('[name="dueDate"]')?.value || '').trim(), + invoiceTypeCode: (document.querySelector('[name="invoiceTypeCode"]')?.value || '').trim(), + currencyCode: (document.querySelector('[name="documentCurrencyCode"]')?.value || '').trim(), + + supplierName: (document.querySelector('[name="supplierName"]')?.value || '').trim(), + supplierVAT: (document.querySelector('[name="supplierVAT"]')?.value || '').trim(), + supplierCity: (document.querySelector('[name="supplierCity"]')?.value || '').trim(), + supplierCountry: (document.querySelector('[name="supplierCountry"]')?.value || '').trim(), + + customerName: (document.querySelector('[name="customerName"]')?.value || '').trim(), + customerVAT: (document.querySelector('[name="customerVAT"]')?.value || '').trim(), + customerCity: (document.querySelector('[name="customerCity"]')?.value || '').trim(), + customerCountry: (document.querySelector('[name="customerCountry"]')?.value || '').trim(), + + lineItems, + vatRows, + ibans, + + subtotal: parseDisplay('subtotal'), + allowances: parseDisplay('totalAllowances'), + charges: parseDisplay('totalCharges'), + totalVat: parseDisplay('vat'), + grandTotal: parseDisplay('total'), + }; +} + +/** Injectează panelul BR în DOM dacă nu există deja. */ +function _ensureBRPanel() { + if (document.getElementById('br-panel')) return; + + const panel = document.createElement('div'); + panel.id = 'br-panel'; + panel.className = 'br-panel br-panel--hidden'; + panel.setAttribute('role', 'region'); + panel.setAttribute('aria-label', 'Probleme de validare'); + panel.setAttribute('aria-live', 'polite'); + + panel.innerHTML = ` + +
    +

    Toate verificările trecute.

    +
    + `; + + document.body.appendChild(panel); + + // Toggle expand/collapse + const header = panel.querySelector('#br-panel-header'); + header.addEventListener('click', () => { + const expanded = panel.classList.toggle('is-expanded'); + header.setAttribute('aria-expanded', expanded ? 'true' : 'false'); + panel.querySelector('#br-panel-toggle').textContent = expanded ? '▼ restrânge' : '▲ extinde'; + }); + header.addEventListener('keydown', (e) => { + if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); header.click(); } + }); +} + +/** + * Actualizează panelul BR cu lista de violări. + * Apelat după fiecare updateTotals() și după parseXML(). + */ +function _updateBRPanel() { + _ensureBRPanel(); + const panel = document.getElementById('br-panel'); + const summary = document.getElementById('br-panel-summary'); + const body = document.getElementById('br-panel-body'); + if (!panel || !summary || !body) return; + + // Rulează regulile + const data = collectInvoiceDataForBR(); + const violations = runBRRules(data); + + const fatals = violations.filter(v => v.severity === 'fatal').length; + const errors = violations.filter(v => v.severity === 'error').length; + const warnings = violations.filter(v => v.severity === 'warning').length; + const totalIssues = fatals + errors + warnings; + + if (totalIssues === 0) { + panel.classList.add('br-panel--hidden'); + panel.classList.remove('br-panel--errors'); + summary.className = 'br-panel__summary br-panel__summary--ok'; + summary.textContent = '✓ 0 erori BR, 0 warnings'; + body.innerHTML = '

    Toate verificările trecute.

    '; + return; + } + + panel.classList.remove('br-panel--hidden'); + panel.classList.toggle('br-panel--errors', (fatals + errors) > 0); + + // Summary text + const parts = []; + if (fatals > 0) parts.push(`${fatals} critice`); + if (errors > 0) parts.push(`${errors} erori`); + if (warnings > 0) parts.push(`${warnings} warning${warnings > 1 ? 's' : ''}`); + summary.textContent = parts.join(' / '); + summary.className = 'br-panel__summary ' + ( + (fatals + errors) > 0 ? 'br-panel__summary--errors' : 'br-panel__summary--warnings' + ); + + // Render items + body.innerHTML = violations.map(v => { + const sev = v.severity === 'fatal' ? 'fatal' : v.severity; + return `
    + ${v.code} + ${_escapeHtml(v.message)} +
    `; + }).join(''); + + // Wire click → scroll + highlight + body.querySelectorAll('.br-panel__item').forEach(item => { + const handler = () => { + const ref = item.dataset.fieldRef; + if (!ref) return; + const target = document.querySelector(ref); + if (!target) return; + target.scrollIntoView({ behavior: 'smooth', block: 'center' }); + target.classList.remove('br-field-highlight'); + void target.offsetWidth; // reflow to restart animation + target.classList.add('br-field-highlight'); + setTimeout(() => target.classList.remove('br-field-highlight'), 2100); + target.focus?.(); + }; + item.addEventListener('click', handler); + item.addEventListener('keydown', (e) => { + if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); handler(); } + }); + }); +} + +function _escapeHtml(str) { + return String(str) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"'); +} + +// ============================================================================ +// PR-ANAF: Integrare API ANAF (validare + CIF lookup) +// Butonul "Validare ANAF" și butoanele "Caută CIF" sunt ascunse la start; +// probeReceiver() le afișează dacă receiver.php este disponibil. +// ============================================================================ + +/** + * Serializează starea curentă a formularului în XML string. + * Atenție: mutează currentInvoice in-place (la fel ca saveXML). + * @returns {string|null} XML string sau null dacă nu există factură. + */ +function _currentXMLString() { + if (!currentInvoice) return null; + const xmlDoc = currentInvoice; + + updateBasicDetails(xmlDoc); + updatePartyDetails(xmlDoc); + updateBillingReference(xmlDoc); + updatePaymentMeans(xmlDoc); + updateAllowanceCharges(xmlDoc); + + xmlDoc.querySelectorAll('cac\\:TaxTotal, TaxTotal').forEach(el => el.remove()); + const mt = xmlDoc.querySelector('cac\\:LegalMonetaryTotal, LegalMonetaryTotal'); + if (mt) mt.remove(); + xmlDoc.querySelectorAll('cac\\:InvoiceLine, InvoiceLine').forEach(el => el.remove()); + + updateTaxTotals(xmlDoc); + updateMonetaryTotals(xmlDoc); + updateLineItems(xmlDoc); + + const serializer = new XMLSerializer(); + let xmlString = serializer.serializeToString(xmlDoc); + if (!xmlString.startsWith('\n' + xmlString; + } + return xmlString; +} + +/** Setează starea loading pe un buton (spinner braille + disabled). */ +function _btnLoading(btn, label) { + btn.disabled = true; + btn.dataset.origText = btn.textContent; + btn.innerHTML = ` ${label}`; +} + +/** Restaurează starea normală a unui buton. */ +function _btnDone(btn) { + btn.disabled = false; + btn.textContent = btn.dataset.origText || btn.textContent; +} + +/** + * Validează factura curentă prin API-ul ANAF. + * Necesită receiver.php cu "anaf_token" configurat în config.json. + */ +window.validateAnaf = async function() { + if (!currentInvoice) { + showToast('Nicio factură încărcată. Deschideți un XML eFactura mai întâi.', 'warning'); + return; + } + const btn = document.getElementById('btnValidateAnaf'); + if (btn) _btnLoading(btn, 'Se validează...'); + try { + const xmlString = _currentXMLString(); + if (!xmlString) throw new Error('Nu s-a putut genera XML-ul facturii.'); + + const result = await anafValidate(xmlString); + if (result.valid) { + showToast('Factură validă ANAF — nicio eroare detectată.', 'success'); + } else { + const errors = result.messages.filter(m => m.severity === 'ERROR' || m.severity === 'FATAL'); + const warns = result.messages.filter(m => m.severity === 'WARNING'); + const parts = []; + if (errors.length) parts.push(`${errors.length} erori`); + if (warns.length) parts.push(`${warns.length} avertismente`); + const sub = result.messages.slice(0, 3).map(m => m.message).join(' | '); + showToast(`ANAF: ${parts.join(', ')}`, 'error', sub.slice(0, 200)); + } + } catch (err) { + showToast('Eroare validare ANAF: ' + err.message, 'error', + 'Verificați că receiver.php este configurat cu anaf_token valid.'); + } finally { + if (btn) _btnDone(btn); + } +}; + +/** + * Caută datele firmei după CIF din câmpul supplierVAT / customerVAT. + * @param {'supplier'|'customer'} party + */ +window.lookupCif = async function(party) { + const isSupplier = party === 'supplier'; + const cifInput = document.querySelector(`[name="${isSupplier ? 'supplier' : 'customer'}VAT"]`); + if (!cifInput) return; + + const cif = (cifInput.value || '').trim(); + if (!cif) { + showToast('Introduceți un CIF/CUI în câmpul Cod TVA mai întâi.', 'warning'); + return; + } + + const btnId = isSupplier ? 'btnLookupSupplierCif' : 'btnLookupCustomerCif'; + const btn = document.getElementById(btnId); + if (btn) _btnLoading(btn, 'Caută...'); + + try { + const result = await anafCifLookup(cif); + if (!result.found) { + const note = result.async + ? 'API ANAF asincron — rezultatele nu sunt disponibile imediat. Reîncercați.' + : 'CIF-ul nu a fost găsit în baza de date ANAF.'; + showToast('CIF negăsit în ANAF.', 'info', note); + return; + } + + // Populează câmpurile firmei + const prefix = isSupplier ? 'supplier' : 'customer'; + const set = (name, val) => { + const el = document.querySelector(`[name="${prefix}${name}"]`); + if (el && val) el.value = val; + }; + + set('Name', result.denumire); + set('CompanyId', result.nrRegCom); + + // CIF cu prefix RO dacă plătitor TVA + if (result.cui) { + const cifFinal = result.tvaActiv ? `RO${result.cui}` : String(result.cui); + const vatEl = document.querySelector(`[name="${prefix}VAT"]`); + if (vatEl) vatEl.value = cifFinal; + } + + // Adresă structurată cu fallback la string brut + set('Address', result.strada || result.adresa); + set('City', result.oras); + // CountrySubentity este SELECT cu valori RO-XX + if (result.judetCod) { + const countyEl = document.querySelector(`[name="${prefix}CountrySubentity"]`); + if (countyEl && [...countyEl.options].some(o => o.value === result.judetCod)) { + countyEl.value = result.judetCod; + } + } + // Country SELECT + const countryEl = document.querySelector(`[name="${prefix}Country"]`); + if (countryEl) countryEl.value = 'RO'; + set('Phone', result.telefon); + + // Toast cu status TVA + eFactura + const statuses = [ + result.tvaActiv ? 'Plătitor TVA' : 'Neplătitor TVA', + result.statusEFactura ? 'Înregistrat eFactura' : null + ].filter(Boolean).join(' · '); + showToast(`Date ANAF importate: ${result.denumire || cif}`, 'success', statuses); + } catch (err) { + showToast('Eroare lookup CIF ANAF: ' + err.message, 'error', + 'Verificați că receiver.php este disponibil pe server.'); + } finally { + if (btn) _btnDone(btn); + } +}; + +// Probează receiver.php la startup — dacă e disponibil, afișează butoanele ANAF. +(async () => { + const available = await probeReceiver().catch(() => false); + if (available) { + document.getElementById('btnValidateAnaf')?.style?.setProperty('display', ''); + document.querySelectorAll('.anaf-cif-btn').forEach(b => b.style.removeProperty('display')); + } +})(); + +// ============================================================================ +// PR-PDF (A8): Descarcă PDF — client-side via html2pdf.js +// Deschide template-ul de print și generează PDF fără dialog de imprimare. +// ============================================================================ +import getHtml2pdf from './vendor/html2pdf.mjs'; + +/** + * Generează și descarcă PDF-ul facturii curente. + * Deschide template-ul de print, îl populează via InvoicePrintHandler, + * apoi html2pdf capturează .invoice-container ca PDF. + */ +window.downloadPDF = async function() { + if (!currentInvoice) { + showToast('Nicio factură încărcată. Deschideți un XML eFactura mai întâi.', 'warning'); + return; + } + const btn = document.getElementById('btnDownloadPDF'); + if (btn) _btnLoading(btn, 'Generare PDF...'); + + try { + // Pasul 1: pre-încarcă html2pdf bundle (lazy, ~900KB) + const html2pdf = await getHtml2pdf(); + + // Pasul 2: colectează datele facturii + const invoiceData = printHandler.collectInvoiceData(); + const invoiceNumber = invoiceData.invoiceNumber || 'factura'; + + // Pasul 3: randează HTML-ul facturii într-un container detașat (off-screen) + const container = document.createElement('div'); + container.style.cssText = 'position:absolute;left:-9999px;top:0;width:210mm;background:#fff;'; + container.id = 'pdf-render-container'; + document.body.appendChild(container); + + // Stiluri inline de bază pentru randarea off-screen + container.innerHTML = ` + +
    +
    +
    +
    FACTURĂ
    +
    Nr. ${_escapeHtml(invoiceData.invoiceNumber)} | Data: ${_escapeHtml(invoiceData.issueDate)} | Scadent: ${_escapeHtml(invoiceData.dueDate)}
    +
    +
    Monedă: ${_escapeHtml(invoiceData.documentCurrencyCode)}
    +
    +
    +
    +
    Furnizor
    +

    ${_escapeHtml(invoiceData.supplier.name)}

    + ${invoiceData.supplier.vat ? `

    CUI: ${_escapeHtml(invoiceData.supplier.vat)}

    ` : ''} + ${invoiceData.supplier.companyId ? `

    Nr. reg.: ${_escapeHtml(invoiceData.supplier.companyId)}

    ` : ''} + ${invoiceData.supplier.address ? `

    ${_escapeHtml(invoiceData.supplier.address)}, ${_escapeHtml(invoiceData.supplier.city)}

    ` : ''} + ${invoiceData.supplier.country ? `

    ${_escapeHtml(invoiceData.supplier.country)}

    ` : ''} +
    +
    +
    Client
    +

    ${_escapeHtml(invoiceData.customer.name)}

    + ${invoiceData.customer.vat ? `

    CUI: ${_escapeHtml(invoiceData.customer.vat)}

    ` : ''} + ${invoiceData.customer.companyId ? `

    Nr. reg.: ${_escapeHtml(invoiceData.customer.companyId)}

    ` : ''} + ${invoiceData.customer.address ? `

    ${_escapeHtml(invoiceData.customer.address)}, ${_escapeHtml(invoiceData.customer.city)}

    ` : ''} + ${invoiceData.customer.country ? `

    ${_escapeHtml(invoiceData.customer.country)}

    ` : ''} +
    +
    + + + + + + + + + + ${invoiceData.items.map(it => ` + + + + + + + + `).join('')} + +
    #DescriereUMCant.Preț unit.TVA%Total net
    ${it.number}${_escapeHtml(it.description)}${_escapeHtml(it.unit)}${_escapeHtml(it.quantity)}${_escapeHtml(it.price)}${_escapeHtml(String(it.vatRate))}%${_escapeHtml(it.totalAmount)}
    + ${invoiceData.note ? `

    Notă: ${_escapeHtml(invoiceData.note)}

    ` : ''} +
    +
    +
    Subtotal:${_escapeHtml(invoiceData.totals.subtotal)}
    + ${invoiceData.totals.allowances ? `
    Reduceri:-${_escapeHtml(invoiceData.totals.allowances)}
    ` : ''} + ${invoiceData.totals.charges ? `
    Taxe:${_escapeHtml(invoiceData.totals.charges)}
    ` : ''} +
    TVA total:${_escapeHtml(invoiceData.totals.vat)}
    +
    TOTAL:${_escapeHtml(invoiceData.totals.total)}
    +
    +
    +
    `; + + await html2pdf().set({ + margin: [0, 0, 0, 0], + filename: 'factura_' + invoiceNumber.replace(/[^a-zA-Z0-9_-]/g, '_') + '.pdf', + image: { type: 'jpeg', quality: 0.97 }, + html2canvas: { scale: 2, useCORS: true, logging: false }, + jsPDF: { unit: 'mm', format: 'a4', orientation: 'portrait' } + }).from(container.querySelector('#pdf-content')).save(); + + showToast('PDF generat și descărcat.', 'success'); + } catch (err) { + showToast('Eroare la generarea PDF: ' + err.message, 'error', + 'Verificați că js/vendor/html2pdf.bundle.min.js este prezent.'); + } finally { + document.getElementById('pdf-render-container')?.remove(); + if (btn) _btnDone(btn); + } +}; + +// ============================================================================ +// PR-A19 (Track 2 D): Numerotare automată cu serie + contor + an +// Cheia localStorage: efactura.sequence.v1 +// Structura: { serie: string, an: number, contor: number } +// Generează numere de forma: RFT2026-0001 +// ============================================================================ + +const SEQ_KEY = 'efactura.sequence.v1'; +const SEQ_DEFAULT = { serie: 'RFT', an: new Date().getFullYear(), contor: 1, includeAn: true, cifre: 4 }; + +/** Citește secvența curentă din localStorage. */ +function _seqRead() { + const stored = getJSON(SEQ_KEY, null); + return { ...SEQ_DEFAULT, ...stored }; +} + +/** Salvează secvența în localStorage. */ +function _seqWrite(seq) { + setJSON(SEQ_KEY, seq); +} + +/** + * Formatează un număr de factură din secvență. + * Cu includeAn=true: "{serie} {an}{pad}" → ex. "RFT 20260042" + * Cu includeAn=false: "{serie} {pad}" → ex. "RFT 0042" + */ +function _seqFormat(seq) { + const cifre = Math.max(1, Math.min(8, parseInt(seq.cifre) || 4)); + const pad = String(seq.contor).padStart(cifre, '0'); + const serie = (seq.serie || 'RFT').trim(); + const an = seq.an || new Date().getFullYear(); + return seq.includeAn + ? `${serie} ${an}${pad}` + : `${serie} ${pad}`; +} + +/** Actualizează previzualizarea din modal cu valorile curente. */ +function _seqUpdatePreview() { + const serie = (document.getElementById('seq-serie')?.value || '').trim().toUpperCase(); + const year = parseInt(document.getElementById('seq-year')?.value) || new Date().getFullYear(); + const contor = parseInt(document.getElementById('seq-counter')?.value) || 1; + const includeAn = document.getElementById('seq-include-an')?.checked ?? true; + const cifre = Math.max(1, Math.min(8, parseInt(document.getElementById('seq-cifre')?.value) || 4)); + const preview = document.getElementById('seq-preview'); + if (preview) { + const pad = String(Math.max(1, contor)).padStart(cifre, '0'); + preview.textContent = includeAn + ? `${serie || 'RFT'} ${year}${pad}` + : `${serie || 'RFT'} ${pad}`; + } +} + +/** Injectează modal-ul în DOM dacă nu există deja. */ +function _ensureNewInvoiceModal() { + if (document.getElementById('modal-new-invoice')) return; + const modal = document.createElement('div'); + modal.id = 'modal-new-invoice'; + modal.className = 'modal-overlay'; + modal.setAttribute('role', 'dialog'); + modal.setAttribute('aria-modal', 'true'); + modal.setAttribute('aria-labelledby', 'modal-new-invoice-title'); + modal.innerHTML = ` +`; + document.body.appendChild(modal); + + // Wire events + document.getElementById('btnCloseNewInvoice').addEventListener('click', window.closeNewInvoiceModal); + document.getElementById('btnGenerateInvoice').addEventListener('click', window.generateNewInvoice); + document.getElementById('seq-serie').addEventListener('input', _seqUpdatePreview); + document.getElementById('seq-counter').addEventListener('input', _seqUpdatePreview); + document.getElementById('seq-include-an').addEventListener('change', _seqUpdatePreview); + document.getElementById('seq-cifre').addEventListener('input', _seqUpdatePreview); + + // Close on backdrop click + modal.addEventListener('click', function(e) { + if (e.target === modal) window.closeNewInvoiceModal(); + }); + + // Esc key + document.addEventListener('keydown', function(e) { + if (e.key === 'Escape' && modal.classList.contains('is-open')) { + window.closeNewInvoiceModal(); + } + }); +} + +/** + * Deschide modal-ul "Factură Nouă" cu valorile curente din secvență. + */ +window.openNewInvoiceModal = function() { + _ensureNewInvoiceModal(); + const seq = _seqRead(); + + const serieEl = document.getElementById('seq-serie'); + const yearEl = document.getElementById('seq-year'); + const counterEl = document.getElementById('seq-counter'); + const includeAnEl = document.getElementById('seq-include-an'); + const cifreEl = document.getElementById('seq-cifre'); + + if (serieEl) serieEl.value = seq.serie || 'RFT'; + if (yearEl) yearEl.value = new Date().getFullYear(); + if (counterEl) counterEl.value = seq.contor || 1; + if (includeAnEl) includeAnEl.checked = seq.includeAn !== false; + if (cifreEl) cifreEl.value = seq.cifre || 4; + + _seqUpdatePreview(); + + const modal = document.getElementById('modal-new-invoice'); + modal.classList.add('is-open'); + serieEl?.focus(); +}; + +/** Închide modal-ul "Factură Nouă". */ +window.closeNewInvoiceModal = function() { + document.getElementById('modal-new-invoice')?.classList.remove('is-open'); +}; + +/** + * Generează o factură nouă cu numărul din secvență, incrementează contorul. + */ +window.generateNewInvoice = function() { + const serieEl = document.getElementById('seq-serie'); + const counterEl = document.getElementById('seq-counter'); + const includeAnEl = document.getElementById('seq-include-an'); + const cifreEl = document.getElementById('seq-cifre'); + + const serie = (serieEl?.value || 'RFT').trim().toUpperCase(); + const contor = Math.max(1, parseInt(counterEl?.value) || 1); + const year = new Date().getFullYear(); + const includeAn = includeAnEl ? includeAnEl.checked : true; + const cifre = Math.max(1, Math.min(8, parseInt(cifreEl?.value) || 4)); + + const seq = { serie, an: year, contor, includeAn, cifre }; + const invoiceNumber = _seqFormat(seq); + + // Salvează secvența cu contorul incrementat + _seqWrite({ ...seq, contor: contor + 1 }); + + // Creează o factură goală și populează formularul + currentInvoice = createEmptyInvoice(); + const serializer = new XMLSerializer(); + const serialized = serializer.serializeToString(currentInvoice).replace(/^<\?xml[^?]*\?>\s*/, ''); + const xmlString = '\n' + serialized; + parseXML(xmlString); + + // Setează numărul de factură generat + const numEl = document.querySelector('[name="invoiceNumber"]'); + if (numEl) numEl.value = invoiceNumber; + + // Setează data emiterii la azi + const today = new Date(); + const pad = n => String(n).padStart(2, '0'); + const dateStr = `${pad(today.getDate())}.${pad(today.getMonth() + 1)}.${today.getFullYear()}`; + const dateEl = document.querySelector('[name="issueDate"]'); + if (dateEl) dateEl.value = dateStr; + + // Populează furnizorul din profil dacă există + const profil = getJSON('efactura.profil.v1', null); + if (profil) { + SUPPLIER_FIELDS.forEach(f => { + const el = document.querySelector(`[name="${f}"]`); + if (el && profil[f] !== undefined) el.value = profil[f]; + }); + } + + window.closeNewInvoiceModal(); + _updateBRPanel(); + showToast(`Factură nouă creată: ${invoiceNumber}`, 'success', 'Contorul a fost incrementat automat.'); +}; + +/** + * Banner an nou (D24) — resetează contorul la 1 pentru noul an. + */ +window.yearRolloverReset = function() { + const seq = _seqRead(); + const newYear = new Date().getFullYear(); + _seqWrite({ ...seq, an: newYear, contor: 1 }); + document.getElementById('year-rollover-banner')?.remove(); + showToast(`Seria ${seq.serie}: contor resetat la 1 pentru ${newYear}.`, 'success'); +}; + +/** + * Banner an nou (D24) — continuă cu contorul existent pentru noul an. + */ +window.yearRolloverContinue = function() { + const seq = _seqRead(); + const newYear = new Date().getFullYear(); + _seqWrite({ ...seq, an: newYear }); + document.getElementById('year-rollover-banner')?.remove(); + showToast(`Seria ${seq.serie}: continuă cu nr. ${seq.contor} pentru ${newYear}.`, 'info'); +}; + +/** Verifică dacă secvența aparține unui an anterior → injectează banner D24. */ +function _checkYearRollover() { + const seq = _seqRead(); + const year = new Date().getFullYear(); + if (!seq.an || seq.an >= year) return; // nicio problemă + + // Evită duplicat + if (document.getElementById('year-rollover-banner')) return; + + const banner = document.createElement('div'); + banner.id = 'year-rollover-banner'; + banner.className = 'year-rollover-banner'; + banner.innerHTML = ` +

    + An nou ${year} detectat. + Seria ${_escapeHtml(seq.serie)} are contorul la ${seq.contor} (an ${seq.an}). + Alegeți cum să continuați numerotarea în ${year}. +

    +`; + + // Injectează sub header (primul copil al .container) + const container = document.querySelector('.container'); + const header = container?.querySelector('.header'); + if (header?.nextSibling) { + container.insertBefore(banner, header.nextSibling); + } else if (container) { + container.appendChild(banner); + } +} + +// Verifică rollover la DOMContentLoaded (după ce restul UI este inițializat) +document.addEventListener('DOMContentLoaded', _checkYearRollover); + +// ============================================================================ +// PR-A13: Catalog produse IndexedDB — autocomplete + save (D15) +// Câmpul "Denumire" din fiecare linie factură primește sugestii din catalog. +// ============================================================================ + +/** ID-ul timeout-ului de debounce pentru autocomplete. */ +let _catalogDebounceTimer = null; + +/** Dropdown curent deschis (referință pentru cleanup). */ +let _activeCatalogDropdown = null; + +/** + * Creează și afișează dropdown-ul de sugestii catalog sub input-ul dat. + * @param {HTMLInputElement} input + * @param {Array} items - Produse din catalog + * @param {number} lineIndex + */ +function _showCatalogDropdown(input, items, lineIndex) { + _hideCatalogDropdown(); + + const wrapper = input.closest('.description-wrapper'); + if (!wrapper) return; + + const dd = document.createElement('div'); + dd.className = 'catalog-dropdown'; + dd.setAttribute('role', 'listbox'); + dd.setAttribute('aria-label', 'Sugestii catalog produse'); + + if (!items.length) { + dd.innerHTML = `
    Niciun produs în catalog pentru "${_escapeHtml(input.value)}"
    `; + } else { + dd.innerHTML = items.map((item, i) => ` +
    +
    ${_escapeHtml(item.name)}
    +
    ${_escapeHtml(item.unit)} · ${_escapeHtml(item.price)} RON · TVA ${_escapeHtml(item.vatType)} ${_escapeHtml(item.vatRate)}%
    +
    `).join(''); + + // Click pe un item → aplică în linia de factură + dd.addEventListener('mousedown', function(e) { + const itemEl = e.target.closest('[data-catalog-id]'); + if (!itemEl) return; + e.preventDefault(); // evită blur pe input + const idx = parseInt(itemEl.dataset.catalogIdx, 10); + const chosen = items[idx]; + if (chosen) _applyCatalogItem(chosen, lineIndex); + _hideCatalogDropdown(); + }); + } + + wrapper.appendChild(dd); + _activeCatalogDropdown = dd; +} + +/** Ascunde dropdown-ul activ. */ +function _hideCatalogDropdown() { + if (_activeCatalogDropdown) { + _activeCatalogDropdown.remove(); + _activeCatalogDropdown = null; + } +} + +/** + * Aplică un produs din catalog în câmpurile liniei de factură. + * @param {Object} item - Produs din catalog + * @param {number} lineIndex + */ +function _applyCatalogItem(item, lineIndex) { + const set = (name, val) => { + const el = document.querySelector(`[name="${name}${lineIndex}"]`); + if (el && val !== undefined) el.value = val; + }; + + set('description', item.name); + set('unit', item.unit); + + const priceEl = document.querySelector(`[name="price${lineIndex}"]`); + if (priceEl && item.price) { + priceEl.value = item.price; + if (priceEl.dataset) priceEl.dataset.raw = item.price; + } + + const vatTypeEl = document.querySelector(`[name="vatType${lineIndex}"]`); + if (vatTypeEl && item.vatType) vatTypeEl.value = item.vatType; + + const vatRateEl = document.querySelector(`[name="vatRate${lineIndex}"]`); + if (vatRateEl && item.vatRate !== undefined) vatRateEl.value = item.vatRate; + + if (item.description) { + const descEl = document.querySelector(`[name="itemDescription${lineIndex}"]`); + if (descEl) descEl.value = item.description; + } + + updateTotals(); +} + +/** + * Inițializează event delegation pe #lineItems pentru autocomplete catalog. + * Apelat din initializeUI(). + */ +function _initCatalogAutocomplete() { + const container = document.getElementById('lineItems'); + if (!container) return; + + container.addEventListener('input', function(e) { + const input = e.target; + if (!input.dataset.catalogInput) return; + const lineIndex = parseInt(input.dataset.catalogInput, 10); + const prefix = input.value.trim(); + + clearTimeout(_catalogDebounceTimer); + + if (!prefix || prefix.length < 2) { + _hideCatalogDropdown(); + return; + } + + _catalogDebounceTimer = setTimeout(async () => { + try { + const items = await catalogSearch(prefix, 8); + _showCatalogDropdown(input, items, lineIndex); + } catch (err) { + // IndexedDB indisponibil (private browsing) — ignorat silențios + } + }, 200); + }); + + container.addEventListener('blur', function(e) { + if (!e.target.dataset?.catalogInput) return; + // Delay pentru a permite click pe dropdown înainte de close + setTimeout(_hideCatalogDropdown, 200); + }, true); + + container.addEventListener('keydown', function(e) { + if (e.key === 'Escape' && _activeCatalogDropdown) { + _hideCatalogDropdown(); + } + }); +} + +/** + * Salvează linia de factură curentă în catalog local (IndexedDB). + * @param {number} lineIndex + */ +window.saveLineToLocalCatalog = async function(lineIndex) { + const nameInput = document.querySelector(`[name="description${lineIndex}"]`); + const name = (nameInput?.value || '').trim(); + + if (!name) { + showToast('Introduceți o denumire înainte de a salva în catalog.', 'warning'); + return; + } + + const priceEl = document.querySelector(`[name="price${lineIndex}"]`); + const vatTypeEl = document.querySelector(`[name="vatType${lineIndex}"]`); + const vatRateEl = document.querySelector(`[name="vatRate${lineIndex}"]`); + const unitEl = document.querySelector(`[name="unit${lineIndex}"]`); + const descEl = document.querySelector(`[name="itemDescription${lineIndex}"]`); + + const product = { + name, + unit: unitEl?.value || 'EA', + price: priceEl?.dataset?.raw || priceEl?.value || '0', + vatType: vatTypeEl?.value || 'S', + vatRate: vatRateEl?.value || '19', + description: descEl?.value || '', + }; + + try { + const id = await catalogAdd(product); + showToast(`"${name}" salvat în catalog.`, 'success', `ID: ${id.slice(0, 8)}…`); + } catch (err) { + const isUnavailable = err?.message?.includes('indexeddb'); + showToast( + isUnavailable + ? 'Catalogul nu este disponibil (private browsing sau browser blocat IndexedDB).' + : 'Eroare la salvare în catalog: ' + err.message, + 'error' + ); + } +}; + +// Inițializare catalog la DOMContentLoaded +document.addEventListener('DOMContentLoaded', _initCatalogAutocomplete); + +// Export for testing if needed +if (typeof module !== 'undefined' && module.exports) { + module.exports = { + calculateTotals, + validateForm, + roundNumber, + formatXML, + createXMLElement, + getXMLValue, + setXMLValue, + formatter, + setupInlineEditing, + updateTotalDisplay, + displayTotals, + updateVATDisplay, + getDisplayValue + }; +} + +import { InvoicePrintHandler } from './print.js'; + +// Create print handler instance +const printHandler = new InvoicePrintHandler(); + +// Initialize when the document is ready +document.addEventListener('DOMContentLoaded', () => { + // Add print controls to the UI + const headerButtonGroup = document.querySelector('.button-group'); + if (headerButtonGroup) { + const printControls = document.createElement('div'); + printControls.className = 'print-controls'; + printControls.style.display = 'flex'; + printControls.style.gap = '8px'; + printControls.style.alignItems = 'center'; + + // Create template selector + const templateSelect = document.createElement('select'); + templateSelect.className = 'form-input'; + templateSelect.style.width = 'auto'; + templateSelect.innerHTML = ` + + + `; + templateSelect.addEventListener('change', (e) => { + printHandler.setTemplate(e.target.value); + }); + + // Create print button + const printButton = document.createElement('button'); + printButton.className = 'button'; + printButton.onclick = () => printHandler.print(); + printButton.innerHTML = 'Printează'; + + // Add elements to controls + printControls.appendChild(templateSelect); + printControls.appendChild(printButton); + + // Add controls to header + headerButtonGroup.appendChild(printControls); + } }); \ No newline at end of file diff --git a/efactura-generator/js/server.js b/efactura-generator/js/server.js index c011acf..05a9cfa 100644 --- a/efactura-generator/js/server.js +++ b/efactura-generator/js/server.js @@ -1,42 +1,43 @@ -// server.js -const http = require('http'); -const fs = require('fs'); -const path = require('path'); - -const PORT = 3000; -const MIME_TYPES = { - '.html': 'text/html', - '.css': 'text/css', - '.js': 'text/javascript', - '.json': 'application/json', - '.xml': 'application/xml' -}; - -const server = http.createServer((req, res) => { - // Convert URL to file path, using index.html for root - let filePath = req.url === '/' ? './index.html' : '.' + req.url; - - // Get file extension for MIME type - const ext = path.extname(filePath); - const contentType = MIME_TYPES[ext] || 'text/plain'; - - // Read and serve the file - fs.readFile(filePath, (err, content) => { - if (err) { - if (err.code === 'ENOENT') { - res.writeHead(404); - res.end('File not found'); - } else { - res.writeHead(500); - res.end('Server error: ' + err.code); - } - } else { - res.writeHead(200, { 'Content-Type': contentType }); - res.end(content); - } - }); -}); - -server.listen(PORT, () => { - console.log(`Server running at http://localhost:${PORT}/`); +// server.js +const http = require('http'); +const fs = require('fs'); +const path = require('path'); + +const PORT = 3000; +const MIME_TYPES = { + '.html': 'text/html', + '.css': 'text/css', + '.js': 'text/javascript', + '.mjs': 'text/javascript', + '.json': 'application/json', + '.xml': 'application/xml' +}; + +const server = http.createServer((req, res) => { + // Convert URL to file path, using index.html for root + let filePath = req.url === '/' ? './index.html' : '.' + req.url; + + // Get file extension for MIME type + const ext = path.extname(filePath); + const contentType = MIME_TYPES[ext] || 'text/plain'; + + // Read and serve the file + fs.readFile(filePath, (err, content) => { + if (err) { + if (err.code === 'ENOENT') { + res.writeHead(404); + res.end('File not found'); + } else { + res.writeHead(500); + res.end('Server error: ' + err.code); + } + } else { + res.writeHead(200, { 'Content-Type': contentType }); + res.end(content); + } + }); +}); + +server.listen(PORT, () => { + console.log(`Server running at http://localhost:${PORT}/`); }); \ No newline at end of file diff --git a/efactura-generator/js/storage.js b/efactura-generator/js/storage.js new file mode 100644 index 0000000..4fa6716 --- /dev/null +++ b/efactura-generator/js/storage.js @@ -0,0 +1,185 @@ +// js/storage.js +// +// Helpers de stocare pentru efactura-generator (PR-PROFIL / A12+A13). +// +// Reguli: +// 1. Toate cheile localStorage/sessionStorage încep cu "efactura." — +// enforced la setter; getJSON acceptă orice cheie pentru compatibilitate +// retroactivă, dar setJSON/cacheSet aruncă dacă prefixul lipsește. +// 2. Quota errors localStorage → toast vizibil "spațiu local plin". +// 3. Cheile convenționale: efactura.{tip}.v1 +// Ex: efactura.profil.v1, efactura.catalog.v1, efactura.session.v1 +// +// Exports: +// getJSON(key, default) → valoare parsată sau default +// setJSON(key, value) → salvează; toast error dacă QuotaExceeded +// cacheGet(key) → sessionStorage (ephemer, null dacă absent) +// cacheSet(key, value) → sessionStorage (silențios dacă eșuează) +// openCatalog() → Promise pentru catalog produse (A13) + +const KEY_PREFIX = 'efactura.'; + +/** + * Validează că cheia respectă prefixul obligatoriu. + * @param {string} key + */ +function _enforcePrefix(key) { + if (typeof key !== 'string' || !key.startsWith(KEY_PREFIX)) { + throw new Error( + `storage.js: cheia "${key}" trebuie să înceapă cu "${KEY_PREFIX}". ` + + `Convenție: efactura.{tip}.v1` + ); + } +} + +/** + * Afișează un toast (dacă window.showToast e disponibil) sau loghează. + * @param {string} msg + * @param {string} variant 'error'|'warning'|'info'|'success' + */ +function _toast(msg, variant = 'error') { + if (typeof window !== 'undefined' && typeof window.showToast === 'function') { + window.showToast(msg, variant); + } else { + console.warn('[storage]', msg); + } +} + +/** + * Citește o valoare JSON din localStorage. + * Returnează `defaultValue` dacă cheia lipsește sau JSON e invalid. + * + * @param {string} key + * @param {*} defaultValue + * @returns {*} + */ +export function getJSON(key, defaultValue = null) { + _enforcePrefix(key); + try { + const raw = localStorage.getItem(key); + if (raw === null) return defaultValue; + return JSON.parse(raw); + } catch (_) { + return defaultValue; + } +} + +/** + * Scrie o valoare JSON în localStorage. + * La QuotaExceededError → toast "spațiu local plin". + * + * @param {string} key + * @param {*} value + */ +export function setJSON(key, value) { + _enforcePrefix(key); + try { + localStorage.setItem(key, JSON.stringify(value)); + } catch (err) { + // QuotaExceededError: code 22 (Firefox/Chrome), 1014 (Firefox NS), sau name check. + const isQuota = err && ( + err.name === 'QuotaExceededError' || + err.name === 'NS_ERROR_DOM_QUOTA_REACHED' || + err.code === 22 || + err.code === 1014 + ); + if (isQuota) { + _toast( + 'Spațiu local plin — datele nu au putut fi salvate.', + 'error' + ); + } else { + _toast(`Eroare la salvare locală: ${err && err.message ? err.message : err}`, 'error'); + } + } +} + +/** + * Citește din sessionStorage (cache ephemer, valabil doar pe durata sesiunii). + * Returnează null dacă absent sau invalid. + * + * @param {string} key + * @returns {*|null} + */ +export function cacheGet(key) { + _enforcePrefix(key); + try { + const raw = sessionStorage.getItem(key); + if (raw === null) return null; + return JSON.parse(raw); + } catch (_) { + return null; + } +} + +/** + * Scrie în sessionStorage. Erorile sunt ignorate silențios (storage e + * ephemer și poate fi blocat de browser în incognito / iframe sandboxed). + * + * @param {string} key + * @param {*} value + */ +export function cacheSet(key, value) { + _enforcePrefix(key); + try { + sessionStorage.setItem(key, JSON.stringify(value)); + } catch (_) { + // Ignorat: sessionStorage e ephemer, erorile nu sunt critice. + } +} + +// IndexedDB pentru catalog produse (A13 lazy init). +let _catalogDb = null; + +/** + * Deschide (sau returnează instanța cached a) bazei de date IndexedDB + * `efactura` v1. Crează object store `products` la prima rulare. + * + * Schema v1 (lock per eng review 14A): + * - DB name: `efactura` + * - store: `products`, keyPath: `id` (uuid v4 generat de caller) + * - indexes: `name`, `sellerItemID`, `cpvCode` + * + * Dacă IndexedDB lipsește (private browsing), Promise rejectează cu Error + * `indexeddb-unavailable` — caller-ul trebuie să degradeze la "feature + * disabled" cu toast (NU să crash-eze). + * + * @returns {Promise} + */ +export function openCatalog() { + if (_catalogDb) return Promise.resolve(_catalogDb); + + return new Promise((resolve, reject) => { + if (typeof indexedDB === 'undefined') { + reject(new Error('indexeddb-unavailable')); + return; + } + const req = indexedDB.open('efactura', 1); + + req.onupgradeneeded = (event) => { + const db = event.target.result; + if (!db.objectStoreNames.contains('products')) { + const store = db.createObjectStore('products', { keyPath: 'id' }); + store.createIndex('name', 'name', { unique: false }); + store.createIndex('sellerItemID', 'sellerItemID', { unique: false }); + store.createIndex('cpvCode', 'cpvCode', { unique: false }); + } + }; + + req.onsuccess = (event) => { + _catalogDb = event.target.result; + resolve(_catalogDb); + }; + + req.onerror = (event) => { + reject(event.target.error); + }; + + req.onblocked = () => { + reject(new Error('indexeddb-blocked')); + }; + }); +} + +// Export prefix pentru tests / consumeri care vor să verifice convenția. +export const STORAGE_PREFIX = KEY_PREFIX; diff --git a/efactura-generator/js/validation/br-ro.js b/efactura-generator/js/validation/br-ro.js new file mode 100644 index 0000000..d3224ea --- /dev/null +++ b/efactura-generator/js/validation/br-ro.js @@ -0,0 +1,481 @@ +/** + * js/validation/br-ro.js — PR-BR (A2) + * Top 30 reguli BR din CIUS-RO Schematron + EN 16931-1. + * Selecție: severity fatal/error din Schematron + reguli care vizează + * câmpuri editabile (CIF, date, totale, coduri TVA, articole factură). + * + * Fiecare regulă: + * { code, severity ('fatal'|'error'|'warning'), message, fieldRef, check(invoiceData) } + * + * invoiceData = obiect snapshot din colectInvoiceDataForBR() în script.js. + * Toate funcțiile sunt pure — fără acces DOM, fără efecte secundare. + */ + +import { validateCIF } from './cif.js'; +import { validateIBAN } from './iban.js'; + +// Coduri TVA valide per CIUS-RO +const VALID_VAT_TYPES = ['S', 'AE', 'O', 'Z', 'E']; +// Coduri tip factură valide per CIUS-RO +const VALID_INVOICE_TYPES = ['380', '381', '384', '389']; +// Coduri țară ISO 3166-1 alfa-2 (set parțial — UE + țări comune) +const EU_COUNTRY_CODES = new Set([ + 'AT','BE','BG','CY','CZ','DE','DK','EE','ES','FI','FR','GR','HR','HU', + 'IE','IT','LT','LU','LV','MT','NL','PL','PT','RO','SE','SI','SK', + 'AD','AL','BA','BY','CH','GB','GE','IS','LI','ME','MK','MD','MN','NO', + 'RS','TR','UA','US','CA','AU','JP','CN','KR','BR','IN','ZA','SG','AE', + 'XK','SM','VA','MC','GI','FO','GL','IM','JE','GG' +]); + +/** + * Parsează un număr din string ignorând formatare (punct sau virgulă drept separator mii). + * Returnează NaN dacă nu e un număr valid. + */ +function parseNum(val) { + if (val === null || val === undefined || val === '') return NaN; + const s = String(val).trim().replace(/\s/g, ''); + // Format ro-RO are virgulă ca separator zecimal ("1.234,56" sau "1,5"). + // Doar când există virgulă tratăm punctele drept separator de mii. + // Altfel: parse canonical decimal-dot (dataset.raw, XML) — "1.000" = 1, NU 1000. + if (s.includes(',')) { + return parseFloat(s.replace(/\./g, '').replace(',', '.')); + } + return parseFloat(s); +} + +/** Parsează o dată din format dd.mm.yyyy → Date object (sau null) */ +function parseRoDate(str) { + if (!str || !/^\d{2}\.\d{2}\.\d{4}$/.test(str.trim())) return null; + const [d, m, y] = str.trim().split('.').map(Number); + const dt = new Date(y, m - 1, d); + if (isNaN(dt.getTime())) return null; + return dt; +} + +/** Compară două valori numerice cu toleranță ε */ +function approxEqual(a, b, eps = 0.02) { + if (isNaN(a) || isNaN(b)) return false; + return Math.abs(a - b) <= eps; +} + +// ============================================================================ +// REGULILE BR — 30 reguli în ordinea: ID, date, furnizor, client, +// articole, TVA, totaluri, CIUS-RO specifice. +// ============================================================================ + +export const BR_RULES = [ + + // ── Identificare factură ───────────────────────────────────────────────── + + { + code: 'BR-01', + severity: 'fatal', + message: 'Factura trebuie să aibă un număr de identificare (ID).', + fieldRef: '[name="invoiceNumber"]', + check: (d) => d.invoiceNumber !== '', + }, + + { + code: 'BR-02', + severity: 'fatal', + message: 'Factura trebuie să aibă o dată de emitere.', + fieldRef: '[name="issueDate"]', + check: (d) => d.issueDate !== '' && parseRoDate(d.issueDate) !== null, + }, + + { + code: 'BR-03', + severity: 'error', + message: 'Codul tipului de factură trebuie să fie 380, 381, 384 sau 389.', + fieldRef: '[name="invoiceTypeCode"]', + check: (d) => VALID_INVOICE_TYPES.includes(d.invoiceTypeCode), + }, + + { + code: 'BR-04', + severity: 'fatal', + message: 'Factura trebuie să specifice moneda (codul ISO 4217).', + fieldRef: '[name="documentCurrencyCode"]', + check: (d) => d.currencyCode !== '' && d.currencyCode.length === 3, + }, + + // ── Date scadență ──────────────────────────────────────────────────────── + + { + code: 'BR-DT-01', + severity: 'error', + message: 'Data emiterii nu poate fi în viitor cu mai mult de 30 zile.', + fieldRef: '[name="issueDate"]', + check: (d) => { + const issued = parseRoDate(d.issueDate); + if (!issued) return true; // BR-02 handles missing date + const limit = new Date(); + limit.setDate(limit.getDate() + 30); + return issued <= limit; + }, + }, + + { + code: 'BR-DT-02', + severity: 'warning', + message: 'Data scadenței (dueDate) nu trebuie să fie anterioară datei de emitere.', + fieldRef: '[name="dueDate"]', + check: (d) => { + if (!d.dueDate) return true; + const issued = parseRoDate(d.issueDate); + const due = parseRoDate(d.dueDate); + if (!issued || !due) return true; + return due >= issued; + }, + }, + + // ── Furnizor ───────────────────────────────────────────────────────────── + + { + code: 'BR-06', + severity: 'fatal', + message: 'Furnizorul trebuie să aibă un nume (RegistrationName).', + fieldRef: '[name="supplierName"]', + check: (d) => d.supplierName !== '', + }, + + { + code: 'BR-07', + severity: 'fatal', + message: 'Adresa furnizorului trebuie să includă orașul.', + fieldRef: '[name="supplierCity"]', + check: (d) => d.supplierCity !== '', + }, + + { + code: 'BR-08', + severity: 'fatal', + message: 'Țara furnizorului trebuie specificată (cod ISO 3166-1).', + fieldRef: '[name="supplierCountry"]', + check: (d) => d.supplierCountry !== '', + }, + + { + code: 'BR-RO-001', + severity: 'error', + message: 'CIF/CUI furnizor invalid: cifra de control nu se potrivește.', + fieldRef: '[name="supplierVAT"]', + check: (d) => { + if (!d.supplierVAT) return true; // gol = alt BR verifică + return validateCIF(d.supplierVAT).valid; + }, + }, + + { + code: 'BR-RO-010', + severity: 'fatal', + message: 'Furnizorul trebuie să aibă un cod de identificare fiscală (CIF/VAT).', + fieldRef: '[name="supplierVAT"]', + check: (d) => d.supplierVAT !== '', + }, + + // ── Client ─────────────────────────────────────────────────────────────── + + { + code: 'BR-07-C', + severity: 'fatal', + message: 'Clientul trebuie să aibă un nume (RegistrationName).', + fieldRef: '[name="customerName"]', + check: (d) => d.customerName !== '', + }, + + { + code: 'BR-08-C', + severity: 'fatal', + message: 'Țara clientului trebuie specificată (cod ISO 3166-1).', + fieldRef: '[name="customerCountry"]', + check: (d) => d.customerCountry !== '', + }, + + { + code: 'BR-RO-002', + severity: 'error', + message: 'CIF/CUI client invalid: cifra de control nu se potrivește.', + fieldRef: '[name="customerVAT"]', + check: (d) => { + if (!d.customerVAT) return true; + return validateCIF(d.customerVAT).valid; + }, + }, + + // ── Articole factură ───────────────────────────────────────────────────── + + { + code: 'BR-21', + severity: 'fatal', + message: (d) => { + const bad = d.lineItems.filter(li => !li.description); + return bad.length === 1 + ? `Linia ${bad[0].index + 1} trebuie să aibă o denumire (descriere).` + : `${bad.length} linii fără denumire (liniile ${bad.map(l => l.index + 1).join(', ')}).`; + }, + fieldRef: null, // dinamic — scroll la prima linie cu eroare + fieldRefDynamic: (d) => { + const bad = d.lineItems.find(li => !li.description); + return bad ? `[name="description${bad.index}"]` : null; + }, + check: (d) => d.lineItems.every(li => li.description !== ''), + }, + + { + code: 'BR-22', + severity: 'fatal', + message: (d) => { + const bad = d.lineItems.filter(li => isNaN(parseNum(li.quantity)) || parseNum(li.quantity) === 0); + return bad.length === 1 + ? `Linia ${bad[0].index + 1} trebuie să aibă o cantitate validă (≠ 0).` + : `${bad.length} linii cu cantitate lipsă sau zero.`; + }, + fieldRefDynamic: (d) => { + const bad = d.lineItems.find(li => isNaN(parseNum(li.quantity)) || parseNum(li.quantity) === 0); + return bad ? `[name="quantity${bad.index}"]` : null; + }, + check: (d) => d.lineItems.every(li => !isNaN(parseNum(li.quantity)) && parseNum(li.quantity) !== 0), + }, + + { + code: 'BR-23', + severity: 'fatal', + message: (d) => { + const bad = d.lineItems.filter(li => isNaN(parseNum(li.unitPrice))); + return `Linia ${bad[0]?.index + 1 || '?'}: prețul unitar trebuie specificat.`; + }, + fieldRefDynamic: (d) => { + const bad = d.lineItems.find(li => isNaN(parseNum(li.unitPrice))); + return bad ? `[name="price${bad.index}"]` : null; + }, + check: (d) => d.lineItems.every(li => !isNaN(parseNum(li.unitPrice))), + }, + + { + code: 'BR-24', + severity: 'fatal', + message: (d) => { + const bad = d.lineItems.filter(li => !VALID_VAT_TYPES.includes(li.vatType)); + return `Linia ${bad[0]?.index + 1 || '?'}: codul categoriei TVA trebuie să fie S/AE/O/Z/E.`; + }, + fieldRefDynamic: (d) => { + const bad = d.lineItems.find(li => !VALID_VAT_TYPES.includes(li.vatType)); + return bad ? `[name="vatType${bad.index}"]` : null; + }, + check: (d) => d.lineItems.every(li => VALID_VAT_TYPES.includes(li.vatType)), + }, + + { + code: 'BR-16', + severity: 'error', + message: (d) => { + const bad = d.lineItems.find(li => { + const qty = parseNum(li.quantity); + const price = parseNum(li.unitPrice); + const disc = parseNum(li.discount) || 0; + const net = parseNum(li.lineTotal); + if (isNaN(qty) || isNaN(price) || isNaN(net)) return false; + return !approxEqual(qty * price - disc, net); + }); + return bad + ? `Linia ${bad.index + 1}: total net ≠ cantitate × preț − discount.` + : 'Total net linie inconsistent.'; + }, + fieldRefDynamic: (d) => { + const bad = d.lineItems.find(li => { + const qty = parseNum(li.quantity); + const price = parseNum(li.unitPrice); + const disc = parseNum(li.discount) || 0; + const net = parseNum(li.lineTotal); + if (isNaN(qty) || isNaN(price) || isNaN(net)) return false; + return !approxEqual(qty * price - disc, net); + }); + return bad ? `[data-line-total-index="${bad.index}"]` : null; + }, + check: (d) => d.lineItems.every(li => { + const qty = parseNum(li.quantity); + const price = parseNum(li.unitPrice); + const disc = parseNum(li.discount) || 0; + const net = parseNum(li.lineTotal); + if (isNaN(qty) || isNaN(price) || isNaN(net)) return true; // BR-22/23 handles + return approxEqual(qty * price - disc, net); + }), + }, + + // ── Factură cu cel puțin un articol ──────────────────────────────────── + + { + code: 'BR-16-L', + severity: 'fatal', + message: 'Factura trebuie să conțină cel puțin un articol (linie factură).', + fieldRef: null, + check: (d) => d.lineItems.length > 0, + }, + + // ── TVA breakdown ──────────────────────────────────────────────────────── + + { + code: 'BR-31', + severity: 'fatal', + message: 'Defalcarea TVA (TaxTotal/TaxSubtotal) nu poate fi goală.', + fieldRef: '#vatBreakdownRows', + check: (d) => d.vatRows.length > 0, + }, + + { + code: 'BR-32', + severity: 'error', + message: (d) => { + const bad = d.vatRows.find(r => { + const rt = parseNum(r.rate); + return isNaN(rt) || rt < 0 || rt > 100; + }); + return `Cota TVA ${bad?.rate ?? ''} este invalidă (trebuie 0–100%).`; + }, + fieldRef: '.vat-rate', + check: (d) => d.vatRows.every(r => { + const rt = parseNum(r.rate); + return !isNaN(rt) && rt >= 0 && rt <= 100; + }), + }, + + { + code: 'BR-45', + severity: 'error', + message: (d) => { + const bad = d.vatRows.find(r => !VALID_VAT_TYPES.includes(r.type)); + return `Codul categoriei TVA "${bad?.type ?? ''}" este invalid. Valori acceptate: S, AE, O, Z, E.`; + }, + fieldRef: '.vat-type', + check: (d) => d.vatRows.every(r => VALID_VAT_TYPES.includes(r.type)), + }, + + { + code: 'BR-AE-01', + severity: 'warning', + message: 'Categoria AE (Taxare Inversă) trebuie să aibă cota TVA 0%.', + fieldRef: '.vat-rate', + check: (d) => d.vatRows + .filter(r => r.type === 'AE') + .every(r => parseNum(r.rate) === 0), + }, + + { + code: 'BR-O-01', + severity: 'warning', + message: 'Categoria O (Neplătitor TVA) trebuie să aibă cota TVA 0%.', + fieldRef: '.vat-rate', + check: (d) => d.vatRows + .filter(r => r.type === 'O') + .every(r => parseNum(r.rate) === 0), + }, + + { + code: 'BR-E-01', + severity: 'warning', + message: 'Categoria E (Neimpozabil) trebuie să aibă cota TVA 0%.', + fieldRef: '.vat-rate', + check: (d) => d.vatRows + .filter(r => r.type === 'E') + .every(r => parseNum(r.rate) === 0), + }, + + // ── Consistență totaluri ───────────────────────────────────────────────── + + { + code: 'BR-CO-15', + severity: 'fatal', + message: (d) => { + const sumRows = d.vatRows.reduce((s, r) => s + (parseNum(r.amount) || 0), 0); + const disp = parseNum(d.totalVat); + const diff = Math.abs(sumRows - disp).toFixed(2); + return `Total TVA afișat (${disp.toFixed(2)}) ≠ suma rândurilor TVA (${sumRows.toFixed(2)}). Diferență: ${diff} RON.`; + }, + fieldRef: '#vat', + check: (d) => { + if (d.vatRows.length === 0) return true; + const sumRows = d.vatRows.reduce((s, r) => s + (parseNum(r.amount) || 0), 0); + const disp = parseNum(d.totalVat); + return approxEqual(sumRows, disp); + }, + }, + + { + code: 'BR-CO-16', + severity: 'fatal', + message: (d) => { + const expected = parseNum(d.subtotal) - parseNum(d.allowances) + parseNum(d.charges) + parseNum(d.totalVat); + const actual = parseNum(d.grandTotal); + const diff = Math.abs(expected - actual).toFixed(2); + return `Total factură (${actual.toFixed(2)}) ≠ subtotal − reduceri + adaosuri + TVA (${expected.toFixed(2)}). Diferență: ${diff} RON.`; + }, + fieldRef: '#total', + check: (d) => { + const expected = parseNum(d.subtotal) - parseNum(d.allowances) + parseNum(d.charges) + parseNum(d.totalVat); + const actual = parseNum(d.grandTotal); + if (isNaN(expected) || isNaN(actual)) return true; + return approxEqual(expected, actual); + }, + }, + + // ── CIUS-RO specifice ──────────────────────────────────────────────────── + + { + code: 'BR-RO-180', + severity: 'error', + message: 'CIUS-RO: codul tipului de factură trebuie să fie 380 (factură), 381 (credit note), 384 (corectată) sau 389 (autofactură).', + fieldRef: '[name="invoiceTypeCode"]', + check: (d) => d.invoiceTypeCode === '' || VALID_INVOICE_TYPES.includes(d.invoiceTypeCode), + }, + + { + code: 'BR-RO-003', + severity: 'warning', + message: 'CIUS-RO: numărul facturii (ID) nu trebuie să fie gol sau să conțină doar spații.', + fieldRef: '[name="invoiceNumber"]', + check: (d) => d.invoiceNumber.trim() !== '', + }, + + { + code: 'BR-IBAN-01', + severity: 'warning', + message: (d) => { + const badIdx = d.ibans.findIndex(ib => ib && !validateIBAN(ib).valid); + return `IBAN #${badIdx + 1} invalid: verificați lungimea și cifrele de control.`; + }, + fieldRef: null, + fieldRefDynamic: (d) => { + const badIdx = d.ibans.findIndex(ib => ib && !validateIBAN(ib).valid); + return badIdx >= 0 ? `[name="paymentMeansIBAN${badIdx}"]` : null; + }, + check: (d) => d.ibans.every(ib => !ib || validateIBAN(ib).valid), + }, + +]; + +/** + * Rulează toate regulile pe invoiceData și returnează lista de violări. + * @param {object} invoiceData — snapshot din collectInvoiceDataForBR() + * @returns {{ code, severity, message, fieldRef }[]} + */ +export function runBRRules(invoiceData) { + const violations = []; + for (const rule of BR_RULES) { + if (!rule.check(invoiceData)) { + const msg = typeof rule.message === 'function' + ? rule.message(invoiceData) + : rule.message; + const fRef = rule.fieldRefDynamic + ? rule.fieldRefDynamic(invoiceData) + : rule.fieldRef; + violations.push({ + code: rule.code, + severity: rule.severity, + message: msg, + fieldRef: fRef, + }); + } + } + return violations; +} diff --git a/efactura-generator/js/validation/cif.js b/efactura-generator/js/validation/cif.js new file mode 100644 index 0000000..9dcfee1 --- /dev/null +++ b/efactura-generator/js/validation/cif.js @@ -0,0 +1,71 @@ +/** + * js/validation/cif.js — PR-VALID-IDS (A9) + * Validare CIF/CUI românesc prin sumă ponderată (mod 11, mod 10). + * Funcție pură, fără efecte secundare, fără dependențe externe. + */ + +// Greutățile pentru cifrele 1-9 (se aplică pe primele 9 cifre ale CIF-ului). +const WEIGHTS = [7, 5, 3, 2, 1, 7, 5, 3, 2]; + +/** + * Validează un CIF/CUI românesc. + * + * Algoritm: + * 1. Elimină prefixul "RO" dacă există (case-insensitive). + * 2. Elimină spații. + * 3. Verifică că are între 2 și 10 cifre. + * 4. Completează cu zerouri la stânga până la 10 cifre. + * 5. Calculează suma ponderată pe primele 9 cifre cu WEIGHTS. + * 6. (sumă * 10) % 11 % 10 trebuie să fie egal cu cifra de control (ultima). + * + * @param {string} value — valoarea brută din câmp (poate fi goală, poate conține "RO") + * @returns {{ valid: boolean, message: string }} + */ +export function validateCIF(value) { + if (!value || value.trim() === '') { + return { valid: true, message: '' }; // câmp gol — valid (nu e required check) + } + + let normalized = value.trim().toUpperCase(); + + // Elimină prefixul RO + if (normalized.startsWith('RO')) { + normalized = normalized.slice(2).trim(); + } + + // Elimină spații și cratime rămase + normalized = normalized.replace(/[\s\-]/g, ''); + + // Trebuie să conțină doar cifre + if (!/^\d+$/.test(normalized)) { + return { valid: false, message: 'CIF invalid: conține caractere nepermise' }; + } + + // Lungime: minim 2, maxim 10 cifre + if (normalized.length < 2 || normalized.length > 10) { + return { valid: false, message: 'CIF invalid: lungimea trebuie să fie între 2 și 10 cifre' }; + } + + // Completează cu zerouri la stânga până la 10 cifre + const padded = normalized.padStart(10, '0'); + + // Extrage primele 9 cifre (pentru ponderare) și cifra de control (ultima) + const digits = padded.split('').map(Number); + const checkDigit = digits[9]; + const controlDigits = digits.slice(0, 9); + + // Calculează suma ponderată + let sum = 0; + for (let i = 0; i < 9; i++) { + sum += controlDigits[i] * WEIGHTS[i]; + } + + // Cifra de control calculată + const computed = (sum * 10) % 11 % 10; + + if (computed !== checkDigit) { + return { valid: false, message: 'CIF invalid: cifra de control nu se potrivește' }; + } + + return { valid: true, message: '' }; +} diff --git a/efactura-generator/js/validation/iban.js b/efactura-generator/js/validation/iban.js new file mode 100644 index 0000000..664b807 --- /dev/null +++ b/efactura-generator/js/validation/iban.js @@ -0,0 +1,63 @@ +/** + * js/validation/iban.js — PR-VALID-IDS (A10) + * Validare IBAN internațional prin algoritmul ISO 13616 (mod 97). + * Funcție pură, fără efecte secundare, fără dependențe externe. + */ + +/** + * Validează un IBAN (orice țară, inclusiv RO). + * + * Algoritm ISO 13616: + * 1. Elimină spații și convertește la uppercase. + * 2. Verifică lungimea minimă (4 caractere). + * 3. Mută primele 4 caractere la sfârșitul șirului. + * 4. Înlocuiește fiecare literă cu echivalentul numeric: A=10, B=11, ..., Z=35. + * 5. Calculează numărul rezultat modulo 97 — trebuie să fie 1. + * + * Lungimi specifice per țară nu sunt forțate (validare structurală generică); + * IBAN-ul RO are 24 caractere, verificat separat cu mesaj specific. + * + * @param {string} value — valoarea brută din câmp + * @returns {{ valid: boolean, message: string }} + */ +export function validateIBAN(value) { + if (!value || value.trim() === '') { + return { valid: true, message: '' }; // câmp gol — valid (nu e required check) + } + + // Normalizare: elimină spații, uppercase + const normalized = value.trim().toUpperCase().replace(/\s/g, ''); + + // Lungime minimă + if (normalized.length < 4) { + return { valid: false, message: 'IBAN invalid: lungime sau check digits' }; + } + + // Verifică că IBAN-ul conține doar litere și cifre + if (!/^[A-Z0-9]+$/.test(normalized)) { + return { valid: false, message: 'IBAN invalid: caractere nepermise' }; + } + + // IBAN RO trebuie să aibă exact 24 caractere + if (normalized.startsWith('RO') && normalized.length !== 24) { + return { valid: false, message: 'IBAN invalid: lungime sau check digits' }; + } + + // Rearanjare: primele 4 caractere la final + const rearranged = normalized.slice(4) + normalized.slice(0, 4); + + // Înlocuiește literele cu cifre: A=10 ... Z=35 + const numericString = rearranged.replace(/[A-Z]/g, ch => String(ch.charCodeAt(0) - 55)); + + // Calculează mod 97 pe un număr mare (string chunking pentru a evita overflow) + let remainder = 0; + for (let i = 0; i < numericString.length; i++) { + remainder = (remainder * 10 + parseInt(numericString[i], 10)) % 97; + } + + if (remainder !== 1) { + return { valid: false, message: 'IBAN invalid: lungime sau check digits' }; + } + + return { valid: true, message: '' }; +} diff --git a/efactura-generator/js/vendor/big.mjs b/efactura-generator/js/vendor/big.mjs new file mode 100644 index 0000000..079b743 --- /dev/null +++ b/efactura-generator/js/vendor/big.mjs @@ -0,0 +1,1027 @@ +/* + * big.js v6.2.1 + * A small, fast, easy-to-use library for arbitrary-precision decimal arithmetic. + * Copyright (c) 2022 Michael Mclaughlin + * https://github.com/MikeMcl/big.js/LICENCE.md + */ + + +/************************************** EDITABLE DEFAULTS *****************************************/ + + + // The default values below must be integers within the stated ranges. + + /* + * The maximum number of decimal places (DP) of the results of operations involving division: + * div and sqrt, and pow with negative exponents. + */ +var DP = 20, // 0 to MAX_DP + + /* + * The rounding mode (RM) used when rounding to the above decimal places. + * + * 0 Towards zero (i.e. truncate, no rounding). (ROUND_DOWN) + * 1 To nearest neighbour. If equidistant, round up. (ROUND_HALF_UP) + * 2 To nearest neighbour. If equidistant, to even. (ROUND_HALF_EVEN) + * 3 Away from zero. (ROUND_UP) + */ + RM = 1, // 0, 1, 2 or 3 + + // The maximum value of DP and Big.DP. + MAX_DP = 1E6, // 0 to 1000000 + + // The maximum magnitude of the exponent argument to the pow method. + MAX_POWER = 1E6, // 1 to 1000000 + + /* + * The negative exponent (NE) at and beneath which toString returns exponential notation. + * (JavaScript numbers: -7) + * -1000000 is the minimum recommended exponent value of a Big. + */ + NE = -7, // 0 to -1000000 + + /* + * The positive exponent (PE) at and above which toString returns exponential notation. + * (JavaScript numbers: 21) + * 1000000 is the maximum recommended exponent value of a Big, but this limit is not enforced. + */ + PE = 21, // 0 to 1000000 + + /* + * When true, an error will be thrown if a primitive number is passed to the Big constructor, + * or if valueOf is called, or if toNumber is called on a Big which cannot be converted to a + * primitive number without a loss of precision. + */ + STRICT = false, // true or false + + +/**************************************************************************************************/ + + + // Error messages. + NAME = '[big.js] ', + INVALID = NAME + 'Invalid ', + INVALID_DP = INVALID + 'decimal places', + INVALID_RM = INVALID + 'rounding mode', + DIV_BY_ZERO = NAME + 'Division by zero', + + // The shared prototype object. + P = {}, + UNDEFINED = void 0, + NUMERIC = /^-?(\d+(\.\d*)?|\.\d+)(e[+-]?\d+)?$/i; + + +/* + * Create and return a Big constructor. + */ +function _Big_() { + + /* + * The Big constructor and exported function. + * Create and return a new instance of a Big number object. + * + * n {number|string|Big} A numeric value. + */ + function Big(n) { + var x = this; + + // Enable constructor usage without new. + if (!(x instanceof Big)) return n === UNDEFINED ? _Big_() : new Big(n); + + // Duplicate. + if (n instanceof Big) { + x.s = n.s; + x.e = n.e; + x.c = n.c.slice(); + } else { + if (typeof n !== 'string') { + if (Big.strict === true && typeof n !== 'bigint') { + throw TypeError(INVALID + 'value'); + } + + // Minus zero? + n = n === 0 && 1 / n < 0 ? '-0' : String(n); + } + + parse(x, n); + } + + // Retain a reference to this Big constructor. + // Shadow Big.prototype.constructor which points to Object. + x.constructor = Big; + } + + Big.prototype = P; + Big.DP = DP; + Big.RM = RM; + Big.NE = NE; + Big.PE = PE; + Big.strict = STRICT; + Big.roundDown = 0; + Big.roundHalfUp = 1; + Big.roundHalfEven = 2; + Big.roundUp = 3; + + return Big; +} + + +/* + * Parse the number or string value passed to a Big constructor. + * + * x {Big} A Big number instance. + * n {number|string} A numeric value. + */ +function parse(x, n) { + var e, i, nl; + + if (!NUMERIC.test(n)) { + throw Error(INVALID + 'number'); + } + + // Determine sign. + x.s = n.charAt(0) == '-' ? (n = n.slice(1), -1) : 1; + + // Decimal point? + if ((e = n.indexOf('.')) > -1) n = n.replace('.', ''); + + // Exponential form? + if ((i = n.search(/e/i)) > 0) { + + // Determine exponent. + if (e < 0) e = i; + e += +n.slice(i + 1); + n = n.substring(0, i); + } else if (e < 0) { + + // Integer. + e = n.length; + } + + nl = n.length; + + // Determine leading zeros. + for (i = 0; i < nl && n.charAt(i) == '0';) ++i; + + if (i == nl) { + + // Zero. + x.c = [x.e = 0]; + } else { + + // Determine trailing zeros. + for (; nl > 0 && n.charAt(--nl) == '0';); + x.e = e - i - 1; + x.c = []; + + // Convert string to array of digits without leading/trailing zeros. + for (e = 0; i <= nl;) x.c[e++] = +n.charAt(i++); + } + + return x; +} + + +/* + * Round Big x to a maximum of sd significant digits using rounding mode rm. + * + * x {Big} The Big to round. + * sd {number} Significant digits: integer, 0 to MAX_DP inclusive. + * rm {number} Rounding mode: 0 (down), 1 (half-up), 2 (half-even) or 3 (up). + * [more] {boolean} Whether the result of division was truncated. + */ +function round(x, sd, rm, more) { + var xc = x.c; + + if (rm === UNDEFINED) rm = x.constructor.RM; + if (rm !== 0 && rm !== 1 && rm !== 2 && rm !== 3) { + throw Error(INVALID_RM); + } + + if (sd < 1) { + more = + rm === 3 && (more || !!xc[0]) || sd === 0 && ( + rm === 1 && xc[0] >= 5 || + rm === 2 && (xc[0] > 5 || xc[0] === 5 && (more || xc[1] !== UNDEFINED)) + ); + + xc.length = 1; + + if (more) { + + // 1, 0.1, 0.01, 0.001, 0.0001 etc. + x.e = x.e - sd + 1; + xc[0] = 1; + } else { + + // Zero. + xc[0] = x.e = 0; + } + } else if (sd < xc.length) { + + // xc[sd] is the digit after the digit that may be rounded up. + more = + rm === 1 && xc[sd] >= 5 || + rm === 2 && (xc[sd] > 5 || xc[sd] === 5 && + (more || xc[sd + 1] !== UNDEFINED || xc[sd - 1] & 1)) || + rm === 3 && (more || !!xc[0]); + + // Remove any digits after the required precision. + xc.length = sd; + + // Round up? + if (more) { + + // Rounding up may mean the previous digit has to be rounded up. + for (; ++xc[--sd] > 9;) { + xc[sd] = 0; + if (sd === 0) { + ++x.e; + xc.unshift(1); + break; + } + } + } + + // Remove trailing zeros. + for (sd = xc.length; !xc[--sd];) xc.pop(); + } + + return x; +} + + +/* + * Return a string representing the value of Big x in normal or exponential notation. + * Handles P.toExponential, P.toFixed, P.toJSON, P.toPrecision, P.toString and P.valueOf. + */ +function stringify(x, doExponential, isNonzero) { + var e = x.e, + s = x.c.join(''), + n = s.length; + + // Exponential notation? + if (doExponential) { + s = s.charAt(0) + (n > 1 ? '.' + s.slice(1) : '') + (e < 0 ? 'e' : 'e+') + e; + + // Normal notation. + } else if (e < 0) { + for (; ++e;) s = '0' + s; + s = '0.' + s; + } else if (e > 0) { + if (++e > n) { + for (e -= n; e--;) s += '0'; + } else if (e < n) { + s = s.slice(0, e) + '.' + s.slice(e); + } + } else if (n > 1) { + s = s.charAt(0) + '.' + s.slice(1); + } + + return x.s < 0 && isNonzero ? '-' + s : s; +} + + +// Prototype/instance methods + + +/* + * Return a new Big whose value is the absolute value of this Big. + */ +P.abs = function () { + var x = new this.constructor(this); + x.s = 1; + return x; +}; + + +/* + * Return 1 if the value of this Big is greater than the value of Big y, + * -1 if the value of this Big is less than the value of Big y, or + * 0 if they have the same value. + */ +P.cmp = function (y) { + var isneg, + x = this, + xc = x.c, + yc = (y = new x.constructor(y)).c, + i = x.s, + j = y.s, + k = x.e, + l = y.e; + + // Either zero? + if (!xc[0] || !yc[0]) return !xc[0] ? !yc[0] ? 0 : -j : i; + + // Signs differ? + if (i != j) return i; + + isneg = i < 0; + + // Compare exponents. + if (k != l) return k > l ^ isneg ? 1 : -1; + + j = (k = xc.length) < (l = yc.length) ? k : l; + + // Compare digit by digit. + for (i = -1; ++i < j;) { + if (xc[i] != yc[i]) return xc[i] > yc[i] ^ isneg ? 1 : -1; + } + + // Compare lengths. + return k == l ? 0 : k > l ^ isneg ? 1 : -1; +}; + + +/* + * Return a new Big whose value is the value of this Big divided by the value of Big y, rounded, + * if necessary, to a maximum of Big.DP decimal places using rounding mode Big.RM. + */ +P.div = function (y) { + var x = this, + Big = x.constructor, + a = x.c, // dividend + b = (y = new Big(y)).c, // divisor + k = x.s == y.s ? 1 : -1, + dp = Big.DP; + + if (dp !== ~~dp || dp < 0 || dp > MAX_DP) { + throw Error(INVALID_DP); + } + + // Divisor is zero? + if (!b[0]) { + throw Error(DIV_BY_ZERO); + } + + // Dividend is 0? Return +-0. + if (!a[0]) { + y.s = k; + y.c = [y.e = 0]; + return y; + } + + var bl, bt, n, cmp, ri, + bz = b.slice(), + ai = bl = b.length, + al = a.length, + r = a.slice(0, bl), // remainder + rl = r.length, + q = y, // quotient + qc = q.c = [], + qi = 0, + p = dp + (q.e = x.e - y.e) + 1; // precision of the result + + q.s = k; + k = p < 0 ? 0 : p; + + // Create version of divisor with leading zero. + bz.unshift(0); + + // Add zeros to make remainder as long as divisor. + for (; rl++ < bl;) r.push(0); + + do { + + // n is how many times the divisor goes into current remainder. + for (n = 0; n < 10; n++) { + + // Compare divisor and remainder. + if (bl != (rl = r.length)) { + cmp = bl > rl ? 1 : -1; + } else { + for (ri = -1, cmp = 0; ++ri < bl;) { + if (b[ri] != r[ri]) { + cmp = b[ri] > r[ri] ? 1 : -1; + break; + } + } + } + + // If divisor < remainder, subtract divisor from remainder. + if (cmp < 0) { + + // Remainder can't be more than 1 digit longer than divisor. + // Equalise lengths using divisor with extra leading zero? + for (bt = rl == bl ? b : bz; rl;) { + if (r[--rl] < bt[rl]) { + ri = rl; + for (; ri && !r[--ri];) r[ri] = 9; + --r[ri]; + r[rl] += 10; + } + r[rl] -= bt[rl]; + } + + for (; !r[0];) r.shift(); + } else { + break; + } + } + + // Add the digit n to the result array. + qc[qi++] = cmp ? n : ++n; + + // Update the remainder. + if (r[0] && cmp) r[rl] = a[ai] || 0; + else r = [a[ai]]; + + } while ((ai++ < al || r[0] !== UNDEFINED) && k--); + + // Leading zero? Do not remove if result is simply zero (qi == 1). + if (!qc[0] && qi != 1) { + + // There can't be more than one zero. + qc.shift(); + q.e--; + p--; + } + + // Round? + if (qi > p) round(q, p, Big.RM, r[0] !== UNDEFINED); + + return q; +}; + + +/* + * Return true if the value of this Big is equal to the value of Big y, otherwise return false. + */ +P.eq = function (y) { + return this.cmp(y) === 0; +}; + + +/* + * Return true if the value of this Big is greater than the value of Big y, otherwise return + * false. + */ +P.gt = function (y) { + return this.cmp(y) > 0; +}; + + +/* + * Return true if the value of this Big is greater than or equal to the value of Big y, otherwise + * return false. + */ +P.gte = function (y) { + return this.cmp(y) > -1; +}; + + +/* + * Return true if the value of this Big is less than the value of Big y, otherwise return false. + */ +P.lt = function (y) { + return this.cmp(y) < 0; +}; + + +/* + * Return true if the value of this Big is less than or equal to the value of Big y, otherwise + * return false. + */ +P.lte = function (y) { + return this.cmp(y) < 1; +}; + + +/* + * Return a new Big whose value is the value of this Big minus the value of Big y. + */ +P.minus = P.sub = function (y) { + var i, j, t, xlty, + x = this, + Big = x.constructor, + a = x.s, + b = (y = new Big(y)).s; + + // Signs differ? + if (a != b) { + y.s = -b; + return x.plus(y); + } + + var xc = x.c.slice(), + xe = x.e, + yc = y.c, + ye = y.e; + + // Either zero? + if (!xc[0] || !yc[0]) { + if (yc[0]) { + y.s = -b; + } else if (xc[0]) { + y = new Big(x); + } else { + y.s = 1; + } + return y; + } + + // Determine which is the bigger number. Prepend zeros to equalise exponents. + if (a = xe - ye) { + + if (xlty = a < 0) { + a = -a; + t = xc; + } else { + ye = xe; + t = yc; + } + + t.reverse(); + for (b = a; b--;) t.push(0); + t.reverse(); + } else { + + // Exponents equal. Check digit by digit. + j = ((xlty = xc.length < yc.length) ? xc : yc).length; + + for (a = b = 0; b < j; b++) { + if (xc[b] != yc[b]) { + xlty = xc[b] < yc[b]; + break; + } + } + } + + // x < y? Point xc to the array of the bigger number. + if (xlty) { + t = xc; + xc = yc; + yc = t; + y.s = -y.s; + } + + /* + * Append zeros to xc if shorter. No need to add zeros to yc if shorter as subtraction only + * needs to start at yc.length. + */ + if ((b = (j = yc.length) - (i = xc.length)) > 0) for (; b--;) xc[i++] = 0; + + // Subtract yc from xc. + for (b = i; j > a;) { + if (xc[--j] < yc[j]) { + for (i = j; i && !xc[--i];) xc[i] = 9; + --xc[i]; + xc[j] += 10; + } + + xc[j] -= yc[j]; + } + + // Remove trailing zeros. + for (; xc[--b] === 0;) xc.pop(); + + // Remove leading zeros and adjust exponent accordingly. + for (; xc[0] === 0;) { + xc.shift(); + --ye; + } + + if (!xc[0]) { + + // n - n = +0 + y.s = 1; + + // Result must be zero. + xc = [ye = 0]; + } + + y.c = xc; + y.e = ye; + + return y; +}; + + +/* + * Return a new Big whose value is the value of this Big modulo the value of Big y. + */ +P.mod = function (y) { + var ygtx, + x = this, + Big = x.constructor, + a = x.s, + b = (y = new Big(y)).s; + + if (!y.c[0]) { + throw Error(DIV_BY_ZERO); + } + + x.s = y.s = 1; + ygtx = y.cmp(x) == 1; + x.s = a; + y.s = b; + + if (ygtx) return new Big(x); + + a = Big.DP; + b = Big.RM; + Big.DP = Big.RM = 0; + x = x.div(y); + Big.DP = a; + Big.RM = b; + + return this.minus(x.times(y)); +}; + + +/* + * Return a new Big whose value is the value of this Big negated. + */ +P.neg = function () { + var x = new this.constructor(this); + x.s = -x.s; + return x; +}; + + +/* + * Return a new Big whose value is the value of this Big plus the value of Big y. + */ +P.plus = P.add = function (y) { + var e, k, t, + x = this, + Big = x.constructor; + + y = new Big(y); + + // Signs differ? + if (x.s != y.s) { + y.s = -y.s; + return x.minus(y); + } + + var xe = x.e, + xc = x.c, + ye = y.e, + yc = y.c; + + // Either zero? + if (!xc[0] || !yc[0]) { + if (!yc[0]) { + if (xc[0]) { + y = new Big(x); + } else { + y.s = x.s; + } + } + return y; + } + + xc = xc.slice(); + + // Prepend zeros to equalise exponents. + // Note: reverse faster than unshifts. + if (e = xe - ye) { + if (e > 0) { + ye = xe; + t = yc; + } else { + e = -e; + t = xc; + } + + t.reverse(); + for (; e--;) t.push(0); + t.reverse(); + } + + // Point xc to the longer array. + if (xc.length - yc.length < 0) { + t = yc; + yc = xc; + xc = t; + } + + e = yc.length; + + // Only start adding at yc.length - 1 as the further digits of xc can be left as they are. + for (k = 0; e; xc[e] %= 10) k = (xc[--e] = xc[e] + yc[e] + k) / 10 | 0; + + // No need to check for zero, as +x + +y != 0 && -x + -y != 0 + + if (k) { + xc.unshift(k); + ++ye; + } + + // Remove trailing zeros. + for (e = xc.length; xc[--e] === 0;) xc.pop(); + + y.c = xc; + y.e = ye; + + return y; +}; + + +/* + * Return a Big whose value is the value of this Big raised to the power n. + * If n is negative, round to a maximum of Big.DP decimal places using rounding + * mode Big.RM. + * + * n {number} Integer, -MAX_POWER to MAX_POWER inclusive. + */ +P.pow = function (n) { + var x = this, + one = new x.constructor('1'), + y = one, + isneg = n < 0; + + if (n !== ~~n || n < -MAX_POWER || n > MAX_POWER) { + throw Error(INVALID + 'exponent'); + } + + if (isneg) n = -n; + + for (;;) { + if (n & 1) y = y.times(x); + n >>= 1; + if (!n) break; + x = x.times(x); + } + + return isneg ? one.div(y) : y; +}; + + +/* + * Return a new Big whose value is the value of this Big rounded to a maximum precision of sd + * significant digits using rounding mode rm, or Big.RM if rm is not specified. + * + * sd {number} Significant digits: integer, 1 to MAX_DP inclusive. + * rm? {number} Rounding mode: 0 (down), 1 (half-up), 2 (half-even) or 3 (up). + */ +P.prec = function (sd, rm) { + if (sd !== ~~sd || sd < 1 || sd > MAX_DP) { + throw Error(INVALID + 'precision'); + } + return round(new this.constructor(this), sd, rm); +}; + + +/* + * Return a new Big whose value is the value of this Big rounded to a maximum of dp decimal places + * using rounding mode rm, or Big.RM if rm is not specified. + * If dp is negative, round to an integer which is a multiple of 10**-dp. + * If dp is not specified, round to 0 decimal places. + * + * dp? {number} Integer, -MAX_DP to MAX_DP inclusive. + * rm? {number} Rounding mode: 0 (down), 1 (half-up), 2 (half-even) or 3 (up). + */ +P.round = function (dp, rm) { + if (dp === UNDEFINED) dp = 0; + else if (dp !== ~~dp || dp < -MAX_DP || dp > MAX_DP) { + throw Error(INVALID_DP); + } + return round(new this.constructor(this), dp + this.e + 1, rm); +}; + + +/* + * Return a new Big whose value is the square root of the value of this Big, rounded, if + * necessary, to a maximum of Big.DP decimal places using rounding mode Big.RM. + */ +P.sqrt = function () { + var r, c, t, + x = this, + Big = x.constructor, + s = x.s, + e = x.e, + half = new Big('0.5'); + + // Zero? + if (!x.c[0]) return new Big(x); + + // Negative? + if (s < 0) { + throw Error(NAME + 'No square root'); + } + + // Estimate. + s = Math.sqrt(x + ''); + + // Math.sqrt underflow/overflow? + // Re-estimate: pass x coefficient to Math.sqrt as integer, then adjust the result exponent. + if (s === 0 || s === 1 / 0) { + c = x.c.join(''); + if (!(c.length + e & 1)) c += '0'; + s = Math.sqrt(c); + e = ((e + 1) / 2 | 0) - (e < 0 || e & 1); + r = new Big((s == 1 / 0 ? '5e' : (s = s.toExponential()).slice(0, s.indexOf('e') + 1)) + e); + } else { + r = new Big(s + ''); + } + + e = r.e + (Big.DP += 4); + + // Newton-Raphson iteration. + do { + t = r; + r = half.times(t.plus(x.div(t))); + } while (t.c.slice(0, e).join('') !== r.c.slice(0, e).join('')); + + return round(r, (Big.DP -= 4) + r.e + 1, Big.RM); +}; + + +/* + * Return a new Big whose value is the value of this Big times the value of Big y. + */ +P.times = P.mul = function (y) { + var c, + x = this, + Big = x.constructor, + xc = x.c, + yc = (y = new Big(y)).c, + a = xc.length, + b = yc.length, + i = x.e, + j = y.e; + + // Determine sign of result. + y.s = x.s == y.s ? 1 : -1; + + // Return signed 0 if either 0. + if (!xc[0] || !yc[0]) { + y.c = [y.e = 0]; + return y; + } + + // Initialise exponent of result as x.e + y.e. + y.e = i + j; + + // If array xc has fewer digits than yc, swap xc and yc, and lengths. + if (a < b) { + c = xc; + xc = yc; + yc = c; + j = a; + a = b; + b = j; + } + + // Initialise coefficient array of result with zeros. + for (c = new Array(j = a + b); j--;) c[j] = 0; + + // Multiply. + + // i is initially xc.length. + for (i = b; i--;) { + b = 0; + + // a is yc.length. + for (j = a + i; j > i;) { + + // Current sum of products at this digit position, plus carry. + b = c[j] + yc[i] * xc[j - i - 1] + b; + c[j--] = b % 10; + + // carry + b = b / 10 | 0; + } + + c[j] = b; + } + + // Increment result exponent if there is a final carry, otherwise remove leading zero. + if (b) ++y.e; + else c.shift(); + + // Remove trailing zeros. + for (i = c.length; !c[--i];) c.pop(); + y.c = c; + + return y; +}; + + +/* + * Return a string representing the value of this Big in exponential notation rounded to dp fixed + * decimal places using rounding mode rm, or Big.RM if rm is not specified. + * + * dp? {number} Decimal places: integer, 0 to MAX_DP inclusive. + * rm? {number} Rounding mode: 0 (down), 1 (half-up), 2 (half-even) or 3 (up). + */ +P.toExponential = function (dp, rm) { + var x = this, + n = x.c[0]; + + if (dp !== UNDEFINED) { + if (dp !== ~~dp || dp < 0 || dp > MAX_DP) { + throw Error(INVALID_DP); + } + x = round(new x.constructor(x), ++dp, rm); + for (; x.c.length < dp;) x.c.push(0); + } + + return stringify(x, true, !!n); +}; + + +/* + * Return a string representing the value of this Big in normal notation rounded to dp fixed + * decimal places using rounding mode rm, or Big.RM if rm is not specified. + * + * dp? {number} Decimal places: integer, 0 to MAX_DP inclusive. + * rm? {number} Rounding mode: 0 (down), 1 (half-up), 2 (half-even) or 3 (up). + * + * (-0).toFixed(0) is '0', but (-0.1).toFixed(0) is '-0'. + * (-0).toFixed(1) is '0.0', but (-0.01).toFixed(1) is '-0.0'. + */ +P.toFixed = function (dp, rm) { + var x = this, + n = x.c[0]; + + if (dp !== UNDEFINED) { + if (dp !== ~~dp || dp < 0 || dp > MAX_DP) { + throw Error(INVALID_DP); + } + x = round(new x.constructor(x), dp + x.e + 1, rm); + + // x.e may have changed if the value is rounded up. + for (dp = dp + x.e + 1; x.c.length < dp;) x.c.push(0); + } + + return stringify(x, false, !!n); +}; + + +/* + * Return a string representing the value of this Big. + * Return exponential notation if this Big has a positive exponent equal to or greater than + * Big.PE, or a negative exponent equal to or less than Big.NE. + * Omit the sign for negative zero. + */ +P[Symbol.for('nodejs.util.inspect.custom')] = P.toJSON = P.toString = function () { + var x = this, + Big = x.constructor; + return stringify(x, x.e <= Big.NE || x.e >= Big.PE, !!x.c[0]); +}; + + +/* + * Return the value of this Big as a primitve number. + */ +P.toNumber = function () { + var n = Number(stringify(this, true, true)); + if (this.constructor.strict === true && !this.eq(n.toString())) { + throw Error(NAME + 'Imprecise conversion'); + } + return n; +}; + + +/* + * Return a string representing the value of this Big rounded to sd significant digits using + * rounding mode rm, or Big.RM if rm is not specified. + * Use exponential notation if sd is less than the number of digits necessary to represent + * the integer part of the value in normal notation. + * + * sd {number} Significant digits: integer, 1 to MAX_DP inclusive. + * rm? {number} Rounding mode: 0 (down), 1 (half-up), 2 (half-even) or 3 (up). + */ +P.toPrecision = function (sd, rm) { + var x = this, + Big = x.constructor, + n = x.c[0]; + + if (sd !== UNDEFINED) { + if (sd !== ~~sd || sd < 1 || sd > MAX_DP) { + throw Error(INVALID + 'precision'); + } + x = round(new Big(x), sd, rm); + for (; x.c.length < sd;) x.c.push(0); + } + + return stringify(x, sd <= x.e || x.e <= Big.NE || x.e >= Big.PE, !!n); +}; + + +/* + * Return a string representing the value of this Big. + * Return exponential notation if this Big has a positive exponent equal to or greater than + * Big.PE, or a negative exponent equal to or less than Big.NE. + * Include the sign for negative zero. + */ +P.valueOf = function () { + var x = this, + Big = x.constructor; + if (Big.strict === true) { + throw Error(NAME + 'valueOf disallowed'); + } + return stringify(x, x.e <= Big.NE || x.e >= Big.PE, true); +}; + + +// Export + + +export var Big = _Big_(); + +/// +export default Big; diff --git a/efactura-generator/js/vendor/html2pdf.bundle.min.js b/efactura-generator/js/vendor/html2pdf.bundle.min.js new file mode 100644 index 0000000..fdb594e --- /dev/null +++ b/efactura-generator/js/vendor/html2pdf.bundle.min.js @@ -0,0 +1,3 @@ +/*! For license information please see html2pdf.bundle.min.js.LICENSE.txt */ +!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define("html2pdf",[],t):"object"==typeof exports?exports.html2pdf=t():e.html2pdf=t()}(self,(function(){return function(){var e,t,r={"./node_modules/@babel/runtime-corejs3/core-js-stable/array/from.js":function(e,t,r){e.exports=r("./node_modules/core-js-pure/stable/array/from.js")},"./node_modules/@babel/runtime-corejs3/core-js-stable/array/is-array.js":function(e,t,r){e.exports=r("./node_modules/core-js-pure/stable/array/is-array.js")},"./node_modules/@babel/runtime-corejs3/core-js-stable/date/now.js":function(e,t,r){e.exports=r("./node_modules/core-js-pure/stable/date/now.js")},"./node_modules/@babel/runtime-corejs3/core-js-stable/instance/bind.js":function(e,t,r){e.exports=r("./node_modules/core-js-pure/stable/instance/bind.js")},"./node_modules/@babel/runtime-corejs3/core-js-stable/instance/concat.js":function(e,t,r){e.exports=r("./node_modules/core-js-pure/stable/instance/concat.js")},"./node_modules/@babel/runtime-corejs3/core-js-stable/instance/every.js":function(e,t,r){e.exports=r("./node_modules/core-js-pure/stable/instance/every.js")},"./node_modules/@babel/runtime-corejs3/core-js-stable/instance/fill.js":function(e,t,r){e.exports=r("./node_modules/core-js-pure/stable/instance/fill.js")},"./node_modules/@babel/runtime-corejs3/core-js-stable/instance/filter.js":function(e,t,r){e.exports=r("./node_modules/core-js-pure/stable/instance/filter.js")},"./node_modules/@babel/runtime-corejs3/core-js-stable/instance/for-each.js":function(e,t,r){e.exports=r("./node_modules/core-js-pure/stable/instance/for-each.js")},"./node_modules/@babel/runtime-corejs3/core-js-stable/instance/includes.js":function(e,t,r){e.exports=r("./node_modules/core-js-pure/stable/instance/includes.js")},"./node_modules/@babel/runtime-corejs3/core-js-stable/instance/index-of.js":function(e,t,r){e.exports=r("./node_modules/core-js-pure/stable/instance/index-of.js")},"./node_modules/@babel/runtime-corejs3/core-js-stable/instance/map.js":function(e,t,r){e.exports=r("./node_modules/core-js-pure/stable/instance/map.js")},"./node_modules/@babel/runtime-corejs3/core-js-stable/instance/reduce.js":function(e,t,r){e.exports=r("./node_modules/core-js-pure/stable/instance/reduce.js")},"./node_modules/@babel/runtime-corejs3/core-js-stable/instance/reverse.js":function(e,t,r){e.exports=r("./node_modules/core-js-pure/stable/instance/reverse.js")},"./node_modules/@babel/runtime-corejs3/core-js-stable/instance/slice.js":function(e,t,r){e.exports=r("./node_modules/core-js-pure/stable/instance/slice.js")},"./node_modules/@babel/runtime-corejs3/core-js-stable/instance/some.js":function(e,t,r){e.exports=r("./node_modules/core-js-pure/stable/instance/some.js")},"./node_modules/@babel/runtime-corejs3/core-js-stable/instance/starts-with.js":function(e,t,r){e.exports=r("./node_modules/core-js-pure/stable/instance/starts-with.js")},"./node_modules/@babel/runtime-corejs3/core-js-stable/instance/trim.js":function(e,t,r){e.exports=r("./node_modules/core-js-pure/stable/instance/trim.js")},"./node_modules/@babel/runtime-corejs3/core-js-stable/instance/values.js":function(e,t,r){e.exports=r("./node_modules/core-js-pure/stable/instance/values.js")},"./node_modules/@babel/runtime-corejs3/core-js-stable/map.js":function(e,t,r){e.exports=r("./node_modules/core-js-pure/stable/map/index.js")},"./node_modules/@babel/runtime-corejs3/core-js-stable/object/define-properties.js":function(e,t,r){e.exports=r("./node_modules/core-js-pure/stable/object/define-properties.js")},"./node_modules/@babel/runtime-corejs3/core-js-stable/object/define-property.js":function(e,t,r){e.exports=r("./node_modules/core-js-pure/stable/object/define-property.js")},"./node_modules/@babel/runtime-corejs3/core-js-stable/object/get-own-property-descriptor.js":function(e,t,r){e.exports=r("./node_modules/core-js-pure/stable/object/get-own-property-descriptor.js")},"./node_modules/@babel/runtime-corejs3/core-js-stable/object/get-own-property-descriptors.js":function(e,t,r){e.exports=r("./node_modules/core-js-pure/stable/object/get-own-property-descriptors.js")},"./node_modules/@babel/runtime-corejs3/core-js-stable/object/get-own-property-symbols.js":function(e,t,r){e.exports=r("./node_modules/core-js-pure/stable/object/get-own-property-symbols.js")},"./node_modules/@babel/runtime-corejs3/core-js-stable/object/keys.js":function(e,t,r){e.exports=r("./node_modules/core-js-pure/stable/object/keys.js")},"./node_modules/@babel/runtime-corejs3/core-js-stable/parse-float.js":function(e,t,r){e.exports=r("./node_modules/core-js-pure/stable/parse-float.js")},"./node_modules/@babel/runtime-corejs3/core-js-stable/parse-int.js":function(e,t,r){e.exports=r("./node_modules/core-js-pure/stable/parse-int.js")},"./node_modules/@babel/runtime-corejs3/core-js-stable/promise.js":function(e,t,r){e.exports=r("./node_modules/core-js-pure/stable/promise/index.js")},"./node_modules/@babel/runtime-corejs3/core-js-stable/reflect/apply.js":function(e,t,r){e.exports=r("./node_modules/core-js-pure/stable/reflect/apply.js")},"./node_modules/@babel/runtime-corejs3/core-js-stable/reflect/construct.js":function(e,t,r){e.exports=r("./node_modules/core-js-pure/stable/reflect/construct.js")},"./node_modules/@babel/runtime-corejs3/core-js-stable/reflect/delete-property.js":function(e,t,r){e.exports=r("./node_modules/core-js-pure/stable/reflect/delete-property.js")},"./node_modules/@babel/runtime-corejs3/core-js-stable/reflect/get-prototype-of.js":function(e,t,r){e.exports=r("./node_modules/core-js-pure/stable/reflect/get-prototype-of.js")},"./node_modules/@babel/runtime-corejs3/core-js-stable/symbol.js":function(e,t,r){e.exports=r("./node_modules/core-js-pure/stable/symbol/index.js")},"./node_modules/@babel/runtime-corejs3/core-js/array/from.js":function(e,t,r){e.exports=r("./node_modules/core-js-pure/features/array/from.js")},"./node_modules/@babel/runtime-corejs3/core-js/array/is-array.js":function(e,t,r){e.exports=r("./node_modules/core-js-pure/features/array/is-array.js")},"./node_modules/@babel/runtime-corejs3/core-js/get-iterator-method.js":function(e,t,r){e.exports=r("./node_modules/core-js-pure/features/get-iterator-method.js")},"./node_modules/@babel/runtime-corejs3/core-js/get-iterator.js":function(e,t,r){e.exports=r("./node_modules/core-js-pure/features/get-iterator.js")},"./node_modules/@babel/runtime-corejs3/core-js/instance/slice.js":function(e,t,r){e.exports=r("./node_modules/core-js-pure/features/instance/slice.js")},"./node_modules/@babel/runtime-corejs3/core-js/object/create.js":function(e,t,r){e.exports=r("./node_modules/core-js-pure/features/object/create.js")},"./node_modules/@babel/runtime-corejs3/core-js/object/define-property.js":function(e,t,r){e.exports=r("./node_modules/core-js-pure/features/object/define-property.js")},"./node_modules/@babel/runtime-corejs3/core-js/object/get-own-property-descriptor.js":function(e,t,r){e.exports=r("./node_modules/core-js-pure/features/object/get-own-property-descriptor.js")},"./node_modules/@babel/runtime-corejs3/core-js/object/get-prototype-of.js":function(e,t,r){e.exports=r("./node_modules/core-js-pure/features/object/get-prototype-of.js")},"./node_modules/@babel/runtime-corejs3/core-js/object/set-prototype-of.js":function(e,t,r){e.exports=r("./node_modules/core-js-pure/features/object/set-prototype-of.js")},"./node_modules/@babel/runtime-corejs3/core-js/promise.js":function(e,t,r){e.exports=r("./node_modules/core-js-pure/features/promise/index.js")},"./node_modules/@babel/runtime-corejs3/core-js/reflect/get.js":function(e,t,r){e.exports=r("./node_modules/core-js-pure/features/reflect/get.js")},"./node_modules/@babel/runtime-corejs3/core-js/symbol.js":function(e,t,r){e.exports=r("./node_modules/core-js-pure/features/symbol/index.js")},"./node_modules/@babel/runtime-corejs3/core-js/symbol/iterator.js":function(e,t,r){e.exports=r("./node_modules/core-js-pure/features/symbol/iterator.js")},"./node_modules/@babel/runtime-corejs3/helpers/esm/arrayLikeToArray.js":function(e,t,r){"use strict";function n(e,t){(null==t||t>e.length)&&(t=e.length);for(var r=0,n=new Array(t);rA){var c=u;u=A,A=c}}else{if("l"!==e&&"landscape"!==e)throw"Invalid orientation: "+e;e="l",A>u&&(c=u,u=A,A=c)}return{width:u,height:A,unit:t,k:a}},t.default=n.jsPDF},"./src/plugin/pagebreaks.js":function(e,t,r){"use strict";r.r(t),r("./node_modules/core-js/modules/es.array.concat.js"),r("./node_modules/core-js/modules/es.array.slice.js"),r("./node_modules/core-js/modules/es.array.join.js"),r("./node_modules/core-js/modules/web.dom-collections.for-each.js"),r("./node_modules/core-js/modules/es.object.keys.js");var n=r("./src/worker.js"),o=r("./src/utils.js"),s={toContainer:n.default.prototype.toContainer};n.default.template.opt.pagebreak={mode:["css","legacy"],before:[],after:[],avoid:[]},n.default.prototype.toContainer=function(){return s.toContainer.call(this).then((function(){var e=this.prop.container,t=this.prop.pageSize.inner.px.height,r=[].concat(this.opt.pagebreak.mode),n={avoidAll:-1!==r.indexOf("avoid-all"),css:-1!==r.indexOf("css"),legacy:-1!==r.indexOf("legacy")},s={},i=this;["before","after","avoid"].forEach((function(t){var r=n.avoidAll&&"avoid"===t;s[t]=r?[]:[].concat(i.opt.pagebreak[t]||[]),s[t].length>0&&(s[t]=Array.prototype.slice.call(e.querySelectorAll(s[t].join(", "))))}));var a=e.querySelectorAll(".html2pdf__page-break");a=Array.prototype.slice.call(a);var A=e.querySelectorAll("*");Array.prototype.forEach.call(A,(function(e){var r={before:!1,after:n.legacy&&-1!==a.indexOf(e),avoid:n.avoidAll};if(n.css){var i=window.getComputedStyle(e),A=["always","page","left","right"];r={before:r.before||-1!==A.indexOf(i.breakBefore||i.pageBreakBefore),after:r.after||-1!==A.indexOf(i.breakAfter||i.pageBreakAfter),avoid:r.avoid||-1!==["avoid","avoid-page"].indexOf(i.breakInside||i.pageBreakInside)}}Object.keys(r).forEach((function(t){r[t]=r[t]||-1!==s[t].indexOf(e)}));var u=e.getBoundingClientRect();if(r.avoid&&!r.before){var c=Math.floor(u.top/t),l=Math.floor(u.bottom/t),d=Math.abs(u.bottom-u.top)/t;l!==c&&d<=1&&(r.before=!0)}if(r.before){var f=(0,o.createElement)("div",{style:{display:"block",height:t-u.top%t+"px"}});e.parentNode.insertBefore(f,e)}r.after&&(f=(0,o.createElement)("div",{style:{display:"block",height:t-u.bottom%t+"px"}}),e.parentNode.insertBefore(f,e.nextSibling))}))}))}},"./src/utils.js":function(e,t,r){"use strict";function n(e){return(n="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e})(e)}r.r(t),r.d(t,{objType:function(){return o},createElement:function(){return s},cloneNode:function(){return i},unitConvert:function(){return a},toPx:function(){return A}}),r("./node_modules/core-js/modules/es.number.constructor.js"),r("./node_modules/core-js/modules/es.symbol.js"),r("./node_modules/core-js/modules/es.symbol.description.js"),r("./node_modules/core-js/modules/es.object.to-string.js"),r("./node_modules/core-js/modules/es.symbol.iterator.js"),r("./node_modules/core-js/modules/es.array.iterator.js"),r("./node_modules/core-js/modules/es.string.iterator.js"),r("./node_modules/core-js/modules/web.dom-collections.iterator.js");var o=function(e){var t=n(e);return"undefined"===t?"undefined":"string"===t||e instanceof String?"string":"number"===t||e instanceof Number?"number":"function"===t||e instanceof Function?"function":e&&e.constructor===Array?"array":e&&1===e.nodeType?"element":"object"===t?"object":"unknown"},s=function(e,t){var r=document.createElement(e);if(t.className&&(r.className=t.className),t.innerHTML){r.innerHTML=t.innerHTML;for(var n=r.getElementsByTagName("script"),o=n.length;o-- >0;null)n[o].parentNode.removeChild(n[o])}for(var s in t.style)r.style[s]=t.style[s];return r},i=function e(t,r){for(var n=3===t.nodeType?document.createTextNode(t.nodeValue):t.cloneNode(!1),o=t.firstChild;o;o=o.nextSibling)!0!==r&&1===o.nodeType&&"SCRIPT"===o.nodeName||n.appendChild(e(o,r));return 1===t.nodeType&&("CANVAS"===t.nodeName?(n.width=t.width,n.height=t.height,n.getContext("2d").drawImage(t,0,0)):"TEXTAREA"!==t.nodeName&&"SELECT"!==t.nodeName||(n.value=t.value),n.addEventListener("load",(function(){n.scrollTop=t.scrollTop,n.scrollLeft=t.scrollLeft}),!0)),n},a=function(e,t){if("number"===o(e))return 72*e/96/t;var r={};for(var n in e)r[n]=72*e[n]/96/t;return r},A=function(e,t){return Math.floor(e*t/72*96)}},"./src/worker.js":function(e,t,r){"use strict";r.r(t),r("./node_modules/core-js/modules/es.object.assign.js"),r("./node_modules/core-js/modules/es.array.map.js"),r("./node_modules/core-js/modules/es.object.keys.js"),r("./node_modules/core-js/modules/es.array.concat.js"),r("./node_modules/core-js/modules/es.object.to-string.js"),r("./node_modules/core-js/modules/es.regexp.to-string.js"),r("./node_modules/core-js/modules/es.function.name.js"),r("./node_modules/core-js/modules/web.dom-collections.for-each.js");var n=r("./node_modules/jspdf/dist/jspdf.es.min.js"),o=r("./node_modules/html2canvas/dist/html2canvas.js"),s=r("./src/utils.js"),i=r("./node_modules/es6-promise/dist/es6-promise.js"),a=r.n(i)().Promise,A=function e(t){var r=Object.assign(e.convert(a.resolve()),JSON.parse(JSON.stringify(e.template))),n=e.convert(a.resolve(),r);return(n=n.setProgress(1,e,1,[e])).set(t)};(A.prototype=Object.create(a.prototype)).constructor=A,A.convert=function(e,t){return e.__proto__=t||A.prototype,e},A.template={prop:{src:null,container:null,overlay:null,canvas:null,img:null,pdf:null,pageSize:null},progress:{val:0,state:null,n:0,stack:[]},opt:{filename:"file.pdf",margin:[0,0,0,0],image:{type:"jpeg",quality:.95},enableLinks:!0,html2canvas:{},jsPDF:{}}},A.prototype.from=function(e,t){return this.then((function(){switch(t=t||function(e){switch((0,s.objType)(e)){case"string":return"string";case"element":return e.nodeName.toLowerCase&&"canvas"===e.nodeName.toLowerCase()?"canvas":"element";default:return"unknown"}}(e)){case"string":return this.set({src:(0,s.createElement)("div",{innerHTML:e})});case"element":return this.set({src:e});case"canvas":return this.set({canvas:e});case"img":return this.set({img:e});default:return this.error("Unknown source type.")}}))},A.prototype.to=function(e){switch(e){case"container":return this.toContainer();case"canvas":return this.toCanvas();case"img":return this.toImg();case"pdf":return this.toPdf();default:return this.error("Invalid target.")}},A.prototype.toContainer=function(){return this.thenList([function(){return this.prop.src||this.error("Cannot duplicate - no source HTML.")},function(){return this.prop.pageSize||this.setPageSize()}]).then((function(){var e={position:"fixed",overflow:"hidden",zIndex:1e3,left:0,right:0,bottom:0,top:0,backgroundColor:"rgba(0,0,0,0.8)"},t={position:"absolute",width:this.prop.pageSize.inner.width+this.prop.pageSize.unit,left:0,right:0,top:0,height:"auto",margin:"auto",backgroundColor:"white"};e.opacity=0;var r=(0,s.cloneNode)(this.prop.src,this.opt.html2canvas.javascriptEnabled);this.prop.overlay=(0,s.createElement)("div",{className:"html2pdf__overlay",style:e}),this.prop.container=(0,s.createElement)("div",{className:"html2pdf__container",style:t}),this.prop.container.appendChild(r),this.prop.overlay.appendChild(this.prop.container),document.body.appendChild(this.prop.overlay)}))},A.prototype.toCanvas=function(){var e=[function(){return document.body.contains(this.prop.container)||this.toContainer()}];return this.thenList(e).then((function(){var e=Object.assign({},this.opt.html2canvas);return delete e.onrendered,o(this.prop.container,e)})).then((function(e){(this.opt.html2canvas.onrendered||function(){})(e),this.prop.canvas=e,document.body.removeChild(this.prop.overlay)}))},A.prototype.toImg=function(){return this.thenList([function(){return this.prop.canvas||this.toCanvas()}]).then((function(){var e=this.prop.canvas.toDataURL("image/"+this.opt.image.type,this.opt.image.quality);this.prop.img=document.createElement("img"),this.prop.img.src=e}))},A.prototype.toPdf=function(){return this.thenList([function(){return this.prop.canvas||this.toCanvas()},function(){return this.prop.pageSize||this.setPageSize()}]).then((function(){var e=this.prop.canvas,t=this.opt,r=e.height,o=Math.floor(e.width*this.prop.pageSize.inner.ratio),s=Math.ceil(r/o),i=this.prop.pageSize.inner.height,a=document.createElement("canvas"),A=a.getContext("2d");a.width=e.width,a.height=o,this.prop.pdf=this.prop.pdf||new n.jsPDF(t.jsPDF);for(var u=0;u~\.\[:]+)/g,Je=/(\.[^\s\+>~\.\[:]+)/g,Ye=/(::[^\s\+>~\.\[:]+|:first-line|:first-letter|:before|:after)/gi,Ze=/(:[\w-]+\([^\)]*\))/gi,$e=/(:[^\s\+>~\.\[:]+)/g,et=/([^\s\+>~\.\[:]+)/g;function tt(e,t){var r=e.match(t);return r?[e.replace(t," "),r.length]:[e,0]}function rt(e){var t=[0,0,0],r=e.replace(/:not\(([^\)]*)\)/g," $1 ").replace(/{[\s\S]*/gm," "),n=0,o=tt(r,Xe),s=(0,u.default)(o,2);r=s[0],n=s[1],t[1]+=n;var i=tt(r,We),a=(0,u.default)(i,2);r=a[0],n=a[1],t[0]+=n;var A=tt(r,Je),c=(0,u.default)(A,2);r=c[0],n=c[1],t[1]+=n;var l=tt(r,Ye),d=(0,u.default)(l,2);r=d[0],n=d[1],t[2]+=n;var f=tt(r,Ze),h=(0,u.default)(f,2);r=h[0],n=h[1],t[1]+=n;var p=tt(r,$e),m=(0,u.default)(p,2);r=m[0],n=m[1],t[1]+=n;var g=tt(r=r.replace(/[\*\s\+>~]/g," ").replace(/[#\.]/g," "),et),y=(0,u.default)(g,2);return r=y[0],n=y[1],t[2]+=n,t.join("")}var nt=1e-8;function ot(e){return Math.sqrt(Math.pow(e[0],2)+Math.pow(e[1],2))}function st(e,t){return(e[0]*t[0]+e[1]*t[1])/(ot(e)*ot(t))}function it(e,t){return(e[0]*t[1]0&&void 0!==arguments[0]?arguments[0]:" ",o=this.document,s=this.name;return A()(t=G()(r=Re(this.getString())).call(r).split(n)).call(t,(function(t){return new e(o,s,t)}))}},{key:"hasValue",value:function(e){var t=this.value;return null!==t&&""!==t&&(e||0!==t)&&void 0!==t}},{key:"isString",value:function(e){var t=this.value,r="string"==typeof t;return r&&e?e.test(t):r}},{key:"isUrlDefinition",value:function(){return this.isString(/^url\(/)}},{key:"isPixels",value:function(){if(!this.hasValue())return!1;var e=this.getString();switch(!0){case/px$/.test(e):case/^[0-9]+$/.test(e):return!0;default:return!1}}},{key:"setValue",value:function(e){return this.value=e,this}},{key:"getValue",value:function(e){return void 0===e||this.hasValue()?this.value:e}},{key:"getNumber",value:function(e){if(!this.hasValue())return void 0===e?0:i()(e);var t=this.value,r=i()(t);return this.isString(/%$/)&&(r/=100),r}},{key:"getString",value:function(e){return void 0===e||this.hasValue()?void 0===this.value?"":String(this.value):String(e)}},{key:"getColor",value:function(e){var t=this.getString(e);return this.isNormalizedColor||(this.isNormalizedColor=!0,t=Ge(t),this.value=t),t}},{key:"getDpi",value:function(){return 96}},{key:"getRem",value:function(){return this.document.rootEmSize}},{key:"getEm",value:function(){return this.document.emSize}},{key:"getUnits",value:function(){return this.getString().replace(/[0-9\.\-]/g,"")}},{key:"getPixels",value:function(e){var t=arguments.length>1&&void 0!==arguments[1]&&arguments[1];if(!this.hasValue())return 0;var r="boolean"==typeof e?[void 0,e]:[e],n=(0,u.default)(r,2),o=n[0],s=n[1],i=this.document.screen.viewPort;switch(!0){case this.isString(/vmin$/):return this.getNumber()/100*Math.min(i.computeSize("x"),i.computeSize("y"));case this.isString(/vmax$/):return this.getNumber()/100*Math.max(i.computeSize("x"),i.computeSize("y"));case this.isString(/vw$/):return this.getNumber()/100*i.computeSize("x");case this.isString(/vh$/):return this.getNumber()/100*i.computeSize("y");case this.isString(/rem$/):return this.getNumber()*this.getRem();case this.isString(/em$/):return this.getNumber()*this.getEm();case this.isString(/ex$/):return this.getNumber()*this.getEm()/2;case this.isString(/px$/):return this.getNumber();case this.isString(/pt$/):return this.getNumber()*this.getDpi()*(1/72);case this.isString(/pc$/):return 15*this.getNumber();case this.isString(/cm$/):return this.getNumber()*this.getDpi()/2.54;case this.isString(/mm$/):return this.getNumber()*this.getDpi()/25.4;case this.isString(/in$/):return this.getNumber()*this.getDpi();case this.isString(/%$/)&&s:return this.getNumber()*this.getEm();case this.isString(/%$/):return this.getNumber()*i.computeSize(o);default:var a=this.getNumber();return t&&a<1?a*i.computeSize(o):a}}},{key:"getMilliseconds",value:function(){return this.hasValue()?this.isString(/ms$/)?this.getNumber():1e3*this.getNumber():0}},{key:"getRadians",value:function(){if(!this.hasValue())return 0;switch(!0){case this.isString(/deg$/):return this.getNumber()*(Math.PI/180);case this.isString(/grad$/):return this.getNumber()*(Math.PI/200);case this.isString(/rad$/):return this.getNumber();default:return this.getNumber()*(Math.PI/180)}}},{key:"getDefinition",value:function(){var e=this.getString(),t=e.match(/#([^\)'"]+)/);return t&&(t=t[1]),t||(t=e),this.document.definitions[t]}},{key:"getFillStyleDefinition",value:function(e,t){var r=this.getDefinition();if(!r)return null;if("function"==typeof r.createGradient)return r.createGradient(this.document.ctx,e,t);if("function"==typeof r.createPattern){if(r.getHrefAttribute().hasValue()){var n=r.getAttribute("patternTransform");r=r.getHrefAttribute().getDefinition(),n.hasValue()&&r.getAttribute("patternTransform",!0).setValue(n.value)}return r.createPattern(this.document.ctx,e,t)}return null}},{key:"getTextBaseline",value:function(){return this.hasValue()?e.textBaselineMapping[this.getString()]:null}},{key:"addOpacity",value:function(t){for(var r=this.getColor(),n=r.length,o=0,s=0;s1&&void 0!==arguments[1]?arguments[1]:0,n=Ke(t),o=(0,u.default)(n,2),s=o[0],i=void 0===s?r:s,a=o[1],A=void 0===a?r:a;return new e(i,A)}},{key:"parseScale",value:function(t){var r=arguments.length>1&&void 0!==arguments[1]?arguments[1]:1,n=Ke(t),o=(0,u.default)(n,2),s=o[0],i=void 0===s?r:s,a=o[1],A=void 0===a?i:a;return new e(i,A)}},{key:"parsePath",value:function(t){for(var r=Ke(t),n=r.length,o=[],s=0;s0}},{key:"runEvents",value:function(){if(this.working){var e=this.screen,t=this.events,r=this.eventElements,n=e.ctx.canvas.style;n&&(n.cursor=""),g()(t).call(t,(function(e,t){for(var n=e.run,o=r[t];o;)n(o),o=o.parent})),this.events=[],this.eventElements=[]}}},{key:"checkPath",value:function(e,t){if(this.working&&t){var r=this.events,n=this.eventElements;g()(r).call(r,(function(r,o){var s=r.x,i=r.y;!n[o]&&t.isPointInPath&&t.isPointInPath(s,i)&&(n[o]=e)}))}}},{key:"checkBoundingBox",value:function(e,t){if(this.working&&t){var r=this.events,n=this.eventElements;g()(r).call(r,(function(r,o){var s=r.x,i=r.y;!n[o]&&t.isPointInBox(s,i)&&(n[o]=e)}))}}},{key:"mapXY",value:function(e,t){for(var r=this.screen,n=r.window,o=r.ctx,s=new mt(e,t),i=o.canvas;i;)s.x-=i.offsetLeft,s.y-=i.offsetTop,i=i.offsetParent;return n.scrollX&&(s.x+=n.scrollX),n.scrollY&&(s.y+=n.scrollY),s}},{key:"onClick",value:function(e){var t=this.mapXY((e||event).clientX,(e||event).clientY),r=t.x,n=t.y;this.events.push({type:"onclick",x:r,y:n,run:function(e){e.onClick&&e.onClick()}})}},{key:"onMouseMove",value:function(e){var t=this.mapXY((e||event).clientX,(e||event).clientY),r=t.x,n=t.y;this.events.push({type:"onmousemove",x:r,y:n,run:function(e){e.onMouseMove&&e.onMouseMove()}})}}]),e}(),yt="undefined"!=typeof window?window:null,vt="undefined"!=typeof fetch?K()(fetch).call(fetch,void 0):null,wt=function(){function e(t){var r=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},n=r.fetch,o=void 0===n?vt:n,s=r.window,i=void 0===s?yt:s;(0,F.default)(this,e),this.ctx=t,this.FRAMERATE=30,this.MAX_VIRTUAL_PIXELS=3e4,this.CLIENT_WIDTH=800,this.CLIENT_HEIGHT=600,this.viewPort=new pt,this.mouse=new gt(this),this.animations=[],this.waits=[],this.frameDuration=0,this.isReadyLock=!1,this.isFirstRender=!0,this.intervalId=null,this.window=i,this.fetch=o}return(0,U.default)(e,[{key:"wait",value:function(e){this.waits.push(e)}},{key:"ready",value:function(){return this.readyPromise?this.readyPromise:M().resolve()}},{key:"isReady",value:function(){var e;if(this.isReadyLock)return!0;var t=k()(e=this.waits).call(e,(function(e){return e()}));return t&&(this.waits=[],this.resolveReady&&this.resolveReady()),this.isReadyLock=t,t}},{key:"setDefaults",value:function(e){e.strokeStyle="rgba(0,0,0,0)",e.lineCap="butt",e.lineJoin="miter",e.miterLimit=4}},{key:"setViewBox",value:function(e){var t=e.document,r=e.ctx,n=e.aspectRatio,o=e.width,s=e.desiredWidth,i=e.height,a=e.desiredHeight,A=e.minX,c=void 0===A?0:A,l=e.minY,d=void 0===l?0:l,f=e.refX,h=e.refY,p=e.clip,m=void 0!==p&&p,g=e.clipX,y=void 0===g?0:g,v=e.clipY,w=void 0===v?0:v,b=Re(n).replace(/^defer\s/,"").split(" "),B=(0,u.default)(b,2),j=B[0]||"xMidYMid",_=B[1]||"meet",C=o/s,x=i/a,E=Math.min(C,x),N=Math.max(C,x),Q=s,F=a;"meet"===_&&(Q*=E,F*=E),"slice"===_&&(Q*=N,F*=N);var U=new ht(t,"refX",f),S=new ht(t,"refY",h),L=U.hasValue()&&S.hasValue();if(L&&r.translate(-E*U.getPixels("x"),-E*S.getPixels("y")),m){var T=E*y,H=E*w;r.beginPath(),r.moveTo(T,H),r.lineTo(o,H),r.lineTo(o,i),r.lineTo(T,i),r.closePath(),r.clip()}if(!L){var I="meet"===_&&E===x,P="slice"===_&&N===x,O="meet"===_&&E===C,k="slice"===_&&N===C;/^xMid/.test(j)&&(I||P)&&r.translate(o/2-Q/2,0),/YMid$/.test(j)&&(O||k)&&r.translate(0,i/2-F/2),/^xMax/.test(j)&&(I||P)&&r.translate(o-Q,0),/YMax$/.test(j)&&(O||k)&&r.translate(0,i-F)}switch(!0){case"none"===j:r.scale(C,x);break;case"meet"===_:r.scale(E,E);break;case"slice"===_:r.scale(N,N)}r.translate(-c,-d)}},{key:"start",value:function(e){var t=this,r=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},n=r.enableRedraw,o=void 0!==n&&n,s=r.ignoreMouse,i=void 0!==s&&s,a=r.ignoreAnimation,A=void 0!==a&&a,u=r.ignoreDimensions,c=void 0!==u&&u,l=r.ignoreClear,d=void 0!==l&&l,f=r.forceRedraw,h=r.scaleWidth,p=r.scaleHeight,m=r.offsetX,g=r.offsetY,y=this.FRAMERATE,v=this.mouse,w=1e3/y;if(this.frameDuration=w,this.readyPromise=new(M())((function(e){t.resolveReady=e})),this.isReady()&&this.render(e,c,d,h,p,m,g),o){var b=P()(),B=b,j=0,_=function r(){b=P()(),(j=b-B)>=w&&(B=b-j%w,t.shouldUpdate(A,f)&&(t.render(e,c,d,h,p,m,g),v.runEvents())),t.intervalId=V()(r)};i||v.start(),this.intervalId=V()(_)}}},{key:"stop",value:function(){this.intervalId&&(V().cancel(this.intervalId),this.intervalId=null),this.mouse.stop()}},{key:"shouldUpdate",value:function(e,t){if(!e){var r,n=this.frameDuration;if(H()(r=this.animations).call(r,(function(e,t){return t.update(n)||e}),!1))return!0}return!("function"!=typeof t||!t())||!(this.isReadyLock||!this.isReady())||!!this.mouse.hasEvents()}},{key:"render",value:function(e,t,r,n,o,s,i){var a=this.CLIENT_WIDTH,A=this.CLIENT_HEIGHT,u=this.viewPort,c=this.ctx,l=this.isFirstRender,d=c.canvas;u.clear(),d.width&&d.height?u.setCurrent(d.width,d.height):u.setCurrent(a,A);var f=e.getStyle("width"),h=e.getStyle("height");!t&&(l||"number"!=typeof n&&"number"!=typeof o)&&(f.hasValue()&&(d.width=f.getPixels("x"),d.style&&(d.style.width="".concat(d.width,"px"))),h.hasValue()&&(d.height=h.getPixels("y"),d.style&&(d.style.height="".concat(d.height,"px"))));var p=d.clientWidth||d.width,m=d.clientHeight||d.height;if(t&&f.hasValue()&&h.hasValue()&&(p=f.getPixels("x"),m=h.getPixels("y")),u.setCurrent(p,m),"number"==typeof s&&e.getAttribute("x",!0).setValue(s),"number"==typeof i&&e.getAttribute("y",!0).setValue(i),"number"==typeof n||"number"==typeof o){var g,y,v=Ke(e.getAttribute("viewBox").getString()),w=0,b=0;if("number"==typeof n){var B=e.getStyle("width");B.hasValue()?w=B.getPixels("x")/n:isNaN(v[2])||(w=v[2]/n)}if("number"==typeof o){var j=e.getStyle("height");j.hasValue()?b=j.getPixels("y")/o:isNaN(v[3])||(b=v[3]/o)}w||(w=b),b||(b=w),e.getAttribute("width",!0).setValue(n),e.getAttribute("height",!0).setValue(o);var _=e.getStyle("transform",!0,!0);_.setValue(L()(g=L()(y="".concat(_.getString()," scale(")).call(y,1/w,", ")).call(g,1/b,")"))}r||c.clearRect(0,0,p,m),e.render(c),l&&(this.isFirstRender=!1)}}]),e}();wt.defaultWindow=yt,wt.defaultFetch=vt;var bt=wt.defaultFetch,Bt="undefined"!=typeof DOMParser?DOMParser:null,jt=function(){function e(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},r=t.fetch,n=void 0===r?bt:r,o=t.DOMParser,s=void 0===o?Bt:o;(0,F.default)(this,e),this.fetch=n,this.DOMParser=s}var t,r;return(0,U.default)(e,[{key:"parse",value:(r=(0,N.default)(E().mark((function e(t){return E().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:if(!/^=0;r--)t[r].unapply(e)}},{key:"applyToPoint",value:function(e){for(var t=this.transforms,r=t.length,n=0;n2&&void 0!==arguments[2]&&arguments[2];if((0,F.default)(this,e),this.document=t,this.node=r,this.captureTextNodes=i,this.attributes={},this.styles={},this.stylesSpecificity={},this.animationFrozen=!1,this.animationFrozenValue="",this.parent=null,this.children=[],r&&1===r.nodeType){if(g()(n=ae()(r.attributes)).call(n,(function(e){var r=Ve(e.nodeName);s.attributes[r]=new ht(t,r,e.value)})),this.addStylesFromStyleDefinition(),this.getAttribute("style").hasValue()){var a,c=A()(a=this.getAttribute("style").getString().split(";")).call(a,(function(e){return G()(e).call(e)}));g()(c).call(c,(function(e){var r;if(e){var n=A()(r=e.split(":")).call(r,(function(e){return G()(e).call(e)})),o=(0,u.default)(n,2),i=o[0],a=o[1];s.styles[i]=new ht(t,i,a)}}))}var l=t.definitions,d=this.getAttribute("id");d.hasValue()&&(l[d.getValue()]||(l[d.getValue()]=this)),g()(o=ae()(r.childNodes)).call(o,(function(e){if(1===e.nodeType)s.addChild(e);else if(i&&(3===e.nodeType||4===e.nodeType)){var r=t.createTextNode(e);r.getText().length>0&&s.addChild(r)}}))}}return(0,U.default)(e,[{key:"getAttribute",value:function(e){var t=arguments.length>1&&void 0!==arguments[1]&&arguments[1],r=this.attributes[e];if(!r&&t){var n=new ht(this.document,e,"");return this.attributes[e]=n,n}return r||ht.empty(this.document)}},{key:"getHrefAttribute",value:function(){for(var e in this.attributes)if("href"===e||/:href$/.test(e))return this.attributes[e];return ht.empty(this.document)}},{key:"getStyle",value:function(e){var t=arguments.length>1&&void 0!==arguments[1]&&arguments[1],r=arguments.length>2&&void 0!==arguments[2]&&arguments[2],n=this.styles[e];if(n)return n;var o=this.getAttribute(e);if(o&&o.hasValue())return this.styles[e]=o,o;if(!r){var s=this.parent;if(s){var i=s.getStyle(e);if(i&&i.hasValue())return i}}if(t){var a=new ht(this.document,e,"");return this.styles[e]=a,a}return n||ht.empty(this.document)}},{key:"render",value:function(e){if("none"!==this.getStyle("display").getString()&&"hidden"!==this.getStyle("visibility").getString()){if(e.save(),this.getStyle("mask").hasValue()){var t=this.getStyle("mask").getDefinition();t&&(this.applyEffects(e),t.apply(e,this))}else if("none"!==this.getStyle("filter").getValue("none")){var r=this.getStyle("filter").getDefinition();r&&(this.applyEffects(e),r.apply(e,this))}else this.setContext(e),this.renderChildren(e),this.clearContext(e);e.restore()}}},{key:"setContext",value:function(e){}},{key:"applyEffects",value:function(e){var t=Ut.fromElement(this.document,this);t&&t.apply(e);var r=this.getStyle("clip-path",!1,!0);if(r.hasValue()){var n=r.getDefinition();n&&n.apply(e)}}},{key:"clearContext",value:function(e){}},{key:"renderChildren",value:function(e){var t;g()(t=this.children).call(t,(function(t){t.render(e)}))}},{key:"addChild",value:function(t){var r,n=t instanceof e?t:this.document.createElement(t);n.parent=this,se()(r=e.ignoreChildTypes).call(r,n.type)||this.children.push(n)}},{key:"matchesSelector",value:function(e){var t,r=this.node;if("function"==typeof r.matches)return r.matches(e);var n=r.getAttribute("class");return!(!n||""===n)&&ne()(t=n.split(" ")).call(t,(function(t){if(".".concat(t)===e)return!0}))}},{key:"addStylesFromStyleDefinition",value:function(){var e=this.document,t=e.styles,r=e.stylesSpecificity;for(var n in t)if("@"!==n[0]&&this.matchesSelector(n)){var o=t[n],s=r[n];if(o)for(var i in o){var a=this.stylesSpecificity[i];void 0===a&&(a="000"),s>=a&&(this.styles[i]=o[i],this.stylesSpecificity[i]=s)}}}},{key:"removeStyles",value:function(e,t){return H()(t).call(t,(function(t,r){var n,o=e.getStyle(r);if(!o.hasValue())return t;var s=o.getString();return o.setValue(""),L()(n=[]).call(n,(0,te.default)(t),[[r,s]])}),[])}},{key:"restoreStyles",value:function(e,t){g()(t).call(t,(function(t){var r=(0,u.default)(t,2),n=r[0],o=r[1];e.getStyle(n,!0).setValue(o)}))}}]),e}();St.ignoreChildTypes=["title"];var Lt=function(e){(0,Z.default)(o,e);var t,r,n=(t=o,r=function(){if("undefined"==typeof Reflect||!Y())return!1;if(Y().sham)return!1;if("function"==typeof Proxy)return!0;try{return Date.prototype.toString.call(Y()(Date,[],(function(){}))),!0}catch(e){return!1}}(),function(){var e,n=(0,ee.default)(t);if(r){var o=(0,ee.default)(this).constructor;e=Y()(n,arguments,o)}else e=n.apply(this,arguments);return(0,$.default)(this,e)});function o(e,t,r){return(0,F.default)(this,o),n.call(this,e,t,r)}return o}(St);function Tt(e){var t=G()(e).call(e);return/^('|")/.test(t)?t:'"'.concat(t,'"')}function Ht(e){if(!e)return"";var t=G()(e).call(e).toLowerCase();switch(t){case"normal":case"italic":case"oblique":case"inherit":case"initial":case"unset":return t;default:return/^oblique\s+(-|)\d+deg$/.test(t)?t:""}}function It(e){if(!e)return"";var t=G()(e).call(e).toLowerCase();switch(t){case"normal":case"bold":case"lighter":case"bolder":case"inherit":case"initial":case"unset":return t;default:return/^[\d.]+$/.test(t)?t:""}}var Pt=function(){function e(t,r,n,o,s,i){(0,F.default)(this,e);var a=i?"string"==typeof i?e.parse(i):i:{};this.fontFamily=s||a.fontFamily,this.fontSize=o||a.fontSize,this.fontStyle=t||a.fontStyle,this.fontWeight=n||a.fontWeight,this.fontVariant=r||a.fontVariant}return(0,U.default)(e,[{key:"toString",value:function(){var e,t,r;return G()(e=[Ht(this.fontStyle),this.fontVariant,It(this.fontWeight),this.fontSize,(t=this.fontFamily,"undefined"==typeof process?t:A()(r=G()(t).call(t).split(",")).call(r,Tt).join(","))].join(" ")).call(e)}}],[{key:"parse",value:function(){var t,r=arguments.length>0&&void 0!==arguments[0]?arguments[0]:"",n=arguments.length>1?arguments[1]:void 0,o="",s="",i="",a="",A="",c=G()(t=Re(r)).call(t).split(" "),l={fontSize:!1,fontStyle:!1,fontWeight:!1,fontVariant:!1};return g()(c).call(c,(function(t){var r,n,c;switch(!0){case!l.fontStyle&&se()(r=e.styles).call(r,t):"inherit"!==t&&(o=t),l.fontStyle=!0;break;case!l.fontVariant&&se()(n=e.variants).call(n,t):"inherit"!==t&&(s=t),l.fontStyle=!0,l.fontVariant=!0;break;case!l.fontWeight&&se()(c=e.weights).call(c,t):"inherit"!==t&&(i=t),l.fontStyle=!0,l.fontVariant=!0,l.fontWeight=!0;break;case!l.fontSize:if("inherit"!==t){var d=t.split("/"),f=(0,u.default)(d,1);a=f[0]}l.fontStyle=!0,l.fontVariant=!0,l.fontWeight=!0,l.fontSize=!0;break;default:"inherit"!==t&&(A+=t)}})),new e(o,s,i,a,A,n)}}]),e}();Pt.styles="normal|italic|oblique|inherit",Pt.variants="normal|small-caps|inherit",Pt.weights="normal|bold|bolder|lighter|100|200|300|400|500|600|700|800|900|inherit";var Ot=function(){function e(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:Number.NaN,r=arguments.length>1&&void 0!==arguments[1]?arguments[1]:Number.NaN,n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:Number.NaN,o=arguments.length>3&&void 0!==arguments[3]?arguments[3]:Number.NaN;(0,F.default)(this,e),this.x1=t,this.y1=r,this.x2=n,this.y2=o,this.addPoint(t,r),this.addPoint(n,o)}return(0,U.default)(e,[{key:"addPoint",value:function(e,t){void 0!==e&&((isNaN(this.x1)||isNaN(this.x2))&&(this.x1=e,this.x2=e),ethis.x2&&(this.x2=e)),void 0!==t&&((isNaN(this.y1)||isNaN(this.y2))&&(this.y1=t,this.y2=t),tthis.y2&&(this.y2=t))}},{key:"addX",value:function(e){this.addPoint(e,null)}},{key:"addY",value:function(e){this.addPoint(null,e)}},{key:"addBoundingBox",value:function(e){if(e){var t=e.x1,r=e.y1,n=e.x2,o=e.y2;this.addPoint(t,r),this.addPoint(n,o)}}},{key:"sumCubic",value:function(e,t,r,n,o){return Math.pow(1-e,3)*t+3*Math.pow(1-e,2)*e*r+3*(1-e)*Math.pow(e,2)*n+Math.pow(e,3)*o}},{key:"bezierCurveAdd",value:function(e,t,r,n,o){var s=6*t-12*r+6*n,i=-3*t+9*r-9*n+3*o,a=3*r-3*t;if(0!==i){var A=Math.pow(s,2)-4*a*i;if(!(A<0)){var u=(-s+Math.sqrt(A))/(2*i);01&&void 0!==arguments[1]&&arguments[1];if(!t){var r=this.getStyle("fill"),n=this.getStyle("fill-opacity"),o=this.getStyle("stroke"),s=this.getStyle("stroke-opacity");if(r.isUrlDefinition()){var i=r.getFillStyleDefinition(this,n);i&&(e.fillStyle=i)}else if(r.hasValue()){"currentColor"===r.getString()&&r.setValue(this.getStyle("color").getColor());var a=r.getColor();"inherit"!==a&&(e.fillStyle="none"===a?"rgba(0,0,0,0)":a)}if(n.hasValue()){var A=new ht(this.document,"fill",e.fillStyle).addOpacity(n).getColor();e.fillStyle=A}if(o.isUrlDefinition()){var u=o.getFillStyleDefinition(this,s);u&&(e.strokeStyle=u)}else if(o.hasValue()){"currentColor"===o.getString()&&o.setValue(this.getStyle("color").getColor());var c=o.getString();"inherit"!==c&&(e.strokeStyle="none"===c?"rgba(0,0,0,0)":c)}if(s.hasValue()){var l=new ht(this.document,"stroke",e.strokeStyle).addOpacity(s).getString();e.strokeStyle=l}var d=this.getStyle("stroke-width");if(d.hasValue()){var f=d.getPixels();e.lineWidth=f||nt}var h=this.getStyle("stroke-linecap"),p=this.getStyle("stroke-linejoin"),m=this.getStyle("stroke-miterlimit"),g=this.getStyle("paint-order"),y=this.getStyle("stroke-dasharray"),v=this.getStyle("stroke-dashoffset");if(h.hasValue()&&(e.lineCap=h.getString()),p.hasValue()&&(e.lineJoin=p.getString()),m.hasValue()&&(e.miterLimit=m.getNumber()),g.hasValue()&&(e.paintOrder=g.getValue()),y.hasValue()&&"none"!==y.getString()){var w=Ke(y.getString());void 0!==e.setLineDash?e.setLineDash(w):void 0!==e.webkitLineDash?e.webkitLineDash=w:void 0===e.mozDash||1===w.length&&0===w[0]||(e.mozDash=w);var b=v.getPixels();void 0!==e.lineDashOffset?e.lineDashOffset=b:void 0!==e.webkitLineDashOffset?e.webkitLineDashOffset=b:void 0!==e.mozDashOffset&&(e.mozDashOffset=b)}}if(this.modifiedEmSizeStack=!1,void 0!==e.font){var B=this.getStyle("font"),j=this.getStyle("font-style"),_=this.getStyle("font-variant"),C=this.getStyle("font-weight"),x=this.getStyle("font-size"),E=this.getStyle("font-family"),N=new Pt(j.getString(),_.getString(),C.getString(),x.hasValue()?"".concat(x.getPixels(!0),"px"):"",E.getString(),Pt.parse(B.getString(),e.font));j.setValue(N.fontStyle),_.setValue(N.fontVariant),C.setValue(N.fontWeight),x.setValue(N.fontSize),E.setValue(N.fontFamily),e.font=N.toString(),x.isPixels()&&(this.document.emSize=x.getPixels(),this.modifiedEmSizeStack=!0)}t||(this.applyEffects(e),e.globalAlpha=this.calculateOpacity())}},{key:"clearContext",value:function(e){(0,de.default)((0,ee.default)(o.prototype),"clearContext",this).call(this,e),this.modifiedEmSizeStack&&this.document.popEmSize()}}]),o}(St);var Rt=function(e){(0,Z.default)(o,e);var t,r,n=(t=o,r=function(){if("undefined"==typeof Reflect||!Y())return!1;if(Y().sham)return!1;if("function"==typeof Proxy)return!0;try{return Date.prototype.toString.call(Y()(Date,[],(function(){}))),!0}catch(e){return!1}}(),function(){var e,n=(0,ee.default)(t);if(r){var o=(0,ee.default)(this).constructor;e=Y()(n,arguments,o)}else e=n.apply(this,arguments);return(0,$.default)(this,e)});function o(e,t,r){var s;return(0,F.default)(this,o),(s=n.call(this,e,t,(this instanceof o?this.constructor:void 0)===o||r)).type="text",s.x=0,s.y=0,s.measureCache=-1,s}return(0,U.default)(o,[{key:"setContext",value:function(e){var t=arguments.length>1&&void 0!==arguments[1]&&arguments[1];(0,de.default)((0,ee.default)(o.prototype),"setContext",this).call(this,e,t);var r=this.getStyle("dominant-baseline").getTextBaseline()||this.getStyle("alignment-baseline").getTextBaseline();r&&(e.textBaseline=r)}},{key:"initializeCoordinates",value:function(e){this.x=this.getAttribute("x").getPixels("x"),this.y=this.getAttribute("y").getPixels("y");var t=this.getAttribute("dx"),r=this.getAttribute("dy");t.hasValue()&&(this.x+=t.getPixels("x")),r.hasValue()&&(this.y+=r.getPixels("y")),this.x+=this.getAnchorDelta(e,this,0)}},{key:"getBoundingBox",value:function(e){var t,r=this;if("text"!==this.type)return this.getTElementBoundingBox(e);this.initializeCoordinates(e);var n=null;return g()(t=this.children).call(t,(function(t,o){var s=r.getChildBoundingBox(e,r,r,o);n?n.addBoundingBox(s):n=s})),n}},{key:"getFontSize",value:function(){var e=this.document,t=this.parent,r=Pt.parse(e.ctx.font).fontSize;return t.getStyle("font-size").getNumber(r)}},{key:"getTElementBoundingBox",value:function(e){var t=this.getFontSize();return new Ot(this.x,this.y-t,this.x+this.measureText(e),this.y)}},{key:"getGlyph",value:function(e,t,r){var n=t[r],o=null;if(e.isArabic){var s=t.length,i=t[r-1],a=t[r+1],A="isolated";(0===r||" "===i)&&r0&&" "!==i&&r0&&" "!==i&&(r===s-1||" "===a)&&(A="initial"),void 0!==e.glyphs[n]&&((o=e.glyphs[n][A])||"glyph"!==e.glyphs[n].type||(o=e.glyphs[n]))}else o=e.glyphs[n];return o||(o=e.missingGlyph),o}},{key:"getText",value:function(){return""}},{key:"getTextFromNode",value:function(e){var t=e||this.node,r=ae()(t.parentNode.childNodes),n=le()(r).call(r,t),o=r.length-1,s=Re(t.value||t.text||t.textContent||"");return 0===n&&(s=Me(s)),n===o&&(s=De(s)),s}},{key:"renderChildren",value:function(e){var t,r=this;if("text"===this.type){this.initializeCoordinates(e),g()(t=this.children).call(t,(function(t,n){r.renderChild(e,r,r,n)}));var n=this.document.screen.mouse;n.isWorking()&&n.checkBoundingBox(this,this.getBoundingBox(e))}else this.renderTElementChildren(e)}},{key:"renderTElementChildren",value:function(e){var t=this.document,r=this.parent,n=this.getText(),o=r.getStyle("font-family").getDefinition();if(o)for(var s,i=o.fontFace.unitsPerEm,a=Pt.parse(t.ctx.font),A=r.getStyle("font-size").getNumber(a.fontSize),u=r.getStyle("font-style").getString(a.fontStyle),c=A/i,l=o.isRTL?ue()(s=n.split("")).call(s).join(""):n,d=Ke(r.getAttribute("dx").getString()),f=l.length,h=0;hr&&i.getAttribute("x").hasValue()||i.getAttribute("text-anchor").hasValue()));A++)a+=i.measureTextRecursive(e);return-1*("end"===n?a:a/2)}return 0}},{key:"adjustChildCoordinates",value:function(e,t,r,n){var o=r.children[n];if("function"!=typeof o.measureText)return o;e.save(),o.setContext(e,!0);var s=o.getAttribute("x"),i=o.getAttribute("y"),a=o.getAttribute("dx"),A=o.getAttribute("dy"),u=o.getAttribute("text-anchor").getString("start");if(0===n&&"textNode"!==o.type&&(s.hasValue()||s.setValue(t.getAttribute("x").getValue("0")),i.hasValue()||i.setValue(t.getAttribute("y").getValue("0")),a.hasValue()||a.setValue(t.getAttribute("dx").getValue("0")),A.hasValue()||A.setValue(t.getAttribute("dy").getValue("0"))),s.hasValue()){if(o.x=s.getPixels("x")+t.getAnchorDelta(e,r,n),"start"!==u){var c=o.measureTextRecursive(e);o.x+=-1*("end"===u?c:c/2)}a.hasValue()&&(o.x+=a.getPixels("x"))}else{if("start"!==u){var l=o.measureTextRecursive(e);t.x+=-1*("end"===u?l:l/2)}a.hasValue()&&(t.x+=a.getPixels("x")),o.x=t.x}return t.x=o.x+o.measureText(e),i.hasValue()?(o.y=i.getPixels("y"),A.hasValue()&&(o.y+=A.getPixels("y"))):(A.hasValue()&&(t.y+=A.getPixels("y")),o.y=t.y),t.y=o.y,o.clearContext(e),e.restore(),o}},{key:"getChildBoundingBox",value:function(e,t,r,n){var o,s=this.adjustChildCoordinates(e,t,r,n);if("function"!=typeof s.getBoundingBox)return null;var i=s.getBoundingBox(e);return i?(g()(o=s.children).call(o,(function(r,n){var o=t.getChildBoundingBox(e,t,s,n);i.addBoundingBox(o)})),i):null}},{key:"renderChild",value:function(e,t,r,n){var o,s=this.adjustChildCoordinates(e,t,r,n);s.render(e),g()(o=s.children).call(o,(function(r,n){t.renderChild(e,t,s,n)}))}},{key:"measureTextRecursive",value:function(e){var t;return H()(t=this.children).call(t,(function(t,r){return t+r.measureTextRecursive(e)}),this.measureText(e))}},{key:"measureText",value:function(e){var t=this.measureCache;if(~t)return t;var r=this.getText(),n=this.measureTargetText(e,r);return this.measureCache=n,n}},{key:"measureTargetText",value:function(e,t){if(!t.length)return 0;var r=this.parent,n=r.getStyle("font-family").getDefinition();if(n){for(var o,s=this.getFontSize(),i=n.isRTL?ue()(o=t.split("")).call(o).join(""):t,a=Ke(r.getAttribute("dx").getString()),A=i.length,u=0,c=0;c0?"":s.getTextFromNode(),s}return(0,U.default)(o,[{key:"getText",value:function(){return this.text}}]),o}(Rt);var Dt=function(e){(0,Z.default)(o,e);var t,r,n=(t=o,r=function(){if("undefined"==typeof Reflect||!Y())return!1;if(Y().sham)return!1;if("function"==typeof Proxy)return!0;try{return Date.prototype.toString.call(Y()(Date,[],(function(){}))),!0}catch(e){return!1}}(),function(){var e,n=(0,ee.default)(t);if(r){var o=(0,ee.default)(this).constructor;e=Y()(n,arguments,o)}else e=n.apply(this,arguments);return(0,$.default)(this,e)});function o(){var e;return(0,F.default)(this,o),(e=n.apply(this,arguments)).type="textNode",e}return o}(Mt);var Kt=function(e){(0,Z.default)(o,e);var t,r,n=(t=o,r=function(){if("undefined"==typeof Reflect||!Y())return!1;if(Y().sham)return!1;if("function"==typeof Proxy)return!0;try{return Date.prototype.toString.call(Y()(Date,[],(function(){}))),!0}catch(e){return!1}}(),function(){var e,n=(0,ee.default)(t);if(r){var o=(0,ee.default)(this).constructor;e=Y()(n,arguments,o)}else e=n.apply(this,arguments);return(0,$.default)(this,e)});function o(e){var t;return(0,F.default)(this,o),(t=n.call(this,e.replace(/[+-.]\s+/g,"-").replace(/[^MmZzLlHhVvCcSsQqTtAae\d\s.,+-].*/g,""))).control=null,t.start=null,t.current=null,t.command=null,t.commands=t.commands,t.i=-1,t.previousCommand=null,t.points=[],t.angles=[],t}return(0,U.default)(o,[{key:"reset",value:function(){this.i=-1,this.command=null,this.previousCommand=null,this.start=new mt(0,0),this.control=new mt(0,0),this.current=new mt(0,0),this.points=[],this.angles=[]}},{key:"isEnd",value:function(){return this.i>=this.commands.length-1}},{key:"next",value:function(){var e=this.commands[++this.i];return this.previousCommand=this.command,this.command=e,e}},{key:"getPoint",value:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:"x",t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"y",r=new mt(this.command[e],this.command[t]);return this.makeAbsolute(r)}},{key:"getAsControlPoint",value:function(e,t){var r=this.getPoint(e,t);return this.control=r,r}},{key:"getAsCurrentPoint",value:function(e,t){var r=this.getPoint(e,t);return this.current=r,r}},{key:"getReflectedControlPoint",value:function(){var e=this.previousCommand.type;if(e!==pe.SVGPathData.CURVE_TO&&e!==pe.SVGPathData.SMOOTH_CURVE_TO&&e!==pe.SVGPathData.QUAD_TO&&e!==pe.SVGPathData.SMOOTH_QUAD_TO)return this.current;var t=this.current,r=t.x,n=t.y,o=this.control,s=o.x,i=o.y;return new mt(2*r-s,2*n-i)}},{key:"makeAbsolute",value:function(e){if(this.command.relative){var t=this.current,r=t.x,n=t.y;e.x+=r,e.y+=n}return e}},{key:"addMarker",value:function(e,t,r){var n=this.points,o=this.angles;r&&o.length>0&&!o[o.length-1]&&(o[o.length-1]=n[n.length-1].angleTo(r)),this.addMarkerAngle(e,t?t.angleTo(e):null)}},{key:"addMarkerAngle",value:function(e,t){this.points.push(e),this.angles.push(t)}},{key:"getMarkerPoints",value:function(){return this.points}},{key:"getMarkerAngles",value:function(){for(var e=this.angles,t=e.length,r=0;ra?i:a,g=i>a?1:i/a,y=i>a?a/i:1;e.translate(c.x,c.y),e.rotate(u),e.scale(g,y),e.arc(0,0,m,l,l+d,Boolean(1-A)),e.scale(1/g,1/y),e.rotate(-u),e.translate(-c.x,-c.y)}}},{key:"pathZ",value:function(e,t){o.pathZ(this.pathParser),e&&t.x1!==t.x2&&t.y1!==t.y2&&e.closePath()}}],[{key:"pathM",value:function(e){var t=e.getAsCurrentPoint();return e.start=e.current,{point:t}}},{key:"pathL",value:function(e){return{current:e.current,point:e.getAsCurrentPoint()}}},{key:"pathH",value:function(e){var t=e.current,r=e.command,n=new mt((r.relative?t.x:0)+r.x,t.y);return e.current=n,{current:t,point:n}}},{key:"pathV",value:function(e){var t=e.current,r=e.command,n=new mt(t.x,(r.relative?t.y:0)+r.y);return e.current=n,{current:t,point:n}}},{key:"pathC",value:function(e){return{current:e.current,point:e.getPoint("x1","y1"),controlPoint:e.getAsControlPoint("x2","y2"),currentPoint:e.getAsCurrentPoint()}}},{key:"pathS",value:function(e){return{current:e.current,point:e.getReflectedControlPoint(),controlPoint:e.getAsControlPoint("x2","y2"),currentPoint:e.getAsCurrentPoint()}}},{key:"pathQ",value:function(e){return{current:e.current,controlPoint:e.getAsControlPoint("x1","y1"),currentPoint:e.getAsCurrentPoint()}}},{key:"pathT",value:function(e){var t=e.current,r=e.getReflectedControlPoint();return e.control=r,{current:t,controlPoint:r,currentPoint:e.getAsCurrentPoint()}}},{key:"pathA",value:function(e){var t=e.current,r=e.command,n=r.rX,o=r.rY,s=r.xRot,i=r.lArcFlag,a=r.sweepFlag,A=s*(Math.PI/180),u=e.getAsCurrentPoint(),c=new mt(Math.cos(A)*(t.x-u.x)/2+Math.sin(A)*(t.y-u.y)/2,-Math.sin(A)*(t.x-u.x)/2+Math.cos(A)*(t.y-u.y)/2),l=Math.pow(c.x,2)/Math.pow(n,2)+Math.pow(c.y,2)/Math.pow(o,2);l>1&&(n*=Math.sqrt(l),o*=Math.sqrt(l));var d=(i===a?-1:1)*Math.sqrt((Math.pow(n,2)*Math.pow(o,2)-Math.pow(n,2)*Math.pow(c.y,2)-Math.pow(o,2)*Math.pow(c.x,2))/(Math.pow(n,2)*Math.pow(c.y,2)+Math.pow(o,2)*Math.pow(c.x,2)));isNaN(d)&&(d=0);var f=new mt(d*n*c.y/o,d*-o*c.x/n),h=new mt((t.x+u.x)/2+Math.cos(A)*f.x-Math.sin(A)*f.y,(t.y+u.y)/2+Math.sin(A)*f.x+Math.cos(A)*f.y),p=it([1,0],[(c.x-f.x)/n,(c.y-f.y)/o]),m=[(c.x-f.x)/n,(c.y-f.y)/o],g=[(-c.x-f.x)/n,(-c.y-f.y)/o],y=it(m,g);return st(m,g)<=-1&&(y=Math.PI),st(m,g)>=1&&(y=0),{currentPoint:u,rX:n,rY:o,sweepFlag:a,xAxisRotation:A,centp:h,a1:p,ad:y}}},{key:"pathZ",value:function(e){e.current=e.start}}]),o}(kt);var Vt=function(e){(0,Z.default)(o,e);var t,r,n=(t=o,r=function(){if("undefined"==typeof Reflect||!Y())return!1;if(Y().sham)return!1;if("function"==typeof Proxy)return!0;try{return Date.prototype.toString.call(Y()(Date,[],(function(){}))),!0}catch(e){return!1}}(),function(){var e,n=(0,ee.default)(t);if(r){var o=(0,ee.default)(this).constructor;e=Y()(n,arguments,o)}else e=n.apply(this,arguments);return(0,$.default)(this,e)});function o(){var e;return(0,F.default)(this,o),(e=n.apply(this,arguments)).type="svg",e.root=!1,e}return(0,U.default)(o,[{key:"setContext",value:function(e){var t=this.document,r=t.screen,n=t.window,s=e.canvas;if(r.setDefaults(e),s.style&&void 0!==e.font&&n&&void 0!==n.getComputedStyle){e.font=n.getComputedStyle(s).getPropertyValue("font");var i=new ht(t,"fontSize",Pt.parse(e.font).fontSize);i.hasValue()&&(t.rootEmSize=i.getPixels("y"),t.emSize=t.rootEmSize)}this.getAttribute("x").hasValue()||this.getAttribute("x",!0).setValue(0),this.getAttribute("y").hasValue()||this.getAttribute("y",!0).setValue(0);var a=r.viewPort,A=a.width,u=a.height;this.getStyle("width").hasValue()||this.getStyle("width",!0).setValue("100%"),this.getStyle("height").hasValue()||this.getStyle("height",!0).setValue("100%"),this.getStyle("color").hasValue()||this.getStyle("color",!0).setValue("black");var c=this.getAttribute("refX"),l=this.getAttribute("refY"),d=this.getAttribute("viewBox"),f=d.hasValue()?Ke(d.getString()):null,h=!this.root&&"visible"!==this.getStyle("overflow").getValue("hidden"),p=0,m=0,g=0,y=0;f&&(p=f[0],m=f[1]),this.root||(A=this.getStyle("width").getPixels("x"),u=this.getStyle("height").getPixels("y"),"marker"===this.type&&(g=p,y=m,p=0,m=0)),r.viewPort.setCurrent(A,u),this.node&&this.getStyle("transform",!1,!0).hasValue()&&!this.getStyle("transform-origin",!1,!0).hasValue()&&this.getStyle("transform-origin",!0,!0).setValue("50% 50%"),(0,de.default)((0,ee.default)(o.prototype),"setContext",this).call(this,e),e.translate(this.getAttribute("x").getPixels("x"),this.getAttribute("y").getPixels("y")),f&&(A=f[2],u=f[3]),t.setViewBox({ctx:e,aspectRatio:this.getAttribute("preserveAspectRatio").getString(),width:r.viewPort.width,desiredWidth:A,height:r.viewPort.height,desiredHeight:u,minX:p,minY:m,refX:c.getValue(),refY:l.getValue(),clip:h,clipX:g,clipY:y}),f&&(r.viewPort.removeCurrent(),r.viewPort.setCurrent(A,u))}},{key:"clearContext",value:function(e){(0,de.default)((0,ee.default)(o.prototype),"clearContext",this).call(this,e),this.document.screen.viewPort.removeCurrent()}},{key:"resize",value:function(e){var t,r=arguments.length>1&&void 0!==arguments[1]?arguments[1]:e,n=arguments.length>2&&void 0!==arguments[2]&&arguments[2],o=this.getAttribute("width",!0),s=this.getAttribute("height",!0),i=this.getAttribute("viewBox"),a=this.getAttribute("style"),A=o.getNumber(0),u=s.getNumber(0);if(n)if("string"==typeof n)this.getAttribute("preserveAspectRatio",!0).setValue(n);else{var c=this.getAttribute("preserveAspectRatio");c.hasValue()&&c.setValue(c.getString().replace(/^\s*(\S.*\S)\s*$/,"$1"))}if(o.setValue(e),s.setValue(r),i.hasValue()||i.setValue(L()(t="0 0 ".concat(A||e," ")).call(t,u||r)),a.hasValue()){var l=this.getStyle("width"),d=this.getStyle("height");l.hasValue()&&l.setValue("".concat(e,"px")),d.hasValue()&&d.setValue("".concat(r,"px"))}}}]),o}(kt);var qt=function(e){(0,Z.default)(o,e);var t,r,n=(t=o,r=function(){if("undefined"==typeof Reflect||!Y())return!1;if(Y().sham)return!1;if("function"==typeof Proxy)return!0;try{return Date.prototype.toString.call(Y()(Date,[],(function(){}))),!0}catch(e){return!1}}(),function(){var e,n=(0,ee.default)(t);if(r){var o=(0,ee.default)(this).constructor;e=Y()(n,arguments,o)}else e=n.apply(this,arguments);return(0,$.default)(this,e)});function o(){var e;return(0,F.default)(this,o),(e=n.apply(this,arguments)).type="rect",e}return(0,U.default)(o,[{key:"path",value:function(e){var t=this.getAttribute("x").getPixels("x"),r=this.getAttribute("y").getPixels("y"),n=this.getStyle("width",!1,!0).getPixels("x"),o=this.getStyle("height",!1,!0).getPixels("y"),s=this.getAttribute("rx"),i=this.getAttribute("ry"),a=s.getPixels("x"),A=i.getPixels("y");if(s.hasValue()&&!i.hasValue()&&(A=a),i.hasValue()&&!s.hasValue()&&(a=A),a=Math.min(a,n/2),A=Math.min(A,o/2),e){var u=(Math.sqrt(2)-1)/3*4;e.beginPath(),o>0&&n>0&&(e.moveTo(t+a,r),e.lineTo(t+n-a,r),e.bezierCurveTo(t+n-a+u*a,r,t+n,r+A-u*A,t+n,r+A),e.lineTo(t+n,r+o-A),e.bezierCurveTo(t+n,r+o-A+u*A,t+n-a+u*a,r+o,t+n-a,r+o),e.lineTo(t+a,r+o),e.bezierCurveTo(t+a-u*a,r+o,t,r+o-A+u*A,t,r+o-A),e.lineTo(t,r+A),e.bezierCurveTo(t,r+A-u*A,t+a-u*a,r,t+a,r),e.closePath())}return new Ot(t,r,t+n,r+o)}},{key:"getMarkers",value:function(){return null}}]),o}(zt);var Gt=function(e){(0,Z.default)(o,e);var t,r,n=(t=o,r=function(){if("undefined"==typeof Reflect||!Y())return!1;if(Y().sham)return!1;if("function"==typeof Proxy)return!0;try{return Date.prototype.toString.call(Y()(Date,[],(function(){}))),!0}catch(e){return!1}}(),function(){var e,n=(0,ee.default)(t);if(r){var o=(0,ee.default)(this).constructor;e=Y()(n,arguments,o)}else e=n.apply(this,arguments);return(0,$.default)(this,e)});function o(){var e;return(0,F.default)(this,o),(e=n.apply(this,arguments)).type="circle",e}return(0,U.default)(o,[{key:"path",value:function(e){var t=this.getAttribute("cx").getPixels("x"),r=this.getAttribute("cy").getPixels("y"),n=this.getAttribute("r").getPixels();return e&&n>0&&(e.beginPath(),e.arc(t,r,n,0,2*Math.PI,!1),e.closePath()),new Ot(t-n,r-n,t+n,r+n)}},{key:"getMarkers",value:function(){return null}}]),o}(zt);var Xt=function(e){(0,Z.default)(o,e);var t,r,n=(t=o,r=function(){if("undefined"==typeof Reflect||!Y())return!1;if(Y().sham)return!1;if("function"==typeof Proxy)return!0;try{return Date.prototype.toString.call(Y()(Date,[],(function(){}))),!0}catch(e){return!1}}(),function(){var e,n=(0,ee.default)(t);if(r){var o=(0,ee.default)(this).constructor;e=Y()(n,arguments,o)}else e=n.apply(this,arguments);return(0,$.default)(this,e)});function o(){var e;return(0,F.default)(this,o),(e=n.apply(this,arguments)).type="ellipse",e}return(0,U.default)(o,[{key:"path",value:function(e){var t=(Math.sqrt(2)-1)/3*4,r=this.getAttribute("rx").getPixels("x"),n=this.getAttribute("ry").getPixels("y"),o=this.getAttribute("cx").getPixels("x"),s=this.getAttribute("cy").getPixels("y");return e&&r>0&&n>0&&(e.beginPath(),e.moveTo(o+r,s),e.bezierCurveTo(o+r,s+t*n,o+t*r,s+n,o,s+n),e.bezierCurveTo(o-t*r,s+n,o-r,s+t*n,o-r,s),e.bezierCurveTo(o-r,s-t*n,o-t*r,s-n,o,s-n),e.bezierCurveTo(o+t*r,s-n,o+r,s-t*n,o+r,s),e.closePath()),new Ot(o-r,s-n,o+r,s+n)}},{key:"getMarkers",value:function(){return null}}]),o}(zt);var Wt=function(e){(0,Z.default)(o,e);var t,r,n=(t=o,r=function(){if("undefined"==typeof Reflect||!Y())return!1;if(Y().sham)return!1;if("function"==typeof Proxy)return!0;try{return Date.prototype.toString.call(Y()(Date,[],(function(){}))),!0}catch(e){return!1}}(),function(){var e,n=(0,ee.default)(t);if(r){var o=(0,ee.default)(this).constructor;e=Y()(n,arguments,o)}else e=n.apply(this,arguments);return(0,$.default)(this,e)});function o(){var e;return(0,F.default)(this,o),(e=n.apply(this,arguments)).type="line",e}return(0,U.default)(o,[{key:"getPoints",value:function(){return[new mt(this.getAttribute("x1").getPixels("x"),this.getAttribute("y1").getPixels("y")),new mt(this.getAttribute("x2").getPixels("x"),this.getAttribute("y2").getPixels("y"))]}},{key:"path",value:function(e){var t=this.getPoints(),r=(0,u.default)(t,2),n=r[0],o=n.x,s=n.y,i=r[1],a=i.x,A=i.y;return e&&(e.beginPath(),e.moveTo(o,s),e.lineTo(a,A)),new Ot(o,s,a,A)}},{key:"getMarkers",value:function(){var e=this.getPoints(),t=(0,u.default)(e,2),r=t[0],n=t[1],o=r.angleTo(n);return[[r,o],[n,o]]}}]),o}(zt);var Jt=function(e){(0,Z.default)(o,e);var t,r,n=(t=o,r=function(){if("undefined"==typeof Reflect||!Y())return!1;if(Y().sham)return!1;if("function"==typeof Proxy)return!0;try{return Date.prototype.toString.call(Y()(Date,[],(function(){}))),!0}catch(e){return!1}}(),function(){var e,n=(0,ee.default)(t);if(r){var o=(0,ee.default)(this).constructor;e=Y()(n,arguments,o)}else e=n.apply(this,arguments);return(0,$.default)(this,e)});function o(e,t,r){var s;return(0,F.default)(this,o),(s=n.call(this,e,t,r)).type="polyline",s.points=[],s.points=mt.parsePath(s.getAttribute("points").getString()),s}return(0,U.default)(o,[{key:"path",value:function(e){var t=this.points,r=(0,u.default)(t,1)[0],n=r.x,o=r.y,s=new Ot(n,o);return e&&(e.beginPath(),e.moveTo(n,o)),g()(t).call(t,(function(t){var r=t.x,n=t.y;s.addPoint(r,n),e&&e.lineTo(r,n)})),s}},{key:"getMarkers",value:function(){var e=this.points,t=e.length-1,r=[];return g()(e).call(e,(function(n,o){o!==t&&r.push([n,n.angleTo(e[o+1])])})),r.length>0&&r.push([e[e.length-1],r[r.length-1][1]]),r}}]),o}(zt);var Yt=function(e){(0,Z.default)(o,e);var t,r,n=(t=o,r=function(){if("undefined"==typeof Reflect||!Y())return!1;if(Y().sham)return!1;if("function"==typeof Proxy)return!0;try{return Date.prototype.toString.call(Y()(Date,[],(function(){}))),!0}catch(e){return!1}}(),function(){var e,n=(0,ee.default)(t);if(r){var o=(0,ee.default)(this).constructor;e=Y()(n,arguments,o)}else e=n.apply(this,arguments);return(0,$.default)(this,e)});function o(){var e;return(0,F.default)(this,o),(e=n.apply(this,arguments)).type="polygon",e}return(0,U.default)(o,[{key:"path",value:function(e){var t=(0,de.default)((0,ee.default)(o.prototype),"path",this).call(this,e),r=(0,u.default)(this.points,1)[0],n=r.x,s=r.y;return e&&(e.lineTo(n,s),e.closePath()),t}}]),o}(Jt);var Zt=function(e){(0,Z.default)(o,e);var t,r,n=(t=o,r=function(){if("undefined"==typeof Reflect||!Y())return!1;if(Y().sham)return!1;if("function"==typeof Proxy)return!0;try{return Date.prototype.toString.call(Y()(Date,[],(function(){}))),!0}catch(e){return!1}}(),function(){var e,n=(0,ee.default)(t);if(r){var o=(0,ee.default)(this).constructor;e=Y()(n,arguments,o)}else e=n.apply(this,arguments);return(0,$.default)(this,e)});function o(){var e;return(0,F.default)(this,o),(e=n.apply(this,arguments)).type="pattern",e}return(0,U.default)(o,[{key:"createPattern",value:function(e,t,r){var n=this.getStyle("width").getPixels("x",!0),o=this.getStyle("height").getPixels("y",!0),s=new Vt(this.document,null);s.attributes.viewBox=new ht(this.document,"viewBox",this.getAttribute("viewBox").getValue()),s.attributes.width=new ht(this.document,"width","".concat(n,"px")),s.attributes.height=new ht(this.document,"height","".concat(o,"px")),s.attributes.transform=new ht(this.document,"transform",this.getAttribute("patternTransform").getValue()),s.children=this.children;var i=this.document.createCanvas(n,o),a=i.getContext("2d"),A=this.getAttribute("x"),u=this.getAttribute("y");A.hasValue()&&u.hasValue()&&a.translate(A.getPixels("x",!0),u.getPixels("y",!0)),r.hasValue()?this.styles["fill-opacity"]=r:ge()(this.styles,"fill-opacity");for(var c=-1;c<=1;c++)for(var l=-1;l<=1;l++)a.save(),s.attributes.x=new ht(this.document,"x",c*i.width),s.attributes.y=new ht(this.document,"y",l*i.height),s.render(a),a.restore();return e.createPattern(i,"repeat")}}]),o}(St);var $t=function(e){(0,Z.default)(o,e);var t,r,n=(t=o,r=function(){if("undefined"==typeof Reflect||!Y())return!1;if(Y().sham)return!1;if("function"==typeof Proxy)return!0;try{return Date.prototype.toString.call(Y()(Date,[],(function(){}))),!0}catch(e){return!1}}(),function(){var e,n=(0,ee.default)(t);if(r){var o=(0,ee.default)(this).constructor;e=Y()(n,arguments,o)}else e=n.apply(this,arguments);return(0,$.default)(this,e)});function o(){var e;return(0,F.default)(this,o),(e=n.apply(this,arguments)).type="marker",e}return(0,U.default)(o,[{key:"render",value:function(e,t,r){if(t){var n=t.x,o=t.y,s=this.getAttribute("orient").getValue("auto"),i=this.getAttribute("markerUnits").getValue("strokeWidth");e.translate(n,o),"auto"===s&&e.rotate(r),"strokeWidth"===i&&e.scale(e.lineWidth,e.lineWidth),e.save();var a=new Vt(this.document,null);a.type=this.type,a.attributes.viewBox=new ht(this.document,"viewBox",this.getAttribute("viewBox").getValue()),a.attributes.refX=new ht(this.document,"refX",this.getAttribute("refX").getValue()),a.attributes.refY=new ht(this.document,"refY",this.getAttribute("refY").getValue()),a.attributes.width=new ht(this.document,"width",this.getAttribute("markerWidth").getValue()),a.attributes.height=new ht(this.document,"height",this.getAttribute("markerHeight").getValue()),a.attributes.overflow=new ht(this.document,"overflow",this.getAttribute("overflow").getValue()),a.attributes.fill=new ht(this.document,"fill",this.getAttribute("fill").getColor("black")),a.attributes.stroke=new ht(this.document,"stroke",this.getAttribute("stroke").getValue("none")),a.children=this.children,a.render(e),e.restore(),"strokeWidth"===i&&e.scale(1/e.lineWidth,1/e.lineWidth),"auto"===s&&e.rotate(-r),e.translate(-n,-o)}}}]),o}(St);var er=function(e){(0,Z.default)(o,e);var t,r,n=(t=o,r=function(){if("undefined"==typeof Reflect||!Y())return!1;if(Y().sham)return!1;if("function"==typeof Proxy)return!0;try{return Date.prototype.toString.call(Y()(Date,[],(function(){}))),!0}catch(e){return!1}}(),function(){var e,n=(0,ee.default)(t);if(r){var o=(0,ee.default)(this).constructor;e=Y()(n,arguments,o)}else e=n.apply(this,arguments);return(0,$.default)(this,e)});function o(){var e;return(0,F.default)(this,o),(e=n.apply(this,arguments)).type="defs",e}return(0,U.default)(o,[{key:"render",value:function(){}}]),o}(St);var tr=function(e){(0,Z.default)(o,e);var t,r,n=(t=o,r=function(){if("undefined"==typeof Reflect||!Y())return!1;if(Y().sham)return!1;if("function"==typeof Proxy)return!0;try{return Date.prototype.toString.call(Y()(Date,[],(function(){}))),!0}catch(e){return!1}}(),function(){var e,n=(0,ee.default)(t);if(r){var o=(0,ee.default)(this).constructor;e=Y()(n,arguments,o)}else e=n.apply(this,arguments);return(0,$.default)(this,e)});function o(){var e;return(0,F.default)(this,o),(e=n.apply(this,arguments)).type="g",e}return(0,U.default)(o,[{key:"getBoundingBox",value:function(e){var t,r=new Ot;return g()(t=this.children).call(t,(function(t){r.addBoundingBox(t.getBoundingBox(e))})),r}}]),o}(kt);var rr=function(e){(0,Z.default)(o,e);var t,r,n=(t=o,r=function(){if("undefined"==typeof Reflect||!Y())return!1;if(Y().sham)return!1;if("function"==typeof Proxy)return!0;try{return Date.prototype.toString.call(Y()(Date,[],(function(){}))),!0}catch(e){return!1}}(),function(){var e,n=(0,ee.default)(t);if(r){var o=(0,ee.default)(this).constructor;e=Y()(n,arguments,o)}else e=n.apply(this,arguments);return(0,$.default)(this,e)});function o(e,t,r){var s;(0,F.default)(this,o),(s=n.call(this,e,t,r)).attributesToInherit=["gradientUnits"],s.stops=[];var i=(0,ye.default)(s),a=i.stops,A=i.children;return g()(A).call(A,(function(e){"stop"===e.type&&a.push(e)})),s}return(0,U.default)(o,[{key:"getGradientUnits",value:function(){return this.getAttribute("gradientUnits").getString("objectBoundingBox")}},{key:"createGradient",value:function(e,t,r){var n=this,o=this;this.getHrefAttribute().hasValue()&&(o=this.getHrefAttribute().getDefinition(),this.inheritStopContainer(o));var s=o.stops,i=this.getGradient(e,t);if(!i)return this.addParentOpacity(r,s[s.length-1].color);if(g()(s).call(s,(function(e){i.addColorStop(e.offset,n.addParentOpacity(r,e.color))})),this.getAttribute("gradientTransform").hasValue()){var a=this.document,A=a.screen,c=A.MAX_VIRTUAL_PIXELS,l=A.viewPort,d=(0,u.default)(l.viewPorts,1)[0],f=new qt(a,null);f.attributes.x=new ht(a,"x",-c/3),f.attributes.y=new ht(a,"y",-c/3),f.attributes.width=new ht(a,"width",c),f.attributes.height=new ht(a,"height",c);var h=new tr(a,null);h.attributes.transform=new ht(a,"transform",this.getAttribute("gradientTransform").getValue()),h.children=[f];var p=new Vt(a,null);p.attributes.x=new ht(a,"x",0),p.attributes.y=new ht(a,"y",0),p.attributes.width=new ht(a,"width",d.width),p.attributes.height=new ht(a,"height",d.height),p.children=[h];var m=a.createCanvas(d.width,d.height),y=m.getContext("2d");return y.fillStyle=i,p.render(y),y.createPattern(m,"no-repeat")}return i}},{key:"inheritStopContainer",value:function(e){var t,r=this;g()(t=this.attributesToInherit).call(t,(function(t){!r.getAttribute(t).hasValue()&&e.getAttribute(t).hasValue()&&r.getAttribute(t,!0).setValue(e.getAttribute(t).getValue())}))}},{key:"addParentOpacity",value:function(e,t){return e.hasValue()?new ht(this.document,"color",t).addOpacity(e).getColor():t}}]),o}(St);var nr=function(e){(0,Z.default)(o,e);var t,r,n=(t=o,r=function(){if("undefined"==typeof Reflect||!Y())return!1;if(Y().sham)return!1;if("function"==typeof Proxy)return!0;try{return Date.prototype.toString.call(Y()(Date,[],(function(){}))),!0}catch(e){return!1}}(),function(){var e,n=(0,ee.default)(t);if(r){var o=(0,ee.default)(this).constructor;e=Y()(n,arguments,o)}else e=n.apply(this,arguments);return(0,$.default)(this,e)});function o(e,t,r){var s;return(0,F.default)(this,o),(s=n.call(this,e,t,r)).type="linearGradient",s.attributesToInherit.push("x1","y1","x2","y2"),s}return(0,U.default)(o,[{key:"getGradient",value:function(e,t){var r="objectBoundingBox"===this.getGradientUnits(),n=r?t.getBoundingBox(e):null;if(r&&!n)return null;this.getAttribute("x1").hasValue()||this.getAttribute("y1").hasValue()||this.getAttribute("x2").hasValue()||this.getAttribute("y2").hasValue()||(this.getAttribute("x1",!0).setValue(0),this.getAttribute("y1",!0).setValue(0),this.getAttribute("x2",!0).setValue(1),this.getAttribute("y2",!0).setValue(0));var o=r?n.x+n.width*this.getAttribute("x1").getNumber():this.getAttribute("x1").getPixels("x"),s=r?n.y+n.height*this.getAttribute("y1").getNumber():this.getAttribute("y1").getPixels("y"),i=r?n.x+n.width*this.getAttribute("x2").getNumber():this.getAttribute("x2").getPixels("x"),a=r?n.y+n.height*this.getAttribute("y2").getNumber():this.getAttribute("y2").getPixels("y");return o===i&&s===a?null:e.createLinearGradient(o,s,i,a)}}]),o}(rr);var or=function(e){(0,Z.default)(o,e);var t,r,n=(t=o,r=function(){if("undefined"==typeof Reflect||!Y())return!1;if(Y().sham)return!1;if("function"==typeof Proxy)return!0;try{return Date.prototype.toString.call(Y()(Date,[],(function(){}))),!0}catch(e){return!1}}(),function(){var e,n=(0,ee.default)(t);if(r){var o=(0,ee.default)(this).constructor;e=Y()(n,arguments,o)}else e=n.apply(this,arguments);return(0,$.default)(this,e)});function o(e,t,r){var s;return(0,F.default)(this,o),(s=n.call(this,e,t,r)).type="radialGradient",s.attributesToInherit.push("cx","cy","r","fx","fy","fr"),s}return(0,U.default)(o,[{key:"getGradient",value:function(e,t){var r="objectBoundingBox"===this.getGradientUnits(),n=t.getBoundingBox(e);if(r&&!n)return null;this.getAttribute("cx").hasValue()||this.getAttribute("cx",!0).setValue("50%"),this.getAttribute("cy").hasValue()||this.getAttribute("cy",!0).setValue("50%"),this.getAttribute("r").hasValue()||this.getAttribute("r",!0).setValue("50%");var o=r?n.x+n.width*this.getAttribute("cx").getNumber():this.getAttribute("cx").getPixels("x"),s=r?n.y+n.height*this.getAttribute("cy").getNumber():this.getAttribute("cy").getPixels("y"),i=o,a=s;this.getAttribute("fx").hasValue()&&(i=r?n.x+n.width*this.getAttribute("fx").getNumber():this.getAttribute("fx").getPixels("x")),this.getAttribute("fy").hasValue()&&(a=r?n.y+n.height*this.getAttribute("fy").getNumber():this.getAttribute("fy").getPixels("y"));var A=r?(n.width+n.height)/2*this.getAttribute("r").getNumber():this.getAttribute("r").getPixels(),u=this.getAttribute("fr").getPixels();return e.createRadialGradient(i,a,u,o,s,A)}}]),o}(rr);var sr=function(e){(0,Z.default)(o,e);var t,r,n=(t=o,r=function(){if("undefined"==typeof Reflect||!Y())return!1;if(Y().sham)return!1;if("function"==typeof Proxy)return!0;try{return Date.prototype.toString.call(Y()(Date,[],(function(){}))),!0}catch(e){return!1}}(),function(){var e,n=(0,ee.default)(t);if(r){var o=(0,ee.default)(this).constructor;e=Y()(n,arguments,o)}else e=n.apply(this,arguments);return(0,$.default)(this,e)});function o(e,t,r){var s;(0,F.default)(this,o),(s=n.call(this,e,t,r)).type="stop";var i=Math.max(0,Math.min(1,s.getAttribute("offset").getNumber())),a=s.getStyle("stop-opacity"),A=s.getStyle("stop-color",!0);return""===A.getString()&&A.setValue("#000"),a.hasValue()&&(A=A.addOpacity(a)),s.offset=i,s.color=A.getColor(),s}return o}(St);var ir=function(e){(0,Z.default)(o,e);var t,r,n=(t=o,r=function(){if("undefined"==typeof Reflect||!Y())return!1;if(Y().sham)return!1;if("function"==typeof Proxy)return!0;try{return Date.prototype.toString.call(Y()(Date,[],(function(){}))),!0}catch(e){return!1}}(),function(){var e,n=(0,ee.default)(t);if(r){var o=(0,ee.default)(this).constructor;e=Y()(n,arguments,o)}else e=n.apply(this,arguments);return(0,$.default)(this,e)});function o(e,t,r){var s;return(0,F.default)(this,o),(s=n.call(this,e,t,r)).type="animate",s.duration=0,s.initialValue=null,s.initialUnits="",s.removed=!1,s.frozen=!1,e.screen.animations.push((0,ye.default)(s)),s.begin=s.getAttribute("begin").getMilliseconds(),s.maxDuration=s.begin+s.getAttribute("dur").getMilliseconds(),s.from=s.getAttribute("from"),s.to=s.getAttribute("to"),s.values=s.getAttribute("values"),we()(s).hasValue()&&we()(s).setValue(we()(s).getString().split(";")),s}return(0,U.default)(o,[{key:"getProperty",value:function(){var e=this.getAttribute("attributeType").getString(),t=this.getAttribute("attributeName").getString();return"CSS"===e?this.parent.getStyle(t,!0):this.parent.getAttribute(t,!0)}},{key:"calcValue",value:function(){var e,t=this.initialUnits,r=this.getProgress(),n=r.progress,o=r.from,s=r.to,i=o.getNumber()+(s.getNumber()-o.getNumber())*n;return"%"===t&&(i*=100),L()(e="".concat(i)).call(e,t)}},{key:"update",value:function(e){var t=this.parent,r=this.getProperty();if(this.initialValue||(this.initialValue=r.getString(),this.initialUnits=r.getUnits()),this.duration>this.maxDuration){var n=this.getAttribute("fill").getString("remove");if("indefinite"===this.getAttribute("repeatCount").getString()||"indefinite"===this.getAttribute("repeatDur").getString())this.duration=0;else if("freeze"!==n||this.frozen){if("remove"===n&&!this.removed)return this.removed=!0,r.setValue(t.animationFrozen?t.animationFrozenValue:this.initialValue),!0}else this.frozen=!0,t.animationFrozen=!0,t.animationFrozenValue=r.getString();return!1}this.duration+=e;var o=!1;if(this.begine.length)&&(t=e.length);for(var r=0,n=new Array(t);r=e.length?{done:!0}:{done:!1,value:e[n++]}},e:function(e){throw e},f:o}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var s,i=!0,a=!1;return{s:function(){r=_e()(e)},n:function(){var e=r.next();return i=e.done,e},e:function(e){a=!0,s=e},f:function(){try{i||null==r.return||r.return()}finally{if(a)throw s}}}}((0,ye.default)(s).children);try{for(A.s();!(i=A.n()).done;){var u=i.value;switch(u.type){case"font-face":s.fontFace=u;var c=u.getStyle("font-family");c.hasValue()&&(a[c.getString()]=(0,ye.default)(s));break;case"missing-glyph":s.missingGlyph=u;break;case"glyph":var l=u;l.arabicForm?(s.isRTL=!0,s.isArabic=!0,void 0===s.glyphs[l.unicode]&&(s.glyphs[l.unicode]={}),s.glyphs[l.unicode][l.arabicForm]=l):s.glyphs[l.unicode]=l}}}catch(e){A.e(e)}finally{A.f()}return s}return(0,U.default)(o,[{key:"render",value:function(){}}]),o}(St);var lr=function(e){(0,Z.default)(o,e);var t,r,n=(t=o,r=function(){if("undefined"==typeof Reflect||!Y())return!1;if(Y().sham)return!1;if("function"==typeof Proxy)return!0;try{return Date.prototype.toString.call(Y()(Date,[],(function(){}))),!0}catch(e){return!1}}(),function(){var e,n=(0,ee.default)(t);if(r){var o=(0,ee.default)(this).constructor;e=Y()(n,arguments,o)}else e=n.apply(this,arguments);return(0,$.default)(this,e)});function o(e,t,r){var s;return(0,F.default)(this,o),(s=n.call(this,e,t,r)).type="font-face",s.ascent=s.getAttribute("ascent").getNumber(),s.descent=s.getAttribute("descent").getNumber(),s.unitsPerEm=s.getAttribute("units-per-em").getNumber(),s}return o}(St);var dr=function(e){(0,Z.default)(o,e);var t,r,n=(t=o,r=function(){if("undefined"==typeof Reflect||!Y())return!1;if(Y().sham)return!1;if("function"==typeof Proxy)return!0;try{return Date.prototype.toString.call(Y()(Date,[],(function(){}))),!0}catch(e){return!1}}(),function(){var e,n=(0,ee.default)(t);if(r){var o=(0,ee.default)(this).constructor;e=Y()(n,arguments,o)}else e=n.apply(this,arguments);return(0,$.default)(this,e)});function o(){var e;return(0,F.default)(this,o),(e=n.apply(this,arguments)).type="missing-glyph",e.horizAdvX=0,e}return o}(zt);var fr=function(e){(0,Z.default)(o,e);var t,r,n=(t=o,r=function(){if("undefined"==typeof Reflect||!Y())return!1;if(Y().sham)return!1;if("function"==typeof Proxy)return!0;try{return Date.prototype.toString.call(Y()(Date,[],(function(){}))),!0}catch(e){return!1}}(),function(){var e,n=(0,ee.default)(t);if(r){var o=(0,ee.default)(this).constructor;e=Y()(n,arguments,o)}else e=n.apply(this,arguments);return(0,$.default)(this,e)});function o(e,t,r){var s;return(0,F.default)(this,o),(s=n.call(this,e,t,r)).type="glyph",s.horizAdvX=s.getAttribute("horiz-adv-x").getNumber(),s.unicode=s.getAttribute("unicode").getString(),s.arabicForm=s.getAttribute("arabic-form").getString(),s}return o}(zt);var hr=function(e){(0,Z.default)(o,e);var t,r,n=(t=o,r=function(){if("undefined"==typeof Reflect||!Y())return!1;if(Y().sham)return!1;if("function"==typeof Proxy)return!0;try{return Date.prototype.toString.call(Y()(Date,[],(function(){}))),!0}catch(e){return!1}}(),function(){var e,n=(0,ee.default)(t);if(r){var o=(0,ee.default)(this).constructor;e=Y()(n,arguments,o)}else e=n.apply(this,arguments);return(0,$.default)(this,e)});function o(){var e;return(0,F.default)(this,o),(e=n.apply(this,arguments)).type="tref",e}return(0,U.default)(o,[{key:"getText",value:function(){var e=this.getHrefAttribute().getDefinition();if(e){var t=e.children[0];if(t)return t.getText()}return""}}]),o}(Rt);var pr=function(e){(0,Z.default)(o,e);var t,r,n=(t=o,r=function(){if("undefined"==typeof Reflect||!Y())return!1;if(Y().sham)return!1;if("function"==typeof Proxy)return!0;try{return Date.prototype.toString.call(Y()(Date,[],(function(){}))),!0}catch(e){return!1}}(),function(){var e,n=(0,ee.default)(t);if(r){var o=(0,ee.default)(this).constructor;e=Y()(n,arguments,o)}else e=n.apply(this,arguments);return(0,$.default)(this,e)});function o(e,t,r){var s,i;(0,F.default)(this,o),(i=n.call(this,e,t,r)).type="a";var a=t.childNodes,A=a[0],u=a.length>0&&k()(s=ae()(a)).call(s,(function(e){return 3===e.nodeType}));return i.hasText=u,i.text=u?i.getTextFromNode(A):"",i}return(0,U.default)(o,[{key:"getText",value:function(){return this.text}},{key:"renderChildren",value:function(e){if(this.hasText){(0,de.default)((0,ee.default)(o.prototype),"renderChildren",this).call(this,e);var t=this.document,r=this.x,n=this.y,s=t.screen.mouse,i=new ht(t,"fontSize",Pt.parse(t.ctx.font).fontSize);s.isWorking()&&s.checkBoundingBox(this,new Ot(r,n-i.getPixels("y"),r+this.measureText(e),n))}else if(this.children.length>0){var a=new tr(this.document,null);a.children=this.children,a.parent=this,a.render(e)}}},{key:"onClick",value:function(){var e=this.document.window;e&&e.open(this.getHrefAttribute().getString())}},{key:"onMouseMove",value:function(){this.document.ctx.canvas.style.cursor="pointer"}}]),o}(Rt);function mr(e,t){(null==t||t>e.length)&&(t=e.length);for(var r=0,n=new Array(t);rA?a:A,p=a>A?1:a/A,m=a>A?A/a:1;e&&(e.translate(s,i),e.rotate(d),e.scale(p,m),e.arc(0,0,h,c,c+l,Boolean(1-f)),e.scale(1/p,1/m),e.rotate(-d),e.translate(-s,-i));break;case Kt.CLOSE_PATH:e&&e.closePath()}}))}},{key:"renderChildren",value:function(e){this.setTextData(e),e.save();var t=this.parent.getStyle("text-decoration").getString(),r=this.getFontSize(),n=this.glyphInfo,o=e.fillStyle;"underline"===t&&e.beginPath(),g()(n).call(n,(function(n,o){var s=n.p0,i=n.p1,a=n.rotation,A=n.text;e.save(),e.translate(s.x,s.y),e.rotate(a),e.fillStyle&&e.fillText(A,0,0),e.strokeStyle&&e.strokeText(A,0,0),e.restore(),"underline"===t&&(0===o&&e.moveTo(s.x,s.y+r/8),e.lineTo(i.x,i.y+r/5))})),"underline"===t&&(e.lineWidth=r/20,e.strokeStyle=o,e.stroke(),e.closePath()),e.restore()}},{key:"getLetterSpacingAt",value:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:0;return this.letterSpacingCache[e]||0}},{key:"findSegmentToFitChar",value:function(e,t,r,n,o,s,i,a,A){var u=s,c=this.measureText(e,a);" "===a&&"justify"===t&&r-1&&(u+=this.getLetterSpacingAt(A));var l=this.textHeight/20,d=this.getEquidistantPointOnPath(u,l,0),f=this.getEquidistantPointOnPath(u+c,l,0),h={p0:d,p1:f},p=d&&f?Math.atan2(f.y-d.y,f.x-d.x):0;if(i){var m=Math.cos(Math.PI/2+p)*i,g=Math.cos(-p)*i;h.p0=yr(yr({},d),{},{x:d.x+m,y:d.y+g}),h.p1=yr(yr({},f),{},{x:f.x+m,y:f.y+g})}return{offset:u+=c,segment:h,rotation:p}}},{key:"measureText",value:function(e,t){var r=this.measuresCache,n=t||this.getText();if(r.has(n))return r.get(n);var o=this.measureTargetText(e,n);return r.set(n,o),o}},{key:"setTextData",value:function(e){var t,r=this;if(!this.glyphInfo){var n=this.getText(),o=n.split(""),s=n.split(" ").length-1,i=A()(t=this.parent.getAttribute("dx").split()).call(t,(function(e){return e.getPixels("x")})),a=this.parent.getAttribute("dy").getPixels("y"),u=this.parent.getStyle("text-anchor").getString("start"),c=this.getStyle("letter-spacing"),l=this.parent.getStyle("letter-spacing"),d=0;c.hasValue()&&"inherit"!==c.getValue()?c.hasValue()&&"initial"!==c.getValue()&&"unset"!==c.getValue()&&(d=c.getPixels()):d=l.getPixels();var f=[],h=n.length;this.letterSpacingCache=f;for(var p=0;p0&&(A-=2*Math.PI),1===o&&A<0&&(A+=2*Math.PI),[i.x,i.y,r,n,a,A,s,o]}},{key:"calcLength",value:function(e,t,r,n){var o=0,s=null,i=null,a=0;switch(r){case Kt.LINE_TO:return this.getLineLength(e,t,n[0],n[1]);case Kt.CURVE_TO:for(o=0,s=this.getPointOnCubicBezier(0,e,t,n[0],n[1],n[2],n[3],n[4],n[5]),a=.01;a<=1;a+=.01)i=this.getPointOnCubicBezier(a,e,t,n[0],n[1],n[2],n[3],n[4],n[5]),o+=this.getLineLength(s.x,s.y,i.x,i.y),s=i;return o;case Kt.QUAD_TO:for(o=0,s=this.getPointOnQuadraticBezier(0,e,t,n[0],n[1],n[2],n[3]),a=.01;a<=1;a+=.01)i=this.getPointOnQuadraticBezier(a,e,t,n[0],n[1],n[2],n[3]),o+=this.getLineLength(s.x,s.y,i.x,i.y),s=i;return o;case Kt.ARC:o=0;var A=n[4],u=n[5],c=n[4]+u,l=Math.PI/180;if(Math.abs(A-c)c;a-=l)i=this.getPointOnEllipticalArc(n[0],n[1],n[2],n[3],a,0),o+=this.getLineLength(s.x,s.y,i.x,i.y),s=i;else for(a=A+l;a5&&void 0!==arguments[5]?arguments[5]:t,i=arguments.length>6&&void 0!==arguments[6]?arguments[6]:r,a=(o-r)/(n-t+nt),A=Math.sqrt(e*e/(1+a*a));nt)return null;var o,s=function(e,t){var r;if(void 0===Fe()||null==Ne()(e)){if(xe()(e)||(r=function(e,t){var r;if(e){if("string"==typeof e)return mr(e,t);var n=Se()(r=Object.prototype.toString.call(e)).call(r,8,-1);return"Object"===n&&e.constructor&&(n=e.constructor.name),"Map"===n||"Set"===n?ae()(e):"Arguments"===n||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)?mr(e,t):void 0}}(e))||t&&e&&"number"==typeof e.length){r&&(e=r);var n=0,o=function(){};return{s:o,n:function(){return n>=e.length?{done:!0}:{done:!1,value:e[n++]}},e:function(e){throw e},f:o}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var s,i=!0,a=!1;return{s:function(){r=_e()(e)},n:function(){var e=r.next();return i=e.done,e},e:function(e){a=!0,s=e},f:function(){try{i||null==r.return||r.return()}finally{if(a)throw s}}}}(this.dataArray);try{for(s.s();!(o=s.n()).done;){var i=o.value;if(!i||!(i.pathLength<5e-5||r+i.pathLength+5e-5=0&&A>l)break;n=this.getPointOnEllipticalArc(i.points[0],i.points[1],i.points[2],i.points[3],A,i.points[6]);break;case Kt.CURVE_TO:(A=a/i.pathLength)>1&&(A=1),n=this.getPointOnCubicBezier(A,i.start.x,i.start.y,i.points[0],i.points[1],i.points[2],i.points[3],i.points[4],i.points[5]);break;case Kt.QUAD_TO:(A=a/i.pathLength)>1&&(A=1),n=this.getPointOnQuadraticBezier(A,i.start.x,i.start.y,i.points[0],i.points[1],i.points[2],i.points[3])}if(n)return n;break}r+=i.pathLength}}catch(e){s.e(e)}finally{s.f()}return null}},{key:"getLineLength",value:function(e,t,r,n){return Math.sqrt((r-e)*(r-e)+(n-t)*(n-t))}},{key:"getPathLength",value:function(){var e;return-1===this.pathLength&&(this.pathLength=H()(e=this.dataArray).call(e,(function(e,t){return t.pathLength>0?e+t.pathLength:e}),0)),this.pathLength}},{key:"getPointOnCubicBezier",value:function(e,t,r,n,o,s,i,a,A){return{x:a*at(e)+s*At(e)+n*ut(e)+t*ct(e),y:A*at(e)+i*At(e)+o*ut(e)+r*ct(e)}}},{key:"getPointOnQuadraticBezier",value:function(e,t,r,n,o,s,i){return{x:s*lt(e)+n*dt(e)+t*ft(e),y:i*lt(e)+o*dt(e)+r*ft(e)}}},{key:"getPointOnEllipticalArc",value:function(e,t,r,n,o,s){var i=Math.cos(s),a=Math.sin(s),A=r*Math.cos(o),u=n*Math.sin(o);return{x:e+(A*i-u*a),y:t+(A*a+u*i)}}},{key:"buildEquidistantCache",value:function(e,t){var r=this.getPathLength(),n=t||.25,o=e||r/100;if(!this.equidistantCache||this.equidistantCache.step!==o||this.equidistantCache.precision!==n){this.equidistantCache={step:o,precision:n,points:[]};for(var s=0,i=0;i<=r;i+=n){var a=this.getPointOnPath(i),A=this.getPointOnPath(i+n);a&&A&&(s+=this.getLineLength(a.x,a.y,A.x,A.y))>=o&&(this.equidistantCache.points.push({x:a.x,y:a.y,distance:i}),s-=o)}}}},{key:"getEquidistantPointOnPath",value:function(e,t,r){if(this.buildEquidistantCache(t,r),e<0||e-this.getPathLength()>5e-5)return null;var n=Math.round(e/this.getPathLength()*(this.equidistantCache.points.length-1));return this.equidistantCache.points[n]||null}}]),o}(Rt);var wr=function(e){(0,Z.default)(i,e);var t,r,n,o,s=(n=i,o=function(){if("undefined"==typeof Reflect||!Y())return!1;if(Y().sham)return!1;if("function"==typeof Proxy)return!0;try{return Date.prototype.toString.call(Y()(Date,[],(function(){}))),!0}catch(e){return!1}}(),function(){var e,t=(0,ee.default)(n);if(o){var r=(0,ee.default)(this).constructor;e=Y()(t,arguments,r)}else e=t.apply(this,arguments);return(0,$.default)(this,e)});function i(e,t,r){var n;(0,F.default)(this,i),(n=s.call(this,e,t,r)).type="image",n.loaded=!1;var o=n.getHrefAttribute().getString();if(!o)return(0,$.default)(n);var a=/\.svg$/.test(o);return e.images.push((0,ye.default)(n)),a?n.loadSvg(o):n.loadImage(o),n.isSvg=a,n}return(0,U.default)(i,[{key:"loadImage",value:(r=(0,N.default)(E().mark((function e(t){var r;return E().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return e.prev=0,e.next=3,this.document.createImage(t);case 3:r=e.sent,this.image=r,e.next=10;break;case 7:e.prev=7,e.t0=e.catch(0),console.error('Error while loading image "'.concat(t,'":'),e.t0);case 10:this.loaded=!0;case 11:case"end":return e.stop()}}),e,this,[[0,7]])}))),function(e){return r.apply(this,arguments)})},{key:"loadSvg",value:(t=(0,N.default)(E().mark((function e(t){var r,n;return E().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return e.prev=0,e.next=3,this.document.fetch(t);case 3:return r=e.sent,e.next=6,r.text();case 6:n=e.sent,this.image=n,e.next=13;break;case 10:e.prev=10,e.t0=e.catch(0),console.error('Error while loading image "'.concat(t,'":'),e.t0);case 13:this.loaded=!0;case 14:case"end":return e.stop()}}),e,this,[[0,10]])}))),function(e){return t.apply(this,arguments)})},{key:"renderChildren",value:function(e){var t=this.document,r=this.image,n=this.loaded,o=this.getAttribute("x").getPixels("x"),s=this.getAttribute("y").getPixels("y"),i=this.getStyle("width").getPixels("x"),a=this.getStyle("height").getPixels("y");if(n&&r&&i&&a){if(e.save(),this.isSvg)t.canvg.forkString(e,this.image,{ignoreMouse:!0,ignoreAnimation:!0,ignoreDimensions:!0,ignoreClear:!0,offsetX:o,offsetY:s,scaleWidth:i,scaleHeight:a}).render();else{var A=this.image;e.translate(o,s),t.setViewBox({ctx:e,aspectRatio:this.getAttribute("preserveAspectRatio").getString(),width:i,desiredWidth:A.width,height:a,desiredHeight:A.height}),this.loaded&&(void 0===A.complete||A.complete)&&e.drawImage(A,0,0)}e.restore()}}},{key:"getBoundingBox",value:function(){var e=this.getAttribute("x").getPixels("x"),t=this.getAttribute("y").getPixels("y"),r=this.getStyle("width").getPixels("x"),n=this.getStyle("height").getPixels("y");return new Ot(e,t,e+r,t+n)}}]),i}(kt);var br=function(e){(0,Z.default)(o,e);var t,r,n=(t=o,r=function(){if("undefined"==typeof Reflect||!Y())return!1;if(Y().sham)return!1;if("function"==typeof Proxy)return!0;try{return Date.prototype.toString.call(Y()(Date,[],(function(){}))),!0}catch(e){return!1}}(),function(){var e,n=(0,ee.default)(t);if(r){var o=(0,ee.default)(this).constructor;e=Y()(n,arguments,o)}else e=n.apply(this,arguments);return(0,$.default)(this,e)});function o(){var e;return(0,F.default)(this,o),(e=n.apply(this,arguments)).type="symbol",e}return(0,U.default)(o,[{key:"render",value:function(e){}}]),o}(kt),Br=function(){function e(t){(0,F.default)(this,e),this.document=t,this.loaded=!1,t.fonts.push(this)}var t;return(0,U.default)(e,[{key:"load",value:(t=(0,N.default)(E().mark((function e(t,r){var n,o,s,i;return E().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return e.prev=0,o=this.document,e.next=4,o.canvg.parser.load(r);case 4:s=e.sent,i=s.getElementsByTagName("font"),g()(n=ae()(i)).call(n,(function(e){var r=o.createElement(e);o.definitions[t]=r})),e.next=12;break;case 9:e.prev=9,e.t0=e.catch(0),console.error('Error while loading font "'.concat(r,'":'),e.t0);case 12:this.loaded=!0;case 13:case"end":return e.stop()}}),e,this,[[0,9]])}))),function(e,r){return t.apply(this,arguments)})}]),e}();var jr=function(e){(0,Z.default)(o,e);var t,r,n=(t=o,r=function(){if("undefined"==typeof Reflect||!Y())return!1;if(Y().sham)return!1;if("function"==typeof Proxy)return!0;try{return Date.prototype.toString.call(Y()(Date,[],(function(){}))),!0}catch(e){return!1}}(),function(){var e,n=(0,ee.default)(t);if(r){var o=(0,ee.default)(this).constructor;e=Y()(n,arguments,o)}else e=n.apply(this,arguments);return(0,$.default)(this,e)});function o(e,t,r){var s,i;(0,F.default)(this,o),(i=n.call(this,e,t,r)).type="style";var a=Re(A()(s=ae()(t.childNodes)).call(s,(function(e){return e.data})).join("").replace(/(\/\*([^*]|[\r\n]|(\*+([^*\/]|[\r\n])))*\*+\/)|(^[\s]*\/\/.*)/gm,"").replace(/@import.*;/g,"")).split("}");return g()(a).call(a,(function(t){var r=G()(t).call(t);if(r){var n=r.split("{"),o=n[0].split(","),s=n[1].split(";");g()(o).call(o,(function(t){var r=G()(t).call(t);if(r){var n=e.styles[r]||{};if(g()(s).call(s,(function(t){var r,o,s=le()(t).call(t,":"),i=G()(r=t.substr(0,s)).call(r),a=G()(o=t.substr(s+1,t.length-s)).call(o);i&&a&&(n[i]=new ht(e,i,a))})),e.styles[r]=n,e.stylesSpecificity[r]=rt(r),"@font-face"===r){var o=n["font-family"].getString().replace(/"|'/g,""),i=n.src.getString().split(",");g()(i).call(i,(function(t){if(le()(t).call(t,'format("svg")')>0){var r=qe(t);r&&new Br(e).load(o,r)}}))}}}))}})),i}return o}(St);jr.parseExternalUrl=qe;var _r=function(e){(0,Z.default)(o,e);var t,r,n=(t=o,r=function(){if("undefined"==typeof Reflect||!Y())return!1;if(Y().sham)return!1;if("function"==typeof Proxy)return!0;try{return Date.prototype.toString.call(Y()(Date,[],(function(){}))),!0}catch(e){return!1}}(),function(){var e,n=(0,ee.default)(t);if(r){var o=(0,ee.default)(this).constructor;e=Y()(n,arguments,o)}else e=n.apply(this,arguments);return(0,$.default)(this,e)});function o(){var e;return(0,F.default)(this,o),(e=n.apply(this,arguments)).type="use",e}return(0,U.default)(o,[{key:"setContext",value:function(e){(0,de.default)((0,ee.default)(o.prototype),"setContext",this).call(this,e);var t=this.getAttribute("x"),r=this.getAttribute("y");t.hasValue()&&e.translate(t.getPixels("x"),0),r.hasValue()&&e.translate(0,r.getPixels("y"))}},{key:"path",value:function(e){var t=this.element;t&&t.path(e)}},{key:"renderChildren",value:function(e){var t=this.document,r=this.element;if(r){var n=r;if("symbol"===r.type&&((n=new Vt(t,null)).attributes.viewBox=new ht(t,"viewBox",r.getAttribute("viewBox").getString()),n.attributes.preserveAspectRatio=new ht(t,"preserveAspectRatio",r.getAttribute("preserveAspectRatio").getString()),n.attributes.overflow=new ht(t,"overflow",r.getAttribute("overflow").getString()),n.children=r.children,r.styles.opacity=new ht(t,"opacity",this.calculateOpacity())),"svg"===n.type){var o=this.getStyle("width",!1,!0),s=this.getStyle("height",!1,!0);o.hasValue()&&(n.attributes.width=new ht(t,"width",o.getString())),s.hasValue()&&(n.attributes.height=new ht(t,"height",s.getString()))}var i=n.parent;n.parent=this,n.render(e),n.parent=i}}},{key:"getBoundingBox",value:function(e){var t=this.element;return t?t.getBoundingBox(e):null}},{key:"elementTransform",value:function(){var e=this.document,t=this.element;return Ut.fromElement(e,t)}},{key:"element",get:function(){return this._element||(this._element=this.getHrefAttribute().getDefinition()),this._element}}]),o}(kt);function Cr(e,t,r,n,o,s){return e[r*n*4+4*t+s]}function xr(e,t,r,n,o,s,i){e[r*n*4+4*t+s]=i}function Er(e,t,r){return e[t]*r}function Nr(e,t,r,n){return t+Math.cos(e)*r+Math.sin(e)*n}var Qr=function(e){(0,Z.default)(o,e);var t,r,n=(t=o,r=function(){if("undefined"==typeof Reflect||!Y())return!1;if(Y().sham)return!1;if("function"==typeof Proxy)return!0;try{return Date.prototype.toString.call(Y()(Date,[],(function(){}))),!0}catch(e){return!1}}(),function(){var e,n=(0,ee.default)(t);if(r){var o=(0,ee.default)(this).constructor;e=Y()(n,arguments,o)}else e=n.apply(this,arguments);return(0,$.default)(this,e)});function o(e,t,r){var s;(0,F.default)(this,o),(s=n.call(this,e,t,r)).type="feColorMatrix";var i=Ke(s.getAttribute("values").getString());switch(s.getAttribute("type").getString("matrix")){case"saturate":var a=i[0];i=[.213+.787*a,.715-.715*a,.072-.072*a,0,0,.213-.213*a,.715+.285*a,.072-.072*a,0,0,.213-.213*a,.715-.715*a,.072+.928*a,0,0,0,0,0,1,0,0,0,0,0,1];break;case"hueRotate":var A=i[0]*Math.PI/180;i=[Nr(A,.213,.787,-.213),Nr(A,.715,-.715,-.715),Nr(A,.072,-.072,.928),0,0,Nr(A,.213,-.213,.143),Nr(A,.715,.285,.14),Nr(A,.072,-.072,-.283),0,0,Nr(A,.213,-.213,-.787),Nr(A,.715,-.715,.715),Nr(A,.072,.928,.072),0,0,0,0,0,1,0,0,0,0,0,1];break;case"luminanceToAlpha":i=[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,.2125,.7154,.0721,0,0,0,0,0,0,1]}return s.matrix=i,s.includeOpacity=s.getAttribute("includeOpacity").hasValue(),s}return(0,U.default)(o,[{key:"apply",value:function(e,t,r,n,o){for(var s=this.includeOpacity,i=this.matrix,a=e.getImageData(0,0,n,o),A=0;A1&&void 0!==o[1]&&o[1],n=document.createElement("img"),r&&(n.crossOrigin="Anonymous"),e.abrupt("return",new(M())((function(e,r){n.onload=function(){e(n)},n.onerror=function(){r()},n.src=t})));case 4:case"end":return e.stop()}}),e)})))).apply(this,arguments)}var Kr=function(){function e(t){var r,n,o=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},s=o.rootEmSize,i=void 0===s?12:s,a=o.emSize,A=void 0===a?12:a,u=o.createCanvas,c=void 0===u?e.createCanvas:u,l=o.createImage,d=void 0===l?e.createImage:l,f=o.anonymousCrossOrigin;(0,F.default)(this,e),this.canvg=t,this.definitions={},this.styles={},this.stylesSpecificity={},this.images=[],this.fonts=[],this.emSizeStack=[],this.uniqueId=0,this.screen=t.screen,this.rootEmSize=i,this.emSize=A,this.createCanvas=c,this.createImage=this.bindCreateImage(d,f),this.screen.wait(K()(r=this.isImagesLoaded).call(r,this)),this.screen.wait(K()(n=this.isFontsLoaded).call(n,this))}return(0,U.default)(e,[{key:"bindCreateImage",value:function(e,t){return"boolean"==typeof t?function(r,n){return e(r,"boolean"==typeof n?n:t)}:e}},{key:"popEmSize",value:function(){this.emSizeStack.pop()}},{key:"getUniqueId",value:function(){return"canvg".concat(++this.uniqueId)}},{key:"isImagesLoaded",value:function(){var e;return k()(e=this.images).call(e,(function(e){return e.loaded}))}},{key:"isFontsLoaded",value:function(){var e;return k()(e=this.fonts).call(e,(function(e){return e.loaded}))}},{key:"createDocumentElement",value:function(e){var t=this.createElement(e.documentElement);return t.root=!0,t.addStylesFromStyleDefinition(),this.documentElement=t,t}},{key:"createElement",value:function(t){var r=t.nodeName.replace(/^[^:]+:/,""),n=e.elementTypes[r];return void 0!==n?new n(this,t):new Lt(this,t)}},{key:"createTextNode",value:function(e){return new Dt(this,e)}},{key:"setViewBox",value:function(e){this.screen.setViewBox(function(e){for(var t=1;t2&&void 0!==arguments[2]?arguments[2]:{};(0,F.default)(this,e),this.parser=new jt(n),this.screen=new wt(t,n),this.options=n;var o=new Kr(this,n),s=o.createDocumentElement(r);this.document=o,this.documentElement=s}var t,r;return(0,U.default)(e,[{key:"fork",value:function(t,r){var n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{};return e.from(t,r,Vr(Vr({},this.options),n))}},{key:"forkString",value:function(t,r){var n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{};return e.fromString(t,r,Vr(Vr({},this.options),n))}},{key:"ready",value:function(){return this.screen.ready()}},{key:"isReady",value:function(){return this.screen.isReady()}},{key:"render",value:(r=(0,N.default)(E().mark((function e(){var t,r=arguments;return E().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return t=r.length>0&&void 0!==r[0]?r[0]:{},this.start(Vr({enableRedraw:!0,ignoreAnimation:!0,ignoreMouse:!0},t)),e.next=4,this.ready();case 4:this.stop();case 5:case"end":return e.stop()}}),e,this)}))),function(){return r.apply(this,arguments)})},{key:"start",value:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},t=this.documentElement,r=this.screen,n=this.options;r.start(t,Vr(Vr({enableRedraw:!0},n),e))}},{key:"stop",value:function(){this.screen.stop()}},{key:"resize",value:function(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:e,r=arguments.length>2&&void 0!==arguments[2]&&arguments[2];this.documentElement.resize(e,t,r)}}],[{key:"from",value:(t=(0,N.default)(E().mark((function t(r,n){var o,s,i,a=arguments;return E().wrap((function(t){for(;;)switch(t.prev=t.next){case 0:return o=a.length>2&&void 0!==a[2]?a[2]:{},s=new jt(o),t.next=4,s.parse(n);case 4:return i=t.sent,t.abrupt("return",new e(r,i,o));case 6:case"end":return t.stop()}}),t)}))),function(e,r){return t.apply(this,arguments)})},{key:"fromString",value:function(t,r){var n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{},o=new jt(n),s=o.parseFromString(r);return new e(t,s,n)}}]),e}(),Gr=Object.freeze({__proto__:null,offscreen:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},t=e.DOMParser,r={window:null,ignoreAnimation:!0,ignoreMouse:!0,DOMParser:t,createCanvas:function(e,t){return new OffscreenCanvas(e,t)},createImage:function(e){return(0,N.default)(E().mark((function t(){var r,n,o;return E().wrap((function(t){for(;;)switch(t.prev=t.next){case 0:return t.next=2,fetch(e);case 2:return r=t.sent,t.next=5,r.blob();case 5:return n=t.sent,t.next=8,createImageBitmap(n);case 8:return o=t.sent,t.abrupt("return",o);case 10:case"end":return t.stop()}}),t)})))()}};return"undefined"==typeof DOMParser&&void 0!==t||ge()(r,"DOMParser"),r},node:function(e){var t=e.DOMParser,r=e.canvas;return{window:null,ignoreAnimation:!0,ignoreMouse:!0,DOMParser:t,fetch:e.fetch,createCanvas:r.createCanvas,createImage:r.loadImage}}});t.default=qr},"./node_modules/core-js-pure/es/array/from.js":function(e,t,r){r("./node_modules/core-js-pure/modules/es.string.iterator.js"),r("./node_modules/core-js-pure/modules/es.array.from.js");var n=r("./node_modules/core-js-pure/internals/path.js");e.exports=n.Array.from},"./node_modules/core-js-pure/es/array/is-array.js":function(e,t,r){r("./node_modules/core-js-pure/modules/es.array.is-array.js");var n=r("./node_modules/core-js-pure/internals/path.js");e.exports=n.Array.isArray},"./node_modules/core-js-pure/es/array/virtual/concat.js":function(e,t,r){r("./node_modules/core-js-pure/modules/es.array.concat.js");var n=r("./node_modules/core-js-pure/internals/entry-virtual.js");e.exports=n("Array").concat},"./node_modules/core-js-pure/es/array/virtual/every.js":function(e,t,r){r("./node_modules/core-js-pure/modules/es.array.every.js");var n=r("./node_modules/core-js-pure/internals/entry-virtual.js");e.exports=n("Array").every},"./node_modules/core-js-pure/es/array/virtual/fill.js":function(e,t,r){r("./node_modules/core-js-pure/modules/es.array.fill.js");var n=r("./node_modules/core-js-pure/internals/entry-virtual.js");e.exports=n("Array").fill},"./node_modules/core-js-pure/es/array/virtual/filter.js":function(e,t,r){r("./node_modules/core-js-pure/modules/es.array.filter.js");var n=r("./node_modules/core-js-pure/internals/entry-virtual.js");e.exports=n("Array").filter},"./node_modules/core-js-pure/es/array/virtual/for-each.js":function(e,t,r){r("./node_modules/core-js-pure/modules/es.array.for-each.js");var n=r("./node_modules/core-js-pure/internals/entry-virtual.js");e.exports=n("Array").forEach},"./node_modules/core-js-pure/es/array/virtual/includes.js":function(e,t,r){r("./node_modules/core-js-pure/modules/es.array.includes.js");var n=r("./node_modules/core-js-pure/internals/entry-virtual.js");e.exports=n("Array").includes},"./node_modules/core-js-pure/es/array/virtual/index-of.js":function(e,t,r){r("./node_modules/core-js-pure/modules/es.array.index-of.js");var n=r("./node_modules/core-js-pure/internals/entry-virtual.js");e.exports=n("Array").indexOf},"./node_modules/core-js-pure/es/array/virtual/map.js":function(e,t,r){r("./node_modules/core-js-pure/modules/es.array.map.js");var n=r("./node_modules/core-js-pure/internals/entry-virtual.js");e.exports=n("Array").map},"./node_modules/core-js-pure/es/array/virtual/reduce.js":function(e,t,r){r("./node_modules/core-js-pure/modules/es.array.reduce.js");var n=r("./node_modules/core-js-pure/internals/entry-virtual.js");e.exports=n("Array").reduce},"./node_modules/core-js-pure/es/array/virtual/reverse.js":function(e,t,r){r("./node_modules/core-js-pure/modules/es.array.reverse.js");var n=r("./node_modules/core-js-pure/internals/entry-virtual.js");e.exports=n("Array").reverse},"./node_modules/core-js-pure/es/array/virtual/slice.js":function(e,t,r){r("./node_modules/core-js-pure/modules/es.array.slice.js");var n=r("./node_modules/core-js-pure/internals/entry-virtual.js");e.exports=n("Array").slice},"./node_modules/core-js-pure/es/array/virtual/some.js":function(e,t,r){r("./node_modules/core-js-pure/modules/es.array.some.js");var n=r("./node_modules/core-js-pure/internals/entry-virtual.js");e.exports=n("Array").some},"./node_modules/core-js-pure/es/array/virtual/values.js":function(e,t,r){r("./node_modules/core-js-pure/modules/es.array.iterator.js");var n=r("./node_modules/core-js-pure/internals/entry-virtual.js");e.exports=n("Array").values},"./node_modules/core-js-pure/es/date/now.js":function(e,t,r){r("./node_modules/core-js-pure/modules/es.date.now.js");var n=r("./node_modules/core-js-pure/internals/path.js");e.exports=n.Date.now},"./node_modules/core-js-pure/es/function/virtual/bind.js":function(e,t,r){r("./node_modules/core-js-pure/modules/es.function.bind.js");var n=r("./node_modules/core-js-pure/internals/entry-virtual.js");e.exports=n("Function").bind},"./node_modules/core-js-pure/es/instance/bind.js":function(e,t,r){var n=r("./node_modules/core-js-pure/es/function/virtual/bind.js"),o=Function.prototype;e.exports=function(e){var t=e.bind;return e===o||e instanceof Function&&t===o.bind?n:t}},"./node_modules/core-js-pure/es/instance/concat.js":function(e,t,r){var n=r("./node_modules/core-js-pure/es/array/virtual/concat.js"),o=Array.prototype;e.exports=function(e){var t=e.concat;return e===o||e instanceof Array&&t===o.concat?n:t}},"./node_modules/core-js-pure/es/instance/every.js":function(e,t,r){var n=r("./node_modules/core-js-pure/es/array/virtual/every.js"),o=Array.prototype;e.exports=function(e){var t=e.every;return e===o||e instanceof Array&&t===o.every?n:t}},"./node_modules/core-js-pure/es/instance/fill.js":function(e,t,r){var n=r("./node_modules/core-js-pure/es/array/virtual/fill.js"),o=Array.prototype;e.exports=function(e){var t=e.fill;return e===o||e instanceof Array&&t===o.fill?n:t}},"./node_modules/core-js-pure/es/instance/filter.js":function(e,t,r){var n=r("./node_modules/core-js-pure/es/array/virtual/filter.js"),o=Array.prototype;e.exports=function(e){var t=e.filter;return e===o||e instanceof Array&&t===o.filter?n:t}},"./node_modules/core-js-pure/es/instance/includes.js":function(e,t,r){var n=r("./node_modules/core-js-pure/es/array/virtual/includes.js"),o=r("./node_modules/core-js-pure/es/string/virtual/includes.js"),s=Array.prototype,i=String.prototype;e.exports=function(e){var t=e.includes;return e===s||e instanceof Array&&t===s.includes?n:"string"==typeof e||e===i||e instanceof String&&t===i.includes?o:t}},"./node_modules/core-js-pure/es/instance/index-of.js":function(e,t,r){var n=r("./node_modules/core-js-pure/es/array/virtual/index-of.js"),o=Array.prototype;e.exports=function(e){var t=e.indexOf;return e===o||e instanceof Array&&t===o.indexOf?n:t}},"./node_modules/core-js-pure/es/instance/map.js":function(e,t,r){var n=r("./node_modules/core-js-pure/es/array/virtual/map.js"),o=Array.prototype;e.exports=function(e){var t=e.map;return e===o||e instanceof Array&&t===o.map?n:t}},"./node_modules/core-js-pure/es/instance/reduce.js":function(e,t,r){var n=r("./node_modules/core-js-pure/es/array/virtual/reduce.js"),o=Array.prototype;e.exports=function(e){var t=e.reduce;return e===o||e instanceof Array&&t===o.reduce?n:t}},"./node_modules/core-js-pure/es/instance/reverse.js":function(e,t,r){var n=r("./node_modules/core-js-pure/es/array/virtual/reverse.js"),o=Array.prototype;e.exports=function(e){var t=e.reverse;return e===o||e instanceof Array&&t===o.reverse?n:t}},"./node_modules/core-js-pure/es/instance/slice.js":function(e,t,r){var n=r("./node_modules/core-js-pure/es/array/virtual/slice.js"),o=Array.prototype;e.exports=function(e){var t=e.slice;return e===o||e instanceof Array&&t===o.slice?n:t}},"./node_modules/core-js-pure/es/instance/some.js":function(e,t,r){var n=r("./node_modules/core-js-pure/es/array/virtual/some.js"),o=Array.prototype;e.exports=function(e){var t=e.some;return e===o||e instanceof Array&&t===o.some?n:t}},"./node_modules/core-js-pure/es/instance/starts-with.js":function(e,t,r){var n=r("./node_modules/core-js-pure/es/string/virtual/starts-with.js"),o=String.prototype;e.exports=function(e){var t=e.startsWith;return"string"==typeof e||e===o||e instanceof String&&t===o.startsWith?n:t}},"./node_modules/core-js-pure/es/instance/trim.js":function(e,t,r){var n=r("./node_modules/core-js-pure/es/string/virtual/trim.js"),o=String.prototype;e.exports=function(e){var t=e.trim;return"string"==typeof e||e===o||e instanceof String&&t===o.trim?n:t}},"./node_modules/core-js-pure/es/map/index.js":function(e,t,r){r("./node_modules/core-js-pure/modules/es.map.js"),r("./node_modules/core-js-pure/modules/es.object.to-string.js"),r("./node_modules/core-js-pure/modules/es.string.iterator.js"),r("./node_modules/core-js-pure/modules/web.dom-collections.iterator.js");var n=r("./node_modules/core-js-pure/internals/path.js");e.exports=n.Map},"./node_modules/core-js-pure/es/object/create.js":function(e,t,r){r("./node_modules/core-js-pure/modules/es.object.create.js");var n=r("./node_modules/core-js-pure/internals/path.js").Object;e.exports=function(e,t){return n.create(e,t)}},"./node_modules/core-js-pure/es/object/define-properties.js":function(e,t,r){r("./node_modules/core-js-pure/modules/es.object.define-properties.js");var n=r("./node_modules/core-js-pure/internals/path.js").Object,o=e.exports=function(e,t){return n.defineProperties(e,t)};n.defineProperties.sham&&(o.sham=!0)},"./node_modules/core-js-pure/es/object/define-property.js":function(e,t,r){r("./node_modules/core-js-pure/modules/es.object.define-property.js");var n=r("./node_modules/core-js-pure/internals/path.js").Object,o=e.exports=function(e,t,r){return n.defineProperty(e,t,r)};n.defineProperty.sham&&(o.sham=!0)},"./node_modules/core-js-pure/es/object/get-own-property-descriptor.js":function(e,t,r){r("./node_modules/core-js-pure/modules/es.object.get-own-property-descriptor.js");var n=r("./node_modules/core-js-pure/internals/path.js").Object,o=e.exports=function(e,t){return n.getOwnPropertyDescriptor(e,t)};n.getOwnPropertyDescriptor.sham&&(o.sham=!0)},"./node_modules/core-js-pure/es/object/get-own-property-descriptors.js":function(e,t,r){r("./node_modules/core-js-pure/modules/es.object.get-own-property-descriptors.js");var n=r("./node_modules/core-js-pure/internals/path.js");e.exports=n.Object.getOwnPropertyDescriptors},"./node_modules/core-js-pure/es/object/get-own-property-symbols.js":function(e,t,r){r("./node_modules/core-js-pure/modules/es.symbol.js");var n=r("./node_modules/core-js-pure/internals/path.js");e.exports=n.Object.getOwnPropertySymbols},"./node_modules/core-js-pure/es/object/get-prototype-of.js":function(e,t,r){r("./node_modules/core-js-pure/modules/es.object.get-prototype-of.js");var n=r("./node_modules/core-js-pure/internals/path.js");e.exports=n.Object.getPrototypeOf},"./node_modules/core-js-pure/es/object/keys.js":function(e,t,r){r("./node_modules/core-js-pure/modules/es.object.keys.js");var n=r("./node_modules/core-js-pure/internals/path.js");e.exports=n.Object.keys},"./node_modules/core-js-pure/es/object/set-prototype-of.js":function(e,t,r){r("./node_modules/core-js-pure/modules/es.object.set-prototype-of.js");var n=r("./node_modules/core-js-pure/internals/path.js");e.exports=n.Object.setPrototypeOf},"./node_modules/core-js-pure/es/parse-float.js":function(e,t,r){r("./node_modules/core-js-pure/modules/es.parse-float.js");var n=r("./node_modules/core-js-pure/internals/path.js");e.exports=n.parseFloat},"./node_modules/core-js-pure/es/parse-int.js":function(e,t,r){r("./node_modules/core-js-pure/modules/es.parse-int.js");var n=r("./node_modules/core-js-pure/internals/path.js");e.exports=n.parseInt},"./node_modules/core-js-pure/es/promise/index.js":function(e,t,r){r("./node_modules/core-js-pure/modules/es.aggregate-error.js"),r("./node_modules/core-js-pure/modules/es.object.to-string.js"),r("./node_modules/core-js-pure/modules/es.promise.js"),r("./node_modules/core-js-pure/modules/es.promise.all-settled.js"),r("./node_modules/core-js-pure/modules/es.promise.any.js"),r("./node_modules/core-js-pure/modules/es.promise.finally.js"),r("./node_modules/core-js-pure/modules/es.string.iterator.js"),r("./node_modules/core-js-pure/modules/web.dom-collections.iterator.js");var n=r("./node_modules/core-js-pure/internals/path.js");e.exports=n.Promise},"./node_modules/core-js-pure/es/reflect/apply.js":function(e,t,r){r("./node_modules/core-js-pure/modules/es.reflect.apply.js");var n=r("./node_modules/core-js-pure/internals/path.js");e.exports=n.Reflect.apply},"./node_modules/core-js-pure/es/reflect/construct.js":function(e,t,r){r("./node_modules/core-js-pure/modules/es.reflect.construct.js");var n=r("./node_modules/core-js-pure/internals/path.js");e.exports=n.Reflect.construct},"./node_modules/core-js-pure/es/reflect/delete-property.js":function(e,t,r){r("./node_modules/core-js-pure/modules/es.reflect.delete-property.js");var n=r("./node_modules/core-js-pure/internals/path.js");e.exports=n.Reflect.deleteProperty},"./node_modules/core-js-pure/es/reflect/get-prototype-of.js":function(e,t,r){r("./node_modules/core-js-pure/modules/es.reflect.get-prototype-of.js");var n=r("./node_modules/core-js-pure/internals/path.js");e.exports=n.Reflect.getPrototypeOf},"./node_modules/core-js-pure/es/reflect/get.js":function(e,t,r){r("./node_modules/core-js-pure/modules/es.reflect.get.js");var n=r("./node_modules/core-js-pure/internals/path.js");e.exports=n.Reflect.get},"./node_modules/core-js-pure/es/string/virtual/includes.js":function(e,t,r){r("./node_modules/core-js-pure/modules/es.string.includes.js");var n=r("./node_modules/core-js-pure/internals/entry-virtual.js");e.exports=n("String").includes},"./node_modules/core-js-pure/es/string/virtual/starts-with.js":function(e,t,r){r("./node_modules/core-js-pure/modules/es.string.starts-with.js");var n=r("./node_modules/core-js-pure/internals/entry-virtual.js");e.exports=n("String").startsWith},"./node_modules/core-js-pure/es/string/virtual/trim.js":function(e,t,r){r("./node_modules/core-js-pure/modules/es.string.trim.js");var n=r("./node_modules/core-js-pure/internals/entry-virtual.js");e.exports=n("String").trim},"./node_modules/core-js-pure/es/symbol/index.js":function(e,t,r){r("./node_modules/core-js-pure/modules/es.array.concat.js"),r("./node_modules/core-js-pure/modules/es.object.to-string.js"),r("./node_modules/core-js-pure/modules/es.symbol.js"),r("./node_modules/core-js-pure/modules/es.symbol.async-iterator.js"),r("./node_modules/core-js-pure/modules/es.symbol.description.js"),r("./node_modules/core-js-pure/modules/es.symbol.has-instance.js"),r("./node_modules/core-js-pure/modules/es.symbol.is-concat-spreadable.js"),r("./node_modules/core-js-pure/modules/es.symbol.iterator.js"),r("./node_modules/core-js-pure/modules/es.symbol.match.js"),r("./node_modules/core-js-pure/modules/es.symbol.match-all.js"),r("./node_modules/core-js-pure/modules/es.symbol.replace.js"),r("./node_modules/core-js-pure/modules/es.symbol.search.js"),r("./node_modules/core-js-pure/modules/es.symbol.species.js"),r("./node_modules/core-js-pure/modules/es.symbol.split.js"),r("./node_modules/core-js-pure/modules/es.symbol.to-primitive.js"),r("./node_modules/core-js-pure/modules/es.symbol.to-string-tag.js"),r("./node_modules/core-js-pure/modules/es.symbol.unscopables.js"),r("./node_modules/core-js-pure/modules/es.json.to-string-tag.js"),r("./node_modules/core-js-pure/modules/es.math.to-string-tag.js"),r("./node_modules/core-js-pure/modules/es.reflect.to-string-tag.js");var n=r("./node_modules/core-js-pure/internals/path.js");e.exports=n.Symbol},"./node_modules/core-js-pure/es/symbol/iterator.js":function(e,t,r){r("./node_modules/core-js-pure/modules/es.symbol.iterator.js"),r("./node_modules/core-js-pure/modules/es.string.iterator.js"),r("./node_modules/core-js-pure/modules/web.dom-collections.iterator.js");var n=r("./node_modules/core-js-pure/internals/well-known-symbol-wrapped.js");e.exports=n.f("iterator")},"./node_modules/core-js-pure/features/array/from.js":function(e,t,r){var n=r("./node_modules/core-js-pure/es/array/from.js");e.exports=n},"./node_modules/core-js-pure/features/array/is-array.js":function(e,t,r){var n=r("./node_modules/core-js-pure/es/array/is-array.js");e.exports=n},"./node_modules/core-js-pure/features/get-iterator-method.js":function(e,t,r){r("./node_modules/core-js-pure/modules/web.dom-collections.iterator.js"),r("./node_modules/core-js-pure/modules/es.string.iterator.js");var n=r("./node_modules/core-js-pure/internals/get-iterator-method.js");e.exports=n},"./node_modules/core-js-pure/features/get-iterator.js":function(e,t,r){r("./node_modules/core-js-pure/modules/web.dom-collections.iterator.js"),r("./node_modules/core-js-pure/modules/es.string.iterator.js");var n=r("./node_modules/core-js-pure/internals/get-iterator.js");e.exports=n},"./node_modules/core-js-pure/features/instance/slice.js":function(e,t,r){var n=r("./node_modules/core-js-pure/es/instance/slice.js");e.exports=n},"./node_modules/core-js-pure/features/object/create.js":function(e,t,r){var n=r("./node_modules/core-js-pure/es/object/create.js");e.exports=n},"./node_modules/core-js-pure/features/object/define-property.js":function(e,t,r){var n=r("./node_modules/core-js-pure/es/object/define-property.js");e.exports=n},"./node_modules/core-js-pure/features/object/get-own-property-descriptor.js":function(e,t,r){var n=r("./node_modules/core-js-pure/es/object/get-own-property-descriptor.js");e.exports=n},"./node_modules/core-js-pure/features/object/get-prototype-of.js":function(e,t,r){var n=r("./node_modules/core-js-pure/es/object/get-prototype-of.js");e.exports=n},"./node_modules/core-js-pure/features/object/set-prototype-of.js":function(e,t,r){var n=r("./node_modules/core-js-pure/es/object/set-prototype-of.js");e.exports=n},"./node_modules/core-js-pure/features/promise/index.js":function(e,t,r){var n=r("./node_modules/core-js-pure/es/promise/index.js");r("./node_modules/core-js-pure/modules/esnext.aggregate-error.js"),r("./node_modules/core-js-pure/modules/esnext.promise.all-settled.js"),r("./node_modules/core-js-pure/modules/esnext.promise.try.js"),r("./node_modules/core-js-pure/modules/esnext.promise.any.js"),e.exports=n},"./node_modules/core-js-pure/features/reflect/get.js":function(e,t,r){var n=r("./node_modules/core-js-pure/es/reflect/get.js");e.exports=n},"./node_modules/core-js-pure/features/symbol/index.js":function(e,t,r){var n=r("./node_modules/core-js-pure/es/symbol/index.js");r("./node_modules/core-js-pure/modules/esnext.symbol.async-dispose.js"),r("./node_modules/core-js-pure/modules/esnext.symbol.dispose.js"),r("./node_modules/core-js-pure/modules/esnext.symbol.matcher.js"),r("./node_modules/core-js-pure/modules/esnext.symbol.metadata.js"),r("./node_modules/core-js-pure/modules/esnext.symbol.observable.js"),r("./node_modules/core-js-pure/modules/esnext.symbol.pattern-match.js"),r("./node_modules/core-js-pure/modules/esnext.symbol.replace-all.js"),e.exports=n},"./node_modules/core-js-pure/features/symbol/iterator.js":function(e,t,r){var n=r("./node_modules/core-js-pure/es/symbol/iterator.js");e.exports=n},"./node_modules/core-js-pure/internals/a-function.js":function(e){e.exports=function(e){if("function"!=typeof e)throw TypeError(String(e)+" is not a function");return e}},"./node_modules/core-js-pure/internals/a-possible-prototype.js":function(e,t,r){var n=r("./node_modules/core-js-pure/internals/is-object.js");e.exports=function(e){if(!n(e)&&null!==e)throw TypeError("Can't set "+String(e)+" as a prototype");return e}},"./node_modules/core-js-pure/internals/add-to-unscopables.js":function(e){e.exports=function(){}},"./node_modules/core-js-pure/internals/an-instance.js":function(e){e.exports=function(e,t,r){if(!(e instanceof t))throw TypeError("Incorrect "+(r?r+" ":"")+"invocation");return e}},"./node_modules/core-js-pure/internals/an-object.js":function(e,t,r){var n=r("./node_modules/core-js-pure/internals/is-object.js");e.exports=function(e){if(!n(e))throw TypeError(String(e)+" is not an object");return e}},"./node_modules/core-js-pure/internals/array-fill.js":function(e,t,r){"use strict";var n=r("./node_modules/core-js-pure/internals/to-object.js"),o=r("./node_modules/core-js-pure/internals/to-absolute-index.js"),s=r("./node_modules/core-js-pure/internals/to-length.js");e.exports=function(e){for(var t=n(this),r=s(t.length),i=arguments.length,a=o(i>1?arguments[1]:void 0,r),A=i>2?arguments[2]:void 0,u=void 0===A?r:o(A,r);u>a;)t[a++]=e;return t}},"./node_modules/core-js-pure/internals/array-for-each.js":function(e,t,r){"use strict";var n=r("./node_modules/core-js-pure/internals/array-iteration.js").forEach,o=r("./node_modules/core-js-pure/internals/array-method-is-strict.js")("forEach");e.exports=o?[].forEach:function(e){return n(this,e,arguments.length>1?arguments[1]:void 0)}},"./node_modules/core-js-pure/internals/array-from.js":function(e,t,r){"use strict";var n=r("./node_modules/core-js-pure/internals/function-bind-context.js"),o=r("./node_modules/core-js-pure/internals/to-object.js"),s=r("./node_modules/core-js-pure/internals/call-with-safe-iteration-closing.js"),i=r("./node_modules/core-js-pure/internals/is-array-iterator-method.js"),a=r("./node_modules/core-js-pure/internals/to-length.js"),A=r("./node_modules/core-js-pure/internals/create-property.js"),u=r("./node_modules/core-js-pure/internals/get-iterator-method.js");e.exports=function(e){var t,r,c,l,d,f,h=o(e),p="function"==typeof this?this:Array,m=arguments.length,g=m>1?arguments[1]:void 0,y=void 0!==g,v=u(h),w=0;if(y&&(g=n(g,m>2?arguments[2]:void 0,2)),null==v||p==Array&&i(v))for(r=new p(t=a(h.length));t>w;w++)f=y?g(h[w],w):h[w],A(r,w,f);else for(d=(l=v.call(h)).next,r=new p;!(c=d.call(l)).done;w++)f=y?s(l,g,[c.value,w],!0):c.value,A(r,w,f);return r.length=w,r}},"./node_modules/core-js-pure/internals/array-includes.js":function(e,t,r){var n=r("./node_modules/core-js-pure/internals/to-indexed-object.js"),o=r("./node_modules/core-js-pure/internals/to-length.js"),s=r("./node_modules/core-js-pure/internals/to-absolute-index.js"),i=function(e){return function(t,r,i){var a,A=n(t),u=o(A.length),c=s(i,u);if(e&&r!=r){for(;u>c;)if((a=A[c++])!=a)return!0}else for(;u>c;c++)if((e||c in A)&&A[c]===r)return e||c||0;return!e&&-1}};e.exports={includes:i(!0),indexOf:i(!1)}},"./node_modules/core-js-pure/internals/array-iteration.js":function(e,t,r){var n=r("./node_modules/core-js-pure/internals/function-bind-context.js"),o=r("./node_modules/core-js-pure/internals/indexed-object.js"),s=r("./node_modules/core-js-pure/internals/to-object.js"),i=r("./node_modules/core-js-pure/internals/to-length.js"),a=r("./node_modules/core-js-pure/internals/array-species-create.js"),A=[].push,u=function(e){var t=1==e,r=2==e,u=3==e,c=4==e,l=6==e,d=7==e,f=5==e||l;return function(h,p,m,g){for(var y,v,w=s(h),b=o(w),B=n(p,m,3),j=i(b.length),_=0,C=g||a,x=t?C(h,j):r||d?C(h,0):void 0;j>_;_++)if((f||_ in b)&&(v=B(y=b[_],_,w),e))if(t)x[_]=v;else if(v)switch(e){case 3:return!0;case 5:return y;case 6:return _;case 2:A.call(x,y)}else switch(e){case 4:return!1;case 7:A.call(x,y)}return l?-1:u||c?c:x}};e.exports={forEach:u(0),map:u(1),filter:u(2),some:u(3),every:u(4),find:u(5),findIndex:u(6),filterOut:u(7)}},"./node_modules/core-js-pure/internals/array-method-has-species-support.js":function(e,t,r){var n=r("./node_modules/core-js-pure/internals/fails.js"),o=r("./node_modules/core-js-pure/internals/well-known-symbol.js"),s=r("./node_modules/core-js-pure/internals/engine-v8-version.js"),i=o("species");e.exports=function(e){return s>=51||!n((function(){var t=[];return(t.constructor={})[i]=function(){return{foo:1}},1!==t[e](Boolean).foo}))}},"./node_modules/core-js-pure/internals/array-method-is-strict.js":function(e,t,r){"use strict";var n=r("./node_modules/core-js-pure/internals/fails.js");e.exports=function(e,t){var r=[][e];return!!r&&n((function(){r.call(null,t||function(){throw 1},1)}))}},"./node_modules/core-js-pure/internals/array-reduce.js":function(e,t,r){var n=r("./node_modules/core-js-pure/internals/a-function.js"),o=r("./node_modules/core-js-pure/internals/to-object.js"),s=r("./node_modules/core-js-pure/internals/indexed-object.js"),i=r("./node_modules/core-js-pure/internals/to-length.js"),a=function(e){return function(t,r,a,A){n(r);var u=o(t),c=s(u),l=i(u.length),d=e?l-1:0,f=e?-1:1;if(a<2)for(;;){if(d in c){A=c[d],d+=f;break}if(d+=f,e?d<0:l<=d)throw TypeError("Reduce of empty array with no initial value")}for(;e?d>=0:l>d;d+=f)d in c&&(A=r(A,c[d],d,u));return A}};e.exports={left:a(!1),right:a(!0)}},"./node_modules/core-js-pure/internals/array-species-create.js":function(e,t,r){var n=r("./node_modules/core-js-pure/internals/is-object.js"),o=r("./node_modules/core-js-pure/internals/is-array.js"),s=r("./node_modules/core-js-pure/internals/well-known-symbol.js")("species");e.exports=function(e,t){var r;return o(e)&&("function"!=typeof(r=e.constructor)||r!==Array&&!o(r.prototype)?n(r)&&null===(r=r[s])&&(r=void 0):r=void 0),new(void 0===r?Array:r)(0===t?0:t)}},"./node_modules/core-js-pure/internals/call-with-safe-iteration-closing.js":function(e,t,r){var n=r("./node_modules/core-js-pure/internals/an-object.js"),o=r("./node_modules/core-js-pure/internals/iterator-close.js");e.exports=function(e,t,r,s){try{return s?t(n(r)[0],r[1]):t(r)}catch(t){throw o(e),t}}},"./node_modules/core-js-pure/internals/check-correctness-of-iteration.js":function(e,t,r){var n=r("./node_modules/core-js-pure/internals/well-known-symbol.js")("iterator"),o=!1;try{var s=0,i={next:function(){return{done:!!s++}},return:function(){o=!0}};i[n]=function(){return this},Array.from(i,(function(){throw 2}))}catch(e){}e.exports=function(e,t){if(!t&&!o)return!1;var r=!1;try{var s={};s[n]=function(){return{next:function(){return{done:r=!0}}}},e(s)}catch(e){}return r}},"./node_modules/core-js-pure/internals/classof-raw.js":function(e){var t={}.toString;e.exports=function(e){return t.call(e).slice(8,-1)}},"./node_modules/core-js-pure/internals/classof.js":function(e,t,r){var n=r("./node_modules/core-js-pure/internals/to-string-tag-support.js"),o=r("./node_modules/core-js-pure/internals/classof-raw.js"),s=r("./node_modules/core-js-pure/internals/well-known-symbol.js")("toStringTag"),i="Arguments"==o(function(){return arguments}());e.exports=n?o:function(e){var t,r,n;return void 0===e?"Undefined":null===e?"Null":"string"==typeof(r=function(e,t){try{return e[t]}catch(e){}}(t=Object(e),s))?r:i?o(t):"Object"==(n=o(t))&&"function"==typeof t.callee?"Arguments":n}},"./node_modules/core-js-pure/internals/collection-strong.js":function(e,t,r){"use strict";var n=r("./node_modules/core-js-pure/internals/object-define-property.js").f,o=r("./node_modules/core-js-pure/internals/object-create.js"),s=r("./node_modules/core-js-pure/internals/redefine-all.js"),i=r("./node_modules/core-js-pure/internals/function-bind-context.js"),a=r("./node_modules/core-js-pure/internals/an-instance.js"),A=r("./node_modules/core-js-pure/internals/iterate.js"),u=r("./node_modules/core-js-pure/internals/define-iterator.js"),c=r("./node_modules/core-js-pure/internals/set-species.js"),l=r("./node_modules/core-js-pure/internals/descriptors.js"),d=r("./node_modules/core-js-pure/internals/internal-metadata.js").fastKey,f=r("./node_modules/core-js-pure/internals/internal-state.js"),h=f.set,p=f.getterFor;e.exports={getConstructor:function(e,t,r,u){var c=e((function(e,n){a(e,c,t),h(e,{type:t,index:o(null),first:void 0,last:void 0,size:0}),l||(e.size=0),null!=n&&A(n,e[u],{that:e,AS_ENTRIES:r})})),f=p(t),m=function(e,t,r){var n,o,s=f(e),i=g(e,t);return i?i.value=r:(s.last=i={index:o=d(t,!0),key:t,value:r,previous:n=s.last,next:void 0,removed:!1},s.first||(s.first=i),n&&(n.next=i),l?s.size++:e.size++,"F"!==o&&(s.index[o]=i)),e},g=function(e,t){var r,n=f(e),o=d(t);if("F"!==o)return n.index[o];for(r=n.first;r;r=r.next)if(r.key==t)return r};return s(c.prototype,{clear:function(){for(var e=f(this),t=e.index,r=e.first;r;)r.removed=!0,r.previous&&(r.previous=r.previous.next=void 0),delete t[r.index],r=r.next;e.first=e.last=void 0,l?e.size=0:this.size=0},delete:function(e){var t=this,r=f(t),n=g(t,e);if(n){var o=n.next,s=n.previous;delete r.index[n.index],n.removed=!0,s&&(s.next=o),o&&(o.previous=s),r.first==n&&(r.first=o),r.last==n&&(r.last=s),l?r.size--:t.size--}return!!n},forEach:function(e){for(var t,r=f(this),n=i(e,arguments.length>1?arguments[1]:void 0,3);t=t?t.next:r.first;)for(n(t.value,t.key,this);t&&t.removed;)t=t.previous},has:function(e){return!!g(this,e)}}),s(c.prototype,r?{get:function(e){var t=g(this,e);return t&&t.value},set:function(e,t){return m(this,0===e?0:e,t)}}:{add:function(e){return m(this,e=0===e?0:e,e)}}),l&&n(c.prototype,"size",{get:function(){return f(this).size}}),c},setStrong:function(e,t,r){var n=t+" Iterator",o=p(t),s=p(n);u(e,t,(function(e,t){h(this,{type:n,target:e,state:o(e),kind:t,last:void 0})}),(function(){for(var e=s(this),t=e.kind,r=e.last;r&&r.removed;)r=r.previous;return e.target&&(e.last=r=r?r.next:e.state.first)?"keys"==t?{value:r.key,done:!1}:"values"==t?{value:r.value,done:!1}:{value:[r.key,r.value],done:!1}:(e.target=void 0,{value:void 0,done:!0})}),r?"entries":"values",!r,!0),c(t)}}},"./node_modules/core-js-pure/internals/collection.js":function(e,t,r){"use strict";var n=r("./node_modules/core-js-pure/internals/export.js"),o=r("./node_modules/core-js-pure/internals/global.js"),s=r("./node_modules/core-js-pure/internals/internal-metadata.js"),i=r("./node_modules/core-js-pure/internals/fails.js"),a=r("./node_modules/core-js-pure/internals/create-non-enumerable-property.js"),A=r("./node_modules/core-js-pure/internals/iterate.js"),u=r("./node_modules/core-js-pure/internals/an-instance.js"),c=r("./node_modules/core-js-pure/internals/is-object.js"),l=r("./node_modules/core-js-pure/internals/set-to-string-tag.js"),d=r("./node_modules/core-js-pure/internals/object-define-property.js").f,f=r("./node_modules/core-js-pure/internals/array-iteration.js").forEach,h=r("./node_modules/core-js-pure/internals/descriptors.js"),p=r("./node_modules/core-js-pure/internals/internal-state.js"),m=p.set,g=p.getterFor;e.exports=function(e,t,r){var p,y=-1!==e.indexOf("Map"),v=-1!==e.indexOf("Weak"),w=y?"set":"add",b=o[e],B=b&&b.prototype,j={};if(h&&"function"==typeof b&&(v||B.forEach&&!i((function(){(new b).entries().next()})))){p=t((function(t,r){m(u(t,p,e),{type:e,collection:new b}),null!=r&&A(r,t[w],{that:t,AS_ENTRIES:y})}));var _=g(e);f(["add","clear","delete","forEach","get","has","set","keys","values","entries"],(function(e){var t="add"==e||"set"==e;!(e in B)||v&&"clear"==e||a(p.prototype,e,(function(r,n){var o=_(this).collection;if(!t&&v&&!c(r))return"get"==e&&void 0;var s=o[e](0===r?0:r,n);return t?this:s}))})),v||d(p.prototype,"size",{configurable:!0,get:function(){return _(this).collection.size}})}else p=r.getConstructor(t,e,y,w),s.REQUIRED=!0;return l(p,e,!1,!0),j[e]=p,n({global:!0,forced:!0},j),v||r.setStrong(p,e,y),p}},"./node_modules/core-js-pure/internals/correct-is-regexp-logic.js":function(e,t,r){var n=r("./node_modules/core-js-pure/internals/well-known-symbol.js")("match");e.exports=function(e){var t=/./;try{"/./"[e](t)}catch(r){try{return t[n]=!1,"/./"[e](t)}catch(e){}}return!1}},"./node_modules/core-js-pure/internals/correct-prototype-getter.js":function(e,t,r){var n=r("./node_modules/core-js-pure/internals/fails.js");e.exports=!n((function(){function e(){}return e.prototype.constructor=null,Object.getPrototypeOf(new e)!==e.prototype}))},"./node_modules/core-js-pure/internals/create-iterator-constructor.js":function(e,t,r){"use strict";var n=r("./node_modules/core-js-pure/internals/iterators-core.js").IteratorPrototype,o=r("./node_modules/core-js-pure/internals/object-create.js"),s=r("./node_modules/core-js-pure/internals/create-property-descriptor.js"),i=r("./node_modules/core-js-pure/internals/set-to-string-tag.js"),a=r("./node_modules/core-js-pure/internals/iterators.js"),A=function(){return this};e.exports=function(e,t,r){var u=t+" Iterator";return e.prototype=o(n,{next:s(1,r)}),i(e,u,!1,!0),a[u]=A,e}},"./node_modules/core-js-pure/internals/create-non-enumerable-property.js":function(e,t,r){var n=r("./node_modules/core-js-pure/internals/descriptors.js"),o=r("./node_modules/core-js-pure/internals/object-define-property.js"),s=r("./node_modules/core-js-pure/internals/create-property-descriptor.js");e.exports=n?function(e,t,r){return o.f(e,t,s(1,r))}:function(e,t,r){return e[t]=r,e}},"./node_modules/core-js-pure/internals/create-property-descriptor.js":function(e){e.exports=function(e,t){return{enumerable:!(1&e),configurable:!(2&e),writable:!(4&e),value:t}}},"./node_modules/core-js-pure/internals/create-property.js":function(e,t,r){"use strict";var n=r("./node_modules/core-js-pure/internals/to-primitive.js"),o=r("./node_modules/core-js-pure/internals/object-define-property.js"),s=r("./node_modules/core-js-pure/internals/create-property-descriptor.js");e.exports=function(e,t,r){var i=n(t);i in e?o.f(e,i,s(0,r)):e[i]=r}},"./node_modules/core-js-pure/internals/define-iterator.js":function(e,t,r){"use strict";var n=r("./node_modules/core-js-pure/internals/export.js"),o=r("./node_modules/core-js-pure/internals/create-iterator-constructor.js"),s=r("./node_modules/core-js-pure/internals/object-get-prototype-of.js"),i=r("./node_modules/core-js-pure/internals/object-set-prototype-of.js"),a=r("./node_modules/core-js-pure/internals/set-to-string-tag.js"),A=r("./node_modules/core-js-pure/internals/create-non-enumerable-property.js"),u=r("./node_modules/core-js-pure/internals/redefine.js"),c=r("./node_modules/core-js-pure/internals/well-known-symbol.js"),l=r("./node_modules/core-js-pure/internals/is-pure.js"),d=r("./node_modules/core-js-pure/internals/iterators.js"),f=r("./node_modules/core-js-pure/internals/iterators-core.js"),h=f.IteratorPrototype,p=f.BUGGY_SAFARI_ITERATORS,m=c("iterator"),g="keys",y="values",v="entries",w=function(){return this};e.exports=function(e,t,r,c,f,b,B){o(r,t,c);var j,_,C,x=function(e){if(e===f&&U)return U;if(!p&&e in Q)return Q[e];switch(e){case g:case y:case v:return function(){return new r(this,e)}}return function(){return new r(this)}},E=t+" Iterator",N=!1,Q=e.prototype,F=Q[m]||Q["@@iterator"]||f&&Q[f],U=!p&&F||x(f),S="Array"==t&&Q.entries||F;if(S&&(j=s(S.call(new e)),h!==Object.prototype&&j.next&&(l||s(j)===h||(i?i(j,h):"function"!=typeof j[m]&&A(j,m,w)),a(j,E,!0,!0),l&&(d[E]=w))),f==y&&F&&F.name!==y&&(N=!0,U=function(){return F.call(this)}),l&&!B||Q[m]===U||A(Q,m,U),d[t]=U,f)if(_={values:x(y),keys:b?U:x(g),entries:x(v)},B)for(C in _)(p||N||!(C in Q))&&u(Q,C,_[C]);else n({target:t,proto:!0,forced:p||N},_);return _}},"./node_modules/core-js-pure/internals/define-well-known-symbol.js":function(e,t,r){var n=r("./node_modules/core-js-pure/internals/path.js"),o=r("./node_modules/core-js-pure/internals/has.js"),s=r("./node_modules/core-js-pure/internals/well-known-symbol-wrapped.js"),i=r("./node_modules/core-js-pure/internals/object-define-property.js").f;e.exports=function(e){var t=n.Symbol||(n.Symbol={});o(t,e)||i(t,e,{value:s.f(e)})}},"./node_modules/core-js-pure/internals/descriptors.js":function(e,t,r){var n=r("./node_modules/core-js-pure/internals/fails.js");e.exports=!n((function(){return 7!=Object.defineProperty({},1,{get:function(){return 7}})[1]}))},"./node_modules/core-js-pure/internals/document-create-element.js":function(e,t,r){var n=r("./node_modules/core-js-pure/internals/global.js"),o=r("./node_modules/core-js-pure/internals/is-object.js"),s=n.document,i=o(s)&&o(s.createElement);e.exports=function(e){return i?s.createElement(e):{}}},"./node_modules/core-js-pure/internals/dom-iterables.js":function(e){e.exports={CSSRuleList:0,CSSStyleDeclaration:0,CSSValueList:0,ClientRectList:0,DOMRectList:0,DOMStringList:0,DOMTokenList:1,DataTransferItemList:0,FileList:0,HTMLAllCollection:0,HTMLCollection:0,HTMLFormElement:0,HTMLSelectElement:0,MediaList:0,MimeTypeArray:0,NamedNodeMap:0,NodeList:1,PaintRequestList:0,Plugin:0,PluginArray:0,SVGLengthList:0,SVGNumberList:0,SVGPathSegList:0,SVGPointList:0,SVGStringList:0,SVGTransformList:0,SourceBufferList:0,StyleSheetList:0,TextTrackCueList:0,TextTrackList:0,TouchList:0}},"./node_modules/core-js-pure/internals/engine-is-browser.js":function(e){e.exports="object"==typeof window},"./node_modules/core-js-pure/internals/engine-is-ios.js":function(e,t,r){var n=r("./node_modules/core-js-pure/internals/engine-user-agent.js");e.exports=/(?:iphone|ipod|ipad).*applewebkit/i.test(n)},"./node_modules/core-js-pure/internals/engine-is-node.js":function(e,t,r){var n=r("./node_modules/core-js-pure/internals/classof-raw.js"),o=r("./node_modules/core-js-pure/internals/global.js");e.exports="process"==n(o.process)},"./node_modules/core-js-pure/internals/engine-is-webos-webkit.js":function(e,t,r){var n=r("./node_modules/core-js-pure/internals/engine-user-agent.js");e.exports=/web0s(?!.*chrome)/i.test(n)},"./node_modules/core-js-pure/internals/engine-user-agent.js":function(e,t,r){var n=r("./node_modules/core-js-pure/internals/get-built-in.js");e.exports=n("navigator","userAgent")||""},"./node_modules/core-js-pure/internals/engine-v8-version.js":function(e,t,r){var n,o,s=r("./node_modules/core-js-pure/internals/global.js"),i=r("./node_modules/core-js-pure/internals/engine-user-agent.js"),a=s.process,A=a&&a.versions,u=A&&A.v8;u?o=(n=u.split("."))[0]<4?1:n[0]+n[1]:i&&(!(n=i.match(/Edge\/(\d+)/))||n[1]>=74)&&(n=i.match(/Chrome\/(\d+)/))&&(o=n[1]),e.exports=o&&+o},"./node_modules/core-js-pure/internals/entry-virtual.js":function(e,t,r){var n=r("./node_modules/core-js-pure/internals/path.js");e.exports=function(e){return n[e+"Prototype"]}},"./node_modules/core-js-pure/internals/enum-bug-keys.js":function(e){e.exports=["constructor","hasOwnProperty","isPrototypeOf","propertyIsEnumerable","toLocaleString","toString","valueOf"]},"./node_modules/core-js-pure/internals/export.js":function(e,t,r){"use strict";var n=r("./node_modules/core-js-pure/internals/global.js"),o=r("./node_modules/core-js-pure/internals/object-get-own-property-descriptor.js").f,s=r("./node_modules/core-js-pure/internals/is-forced.js"),i=r("./node_modules/core-js-pure/internals/path.js"),a=r("./node_modules/core-js-pure/internals/function-bind-context.js"),A=r("./node_modules/core-js-pure/internals/create-non-enumerable-property.js"),u=r("./node_modules/core-js-pure/internals/has.js"),c=function(e){var t=function(t,r,n){if(this instanceof e){switch(arguments.length){case 0:return new e;case 1:return new e(t);case 2:return new e(t,r)}return new e(t,r,n)}return e.apply(this,arguments)};return t.prototype=e.prototype,t};e.exports=function(e,t){var r,l,d,f,h,p,m,g,y=e.target,v=e.global,w=e.stat,b=e.proto,B=v?n:w?n[y]:(n[y]||{}).prototype,j=v?i:i[y]||(i[y]={}),_=j.prototype;for(d in t)r=!s(v?d:y+(w?".":"#")+d,e.forced)&&B&&u(B,d),h=j[d],r&&(p=e.noTargetGet?(g=o(B,d))&&g.value:B[d]),f=r&&p?p:t[d],r&&typeof h==typeof f||(m=e.bind&&r?a(f,n):e.wrap&&r?c(f):b&&"function"==typeof f?a(Function.call,f):f,(e.sham||f&&f.sham||h&&h.sham)&&A(m,"sham",!0),j[d]=m,b&&(u(i,l=y+"Prototype")||A(i,l,{}),i[l][d]=f,e.real&&_&&!_[d]&&A(_,d,f)))}},"./node_modules/core-js-pure/internals/fails.js":function(e){e.exports=function(e){try{return!!e()}catch(e){return!0}}},"./node_modules/core-js-pure/internals/freezing.js":function(e,t,r){var n=r("./node_modules/core-js-pure/internals/fails.js");e.exports=!n((function(){return Object.isExtensible(Object.preventExtensions({}))}))},"./node_modules/core-js-pure/internals/function-bind-context.js":function(e,t,r){var n=r("./node_modules/core-js-pure/internals/a-function.js");e.exports=function(e,t,r){if(n(e),void 0===t)return e;switch(r){case 0:return function(){return e.call(t)};case 1:return function(r){return e.call(t,r)};case 2:return function(r,n){return e.call(t,r,n)};case 3:return function(r,n,o){return e.call(t,r,n,o)}}return function(){return e.apply(t,arguments)}}},"./node_modules/core-js-pure/internals/function-bind.js":function(e,t,r){"use strict";var n=r("./node_modules/core-js-pure/internals/a-function.js"),o=r("./node_modules/core-js-pure/internals/is-object.js"),s=[].slice,i={},a=function(e,t,r){if(!(t in i)){for(var n=[],o=0;od;d++)if((h=j(e[d]))&&h instanceof u)return h;return new u(!1)}c=l.call(e)}for(p=c.next;!(m=p.call(c)).done;){try{h=j(m.value)}catch(e){throw A(c),e}if("object"==typeof h&&h&&h instanceof u)return h}return new u(!1)}},"./node_modules/core-js-pure/internals/iterator-close.js":function(e,t,r){var n=r("./node_modules/core-js-pure/internals/an-object.js");e.exports=function(e){var t=e.return;if(void 0!==t)return n(t.call(e)).value}},"./node_modules/core-js-pure/internals/iterators-core.js":function(e,t,r){"use strict";var n,o,s,i=r("./node_modules/core-js-pure/internals/fails.js"),a=r("./node_modules/core-js-pure/internals/object-get-prototype-of.js"),A=r("./node_modules/core-js-pure/internals/create-non-enumerable-property.js"),u=r("./node_modules/core-js-pure/internals/has.js"),c=r("./node_modules/core-js-pure/internals/well-known-symbol.js"),l=r("./node_modules/core-js-pure/internals/is-pure.js"),d=c("iterator"),f=!1;[].keys&&("next"in(s=[].keys())?(o=a(a(s)))!==Object.prototype&&(n=o):f=!0);var h=null==n||i((function(){var e={};return n[d].call(e)!==e}));h&&(n={}),l&&!h||u(n,d)||A(n,d,(function(){return this})),e.exports={IteratorPrototype:n,BUGGY_SAFARI_ITERATORS:f}},"./node_modules/core-js-pure/internals/iterators.js":function(e){e.exports={}},"./node_modules/core-js-pure/internals/microtask.js":function(e,t,r){var n,o,s,i,a,A,u,c,l=r("./node_modules/core-js-pure/internals/global.js"),d=r("./node_modules/core-js-pure/internals/object-get-own-property-descriptor.js").f,f=r("./node_modules/core-js-pure/internals/task.js").set,h=r("./node_modules/core-js-pure/internals/engine-is-ios.js"),p=r("./node_modules/core-js-pure/internals/engine-is-webos-webkit.js"),m=r("./node_modules/core-js-pure/internals/engine-is-node.js"),g=l.MutationObserver||l.WebKitMutationObserver,y=l.document,v=l.process,w=l.Promise,b=d(l,"queueMicrotask"),B=b&&b.value;B||(n=function(){var e,t;for(m&&(e=v.domain)&&e.exit();o;){t=o.fn,o=o.next;try{t()}catch(e){throw o?i():s=void 0,e}}s=void 0,e&&e.enter()},h||m||p||!g||!y?w&&w.resolve?((u=w.resolve(void 0)).constructor=w,c=u.then,i=function(){c.call(u,n)}):i=m?function(){v.nextTick(n)}:function(){f.call(l,n)}:(a=!0,A=y.createTextNode(""),new g(n).observe(A,{characterData:!0}),i=function(){A.data=a=!a})),e.exports=B||function(e){var t={fn:e,next:void 0};s&&(s.next=t),o||(o=t,i()),s=t}},"./node_modules/core-js-pure/internals/native-promise-constructor.js":function(e,t,r){var n=r("./node_modules/core-js-pure/internals/global.js");e.exports=n.Promise},"./node_modules/core-js-pure/internals/native-symbol.js":function(e,t,r){var n=r("./node_modules/core-js-pure/internals/engine-v8-version.js"),o=r("./node_modules/core-js-pure/internals/fails.js");e.exports=!!Object.getOwnPropertySymbols&&!o((function(){var e=Symbol();return!String(e)||!(Object(e)instanceof Symbol)||!Symbol.sham&&n&&n<41}))},"./node_modules/core-js-pure/internals/native-weak-map.js":function(e,t,r){var n=r("./node_modules/core-js-pure/internals/global.js"),o=r("./node_modules/core-js-pure/internals/inspect-source.js"),s=n.WeakMap;e.exports="function"==typeof s&&/native code/.test(o(s))},"./node_modules/core-js-pure/internals/new-promise-capability.js":function(e,t,r){"use strict";var n=r("./node_modules/core-js-pure/internals/a-function.js"),o=function(e){var t,r;this.promise=new e((function(e,n){if(void 0!==t||void 0!==r)throw TypeError("Bad Promise constructor");t=e,r=n})),this.resolve=n(t),this.reject=n(r)};e.exports.f=function(e){return new o(e)}},"./node_modules/core-js-pure/internals/not-a-regexp.js":function(e,t,r){var n=r("./node_modules/core-js-pure/internals/is-regexp.js");e.exports=function(e){if(n(e))throw TypeError("The method doesn't accept regular expressions");return e}},"./node_modules/core-js-pure/internals/number-parse-float.js":function(e,t,r){var n=r("./node_modules/core-js-pure/internals/global.js"),o=r("./node_modules/core-js-pure/internals/string-trim.js").trim,s=r("./node_modules/core-js-pure/internals/whitespaces.js"),i=n.parseFloat,a=1/i(s+"-0")!=-1/0;e.exports=a?function(e){var t=o(String(e)),r=i(t);return 0===r&&"-"==t.charAt(0)?-0:r}:i},"./node_modules/core-js-pure/internals/number-parse-int.js":function(e,t,r){var n=r("./node_modules/core-js-pure/internals/global.js"),o=r("./node_modules/core-js-pure/internals/string-trim.js").trim,s=r("./node_modules/core-js-pure/internals/whitespaces.js"),i=n.parseInt,a=/^[+-]?0[Xx]/,A=8!==i(s+"08")||22!==i(s+"0x16");e.exports=A?function(e,t){var r=o(String(e));return i(r,t>>>0||(a.test(r)?16:10))}:i},"./node_modules/core-js-pure/internals/object-create.js":function(e,t,r){var n,o=r("./node_modules/core-js-pure/internals/an-object.js"),s=r("./node_modules/core-js-pure/internals/object-define-properties.js"),i=r("./node_modules/core-js-pure/internals/enum-bug-keys.js"),a=r("./node_modules/core-js-pure/internals/hidden-keys.js"),A=r("./node_modules/core-js-pure/internals/html.js"),u=r("./node_modules/core-js-pure/internals/document-create-element.js"),c=r("./node_modules/core-js-pure/internals/shared-key.js")("IE_PROTO"),l=function(){},d=function(e){return" - - -
    -
    -
    -

    FACTURĂ

    -
    Seria & Nr:
    -
    - Data emiterii: | - Data scadentă: -
    -
    -
    -
    -
    -
    -
    - -
    -
    -
    Furnizor
    - -
    -
    -
    Client
    - -
    -
    - - - - - - - - - - - - - - -
    Nr.DenumireUMCant.PrețTVATotal
    - - - -
    -
    -

    Monedă Factură:

    - -
    - -
    -
    - Subtotal: - -
    - - -
    - Valoare Netă: - -
    - -
    -
    -
    Tip TVA
    -
    Cotă
    -
    Bază
    -
    TVA
    -
    - - -
    - Total TVA (): - -
    - -
    - -
    - Total cu TVA: - -
    -
    -
    - - -
    - - - + + + + + Factură + + + + + +
    + + +
    +
    +
    +
    +

    FACTURĂ

    +
    Seria & Nr:
    +
    + Data emiterii: | + Data scadentă: +
    +
    +
    +
    +
    +
    +
    + +
    +
    +
    Furnizor
    + +
    +
    +
    Client
    + +
    +
    + + + + + + + + + + + + + + +
    Nr.DenumireUMCant.PrețTVATotal
    + + + +
    +
    +

    Monedă Factură:

    + +
    + +
    +
    + Subtotal: + +
    + + +
    + Valoare Netă: + +
    + +
    +
    +
    Tip TVA
    +
    Cotă
    +
    Bază
    +
    TVA
    +
    + + +
    + Total TVA (): + +
    + +
    + +
    + Total cu TVA: + +
    +
    +
    + + +
    + + + \ No newline at end of file diff --git a/efactura-generator/templates/print.html b/efactura-generator/templates/print.html index 914c4c0..2d7d99c 100644 --- a/efactura-generator/templates/print.html +++ b/efactura-generator/templates/print.html @@ -1,378 +1,446 @@ - - - - - Factură - - - - -
    -
    -
    -

    FACTURĂ

    -
    Seria & Nr:
    -
    - Data emiterii: | - Data scadentă: -
    -
    -
    -
    -
    -
    -
    - -
    -
    -
    Furnizor
    - -
    -
    -
    Client
    - -
    -
    - - - - - - - - - - - - - - -
    Nr.DenumireUMCant.PrețTVATotal
    - - - -
    -
    -

    Monedă Factură:

    - -
    - -
    -
    - Subtotal: - -
    -
    - Total Reduceri: - -
    -
    - Total Taxe: - -
    -
    - Valoare Netă: - -
    - -
    -
    Defalcare TVA
    -
    -
    Tip TVA
    -
    Cotă
    -
    Bază
    -
    TVA
    -
    - - - - -
    - -
    - Total cu TVA: - -
    -
    -
    - - -
    - + + + + + Factură + + + + + +
    + + +
    +
    +
    +
    +

    FACTURĂ

    +
    Seria & Nr:
    +
    + Data emiterii: | + Data scadentă: +
    +
    +
    +
    +
    +
    +
    + +
    +
    +
    Furnizor
    + +
    +
    +
    Client
    + +
    +
    + + + + + + + + + + + + + + +
    Nr.DenumireUMCant.PrețTVATotal
    + + + +
    +
    +

    Monedă Factură:

    + +
    + +
    +
    + Subtotal: + +
    +
    + Total Reduceri: + +
    +
    + Total Taxe: + +
    +
    + Valoare Netă: + +
    + +
    +
    Defalcare TVA
    +
    +
    Tip TVA
    +
    Cotă
    +
    Bază
    +
    TVA
    +
    + + + + +
    + +
    + Total cu TVA: + +
    +
    +
    + + +
    + + + \ No newline at end of file