✨ feat(formule): Intègre la gestion des formules et packs libres dans le parcours de réservation et les documents.
This commit is contained in:
@@ -130,7 +130,9 @@ export class SearchOptionsFormule extends HTMLButtonElement {
|
|||||||
const row = this.closest('.form-repeater__row');
|
const row = this.closest('.form-repeater__row');
|
||||||
if (row) {
|
if (row) {
|
||||||
const nameInput = row.querySelector('input[name*="[product]"]');
|
const nameInput = row.querySelector('input[name*="[product]"]');
|
||||||
|
const nameId = row.querySelector('input[name*="[id]"]');
|
||||||
if(nameInput) nameInput.value = option.name;
|
if(nameInput) nameInput.value = option.name;
|
||||||
|
if(nameId) nameId.value = option.id;
|
||||||
|
|
||||||
const fieldset = row.querySelector('fieldset');
|
const fieldset = row.querySelector('fieldset');
|
||||||
if (fieldset) {
|
if (fieldset) {
|
||||||
@@ -275,8 +277,10 @@ export class SearchProductFormule extends HTMLButtonElement {
|
|||||||
fillFormLine(product) {
|
fillFormLine(product) {
|
||||||
const row = this.closest('.form-repeater__row');
|
const row = this.closest('.form-repeater__row');
|
||||||
if (row) {
|
if (row) {
|
||||||
|
const nameId = row.querySelector('input[name*="[id]"]');
|
||||||
const nameInput = row.querySelector('input[name*="[product]"]');
|
const nameInput = row.querySelector('input[name*="[product]"]');
|
||||||
if(nameInput) nameInput.value = product.name;
|
if(nameInput) nameInput.value = product.name;
|
||||||
|
if(nameId) nameId.value = product.id;
|
||||||
|
|
||||||
const fieldset = row.querySelector('fieldset');
|
const fieldset = row.querySelector('fieldset');
|
||||||
if (fieldset) {
|
if (fieldset) {
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import { CookieBanner } from "./tools/CookieBanner.js";
|
|||||||
import { FlowReserve } from "./tools/FlowReserve.js";
|
import { FlowReserve } from "./tools/FlowReserve.js";
|
||||||
import { FlowDatePicker } from "./tools/FlowDatePicker.js";
|
import { FlowDatePicker } from "./tools/FlowDatePicker.js";
|
||||||
import { FlowAddToCart } from "./tools/FlowAddToCart.js";
|
import { FlowAddToCart } from "./tools/FlowAddToCart.js";
|
||||||
|
import { FlowFormuleConfigurator } from "./tools/FlowFormuleConfigurator.js";
|
||||||
|
import { SubmitClearStorage } from "./tools/SubmitClearStorage.js";
|
||||||
import { LeafletMap } from "./tools/LeafletMap.js";
|
import { LeafletMap } from "./tools/LeafletMap.js";
|
||||||
import { LocalStorageClear } from "./tools/LocalStorageClear.js";
|
import { LocalStorageClear } from "./tools/LocalStorageClear.js";
|
||||||
import * as Turbo from "@hotwired/turbo";
|
import * as Turbo from "@hotwired/turbo";
|
||||||
@@ -265,6 +267,12 @@ const registerComponents = () => {
|
|||||||
|
|
||||||
if(!customElements.get('flow-add-to-cart'))
|
if(!customElements.get('flow-add-to-cart'))
|
||||||
customElements.define('flow-add-to-cart',FlowAddToCart)
|
customElements.define('flow-add-to-cart',FlowAddToCart)
|
||||||
|
|
||||||
|
if(!customElements.get('flow-formule-configurator'))
|
||||||
|
customElements.define('flow-formule-configurator',FlowFormuleConfigurator)
|
||||||
|
|
||||||
|
if(!customElements.get('submit-clear-storage'))
|
||||||
|
customElements.define('submit-clear-storage',SubmitClearStorage, { extends: 'form' })
|
||||||
};
|
};
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
|||||||
190
assets/tools/FlowFormuleConfigurator.js
Normal file
190
assets/tools/FlowFormuleConfigurator.js
Normal file
@@ -0,0 +1,190 @@
|
|||||||
|
export class FlowFormuleConfigurator extends HTMLElement {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.counts = { structure: 0, alimentaire: 0, barnum: 0 };
|
||||||
|
this.selection = [];
|
||||||
|
this.blocked = false;
|
||||||
|
this.mode = 'free';
|
||||||
|
}
|
||||||
|
|
||||||
|
connectedCallback() {
|
||||||
|
try {
|
||||||
|
this.limits = JSON.parse(this.getAttribute('data-limits') || '{}');
|
||||||
|
this.prices = JSON.parse(this.getAttribute('data-prices') || '{}');
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Invalid limits/prices JSON', e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.formuleId = this.getAttribute('data-formule-id');
|
||||||
|
this.mode = this.getAttribute('data-mode') || 'free';
|
||||||
|
this.validateBtn = this.querySelector('#btn-validate-pack');
|
||||||
|
|
||||||
|
if (this.mode === 'pack') {
|
||||||
|
try {
|
||||||
|
this.selection = JSON.parse(this.getAttribute('data-preselected-ids') || '[]');
|
||||||
|
} catch (e) { console.error('Invalid preselected-ids JSON', e); }
|
||||||
|
} else {
|
||||||
|
this.querySelectorAll('.product-card').forEach(card => {
|
||||||
|
card.addEventListener('click', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
this.toggleItem(card);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.validateBtn) {
|
||||||
|
this.validateBtn.addEventListener('click', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
this.validate();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.checkDuration();
|
||||||
|
this.updateUI();
|
||||||
|
}
|
||||||
|
|
||||||
|
checkDuration() {
|
||||||
|
const datesStr = sessionStorage.getItem('reservation_dates');
|
||||||
|
if (!datesStr) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const dates = JSON.parse(datesStr);
|
||||||
|
if (!dates.start || !dates.end) return;
|
||||||
|
|
||||||
|
const start = new Date(dates.start);
|
||||||
|
const end = new Date(dates.end);
|
||||||
|
const diffTime = Math.abs(end - start);
|
||||||
|
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)) + 1;
|
||||||
|
|
||||||
|
let message = null;
|
||||||
|
let block = false;
|
||||||
|
|
||||||
|
if (diffDays > 5) {
|
||||||
|
message = `Attention : La durée de votre réservation (${diffDays} jours) dépasse la limite autorisée de 5 jours pour cette formule.`;
|
||||||
|
block = true;
|
||||||
|
} else if (diffDays >= 3 && diffDays <= 5) {
|
||||||
|
message = `Information : Pour une durée de ${diffDays} jours, le tarif "5 jours" sera appliqué automatiquement.`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message) {
|
||||||
|
let msgContainer = this.querySelector('.formule-warning');
|
||||||
|
if (!msgContainer) {
|
||||||
|
msgContainer = document.createElement('div');
|
||||||
|
this.prepend(msgContainer);
|
||||||
|
}
|
||||||
|
msgContainer.className = `formule-warning p-4 mb-6 rounded-2xl text-sm font-bold text-center border-2 ${block ? 'bg-red-50 border-red-100 text-red-600' : 'bg-blue-50 border-blue-100 text-blue-600'}`;
|
||||||
|
msgContainer.innerHTML = message;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.blocked = block;
|
||||||
|
this.updateUI();
|
||||||
|
|
||||||
|
} catch (e) { console.error(e); }
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleItem(el) {
|
||||||
|
if (this.blocked || this.mode === 'pack') return;
|
||||||
|
|
||||||
|
const category = el.getAttribute('data-category');
|
||||||
|
const id = el.getAttribute('data-id');
|
||||||
|
const isSelected = el.getAttribute('data-selected') === 'true';
|
||||||
|
|
||||||
|
if (isSelected) {
|
||||||
|
this.deselect(el, category, id);
|
||||||
|
} else {
|
||||||
|
if (this.counts[category] < this.limits[category]) {
|
||||||
|
this.select(el, category, id);
|
||||||
|
} else {
|
||||||
|
this.shake(el);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.updateUI();
|
||||||
|
}
|
||||||
|
|
||||||
|
select(el, category, id) {
|
||||||
|
el.setAttribute('data-selected', 'true');
|
||||||
|
el.classList.add('ring-4', 'ring-[#f39e36]', 'scale-95');
|
||||||
|
|
||||||
|
const indicator = el.querySelector('.selection-indicator');
|
||||||
|
if (indicator) {
|
||||||
|
indicator.classList.remove('opacity-0', 'group-hover:opacity-100');
|
||||||
|
indicator.classList.add('opacity-100');
|
||||||
|
indicator.querySelector('div')?.classList.remove('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
this.counts[category]++;
|
||||||
|
this.selection.push(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
deselect(el, category, id) {
|
||||||
|
el.setAttribute('data-selected', 'false');
|
||||||
|
el.classList.remove('ring-4', 'ring-[#f39e36]', 'scale-95');
|
||||||
|
|
||||||
|
const indicator = el.querySelector('.selection-indicator');
|
||||||
|
if (indicator) {
|
||||||
|
indicator.classList.remove('opacity-100');
|
||||||
|
indicator.classList.add('opacity-0', 'group-hover:opacity-100');
|
||||||
|
indicator.querySelector('div')?.classList.add('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
this.counts[category]--;
|
||||||
|
this.selection = this.selection.filter(item => item !== id);
|
||||||
|
}
|
||||||
|
|
||||||
|
shake(el) {
|
||||||
|
el.classList.add('animate-pulse');
|
||||||
|
setTimeout(() => el.classList.remove('animate-pulse'), 500);
|
||||||
|
alert("Limite atteinte pour cette catégorie.");
|
||||||
|
}
|
||||||
|
|
||||||
|
updateUI() {
|
||||||
|
if (this.mode === 'free') {
|
||||||
|
['structure', 'alimentaire', 'barnum'].forEach(cat => {
|
||||||
|
const span = this.querySelector(`#count-${cat}`);
|
||||||
|
if (span) span.innerText = this.counts[cat];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const total = this.selection.length;
|
||||||
|
if (this.validateBtn) {
|
||||||
|
if (this.blocked) {
|
||||||
|
this.validateBtn.innerText = 'Durée non autorisée (> 5j)';
|
||||||
|
this.validateBtn.classList.add('opacity-50', 'pointer-events-none', 'translate-y-4', 'bg-red-600');
|
||||||
|
this.validateBtn.classList.remove('bg-slate-900', 'hover:bg-[#fc0e50]');
|
||||||
|
} else {
|
||||||
|
this.validateBtn.innerText = this.mode === 'pack' ? 'Réserver ce pack' : `Valider mon pack (${total})`;
|
||||||
|
this.validateBtn.classList.remove('bg-red-600');
|
||||||
|
this.validateBtn.classList.add('bg-slate-900', 'hover:bg-[#fc0e50]');
|
||||||
|
|
||||||
|
if (total > 0) {
|
||||||
|
this.validateBtn.classList.remove('opacity-50', 'pointer-events-none', 'translate-y-4');
|
||||||
|
} else {
|
||||||
|
this.validateBtn.classList.add('opacity-50', 'pointer-events-none', 'translate-y-4');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
validate() {
|
||||||
|
if (this.blocked || this.selection.length === 0) return;
|
||||||
|
|
||||||
|
sessionStorage.setItem('active_formule', this.formuleId);
|
||||||
|
|
||||||
|
let list = [];
|
||||||
|
try {
|
||||||
|
list = JSON.parse(sessionStorage.getItem('pl_list') || '[]');
|
||||||
|
} catch(e) {}
|
||||||
|
|
||||||
|
const newItems = this.selection.filter(id => !list.includes(id));
|
||||||
|
if (newItems.length > 0) {
|
||||||
|
list = [...list, ...newItems];
|
||||||
|
sessionStorage.setItem('pl_list', JSON.stringify(list));
|
||||||
|
}
|
||||||
|
|
||||||
|
window.dispatchEvent(new CustomEvent('cart:updated'));
|
||||||
|
|
||||||
|
const cart = document.querySelector('[is="flow-reserve"]');
|
||||||
|
if (cart) cart.open();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -267,7 +267,8 @@ export class FlowReserve extends HTMLAnchorElement {
|
|||||||
ids,
|
ids,
|
||||||
options,
|
options,
|
||||||
start: dates.start,
|
start: dates.start,
|
||||||
end: dates.end
|
end: dates.end,
|
||||||
|
formule: sessionStorage.getItem('active_formule')
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -362,6 +363,7 @@ export class FlowReserve extends HTMLAnchorElement {
|
|||||||
</div>
|
</div>
|
||||||
<div class="flex-1 flex flex-col justify-between">
|
<div class="flex-1 flex flex-col justify-between">
|
||||||
<div>
|
<div>
|
||||||
|
${product.in_formule ? `<span class="inline-block px-2 py-0.5 rounded text-[8px] font-black uppercase tracking-widest bg-blue-100 text-blue-600 mb-1">Inclus</span>` : ''}
|
||||||
<h4 class="font-black text-slate-900 leading-tight uppercase italic text-sm line-clamp-2">${product.name}</h4>
|
<h4 class="font-black text-slate-900 leading-tight uppercase italic text-sm line-clamp-2">${product.name}</h4>
|
||||||
<div class="mt-1 flex flex-wrap gap-2 text-[10px] text-slate-500 font-bold uppercase">
|
<div class="mt-1 flex flex-wrap gap-2 text-[10px] text-slate-500 font-bold uppercase">
|
||||||
<span>1J: ${this.formatPrice(product.priceHt1Day)} HT</span>
|
<span>1J: ${this.formatPrice(product.priceHt1Day)} HT</span>
|
||||||
@@ -395,13 +397,23 @@ export class FlowReserve extends HTMLAnchorElement {
|
|||||||
const total = data.total || {};
|
const total = data.total || {};
|
||||||
const hasTva = total.totalTva > 0;
|
const hasTva = total.totalTva > 0;
|
||||||
const promotion = data.promotion;
|
const promotion = data.promotion;
|
||||||
|
const formuleName = total.formule;
|
||||||
|
|
||||||
let promoHtml = '';
|
let promoHtml = '';
|
||||||
let originalTotalHT = total.totalHT;
|
let originalTotalHT = total.totalHT;
|
||||||
|
|
||||||
|
if (formuleName) {
|
||||||
|
promoHtml += `
|
||||||
|
<div class="flex justify-between text-xs text-blue-600 font-bold uppercase mb-2">
|
||||||
|
<span>Formule</span>
|
||||||
|
<span>${formuleName}</span>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
if (promotion && total.discount > 0) {
|
if (promotion && total.discount > 0) {
|
||||||
originalTotalHT = total.totalHT + total.discount;
|
originalTotalHT = total.totalHT + total.discount;
|
||||||
promoHtml = `
|
promoHtml += `
|
||||||
<div class="flex justify-between text-xs text-[#f39e36] font-bold uppercase">
|
<div class="flex justify-between text-xs text-[#f39e36] font-bold uppercase">
|
||||||
<span>${promotion.name} (-${promotion.percentage}%)</span>
|
<span>${promotion.name} (-${promotion.percentage}%)</span>
|
||||||
<span>-${this.formatPrice(total.discount)}</span>
|
<span>-${this.formatPrice(total.discount)}</span>
|
||||||
@@ -460,7 +472,8 @@ export class FlowReserve extends HTMLAnchorElement {
|
|||||||
ids,
|
ids,
|
||||||
options,
|
options,
|
||||||
start: dates.start,
|
start: dates.start,
|
||||||
end: dates.end
|
end: dates.end,
|
||||||
|
formule: sessionStorage.getItem('active_formule')
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
12
assets/tools/SubmitClearStorage.js
Normal file
12
assets/tools/SubmitClearStorage.js
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
export class SubmitClearStorage extends HTMLFormElement {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
connectedCallback() {
|
||||||
|
this.addEventListener('submit', () => {
|
||||||
|
localStorage.clear();
|
||||||
|
sessionStorage.clear();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -38,7 +38,7 @@ export class UtmAccount extends HTMLElement {
|
|||||||
|
|
||||||
// 4. Envoi du sessionId à ton backend Symfony
|
// 4. Envoi du sessionId à ton backend Symfony
|
||||||
if (sessionId) {
|
if (sessionId) {
|
||||||
await fetch('/reservation/umami', {
|
await fetch('/umami', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ umami_session: sessionId })
|
body: JSON.stringify({ umami_session: sessionId })
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ nelmio_security:
|
|||||||
- "https://challenges.cloudflare.com"
|
- "https://challenges.cloudflare.com"
|
||||||
- "https://tools-security.esy-web.dev"
|
- "https://tools-security.esy-web.dev"
|
||||||
- "https://checkout.stripe.com/"
|
- "https://checkout.stripe.com/"
|
||||||
|
- "https://unpkg.com/"
|
||||||
frame-src:
|
frame-src:
|
||||||
- "'self'"
|
- "'self'"
|
||||||
- "https://chat.esy-web.dev"
|
- "https://chat.esy-web.dev"
|
||||||
|
|||||||
35
migrations/Version20260209123602.php
Normal file
35
migrations/Version20260209123602.php
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
<?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 Version20260209123602 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 formule_id INT DEFAULT NULL');
|
||||||
|
$this->addSql('ALTER TABLE order_session ADD CONSTRAINT FK_263E7C9F2A68F4D1 FOREIGN KEY (formule_id) REFERENCES formules (id)');
|
||||||
|
$this->addSql('CREATE INDEX IDX_263E7C9F2A68F4D1 ON order_session (formule_id)');
|
||||||
|
}
|
||||||
|
|
||||||
|
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 CONSTRAINT FK_263E7C9F2A68F4D1');
|
||||||
|
$this->addSql('DROP INDEX IDX_263E7C9F2A68F4D1');
|
||||||
|
$this->addSql('ALTER TABLE order_session DROP formule_id');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -139,6 +139,18 @@ class FormulesController extends AbstractController
|
|||||||
$form->handleRequest($request);
|
$form->handleRequest($request);
|
||||||
|
|
||||||
if ($form->isSubmitted() && $form->isValid()) {
|
if ($form->isSubmitted() && $form->isValid()) {
|
||||||
|
if($formule->getType() == "free") {
|
||||||
|
$r = $formule->getFormulesRestriction();
|
||||||
|
if(!$r) {
|
||||||
|
$r = new FormulesRestriction();
|
||||||
|
$r->setNbStructureMax(0);
|
||||||
|
$r->setNbAlimentaireMax(0);
|
||||||
|
$r->setNbBarhumsMax(0);
|
||||||
|
$r->setRestrictionConfig([]);
|
||||||
|
$this->em->persist($r);
|
||||||
|
$formule->setFormulesRestriction($r);
|
||||||
|
}
|
||||||
|
}
|
||||||
$this->em->persist($formule);
|
$this->em->persist($formule);
|
||||||
$this->em->flush();
|
$this->em->flush();
|
||||||
|
|
||||||
@@ -341,6 +353,6 @@ class FormulesController extends AbstractController
|
|||||||
if ($restriction && !empty($restriction->getRestrictionConfig())) {
|
if ($restriction && !empty($restriction->getRestrictionConfig())) {
|
||||||
return $restriction->getRestrictionConfig();
|
return $restriction->getRestrictionConfig();
|
||||||
}
|
}
|
||||||
return [['type' => 'structure', 'product' => '']];
|
return [['type' => 'structure', 'product' => '','id'=>0]];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ namespace App\Controller;
|
|||||||
|
|
||||||
use App\Entity\Customer;
|
use App\Entity\Customer;
|
||||||
use App\Entity\CustomerTracking;
|
use App\Entity\CustomerTracking;
|
||||||
|
use App\Entity\Formules;
|
||||||
use App\Entity\Product;
|
use App\Entity\Product;
|
||||||
use App\Entity\ProductReserve;
|
use App\Entity\ProductReserve;
|
||||||
use App\Entity\Promotion;
|
use App\Entity\Promotion;
|
||||||
@@ -103,6 +104,29 @@ class ReserverController extends AbstractController
|
|||||||
$selectedOptionsMap = $sessionData['options'] ?? [];
|
$selectedOptionsMap = $sessionData['options'] ?? [];
|
||||||
$runningTotalHT = 0;
|
$runningTotalHT = 0;
|
||||||
|
|
||||||
|
$formule = $session->getFormule();
|
||||||
|
$formuleConfig = [];
|
||||||
|
if ($formule) {
|
||||||
|
$restriction = $formule->getFormulesRestriction();
|
||||||
|
if ($restriction) {
|
||||||
|
$formuleConfig = $restriction->getRestrictionConfig() ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$formulaBasePrice = 0;
|
||||||
|
if ($duration <= 1) $formulaBasePrice = $formule->getPrice1j();
|
||||||
|
elseif ($duration <= 2) $formulaBasePrice = $formule->getPrice2j();
|
||||||
|
else $formulaBasePrice = $formule->getPrice5j();
|
||||||
|
|
||||||
|
$line = new DevisLine();
|
||||||
|
$line->setProduct("Formule : " . $formule->getName());
|
||||||
|
$line->setPriceHt($formulaBasePrice);
|
||||||
|
$line->setPriceHtSup(0);
|
||||||
|
$line->setDay(1);
|
||||||
|
$devis->addDevisLine($line);
|
||||||
|
|
||||||
|
$runningTotalHT += $formulaBasePrice;
|
||||||
|
}
|
||||||
|
|
||||||
if (!empty($ids)) {
|
if (!empty($ids)) {
|
||||||
$products = $productRepository->findBy(['id' => $ids]);
|
$products = $productRepository->findBy(['id' => $ids]);
|
||||||
$processedProductIds = [];
|
$processedProductIds = [];
|
||||||
@@ -110,15 +134,31 @@ class ReserverController extends AbstractController
|
|||||||
foreach ($products as $product) {
|
foreach ($products as $product) {
|
||||||
$processedProductIds[] = $product->getId();
|
$processedProductIds[] = $product->getId();
|
||||||
|
|
||||||
|
$isInFormule = false;
|
||||||
|
if ($formule) {
|
||||||
|
foreach ($formuleConfig as $c) {
|
||||||
|
if (($c['product'] ?? '') === $product->getName()) {
|
||||||
|
$isInFormule = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$line = new DevisLine();
|
$line = new DevisLine();
|
||||||
$line->setProduct($product->getName());
|
$line->setProduct($product->getName());
|
||||||
|
|
||||||
|
if ($formule && $isInFormule) {
|
||||||
|
$line->setPriceHt(0);
|
||||||
|
$line->setPriceHtSup(0);
|
||||||
|
} else {
|
||||||
$line->setPriceHt($product->getPriceDay());
|
$line->setPriceHt($product->getPriceDay());
|
||||||
$line->setPriceHtSup($product->getPriceSup());
|
$line->setPriceHtSup($product->getPriceSup());
|
||||||
|
$runningTotalHT += $product->getPriceDay() + ($product->getPriceSup() * max(0, $duration - 1));
|
||||||
|
}
|
||||||
|
|
||||||
$line->setDay($duration);
|
$line->setDay($duration);
|
||||||
$devis->addDevisLine($line);
|
$devis->addDevisLine($line);
|
||||||
|
|
||||||
$runningTotalHT += $product->getPriceDay() + ($product->getPriceSup() * max(0, $duration - 1));
|
|
||||||
|
|
||||||
if (isset($selectedOptionsMap[$product->getId()])) {
|
if (isset($selectedOptionsMap[$product->getId()])) {
|
||||||
$optionIds = $selectedOptionsMap[$product->getId()];
|
$optionIds = $selectedOptionsMap[$product->getId()];
|
||||||
if (!empty($optionIds)) {
|
if (!empty($optionIds)) {
|
||||||
@@ -377,7 +417,8 @@ class ReserverController extends AbstractController
|
|||||||
ProductRepository $productRepository,
|
ProductRepository $productRepository,
|
||||||
OptionsRepository $optionsRepository,
|
OptionsRepository $optionsRepository,
|
||||||
UploaderHelper $uploaderHelper,
|
UploaderHelper $uploaderHelper,
|
||||||
PromotionRepository $promotionRepository
|
PromotionRepository $promotionRepository,
|
||||||
|
FormulesRepository $formulesRepository
|
||||||
): Response {
|
): Response {
|
||||||
$data = json_decode($request->getContent(), true);
|
$data = json_decode($request->getContent(), true);
|
||||||
$ids = $data['ids'] ?? [];
|
$ids = $data['ids'] ?? [];
|
||||||
@@ -398,7 +439,13 @@ class ReserverController extends AbstractController
|
|||||||
$promotions = $promotionRepository->findActivePromotions(new \DateTime());
|
$promotions = $promotionRepository->findActivePromotions(new \DateTime());
|
||||||
$promotion = $promotions[0] ?? null;
|
$promotion = $promotions[0] ?? null;
|
||||||
|
|
||||||
$cartData = $this->buildCartData($products, $selectedOptionsMap, $duration, $optionsRepository, $uploaderHelper, $promotion);
|
$formuleId = $data['formule'] ?? null;
|
||||||
|
$formule = null;
|
||||||
|
if ($formuleId) {
|
||||||
|
$formule = $formulesRepository->find($formuleId);
|
||||||
|
}
|
||||||
|
|
||||||
|
$cartData = $this->buildCartData($products, $selectedOptionsMap, $duration, $optionsRepository, $uploaderHelper, $promotion, $formule);
|
||||||
|
|
||||||
return new JsonResponse([
|
return new JsonResponse([
|
||||||
'start_date' => $startStr,
|
'start_date' => $startStr,
|
||||||
@@ -407,12 +454,13 @@ class ReserverController extends AbstractController
|
|||||||
'options' => $cartData['rootOptions'],
|
'options' => $cartData['rootOptions'],
|
||||||
'unavailable_products_ids' => $removedIds,
|
'unavailable_products_ids' => $removedIds,
|
||||||
'total' => $cartData['total'],
|
'total' => $cartData['total'],
|
||||||
'promotion' => $promotion ? ['name' => $promotion->getName(), 'percentage' => $promotion->getPercentage()] : null
|
'promotion' => $promotion ? ['name' => $promotion->getName(), 'percentage' => $promotion->getPercentage()] : null,
|
||||||
|
'formule' => $formule ? ['name' => $formule->getName()] : null
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[Route('/session', name: 'reservation_session_create', methods: ['POST'])]
|
#[Route('/session', name: 'reservation_session_create', methods: ['POST'])]
|
||||||
public function createSession(Request $request, EntityManagerInterface $em, OrderSessionRepository $sessionRepository, PromotionRepository $promotionRepository): Response
|
public function createSession(Request $request, EntityManagerInterface $em, OrderSessionRepository $sessionRepository, PromotionRepository $promotionRepository, FormulesRepository $formulesRepository, ProductRepository $productRepository): Response
|
||||||
{
|
{
|
||||||
$data = json_decode($request->getContent(), true);
|
$data = json_decode($request->getContent(), true);
|
||||||
$existingUuid = $request->getSession()->get('order_session_uuid');
|
$existingUuid = $request->getSession()->get('order_session_uuid');
|
||||||
@@ -442,6 +490,65 @@ class ReserverController extends AbstractController
|
|||||||
$session->setPromotion(null);
|
$session->setPromotion(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$formuleId = $data['formule'] ?? null;
|
||||||
|
if ($formuleId) {
|
||||||
|
$formule = $formulesRepository->find($formuleId);
|
||||||
|
if ($formule) {
|
||||||
|
// --- DURATION CHECK ---
|
||||||
|
$startStr = $data['start'] ?? null;
|
||||||
|
$endStr = $data['end'] ?? null;
|
||||||
|
$duration = $this->calculateDuration($startStr, $endStr);
|
||||||
|
|
||||||
|
if ($duration > 5) {
|
||||||
|
return new JsonResponse(['error' => "Impossible d'ajouter cette formule : la durée de réservation excède 5 jours."], Response::HTTP_BAD_REQUEST);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- SECURITY CHECK ---
|
||||||
|
$restriction = $formule->getFormulesRestriction();
|
||||||
|
if ($restriction) {
|
||||||
|
$ids = $data['ids'] ?? [];
|
||||||
|
if (!empty($ids)) {
|
||||||
|
$products = $productRepository->findBy(['id' => $ids]);
|
||||||
|
$config = $restriction->getRestrictionConfig() ?? [];
|
||||||
|
|
||||||
|
$counts = ['structure' => 0, 'alimentaire' => 0, 'barhnums' => 0];
|
||||||
|
|
||||||
|
foreach ($products as $product) {
|
||||||
|
$pName = $product->getName();
|
||||||
|
$type = null;
|
||||||
|
foreach ($config as $c) {
|
||||||
|
if (($c['product'] ?? '') === $pName) {
|
||||||
|
$type = $c['type'] ?? null;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($type === 'structure') {
|
||||||
|
$counts['structure']++;
|
||||||
|
} elseif ($type === 'alimentaire') {
|
||||||
|
$counts['alimentaire']++;
|
||||||
|
} elseif ($type) {
|
||||||
|
// Only count if type is defined in config (meaning it's part of the formula)
|
||||||
|
$counts['barhnums']++;
|
||||||
|
}
|
||||||
|
// Products not in the formula config are considered "extras" and allowed.
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($counts['structure'] > $restriction->getNbStructureMax()) {
|
||||||
|
return new JsonResponse(['error' => "Le nombre maximum de structures est dépassé ({$restriction->getNbStructureMax()})."], Response::HTTP_BAD_REQUEST);
|
||||||
|
}
|
||||||
|
if ($counts['alimentaire'] > $restriction->getNbAlimentaireMax()) {
|
||||||
|
return new JsonResponse(['error' => "Le nombre maximum d'éléments alimentaires est dépassé ({$restriction->getNbAlimentaireMax()})."], Response::HTTP_BAD_REQUEST);
|
||||||
|
}
|
||||||
|
if ($counts['barhnums'] > $restriction->getNbBarhumsMax()) {
|
||||||
|
return new JsonResponse(['error' => "Le nombre maximum de barnums/mobilier est dépassé ({$restriction->getNbBarhumsMax()})."], Response::HTTP_BAD_REQUEST);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$session->setFormule($formule);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$user = $this->getUser();
|
$user = $this->getUser();
|
||||||
if ($user instanceof Customer) {
|
if ($user instanceof Customer) {
|
||||||
$session->setCustomer($user);
|
$session->setCustomer($user);
|
||||||
@@ -531,7 +638,9 @@ class ReserverController extends AbstractController
|
|||||||
$promotion->setPercentage($promoData['percentage']);
|
$promotion->setPercentage($promoData['percentage']);
|
||||||
}
|
}
|
||||||
|
|
||||||
$cartData = $this->buildCartData($products, $selectedOptionsMap, $duration, $optionsRepository, $uploaderHelper, $promotion);
|
$formule = $session->getFormule();
|
||||||
|
|
||||||
|
$cartData = $this->buildCartData($products, $selectedOptionsMap, $duration, $optionsRepository, $uploaderHelper, $promotion, $formule);
|
||||||
|
|
||||||
// --- Calcul Frais de Livraison ---
|
// --- Calcul Frais de Livraison ---
|
||||||
$deliveryData = $this->calculateDelivery(
|
$deliveryData = $this->calculateDelivery(
|
||||||
@@ -554,6 +663,7 @@ class ReserverController extends AbstractController
|
|||||||
'totalTTC' => $cartData['total']['totalTTC'],
|
'totalTTC' => $cartData['total']['totalTTC'],
|
||||||
'discount' => $cartData['total']['discount'],
|
'discount' => $cartData['total']['discount'],
|
||||||
'promotion' => $cartData['total']['promotion'],
|
'promotion' => $cartData['total']['promotion'],
|
||||||
|
'formule' => $cartData['total']['formule'],
|
||||||
'tvaEnabled' => $cartData['tvaEnabled'],
|
'tvaEnabled' => $cartData['tvaEnabled'],
|
||||||
],
|
],
|
||||||
'delivery' => $deliveryData
|
'delivery' => $deliveryData
|
||||||
@@ -633,7 +743,9 @@ class ReserverController extends AbstractController
|
|||||||
$promotion->setPercentage($promoData['percentage']);
|
$promotion->setPercentage($promoData['percentage']);
|
||||||
}
|
}
|
||||||
|
|
||||||
$cartData = $this->buildCartData($products, $selectedOptionsMap, $duration, $optionsRepository, $uploaderHelper, $promotion);
|
$formule = $session->getFormule();
|
||||||
|
|
||||||
|
$cartData = $this->buildCartData($products, $selectedOptionsMap, $duration, $optionsRepository, $uploaderHelper, $promotion, $formule);
|
||||||
|
|
||||||
return $this->render('revervation/flow.twig', [
|
return $this->render('revervation/flow.twig', [
|
||||||
'session' => $session,
|
'session' => $session,
|
||||||
@@ -650,6 +762,7 @@ class ReserverController extends AbstractController
|
|||||||
'totalTTC' => $cartData['total']['totalTTC'],
|
'totalTTC' => $cartData['total']['totalTTC'],
|
||||||
'discount' => $cartData['total']['discount'],
|
'discount' => $cartData['total']['discount'],
|
||||||
'promotion' => $cartData['total']['promotion'],
|
'promotion' => $cartData['total']['promotion'],
|
||||||
|
'formule' => $cartData['total']['formule'],
|
||||||
'tvaEnabled' => $cartData['tvaEnabled'],
|
'tvaEnabled' => $cartData['tvaEnabled'],
|
||||||
]
|
]
|
||||||
]);
|
]);
|
||||||
@@ -1209,15 +1322,24 @@ class ReserverController extends AbstractController
|
|||||||
return $result;
|
return $result;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function buildCartData(array $products, array $selectedOptionsMap, int $duration, OptionsRepository $optionsRepository, UploaderHelper $uploaderHelper, ?Promotion $promotion = null): array
|
private function buildCartData(array $products, array $selectedOptionsMap, int $duration, OptionsRepository $optionsRepository, UploaderHelper $uploaderHelper, ?Promotion $promotion = null, ?Formules $formule = null): array
|
||||||
{
|
{
|
||||||
$items = [];
|
$items = [];
|
||||||
$rootOptions = [];
|
$rootOptions = [];
|
||||||
$totalHT = 0;
|
$totalHT = 0;
|
||||||
|
$formulaExtras = 0;
|
||||||
$tvaEnabled = $this->isTvaEnabled();
|
$tvaEnabled = $this->isTvaEnabled();
|
||||||
$tvaRate = $tvaEnabled ? 0.20 : 0;
|
$tvaRate = $tvaEnabled ? 0.20 : 0;
|
||||||
$processedProductIds = [];
|
$processedProductIds = [];
|
||||||
|
|
||||||
|
$formuleConfig = [];
|
||||||
|
if ($formule) {
|
||||||
|
$restriction = $formule->getFormulesRestriction();
|
||||||
|
if ($restriction) {
|
||||||
|
$formuleConfig = $restriction->getRestrictionConfig() ?? [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
foreach ($products as $product) {
|
foreach ($products as $product) {
|
||||||
$processedProductIds[] = $product->getId();
|
$processedProductIds[] = $product->getId();
|
||||||
$price1Day = $product->getPriceDay();
|
$price1Day = $product->getPriceDay();
|
||||||
@@ -1247,31 +1369,54 @@ class ReserverController extends AbstractController
|
|||||||
$optionsTotalHT += $optPrice;
|
$optionsTotalHT += $optPrice;
|
||||||
} else {
|
} else {
|
||||||
$rootOptions[] = $optData;
|
$rootOptions[] = $optData;
|
||||||
|
if ($formule) {
|
||||||
|
$formulaExtras += $optPrice;
|
||||||
|
} else {
|
||||||
$totalHT += $optPrice;
|
$totalHT += $optPrice;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$productTotalHT += $optionsTotalHT;
|
$productTotalHT += $optionsTotalHT;
|
||||||
$productTotalTTC = $productTotalHT * (1 + $tvaRate);
|
$productTotalTTC = $productTotalHT * (1 + $tvaRate);
|
||||||
|
|
||||||
|
$isInFormule = false;
|
||||||
|
if ($formule) {
|
||||||
|
foreach ($formuleConfig as $c) {
|
||||||
|
if (($c['product'] ?? '') === $product->getName()) {
|
||||||
|
$isInFormule = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($formule) {
|
||||||
|
if ($isInFormule) {
|
||||||
|
$formulaExtras += $optionsTotalHT;
|
||||||
|
} else {
|
||||||
|
$formulaExtras += $productTotalHT;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$totalHT += $productTotalHT;
|
||||||
|
}
|
||||||
|
|
||||||
$items[] = [
|
$items[] = [
|
||||||
'id' => $product->getId(), // Ensure ID is present for basketJson
|
'id' => $product->getId(),
|
||||||
'name' => $product->getName(),
|
'name' => $product->getName(),
|
||||||
'product' => $product, // Kept for twig compatibility if needed
|
'product' => $product,
|
||||||
'image' => $uploaderHelper->asset($product, 'imageFile'),
|
'image' => $uploaderHelper->asset($product, 'imageFile'),
|
||||||
'priceHt1Day' => $price1Day, // For basketJson
|
'priceHt1Day' => $price1Day,
|
||||||
'priceHTSupDay' => $priceSup, // For basketJson
|
'priceHTSupDay' => $priceSup,
|
||||||
'priceTTC1Day' => $price1Day * (1 + $tvaRate),
|
'priceTTC1Day' => $price1Day * (1 + $tvaRate),
|
||||||
'price1Day' => $price1Day, // For flow template
|
'price1Day' => $price1Day,
|
||||||
'priceSup' => $priceSup, // For flow template
|
'priceSup' => $priceSup,
|
||||||
'totalPriceHT' => $productTotalHT,
|
'totalPriceHT' => $productTotalHT,
|
||||||
'totalPriceTTC' => $productTotalTTC,
|
'totalPriceTTC' => $productTotalTTC,
|
||||||
'options' => $productOptions
|
'options' => $productOptions,
|
||||||
|
'in_formule' => $isInFormule
|
||||||
];
|
];
|
||||||
|
|
||||||
$totalHT += $productTotalHT;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Traiter les options orphelines
|
// Traiter les options orphelines
|
||||||
@@ -1286,10 +1431,27 @@ class ReserverController extends AbstractController
|
|||||||
'price' => $optPrice,
|
'price' => $optPrice,
|
||||||
'orphan_product_id' => $prodId
|
'orphan_product_id' => $prodId
|
||||||
];
|
];
|
||||||
|
if ($formule) {
|
||||||
|
$formulaExtras += $optPrice;
|
||||||
|
} else {
|
||||||
$totalHT += $optPrice;
|
$totalHT += $optPrice;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($formule) {
|
||||||
|
$formulaBasePrice = 0;
|
||||||
|
if ($duration <= 1) {
|
||||||
|
$formulaBasePrice = $formule->getPrice1j();
|
||||||
|
} elseif ($duration <= 2) {
|
||||||
|
$formulaBasePrice = $formule->getPrice2j();
|
||||||
|
} else {
|
||||||
|
$formulaBasePrice = $formule->getPrice5j();
|
||||||
|
}
|
||||||
|
|
||||||
|
$totalHT = $formulaBasePrice + $formulaExtras;
|
||||||
|
}
|
||||||
|
|
||||||
$discountAmount = 0;
|
$discountAmount = 0;
|
||||||
if ($promotion) {
|
if ($promotion) {
|
||||||
@@ -1308,7 +1470,8 @@ class ReserverController extends AbstractController
|
|||||||
'totalTva' => $totalTva,
|
'totalTva' => $totalTva,
|
||||||
'totalTTC' => $totalTTC,
|
'totalTTC' => $totalTTC,
|
||||||
'discount' => $discountAmount,
|
'discount' => $discountAmount,
|
||||||
'promotion' => $promotion ? $promotion->getName() : null
|
'promotion' => $promotion ? $promotion->getName() : null,
|
||||||
|
'formule' => $formule ? $formule->getName() : null
|
||||||
],
|
],
|
||||||
'tvaEnabled' => $tvaEnabled
|
'tvaEnabled' => $tvaEnabled
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -76,6 +76,26 @@ class Webhooks extends AbstractController
|
|||||||
if ($pl->getType() === 'accompte' && $contrat->isSigned()) {
|
if ($pl->getType() === 'accompte' && $contrat->isSigned()) {
|
||||||
$contrat->setReservationState('ready');
|
$contrat->setReservationState('ready');
|
||||||
$entityManager->persist($contrat);
|
$entityManager->persist($contrat);
|
||||||
|
|
||||||
|
// Send email to Prestataire
|
||||||
|
$prestataire = $contrat->getPrestataire();
|
||||||
|
if ($prestataire) {
|
||||||
|
$dateStart = $contrat->getDateAt()->format('d/m/Y');
|
||||||
|
$dateEnd = $contrat->getEndAt()->format('d/m/Y');
|
||||||
|
|
||||||
|
$mailer->send(
|
||||||
|
$prestataire->getEmail(),
|
||||||
|
$prestataire->getSurname() . ' ' . $prestataire->getName(),
|
||||||
|
"Nouveau contrat attribué - " . $contrat->getNumReservation(),
|
||||||
|
"mails/prestataire/new_contrat.twig",
|
||||||
|
[
|
||||||
|
'contrat' => $contrat,
|
||||||
|
'prestataire' => $prestataire,
|
||||||
|
'dateStart' => $dateStart,
|
||||||
|
'dateEnd' => $dateEnd
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$entityManager->flush();
|
$entityManager->flush();
|
||||||
|
|||||||
@@ -134,9 +134,11 @@ class Formules
|
|||||||
/**
|
/**
|
||||||
* @param \DateTimeImmutable|null $updatedAt
|
* @param \DateTimeImmutable|null $updatedAt
|
||||||
*/
|
*/
|
||||||
public function setUpdatedAt(?\DateTimeImmutable $updatedAt): void
|
public function setUpdatedAt(?\DateTimeImmutable $updatedAt): static
|
||||||
{
|
{
|
||||||
$this->updatedAt = $updatedAt;
|
$this->updatedAt = $updatedAt;
|
||||||
|
|
||||||
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -93,6 +93,9 @@ class OrderSession
|
|||||||
#[ORM\Column(type: Types::JSON, nullable: true)]
|
#[ORM\Column(type: Types::JSON, nullable: true)]
|
||||||
private ?array $promotion = null;
|
private ?array $promotion = null;
|
||||||
|
|
||||||
|
#[ORM\ManyToOne(targetEntity: Formules::class)]
|
||||||
|
private ?Formules $formule = null;
|
||||||
|
|
||||||
#[ORM\OneToOne(mappedBy: 'orderSession', cascade: ['persist', 'remove'])]
|
#[ORM\OneToOne(mappedBy: 'orderSession', cascade: ['persist', 'remove'])]
|
||||||
private ?Devis $devis = null;
|
private ?Devis $devis = null;
|
||||||
|
|
||||||
@@ -469,4 +472,16 @@ class OrderSession
|
|||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getFormule(): ?Formules
|
||||||
|
{
|
||||||
|
return $this->formule;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setFormule(?Formules $formule): static
|
||||||
|
{
|
||||||
|
$this->formule = $formule;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -173,10 +173,10 @@ class DevisPdfService extends Fpdf
|
|||||||
$this->SetXY(80, $currentY);
|
$this->SetXY(80, $currentY);
|
||||||
$this->SetFont('Arial', '', 8);
|
$this->SetFont('Arial', '', 8);
|
||||||
$this->Cell(15, 10, $nbDays, 'LRB', 0, 'C');
|
$this->Cell(15, 10, $nbDays, 'LRB', 0, 'C');
|
||||||
$this->Cell(25, 10, number_format($price1Day, 2, ',', ' ') . $this->euro(), 'RB', 0, 'R');
|
$this->Cell(25, 10, ($price1Day == 0 ? "Inclus" : number_format($price1Day, 2, ',', ' ') . $this->euro()), 'RB', 0, 'R');
|
||||||
$this->Cell(25, 10, number_format($priceSupHT, 2, ',', ' ') . $this->euro(), 'RB', 0, 'R');
|
$this->Cell(25, 10, ($price1Day == 0 ? "-" : number_format($priceSupHT, 2, ',', ' ') . $this->euro()), 'RB', 0, 'R');
|
||||||
$this->Cell(15, 10, $tvaLabel, 'RB', 0, 'C');
|
$this->Cell(15, 10, $tvaLabel, 'RB', 0, 'C');
|
||||||
$this->Cell(40, 10, number_format($lineTotalHT, 2, ',', ' ') . $this->euro(), 'RB', 1, 'R');
|
$this->Cell(40, 10, ($lineTotalHT == 0 && $price1Day == 0 ? "Inclus" : number_format($lineTotalHT, 2, ',', ' ') . $this->euro()), 'RB', 1, 'R');
|
||||||
|
|
||||||
$this->Line(10, $currentY, 10, $currentY + 10);
|
$this->Line(10, $currentY, 10, $currentY + 10);
|
||||||
$this->Line(10, $currentY + 10, 80, $currentY + 10);
|
$this->Line(10, $currentY + 10, 80, $currentY + 10);
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ class StripeExtension extends AbstractExtension
|
|||||||
$p = $this->em->getRepository(Product::class)->findOneBy(['name' => $name]);
|
$p = $this->em->getRepository(Product::class)->findOneBy(['name' => $name]);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
'id' => $p->getId(),
|
||||||
'name' => $p->getName(),
|
'name' => $p->getName(),
|
||||||
'image' => $this->uploaderHelper->asset($p,'imageFile'),
|
'image' => $this->uploaderHelper->asset($p,'imageFile'),
|
||||||
];
|
];
|
||||||
@@ -294,32 +295,79 @@ class StripeExtension extends AbstractExtension
|
|||||||
}
|
}
|
||||||
|
|
||||||
$totalHT = 0;
|
$totalHT = 0;
|
||||||
|
$formulaExtras = 0;
|
||||||
$productRepo = $this->em->getRepository(Product::class);
|
$productRepo = $this->em->getRepository(Product::class);
|
||||||
$optionsRepo = $this->em->getRepository(Options::class);
|
$optionsRepo = $this->em->getRepository(Options::class);
|
||||||
|
|
||||||
|
$formule = $session->getFormule();
|
||||||
|
$config = [];
|
||||||
|
if ($formule && ($restrict = $formule->getFormulesRestriction())) {
|
||||||
|
$config = $restrict->getRestrictionConfig() ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
// Products
|
// Products
|
||||||
foreach ($ids as $id) {
|
foreach ($ids as $id) {
|
||||||
$product = $productRepo->find($id);
|
$product = $productRepo->find($id);
|
||||||
if ($product) {
|
if ($product) {
|
||||||
$price = $product->getPriceDay() + ($product->getPriceSup() * max(0, $duration - 1));
|
$price = $product->getPriceDay() + ($product->getPriceSup() * max(0, $duration - 1));
|
||||||
|
|
||||||
|
$isInFormule = false;
|
||||||
|
if ($formule) {
|
||||||
|
foreach ($config as $c) {
|
||||||
|
if (($c['product'] ?? '') === $product->getName()) {
|
||||||
|
$isInFormule = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($formule) {
|
||||||
|
if (!$isInFormule) {
|
||||||
|
$formulaExtras += $price;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
$totalHT += $price;
|
$totalHT += $price;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Options
|
// Options
|
||||||
foreach ($selectedOptionsMap as $prodId => $optIds) {
|
foreach ($selectedOptionsMap as $prodId => $optIds) {
|
||||||
foreach ($optIds as $optId) {
|
foreach ($optIds as $optId) {
|
||||||
$option = $optionsRepo->find($optId);
|
$option = $optionsRepo->find($optId);
|
||||||
if ($option) {
|
if ($option) {
|
||||||
|
if ($formule) {
|
||||||
|
$formulaExtras += $option->getPriceHt();
|
||||||
|
} else {
|
||||||
$totalHT += $option->getPriceHt();
|
$totalHT += $option->getPriceHt();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($formule) {
|
||||||
|
$formulaBasePrice = 0;
|
||||||
|
if ($duration <= 1) $formulaBasePrice = $formule->getPrice1j();
|
||||||
|
elseif ($duration <= 2) $formulaBasePrice = $formule->getPrice2j();
|
||||||
|
else $formulaBasePrice = $formule->getPrice5j();
|
||||||
|
|
||||||
|
$totalHT = $formulaBasePrice + $formulaExtras;
|
||||||
|
}
|
||||||
|
|
||||||
|
$originalHT = $totalHT;
|
||||||
|
$discountAmount = 0;
|
||||||
|
|
||||||
|
if ($promo = $session->getPromotion()) {
|
||||||
|
$discountAmount = $totalHT * ($promo['percentage'] / 100);
|
||||||
|
$totalHT -= $discountAmount;
|
||||||
|
}
|
||||||
|
|
||||||
$tvaEnabled = isset($_ENV['TVA_ENABLED']) && $_ENV['TVA_ENABLED'] === "true";
|
$tvaEnabled = isset($_ENV['TVA_ENABLED']) && $_ENV['TVA_ENABLED'] === "true";
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'ht' => $totalHT,
|
'ht' => $totalHT,
|
||||||
|
'originalHT' => $originalHT,
|
||||||
|
'discount' => $discountAmount,
|
||||||
'duration' => $duration,
|
'duration' => $duration,
|
||||||
'tvaEnabled' => $tvaEnabled
|
'tvaEnabled' => $tvaEnabled
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
crossorigin=""/>
|
crossorigin=""/>
|
||||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
|
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
|
||||||
integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo="
|
integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo="
|
||||||
crossorigin=""></script>
|
crossorigin="" nonce="{{ csp_nonce('script') }}"></script>
|
||||||
|
|
||||||
<div class="space-y-8 pb-20">
|
<div class="space-y-8 pb-20">
|
||||||
|
|
||||||
@@ -219,6 +219,18 @@
|
|||||||
Contenu de la demande
|
Contenu de la demande
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
|
{% if session.formule %}
|
||||||
|
<div class="mb-6 p-4 bg-blue-500/10 border border-blue-500/20 rounded-xl flex items-center gap-3">
|
||||||
|
<div class="p-2 bg-blue-500/20 rounded-lg text-blue-400">
|
||||||
|
<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="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"/></svg>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="text-[10px] font-bold text-blue-400 uppercase tracking-widest">Formule appliquée</p>
|
||||||
|
<p class="text-white font-bold">{{ session.formule.name }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% if session.products['start'] is defined and session.products['end'] is defined %}
|
{% 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-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">
|
<div class="flex items-center gap-2">
|
||||||
@@ -329,6 +341,16 @@
|
|||||||
{% set totalData = totalSession(session) %}
|
{% set totalData = totalSession(session) %}
|
||||||
<div class="mt-8 pt-6 border-t border-slate-700">
|
<div class="mt-8 pt-6 border-t border-slate-700">
|
||||||
<div class="flex flex-col gap-2 items-end">
|
<div class="flex flex-col gap-2 items-end">
|
||||||
|
{% if session.promotion %}
|
||||||
|
<div class="flex items-center gap-2 text-sm text-amber-400 font-bold uppercase mb-1">
|
||||||
|
<span>Promotion : {{ session.promotion.name }}</span>
|
||||||
|
<span class="bg-amber-500/10 px-2 py-0.5 rounded text-amber-500">-{{ session.promotion.percentage }}%</span>
|
||||||
|
</div>
|
||||||
|
<div class="text-xs text-slate-500 font-bold line-through">
|
||||||
|
{{ totalData.originalHT|number_format(2, ',', ' ') }} € HT
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<div class="text-sm text-slate-400">
|
<div class="text-sm text-slate-400">
|
||||||
Durée : <span class="text-white font-bold">{{ totalData.duration }} jour{{ totalData.duration > 1 ? 's' : '' }}</span>
|
Durée : <span class="text-white font-bold">{{ totalData.duration }} jour{{ totalData.duration > 1 ? 's' : '' }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -63,7 +63,7 @@
|
|||||||
<li class="form-repeater__row group animate-in slide-in-from-right-5 duration-300">
|
<li class="form-repeater__row group animate-in slide-in-from-right-5 duration-300">
|
||||||
<fieldset class="backdrop-blur-md bg-white/5 border border-white/10 rounded-[2.5rem] p-6 hover:border-blue-500/30 transition-all shadow-xl">
|
<fieldset class="backdrop-blur-md bg-white/5 border border-white/10 rounded-[2.5rem] p-6 hover:border-blue-500/30 transition-all shadow-xl">
|
||||||
<div class="grid grid-cols-1 lg:grid-cols-12 gap-5 items-end">
|
<div class="grid grid-cols-1 lg:grid-cols-12 gap-5 items-end">
|
||||||
|
<input type="hidden" name="rest[{{ key }}][id]" value="{{ line.id }}">
|
||||||
{# 1. PRODUIT #}
|
{# 1. PRODUIT #}
|
||||||
<div class="lg:col-span-7">
|
<div class="lg:col-span-7">
|
||||||
<label class="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1 mb-2 block">Produit / Prestation</label>
|
<label class="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1 mb-2 block">Produit / Prestation</label>
|
||||||
|
|||||||
@@ -24,8 +24,8 @@
|
|||||||
<div class="w-2 h-2 rounded-full bg-emerald-400 shadow-[0_0_8px_rgba(52,211,153,0.8)] animate-pulse"></div>
|
<div class="w-2 h-2 rounded-full bg-emerald-400 shadow-[0_0_8px_rgba(52,211,153,0.8)] animate-pulse"></div>
|
||||||
<span class="text-[10px] font-black text-emerald-400 uppercase tracking-widest">En ligne</span>
|
<span class="text-[10px] font-black text-emerald-400 uppercase tracking-widest">En ligne</span>
|
||||||
</div>
|
</div>
|
||||||
<a data-turbo="false" href="{{ path('app_crm_formules_view', {id: formule.id, act: 'togglePublish', status: 'false'}) }}"
|
<a href="{{ path('app_crm_formules_view', {id: formule.id, act: 'togglePublish', status: 'false'}) }}"
|
||||||
onclick="return confirm('Voulez-vous vraiment masquer cette formule ?')"
|
data-turbo-confirm="Voulez-vous vraiment masquer cette formule ?"
|
||||||
class="px-6 py-3 bg-white/5 hover:bg-rose-600 text-slate-400 hover:text-white text-[10px] font-black uppercase tracking-widest transition-all active:scale-95 border-l border-white/10">
|
class="px-6 py-3 bg-white/5 hover:bg-rose-600 text-slate-400 hover:text-white text-[10px] font-black uppercase tracking-widest transition-all active:scale-95 border-l border-white/10">
|
||||||
Désactiver
|
Désactiver
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
28
templates/mails/prestataire/new_contrat.twig
Normal file
28
templates/mails/prestataire/new_contrat.twig
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
{% extends 'mails/base.twig' %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<mj-section background-color="#ffffff" padding-bottom="0px">
|
||||||
|
<mj-column width="100%">
|
||||||
|
<mj-text font-size="20px" font-weight="900" color="#0f172a" text-transform="uppercase" font-style="italic" align="center">
|
||||||
|
Nouveau Contrat Attribué
|
||||||
|
</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 font-size="16px" color="#1e293b" padding-bottom="15px">
|
||||||
|
Bonjour {{ datas.prestataire.surname }} {{ datas.prestataire.name }},
|
||||||
|
</mj-text>
|
||||||
|
|
||||||
|
<mj-text font-size="16px" color="#0f172a" line-height="24px">
|
||||||
|
Un nouveaux contrat à vous été attribuer pour les date du <b>{{ datas.dateStart }}</b> au <b>{{ datas.dateEnd }}</b>.
|
||||||
|
</mj-text>
|
||||||
|
|
||||||
|
<mj-text padding-top="20px" font-size="12px" color="#64748b">
|
||||||
|
Référence : #{{ datas.contrat.numReservation }}
|
||||||
|
</mj-text>
|
||||||
|
</mj-column>
|
||||||
|
</mj-section>
|
||||||
|
{% endblock %}
|
||||||
@@ -82,7 +82,12 @@
|
|||||||
<div class="lg:col-span-3 space-y-6">
|
<div class="lg:col-span-3 space-y-6">
|
||||||
<div class="bg-white rounded-[2.5rem] border border-slate-100 shadow-xl shadow-slate-200/40 overflow-hidden">
|
<div class="bg-white rounded-[2.5rem] border border-slate-100 shadow-xl shadow-slate-200/40 overflow-hidden">
|
||||||
<div class="p-8 border-b border-slate-50 flex justify-between items-center bg-slate-50/30">
|
<div class="p-8 border-b border-slate-50 flex justify-between items-center bg-slate-50/30">
|
||||||
|
<div>
|
||||||
<h2 class="text-xs font-black uppercase tracking-widest text-slate-900">Détail des prestations & Options</h2>
|
<h2 class="text-xs font-black uppercase tracking-widest text-slate-900">Détail des prestations & Options</h2>
|
||||||
|
{% if contrat.devis and contrat.devis.orderSession and contrat.devis.orderSession.formule %}
|
||||||
|
<p class="text-[10px] font-bold text-blue-600 uppercase tracking-widest mt-1">Formule : {{ contrat.devis.orderSession.formule.name }}</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
<span class="bg-blue-600 text-white text-[10px] font-black px-4 py-1.5 rounded-full uppercase">{{ days }} Jours</span>
|
<span class="bg-blue-600 text-white text-[10px] font-black px-4 py-1.5 rounded-full uppercase">{{ days }} Jours</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="overflow-x-auto">
|
<div class="overflow-x-auto">
|
||||||
|
|||||||
@@ -70,6 +70,18 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{% if cart.formule %}
|
||||||
|
<div class="mb-6 bg-blue-50 border border-blue-100 rounded-xl p-4 flex items-center gap-3">
|
||||||
|
<div class="bg-white p-2 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="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"/></svg>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="text-[10px] font-black text-blue-400 uppercase tracking-widest">Formule appliquée</p>
|
||||||
|
<p class="text-sm font-bold text-slate-900">{{ cart.formule }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
{% for item in cart.items %}
|
{% for item in cart.items %}
|
||||||
<div class="flex items-center bg-white p-4 rounded-xl border border-slate-200 shadow-sm">
|
<div class="flex items-center bg-white p-4 rounded-xl border border-slate-200 shadow-sm">
|
||||||
@@ -83,6 +95,9 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
|
{% if item.in_formule is defined and item.in_formule %}
|
||||||
|
<span class="inline-block px-2 py-0.5 rounded text-[8px] font-black uppercase tracking-widest bg-blue-100 text-blue-600 mb-1">Inclus formule</span>
|
||||||
|
{% endif %}
|
||||||
<h3 class="font-bold text-slate-800">{{ item.product.name }}</h4>
|
<h3 class="font-bold text-slate-800">{{ item.product.name }}</h4>
|
||||||
|
|
||||||
<div class="text-xs text-slate-600 bg-slate-50 p-2 rounded-lg border border-slate-100 inline-block mt-2">
|
<div class="text-xs text-slate-600 bg-slate-50 p-2 rounded-lg border border-slate-100 inline-block mt-2">
|
||||||
|
|||||||
@@ -32,6 +32,18 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{% if cart.formule %}
|
||||||
|
<div class="mb-6 bg-blue-50 border border-blue-100 rounded-xl p-4 flex items-center gap-3">
|
||||||
|
<div class="bg-white p-2 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="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"/></svg>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="text-[10px] font-black text-blue-400 uppercase tracking-widest">Formule appliquée</p>
|
||||||
|
<p class="text-sm font-bold text-slate-900">{{ cart.formule }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
{% for item in cart.items %}
|
{% for item in cart.items %}
|
||||||
<div class="flex items-center bg-white p-4 rounded-xl border border-slate-200 shadow-sm">
|
<div class="flex items-center bg-white p-4 rounded-xl border border-slate-200 shadow-sm">
|
||||||
@@ -45,6 +57,9 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
|
{% if item.in_formule is defined and item.in_formule %}
|
||||||
|
<span class="inline-block px-2 py-0.5 rounded text-[8px] font-black uppercase tracking-widest bg-blue-100 text-blue-600 mb-1">Inclus formule</span>
|
||||||
|
{% endif %}
|
||||||
<h4 class="font-bold text-slate-800">{{ item.product.name }}</h4>
|
<h4 class="font-bold text-slate-800">{{ item.product.name }}</h4>
|
||||||
<div class="text-xs text-slate-500 mb-2 prose prose-sm max-w-none">
|
<div class="text-xs text-slate-500 mb-2 prose prose-sm max-w-none">
|
||||||
{{ item.product.description|raw }}
|
{{ item.product.description|raw }}
|
||||||
@@ -361,7 +376,7 @@
|
|||||||
Télécharger le devis
|
Télécharger le devis
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<form data-turbo="false" method="post" action="{{ path('reservation_flow_confirmed', {sessionId: session.uuid}) }}" onsubmit="localStorage.clear();" class="w-full md:w-auto">
|
<form is="submit-clear-storage" data-turbo="false" method="post" action="{{ path('reservation_flow_confirmed', {sessionId: session.uuid}) }}" 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">
|
<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
|
Je confirme la commande
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -92,6 +92,11 @@
|
|||||||
<div class="max-w-7xl mx-auto px-4 py-20">
|
<div class="max-w-7xl mx-auto px-4 py-20">
|
||||||
|
|
||||||
{% if formule.type == "free" %}
|
{% if formule.type == "free" %}
|
||||||
|
<flow-formule-configurator
|
||||||
|
data-limits='{"structure": {{ formule.formulesRestriction.nbStructureMax|default(0) }}, "alimentaire": {{ formule.formulesRestriction.nbAlimentaireMax|default(0) }}, "barnum": {{ formule.formulesRestriction.nbBarhumsMax|default(0) }}}'
|
||||||
|
data-prices='{"p1": {{ formule.price1j|default(0) }}, "p2": {{ formule.price2j|default(0) }}, "p5": {{ formule.price5j|default(0) }}}'
|
||||||
|
data-formule-id="{{ formule.id }}">
|
||||||
|
|
||||||
{# --- DESIGN BENTO POUR FORMULE FREE --- #}
|
{# --- DESIGN BENTO POUR FORMULE FREE --- #}
|
||||||
<div class="flex items-center space-x-4 mb-12">
|
<div class="flex items-center space-x-4 mb-12">
|
||||||
<h2 class="text-4xl font-black text-slate-900 uppercase italic tracking-tighter">Composez <span class="text-[#f39e36]">votre pack</span></h2>
|
<h2 class="text-4xl font-black text-slate-900 uppercase italic tracking-tighter">Composez <span class="text-[#f39e36]">votre pack</span></h2>
|
||||||
@@ -102,21 +107,27 @@
|
|||||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-16">
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-16">
|
||||||
<div class="bg-blue-50 border-4 border-slate-900 rounded-[2.5rem] p-8 flex flex-col justify-between h-30 group hover:-translate-y-1 transition-transform">
|
<div class="bg-blue-50 border-4 border-slate-900 rounded-[2.5rem] p-8 flex flex-col justify-between h-30 group hover:-translate-y-1 transition-transform">
|
||||||
<div>
|
<div>
|
||||||
<p class="text-4xl font-black text-slate-900 leading-none">{{ formule.formulesRestriction.nbStructureMax|default(0) }}</p>
|
<p class="text-4xl font-black text-slate-900 leading-none">
|
||||||
|
<span id="count-structure" class="text-[#f39e36]">0</span><span class="text-2xl text-slate-400">/{{ formule.formulesRestriction.nbStructureMax|default(0) }}</span>
|
||||||
|
</p>
|
||||||
<p class="text-xs font-black text-slate-400 uppercase italic mt-2 tracking-widest">Structures au choix</p>
|
<p class="text-xs font-black text-slate-400 uppercase italic mt-2 tracking-widest">Structures au choix</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="bg-orange-50 border-4 border-slate-900 rounded-[2.5rem] p-8 flex flex-col justify-between h-30 group hover:-translate-y-1 transition-transform">
|
<div class="bg-orange-50 border-4 border-slate-900 rounded-[2.5rem] p-8 flex flex-col justify-between h-30 group hover:-translate-y-1 transition-transform">
|
||||||
<div>
|
<div>
|
||||||
<p class="text-4xl font-black text-slate-900 leading-none">{{ formule.formulesRestriction.nbAlimentaireMax|default(0) }}</p>
|
<p class="text-4xl font-black text-slate-900 leading-none">
|
||||||
|
<span id="count-alimentaire" class="text-[#f39e36]">0</span><span class="text-2xl text-slate-400">/{{ formule.formulesRestriction.nbAlimentaireMax|default(0) }}</span>
|
||||||
|
</p>
|
||||||
<p class="text-xs font-black text-slate-400 uppercase italic mt-2 tracking-widest">Animations Alimentaires</p>
|
<p class="text-xs font-black text-slate-400 uppercase italic mt-2 tracking-widest">Animations Alimentaires</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="bg-purple-50 border-4 border-slate-900 rounded-[2.5rem] p-8 flex flex-col justify-between h-30 group hover:-translate-y-1 transition-transform">
|
<div class="bg-purple-50 border-4 border-slate-900 rounded-[2.5rem] p-8 flex flex-col justify-between h-30 group hover:-translate-y-1 transition-transform">
|
||||||
<div>
|
<div>
|
||||||
<p class="text-4xl font-black text-slate-900 leading-none">{{ formule.formulesRestriction.nbBarhumsMax|default(0) }}</p>
|
<p class="text-4xl font-black text-slate-900 leading-none">
|
||||||
|
<span id="count-barnum" class="text-[#f39e36]">0</span><span class="text-2xl text-slate-400">/{{ formule.formulesRestriction.nbBarhumsMax|default(0) }}</span>
|
||||||
|
</p>
|
||||||
<p class="text-xs font-black text-slate-400 uppercase italic mt-2 tracking-widest">Barnums & Mobilier</p>
|
<p class="text-xs font-black text-slate-400 uppercase italic mt-2 tracking-widest">Barnums & Mobilier</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -124,10 +135,25 @@
|
|||||||
|
|
||||||
{# Catalogue des produits éligibles #}
|
{# Catalogue des produits éligibles #}
|
||||||
<h3 class="text-[10px] font-black text-slate-400 uppercase tracking-[0.4em] mb-8 text-center italic">— Catalogue éligible à cette formule —</h3>
|
<h3 class="text-[10px] font-black text-slate-400 uppercase tracking-[0.4em] mb-8 text-center italic">— Catalogue éligible à cette formule —</h3>
|
||||||
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-5 gap-6">
|
|
||||||
|
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-5 gap-6" id="pack-grid">
|
||||||
{% for item in formule.formulesRestriction.restrictionConfig %}
|
{% for item in formule.formulesRestriction.restrictionConfig %}
|
||||||
{% set product = loadProductByName(item.product) %}
|
{% set product = loadProductByName(item.product) %}
|
||||||
<div class="group border-2 border-slate-900 rounded-[2rem] p-2 bg-white hover:bg-slate-50 transition-all shadow-[8px_8px_0px_0px_rgba(15,23,42,1)] hover:shadow-none hover:translate-x-1 hover:translate-y-1">
|
{% set catKey = item.type %}
|
||||||
|
{% if item.type != 'structure' and item.type != 'alimentaire' %}
|
||||||
|
{% set catKey = 'barnum' %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="product-card group cursor-pointer border-2 border-slate-900 rounded-[2rem] p-2 bg-white hover:bg-slate-50 transition-all shadow-[8px_8px_0px_0px_rgba(15,23,42,1)] hover:shadow-none hover:translate-x-1 hover:translate-y-1 relative"
|
||||||
|
data-category="{{ catKey }}"
|
||||||
|
data-id="{{ product.id }}"
|
||||||
|
data-selected="false">
|
||||||
|
|
||||||
|
{# Checkbox Indicator #}
|
||||||
|
<div class="absolute top-4 right-4 z-10 w-6 h-6 bg-white border-2 border-slate-900 rounded-full flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity selection-indicator">
|
||||||
|
<div class="w-3 h-3 bg-[#f39e36] rounded-full hidden"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="aspect-square rounded-[1.5rem] overflow-hidden bg-slate-100 mb-3 border border-slate-900/10">
|
<div class="aspect-square rounded-[1.5rem] overflow-hidden bg-slate-100 mb-3 border border-slate-900/10">
|
||||||
{% if product.image %}
|
{% if product.image %}
|
||||||
<img src="{{ product.image|imagine_filter('webp') }}" alt="{{ product.name }}" class="w-full h-full object-cover group-hover:scale-110 transition-transform duration-500">
|
<img src="{{ product.image|imagine_filter('webp') }}" alt="{{ product.name }}" class="w-full h-full object-cover group-hover:scale-110 transition-transform duration-500">
|
||||||
@@ -144,6 +170,14 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{# Action Bar #}
|
||||||
|
<div class="mt-12 text-center sticky bottom-8 z-50">
|
||||||
|
<a id="btn-validate-pack" href="#" class="inline-block px-12 py-4 bg-slate-900 text-white rounded-full font-black uppercase italic tracking-widest hover:bg-[#fc0e50] transition-all shadow-xl opacity-50 pointer-events-none transform translate-y-4">
|
||||||
|
Valider mon pack (0)
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</flow-formule-configurator>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if formule.type == "pack" %}
|
{% if formule.type == "pack" %}
|
||||||
|
|||||||
Reference in New Issue
Block a user