From 1896f83107aec19513500e7e934632d0ddddc7fc Mon Sep 17 00:00:00 2001 From: Serreau Jovann Date: Thu, 5 Feb 2026 08:18:29 +0100 Subject: [PATCH] =?UTF-8?q?```=20=E2=9C=A8=20feat(reservation/flow):=20Am?= =?UTF-8?q?=C3=A9liore=20le=20flux=20de=20r=C3=A9servation=20et=20ajoute?= =?UTF-8?q?=20des=20options.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cette commit améliore le flux de réservation, ajoute une estimation des frais de livraison et gère les options de produit et les paiements. ``` --- .gitea/workflows/install-deps.yml | 2 +- assets/admin.js | 4 +- assets/reserve.js | 3 +- assets/tools/LocalStorageClear.js | 11 + cert/email_smime.p12 | Bin 0 -> 7023 bytes cert/email_smime.pem | 125 +++++++ cert/email_smime_key.pem | 32 ++ migrations/Version20260204131618.php | 33 ++ migrations/Version20260204141133.php | 33 ++ migrations/Version20260204142338.php | 33 ++ src/Controller/ContratController.php | 52 --- src/Controller/Dashboard/FlowController.php | 162 ++++++++ src/Controller/ReserverController.php | 221 ++++++++++- src/Controller/SignatureController.php | 14 + src/Controller/Webhooks.php | 32 -- src/Entity/Customer.php | 28 ++ src/Entity/OrderSession.php | 60 +++ src/Service/Mailer/Mailer.php | 24 +- src/Twig/StripeExtension.php | 104 +++++- templates/dashboard/base.twig | 13 + templates/dashboard/flow/index.twig | 122 +++++++ templates/dashboard/flow/view.twig | 386 ++++++++++++++++++++ templates/mails/reserve/confirmation.twig | 51 +++ templates/reservation/contrat/view.twig | 96 +---- templates/revervation/flow.twig | 36 +- templates/revervation/flow_confirmed.twig | 131 ++++++- templates/revervation/register.twig | 27 +- templates/revervation/success.twig | 34 ++ 28 files changed, 1654 insertions(+), 215 deletions(-) create mode 100644 assets/tools/LocalStorageClear.js create mode 100644 cert/email_smime.p12 create mode 100644 cert/email_smime.pem create mode 100644 cert/email_smime_key.pem create mode 100644 migrations/Version20260204131618.php create mode 100644 migrations/Version20260204141133.php create mode 100644 migrations/Version20260204142338.php create mode 100644 src/Controller/Dashboard/FlowController.php create mode 100644 templates/dashboard/flow/index.twig create mode 100644 templates/dashboard/flow/view.twig create mode 100644 templates/mails/reserve/confirmation.twig create mode 100644 templates/revervation/success.twig 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 0000000000000000000000000000000000000000..8c853ab42404e8b5a2fac8516575c68ed209cdab GIT binary patch literal 7023 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 I + + 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) #} -