```
✨ feat(reservation/flow): Améliore le flux de réservation et ajoute des options.
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.
```
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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'))
|
||||
|
||||
11
assets/tools/LocalStorageClear.js
Normal file
11
assets/tools/LocalStorageClear.js
Normal file
@@ -0,0 +1,11 @@
|
||||
|
||||
export class LocalStorageClear extends HTMLElement {
|
||||
connectedCallback() {
|
||||
const keysAttr = this.getAttribute('keys');
|
||||
if (keysAttr) {
|
||||
keysAttr.split(',').forEach(key => {
|
||||
localStorage.removeItem(key);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
BIN
cert/email_smime.p12
Normal file
BIN
cert/email_smime.p12
Normal file
Binary file not shown.
125
cert/email_smime.pem
Normal file
125
cert/email_smime.pem
Normal file
@@ -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: <Unsupported tag 6>
|
||||
<Unsupported tag 6>
|
||||
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: <Unsupported tag 6>
|
||||
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-----
|
||||
32
cert/email_smime_key.pem
Normal file
32
cert/email_smime_key.pem
Normal file
@@ -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: <No 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-----
|
||||
33
migrations/Version20260204131618.php
Normal file
33
migrations/Version20260204131618.php
Normal file
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Auto-generated Migration: Please modify to your needs!
|
||||
*/
|
||||
final class Version20260204131618 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
// this up() migration is auto-generated, please modify it to your needs
|
||||
$this->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');
|
||||
}
|
||||
}
|
||||
33
migrations/Version20260204141133.php
Normal file
33
migrations/Version20260204141133.php
Normal file
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Auto-generated Migration: Please modify to your needs!
|
||||
*/
|
||||
final class Version20260204141133 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
// this up() migration is auto-generated, please modify it to your needs
|
||||
$this->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');
|
||||
}
|
||||
}
|
||||
33
migrations/Version20260204142338.php
Normal file
33
migrations/Version20260204142338.php
Normal file
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Auto-generated Migration: Please modify to your needs!
|
||||
*/
|
||||
final class Version20260204142338 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
// this up() migration is auto-generated, please modify it to your needs
|
||||
$this->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');
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
162
src/Controller/Dashboard/FlowController.php
Normal file
162
src/Controller/Dashboard/FlowController.php
Normal file
@@ -0,0 +1,162 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controller\Dashboard;
|
||||
|
||||
use App\Logger\AppLogger;
|
||||
use App\Repository\OrderSessionRepository;
|
||||
use Knp\Component\Pager\PaginatorInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
#[Route('/crm/flow')]
|
||||
class FlowController extends AbstractController
|
||||
{
|
||||
public function __construct(
|
||||
private readonly AppLogger $appLogger,
|
||||
private readonly OrderSessionRepository $orderSessionRepository,
|
||||
private readonly HttpClientInterface $client
|
||||
) {
|
||||
}
|
||||
|
||||
#[Route('', name: 'app_crm_flow', methods: ['GET'])]
|
||||
public function index(Request $request, PaginatorInterface $paginator): Response
|
||||
{
|
||||
$this->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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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'));
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -45,6 +45,19 @@
|
||||
{{ menu.nav_link(path('app_crm_formules'), 'Formules', '<path d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"></path>', 'app_crm_formules') }}
|
||||
{{ menu.nav_link(path('app_crm_facture'), 'Facture', '<path d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>', 'app_crm_facture') }}
|
||||
{{ menu.nav_link(path('app_crm_customer'), 'Clients', '<path d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z"></path>', 'app_clients') }}
|
||||
|
||||
{% set pendingCount = getPendingOrderSessionCount() %}
|
||||
<a data-turbo="false" href="{{ path('app_crm_flow') }}" class="flex items-center justify-between px-4 py-3 rounded-xl transition-all duration-200 group {{ app.current_route == 'app_crm_flow' ? 'bg-blue-600 text-white shadow-lg shadow-blue-500/30' : 'hover:bg-slate-800 text-slate-400' }}">
|
||||
<div class="flex items-center space-x-3">
|
||||
<svg class="w-5 h-5 {{ app.current_route == 'app_crm_flow' ? 'text-white' : 'text-slate-400 group-hover:text-blue-500' }}" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 21a9.004 9.004 0 008.716-6.747M12 21a9.004 9.004 0 01-8.716-6.747M12 21c2.485 0 4.5-4.03 4.5-9S14.485 3 12 3m0 18c-2.485 0-4.5-4.03-4.5-9S9.515 3 12 3m0 0a8.997 8.997 0 017.843 4.582M12 3a8.997 8.997 0 00-7.843 4.582m15.686 0A11.953 11.953 0 0112 10.5c-2.998 0-5.74-1.1-7.843-2.918m15.686 0A8.959 8.959 0 0121 12c0 .778-.099 1.533-.284 2.253m0 0A17.919 17.919 0 0112 16.5c-3.162 0-6.133-.815-8.716-2.247m0 0A9.015 9.015 0 013 12c0-1.605.42-3.113 1.157-4.418" />
|
||||
</svg>
|
||||
<span class="font-semibold text-sm">Réservation sur internet</span>
|
||||
</div>
|
||||
{% if pendingCount > 0 %}
|
||||
<span class="bg-red-500 text-white text-[10px] font-bold px-2 py-0.5 rounded-full">{{ pendingCount }}</span>
|
||||
{% endif %}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
122
templates/dashboard/flow/index.twig
Normal file
122
templates/dashboard/flow/index.twig
Normal file
@@ -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 %}
|
||||
<div class="space-y-8 pb-20">
|
||||
|
||||
{# --- FILTRES (Optionnel) --- #}
|
||||
{# Pour l'instant simple liste #}
|
||||
|
||||
<div id="flowList" class="grid gap-6">
|
||||
{% for session in sessions %}
|
||||
<div class="contrat-card relative overflow-hidden group">
|
||||
{# Background Hover Effect #}
|
||||
<div class="absolute -inset-px bg-gradient-to-r from-transparent via-white/5 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-500 rounded-[2rem]"></div>
|
||||
|
||||
<div class="relative bg-white/[0.03] border border-white/10 backdrop-blur-md rounded-[2rem] transition-all duration-300 group-hover:bg-white/[0.06] group-hover:translate-y-[-2px] group-hover:shadow-2xl group-hover:shadow-blue-500/10">
|
||||
<div class="grid grid-cols-1 lg:grid-cols-12 items-center">
|
||||
|
||||
{# 1. DATE & STATUS #}
|
||||
<div class="lg:col-span-2 p-8 border-b lg:border-b-0 lg:border-r border-white/5 text-center lg:text-left">
|
||||
<span class="text-[10px] font-bold text-blue-400 uppercase tracking-[0.2em] mb-1 block">Créé le</span>
|
||||
<h3 class="text-white font-bold text-sm tracking-tight mb-3">
|
||||
{{ session.createdAt|date('d/m/Y H:i') }}
|
||||
</h3>
|
||||
|
||||
{% if session.state == 'send' %}
|
||||
<div class="inline-flex items-center gap-1.5 px-3 py-1 rounded-full bg-blue-500/10 border border-blue-500/20 text-blue-400 text-[9px] font-bold uppercase tracking-wider">
|
||||
<span class="w-1.5 h-1.5 rounded-full bg-blue-500 animate-pulse"></span> Envoyée
|
||||
</div>
|
||||
{% elseif session.state == 'created' %}
|
||||
<div class="inline-flex items-center gap-1.5 px-3 py-1 rounded-full bg-slate-500/10 border border-slate-500/20 text-slate-400 text-[9px] font-bold uppercase tracking-wider">
|
||||
Brouillon
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="inline-flex items-center gap-1.5 px-3 py-1 rounded-full bg-white/10 border border-white/20 text-slate-300 text-[9px] font-bold uppercase tracking-wider">
|
||||
{{ session.state|capitalize }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{# 2. CLIENT #}
|
||||
<div class="lg:col-span-3 p-8 border-b lg:border-b-0 lg:border-r border-white/5">
|
||||
{% if session.customer %}
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="w-12 h-12 bg-gradient-to-tr from-blue-600/20 to-indigo-600/20 rounded-2xl flex items-center justify-center border border-white/10 group-hover:scale-110 transition-transform">
|
||||
<svg class="w-6 h-6 text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" stroke-width="1.5"></path></svg>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-white font-bold text-base tracking-tight uppercase">{{ session.customer.surname }} {{ session.customer.name }}</p>
|
||||
<p class="text-slate-500 text-xs font-medium line-clamp-1">{{ session.customer.email }}</p>
|
||||
<p class="text-slate-500 text-xs font-medium">{{ session.customer.phone }}</p>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="flex items-center gap-4 opacity-50">
|
||||
<div class="w-12 h-12 bg-slate-800 rounded-2xl flex items-center justify-center border border-white/5">
|
||||
<svg class="w-6 h-6 text-slate-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" stroke-width="1.5"></path></svg>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-slate-400 font-bold text-sm tracking-tight italic">Client non identifié</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{# 3. LIEU & TYPE #}
|
||||
<div class="lg:col-span-3 p-8 border-b lg:border-b-0 lg:border-r border-white/5">
|
||||
<div class="flex flex-col gap-2">
|
||||
<div>
|
||||
<span class="text-[10px] font-bold text-slate-300 uppercase tracking-widest mb-1 block">Événement</span>
|
||||
<p class="text-slate-200 font-bold text-sm">{{ session.type|default('Non spécifié') }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-slate-400 text-xs flex items-center gap-1">
|
||||
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" /><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" /></svg>
|
||||
{{ session.townEvent|default('Ville inconnue') }} <span class="text-slate-600">({{ session.zipCodeEvent }})</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# 4. DETAILS (Nb produits) #}
|
||||
<div class="lg:col-span-2 p-8 border-b lg:border-b-0 border-white/5">
|
||||
<span class="text-[10px] font-bold text-slate-300 uppercase tracking-widest mb-1 block">Contenu</span>
|
||||
<p class="text-white font-bold text-sm">
|
||||
{{ session.products['ids']|default([])|length }} produits
|
||||
</p>
|
||||
{% if session.products['start'] is defined %}
|
||||
<p class="text-xs text-slate-500 mt-1">
|
||||
Du {{ session.products['start']|date('d/m') }} au {{ session.products['end']|date('d/m/Y') }}
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{# 5. ACTIONS #}
|
||||
<div class="lg:col-span-2 p-6 flex items-center justify-end pr-8">
|
||||
<a data-turbo="false" href="{{ path('app_crm_flow_view', {id: session.id}) }}"
|
||||
class="group/btn relative px-4 py-2 bg-blue-600/10 hover:bg-blue-600 text-blue-400 hover:text-white border border-blue-500/20 rounded-xl transition-all flex items-center gap-2 text-xs font-bold uppercase tracking-wider">
|
||||
Voir
|
||||
<svg class="w-4 h-4 transition-transform group-hover/btn:translate-x-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"></path></svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="text-center py-20">
|
||||
<div class="w-16 h-16 bg-slate-800 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||
<svg class="w-8 h-8 text-slate-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4"></path></svg>
|
||||
</div>
|
||||
<p class="text-slate-500 font-medium">Aucune demande de réservation trouvée.</p>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<div class="mt-12 glass-pagination">
|
||||
{{ knp_pagination_render(sessions) }}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
386
templates/dashboard/flow/view.twig
Normal file
386
templates/dashboard/flow/view.twig
Normal file
@@ -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 %}
|
||||
<div class="flex items-center gap-3">
|
||||
<a href="{{ path('app_crm_flow') }}"
|
||||
class="group relative flex items-center gap-2 px-6 py-3 bg-slate-700 hover:bg-slate-600 rounded-2xl text-white text-xs font-bold uppercase tracking-wider transition-all shadow-lg">
|
||||
<svg class="w-4 h-4 transition-transform group-hover:-translate-x-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18" />
|
||||
</svg>
|
||||
Retour
|
||||
</a>
|
||||
<a href="{{ path('app_crm_flow_delete', {id: session.id}) }}"
|
||||
onclick="return confirm('Voulez-vous vraiment supprimer cette demande ? Cette action est irréversible.')"
|
||||
data-turbo="false"
|
||||
class="group relative flex items-center gap-2 px-6 py-3 bg-red-600 hover:bg-red-500 rounded-2xl text-white text-xs font-bold uppercase tracking-wider transition-all shadow-lg shadow-red-500/20">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
||||
</svg>
|
||||
Refuser
|
||||
</a>
|
||||
<a href="{{ path('app_crm_flow_allow', {id: session.id}) }}"
|
||||
data-turbo="false"
|
||||
class="group relative flex items-center gap-2 px-6 py-3 bg-emerald-600 hover:bg-emerald-500 rounded-2xl text-white text-xs font-bold uppercase tracking-wider transition-all shadow-lg shadow-emerald-500/20">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
Valider la demande
|
||||
</a>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
|
||||
integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY="
|
||||
crossorigin=""/>
|
||||
|
||||
<div class="space-y-8 pb-20">
|
||||
|
||||
{# STATUS CARD #}
|
||||
<div class="grid grid-cols-1 lg:grid-cols-4 gap-6">
|
||||
<div class="p-6 bg-slate-800/50 backdrop-blur-md border border-slate-700/50 rounded-[2rem] flex flex-col items-center justify-center text-center">
|
||||
<span class="text-[10px] font-bold text-slate-400 uppercase tracking-[0.2em] mb-2">Statut</span>
|
||||
{% if session.state == 'send' %}
|
||||
<div class="inline-flex items-center gap-2 px-4 py-2 rounded-full bg-blue-500/10 border border-blue-500/20 text-blue-400 text-xs font-bold uppercase tracking-wider">
|
||||
<span class="w-2 h-2 rounded-full bg-blue-500 animate-pulse"></span> Envoyée
|
||||
</div>
|
||||
{% elseif session.state == 'created' %}
|
||||
<div class="inline-flex items-center gap-2 px-4 py-2 rounded-full bg-slate-500/10 border border-slate-500/20 text-slate-400 text-xs font-bold uppercase tracking-wider">
|
||||
Brouillon
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="inline-flex items-center gap-2 px-4 py-2 rounded-full bg-white/10 border border-white/20 text-slate-300 text-xs font-bold uppercase tracking-wider">
|
||||
{{ session.state|capitalize }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="p-6 bg-slate-800/50 backdrop-blur-md border border-slate-700/50 rounded-[2rem] flex flex-col items-center justify-center text-center">
|
||||
<span class="text-[10px] font-bold text-slate-400 uppercase tracking-[0.2em] mb-2">Création</span>
|
||||
<p class="text-white font-bold text-lg">{{ session.createdAt|date('d/m/Y') }}</p>
|
||||
<p class="text-slate-500 text-xs">{{ session.createdAt|date('H:i') }}</p>
|
||||
</div>
|
||||
|
||||
<div class="p-6 bg-slate-800/50 backdrop-blur-md border border-slate-700/50 rounded-[2rem] flex flex-col items-center justify-center text-center">
|
||||
<span class="text-[10px] font-bold text-slate-400 uppercase tracking-[0.2em] mb-2">Dernière mise à jour</span>
|
||||
{% if session.updatedAt %}
|
||||
<p class="text-white font-bold text-lg">{{ session.updatedAt|date('d/m/Y') }}</p>
|
||||
<p class="text-slate-500 text-xs">{{ session.updatedAt|date('H:i') }}</p>
|
||||
{% else %}
|
||||
<p class="text-slate-500 text-sm italic">Aucune modification</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="p-6 bg-slate-800/50 backdrop-blur-md border border-slate-700/50 rounded-[2rem] flex flex-col items-center justify-center text-center">
|
||||
<span class="text-[10px] font-bold text-slate-400 uppercase tracking-[0.2em] mb-2">Type d'événement</span>
|
||||
<p class="text-white font-bold text-lg">{{ session.type|default('Non défini') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
||||
|
||||
{# CLIENT INFO #}
|
||||
<div class="bg-slate-800/40 backdrop-blur-xl border border-slate-700/50 rounded-[2rem] p-8">
|
||||
<h3 class="text-lg font-bold text-white mb-6 flex items-center gap-3">
|
||||
<div class="p-2 bg-blue-500/10 rounded-lg text-blue-500">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"></path></svg>
|
||||
</div>
|
||||
Informations Client
|
||||
</h3>
|
||||
|
||||
{% if session.customer %}
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-center justify-between p-4 bg-slate-900/50 rounded-xl border border-slate-700/50">
|
||||
<span class="text-slate-400 text-xs uppercase tracking-wider font-bold">Nom complet</span>
|
||||
<span class="text-white font-bold">{{ session.customer.surname }} {{ session.customer.name }}</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between p-4 bg-slate-900/50 rounded-xl border border-slate-700/50">
|
||||
<span class="text-slate-400 text-xs uppercase tracking-wider font-bold">Email</span>
|
||||
<a href="mailto:{{ session.customer.email }}" class="text-blue-400 hover:text-blue-300 transition-colors font-medium">{{ session.customer.email }}</a>
|
||||
</div>
|
||||
<div class="flex items-center justify-between p-4 bg-slate-900/50 rounded-xl border border-slate-700/50">
|
||||
<span class="text-slate-400 text-xs uppercase tracking-wider font-bold">Téléphone</span>
|
||||
<a href="tel:{{ session.customer.phone }}" class="text-white font-medium hover:text-blue-400 transition-colors">{{ session.customer.phone }}</a>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="p-8 text-center border-2 border-dashed border-slate-700 rounded-2xl">
|
||||
<p class="text-slate-500 font-medium">Aucun client associé pour le moment.</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{# LOCATION & TECHNICAL INFO #}
|
||||
<div class="bg-slate-800/40 backdrop-blur-xl border border-slate-700/50 rounded-[2rem] p-8">
|
||||
<h3 class="text-lg font-bold text-white mb-6 flex items-center gap-3">
|
||||
<div class="p-2 bg-purple-500/10 rounded-lg text-purple-500">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" /><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"></path></svg>
|
||||
</div>
|
||||
Lieu & Technique
|
||||
</h3>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div class="p-4 bg-slate-900/50 rounded-xl border border-slate-700/50">
|
||||
<span class="block text-slate-400 text-[10px] uppercase tracking-wider font-bold mb-1">Adresse de l'événement</span>
|
||||
<p class="text-white font-medium">{{ session.adressEvent|default('Non renseignée') }}</p>
|
||||
{% if session.adress2Event %}<p class="text-slate-400 text-sm mt-1">{{ session.adress2Event }}</p>{% endif %}
|
||||
<p class="text-blue-400 font-bold mt-1">{{ session.zipCodeEvent }} {{ session.townEvent }}</p>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="p-4 bg-slate-900/50 rounded-xl border border-slate-700/50">
|
||||
<span class="block text-slate-400 text-[10px] uppercase tracking-wider font-bold mb-1">Type de sol</span>
|
||||
<p class="text-white font-medium">{{ session.typeSol|default('-') }}</p>
|
||||
</div>
|
||||
<div class="p-4 bg-slate-900/50 rounded-xl border border-slate-700/50">
|
||||
<span class="block text-slate-400 text-[10px] uppercase tracking-wider font-bold mb-1">Pente</span>
|
||||
<p class="text-white font-medium">{{ session.pente|default('-') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="p-4 bg-slate-900/50 rounded-xl border border-slate-700/50">
|
||||
<span class="block text-slate-400 text-[10px] uppercase tracking-wider font-bold mb-1">Accès</span>
|
||||
<p class="text-slate-300 text-sm leading-relaxed">{{ session.access|default('Aucune information') }}</p>
|
||||
</div>
|
||||
|
||||
<div class="p-4 bg-slate-900/50 rounded-xl border border-slate-700/50 flex justify-between items-center">
|
||||
<span class="text-slate-400 text-xs uppercase tracking-wider font-bold">Distance Prise élec.</span>
|
||||
<span class="text-white font-bold">{{ session.distancePower|default(0) }} m</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if session.deliveryDistance is not null %}
|
||||
<div class="mt-6 border-t border-slate-700/50 pt-6">
|
||||
<h4 class="text-white font-bold text-sm mb-4">Détails Livraison</h4>
|
||||
|
||||
<div class="bg-slate-900/50 rounded-xl border border-slate-700/50 overflow-hidden mb-4">
|
||||
{# Map #}
|
||||
{% if session.deliveryGeometry %}
|
||||
<div class="h-48 w-full relative z-0">
|
||||
<leaflet-map class="absolute inset-0 w-full h-full"
|
||||
data-geometry="{{ session.deliveryGeometry|json_encode|e('html_attr') }}">
|
||||
</leaflet-map>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# Details #}
|
||||
<div class="p-4 space-y-2 text-xs">
|
||||
<div class="flex justify-between">
|
||||
<span class="text-slate-400">Distance réelle (Aller)</span>
|
||||
<span class="text-white font-bold">{{ session.deliveryDistance|number_format(1, ',', ' ') }} km</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-slate-400">Franchise</span>
|
||||
<span class="text-emerald-400 font-bold">- 10.0 km</span>
|
||||
</div>
|
||||
<div class="flex justify-between border-t border-slate-700 pt-2">
|
||||
<span class="text-slate-400">Distance facturée</span>
|
||||
<span class="text-white font-bold">{{ max(0, session.deliveryDistance - 10)|number_format(1, ',', ' ') }} km</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-slate-400">Trajets</span>
|
||||
<span class="text-white font-bold">4 (2 A/R)</span>
|
||||
</div>
|
||||
<div class="mt-2 p-2 bg-indigo-500/10 border border-indigo-500/20 rounded-lg text-center">
|
||||
<code class="text-[10px] text-indigo-300 font-mono">
|
||||
({{ session.deliveryDistance|number_format(1) }} - 10) x 4 x 0.50€ = {{ session.deliveryPrice|number_format(2) }}€
|
||||
</code>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="mt-6 pt-6 border-t border-slate-700/50">
|
||||
<h4 class="text-white font-bold text-sm mb-4">Gestion Livraison</h4>
|
||||
<form action="{{ path('app_crm_flow_update', {id: session.id}) }}" method="post" class="space-y-4">
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-[10px] font-bold text-slate-400 uppercase tracking-widest mb-1">Distance (km)</label>
|
||||
<input type="number" step="0.1" name="deliveryDistance" value="{{ session.deliveryDistance }}"
|
||||
class="w-full bg-slate-900/50 border border-slate-700 rounded-lg px-3 py-2 text-white text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 outline-none transition-all">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-[10px] font-bold text-slate-400 uppercase tracking-widest mb-1">Prix Livraison (€)</label>
|
||||
<input type="number" step="0.01" name="deliveryPrice" value="{{ session.deliveryPrice }}"
|
||||
class="w-full bg-slate-900/50 border border-slate-700 rounded-lg px-3 py-2 text-white text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 outline-none transition-all">
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="w-full py-2 bg-blue-600 hover:bg-blue-500 text-white text-xs font-bold uppercase tracking-widest rounded-lg transition-all">
|
||||
Mettre à jour
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# CART CONTENT #}
|
||||
<div class="bg-slate-800/40 backdrop-blur-xl border border-slate-700/50 rounded-[2rem] p-8">
|
||||
<h3 class="text-lg font-bold text-white mb-6 flex items-center gap-3">
|
||||
<div class="p-2 bg-emerald-500/10 rounded-lg text-emerald-500">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 11V7a4 4 0 00-8 0v4M5 9h14l1 12H4L5 9z"></path></svg>
|
||||
</div>
|
||||
Contenu de la demande
|
||||
</h3>
|
||||
|
||||
{% if session.products['start'] is defined and session.products['end'] is defined %}
|
||||
<div class="flex items-center gap-4 mb-8 p-4 bg-slate-900/50 rounded-xl border border-slate-700/50 w-fit">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-slate-400 text-xs font-bold uppercase">Du</span>
|
||||
<span class="text-white font-bold">{{ session.products['start']|date('d/m/Y') }}</span>
|
||||
</div>
|
||||
<span class="text-slate-600">➔</span>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-slate-400 text-xs font-bold uppercase">Au</span>
|
||||
<span class="text-white font-bold">{{ session.products['end']|date('d/m/Y') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="space-y-4">
|
||||
{% if session.products['ids'] is defined %}
|
||||
{% for productId in session.products['ids'] %}
|
||||
{% set product = loadProductById(productId) %}
|
||||
{% if product %}
|
||||
<div class="flex items-center gap-4 p-4 bg-slate-900/50 rounded-xl border border-slate-700/50">
|
||||
{% if product.image %}
|
||||
<img src="{{ product.image }}" alt="{{ product.name }}" class="w-12 h-12 rounded-lg object-cover bg-slate-800">
|
||||
{% else %}
|
||||
<div class="w-12 h-12 rounded-lg bg-slate-800 flex items-center justify-center text-slate-500">
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"></path></svg>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div>
|
||||
<p class="text-white font-bold">{{ product.name }}</p>
|
||||
<span class="text-xs text-slate-500">Réf: #{{ productId }}</span>
|
||||
<div class="mt-1 text-xs text-slate-400">
|
||||
<span class="block">1er jour : {{ product.price1day|number_format(2, ',', ' ') }} €</span>
|
||||
{% if product.priceSup > 0 %}
|
||||
<span class="block">Jours supp : {{ product.priceSup|number_format(2, ',', ' ') }} €</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# LINKED OPTIONS #}
|
||||
{% if session.products.options[productId] is defined and session.products.options[productId]|length > 0 %}
|
||||
<div class="mt-2 ml-16 space-y-1">
|
||||
{% for optionId in session.products.options[productId] %}
|
||||
{% set option = loadOptionById(optionId) %}
|
||||
{% if option %}
|
||||
<div class="flex items-center gap-2 text-xs text-slate-400">
|
||||
<span class="bg-indigo-500/10 text-indigo-400 px-1.5 py-0.5 rounded text-[10px] font-bold uppercase border border-indigo-500/20">Option</span>
|
||||
<span>{{ option.name }}</span>
|
||||
<span class="font-bold text-slate-300">+ {{ option.price|number_format(2, ',', ' ') }} €</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<div class="flex items-center justify-between p-4 bg-slate-900/50 rounded-xl border border-slate-700/50">
|
||||
<span class="text-slate-400 italic">Produit introuvable (ID #{{ productId }})</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<p class="text-slate-500 italic">Aucun produit.</p>
|
||||
{% 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 %}
|
||||
<div class="mt-6 pt-6 border-t border-slate-700/50">
|
||||
<span class="text-[10px] font-bold text-slate-400 uppercase tracking-[0.2em] mb-3 block">Options supplémentaires</span>
|
||||
{% for prodId, opts in session.options %}
|
||||
{% if prodId not in session.products['ids']|default([]) %}
|
||||
{% for optionId in opts %}
|
||||
{% set option = loadOptionById(optionId) %}
|
||||
{% if option %}
|
||||
<div class="flex items-center gap-3 p-3 bg-slate-900/30 rounded-xl border border-slate-700/30 mb-2">
|
||||
<div class="h-8 w-8 bg-indigo-500/10 rounded-lg flex items-center justify-center text-indigo-500 border border-indigo-500/20">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"/></svg>
|
||||
</div>
|
||||
<span class="text-sm font-medium text-slate-300">{{ option.name }}</span>
|
||||
<span class="ml-auto text-sm font-bold text-white">{{ option.price|number_format(2, ',', ' ') }} € HT</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if session.details %}
|
||||
<div class="mt-8">
|
||||
<span class="text-[10px] font-bold text-slate-400 uppercase tracking-[0.2em] mb-2 block">Notes client</span>
|
||||
<div class="p-4 bg-amber-500/10 border border-amber-500/20 rounded-xl text-amber-200 text-sm leading-relaxed">
|
||||
{{ session.details }}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# TOTAL #}
|
||||
{% set totalData = totalSession(session) %}
|
||||
<div class="mt-8 pt-6 border-t border-slate-700">
|
||||
<div class="flex flex-col gap-2 items-end">
|
||||
<div class="text-sm text-slate-400">
|
||||
Durée : <span class="text-white font-bold">{{ totalData.duration }} jour{{ totalData.duration > 1 ? 's' : '' }}</span>
|
||||
</div>
|
||||
<div class="text-xl text-white">
|
||||
Total HT : <span class="font-black">{{ totalData.ht|number_format(2, ',', ' ') }} €</span>
|
||||
</div>
|
||||
{% if totalData.tvaEnabled %}
|
||||
<div class="text-sm text-slate-500">
|
||||
Soit <span class="font-bold text-slate-300">{{ (totalData.ht * 1.20)|number_format(2, ',', ' ') }} € TTC</span> (TVA 20%)
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div> </div>
|
||||
|
||||
{# BILLING INFO #}
|
||||
<div class="bg-slate-800/40 backdrop-blur-xl border border-slate-700/50 rounded-[2rem] p-8">
|
||||
<h3 class="text-lg font-bold text-white mb-6 flex items-center gap-3">
|
||||
<div class="p-2 bg-indigo-500/10 rounded-lg text-indigo-500">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path></svg>
|
||||
</div>
|
||||
Facturation
|
||||
</h3>
|
||||
|
||||
<div class="p-4 bg-slate-900/50 rounded-xl border border-slate-700/50">
|
||||
<p class="text-white font-medium">{{ session.billingAddress|default('Même que livraison') }}</p>
|
||||
<p class="text-blue-400 font-bold mt-1">{{ session.billingZipCode }} {{ session.billingTown }}</p>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 pt-6 border-t border-slate-700/50">
|
||||
<form action="{{ path('app_crm_flow_update', {id: session.id}) }}" method="post" class="flex items-end gap-4">
|
||||
<div class="flex-1">
|
||||
<label class="block text-[10px] font-bold text-slate-400 uppercase tracking-widest mb-1">Mode de paiement</label>
|
||||
<select name="typePaiement" class="w-full bg-slate-900/50 border border-slate-700 rounded-lg px-3 py-2 text-white text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 outline-none transition-all">
|
||||
<option value="">Sélectionner...</option>
|
||||
{% for type in ['Paiement Via Chorus', 'Paiement En ligne', 'Paiement Après événement', 'Autre mode de paiement'] %}
|
||||
<option value="{{ type }}" {% if session.typePaiement == type %}selected{% endif %}>{{ type }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<button type="submit" class="py-2 px-4 bg-indigo-600 hover:bg-indigo-500 text-white text-xs font-bold uppercase tracking-widest rounded-lg transition-all h-[38px]">
|
||||
Enregistrer
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{% endblock %}
|
||||
51
templates/mails/reserve/confirmation.twig
Normal file
51
templates/mails/reserve/confirmation.twig
Normal file
@@ -0,0 +1,51 @@
|
||||
{% extends 'mails/base.twig' %}
|
||||
{% block content %}
|
||||
<mj-section background-color="#ffffff" padding-bottom="0px">
|
||||
<mj-column width="100%">
|
||||
<mj-text font-size="22px" font-weight="900" color="#0f172a" text-transform="uppercase" font-style="italic" align="center">
|
||||
Nouvelle <span style="color:#2563eb">Demande de Réservation</span>
|
||||
</mj-text>
|
||||
<mj-divider border-width="1px" border-color="#f1f5f9" padding-top="20px" padding-bottom="20px" />
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
|
||||
<mj-section background-color="#ffffff" padding-top="0px">
|
||||
<mj-column width="100%">
|
||||
<mj-text padding-bottom="0px" font-size="10px" font-weight="900" color="#94a3b8" text-transform="uppercase" letter-spacing="1px">
|
||||
Client
|
||||
</mj-text>
|
||||
<mj-text font-size="18px" font-weight="700" color="#1e293b" padding-top="5px">
|
||||
{{ datas.session.customer.surname }} {{ datas.session.customer.name|upper }}
|
||||
</mj-text>
|
||||
|
||||
<mj-text padding-top="15px" padding-bottom="0px" font-size="10px" font-weight="900" color="#94a3b8" text-transform="uppercase" letter-spacing="1px">
|
||||
Coordonnées
|
||||
</mj-text>
|
||||
<mj-text font-size="15px" color="#0f172a" padding-top="5px">
|
||||
<b>Tél :</b> {{ datas.session.customer.phone }} <br/>
|
||||
<b>Email :</b> <span style="color:#2563eb; font-weight:700;">{{ datas.session.customer.email }}</span>
|
||||
</mj-text>
|
||||
|
||||
<mj-text padding-top="25px" padding-bottom="0px" font-size="10px" font-weight="900" color="#94a3b8" text-transform="uppercase" letter-spacing="1px">
|
||||
Détails Événement
|
||||
</mj-text>
|
||||
<mj-text font-size="15px" color="#0f172a" padding-top="5px">
|
||||
<b>Type :</b> {{ datas.session.type }} <br/>
|
||||
<b>Lieu :</b> {{ datas.session.adressEvent }} {{ datas.session.zipCodeEvent }} {{ datas.session.townEvent }}
|
||||
</mj-text>
|
||||
|
||||
<mj-text padding-top="25px" padding-bottom="0px" font-size="10px" font-weight="900" color="#94a3b8" text-transform="uppercase" letter-spacing="1px">
|
||||
Lien vers la session
|
||||
</mj-text>
|
||||
<mj-text font-size="15px" color="#0f172a" padding-top="5px">
|
||||
{{ system.path }}/flow/{{ datas.session.uuid }}/confirmed
|
||||
</mj-text>
|
||||
</mj-column>
|
||||
|
||||
<mj-column width="100%">
|
||||
<mj-button background-color="#2563eb" color="#ffffff" font-size="14px" font-weight="900" text-transform="uppercase" border-radius="16px" padding-top="30px" inner-padding="18px 30px" href="mailto:{{ datas.session.customer.email }}">
|
||||
Répondre au client ⚡
|
||||
</mj-button>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
{% endblock %}
|
||||
@@ -189,54 +189,8 @@
|
||||
</div>
|
||||
|
||||
{% set acompteOk = contratPaymentPay(contrat, 'accompte') %}
|
||||
{% set cautionOk = contratPaymentPay(contrat, 'caution') %}
|
||||
|
||||
{% if solde > 0 %}
|
||||
{% if acompteOk and cautionOk %}
|
||||
<form data-turbo="false" action="{{ path('gestion_contrat_view', {'num': contrat.numReservation, 'act': 'soldePay'}) }}" method="GET" class="space-y-4">
|
||||
{# On garde les paramètres de la route pour le formulaire en GET #}
|
||||
<input type="hidden" name="num" value="{{ contrat.numReservation }}">
|
||||
<input type="hidden" name="act" value="soldePay">
|
||||
|
||||
<div class="space-y-2">
|
||||
<label for="amountToPay" class="text-[10px] font-black uppercase text-slate-400 ml-1">Montant à régler maintenant</label>
|
||||
<div class="relative">
|
||||
<input type="number"
|
||||
name="amountToPay"
|
||||
id="amountToPay"
|
||||
step="0.01"
|
||||
min="1"
|
||||
max="{{ solde }}"
|
||||
value="{{ solde }}"
|
||||
class="w-full bg-white/5 border border-white/10 rounded-2xl px-6 py-4 text-2xl font-black italic text-white focus:outline-none focus:ring-2 focus:ring-blue-500 focus:bg-white/10 transition-all"
|
||||
>
|
||||
<span class="absolute right-6 top-1/2 -translate-y-1/2 text-xl font-black italic text-blue-400">€</span>
|
||||
</div>
|
||||
<p class="text-[9px] text-slate-500 italic ml-1 italic">Saisissez un montant (Max. {{ solde|number_format(2, ',', ' ') }}€)</p>
|
||||
</div>
|
||||
|
||||
<button type="submit"
|
||||
class="group flex items-center justify-center gap-3 w-full bg-blue-600 hover:bg-blue-500 text-white py-4 rounded-2xl font-black uppercase italic transition-all shadow-lg shadow-blue-900/20">
|
||||
<span>Procéder au paiement</span>
|
||||
<svg class="w-5 h-5 group-hover:translate-x-1 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M13 7l5 5m0 0l-5 5m5-5H6"/>
|
||||
</svg>
|
||||
</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<div class="bg-white/5 border border-white/10 rounded-2xl p-6">
|
||||
<div class="flex items-start gap-3">
|
||||
<svg class="w-5 h-5 text-amber-500 shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/></svg>
|
||||
<p class="text-[10px] text-slate-400 font-bold uppercase italic leading-tight">
|
||||
<span class="text-amber-500">Paiement du solde indisponible</span><br><br>
|
||||
Vous devez d'abord :<br>
|
||||
<span class="{{ acompteOk ? 'text-green-500' : 'text-slate-300' }}">1. Régler l'acompte ({{ acompteOk ? 'OK' : 'En attente' }})</span><br>
|
||||
<span class="{{ cautionOk ? 'text-green-500' : 'text-slate-300' }}">2. Déposer la caution ({{ cautionOk ? 'OK' : 'En attente' }})</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{% if solde <=0 %}
|
||||
<div class="flex items-center gap-3 bg-green-500/20 text-green-400 p-6 rounded-2xl border border-green-500/30">
|
||||
<svg class="w-8 h-8 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M5 13l4 4L19 7"/>
|
||||
@@ -330,54 +284,6 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# 3. CAUTION #}
|
||||
{% if not contratPaymentPay(contrat, 'caution') %}
|
||||
<div class="bg-white rounded-[2rem] border border-slate-100 shadow-xl shadow-slate-200/20 overflow-hidden">
|
||||
<div class="bg-slate-800 p-6 text-white flex items-center gap-4">
|
||||
<div class="w-10 h-10 bg-white/20 rounded-xl flex items-center justify-center"><svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M12 9v2m0 4h.01"></path></svg></div>
|
||||
<p class="text-sm font-black uppercase italic leading-none">Caution</p>
|
||||
</div>
|
||||
<div class="p-6 text-center">
|
||||
<p class="text-3xl font-black text-slate-900 italic mb-4">{{ totalCaution|number_format(2, ',', ' ') }}€</p>
|
||||
{% set canPayCaution = (date('now') >= contrat.dateAt.modify('-7 days')) %}
|
||||
{% if canPayCaution and contratPaymentPay(contrat, 'accompte') %}
|
||||
<a data-turbo="false" href="{{ path('gestion_contrat_view', {'num': contrat.numReservation,'act':'cautionPay'}) }}" class="block w-full bg-slate-900 text-white py-4 rounded-xl font-black uppercase text-xs hover:bg-blue-600 transition-all shadow-md">Déposer l'empreinte</a>
|
||||
{% else %}
|
||||
<div class="p-3 bg-slate-50 rounded-xl border border-slate-100">
|
||||
<p class="text-[9px] text-slate-400 font-black uppercase tracking-tighter">Lien actif le {{ contrat.dateAt.modify('-7 days')|date('d/m/Y') }}</p>
|
||||
<p class="text-[8px] text-slate-300 italic mt-1">(7j avant prestation)</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="bg-white rounded-[2rem] border border-green-100 shadow-xl shadow-green-100/20 overflow-hidden">
|
||||
<div class="bg-green-500 p-6 text-white flex items-center gap-4">
|
||||
<div class="w-10 h-10 bg-white/20 rounded-xl flex items-center justify-center"><svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"></path></svg></div>
|
||||
<p class="text-sm font-black uppercase italic leading-none">Caution Sécurisée</p>
|
||||
</div>
|
||||
<div class="p-5">
|
||||
{% for payment in paymentCaution %}
|
||||
<div class="bg-slate-50 p-4 rounded-2xl border border-slate-100">
|
||||
<div class="flex justify-between items-center mb-2">
|
||||
<p class="text-[9px] font-black text-slate-400 uppercase tracking-widest">Dépôt</p>
|
||||
<p class="text-lg font-black text-slate-900 italic leading-none">{{ payment.amount|number_format(2, ',', ' ') }}€</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-[10px] font-black uppercase text-slate-700 italic">{{ payment.card.card.brand|default('Carte') }}</span>
|
||||
<span class="text-[9px] text-slate-400 font-bold uppercase italic">**** {{ payment.card.card.last4|default('') }}</span>
|
||||
{% if payment.card.card.funding == "debit" %}<span class="text-[7px] bg-slate-200 text-slate-500 px-1 rounded uppercase">Debit</span>{% endif %}
|
||||
</div>
|
||||
<div class="flex items-center gap-1 mt-2">
|
||||
<span class="w-1.5 h-1.5 rounded-full bg-green-500"></span>
|
||||
<p class="text-[8px] text-green-600 font-black uppercase tracking-tighter">Empreinte bancaire validée</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
{# ... (Garder tout le code précédent inchangé jusqu'à la fin de la grille 3 colonnes) ... #}
|
||||
|
||||
|
||||
@@ -84,9 +84,8 @@
|
||||
{% endif %}
|
||||
<div class="flex-1">
|
||||
<h4 class="font-bold text-slate-800">{{ item.product.name }}</h4>
|
||||
<p class="text-xs text-slate-500 line-clamp-1 mb-2">{{ item.product.description }}</p>
|
||||
|
||||
<div class="text-xs text-slate-600 bg-slate-50 p-2 rounded-lg border border-slate-100 inline-block">
|
||||
<div class="text-xs text-slate-600 bg-slate-50 p-2 rounded-lg border border-slate-100 inline-block mt-2">
|
||||
<div class="flex flex-wrap gap-x-3 gap-y-1">
|
||||
<span>1er jour : <strong class="text-slate-800">{{ item.price1Day|number_format(2, ',', ' ') }} €</strong></span>
|
||||
{% if cart.duration > 1 %}
|
||||
@@ -95,6 +94,20 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if item.options is defined and item.options|length > 0 %}
|
||||
<div class="mt-2 text-xs">
|
||||
<p class="font-semibold text-slate-500 uppercase tracking-wider mb-1">Options incluses :</p>
|
||||
<ul class="list-disc list-inside text-slate-600 space-y-0.5">
|
||||
{% for option in item.options %}
|
||||
<li>
|
||||
{{ option.name }}
|
||||
<span class="font-medium text-slate-800">({{ option.price|number_format(2, ',', ' ') }} €)</span>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<p class="font-bold text-slate-900">{{ item.totalPriceHT|number_format(2, ',', ' ') }} € HT</p>
|
||||
@@ -106,6 +119,25 @@
|
||||
{% else %}
|
||||
<p class="text-center text-slate-500 py-4">Aucun produit sélectionné.</p>
|
||||
{% endfor %}
|
||||
|
||||
{% if cart.options is defined and cart.options|length > 0 %}
|
||||
<div class="mt-4 border-t border-slate-100 pt-4">
|
||||
<h4 class="text-sm font-bold text-slate-900 mb-3">Options supplémentaires</h4>
|
||||
{% for option in cart.options %}
|
||||
<div class="flex items-center justify-between bg-white p-3 rounded-xl border border-slate-200 shadow-sm mb-2">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="bg-indigo-50 p-2 rounded-lg text-indigo-600">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
|
||||
</svg>
|
||||
</div>
|
||||
<span class="font-medium text-slate-800 text-sm">{{ option.name }}</span>
|
||||
</div>
|
||||
<span class="font-bold text-slate-900 text-sm">{{ option.price|number_format(2, ',', ' ') }} € HT</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="mt-6 border-t border-slate-200 pt-4 space-y-2">
|
||||
|
||||
@@ -3,6 +3,13 @@
|
||||
|
||||
{% block title %}Confirmation de votre demande{% endblock %}
|
||||
|
||||
{% block stylesheets %}
|
||||
{{ parent() }}
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
|
||||
integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY="
|
||||
crossorigin=""/>
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<div class="max-w-7xl mx-auto px-4 py-12">
|
||||
<div class="bg-white rounded-3xl shadow-xl p-8 border border-gray-100">
|
||||
@@ -52,7 +59,7 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{# Linked Options #}
|
||||
{% if item.options is defined and item.options|length > 0 %}
|
||||
<div class="mt-2 space-y-1">
|
||||
@@ -76,7 +83,7 @@
|
||||
{% else %}
|
||||
<p class="text-center text-slate-500 py-4">Aucun produit sélectionné.</p>
|
||||
{% endfor %}
|
||||
|
||||
|
||||
{# Orphan Options #}
|
||||
{% if cart.options is defined and cart.options|length > 0 %}
|
||||
<div class="border-t border-slate-100 pt-4 mt-4">
|
||||
@@ -241,10 +248,100 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-yellow-100 border-l-4 border-yellow-500 text-yellow-700 p-4 mb-6" role="alert">
|
||||
<p class="font-bold">Information importante sur la livraison</p>
|
||||
<p>Pour la livraison, des frais peuvent s'appliquer selon l'endroit et le type de structure réservée. Une fois que vous avez reconfirmé votre réservation, nos équipes vous retourneront un devis complet avec les frais de livraison si vous prévoyez cette option.</p>
|
||||
</div>
|
||||
{# Delivery Estimation #}
|
||||
{% if delivery is defined and delivery.estimation is not null %}
|
||||
<div class="bg-white rounded-2xl border border-slate-200 shadow-sm mb-8 overflow-hidden">
|
||||
<div class="p-6 border-b border-slate-100 flex items-center gap-3 bg-slate-50/50">
|
||||
<div class="bg-emerald-100 p-2 rounded-lg text-emerald-600">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="text-lg font-bold text-slate-900">Estimation des frais de livraison</h3>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col md:flex-row">
|
||||
{# Details (60%) #}
|
||||
<div class="w-full md:w-3/5 p-6 border-b md:border-b-0 md:border-r border-slate-100">
|
||||
{% if delivery.details.isFree %}
|
||||
<div class="bg-emerald-50 border border-emerald-100 rounded-xl p-4 mb-6">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="text-2xl font-black text-emerald-600 italic">Offert !</span>
|
||||
<span class="px-3 py-1 bg-white text-emerald-700 rounded-full text-[10px] font-bold uppercase tracking-wide shadow-sm">Zone gratuite</span>
|
||||
</div>
|
||||
<p class="text-sm text-emerald-800 mt-2 font-medium">Votre événement se trouve à moins de 10km de nos locaux.</p>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="mb-6">
|
||||
<span class="block text-xs font-bold text-slate-400 uppercase tracking-widest mb-1">Coût estimé</span>
|
||||
<p class="text-4xl font-black text-slate-900 italic">
|
||||
{{ delivery.estimation|format_currency('EUR') }}
|
||||
</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="space-y-3">
|
||||
<p class="text-[10px] font-black text-slate-400 uppercase tracking-widest mb-4">Détails du calcul</p>
|
||||
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-slate-500">Distance réelle (Aller)</span>
|
||||
<span class="font-bold text-slate-700">{{ delivery.details.distance|number_format(1, ',', ' ') }} km</span>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-slate-500">Franchise kilométrique</span>
|
||||
<span class="font-bold text-emerald-500">- 10.0 km (Offerts)</span>
|
||||
</div>
|
||||
|
||||
<div class="border-t border-slate-100 my-2"></div>
|
||||
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-slate-500">Distance facturée</span>
|
||||
<span class="font-bold text-slate-700">{{ delivery.details.chargedDistance|number_format(1, ',', ' ') }} km</span>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-slate-500">Nombre de trajets</span>
|
||||
<span class="font-bold text-slate-700">{{ delivery.details.trips }} (2 A/R)</span>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-slate-500">Tarif kilométrique</span>
|
||||
<span class="font-bold text-slate-700">{{ delivery.details.rate }} € / km</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if not delivery.details.isFree %}
|
||||
<div class="mt-4 p-3 bg-indigo-50 border border-indigo-100 rounded-xl text-center">
|
||||
<p class="text-[10px] font-bold text-indigo-400 uppercase mb-1">Formule appliquée</p>
|
||||
<code class="text-xs text-indigo-700 font-mono font-bold">
|
||||
({{ delivery.details.distance|number_format(1) }} - 10) x {{ delivery.details.trips }} x {{ delivery.details.rate }}€ = {{ delivery.estimation|number_format(2) }}€
|
||||
</code>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="mt-4 p-3 bg-slate-50 rounded-xl text-center">
|
||||
<p class="text-[10px] text-slate-400 italic">
|
||||
Cette estimation est indicative. Le montant définitif figurera sur votre devis.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Map (40%) #}
|
||||
<div class="w-full md:w-2/5 h-64 md:h-auto min-h-[16rem] bg-slate-100 relative">
|
||||
{% if delivery.geometry %}
|
||||
<leaflet-map class="absolute inset-0 z-0"
|
||||
data-geometry="{{ delivery.geometry|json_encode|e('html_attr') }}">
|
||||
</leaflet-map>
|
||||
{% else %}
|
||||
<div class="absolute inset-0 flex items-center justify-center text-slate-400">
|
||||
<span class="text-xs font-medium">Carte non disponible</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="pt-6 border-t border-slate-100 mt-8 flex flex-col md:flex-row justify-center gap-4">
|
||||
<a data-turbo="false" href="{{ path('reservation_generate_devis', {sessionId: session.uuid}) }}" class="w-full md:w-auto px-8 py-4 bg-gray-200 text-gray-800 font-bold rounded-2xl shadow-sm hover:shadow-md hover:scale-[1.01] transition-all flex items-center justify-center gap-2 text-lg">
|
||||
@@ -253,13 +350,23 @@
|
||||
</svg>
|
||||
Télécharger le devis
|
||||
</a>
|
||||
<a href="#" class="w-full md:w-auto px-8 py-4 bg-green-500 text-white font-bold rounded-2xl shadow-lg shadow-green-200 hover:shadow-xl hover:scale-[1.02] transition-all flex items-center justify-center gap-2 text-lg">
|
||||
Prendre les options de livraison
|
||||
</a>
|
||||
<button type="submit" class="w-full md:w-auto px-8 py-4 bg-gradient-to-r from-blue-600 to-blue-700 text-white font-bold rounded-2xl shadow-lg shadow-blue-200 hover:shadow-xl hover:scale-[1.02] transition-all flex items-center justify-center gap-2 text-lg">
|
||||
Je confirme la commande
|
||||
</button>
|
||||
|
||||
<form data-turbo="false" method="post" action="{{ path('reservation_flow_confirmed', {sessionId: session.uuid}) }}" onsubmit="localStorage.clear();" class="w-full md:w-auto">
|
||||
<button type="submit" class="w-full px-8 py-4 bg-gradient-to-r from-blue-600 to-blue-700 text-white font-bold rounded-2xl shadow-lg shadow-blue-200 hover:shadow-xl hover:scale-[1.02] transition-all flex items-center justify-center gap-2 text-lg">
|
||||
Je confirme la commande
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block javascripts %}
|
||||
{{ parent() }}
|
||||
{% if delivery is defined and delivery.geometry %}
|
||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
|
||||
integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo="
|
||||
crossorigin=""
|
||||
nonce="{{ csp_nonce('script') }}"></script>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
</label>
|
||||
<label class="flex-1 text-center py-3 rounded-xl cursor-pointer transition-all font-bold text-sm has-[:checked]:bg-white has-[:checked]:text-blue-600 has-[:checked]:shadow-sm text-gray-500">
|
||||
<input type="radio" name="type" value="buisness" class="hidden" onchange="toggleSiret(true)">
|
||||
Professionnel / Asso
|
||||
Professionnel / Asso / Mairie
|
||||
</label>
|
||||
</div>
|
||||
|
||||
@@ -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">
|
||||
</div>
|
||||
|
||||
{# SIRET (Caché par défaut) #}
|
||||
<div id="siret-container" class="md:col-span-2 hidden">
|
||||
<label class="block text-xs font-bold text-gray-400 uppercase mb-2 ml-2">Numéro SIRET</label>
|
||||
<input type="text" name="siret" placeholder="123 456 789 00012"
|
||||
class="w-full bg-gray-50 border-none rounded-2xl px-6 py-4 focus:ring-2 focus:ring-blue-500 outline-none">
|
||||
{# SIRET & Business Fields (Caché par défaut) #}
|
||||
<div id="siret-container" class="md:col-span-2 hidden grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div class="md:col-span-2">
|
||||
<label class="block text-xs font-bold text-gray-400 uppercase mb-2 ml-2">Type d'organisation</label>
|
||||
<select name="typCompany" class="w-full bg-gray-50 border-none rounded-2xl px-6 py-4 focus:ring-2 focus:ring-blue-500 outline-none appearance-none font-medium">
|
||||
<option value="Entreprise">Entreprise</option>
|
||||
<option value="Association">Association</option>
|
||||
<option value="Service Public">Service Public (Mairie, École...)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-bold text-gray-400 uppercase mb-2 ml-2">Raison Sociale</label>
|
||||
<input type="text" name="raisonSocial" placeholder="Ex: Ma Société SAS"
|
||||
class="w-full bg-gray-50 border-none rounded-2xl px-6 py-4 focus:ring-2 focus:ring-blue-500 outline-none">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-bold text-gray-400 uppercase mb-2 ml-2">Numéro SIRET</label>
|
||||
<input type="text" name="siret" placeholder="123 456 789 00012"
|
||||
class="w-full bg-gray-50 border-none rounded-2xl px-6 py-4 focus:ring-2 focus:ring-blue-500 outline-none">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Mot de passe #}
|
||||
|
||||
34
templates/revervation/success.twig
Normal file
34
templates/revervation/success.twig
Normal file
@@ -0,0 +1,34 @@
|
||||
{% extends 'revervation/base.twig' %}
|
||||
|
||||
{% block title %}Confirmation de commande | Ludik Event{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<div class="min-h-screen bg-slate-50 flex flex-col justify-center py-12 sm:px-6 lg:px-8">
|
||||
<div class="sm:mx-auto sm:w-full sm:max-w-md">
|
||||
<div class="bg-white py-8 px-4 shadow-xl shadow-slate-200 sm:rounded-3xl sm:px-10 border border-slate-100 text-center">
|
||||
|
||||
<div class="mx-auto flex items-center justify-center h-20 w-20 rounded-full bg-green-100 mb-6 animate-in zoom-in duration-500">
|
||||
<svg class="h-10 w-10 text-green-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<h2 class="text-3xl font-black text-slate-900 mb-2 uppercase italic tracking-tight">Merci !</h2>
|
||||
<p class="text-lg text-slate-600 font-medium mb-8">Votre demande a bien été envoyée.</p>
|
||||
|
||||
<div class="space-y-4 text-sm text-slate-500 bg-slate-50 p-6 rounded-2xl border border-slate-100">
|
||||
<p>Notre équipe va étudier votre demande et vous recevrez un devis complet par email dans les plus brefs délais.</p>
|
||||
<p>Pensez à vérifier vos courriers indésirables (spam).</p>
|
||||
</div>
|
||||
|
||||
<div class="mt-8">
|
||||
<a href="{{ path('reservation') }}" class="w-full flex justify-center py-4 px-4 border border-transparent rounded-2xl shadow-lg text-sm font-bold text-white bg-slate-900 hover:bg-[#f39e36] focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-slate-900 transition-all uppercase tracking-widest">
|
||||
Retour à l'accueil
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>,flowConfirmed
|
||||
|
||||
<local-storage-clear keys="lp_list,lp_options"></local-storage-clear>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user