diff --git a/.gitea/workflows/install-deps.yml b/.gitea/workflows/install-deps.yml index af7e70e..bcdacee 100644 --- a/.gitea/workflows/install-deps.yml +++ b/.gitea/workflows/install-deps.yml @@ -28,4 +28,4 @@ jobs: key: ${{ secrets.SSH_PRIVATE_KEY }} port: 22 script: | - cd /var/www/ludikevent-intranet && sudo update-alternatives --set php /usr/bin/php8.4 && php bin/console app:maintenance on && git pull origin master && sh ./update.sh && php bin/console app:maintenance off && php bin/console app:purge-cloudflare && sudo update-alternatives --set php /usr/bin/php8.3 + cd /var/www/ludikevent-intranet && sudo update-alternatives --set php /usr/bin/php8.4 && php bin/console app:maintenance on && git reset --hard HEAD && git pull origin master && sh ./update.sh && php bin/console app:maintenance off && php bin/console app:purge-cloudflare && sudo update-alternatives --set php /usr/bin/php8.3 diff --git a/assets/admin.js b/assets/admin.js index 7cc19c9..3106034 100644 --- a/assets/admin.js +++ b/assets/admin.js @@ -14,6 +14,7 @@ import PlaningLogestics from "./libs/PlaningLogestics.js"; import {SortableReorder} from "./libs/SortableReorder.js"; import { StripeCommissionCalculator } from "./libs/StripeCommissionCalculator.js"; import { ProductAddOption } from "./libs/ProductAddOption.js"; +import { LeafletMap } from "./tools/LeafletMap.js"; // --- CONFIGURATION SENTRY --- Sentry.init({ @@ -43,7 +44,8 @@ const registerCustomElements = () => { { name: 'search-optionsdevis', class: SearchOptionsDevis, extends: 'button' }, { name: 'crm-editor', class: CrmEditor, extends: 'textarea' }, { name: 'stripe-commission-calculator', class: StripeCommissionCalculator, extends: 'div' }, - { name: 'product-add-option', class: ProductAddOption, extends: 'button' } + { name: 'product-add-option', class: ProductAddOption, extends: 'button' }, + { name: 'leaflet-map', class: LeafletMap } ]; elements.forEach(el => { diff --git a/assets/reserve.js b/assets/reserve.js index e57d3f8..c630edb 100644 --- a/assets/reserve.js +++ b/assets/reserve.js @@ -5,6 +5,7 @@ import { FlowReserve } from "./tools/FlowReserve.js"; import { FlowDatePicker } from "./tools/FlowDatePicker.js"; import { FlowAddToCart } from "./tools/FlowAddToCart.js"; import { LeafletMap } from "./tools/LeafletMap.js"; +import { LocalStorageClear } from "./tools/LocalStorageClear.js"; import * as Turbo from "@hotwired/turbo"; import { onLCP, onINP, onCLS } from 'web-vitals'; import AOS from 'aos'; @@ -253,7 +254,7 @@ const initRegisterLogic = () => { // --- INITIALISATION --- const registerComponents = () => { - const comps = [['utm-event', UtmEvent], ['utm-account', UtmAccount], ['cookie-banner', CookieBanner], ['leaflet-map', LeafletMap]]; + const comps = [['utm-event', UtmEvent], ['utm-account', UtmAccount], ['cookie-banner', CookieBanner], ['leaflet-map', LeafletMap], ['local-storage-clear', LocalStorageClear]]; comps.forEach(([name, cl]) => { if (!customElements.get(name)) customElements.define(name, cl); }); if(!customElements.get('flow-reserve')) diff --git a/assets/tools/LocalStorageClear.js b/assets/tools/LocalStorageClear.js new file mode 100644 index 0000000..2e6f8f1 --- /dev/null +++ b/assets/tools/LocalStorageClear.js @@ -0,0 +1,11 @@ + +export class LocalStorageClear extends HTMLElement { + connectedCallback() { + const keysAttr = this.getAttribute('keys'); + if (keysAttr) { + keysAttr.split(',').forEach(key => { + localStorage.removeItem(key); + }); + } + } +} diff --git a/cert/email_smime.p12 b/cert/email_smime.p12 new file mode 100644 index 0000000..8c853ab Binary files /dev/null and b/cert/email_smime.p12 differ diff --git a/cert/email_smime.pem b/cert/email_smime.pem new file mode 100644 index 0000000..0cd1a33 --- /dev/null +++ b/cert/email_smime.pem @@ -0,0 +1,125 @@ +Bag Attributes + localKeyID: DF 79 62 A4 C4 A4 70 75 2D 68 FF C7 FD AF 35 E1 7B BE A0 75 + friendlyName: no-reply@esy-web.fr +subject=CN = no-reply@esy-web.fr +issuer=C = IT, ST = Bergamo, L = Ponte San Pietro, O = Actalis S.p.A., CN = Actalis Client Authentication CA G3 +-----BEGIN CERTIFICATE----- +MIIF6zCCA9OgAwIBAgIQMdJDXJvY5Eybkx2x+JNOYjANBgkqhkiG9w0BAQsFADCB +gTELMAkGA1UEBhMCSVQxEDAOBgNVBAgMB0JlcmdhbW8xGTAXBgNVBAcMEFBvbnRl +IFNhbiBQaWV0cm8xFzAVBgNVBAoMDkFjdGFsaXMgUy5wLkEuMSwwKgYDVQQDDCNB +Y3RhbGlzIENsaWVudCBBdXRoZW50aWNhdGlvbiBDQSBHMzAeFw0yNjAyMDQwOTMy +MDhaFw0yNzAyMDQwOTMyMDhaMB4xHDAaBgNVBAMME25vLXJlcGx5QGVzeS13ZWIu +ZnIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDQFhbttYZYfQc/FZ9N +qikTdDoA5jvXaZLKtmadF51M4jHypRf/4c8kmMm/zFkGHeu3Syprp1bxEpOvDtyj +1UmNoiOBEax/Gf6J4CSCz2Naf39Ps2wOpxJlwNI50zhTcGZa3r5ns33t2lvZd9Ul +1xKs58YkX99Af83qSCHbzYtyjqX9NKlkK5wHvzS1/Xd56SMxrCd6VkTvYG23QLMx +5LHK2Ei2IOMEZ4XxXKKmoXfvUtbtCfFs0l8n12Xnu7W4k3t1ijwQ0oVZ8DPoNPQX +yMKZ3rPlM2BkxkX2TjuDBJrArhqF3V3oUlYeFhiTUvt1bzPGZCMPy095QcINul3K +6VGHAgMBAAGjggG/MIIBuzAMBgNVHRMBAf8EAjAAMB8GA1UdIwQYMBaAFL6XqaqE +v4C/EFN9CTL54S4yG893MH4GCCsGAQUFBwEBBHIwcDA7BggrBgEFBQcwAoYvaHR0 +cDovL2NhY2VydC5hY3RhbGlzLml0L2NlcnRzL2FjdGFsaXMtYXV0Y2xpZzMwMQYI +KwYBBQUHMAGGJWh0dHA6Ly9vY3NwMDkuYWN0YWxpcy5pdC9WQS9BVVRIQ0wtRzMw +HgYDVR0RBBcwFYETbm8tcmVwbHlAZXN5LXdlYi5mcjBSBgNVHSAESzBJMDwGBiuB +HxABATAyMDAGCCsGAQUFBwIBFiRodHRwczovL3d3dy5hY3RhbGlzLml0L2FyZWEt +ZG93bmxvYWQwCQYHZ4EMAQUBAjAdBgNVHSUEFjAUBggrBgEFBQcDAgYIKwYBBQUH +AwQwSAYDVR0fBEEwPzA9oDugOYY3aHR0cDovL2NybDA5LmFjdGFsaXMuaXQvUmVw +b3NpdG9yeS9BVVRIQ0wtRzMvZ2V0TGFzdENSTDAdBgNVHQ4EFgQU33lipMSkcHUt +aP/H/a814Xu+oHUwDgYDVR0PAQH/BAQDAgWgMA0GCSqGSIb3DQEBCwUAA4ICAQB/ +iPZkXh6YIwf7dzjIyyLoO/ZwXVF63jR9Yvdrzt+UIkgbN3wieLOS+9feC5IDgcJ0 +3zl3NkBODVqM5r1fMuhOGazZirZ35jmfV7FgAWEGzuCDiVPKc95hegUhoCnvGniE +UKn0NAJvkk0ml6n95Yxk6PwAtE9B4uNCJxGf5gEAWgi2Ln35j3GVqkwrryiYf3qe +TsWTqGOoS+b/RF6Vxl+BuVPuVK9hhNi6b4sE3OltBeL71u4JnLUpwxpSqCbxsvgd +XEfYZnsIrI44rE2DGGJI/pQ4Oc4URI/PbyuSkLgqZ2C2zjw/gJcztiNk3CahtqxE +ZoBc3XhxAjTsCS5JYsgZWl8n9oI6Vb92zNxGd1I/aFEh9qn4YlWp2g9DwMzoH0Yo +XLZVHZz0dgzZNMWPgKlHhrs7hrYg80b5MuH0kUaP/CEknuVqm77BRp6WdUhAt493 +FSM6wlMggyDFKJfo/VDJUxpOtLwh/gP44Vkgejq99mueOswhd0tZnBuDdXfEEtqG +HBIOGivnLUaJM1zI8C+j56ZaHq8esLR203sW4jq23HE17VE3yKuGBR++vvEuq1QN +PV36jR65aBsCyL9zVcyudficLeR6gbIry2BCSDh06yAIMF/vOGKU1eeBD96SBwbl +IpXYYRRgxMej5xt2p+cKvndOrmEkA80qQ2DsT6RcJA== +-----END CERTIFICATE----- +Bag Attributes + 2.16.840.1.113894.746875.1.1: + + friendlyName: Actalis Client Authentication CA G3 +subject=C = IT, ST = Bergamo, L = Ponte San Pietro, O = Actalis S.p.A., CN = Actalis Client Authentication CA G3 +issuer=C = IT, L = Milan, O = Actalis S.p.A./03358520967, CN = Actalis Authentication Root CA +-----BEGIN CERTIFICATE----- +MIIHbTCCBVWgAwIBAgIQFxA+3j2KHLXKBlGT58pDazANBgkqhkiG9w0BAQsFADBr +MQswCQYDVQQGEwJJVDEOMAwGA1UEBwwFTWlsYW4xIzAhBgNVBAoMGkFjdGFsaXMg +Uy5wLkEuLzAzMzU4NTIwOTY3MScwJQYDVQQDDB5BY3RhbGlzIEF1dGhlbnRpY2F0 +aW9uIFJvb3QgQ0EwHhcNMjAwNzA2MDg0NTQ3WhcNMzAwOTIyMTEyMjAyWjCBgTEL +MAkGA1UEBhMCSVQxEDAOBgNVBAgMB0JlcmdhbW8xGTAXBgNVBAcMEFBvbnRlIFNh +biBQaWV0cm8xFzAVBgNVBAoMDkFjdGFsaXMgUy5wLkEuMSwwKgYDVQQDDCNBY3Rh +bGlzIENsaWVudCBBdXRoZW50aWNhdGlvbiBDQSBHMzCCAiIwDQYJKoZIhvcNAQEB +BQADggIPADCCAgoCggIBAO3mh5ahwaS27cJCVfc/Dw8iYF8T4KZDiIZJkXkcGy8a +UA/cRgHu9ro6hsxRYe/ED4AIcSlarRh82HqtFSVQs4ZwikQW1V/icCIS91C2IVAG +a1YlKfedqgweqky+bBniUvRevVT0keZOqRTcO5hw007dL6FhYNmlZBt5IaJs1V6I +niRjokOHR++qWgrUGy5LefY6ACs9gZ8Bi0OMK9PZ37pibeQCsdmMRytl4Ej7JVWe +M/BtNIIprHwO1LY0/8InpGOmdG+5LC6xHLzg53B0HvVUqzUQNePUhNwJZFmmTP46 +FXovxmH4/SuY5IkXop0eJqjN+dxRHHizngYUk1EaTHUOcLFy4vQ0kxgbjb+GsNg6 +M2/6gZZIRk78JPdpotIwHnBNtkp9wPVH61NqdcP7kbPkyLXkNMTtAfydpmNnGqqH +LEvUrK4iBpUPG9C09KOjm9OyhrT2uf5SLzJsee9g79r/rw4hAgcsZtR3YI6fCbRO +JncmD+hgbHCck+9TWcNc1x5xZMgm8UXmoPamkkfceAlVV49QQ5jUTgqneTQHyF1F +2ExXmf47pEIoJMVxloRIXywQuB2uqcIs8/X6tfsMDynFmhfT/0mTrgQ6xt9DIsgm +WuuhvZhLReWS7oeKxnyqscuGeTMXnLs7fjGZq0inyhnlznhA/4rl+WdNjNaO4jEv +AgMBAAGjggH0MIIB8DAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFFLYiDrI +n3hm7YnzezhwlMkCAjbQMEEGCCsGAQUFBwEBBDUwMzAxBggrBgEFBQcwAYYlaHR0 +cDovL29jc3AwNS5hY3RhbGlzLml0L1ZBL0FVVEgtUk9PVDBFBgNVHSAEPjA8MDoG +BFUdIAAwMjAwBggrBgEFBQcCARYkaHR0cHM6Ly93d3cuYWN0YWxpcy5pdC9hcmVh +LWRvd25sb2FkMB0GA1UdJQQWMBQGCCsGAQUFBwMCBggrBgEFBQcDBDCB4wYDVR0f +BIHbMIHYMIGWoIGToIGQhoGNbGRhcDovL2xkYXAwNS5hY3RhbGlzLml0L2NuJTNk +QWN0YWxpcyUyMEF1dGhlbnRpY2F0aW9uJTIwUm9vdCUyMENBLG8lM2RBY3RhbGlz +JTIwUy5wLkEuJTJmMDMzNTg1MjA5NjcsYyUzZElUP2NlcnRpZmljYXRlUmV2b2Nh +dGlvbkxpc3Q7YmluYXJ5MD2gO6A5hjdodHRwOi8vY3JsMDUuYWN0YWxpcy5pdC9S +ZXBvc2l0b3J5L0FVVEgtUk9PVC9nZXRMYXN0Q1JMMB0GA1UdDgQWBBS+l6mqhL+A +vxBTfQky+eEuMhvPdzAOBgNVHQ8BAf8EBAMCAQYwDQYJKoZIhvcNAQELBQADggIB +ACab5xtZDXSzEgPp51X3hICFzULDO2EcV8em5hLfSCKxZR9amCnjcODVfMbaKfdU +ZXtevMIIZmHgkz9dBan7ijGbJXjZCPP29zwZGSyCjpfadg5s9hnNCN1r3DGwIHfy +LgbcfffDyV/2wW+XTGbhldnazZsX892q+srRmC8XnX4ygg+eWL/AkHDenvbFuTlJ +vUyd5I7e1nb3dYXMObPu24ZTQ9/K1hSQbs7pqecaptTUjoIDpBUpSp4Us+h1I4MA +WonemKYoPS9f0y65JrRCKcfsKSI+1kwPSanDDMiydKzeo46XrS0hlA5NzQjqUJ7U +suGvPtDvknqc0v03nNXBnUjejYtvwO3sEDXdUW5m9kjNqlQZXzdHumZJVqPUGKTW +cn9Hf3d7qbCmmxPXjQoNUuHg56fLCanZWkEO4SP1GAgIA7SyJu/yffv0ts7sBFrS +TD3L2mCAXM3Y8BfblvvDSf2bvySm/fPe9brmuzrCXsTxUQc1+/z5ydvzV3E3cLnU +oSXP6XfXNyEVO6sPkcUSnISHM798xLkCTB5EkjPCjPE2zs4v9L9JVOkkskvW6RnW +WccdfR3fELNHL/kep8re6IbbYs8Hn5GM0Ohs8CMDPYEox+QX/6/SnOfyaqqSilBo +nMQBstsymBBgdEKO+tTHHCMnJQVvZn7jRQ20wXgxMrvN +-----END CERTIFICATE----- +Bag Attributes + 2.16.840.1.113894.746875.1.1: + friendlyName: Actalis Authentication Root CA +subject=C = IT, L = Milan, O = Actalis S.p.A./03358520967, CN = Actalis Authentication Root CA +issuer=C = IT, L = Milan, O = Actalis S.p.A./03358520967, CN = Actalis Authentication Root CA +-----BEGIN CERTIFICATE----- +MIIFuzCCA6OgAwIBAgIIVwoRl0LE48wwDQYJKoZIhvcNAQELBQAwazELMAkGA1UE +BhMCSVQxDjAMBgNVBAcMBU1pbGFuMSMwIQYDVQQKDBpBY3RhbGlzIFMucC5BLi8w +MzM1ODUyMDk2NzEnMCUGA1UEAwweQWN0YWxpcyBBdXRoZW50aWNhdGlvbiBSb290 +IENBMB4XDTExMDkyMjExMjIwMloXDTMwMDkyMjExMjIwMlowazELMAkGA1UEBhMC +SVQxDjAMBgNVBAcMBU1pbGFuMSMwIQYDVQQKDBpBY3RhbGlzIFMucC5BLi8wMzM1 +ODUyMDk2NzEnMCUGA1UEAwweQWN0YWxpcyBBdXRoZW50aWNhdGlvbiBSb290IENB +MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAp8bEpSmkLO/lGMWwUKNv +UTufClrJwkg4CsIcoBh/kbWHuUA/3R1oHwiD1S0eiKD4j1aPbZkCkpAW1V8IbInX +4ay8IMKx4INRimlNAJZaby/ARH6jDuSRzVju3PvHHkVH3Se5CAGfpiEd9UEtL0z9 +KK3giq0itFZljoZUj5NDKd45RnijMCO6zfB9E1fAXdKDa0hMxKufgFpbOr3JpyI/ +gCczWw63igxdBzcIy2zSekciRDXFzMwujt0q7bd9Zg1fYVEiVRvjRuPjPdA1Yprb +rxTIW6HMiRvhMCb8oJsfgadHHwTrozmSBp+Z07/T6k9QnBn+locePGX2oxgkg4YQ +51Q+qDp2JE+BIcXjDwL4k5RHILv+1A7TaLndxHqEguNTVHnd25zS8gebLra8Pu2F +be8lEfKXGkJh90qX6IuxEAf6ZYGyojnP9zz/GPvG8VqLWeICrHuS0E4UT1lF9gxe +KF+w6D9Fz8+vm2/7hNN3WpVvrJSEnu68wEqPSpP4RCHiMUVhUE4Q2OM1fEwZtN4F +v6MGn8i1zeQf1xcGDXqVdFUNaBr8EBtiZJ1t4JWgw5QHVw0U5r0F+7if5t+L4sbn +fpb2U8WANFAoWPASUHEXMLrmeGO89LKtmyuy/uE5jF66CyCU3nuDuP/jVo23Eek7 +jPKxwV2dpAtMK9myGPW1n0sCAwEAAaNjMGEwHQYDVR0OBBYEFFLYiDrIn3hm7Ynz +ezhwlMkCAjbQMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUUtiIOsifeGbt +ifN7OHCUyQICNtAwDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3DQEBCwUAA4ICAQAL +e3KHwGCmSUyIWOYdiPcUZEim2FgKDk8TNd81HdTtBjHIgT5q1d07GjLukD0R0i70 +jsNjLiNmsGe+b7bAEzlgqqI0JZN1Ut6nna0Oh4lScWoWPBkdg/iaKWW+9D+a2fDz +WochcYBNy+A4mz+7+uAwTc+G02UQGRjRlwKxK3JCaKygvU5a2hi/a5iB0P2avl4V +SM0RFbnAKVy06Ij3Pjaut2L9HmLecHgQHEhb2rykOLpn7VU+Xlff1ANATIGk0k9j +pwlCCRT8AKnCgHNPLsBA2RF7SOp6AsDT6ygBJlh0wcBzIm2Tlf05fbsq4/aC4yyX +X04fkZT6/iyj2HYauE2yOE+b+h1IYHkm4vP9qdCa6HCPSXrW5b0KDtst842/6+Ok +fcvHlXHo2qN8xcL4dJIEG4aspCJTQLas/kx2z/uUMsA1n3Y/buWQbqCmJqK4LL7R +K4X9p2jIugErsWx0Hbhzlefut8cl8ABMALJ+tguLHPPAUJ4lueAI3jZm/zel0btU +ZCzJJ7VLkn5l/9Mt4blOvH+kQSGQQXemOR/qnuOf0GZvBeyqdn6/axag67XH/JJU +LysRJyU3eExRarDzzFhdFPFqSBX/wge2sY0PjlxQRrM9vwGYT7JZVEc+NHt4bVaT +LnPqZih4zR0Uv6CPLy64Lo7yFIrM6bV8+2ydDKXhlg== +-----END CERTIFICATE----- diff --git a/cert/email_smime_key.pem b/cert/email_smime_key.pem new file mode 100644 index 0000000..523d1e5 --- /dev/null +++ b/cert/email_smime_key.pem @@ -0,0 +1,32 @@ +Bag Attributes + localKeyID: DF 79 62 A4 C4 A4 70 75 2D 68 FF C7 FD AF 35 E1 7B BE A0 75 + friendlyName: no-reply@esy-web.fr +Key Attributes: +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDQFhbttYZYfQc/ +FZ9NqikTdDoA5jvXaZLKtmadF51M4jHypRf/4c8kmMm/zFkGHeu3Syprp1bxEpOv +Dtyj1UmNoiOBEax/Gf6J4CSCz2Naf39Ps2wOpxJlwNI50zhTcGZa3r5ns33t2lvZ +d9Ul1xKs58YkX99Af83qSCHbzYtyjqX9NKlkK5wHvzS1/Xd56SMxrCd6VkTvYG23 +QLMx5LHK2Ei2IOMEZ4XxXKKmoXfvUtbtCfFs0l8n12Xnu7W4k3t1ijwQ0oVZ8DPo +NPQXyMKZ3rPlM2BkxkX2TjuDBJrArhqF3V3oUlYeFhiTUvt1bzPGZCMPy095QcIN +ul3K6VGHAgMBAAECggEAJiiG19x74Go9E/JxHhrgIXMk19lgi+YORyIfuxsDe3/X +TPm9Zu0PwVRLWuKsSJTgDuK3yroLFXYkouuExn5sWI6tGBQXn88ygDIcP+ir/YJ3 +5DOw0xcMJqCvbG8xWwu3mV5TaEzgAOgUA9MXwTKpeA+gpDb4h6loJ1hG7TnKIp11 +SfGE85uiFfvQuCKuZpBXfK3cN4DncZOMhkK/m5e6owy4wf6BjoNg5pWDO0DbfIn0 +YhInBY1Vt29TtUcdRqG0iolcly8E91OqtQ4+kuQY5R1DGaEdG5HJ3leR/YXdYLaD +cvstkAZ6Wkepz2nZ2HtELXeFXYEe0g456jiMPpndUQKBgQDzbpV6iAhKiU/mmm9T +IoTkSiFdWWZaOIC7zHn0KvUd/Q/JcxsSpkCsS2CR7sw6Upc0HdK4Ol6OKF5xIijZ +Ry51xiOvMU2IE6/3RK3Yc27JK9n6hjkupaY6ncVzw9KZvHPWJrA9ohIof5hevBUt +vSDEqD95osrG0wulmBIW6VbyWwKBgQDa1FhmmljKlD/fjfTYhkgKpterIJ112EDT +v2wXH1GZ9vN8xo2ZN9T67APHxA7dz72IkMqQJ9YzpkhdWUKLCNOYECdNxE38ajyd +arfPkmbCfS6DSgMgDUg7DuLV3iDF9O9+6zK88RY/wHH3NVuvFNvGpps1JBkxoR6N +br7uf6AtRQKBgB5X1WgNlsL7tKrw6xl4vwnZt6j0IM70Jg/CoBzwUddoGNSqdWBw +urT/PE1Ub76BVvmXEhIGrIyZuZPuhxr2RXNGvGH1Ck4A7jkrJWRKly+aOLSCkJQg +78R0sA0LBrBFDkdOUT+NeSf1J5//X8Bwx6nbsvXNdessmVINz4ttkHnrAoGAERzL +8PIfAXCIci4CuK//kD2t3ecGCUIpB6YPiNtdIUIrllVcm1+/WwP747JUS0pEkxpn +jNBgstdND2e8iWzeRyT0PeOdCaExLko7J5NWT91ENuYhym7feCbY3Eqrm29lDzLL +W/UqfT/Kab+VdOKXsTg0KPqysavc3MiNS89VMlUCgYEA1z338zWm1VyNBhzuC7Ui +jHerwf26rUAwDP+DqMPBwTezOltpr+7r89PKylEZJfOgoEiLMD6vBVdwvGCjAOEF +dZ+zUMArk3X8C/Cdvx9p1dUYwHD9usgrfsXqin1JYPgkEXzc7cBuKCt1L+ZE1p+7 +ETkTi8pYJ03ZieAFEV3qW9M= +-----END PRIVATE KEY----- diff --git a/migrations/Version20260204131618.php b/migrations/Version20260204131618.php new file mode 100644 index 0000000..cadb33b --- /dev/null +++ b/migrations/Version20260204131618.php @@ -0,0 +1,33 @@ +addSql('ALTER TABLE order_session ADD delivery_distance DOUBLE PRECISION DEFAULT NULL'); + $this->addSql('ALTER TABLE order_session ADD delivery_price DOUBLE PRECISION DEFAULT NULL'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('ALTER TABLE order_session DROP delivery_distance'); + $this->addSql('ALTER TABLE order_session DROP delivery_price'); + } +} diff --git a/migrations/Version20260204141133.php b/migrations/Version20260204141133.php new file mode 100644 index 0000000..6fa02f4 --- /dev/null +++ b/migrations/Version20260204141133.php @@ -0,0 +1,33 @@ +addSql('ALTER TABLE order_session ADD delivery_geometry JSON DEFAULT NULL'); + $this->addSql('ALTER TABLE order_session ADD type_paiement VARCHAR(255) DEFAULT NULL'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('ALTER TABLE order_session DROP delivery_geometry'); + $this->addSql('ALTER TABLE order_session DROP type_paiement'); + } +} diff --git a/migrations/Version20260204142338.php b/migrations/Version20260204142338.php new file mode 100644 index 0000000..b157ade --- /dev/null +++ b/migrations/Version20260204142338.php @@ -0,0 +1,33 @@ +addSql('ALTER TABLE customer ADD typ_company VARCHAR(255) DEFAULT NULL'); + $this->addSql('ALTER TABLE customer ADD raison_social VARCHAR(255) DEFAULT NULL'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('ALTER TABLE customer DROP typ_company'); + $this->addSql('ALTER TABLE customer DROP raison_social'); + } +} diff --git a/src/Controller/ContratController.php b/src/Controller/ContratController.php index 853c8a1..6ef1fa3 100644 --- a/src/Controller/ContratController.php +++ b/src/Controller/ContratController.php @@ -62,21 +62,6 @@ class ContratController extends AbstractController EntityManagerInterface $entityManager, ): Response { $type = $request->query->get('type', 'accompte'); - - if ($type === "caution") { - $pl = $entityManager->getRepository(ContratsPayments::class)->findOneBy([ - 'type' => 'caution', - 'contrat' => $contrat - ]); - - // Si le paiement est déjà marqué comme complété par le Webhook - if ($pl && $pl->getState() === "complete") { - return $this->render('reservation/contrat/success.twig', [ - 'contrat' => $contrat, - 'type' => $type - ]); - } - } if ($type === "accompte") { $pl = $entityManager->getRepository(ContratsPayments::class)->findOneBy([ @@ -273,43 +258,6 @@ class ContratController extends AbstractController } } - // --- GESTION PAIEMENT CAUTION (GARANTIE) --- - if ($request->query->get('act') === 'cautionPay') { - // On vérifie si une caution n'est pas déjà validée - $existingCaution = $entityManager->getRepository(ContratsPayments::class)->findOneBy([ - 'contrat' => $contrat, - 'type' => 'caution', - 'state' => 'complete' - ]); - - if (!$existingCaution) { - // Appel au service Stripe pour la caution (montant totalCaution calculé plus haut) - $result = $stripeClient->createPaymentCaution($totalCaution, $contrat); - - if ($result['state']) { - $pl = new ContratsPayments(); - $pl->setContrat($contrat); - $pl->setType("caution"); - $pl->setAmount($totalCaution); - $pl->setPaymentAt(new \DateTimeImmutable('now')); - $pl->setState("created"); - $pl->setPaymentId($result['id']); - - $entityManager->persist($pl); - $entityManager->flush(); - - return new RedirectResponse($result['url']); - } - } else { - // SCÉNARIO 2 : RÉCUPÉRATION OU MISE À JOUR (si le montant a changé par exemple) - $result = $stripeClient->linkPaymentCaution($totalCaution, $contrat, $existingCaution); - - if ($result['state']) { - return new RedirectResponse($result['url']); - } - } - } - if ($request->query->has('act') && $request->query->get('act') === 'soldePay') { // 1. Récupération et sécurisation du montant $amountRequested = (float) $request->query->get('amountToPay', $solde); diff --git a/src/Controller/Dashboard/FlowController.php b/src/Controller/Dashboard/FlowController.php new file mode 100644 index 0000000..11d3e3b --- /dev/null +++ b/src/Controller/Dashboard/FlowController.php @@ -0,0 +1,162 @@ +appLogger->record('VIEW', 'Consultation des réservations en ligne'); + + $query = $this->orderSessionRepository->findBy(['state' => 'send'], ['id' => 'DESC']); + + $pagination = $paginator->paginate( + $query, + $request->query->getInt('page', 1), + 20 + ); + + return $this->render('dashboard/flow/index.twig', [ + 'sessions' => $pagination, + ]); + } + #[Route('/{id}', name: 'app_crm_flow_view', methods: ['GET'])] + public function view(\App\Entity\OrderSession $session, \Doctrine\ORM\EntityManagerInterface $em): Response + { + $this->appLogger->record('VIEW', 'Consultation détails réservation en ligne #' . $session->getId()); + + // Auto-calculation of delivery if missing or geometry missing + if (($session->getDeliveryDistance() === null || $session->getDeliveryPrice() === null || $session->getDeliveryGeometry() === null) && $session->getAdressEvent()) { + $this->calculateDelivery($session); + $em->flush(); + } + + return $this->render('dashboard/flow/view.twig', [ + 'session' => $session, + ]); + } + + #[Route('/update/{id}', name: 'app_crm_flow_update', methods: ['POST'])] + public function update(\App\Entity\OrderSession $session, Request $request, \Doctrine\ORM\EntityManagerInterface $em): Response + { + if ($request->request->has('deliveryDistance')) { + $session->setDeliveryDistance((float)$request->request->get('deliveryDistance')); + } + if ($request->request->has('deliveryPrice')) { + $session->setDeliveryPrice((float)$request->request->get('deliveryPrice')); + } + if ($request->request->has('typePaiement')) { + $session->setTypePaiement($request->request->get('typePaiement')); + } + + // Recalculate if address changed or forced update (optional, but good for consistency) + // For now, simple update. + + $em->flush(); + + $this->addFlash('success', 'Informations mises à jour.'); + + return $this->redirectToRoute('app_crm_flow_view', ['id' => $session->getId()]); + } + + #[Route('/allow/{id}', name: 'app_crm_flow_allow', methods: ['GET'])] + public function allow(\App\Entity\OrderSession $session, \Doctrine\ORM\EntityManagerInterface $em): Response + { + $session->setState('allow'); + $em->flush(); + + $this->addFlash('success', 'La réservation a été validée.'); + + return $this->redirectToRoute('app_crm_flow'); + } + + #[Route('/delete/{id}', name: 'app_crm_flow_delete', methods: ['GET'])] + public function delete(\App\Entity\OrderSession $session, \Doctrine\ORM\EntityManagerInterface $em): Response + { + $em->remove($session); + $em->flush(); + + $this->addFlash('success', 'La réservation a été supprimée (refusée).'); + + return $this->redirectToRoute('app_crm_flow'); + } + + private function calculateDelivery(\App\Entity\OrderSession $session): void + { + if (!$session->getAdressEvent() || !$session->getZipCodeEvent() || !$session->getTownEvent()) { + return; + } + + $query = sprintf('%s %s %s', $session->getAdressEvent(), $session->getZipCodeEvent(), $session->getTownEvent()); + + try { + $response = $this->client->request('GET', 'https://api-adresse.data.gouv.fr/search/', [ + 'query' => [ + 'q' => $query, + 'limit' => 1 + ] + ]); + + $content = $response->toArray(); + + if (!empty($content['features'])) { + $coords = $content['features'][0]['geometry']['coordinates']; + $lon = $coords[0]; + $lat = $coords[1]; + + // Point de départ (LudikEvent) + $startLat = 49.849; + $startLon = 3.286; + + // Calcul itinéraire via API Geoplateforme + $itineraireResponse = $this->client->request('GET', 'https://data.geopf.fr/navigation/itineraire', [ + 'query' => [ + 'resource' => 'bdtopo-osrm', + 'start' => $startLon . ',' . $startLat, + 'end' => $lon . ',' . $lat, + 'profile' => 'car', + 'optimization' => 'fastest', + 'distanceUnit' => 'kilometer', + 'geometryFormat' => 'geojson' + ] + ]); + + $itineraire = $itineraireResponse->toArray(); + $distance = $itineraire['distance']; + $geometry = $itineraire['geometry'] ?? null; + + $rate = 0.50; + $trips = 4; + $price = 0.0; + + if ($distance > 10) { + $chargedDistance = $distance - 10; + $price = ($chargedDistance * $trips) * $rate; + } + + $session->setDeliveryDistance($distance); + $session->setDeliveryPrice($price); + $session->setDeliveryGeometry($geometry); + } + } catch (\Exception $e) { + // Log error or silent fail + } + } +} diff --git a/src/Controller/ReserverController.php b/src/Controller/ReserverController.php index 93cc05b..2872a61 100644 --- a/src/Controller/ReserverController.php +++ b/src/Controller/ReserverController.php @@ -50,13 +50,18 @@ class ReserverController extends AbstractController string $sessionId, OrderSessionRepository $repository, ProductRepository $productRepository, - KernelInterface $kernel + KernelInterface $kernel, + \App\Repository\OptionsRepository $optionsRepository ): Response { $session = $repository->findOneBy(['uuid' => $sessionId]); if (!$session) { return $this->redirectToRoute('reservation'); } + if ($session->getState() === 'send') { + return $this->redirectToRoute('reservation_flow_success', ['sessionId' => $sessionId]); + } + $sessionData = $session->getProducts(); $ids = $sessionData['ids'] ?? []; $startStr = $sessionData['start'] ?? null; @@ -106,15 +111,50 @@ class ReserverController extends AbstractController $devis->setStartAt($start); $devis->setEndAt($end); + $selectedOptionsMap = $sessionData['options'] ?? []; + if (!empty($ids)) { $products = $productRepository->findBy(['id' => $ids]); + $processedProductIds = []; + foreach ($products as $product) { + $processedProductIds[] = $product->getId(); + $line = new DevisLine(); $line->setProduct($product->getName()); $line->setPriceHt($product->getPriceDay()); $line->setPriceHtSup($product->getPriceSup()); $line->setDay($duration); $devis->addDevisLine($line); + + if (isset($selectedOptionsMap[$product->getId()])) { + $optionIds = $selectedOptionsMap[$product->getId()]; + if (!empty($optionIds)) { + $options = $optionsRepository->findBy(['id' => $optionIds]); + foreach ($options as $option) { + $lineOpt = new DevisLine(); + $lineOpt->setProduct("Option : " . $option->getName()); + $lineOpt->setPriceHt($option->getPriceHt()); + $lineOpt->setPriceHtSup(0); + $lineOpt->setDay($duration); + $devis->addDevisLine($lineOpt); + } + } + } + } + + foreach ($selectedOptionsMap as $prodId => $optIds) { + if (!in_array($prodId, $processedProductIds) && !empty($optIds)) { + $options = $optionsRepository->findBy(['id' => $optIds]); + foreach ($options as $option) { + $lineOpt = new DevisLine(); + $lineOpt->setProduct("Option : " . $option->getName()); + $lineOpt->setPriceHt($option->getPriceHt()); + $lineOpt->setPriceHtSup(0); + $lineOpt->setDay($duration); + $devis->addDevisLine($lineOpt); + } + } } } @@ -494,13 +534,35 @@ class ReserverController extends AbstractController UploaderHelper $uploaderHelper, ProductReserveRepository $productReserveRepository, EntityManagerInterface $em, - \App\Repository\OptionsRepository $optionsRepository + \App\Repository\OptionsRepository $optionsRepository, + HttpClientInterface $client, + Request $request, + Mailer $mailer ): Response { $session = $repository->findOneBy(['uuid' => $sessionId]); if (!$session) { return $this->render('revervation/session_lost.twig'); } + if ($session->getState() === 'send') { + return $this->redirectToRoute('reservation_flow_success', ['sessionId' => $sessionId]); + } + + if ($request->isMethod('POST')) { + $mailer->send( + 'contact@ludikevent.fr', + "Ludikevent", + "[Ludikevent] - Nouvelle demande de réservation", + "mails/reserve/confirmation.twig", + ['session' => $session] + ); + + $session->setState('send'); + $em->flush(); + $request->getSession()->remove('order_session_uuid'); + return $this->redirectToRoute('reservation_flow_success', ['sessionId' => $sessionId]); + } + $sessionData = $session->getProducts(); $ids = $sessionData['ids'] ?? []; $selectedOptionsMap = $sessionData['options'] ?? []; @@ -620,6 +682,73 @@ class ReserverController extends AbstractController $totalTva = $totalHT * $tvaRate; $totalTTC = $totalHT + $totalTva; + // --- Calcul Frais de Livraison --- + $deliveryEstimation = null; + $deliveryDetails = null; + $deliveryGeometry = null; + + if ($session->getAdressEvent() && $session->getZipCodeEvent() && $session->getTownEvent()) { + $query = sprintf('%s %s %s', $session->getAdressEvent(), $session->getZipCodeEvent(), $session->getTownEvent()); + try { + $response = $client->request('GET', 'https://api-adresse.data.gouv.fr/search/', [ + 'query' => [ + 'q' => $query, + 'limit' => 1 + ] + ]); + + $content = $response->toArray(); + + if (!empty($content['features'])) { + $coords = $content['features'][0]['geometry']['coordinates']; + $lon = $coords[0]; + $lat = $coords[1]; + + // Point de départ (LudikEvent) + $startLat = 49.849; + $startLon = 3.286; + + // Calcul itinéraire via API Geoplateforme + $itineraireResponse = $client->request('GET', 'https://data.geopf.fr/navigation/itineraire', [ + 'query' => [ + 'resource' => 'bdtopo-osrm', + 'start' => $startLon . ',' . $startLat, + 'end' => $lon . ',' . $lat, + 'profile' => 'car', + 'optimization' => 'fastest', + 'distanceUnit' => 'kilometer', + 'geometryFormat' => 'geojson' + ] + ]); + + $itineraire = $itineraireResponse->toArray(); + $distance = $itineraire['distance']; + $deliveryGeometry = $itineraire['geometry'] ?? null; + + $rate = 0.50; + $trips = 4; + + if ($distance <= 10) { + $deliveryEstimation = 0.0; + $chargedDistance = 0.0; + } else { + $chargedDistance = $distance - 10; + $deliveryEstimation = ($chargedDistance * $trips) * $rate; + } + + $deliveryDetails = [ + 'distance' => $distance, + 'chargedDistance' => $chargedDistance, + 'trips' => $trips, + 'rate' => $rate, + 'isFree' => ($distance <= 10) + ]; + } + } catch (\Exception $e) { + // Silent fail for delivery calculation in flow + } + } + return $this->render('revervation/flow_confirmed.twig', [ 'session' => $session, 'cart' => [ @@ -632,10 +761,31 @@ class ReserverController extends AbstractController 'totalTva' => $totalTva, 'totalTTC' => $totalTTC, 'tvaEnabled' => $tvaEnabled, + ], + 'delivery' => [ + 'estimation' => $deliveryEstimation, + 'details' => $deliveryDetails, + 'geometry' => $deliveryGeometry ] ]); } + #[Route('/flow/{sessionId}/success', name: 'reservation_flow_success', methods: ['GET'])] + public function flowSuccess(string $sessionId, OrderSessionRepository $repository): Response + { + $session = $repository->findOneBy(['uuid' => $sessionId]); + + if (!$session) { + return $this->redirectToRoute('reservation'); + } + + if ($session->getState() !== 'send') { + return $this->redirectToRoute('reservation_flow', ['sessionId' => $sessionId]); + } + + return $this->render('revervation/success.twig'); + } + #[Route('/flow/{sessionId}', name: 'reservation_flow', methods: ['GET', 'POST'])] public function flowLogin( string $sessionId, @@ -643,8 +793,9 @@ class ReserverController extends AbstractController OrderSessionRepository $repository, ProductRepository $productRepository, UploaderHelper $uploaderHelper, - ProductReserveRepository $productReserveRepository, // Added dependency - EntityManagerInterface $em + ProductReserveRepository $productReserveRepository, + EntityManagerInterface $em, + \App\Repository\OptionsRepository $optionsRepository ): Response { // This is the POST target for the login form, but also the GET page. // The authenticator handles the POST. For GET, we just render the page. @@ -653,8 +804,13 @@ class ReserverController extends AbstractController return $this->render('revervation/session_lost.twig'); } + if ($session->getState() === 'send') { + return $this->redirectToRoute('reservation_flow_success', ['sessionId' => $sessionId]); + } + $sessionData = $session->getProducts(); $ids = $sessionData['ids'] ?? []; + $selectedOptionsMap = $sessionData['options'] ?? []; $startStr = $sessionData['start'] ?? null; $endStr = $sessionData['end'] ?? null; @@ -698,12 +854,45 @@ class ReserverController extends AbstractController $tvaEnabled = isset($_ENV['TVA_ENABLED']) && $_ENV['TVA_ENABLED'] === "true"; $tvaRate = $tvaEnabled ? 0.20 : 0; + $rootOptions = []; + $processedProductIds = []; + foreach ($products as $product) { + $processedProductIds[] = $product->getId(); $price1Day = $product->getPriceDay(); $priceSup = $product->getPriceSup() ?? 0.0; // Calcul du coût total pour ce produit selon la durée $productTotalHT = $price1Day + ($priceSup * max(0, $duration - 1)); + + // Traitement des options + $productOptions = []; + $optionsTotalHT = 0; + + if (isset($selectedOptionsMap[$product->getId()])) { + $optionIds = $selectedOptionsMap[$product->getId()]; + if (!empty($optionIds)) { + $optionsEntities = $optionsRepository->findBy(['id' => $optionIds]); + foreach ($optionsEntities as $option) { + $optPrice = $option->getPriceHt(); + $optData = [ + 'id' => $option->getId(), + 'name' => $option->getName(), + 'price' => $optPrice + ]; + + if ($product->getOptions()->contains($option)) { + $productOptions[] = $optData; + $optionsTotalHT += $optPrice; + } else { + $rootOptions[] = $optData; + $totalHT += $optPrice; + } + } + } + } + + $productTotalHT += $optionsTotalHT; $productTotalTTC = $productTotalHT * (1 + $tvaRate); $items[] = [ @@ -713,11 +902,28 @@ class ReserverController extends AbstractController 'priceSup' => $priceSup, 'totalPriceHT' => $productTotalHT, 'totalPriceTTC' => $productTotalTTC, + 'options' => $productOptions ]; $totalHT += $productTotalHT; } + // Traiter les options orphelines + foreach ($selectedOptionsMap as $prodId => $optIds) { + if (!in_array($prodId, $processedProductIds) && !empty($optIds)) { + $optionsEntities = $optionsRepository->findBy(['id' => $optIds]); + foreach ($optionsEntities as $option) { + $optPrice = $option->getPriceHt(); + $rootOptions[] = [ + 'id' => $option->getId(), + 'name' => $option->getName(), + 'price' => $optPrice + ]; + $totalHT += $optPrice; + } + } + } + $totalTva = $totalHT * $tvaRate; $totalTTC = $totalHT + $totalTva; @@ -727,6 +933,7 @@ class ReserverController extends AbstractController 'error' => $authenticationUtils->getLastAuthenticationError(), 'cart' => [ 'items' => $items, + 'options' => $rootOptions, 'startDate' => $startStr ? new \DateTimeImmutable($startStr) : null, 'endDate' => $endStr ? new \DateTimeImmutable($endStr) : null, 'duration' => $duration, @@ -750,6 +957,10 @@ class ReserverController extends AbstractController return $this->redirectToRoute('reservation'); } + if ($session->getState() === 'send') { + return $this->redirectToRoute('reservation_flow_success', ['sessionId' => $sessionId]); + } + $session->setBillingAddress($request->request->get('billingAddress')); $session->setBillingZipCode($request->request->get('billingZipCode')); $session->setBillingTown($request->request->get('billingTown')); @@ -922,6 +1133,8 @@ class ReserverController extends AbstractController if ($customer->getType() === 'buisness') { $customer->setSiret($payload->getString('siret')); + $customer->setRaisonSocial($payload->getString('raisonSocial')); + $customer->setTypCompany($payload->getString('typCompany')); } $hashedPassword = $hasher->hashPassword($customer, $payload->getString('password')); diff --git a/src/Controller/SignatureController.php b/src/Controller/SignatureController.php index 6a840b5..84c30ae 100644 --- a/src/Controller/SignatureController.php +++ b/src/Controller/SignatureController.php @@ -11,6 +11,7 @@ use App\Form\RequestPasswordRequestType; use App\Logger\AppLogger; use App\Repository\ContratsRepository; use App\Repository\DevisRepository; +use App\Repository\ProductRepository; use App\Service\Mailer\Mailer; use App\Service\ResetPassword\Event\ResetPasswordConfirmEvent; use App\Service\ResetPassword\Event\ResetPasswordEvent; @@ -38,6 +39,7 @@ class SignatureController extends AbstractController Client $client, DevisRepository $devisRepository, ContratsRepository $contratsRepository, + ProductRepository $productRepository, EntityManagerInterface $entityManager, Request $request, Mailer $mailer, @@ -83,6 +85,18 @@ class SignatureController extends AbstractController ]; $entityManager->persist($contrats); + + foreach ($contrats->getContratsLines() as $line) { + $p = $productRepository->findOneBy(['name' => $line->getName()]); + $pr = new ProductReserve(); + $pr->setContrat($contrats); + $pr->setProduct($p); + $pr->setStartAt($contrats->getDateAt()); + $pr->setEndAt($contrats->getEndAt()); + $pr->setCustomer($contrats->getCustomer()); + $pr->setDevis($contrats->getDevis()); + $entityManager->persist($pr); + } $entityManager->flush(); // 5. Envoi du mail de confirmation avec le récapitulatif diff --git a/src/Controller/Webhooks.php b/src/Controller/Webhooks.php index 3634a0a..a76d188 100644 --- a/src/Controller/Webhooks.php +++ b/src/Controller/Webhooks.php @@ -48,38 +48,6 @@ class Webhooks extends AbstractController $contrat = $pl->getContrat(); - - if($pl->getType() == "accompte") { - foreach ($contrat->getContratsLines() as $line) { - $p = $productRepository->findOneBy(['name' => $line->getProduct()]); - $pr = new ProductReserve(); - $pr->setContrat($contrat); - $pr->setProduct($p); - $pr->setStartAt($contrat->getDateAt()); - $pr->setEndAt($contrat->getEndAt()); - $pr->setCustomer($contrat->getCustomer()); - $pr->setDevis($contrat->getDevis()); - $entityManager->persist($pr); - $entityManager->flush(); - } - } - $contrat = $pl->getContrat(); - - if($pl->getType() == "accompte") { - foreach ($contrat->getContratsLines() as $line) { - $r = explode(" - ",$line->getName()); - if(isset($r[1])) { - $p = $productRepository->findOneBy(['ref' => $r[1]]); - $pr = new ProductReserve(); - $pr->setContrat($contrat); - $pr->setProduct($p); - $pr->setStartAt($contrat->getDateAt()); - $pr->setEndAt($contrat->getEndAt()); - $pr->setCustomer($contrat->getCustomer()); - $entityManager->persist($pr); - } - } - } $customer = $contrat->getCustomer(); $pdf = new PlPdf($kernel, $pl, $contrat); diff --git a/src/Entity/Customer.php b/src/Entity/Customer.php index c69c70f..44a2164 100644 --- a/src/Entity/Customer.php +++ b/src/Entity/Customer.php @@ -60,6 +60,12 @@ class Customer implements UserInterface, PasswordAuthenticatedUserInterface #[ORM\Column(length: 255, nullable: true)] private ?string $siret = null; + #[ORM\Column(length: 255, nullable: true)] + private ?string $typCompany = null; + + #[ORM\Column(length: 255, nullable: true)] + private ?string $raisonSocial = null; + #[ORM\Column(length: 255, nullable: true)] private ?string $customerId = null; @@ -245,6 +251,28 @@ class Customer implements UserInterface, PasswordAuthenticatedUserInterface return $this; } + public function getTypCompany(): ?string + { + return $this->typCompany; + } + + public function setTypCompany(?string $typCompany): static + { + $this->typCompany = $typCompany; + return $this; + } + + public function getRaisonSocial(): ?string + { + return $this->raisonSocial; + } + + public function setRaisonSocial(?string $raisonSocial): static + { + $this->raisonSocial = $raisonSocial; + return $this; + } + public function getCustomerId(): ?string { return $this->customerId; diff --git a/src/Entity/OrderSession.php b/src/Entity/OrderSession.php index d4f5c9f..0645dd2 100644 --- a/src/Entity/OrderSession.php +++ b/src/Entity/OrderSession.php @@ -78,6 +78,18 @@ class OrderSession #[ORM\Column(nullable: true)] private ?float $distancePower = null; + #[ORM\Column(nullable: true)] + private ?float $deliveryDistance = null; + + #[ORM\Column(nullable: true)] + private ?float $deliveryPrice = null; + + #[ORM\Column(type: Types::JSON, nullable: true)] + private ?array $deliveryGeometry = null; + + #[ORM\Column(length: 255, nullable: true)] + private ?string $typePaiement = null; + public function __construct() { $this->createdAt = new \DateTimeImmutable(); @@ -354,4 +366,52 @@ class OrderSession return $this; } + + public function getDeliveryDistance(): ?float + { + return $this->deliveryDistance; + } + + public function setDeliveryDistance(?float $deliveryDistance): static + { + $this->deliveryDistance = $deliveryDistance; + + return $this; + } + + public function getDeliveryPrice(): ?float + { + return $this->deliveryPrice; + } + + public function setDeliveryPrice(?float $deliveryPrice): static + { + $this->deliveryPrice = $deliveryPrice; + + return $this; + } + + public function getDeliveryGeometry(): ?array + { + return $this->deliveryGeometry; + } + + public function setDeliveryGeometry(?array $deliveryGeometry): static + { + $this->deliveryGeometry = $deliveryGeometry; + + return $this; + } + + public function getTypePaiement(): ?string + { + return $this->typePaiement; + } + + public function setTypePaiement(?string $typePaiement): static + { + $this->typePaiement = $typePaiement; + + return $this; + } } diff --git a/src/Service/Mailer/Mailer.php b/src/Service/Mailer/Mailer.php index 27bae05..35c18b1 100644 --- a/src/Service/Mailer/Mailer.php +++ b/src/Service/Mailer/Mailer.php @@ -8,11 +8,13 @@ use Symfony\Component\HttpKernel\Profiler\Profiler; use Symfony\Component\Mailer\Exception\TransportExceptionInterface; use Symfony\Component\Mailer\MailerInterface; use Symfony\Component\Mime\Address; +use Symfony\Component\Mime\Crypto\SMimeSigner; use Symfony\Component\Mime\Email; use Symfony\Component\Process\Exception\ProcessFailedException; use Symfony\Component\Process\Process; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; use Twig\Environment; +use Symfony\Component\HttpKernel\KernelInterface; class Mailer { @@ -24,6 +26,7 @@ class Mailer private readonly UrlGeneratorInterface $urlGenerator, private readonly ?Profiler $profiler, private readonly Environment $environment, + private readonly KernelInterface $kernel, ) { $this->mailer = $mailer; } @@ -62,7 +65,8 @@ class Mailer ): void { $domain = "ludikevent.fr"; $dest = new Address($address, $addressName); - $src = new Address("contact@" . $domain, "Ludikevent"); + $src = new Address("no-reply@esy-web.fr", "Ludikevent"); + $replyTo = new Address("contact@ludikevent.fr", "Ludikevent"); // 1. Génération du Message-ID (SANS les crochets < >, Symfony les ajoute) $messageId = sprintf('%s.%s@%s', @@ -77,7 +81,8 @@ class Mailer $mail = (new Email()) ->subject($subject) ->to($dest) - ->from($src); + ->from($src) + ->replyTo($replyTo); // 3. Configuration des Headers $headers = $mail->getHeaders(); @@ -123,10 +128,17 @@ class Mailer $mail->html($htmlContent); - try { - $this->mailer->send($mail); - } catch (TransportExceptionInterface $e) { - throw $e; + // Signer l'email S/MIME + $certPath = $this->kernel->getProjectDir() . '/cert/email_smime.pem'; + $keyPath = $this->kernel->getProjectDir() . '/cert/email_smime_key.pem'; + $passphrase = 'vwWRh+1+0u2U9ppMB3'; + + if (file_exists($certPath) && file_exists($keyPath)) { + $signer = new SMimeSigner($certPath, $keyPath, $passphrase); + $signedEmail = $signer->sign($mail); + $this->mailer->send($signedEmail); + } else { + $this->mailer->send($mail); } } } diff --git a/src/Twig/StripeExtension.php b/src/Twig/StripeExtension.php index 6b64b7e..663c777 100644 --- a/src/Twig/StripeExtension.php +++ b/src/Twig/StripeExtension.php @@ -6,7 +6,10 @@ use App\Entity\Contrats; use App\Entity\ContratsPayments; use App\Entity\Devis; use App\Entity\DevisOptions; +use App\Entity\Options; use App\Entity\Product; +use App\Repository\OrderSessionRepository; +use App\Repository\ProductRepository; use App\Service\Stripe\Client; use Doctrine\ORM\EntityManagerInterface; use Jaybizzle\CrawlerDetect\CrawlerDetect; @@ -17,8 +20,14 @@ use Vich\UploaderBundle\Templating\Helper\UploaderHelper; class StripeExtension extends AbstractExtension { - public function __construct(private readonly UploaderHelper $uploaderHelper,private readonly \App\Service\Signature\Client $clientSignature,private readonly Client $client,private readonly EntityManagerInterface $em) - { + public function __construct( + private readonly UploaderHelper $uploaderHelper, + private readonly \App\Service\Signature\Client $clientSignature, + private readonly Client $client, + private readonly EntityManagerInterface $em, + private readonly OrderSessionRepository $orderSessionRepository, + + ) { } public function getFilters(): array @@ -228,10 +237,99 @@ class StripeExtension extends AbstractExtension new TwigFunction('isBot', [$this, 'isBot']), new TwigFunction('syncStripe', [$this, 'syncStripe']), new TwigFunction('contratPaymentPay', [$this, 'contratPaymentPay']), - new TwigFunction('loadProductByName',[$this,'loadProductByName']) + new TwigFunction('loadProductByName',[$this,'loadProductByName']), + new TwigFunction('loadProductById',[$this,'loadProductById']), + new TwigFunction('loadOptionById',[$this,'loadOptionById']), + new TwigFunction('totalSession',[$this,'totalSession']), + new TwigFunction('getPendingOrderSessionCount', [$this, 'getPendingOrderSessionCount']), ]; } + public function loadProductById($id) + { + $p = $this->em->getRepository(Product::class)->find($id); + + if (!$p) return null; + + return [ + 'name' => $p->getName(), + 'image' => $this->uploaderHelper->asset($p,'imageFile'), + 'price1day' => $p->getPriceDay(), + 'priceSup' => $p->getPriceSup(), + ]; + } + + public function loadOptionById($id) + { + $o = $this->em->getRepository(Options::class)->find($id); + + if (!$o) return null; + + return [ + 'name' => $o->getName(), + 'price' => $o->getPriceHt(), + ]; + } + + public function totalSession(\App\Entity\OrderSession $session): array + { + $sessionData = $session->getProducts(); + $ids = $sessionData['ids'] ?? []; + $selectedOptionsMap = $sessionData['options'] ?? []; + + $startStr = $sessionData['start'] ?? null; + $endStr = $sessionData['end'] ?? null; + + $duration = 1; + if ($startStr && $endStr) { + try { + $start = new \DateTimeImmutable($startStr); + $end = new \DateTimeImmutable($endStr); + if ($end >= $start) { + $duration = $start->diff($end)->days + 1; + } + } catch (\Exception $e) { + $duration = 1; + } + } + + $totalHT = 0; + $productRepo = $this->em->getRepository(Product::class); + $optionsRepo = $this->em->getRepository(Options::class); + + // Products + foreach ($ids as $id) { + $product = $productRepo->find($id); + if ($product) { + $price = $product->getPriceDay() + ($product->getPriceSup() * max(0, $duration - 1)); + $totalHT += $price; + } + } + + // Options + foreach ($selectedOptionsMap as $prodId => $optIds) { + foreach ($optIds as $optId) { + $option = $optionsRepo->find($optId); + if ($option) { + $totalHT += $option->getPriceHt(); + } + } + } + + $tvaEnabled = isset($_ENV['TVA_ENABLED']) && $_ENV['TVA_ENABLED'] === "true"; + + return [ + 'ht' => $totalHT, + 'duration' => $duration, + 'tvaEnabled' => $tvaEnabled + ]; + } + + public function getPendingOrderSessionCount(): int + { + return $this->orderSessionRepository->count(['state' => 'send']); + } + public function devisSignUrl(Devis $devis): string { return $this->clientSignature->getLinkSign($devis->getSignatureId()); diff --git a/templates/dashboard/base.twig b/templates/dashboard/base.twig index f3818db..af21a31 100644 --- a/templates/dashboard/base.twig +++ b/templates/dashboard/base.twig @@ -45,6 +45,19 @@ {{ menu.nav_link(path('app_crm_formules'), 'Formules', '', 'app_crm_formules') }} {{ menu.nav_link(path('app_crm_facture'), 'Facture', '', 'app_crm_facture') }} {{ menu.nav_link(path('app_crm_customer'), 'Clients', '', 'app_clients') }} + + {% set pendingCount = getPendingOrderSessionCount() %} + +
+ + + + Réservation sur internet +
+ {% if pendingCount > 0 %} + {{ pendingCount }} + {% endif %} +
diff --git a/templates/dashboard/flow/index.twig b/templates/dashboard/flow/index.twig new file mode 100644 index 0000000..4c67260 --- /dev/null +++ b/templates/dashboard/flow/index.twig @@ -0,0 +1,122 @@ +{% extends 'dashboard/base.twig' %} + +{% block title %}Réservations en ligne{% endblock %} +{% block title_header %}Réservations en ligne{% endblock %} + +{% block body %} +
+ + {# --- FILTRES (Optionnel) --- #} + {# Pour l'instant simple liste #} + +
+ {% for session in sessions %} +
+ {# Background Hover Effect #} +
+ +
+
+ + {# 1. DATE & STATUS #} +
+ Créé le +

+ {{ session.createdAt|date('d/m/Y H:i') }} +

+ + {% if session.state == 'send' %} +
+ Envoyée +
+ {% elseif session.state == 'created' %} +
+ Brouillon +
+ {% else %} +
+ {{ session.state|capitalize }} +
+ {% endif %} +
+ + {# 2. CLIENT #} +
+ {% if session.customer %} +
+
+ +
+
+

{{ session.customer.surname }} {{ session.customer.name }}

+

{{ session.customer.email }}

+

{{ session.customer.phone }}

+
+
+ {% else %} +
+
+ +
+
+

Client non identifié

+
+
+ {% endif %} +
+ + {# 3. LIEU & TYPE #} +
+
+
+ Événement +

{{ session.type|default('Non spécifié') }}

+
+
+

+ + {{ session.townEvent|default('Ville inconnue') }} ({{ session.zipCodeEvent }}) +

+
+
+
+ + {# 4. DETAILS (Nb produits) #} +
+ Contenu +

+ {{ session.products['ids']|default([])|length }} produits +

+ {% if session.products['start'] is defined %} +

+ Du {{ session.products['start']|date('d/m') }} au {{ session.products['end']|date('d/m/Y') }} +

+ {% endif %} +
+ + {# 5. ACTIONS #} + +
+
+
+ {% else %} +
+
+ +
+

Aucune demande de réservation trouvée.

+
+ {% endfor %} +
+ +
+ {{ knp_pagination_render(sessions) }} +
+
+{% endblock %} diff --git a/templates/dashboard/flow/view.twig b/templates/dashboard/flow/view.twig new file mode 100644 index 0000000..1c8714b --- /dev/null +++ b/templates/dashboard/flow/view.twig @@ -0,0 +1,386 @@ +{% extends 'dashboard/base.twig' %} + +{% block title %}Détail de la réservation #{{ session.id }}{% endblock %} +{% block title_header %}Réservation #{{ session.id }}{% endblock %} + +{% block actions %} + +{% endblock %} + +{% block body %} + + +
+ + {# STATUS CARD #} +
+
+ Statut + {% if session.state == 'send' %} +
+ Envoyée +
+ {% elseif session.state == 'created' %} +
+ Brouillon +
+ {% else %} +
+ {{ session.state|capitalize }} +
+ {% endif %} +
+ +
+ Création +

{{ session.createdAt|date('d/m/Y') }}

+

{{ session.createdAt|date('H:i') }}

+
+ +
+ Dernière mise à jour + {% if session.updatedAt %} +

{{ session.updatedAt|date('d/m/Y') }}

+

{{ session.updatedAt|date('H:i') }}

+ {% else %} +

Aucune modification

+ {% endif %} +
+ +
+ Type d'événement +

{{ session.type|default('Non défini') }}

+
+
+ +
+ + {# CLIENT INFO #} +
+

+
+ +
+ Informations Client +

+ + {% if session.customer %} +
+
+ Nom complet + {{ session.customer.surname }} {{ session.customer.name }} +
+ +
+ Téléphone + {{ session.customer.phone }} +
+
+ {% else %} +
+

Aucun client associé pour le moment.

+
+ {% endif %} +
+ + {# LOCATION & TECHNICAL INFO #} +
+

+
+ +
+ Lieu & Technique +

+ +
+
+ Adresse de l'événement +

{{ session.adressEvent|default('Non renseignée') }}

+ {% if session.adress2Event %}

{{ session.adress2Event }}

{% endif %} +

{{ session.zipCodeEvent }} {{ session.townEvent }}

+
+ +
+
+ Type de sol +

{{ session.typeSol|default('-') }}

+
+
+ Pente +

{{ session.pente|default('-') }}

+
+
+ +
+ Accès +

{{ session.access|default('Aucune information') }}

+
+ +
+ Distance Prise élec. + {{ session.distancePower|default(0) }} m +
+
+ + {% if session.deliveryDistance is not null %} +
+

Détails Livraison

+ +
+ {# Map #} + {% if session.deliveryGeometry %} +
+ + +
+ {% endif %} + + {# Details #} +
+
+ Distance réelle (Aller) + {{ session.deliveryDistance|number_format(1, ',', ' ') }} km +
+
+ Franchise + - 10.0 km +
+
+ Distance facturée + {{ max(0, session.deliveryDistance - 10)|number_format(1, ',', ' ') }} km +
+
+ Trajets + 4 (2 A/R) +
+
+ + ({{ session.deliveryDistance|number_format(1) }} - 10) x 4 x 0.50€ = {{ session.deliveryPrice|number_format(2) }}€ + +
+
+
+
+ {% endif %} + +
+

Gestion Livraison

+
+
+
+ + +
+
+ + +
+
+ +
+
+
+
+ + {# CART CONTENT #} +
+

+
+ +
+ Contenu de la demande +

+ + {% if session.products['start'] is defined and session.products['end'] is defined %} +
+
+ Du + {{ session.products['start']|date('d/m/Y') }} +
+ +
+ Au + {{ session.products['end']|date('d/m/Y') }} +
+
+ {% endif %} + +
+ {% if session.products['ids'] is defined %} + {% for productId in session.products['ids'] %} + {% set product = loadProductById(productId) %} + {% if product %} +
+ {% if product.image %} + {{ product.name }} + {% else %} +
+ +
+ {% endif %} +
+

{{ product.name }}

+ Réf: #{{ productId }} +
+ 1er jour : {{ product.price1day|number_format(2, ',', ' ') }} € + {% if product.priceSup > 0 %} + Jours supp : {{ product.priceSup|number_format(2, ',', ' ') }} € + {% endif %} +
+
+
+ + {# LINKED OPTIONS #} + {% if session.products.options[productId] is defined and session.products.options[productId]|length > 0 %} +
+ {% for optionId in session.products.options[productId] %} + {% set option = loadOptionById(optionId) %} + {% if option %} +
+ Option + {{ option.name }} + + {{ option.price|number_format(2, ',', ' ') }} € +
+ {% endif %} + {% endfor %} +
+ {% endif %} + {% else %} +
+ Produit introuvable (ID #{{ productId }}) +
+ {% endif %} + {% endfor %} + {% else %} +

Aucun produit.

+ {% endif %} + + {# ORPHAN OPTIONS #} + {% if session.options is defined %} + {% set hasOrphans = false %} + {% for prodId, opts in session.options %} + {% if prodId not in session.products['ids']|default([]) %} + {% set hasOrphans = true %} + {% endif %} + {% endfor %} + + {% if hasOrphans %} +
+ Options supplémentaires + {% for prodId, opts in session.options %} + {% if prodId not in session.products['ids']|default([]) %} + {% for optionId in opts %} + {% set option = loadOptionById(optionId) %} + {% if option %} +
+
+ +
+ {{ option.name }} + {{ option.price|number_format(2, ',', ' ') }} € HT +
+ {% endif %} + {% endfor %} + {% endif %} + {% endfor %} +
+ {% endif %} + {% endif %} +
+ + {% if session.details %} +
+ Notes client +
+ {{ session.details }} +
+
+ {% endif %} + + {# TOTAL #} + {% set totalData = totalSession(session) %} +
+
+
+ Durée : {{ totalData.duration }} jour{{ totalData.duration > 1 ? 's' : '' }} +
+
+ Total HT : {{ totalData.ht|number_format(2, ',', ' ') }} € +
+ {% if totalData.tvaEnabled %} +
+ Soit {{ (totalData.ht * 1.20)|number_format(2, ',', ' ') }} € TTC (TVA 20%) +
+ {% endif %} +
+
+ + {# BILLING INFO #} +
+

+
+ +
+ Facturation +

+ +
+

{{ session.billingAddress|default('Même que livraison') }}

+

{{ session.billingZipCode }} {{ session.billingTown }}

+
+ +
+
+
+ + +
+ +
+
+
+ +
+{% endblock %} diff --git a/templates/mails/reserve/confirmation.twig b/templates/mails/reserve/confirmation.twig new file mode 100644 index 0000000..ad05517 --- /dev/null +++ b/templates/mails/reserve/confirmation.twig @@ -0,0 +1,51 @@ +{% extends 'mails/base.twig' %} +{% block content %} + + + + Nouvelle Demande de Réservation + + + + + + + + + Client + + + {{ datas.session.customer.surname }} {{ datas.session.customer.name|upper }} + + + + Coordonnées + + + Tél : {{ datas.session.customer.phone }}
+ Email : {{ datas.session.customer.email }} +
+ + + Détails Événement + + + Type : {{ datas.session.type }}
+ Lieu : {{ datas.session.adressEvent }} {{ datas.session.zipCodeEvent }} {{ datas.session.townEvent }} +
+ + + Lien vers la session + + + {{ system.path }}/flow/{{ datas.session.uuid }}/confirmed + +
+ + + + Répondre au client ⚡ + + +
+{% endblock %} diff --git a/templates/reservation/contrat/view.twig b/templates/reservation/contrat/view.twig index 2509bd5..d62523f 100644 --- a/templates/reservation/contrat/view.twig +++ b/templates/reservation/contrat/view.twig @@ -189,54 +189,8 @@ {% set acompteOk = contratPaymentPay(contrat, 'accompte') %} - {% set cautionOk = contratPaymentPay(contrat, 'caution') %} - {% if solde > 0 %} - {% if acompteOk and cautionOk %} -
- {# On garde les paramètres de la route pour le formulaire en GET #} - - - -
- -
- - -
-

Saisissez un montant (Max. {{ solde|number_format(2, ',', ' ') }}€)

-
- - -
- {% else %} -
-
- -

- Paiement du solde indisponible

- Vous devez d'abord :
- 1. Régler l'acompte ({{ acompteOk ? 'OK' : 'En attente' }})
- 2. Déposer la caution ({{ cautionOk ? 'OK' : 'En attente' }}) -

-
-
- {% endif %} - {% else %} + {% if solde <=0 %}
@@ -330,54 +284,6 @@
{% endif %} - {# 3. CAUTION #} - {% if not contratPaymentPay(contrat, 'caution') %} -
-
-
-

Caution

-
-
-

{{ totalCaution|number_format(2, ',', ' ') }}€

- {% set canPayCaution = (date('now') >= contrat.dateAt.modify('-7 days')) %} - {% if canPayCaution and contratPaymentPay(contrat, 'accompte') %} - Déposer l'empreinte - {% else %} -
-

Lien actif le {{ contrat.dateAt.modify('-7 days')|date('d/m/Y') }}

-

(7j avant prestation)

-
- {% endif %} -
-
- {% else %} -
-
-
-

Caution Sécurisée

-
-
- {% for payment in paymentCaution %} -
-
-

Dépôt

-

{{ payment.amount|number_format(2, ',', ' ') }}€

-
-
- {{ payment.card.card.brand|default('Carte') }} - **** {{ payment.card.card.last4|default('') }} - {% if payment.card.card.funding == "debit" %}Debit{% endif %} -
-
- -

Empreinte bancaire validée

-
-
- {% endfor %} -
-
- {% endif %} - {# ... (Garder tout le code précédent inchangé jusqu'à la fin de la grille 3 colonnes) ... #} diff --git a/templates/revervation/flow.twig b/templates/revervation/flow.twig index 4201a8f..b2870fa 100644 --- a/templates/revervation/flow.twig +++ b/templates/revervation/flow.twig @@ -84,9 +84,8 @@ {% endif %}

{{ item.product.name }}

-

{{ item.product.description }}

-
+
1er jour : {{ item.price1Day|number_format(2, ',', ' ') }} € {% if cart.duration > 1 %} @@ -95,6 +94,20 @@ {% endif %}
+ + {% if item.options is defined and item.options|length > 0 %} +
+

Options incluses :

+
    + {% for option in item.options %} +
  • + {{ option.name }} + ({{ option.price|number_format(2, ',', ' ') }} €) +
  • + {% endfor %} +
+
+ {% endif %}

{{ item.totalPriceHT|number_format(2, ',', ' ') }} € HT

@@ -106,6 +119,25 @@ {% else %}

Aucun produit sélectionné.

{% endfor %} + + {% if cart.options is defined and cart.options|length > 0 %} +
+

Options supplémentaires

+ {% for option in cart.options %} +
+
+
+ + + +
+ {{ option.name }} +
+ {{ option.price|number_format(2, ',', ' ') }} € HT +
+ {% endfor %} +
+ {% endif %}
diff --git a/templates/revervation/flow_confirmed.twig b/templates/revervation/flow_confirmed.twig index a12d93a..c645feb 100644 --- a/templates/revervation/flow_confirmed.twig +++ b/templates/revervation/flow_confirmed.twig @@ -3,6 +3,13 @@ {% block title %}Confirmation de votre demande{% endblock %} +{% block stylesheets %} + {{ parent() }} + +{% endblock %} + {% block body %}
@@ -52,7 +59,7 @@ {% endif %}
- + {# Linked Options #} {% if item.options is defined and item.options|length > 0 %}
@@ -76,7 +83,7 @@ {% else %}

Aucun produit sélectionné.

{% endfor %} - + {# Orphan Options #} {% if cart.options is defined and cart.options|length > 0 %}
@@ -241,10 +248,100 @@
- + {# Delivery Estimation #} + {% if delivery is defined and delivery.estimation is not null %} +
+
+
+ + + +
+

Estimation des frais de livraison

+
+ +
+ {# Details (60%) #} +
+ {% if delivery.details.isFree %} +
+
+ Offert ! + Zone gratuite +
+

Votre événement se trouve à moins de 10km de nos locaux.

+
+ {% else %} +
+ Coût estimé +

+ {{ delivery.estimation|format_currency('EUR') }} +

+
+ {% endif %} + +
+

Détails du calcul

+ +
+ Distance réelle (Aller) + {{ delivery.details.distance|number_format(1, ',', ' ') }} km +
+ +
+ Franchise kilométrique + - 10.0 km (Offerts) +
+ +
+ +
+ Distance facturée + {{ delivery.details.chargedDistance|number_format(1, ',', ' ') }} km +
+ +
+ Nombre de trajets + {{ delivery.details.trips }} (2 A/R) +
+ +
+ Tarif kilométrique + {{ delivery.details.rate }} € / km +
+
+ + {% if not delivery.details.isFree %} +
+

Formule appliquée

+ + ({{ delivery.details.distance|number_format(1) }} - 10) x {{ delivery.details.trips }} x {{ delivery.details.rate }}€ = {{ delivery.estimation|number_format(2) }}€ + +
+ {% endif %} + +
+

+ Cette estimation est indicative. Le montant définitif figurera sur votre devis. +

+
+
+ + {# Map (40%) #} +
+ {% if delivery.geometry %} + + + {% else %} +
+ Carte non disponible +
+ {% endif %} +
+
+
+ {% endif %}
@@ -253,13 +350,23 @@ Télécharger le devis - - Prendre les options de livraison - - + +
+ +
{% endblock %} + +{% block javascripts %} + {{ parent() }} + {% if delivery is defined and delivery.geometry %} + + {% endif %} +{% endblock %} diff --git a/templates/revervation/register.twig b/templates/revervation/register.twig index cfa65c7..a86f6fd 100644 --- a/templates/revervation/register.twig +++ b/templates/revervation/register.twig @@ -21,7 +21,7 @@ @@ -58,11 +58,26 @@ class="w-full bg-gray-50 border-none rounded-2xl px-6 py-4 focus:ring-2 focus:ring-blue-500 outline-none"> - {# SIRET (Caché par défaut) #} -