import { SolidTemplateElement, Helpers, store } from 'https://cdn.skypack.dev/@startinblox/core@0.16'; const base_url = import.meta.url.replace(/\/[^\/]*$/, ''); // const base_url = "https://cdn.skypack.dev/@startinblox/component-invoicing@1.2"; Helpers.importCSS(`${base_url}/css/main.css?min`); export class SolidInvoicing extends SolidTemplateElement { constructor() { super(); this.setTranslationsPath(`${base_url}/locales`); if (window.hubl) { this.localize = (key) => { return window.hubl.intl.t("invoices." + key) || this.strings[key] || key; } } } static get propsDefinition() { return { dataSrc: 'data-src', routerPrefix: 'router-prefix', uploadUrl: 'upload-dir' } } connectedCallback() { // display invoice list document.querySelectorAll('.invoices-list').forEach((list) => { console.log(list); list.querySelector('.show-list').addEventListener('click', () => { console.log("vlop"); list.querySelector('.invoice-list-display').classList.toggle('invoice-list-visible') }); }); this.addEventListener('click', (e) => { // display invoice list if(e.target && e.target.closest('.show-list')){ let invoice = e.target.closest('.invoices-list'); invoice.classList.toggle('invoices-list-visible') } // display invoice details if(e.target && e.target.closest('[name="invoice-title"]')){ let invoice = e.target.closest('[name="invoice-title"]'); invoice.closest('solid-display').classList.toggle('invoice-visible') } // close dialog if(e.target && e.target.closest('.close-button')){ let routeName = e.target.closest('#clients-invoices') ? 'invoice-list' : 'freelance-invoice-list' window.dispatchEvent( new CustomEvent('requestNavigation', { detail: { route: routeName } }), ); } // access pdf invoice if (e.target && e.target.closest('.print')) { let printWindow = window.open('', 'PRINT', 'height=400,width=600'); printWindow.document.write(` <html> <head> <title></title> <link rel="stylesheet" href="${base_url}/css/main.css?min" media="screen"/> <link rel="stylesheet" href="${base_url}/css/main.css?min" media="print"/> <style>:root{--solid-invoicing-primary: #6259e5;--solid-invoicing-secondary: #ffb700; font-family: sans-serif}</style> <script src="https://raw.githack.com/eKoopmans/html2pdf/master/dist/html2pdf.bundle.js"></script> </head> <body> ${document.getElementById('print').innerHTML} <script> html2pdf(document.body) </script> </body> </html> `); printWindow.document.close(); printWindow.focus(); /* setTimeout(() => { printWindow.print(); printWindow.close(); }, 1000); */ return true; } // Duplicate invoice if (e.target && e.target.closest('.duplication-button')) { const form = this.querySelector('.duplicate-form').component; form.getFormValue().then((value) => { let resources = value; resources["@context"] = form.context; const resourceId = this.querySelector('.new-customer-invoice-form')?.component.resourceId; if (!resourceId) { console.warn('No URI found to post a new invoice.'); return; } store.post(this.cleanInvoiceBeforeDuplicate(resources), resourceId).then(() => { this.dispatchEvent( new CustomEvent('requestNavigation', { bubbles: true, detail: { route: "invoice-list" }, }), ); }); }); } // Close duplicate dialog if (e.target && e.target.closest('.close-duplication-dialog')) { this.dispatchEvent( new CustomEvent('requestNavigation', { bubbles: true, detail: { route: "invoice-list" }, }), ); } }); } cleanInvoiceBeforeDuplicate(invoice) { invoice.title= invoice.title + " - 2" delete invoice["@id"]; delete invoice.batches["@id"]; invoice.batches.forEach((batch, indexb) => { delete invoice.batches[indexb]["@id"]; batch.tasks.forEach((task, indext) => { delete invoice.batches[indexb].tasks[indext]["@id"]; }); }); return invoice; } /** * Get total amount of a list of invoices */ async getTotalAmount(displayElement) { return new Promise((resolve, reject) => { if (!displayElement) reject('Element not found'); displayElement.addEventListener('populate', (e) => { const invoices = e.target.component.resource; if (!invoices) resolve(0); Promise.all(invoices['ldp:contains'].map(invoice => invoice.htAmount)) .then(a => a.reduce((acc, current) => acc + (current || 0))) .then(res => resolve(res)) .catch(e => reject(e)) }) }) } /** * Generate recap and show values */ generateRecap() { const customerInvoices = this.querySelector('#customer-invoices') const freelancerInvoices = this.querySelector('#freelancer-invoices') const recapFreelancerInvoices = this.querySelector('#recap-freelancer-invoices') const recapCustomerInvoices = this.querySelector('#recap-customer-invoices') const recapFees = this.querySelector('#recap-fees') const recapBusinessProvision = this.querySelector('#recap-business-provision') const recapTotal = this.querySelector('#recap-total') Promise.all([this.getTotalAmount(customerInvoices), this.getTotalAmount(freelancerInvoices)]) .then(([customerTotal, freelancerTotal]) => { if (customerTotal) { recapCustomerInvoices.innerText = customerTotal.toFixed(2); recapFees.innerText = (customerTotal * 0.05).toFixed(2); recapBusinessProvision.innerText = (customerTotal * 0.10).toFixed(2); } if (freelancerTotal) { recapFreelancerInvoices.innerText = freelancerTotal.toFixed(2); } recapTotal.innerText = ((customerTotal || 0) - (freelancerTotal || 0)).toFixed(2); }) .catch(() => {}) } renderCallback() { this.generateRecap(); } template({dataSrc, routerPrefix, uploadUrl}) { let prefix = routerPrefix ? `route-prefix="${routerPrefix}"`: '' return ` <div id="solid-invoicing" class="solid-invoicing segment full padding-large sm-padding-xsmall sm-padding-top-xlarge"> <!-- widgets ==================================== --> <solid-widget name="widget-money"> <template> <div>\${value?value:0} € HT</div> </template> </solid-widget> <solid-widget name="widget-tva-rate"> <template> <div>${this.localize('widget.tva_rate')} \${value} %</div> </template> </solid-widget> <solid-widget name="widget-invoice-number"> <template> <div>N°\${value}</div> </template> </solid-widget> <solid-widget name="widget-batches"> <template> <solid-display class="batches-list" data-src="\${value}" fields="batch-header(title, amount), batch-body(tasks), batch-footer(ht-total(ht-label, htAmount))" value-amount="${this.localize('value.amount')}" value-ht-label="${this.localize('label.value_ht')}" widget-tasks="widget-tasks" widget-htAmount="widget-money" ></solid-display> </template> </solid-widget> <solid-widget name="widget-tasks"> <template> <solid-display class="tasks-list" data-src="\${value}" fields="price-line(title, htAmount)" widget-htAmount="widget-money" ></solid-display> </template> </solid-widget> <solid-widget name="widget-form-batches"> <template> <solid-form data-src="\${value}" data-holder naked fields="batch-header(title), batch-body(tasks)" widget-tasks="widget-form-tasks" widget-title="solid-form-placeholder-text" label-title="${this.localize('label.title_batch')}" label-tasks="" multiple-tasks> </solid-form> </template> </solid-widget> <solid-widget name="widget-form-tasks"> <template> <solid-form data-src="\${value}" class="tasks-list" data-holder naked fields="price-line(title, htAmount)" widget-htAmount="solid-form-placeholder-text" widget-title="solid-form-placeholder-text" label-title="${this.localize('label.title_task')}" label-htAmount="${this.localize('label.ht_amount')}" ></solid-form> </template> </solid-widget> <solid-widget name="widget-render-title"> <template> <div><strong>${this.localize('widget.render_title')}\${value}</strong></div> </template> </solid-widget> <solid-widget name="widget-render-metadata"> <template> <div> <div class="invoice-render__fees"> <p>\${value}</p> <p>${this.localize('widget.render_metadata_late')}</p> <p>${this.localize('widget.render_metadata_indemnity')}</p> </div> <div class="invoice-render__rib"> <div><strong>${this.localize('widget.render_metadata_bank_transfert')}</strong></div> <table> <tr> <td>BIC</td> <td>CMCIFR2A</td> </tr> <tr> <td>IBAN</td> <td>FR76 1027 8060 8200 0205 5370 166</td> </tr> </table> </div> </div> </template> </solid-widget> <solid-widget name="widget-embed-drive"> <template> <embed src="\${value}" width="560px" height="800px"/> </template> </solid-widget> <solid-widget name="widget-nomenclature"> <template> <div> <b>${this.localize('title.nomenclature')}</b> : ${this.localize('text.nomenclature')} </div> </template> </solid-widget> <solid-widget name="widget-state"> <template> \${value == 'edited' ? '<div class="sm-full button text-xsmall text-bold text-center reversed color-secondary bordered">${this.localize('option.edited')}</div>' : '' } \${value == 'pending' ? '<div class="sm-full button text-xsmall text-bold text-center color-secondary bordered">${this.localize('option.pending')}</div>' : '' } \${value == 'sent' ? '<div class="sm-full button text-xsmall text-bold text-center color-third bordered">${this.localize('option.sent')}</div>' : '' } \${value == 'paid' ? '<div class="sm-full button text-xsmall text-bold text-center reversed color-third bordered">${this.localize('option.paid')}</div>' : '' } \${value == 'late' ? '<div class="sm-full button text-xsmall text-bold text-center reversed color-primary bordered">${this.localize('option.late')}</div>' : '' } </template> </solid-widget> <solid-widget name="close-button"> <template> <button class="close-button icon icon-close"> </button> </template> </solid-widget> <!-- /widgets ==================================== --> <header class="segment full main-menu"> <solid-router class="segment full margin-bottom-medium" default-route="invoices-home" ${prefix}> <solid-route name="invoices-home" class="segment half padding-right-medium"> <div class="segment full border-bottom padding-bottom-small text-uppercase text-large">${this.localize('header.home')}</div> </solid-route> <solid-route name="invoices-app" class="segment half padding-left-medium"> <div class="segment full border-bottom padding-bottom-small text-uppercase text-large">${this.localize('header.list')}</div> </solid-route> </solid-router> </header> <section id="invoices-home" data-view="invoices-home"> <p> <div> <p>Montant facturable en HT</p> <span id="recap-total"></span> </div> <div> <p>Factures clients</p> <span id="recap-customer-invoices"></span> </div> <div> <p>Factures indépendants</p> <span id="recap-freelancer-invoices"></span> </div> <div> <p>Frais de fonctionnement</p> <span id="recap-fees"></span> </div> <div> <p>Apport d'affaire</p> <span id="recap-business-provision"></span> </div> </p> </section> <section id="invoices-app" data-view="invoices-app" hidden=""> <!-- customer ==================================== --> <section id="clients-invoices" class="invoices-list invoices-list--clients"> <header class="segment"> <div class="show-list segment text-bold text-large text-uppercase padding-xsmall icon icon-arrow-down">${this.localize('title.customer_invoices')}</div> <solid-router default-route="invoice-list" ${prefix}> <solid-route name="invoice-list"></solid-route> <solid-route name="add-invoice" class="segment sm-full button text-xsmall text-bold text-uppercase text-center reversed color-secondary bordered icon icon-plus">${this.localize('button.invoice_create')}</solid-route> <solid-route name="show-invoice" use-id></solid-route> <solid-route name="edit-invoice" use-id></solid-route> <solid-route name="duplicate-invoice" use-id></solid-route> </solid-router> </header> <dialog data-view="add-invoice" class="invoice-form"> <close-button></close-button> <h3 class="text-color-heading">${this.localize('title.invoice_create')}</h3> <solid-form class="new-customer-invoice-form" data-src="${dataSrc}" nested-field="customerInvoices" fields="first-line(title, identifier), second-line(state, invoicingDate), batches, last-line(tvaRate, additionalText)" label-title="${this.localize('label.invoice_title')}" label-identifier="${this.localize('label.invoice_identifier')}" label-state="${this.localize('label.invoice_state')}" label-invoicingDate="${this.localize('label.invoice_invoicing_date')}" label-tvaRate="${this.localize('label.invoice_tva_rate')}" label-additionalText="${this.localize('label.invoice_additional_text')}" label-batches="" widget-state="solid-form-dropdown-label" widget-batches="widget-form-batches" widget-invoicingDate="solid-form-date-label" widget-tvaRate="solid-form-number-label" enum-state="${this.localize('option.edited')} = edited, ${this.localize('option.pending')} = pending, ${this.localize('option.sent')} = sent, ${this.localize('option.late')} = late, ${this.localize('option.paid')} = paid" class-state="custom-select" multiple-batches next="invoice-list" required-title required-identifier required-invoicingDate required-tvaRate submit-button="${this.localize('button.submit_form')}" ></solid-form> </dialog> <dialog data-view="edit-invoice" class="invoice-form"> <close-button></close-button> <h3>${this.localize('title.invoice_modify')}</h3> <solid-form bind-resources fields="first-line(title, identifier), second-line(state, invoicingDate), batches, last-line(tvaRate, additionalText)" label-title="${this.localize('label.invoice_title')}" label-identifier="${this.localize('label.invoice_identifier')}" label-state="${this.localize('label.invoice_state')}" label-invoicingDate="${this.localize('label.invoice_invoicing_date')}" label-tvaRate="${this.localize('label.invoice_tva_rate')}" label-additionalText="${this.localize('label.invoice_additional_text')}" label-batches="" widget-state="solid-form-dropdown-label" widget-batches="widget-form-batches" widget-invoicingDate="solid-form-date-label" widget-tvaRate="solid-form-number-label" enum-state="${this.localize('option.edited')} = edited, ${this.localize('option.pending')} = pending, ${this.localize('option.sent')} = sent, ${this.localize('option.late')} = late, ${this.localize('option.paid')} = paid" class-state="custom-select" multiple-batches next="invoice-list" required-title required-identifier required-invoicingDate required-tvaRate submit-button="${this.localize('button.edit_submit_form')}" ></solid-form> <solid-delete class="sm-full button text-xsmall text-bold text-uppercase text-center color-secondary bordered icon icon-trash" bind-resources next="invoice-list" confirmation-message="${this.localize('confirm.delete_invoice')}" confirmation-type="dialog" confirmation-submit-text="${this.localize('validate.delete_invoice')}" confirmation-cancel-text="${this.localize('cancel.delete_invoice')}" confirmation-submit-class="segment sm-full button text-xsmall text-bold reversed text-uppercase text-center color-secondary bordered" confirmation-cancel-class="cancel segment sm-full button text-xsmall text-bold text-uppercase text-center color-secondary bordered" data-label="${this.localize('button.delete_submit_form')}" ></solid-delete> </dialog> <dialog data-view="duplicate-invoice" class="invoice-form" id="duplicate"> <close-button></close-button> <h3>${this.localize('title.invoice_duplicate')}</h3> <h4>${this.localize('question.invoice_duplicate')}</h4> <solid-form class="duplicate-form" bind-resources fields="first-line(title, identifier), second-line(state, invoicingDate), batches, last-line(tvaRate, additionalText)" widget-state="solid-form-dropdown-label" widget-batches="widget-form-batches" widget-invoicingDate="solid-form-date-label" widget-tvaRate="solid-form-number-label" enum-state="${this.localize('option.edited')} = edited, ${this.localize('option.pending')} = pending, ${this.localize('option.sent')} = sent, ${this.localize('option.late')} = late, ${this.localize('option.paid')} = paid" class-state="custom-select" class-title="invoice-title" multiple-batches naked ></solid-form> <div class="popup-buttons"> <button class="popup-buttons__cancel close-duplication-dialog">${this.localize('button.duplicate_cancel')}</button> <button class="popup-buttons__validate duplication-button">${this.localize('button.duplicate_confirm')}</button> </div> </dialog> <solid-display id="customer-invoices" data-view="invoice-list" class="invoices-list__item invoice-list-display" data-src="${dataSrc}" nested-field="customerInvoices" fields="invoice-title(invoice-main(invoice-header(title, identifier), invoice-footer(invoicingDate, htAmount)), invoice-aside(state)), invoice-hidden(actions(duplicate, edit, show), batches, invoice-totals(totals-header, ht-total(ht-label, htAmount), tva-total(tvaRate, tvaAmount), ttc-total(ttc-label, ttcAmount)))" value-totals-header="${this.localize('value.totals_header')}" value-ht-label="${this.localize('label.value_ht')}" value-ttc-label="${this.localize('label.value_ttc')}" widget-batches="widget-batches" widget-htAmount="widget-money" widget-state="widget-state" widget-tvaAmount="widget-money" widget-ttcAmount="widget-money" widget-tvaRate="widget-tva-rate" widget-identifier="widget-invoice-number" label-edit="${this.localize('label.edit')}" label-show="${this.localize('label.show')}" label-duplicate="${this.localize('label.duplicate')}" action-edit="edit-invoice" action-show="show-invoice" action-duplicate="duplicate-invoice" class-edit="mdi-pencil mdi" class-show="mdi-eye mdi" class-duplicate="mdi-file-replace-outline mdi" order-desc="invoicingDate" ></solid-display> <!-- render --> <dialog class="invoice-dialog" data-view="show-invoice"> <close-button></close-button> <div id="print"> <div class="invoice-render"> <img src="${base_url}/images/happy-dev-logo.png" width="200" /> <solid-display class="display-invoice" bind-resources fields="invoice-head(invoice-header-left(identifier, invoicingDate), invoice-header-right(customer-infos(project.customer.name, project.customer.address))), batches, invoice-payment(additionalText, invoice-totals(ht-total(ht-label, htAmount), tva-total(tvaRate, tvaAmount), ttc-total(ttc-label, ttcAmount), tva-notice)), invoice-footer" value-ht-label="${this.localize('label.value_ht')}" value-ttc-label="${this.localize('label.value_ttc')}" value-tva-notice="${this.localize('value.tva_notice')}" widget-identifier="widget-render-title" label-invoicingDate="${this.localize('label.invoice_invoicing_date2')}" widget-invoicingDate="solid-display-value-date-label" widget-additionalText="widget-render-metadata" widget-batches="widget-batches" widget-htAmount="widget-money" widget-tvaAmount="widget-money" widget-ttcAmount="widget-money" widget-tvaRate="widget-tva-rate" class-project.customer.name="segment block whitespace-normal text-color-heading text-bold margin-bottom-xsmall" ></solid-display> <div class="invoice-render__footer"> <div class="invoice-render__hd-address"> <p><strong>Happy Dev</strong></p> <p>75 rue du Javelot</p> <p>75013 Paris</p> <p>contact@happy-dev.fr</p> </div> <div class="invoice-render__hd-legal"> <p><strong>SIRET : </strong>81314060500011</p> <p><strong>${this.localize('text.tva_number')}</strong>FR 61 813140605</p> <p>${this.localize('text.sas_info')}</p> </div> </div> </div> </div> <br /> <div class="popup-buttons"> <button class="popup-buttons__validate print">${this.localize('button.print')}</button> </div> </dialog> <!-- /render --> </section> <!-- /customer ==================================== --> <!-- freelance ==================================== --> <section id="freelances-invoices" class="invoices-list invoices-list--freelances"> <header class="segment"> <div class="show-list segment text-bold text-large text-uppercase padding-xsmall icon icon-arrow-down">${this.localize('title.freelance_invoices')}</div> <solid-router default-route="freelance-invoice-list" ${prefix}> <solid-route name="freelance-invoice-list"></solid-route> <solid-route name="edit-freelance-invoice"></solid-route> <solid-route name="add-freelance-invoice" class="segment sm-full button text-xsmall text-bold text-uppercase text-center reversed color-secondary bordered icon icon-cloud-upload">${this.localize('button.invoice_import')}</solid-route> </solid-router> </header> <dialog data-view="add-freelance-invoice" class="invoice-form"> <close-button></close-button> <h3>${this.localize('title.invoice_import')}</h3> <solid-form data-src="${dataSrc}" nested-field="freelancerInvoices" fields="first-line(freelanceFullname, identifier), second-line(htAmount, nomenclatureNote), third-line(uploadUrl)" label-freelanceFullname="${this.localize('label.freelance_fullname')}" label-identifier="${this.localize('label.freelance_identifier')}" label-uploadUrl="${this.localize('label.upload_url')}" label-htAmount="${this.localize('label.freelance_ht_amount')}" widget-nomenclatureNote="widget-nomenclature" widget-uploadUrl="solid-form-file-label" widget-htAmount="solid-form-number-label" upload-url-uploadUrl="${uploadUrl}" required-freelanceFullname required-identifier required-htAmount required-uploadUrl next="freelance-invoice-list" submit-button="${this.localize('button.submit_form')}" ></solid-form> </dialog> <dialog data-view="edit-freelance-invoice" class="invoice-form"> <close-button></close-button> <h3>Modifier une facture</h3> <solid-form bind-resources fields="first-line(freelanceFullname, identifier), second-line(invoicingDate, htAmount), third-line(state, uploadUrl)" label-freelanceFullname="${this.localize('label.freelance_fullname')}" label-identifier="${this.localize('label.freelance_identifier')}" label-invoicingDate="${this.localize('label.freelance_invoicing_date')}" label-state="${this.localize('label.invoice_state')}" label-uploadUrl="${this.localize('label.edit_upload_url')}" label-htAmount="${this.localize('label.freelance_ht_amount')}" widget-invoicingDate="solid-form-date-label" widget-uploadUrl="solid-form-file-label" widget-htAmount="solid-form-number-label" widget-state="solid-form-dropdown-label" upload-url-uploadUrl="${uploadUrl}" enum-state="${this.localize('option.edited')} = edited, ${this.localize('option.pending')} = pending, ${this.localize('option.sent')} = sent, ${this.localize('option.late')} = late, ${this.localize('option.paid')} = paid" class-state="custom-select" required-freelanceFullname required-identifier required-invoicingDate required-htAmount next="freelance-invoice-list" submit-button="${this.localize('button.submit_form')}" ></solid-form> </dialog> <solid-display id="freelancer-invoices" class="invoice-list-display" data-view="freelance-invoice-list" data-src="${dataSrc}" nested-field="freelancerInvoices" fields="invoice-title(invoice-main(invoice-header(freelanceFullname, identifier), invoice-footer(invoicingDate, htAmount)), invoice-aside(state)), invoice-hidden(actions(edit), uploadUrl)" label-edit=""${this.localize('label.edit')}"" action-edit="edit-freelance-invoice" widget-htAmount="widget-money" widget-state="widget-state" widget-uploadUrl="widget-embed-drive" order-desc="invoicingDate" ></solid-display> </section> <!-- /freelance ==================================== --> </section> </div> `; } } customElements.define('solid-invoicing', SolidInvoicing);