Add security remove paste text in editor

Fix design for mobile formule product
This commit is contained in:
Serreau Jovann
2026-01-30 09:13:01 +01:00
parent e644dc4b85
commit 4873c24bb2
12 changed files with 343 additions and 145 deletions

View File

@@ -3,7 +3,7 @@ export class CrmEditor extends HTMLTextAreaElement {
this.style.display = 'none';
this.editorContainer = document.createElement('div');
this.editorContainer.className = "w-full rounded-2xl overflow-hidden ring-1 ring-white/20 bg-slate-950/40 backdrop-blur-2xl shadow-2xl transition-all duration-300 focus-within:ring-blue-500/50 my-4";
this.editorContainer.className = "w-full rounded-2xl overflow-hidden ring-1 ring-white/20 bg-slate-950/40 backdrop-blur-2xl shadow-2xl transition-all duration-300 focus-within:ring-blue-500/50 my-4 relative";
this.editorContainer.innerHTML = `
<div class="flex items-center gap-1.5 p-3 border-b border-white/10 bg-black/40">
@@ -37,6 +37,11 @@ export class CrmEditor extends HTMLTextAreaElement {
</button>
</div>
<!-- Toast de sécurité -->
<div id="paste-error" class="absolute top-16 left-1/2 -translate-x-1/2 bg-red-500 text-white px-4 py-2 rounded-full text-xs font-bold shadow-xl opacity-0 transition-all duration-300 pointer-events-none z-50">
Copie impossible
</div>
<div id="wysiwyg-area"
contenteditable="true"
class="p-6 min-h-[300px] max-h-[600px] overflow-y-auto focus:outline-none bg-white text-slate-900 prose prose-slate max-w-none shadow-inner break-words w-full">
@@ -55,12 +60,19 @@ export class CrmEditor extends HTMLTextAreaElement {
this.wysiwyg = this.editorContainer.querySelector('#wysiwyg-area');
this.colorInput = this.editorContainer.querySelector('#text-color-input');
this.colorBar = this.editorContainer.querySelector('#color-bar');
this.pasteError = this.editorContainer.querySelector('#paste-error');
this.initEvents();
this.updateStats();
}
initEvents() {
// Sécurité anti-collage
this.wysiwyg.addEventListener('paste', (e) => {
e.preventDefault();
this.showPasteWarning();
});
// Boutons standards
const buttons = this.editorContainer.querySelectorAll('button[data-cmd]');
buttons.forEach(btn => {
@@ -90,6 +102,16 @@ export class CrmEditor extends HTMLTextAreaElement {
});
}
showPasteWarning() {
this.pasteError.classList.remove('opacity-0', 'scale-90');
this.pasteError.classList.add('opacity-100', 'scale-100');
setTimeout(() => {
this.pasteError.classList.add('opacity-0', 'scale-90');
this.pasteError.classList.remove('opacity-100', 'scale-100');
}, 2000);
}
updateStats() {
const text = this.wysiwyg.innerText || "";
const count = text.trim().length;

View File

@@ -201,6 +201,7 @@ const initBackToTop = () => {
const initMobileMenu = () => {
const btn = document.getElementById('menu-button');
const menu = document.getElementById('mobile-menu');
console.log(btn,menu)
if (!btn || !menu) return;
btn.onclick = () => {
const open = menu.classList.toggle('hidden');

View File

@@ -178,26 +178,43 @@ class ContratController extends AbstractController
$totalDays = $dateStart->diff($dateEnd)->days + 1;
$totalHT = 0;
$totalTTC = 0;
$totalCaution = 0;
// Calcul des lignes (Location dégressive)
foreach ($contrat->getContratsLines() as $line) {
if( isset($_ENV['TVA_ENABLED']) && $_ENV['TVA_ENABLED'] === "true") {
$totalTTC += $line->getPrice1DayHt() * 1.20;
}
$priceLine = $line->getPrice1DayHt();
if ($totalDays > 1) {
$priceLine += ($line->getPriceSupDayHt() * ($totalDays - 1));
if( isset($_ENV['TVA_ENABLED']) && $_ENV['TVA_ENABLED'] === "true") {
$totalTTC += ($line->getPriceSupDayHt()*1.20 * ($totalDays - 1));
}
$priceLine += (($line->getPriceSupDayHt()) * ($totalDays - 1));
}
$totalHT += $priceLine;
$totalCaution += $line->getCaution();
}
// Ajout des options (Forfait)
foreach ($contrat->getContratsOptions() as $option) {
if( isset($_ENV['TVA_ENABLED']) && $_ENV['TVA_ENABLED'] === "true") {
$totalTTC += ($option->getPrice()*1.20);
}
$totalHT += $option->getPrice();
}
// --- NOUVEAUX CALCULS ---
$arrhes = $totalHT * 0.25; // 25%
$solde = $totalHT;
if( isset($_ENV['TVA_ENABLED']) && $_ENV['TVA_ENABLED'] === "true") {
$arrhes = $totalTTC * 0.25; // 25%
} else {
$arrhes = $totalHT * 0.25; // 25%
}
if( isset($_ENV['TVA_ENABLED']) && $_ENV['TVA_ENABLED'] === "true") {
$solde = $totalTTC;
} else {
$solde = $totalHT;
}
/** @var ContratsPayments $paymentList */
$paymentList =[];
@@ -406,9 +423,11 @@ class ContratController extends AbstractController
'totalHT' => $totalHT,
'totalCaution' => $totalCaution,
'arrhes' => $arrhes,
'totalTTC' => $totalTTC,
'paymentList' => $paymentList,
'paymentCtaList' => $paymentCtaList,
'paymentCaution' => $paymentCaution,
'tvaEnabled' => isset($_ENV['TVA_ENABLED']) && $_ENV['TVA_ENABLED'] === "true",
'solde' => $solde,
'signUrl' => (!$contrat->isSigned())?$client->getLinkSign($contrat->getSignID()):null,
'signEvents' => ($contrat->getSignID() !="")?$client->eventSign($contrat):[],

View File

@@ -142,6 +142,8 @@ class ReserverController extends AbstractController
return $this->render('revervation/catalogue.twig',[
'products' => $productRepository->findAll(),
'tvaEnabled' => isset($_ENV['TVA_ENABLED']) && $_ENV['TVA_ENABLED'] === "true",
]);
}
#[Route('/formules', name: 'reservation_formules')]
@@ -150,6 +152,8 @@ class ReserverController extends AbstractController
return $this->render('revervation/formules.twig',[
'formules' => $formulesRepository->findBy(['isPublish'=>true],['updatedAt' => 'DESC']),
'tvaEnabled' => isset($_ENV['TVA_ENABLED']) && $_ENV['TVA_ENABLED'] === "true",
]);
}
#[Route('/formules/{slug}', name: 'reservation_formule_show')]
@@ -166,7 +170,9 @@ class ReserverController extends AbstractController
}
return $this->render('revervation/formule/show.twig',[
'formule' => $formule
'formule' => $formule,
'tvaEnabled' => isset($_ENV['TVA_ENABLED']) && $_ENV['TVA_ENABLED'] === "true",
]);
}
#[Route('/comment-reserver', name: 'reservation_workflow')]
@@ -207,6 +213,7 @@ class ReserverController extends AbstractController
return $this->render('revervation/produit.twig', [
'product' => $product,
'tvaEnabled' => isset($_ENV['TVA_ENABLED']) && $_ENV['TVA_ENABLED'] === "true",
'otherProducts' => array_slice($otherProducts, 0, 4)
]);
}

View File

@@ -315,6 +315,11 @@ class ContratPdfService extends Fpdf
private function renderMainContent(): void
{
$tvaEnabled = isset($_ENV['TVA_ENABLED']) && $_ENV['TVA_ENABLED'] === "true";
// Configuration TVA
$tvaRate = $tvaEnabled ? 0.20 : 0;
$tvaLabel = $tvaEnabled ? '20%' : '0%';
// --- 1. BLOCS IDENTITÉ (LOCATAIRE & LIEU) ---
$this->SetY(55);
$this->SetFont('Arial', 'B', 10);
@@ -368,10 +373,11 @@ class ContratPdfService extends Fpdf
$this->SetFont('Arial', 'B', 9);
$this->SetFillColor(37, 99, 235);
$this->SetTextColor(255, 255, 255);
$this->Cell(100, 8, $this->clean("Désignation"), 1, 0, 'L', true);
$this->Cell(30, 8, $this->clean("Tarif HT"), 1, 0, 'C', true);
$this->Cell(30, 8, $this->clean("Durée"), 1, 0, 'C', true);
$this->Cell(30, 8, $this->clean("Sous-Total"), 1, 1, 'C', true);
$this->Cell(85, 8, $this->clean("Désignation"), 1, 0, 'L', true);
$this->Cell(25, 8, $this->clean("Tarif HT"), 1, 0, 'C', true);
$this->Cell(15, 8, $this->clean("Durée"), 1, 0, 'C', true);
$this->Cell(25, 8, $this->clean("TVA"), 1, 0, 'C', true);
$this->Cell(40, 8, $this->clean("Total HT"), 1, 1, 'C', true);
$this->SetTextColor(0, 0, 0);
$this->SetFont('Arial', '', 9);
@@ -382,34 +388,52 @@ class ContratPdfService extends Fpdf
$totalHt += $sousTotal;
$totalCaution += $line->getCaution();
$this->Cell(100, 7, $this->clean($line->getName()), 1, 0, 'L');
$this->Cell(30, 7, number_format($line->getPrice1DayHt(), 2) . $this->euro(), 1, 0, 'C');
$this->Cell(30, 7, $nbJoursTotal . " j.", 1, 0, 'C');
$this->Cell(30, 7, number_format($sousTotal, 2) . $this->euro(), 1, 1, 'R');
$this->Cell(85, 7, $this->clean($line->getName()), 1, 0, 'L');
$this->Cell(25, 7, number_format($line->getPrice1DayHt(), 2, ',', ' ') . $this->euro(), 1, 0, 'R');
$this->Cell(15, 7, $nbJoursTotal . " j.", 1, 0, 'C');
$this->Cell(25, 7, $tvaLabel, 1, 0, 'C');
$this->Cell(40, 7, number_format($sousTotal, 2, ',', ' ') . $this->euro(), 1, 1, 'R');
}
foreach ($this->contrats->getContratsOptions() as $opt) {
$totalHt += $opt->getPrice();
$this->SetFillColor(245, 245, 245);
$this->Cell(100, 7, $this->clean("[Option] " . $opt->getName()), 1, 0, 'L', true);
$this->Cell(30, 7, number_format($opt->getPrice(), 2) . $this->euro(), 1, 0, 'C', true);
$this->Cell(30, 7, "Forfait", 1, 0, 'C', true);
$this->Cell(30, 7, number_format($opt->getPrice(), 2) . $this->euro(), 1, 1, 'R', true);
$this->Cell(85, 7, $this->clean("[Option] " . $opt->getName()), 1, 0, 'L', true);
$this->Cell(25, 7, number_format($opt->getPrice(), 2, ',', ' ') . $this->euro(), 1, 0, 'R', true);
$this->Cell(15, 7, "Forfait", 1, 0, 'C', true);
$this->Cell(25, 7, $tvaLabel, 1, 0, 'C', true);
$this->Cell(40, 7, number_format($opt->getPrice(), 2, ',', ' ') . $this->euro(), 1, 1, 'R', true);
}
$this->Ln(5);
// --- 4. RÉCAPITULATIF FINANCIER ---
$arrhes = $totalHt * 0.25;
$amountTva = $totalHt * $tvaRate;
$totalTtc = $totalHt + $amountTva;
$arrhes = $totalTtc * 0.25; // Arrhes calculées sur le TTC
$this->Ln(5);
$this->SetX(110);
$this->SetFont('Arial', 'B', 10);
$this->Cell(60, 9, $this->clean("TOTAL GÉNÉRAL HT"), 1, 0, 'L');
$this->Cell(30, 9, number_format($totalHt, 2) . $this->euro(), 1, 1, 'R');
$this->SetFont('Arial', 'B', 9);
$this->Cell(50, 7, $this->clean("TOTAL HT"), 'B', 0, 'L');
$this->Cell(40, 7, number_format($totalHt, 2, ',', ' ') . $this->euro(), 'B', 1, 'R');
if($tvaEnabled) {
$this->SetX(110);
$this->SetFont('Arial', '', 9);
$this->Cell(50, 7, $this->clean("TVA (" . $tvaLabel . ")"), 'B', 0, 'L');
$this->Cell(40, 7, number_format($amountTva, 2, ',', ' ') . $this->euro(), 'B', 1, 'R');
}
$this->SetX(110);
$this->SetTextColor(37, 99, 235);
$this->Cell(60, 9, $this->clean("ARRHES À VERSER (25%)"), 1, 0, 'L');
$this->Cell(30, 9, number_format($arrhes, 2) . $this->euro(), 1, 1, 'R');
$this->SetFont('Arial', 'B', 11);
$this->SetFillColor(37, 99, 235); $this->SetTextColor(255, 255, 255);
$this->Cell(50, 9, $this->clean(" TOTAL TTC"), 0, 0, 'L', true);
$this->Cell(40, 9, number_format($totalTtc, 2, ',', ' ') . $this->euro() . " ", 0, 1, 'R', true);
$this->Ln(2);
$this->SetX(110);
$this->SetTextColor(37, 99, 235); $this->SetFont('Arial', 'B', 10);
$this->Cell(50, 9, $this->clean("ARRHES À VERSER (25%)"), 'B', 0, 'L');
$this->Cell(40, 9, number_format($arrhes, 2, ',', ' ') . $this->euro(), 'B', 1, 'R');
$this->SetX(110);
$this->SetFont('Arial', 'I', 7); $this->SetTextColor(100, 100, 100);
@@ -418,8 +442,15 @@ class ContratPdfService extends Fpdf
$this->Ln(2);
$this->SetX(110);
$this->SetFont('Arial', 'B', 10); $this->SetTextColor(220, 38, 38);
$this->Cell(60, 9, $this->clean("CAUTION DE GARANTIE"), 1, 0, 'L');
$this->Cell(30, 9, number_format($totalCaution, 2) . $this->euro(), 1, 1, 'R');
$this->Cell(50, 9, $this->clean("CAUTION DE GARANTIE"), 'B', 0, 'L');
$this->Cell(40, 9, number_format($totalCaution, 2, ',', ' ') . $this->euro(), 'B', 1, 'R');
// Mentions légales
if (!$tvaEnabled) {
$this->Ln(5);
$this->SetFont('Arial', 'I', 8); $this->SetTextColor(0, 0, 0);
$this->Cell(0, 5, $this->clean('TVA non applicable, art. 293 B du CGI'), 0, 1, 'R');
}
$this->SetTextColor(0, 0, 0);
}

View File

@@ -87,6 +87,11 @@ class DevisPdfService extends Fpdf
{
$this->AddPage();
// Vérification de l'activation de la TVA
$tvaEnabled = isset($_ENV['TVA_ENABLED']) && $_ENV['TVA_ENABLED'] === "true";
$tvaRate = $tvaEnabled ? 0.20 : 0;
$tvaLabel = $tvaEnabled ? '20%' : '0%';
// --- BLOC CLIENT ---
$customer = $this->devis->getCustomer();
$this->SetY(55);
@@ -107,11 +112,11 @@ class DevisPdfService extends Fpdf
$this->renderAddressBlock('ADRESSE DE FACTURATION', $this->devis->getBillAddress(), 'L', 10, $yAddress);
$this->renderAddressBlock('ADRESSE DE PRESTATION', $this->devis->getAddressShip(), 'R', 110, $yAddress);
// --- AJOUT : BLOC DATES DE L'ÉVÉNEMENT ---
// --- BLOC DATES DE L'ÉVÉNEMENT ---
$this->SetY($yAddress + 25);
$this->SetDrawColor(230, 230, 230);
$this->SetFillColor(250, 250, 250);
$this->Rect(10, $this->GetY(), 190, 12, 2, 'DF'); // Petit encadré gris clair
$this->Rect(10, $this->GetY(), 190, 12, 2, 'DF');
$this->SetY($this->GetY() + 3.5);
$this->SetFont('Arial', 'B', 9);
@@ -123,7 +128,6 @@ class DevisPdfService extends Fpdf
$dateStart = $this->devis->getStartAt() ? $this->devis->getStartAt()->format('d/m/Y à H:i') : 'N/C';
$dateEnd = $this->devis->getEndAt() ? $this->devis->getEndAt()->format('d/m/Y à H:i') : 'N/C';
$this->Cell(0, 5, $this->clean("Du $dateStart au $dateEnd"), 0, 1, 'L');
$this->SetY($this->GetY() + 8);
@@ -146,81 +150,65 @@ class DevisPdfService extends Fpdf
foreach ($this->devis->getDevisLines() as $line) {
$p = $this->productRepository->findOneBy(['name'=>$line->getProduct()]);
if($p instanceof Product) {
$totalCaution = $totalCaution + $p->getCaution();
$totalCaution += $p->getCaution();
}
$price1Day = $line->getPriceHt();
$priceSupHT = $line->getPriceHtSup() ?? 0;
$nbDays = $line->getDay();
// Calcul : J1 + (Jours restants * Prix HT Sup)
$lineTotalHT = $price1Day + (max(0, $nbDays - 1) * $priceSupHT);
$totalHT += $lineTotalHT;
$productName = $line->getProduct();
$ref= "";
if($p instanceof Product) {
$ref = $totalCaution + $p->getRef();
}
$ref = ($p instanceof Product) ? $p->getRef() : "";
$currentY = $this->GetY();
// --- COLONNE DÉSIGNATION (NOM + REF / DATES) ---
// Ligne Produit
$this->SetXY(10, $currentY+2.5);
$this->SetFont('Arial', 'B', 8);
$this->Cell(70, 5, $this->clean($productName . ' (Ref: ' . $ref . ')'), 0, 0, 'L');
// --- COLONNES NUMÉRIQUES ---
$this->Cell(70, 5, $this->clean($productName . ($ref ? ' (Ref: ' . $ref . ')' : '')), 0, 0, 'L');
$this->SetXY(80, $currentY);
$this->SetFont('Arial', '', 8);
$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, number_format($priceSupHT, 2, ',', ' ') . $this->euro(), 'RB', 0, 'R');
$this->Cell(15, 10, '0%', 'RB', 0, 'C');
$this->Cell(15, 10, $tvaLabel, 'RB', 0, 'C');
$this->Cell(40, 10, number_format($lineTotalHT, 2, ',', ' ') . $this->euro(), 'RB', 1, 'R');
// Fermeture de la bordure pour la désignation
$this->Line(10, $currentY, 10, $currentY + 10);
$this->Line(10, $currentY + 10, 80, $currentY + 10);
if ($this->GetY() > 250) {
$this->AddPage();
}
if ($this->GetY() > 250) { $this->AddPage(); }
}
//options
// --- 2. TABLEAU DES OPTIONS (SÉPARÉ) ---
// --- TABLEAU DES OPTIONS ---
if (count($this->devis->getDevisOptions()) > 0) {
$this->Ln(5); // Espace entre les deux tableaux
// En-tête du tableau des options
$this->Ln(5);
$this->SetFont('Arial', 'B', 8);
$this->SetFillColor(230, 235, 245); // Bleu très léger pour différencier
$this->Cell(150, 8, $this->clean('Options & Services additionnels'), 1, 0, 'L', true);
$this->SetFillColor(230, 235, 245);
$this->Cell(135, 8, $this->clean('Options & Services additionnels'), 1, 0, 'L', true);
$this->Cell(15, 8, $this->clean('TVA'), 1, 0, 'C', true);
$this->Cell(40, 8, $this->clean('Total HT'), 1, 1, 'R', true);
$this->SetFont('Arial', '', 9);
$this->SetTextColor(0, 0, 0);
foreach ($this->devis->getDevisOptions() as $devisOption) {
$option = $devisOption->getOption();
$priceHT = $devisOption->getPriceHt();
$totalHT += $priceHT; // On l'ajoute au total général
$totalHT += $priceHT;
$currentY = $this->GetY();
// Colonne Désignation
$this->SetXY(10, $currentY);
$this->Cell(150, 8, $this->clean($option." - ".$devisOption->getDetails()), 'LRB', 0, 'L');
// Colonne Prix
$this->Cell(135, 8, $this->clean($devisOption->getOption()." - ".$devisOption->getDetails()), 'LRB', 0, 'L');
$this->Cell(15, 8, $tvaLabel, 'RB', 0, 'C');
$this->Cell(40, 8, number_format($priceHT, 2, ',', ' ') . $this->euro(), 'RB', 1, 'R');
if ($this->GetY() > 260) {
$this->AddPage();
}
if ($this->GetY() > 260) { $this->AddPage(); }
}
}
// --- CALCULS FINAUX ---
$amountTVA = $totalHT * $tvaRate;
$totalTTC = $totalHT + $amountTVA;
// --- BLOC TOTAUX ---
$this->Ln(5);
@@ -231,23 +219,21 @@ class DevisPdfService extends Fpdf
$this->Cell(130);
$this->SetFont('Arial', '', 9);
$this->Cell(30, 8, 'TVA (0%)', 0, 0, 'L');
$this->Cell(30, 8, '0,00' . $this->euro(), 0, 1, 'R');
$this->Cell(30, 8, 'TVA ('.$tvaLabel.')', 0, 0, 'L');
$this->Cell(30, 8, number_format($amountTVA, 2, ',', ' ') . $this->euro(), 0, 1, 'R');
$this->Ln(2);
$this->Cell(130);
$this->SetFont('Arial', 'B', 11);
$this->SetFillColor(37, 99, 235); $this->SetTextColor(255, 255, 255);
$this->Cell(30, 10, ' TOTAL TTC', 0, 0, 'L', true);
$this->Cell(30, 10, number_format($totalHT, 2, ',', ' ') . $this->euro() . ' ', 0, 1, 'R', true);
$this->Cell(30, 10, number_format($totalTTC, 2, ',', ' ') . $this->euro() . ' ', 0, 1, 'R', true);
// --- AJOUT : TOTAL DE LA CAUTION ---
// --- CAUTION ---
$this->Ln(2);
$this->SetTextColor(80, 80, 80);
$this->Cell(130);
$this->SetFont('Arial', 'B', 10);
$this->SetTextColor(80, 80, 80); // Gris pour distinguer de la vente
$this->SetDrawColor(200, 200, 200);
// On crée un petit encadré pour la caution
$this->Cell(30, 8, ' TOTAL CAUTION', 'B', 0, 'L');
$this->Cell(30, 8, number_format($totalCaution, 2, ',', ' ') . $this->euro(), 'B', 1, 'R');
@@ -255,11 +241,13 @@ class DevisPdfService extends Fpdf
$this->Cell(130);
$this->Cell(60, 4, $this->clean('(Caution non encaissée, voir CGV Article 6)'), 0, 1, 'R');
// Mention légale auto-entrepreneur
$this->Ln(5);
$this->SetTextColor(80, 80, 80);
$this->SetFont('Arial', 'I', 8);
$this->Cell(0, 5, $this->clean('TVA non applicable, art. 293 B du CGI'), 0, 1, 'R');
// Mention légale auto-entrepreneur (uniquement si TVA désactivée)
if (!$tvaEnabled) {
$this->Ln(5);
$this->SetTextColor(80, 80, 80);
$this->SetFont('Arial', 'I', 8);
$this->Cell(0, 5, $this->clean('TVA non applicable, art. 293 B du CGI'), 0, 1, 'R');
}
$this->addCGV();
$this->addSignaturePage();

View File

@@ -51,26 +51,45 @@ class StripeExtension extends AbstractExtension
public function totalQuoto(Devis $devis): float
{
$totalHT = 0;
$totalCautions = 0;
$tvaEnabled = isset($_ENV['TVA_ENABLED']) && $_ENV['TVA_ENABLED'] === "true";
$tvaRate = 0.20; // 20% de TVA
// 1. Calcul des lignes de produits (Location)
// 1. Calcul du montant HT des lignes et des cautions
foreach ($devis->getDevisLines() as $line) {
$price1Day = $line->getPriceHt() ?? 0;
$priceSupHT = $line->getPriceHtSup() ?? 0;
$nbDays = $line->getDay() ?? 1;
$price1Day = (float) ($line->getPriceHt() ?? 0);
$priceSupHT = (float) ($line->getPriceHtSup() ?? 0);
$nbDays = (int) ($line->getDay() ?? 1);
// Calcul : J1 + (Jours supplémentaires * Prix Sup)
$lineTotalHT = $price1Day + (max(0, $nbDays - 1) * $priceSupHT);
// Calcul HT de la ligne
$lineHT = $price1Day + (max(0, $nbDays - 1) * $priceSupHT);
$totalHT += $lineHT;
$totalHT += $lineTotalHT;
// Récupération de la caution liée au produit
$product = $this->em->getRepository(Product::class)->findOneBy([
'name' => $line->getProduct()
]);
if ($product) {
$totalCautions += (float) ($product->getCaution() ?? 0);
}
}
// 2. Calcul des options additionnelles
/** @var DevisOptions $devisOption */
foreach ($devis->getDevisOptions() as $devisOption) {
$totalHT += $devisOption->getPriceHt() ?? 0;
// 2. Ajout des options au montant HT
foreach ($devis->getDevisOptions() as $option) {
$totalHT += (float) ($option->getPriceHt() ?? 0);
}
return (float) $totalHT;
// 3. Application de la TVA si activée
$montantPrestation = $totalHT;
if ($tvaEnabled) {
$montantPrestation = $totalHT * (1 + $tvaRate);
}
// 4. Total final = Prestation (HT ou TTC) + Cautions
$totalFinal = $montantPrestation + $totalCautions;
return round($totalFinal, 2);
}
public function totalContratAccompte(Contrats $devis): float
{
@@ -81,23 +100,25 @@ class StripeExtension extends AbstractExtension
$totalHT = $totalHT + $line->getCaution();
}
return (float) $totalHT;
}
public function totalQuotoAccompte(Devis $devis): float
{
$totalHT = 0;
$totalCautions = 0;
// 1. Calcul des lignes de produits (Location)
foreach ($devis->getDevisLines() as $line) {
$p = $this->em->getRepository(Product::class)->findOneBy(['name'=>$line->getProduct()]);
$totalHT = $totalHT + $p->getCaution();
// On récupère le produit en base via le nom stocké dans la ligne
$product = $this->em->getRepository(Product::class)->findOneBy([
'name' => $line->getProduct()
]);
if ($product) {
// Additionne la caution du produit (valeur par défaut 0 si nulle)
$totalCautions += (float) ($product->getCaution() ?? 0);
}
}
return (float) $totalHT;
return round($totalCautions, 2);
}
public function totalPayContrat(Contrats $contrats)
@@ -132,15 +153,15 @@ class StripeExtension extends AbstractExtension
// Calcul du nombre de jours (minimum 1 jour)
$nbDays = 1;
if ($dateDebut && $dateFin) {
if ($dateDebut instanceof \DateTimeInterface && $dateFin instanceof \DateTimeInterface) {
$diff = $dateDebut->diff($dateFin);
$nbDays = $diff->days + 1; // +1 pour inclure le jour de départ
}
// 1. Calcul des lignes de produits (Location)
foreach ($devis->getContratsLines() as $line) {
$price1Day = $line->getPrice1DayHt() ?? 0;
$priceSupHT = $line->getPriceSupDayHt() ?? 0;
$price1Day = (float) ($line->getPrice1DayHt() ?? 0);
$priceSupHT = (float) ($line->getPriceSupDayHt() ?? 0);
// Calcul : J1 + (Jours restants * Prix Sup)
$lineTotalHT = $price1Day + (max(0, $nbDays - 1) * $priceSupHT);
@@ -149,7 +170,16 @@ class StripeExtension extends AbstractExtension
// 2. Calcul des options additionnelles
foreach ($devis->getContratsOptions() as $devisOption) {
$totalHT += $devisOption->getPrice() ?? 0;
$totalHT += (float) ($devisOption->getPrice() ?? 0);
}
// 3. Gestion de la TVA (20%)
// Vérification de l'activation via la variable d'environnement
$tvaEnabled = isset($_ENV['TVA_ENABLED']) && $_ENV['TVA_ENABLED'] === "true";
if ($tvaEnabled) {
$tauxTVA = 1.20; // Taux de 20%
return (float) ($totalHT * $tauxTVA);
}
return (float) $totalHT;

View File

@@ -89,13 +89,26 @@
<table class="w-full text-left">
<tbody class="divide-y divide-slate-50">
{% for line in contrat.contratsLines %}
{% set priceLine = line.price1DayHt + (line.priceSupDayHt * (days - 1)) %}
{% if tvaEnabled %}
{% set priceLine = (line.price1DayHt*1.20) + ((line.priceSupDayHt*1.20) * (days - 1)) %}
{% else %}
{% set priceLine = line.price1DayHt + (line.priceSupDayHt * (days - 1)) %}
{% endif %}
<tr class="hover:bg-slate-50/30 transition-colors">
<td class="px-8 py-6">
<p class="font-black text-slate-900 uppercase text-sm leading-tight">{{ line.name }}</p>
{% if tvaEnabled %}
<p class="text-[10px] text-slate-400 font-bold italic mt-1 uppercase">Prix 1 Jour : {{ (line.price1DayHt*1.20)|number_format(0, ',', ' ') }}€ TTC</p>
<p class="text-[10px] text-slate-400 font-bold italic mt-1 uppercase">Prix Jour suplémentaire : {{ (line.priceSupDayHt*1.20)|number_format(0, ',', ' ') }}€ TTC</p>
{% else %}
<p class="text-[10px] text-slate-400 font-bold italic mt-1 uppercase">Prix 1 Jour : {{ line.price1DayHt|number_format(0, ',', ' ') }}€ HT</p>
<p class="text-[10px] text-slate-400 font-bold italic mt-1 uppercase">Prix Jour suplémentaire : {{ line.priceSupDayHt|number_format(0, ',', ' ') }}€ HT</p>
{% endif %}
<p class="text-[10px] text-slate-400 font-bold italic mt-1 uppercase">Caution : {{ line.caution|number_format(0, ',', ' ') }}€</p>
</td>
<td class="px-8 py-6 text-right font-black text-slate-900 italic text-lg">{{ priceLine|number_format(2, ',', ' ') }}€</td>
</tr>
{% endfor %}
{% for line in contrat.contratsOptions %}
@@ -104,7 +117,11 @@
<p class="font-bold text-blue-600 uppercase text-xs italic tracking-widest mb-1">Option</p>
<p class="font-black text-slate-900 uppercase text-sm leading-tight">{{ line.name }}</p>
</td>
<td class="px-8 py-6 text-right font-black text-slate-900 italic text-lg">{{ line.price|number_format(2, ',', ' ') }}€</td>
{% if tvaEnabled %}
<td class="px-8 py-6 text-right font-black text-slate-900 italic text-lg">{{ (line.price*1.20)|number_format(2, ',', ' ') }}€</td>
{% else %}
<td class="px-8 py-6 text-right font-black text-slate-900 italic text-lg">{{ line.price|number_format(2, ',', ' ') }}€</td>
{% endif %}
</tr>
{% endfor %}
</tbody>
@@ -145,7 +162,22 @@
</div>
<p class="text-3xl font-black text-slate-900 italic tracking-tighter">{{ totalHT|number_format(2, ',', ' ') }}€</p>
</div>
{% if tvaEnabled %}
<div class="bg-white rounded-[1.5rem] p-6 border border-slate-100 shadow-sm flex justify-between items-center">
<div>
<p class="text-[9px] font-black text-slate-400 uppercase tracking-widest">Total Prestations</p>
<p class="text-xs font-bold text-slate-400 italic">Total TVA</p>
</div>
<p class="text-3xl font-black text-slate-900 italic tracking-tighter">{{ (totalTTC-totalHT)|number_format(2, ',', ' ') }}€</p>
</div>
<div class="bg-white rounded-[1.5rem] p-6 border border-slate-100 shadow-sm flex justify-between items-center">
<div>
<p class="text-[9px] font-black text-slate-400 uppercase tracking-widest">Total Prestations</p>
<p class="text-xs font-bold text-slate-400 italic">Total TTC</p>
</div>
<p class="text-3xl font-black text-slate-900 italic tracking-tighter">{{ totalTTC|number_format(2, ',', ' ') }}€</p>
</div>
{% endif %}
{# SOLDE FINAL - Bloc Principal avec saisie du montant #}
<div class="bg-slate-900 rounded-[2rem] p-8 text-white shadow-xl shadow-slate-200 relative overflow-hidden">
<div class="absolute top-0 right-0 -mt-4 -mr-4 w-24 h-24 bg-blue-600/10 rounded-full blur-2xl"></div>

View File

@@ -110,6 +110,22 @@
</div>
</div>
</div>
{# Menu Mobile #}
<div id="mobile-menu" class="hidden md:hidden bg-white border-t border-gray-100 shadow-xl">
<div class="px-4 pt-2 pb-6 space-y-2">
<a href="{{ path('reservation') }}" class="block px-3 py-2 text-base font-medium text-gray-700 hover:bg-gray-50 rounded-xl">{{ 'Accueil'|trans }}</a>
<a href="{{ path('reservation_catalogue') }}" class="block px-3 py-2 text-base font-medium text-gray-700 hover:bg-gray-50 rounded-xl">{{ 'Nos structures'|trans }}</a>
<a href="{{ path('reservation_formules') }}" class="block px-3 py-2 text-base font-medium text-gray-700 hover:bg-gray-50 rounded-xl">{{ 'Nos Formules'|trans }}</a>
<a target="_blank" href="/provider/Catalogue.pdf" class="block px-3 py-2 text-base font-medium text-gray-700 hover:bg-gray-50 rounded-xl">{{ 'Catalogue'|trans }}</a>
<a href="{{ path('reservation_workflow') }}" class="block px-3 py-2 text-base font-medium text-gray-700 hover:bg-gray-50 rounded-xl">{{ 'Comment reserver'|trans }}</a>
<a href="{{ path('reservation_search') }}" class="block px-3 py-2 text-base font-medium text-gray-700 hover:bg-gray-50 rounded-xl">{{ 'Rechercher'|trans }}</a>
<div class="pt-4 border-t border-gray-50">
<a href="tel:0614172447" class="block px-3 py-3 text-center bg-blue-600 text-white rounded-xl font-bold">
{{ 'Appeler le'|trans }} 06 14 17 24 47
</a>
</div>
</div>
</div>
</nav>
{# --- MESSAGES FLASH --- #}

View File

@@ -64,44 +64,63 @@
<div class="product-item group transition-all duration-500" data-category="{{ product.category|lower }}">
<a href="{{ path('reservation_product_show', {id: product.slug}) }}" class="block">
<div class="relative overflow-hidden rounded-[1rem] bg-slate-100 aspect-square mb-6 shadow-sm group-hover:shadow-2xl transition-all duration-700">
<div class="relative overflow-hidden rounded-[1.25rem] md:rounded-[1rem] bg-slate-100 aspect-square mb-4 md:mb-6 shadow-sm group-hover:shadow-2xl transition-all duration-700">
{% if product.imageName %}
<img src="{{ vich_uploader_asset(product,'imageFile') | imagine_filter('webp') }}" alt="{{ product.name }}" class="w-full h-full object-cover transform group-hover:scale-110 transition-transform duration-1000">
{% else %}
<img src="{{ asset('provider/images/favicon.png') | imagine_filter('webp') }}" alt="{{ product.name }}" class="w-full h-full object-cover opacity-50 transform group-hover:scale-110 transition-transform duration-1000">
{% endif %}
{# PRIX AVEC CLÉ #}
<div class="absolute top-5 right-5 bg-white/95 backdrop-blur-md px-4 py-2 rounded-2xl shadow-md border border-slate-100">
<p class="text-slate-900 font-black text-sm italic leading-none">
{{ 'catalog.product.from'|trans }} {{ product.priceDay|format_currency('EUR') }}
{% if product.category == "barnums" %}
/ {{ 'catalog.product.weekend'|trans }}
{# PRIX FLOTTANT (Ultra-minimal sur mobile) #}
<div class="absolute top-3 right-3 md:top-5 md:right-5 bg-white/95 backdrop-blur-md px-3 py-1.5 md:px-4 md:py-2 rounded-xl md:rounded-2xl shadow-md border border-slate-100">
<p class="text-slate-900 font-black text-xs md:text-sm italic leading-none whitespace-nowrap">
{# "Dès" masqué sur mobile pour gagner de la place #}
<span class="hidden md:inline text-[10px] opacity-60 not-italic mr-1">{{ 'catalog.product.from'|trans }}</span>
{% if tvaEnabled %}
{{ (product.priceDay*1.20)|format_currency('EUR') }} <span class="text-[8px] md:text-[10px]">TTC</span>
{% else %}
/ {{ 'catalog.product.day'|trans }}
{{ product.priceDay|format_currency('EUR') }}
{% endif %}
{# Suffixe masqué sur mobile #}
<span class="hidden md:inline text-slate-400">
{% if product.category == "barnums" %} / {{ 'catalog.product.weekend'|trans }}{% else %} / {{ 'catalog.product.day'|trans }}{% endif %}
</span>
</p>
</div>
</div>
<div class="px-2">
<div class="flex items-center gap-3 mb-2">
<span class="text-[10px] font-black text-blue-700 uppercase tracking-[0.2em] italic">
{{ product.category }} {# Souvent laissé brut car vient de la base #}
</span>
<span class="w-4 h-[1px] bg-slate-300"></span>
<span class="text-[9px] font-bold text-slate-500 uppercase tracking-widest">
{{ 'catalog.product.ref'|trans }} {{ product.ref }}
</span>
<div class="px-1 md:px-2">
{# META DATA (Avec mention de durée sur mobile) #}
<div class="flex flex-wrap items-center gap-x-2 gap-y-1 mb-2">
<span class="text-[9px] md:text-[10px] font-black text-blue-700 uppercase tracking-[0.15em] md:tracking-[0.2em] italic">
{{ product.category }}
</span>
<span class="w-3 h-[1px] bg-slate-300"></span>
<span class="text-[9px] font-bold text-slate-500 uppercase tracking-widest flex items-center gap-1">
{{ 'catalog.product.ref'|trans }} {{ product.ref }}
{# Mention de durée visible UNIQUEMENT sur mobile #}
<span class="md:hidden inline-flex items-center before:content-['•'] before:mr-1 before:text-slate-300 text-slate-400 font-medium lowercase">
{% if product.category == "barnums" %}
{{ 'catalog.product.weekend'|trans }}
{% else %}
{{ 'catalog.product.day'|trans }}
{% endif %}
</span>
</span>
</div>
<h3 class="text-2xl font-black text-slate-900 uppercase tracking-tighter leading-[0.95] group-hover:text-[#f39e36] transition-colors line-clamp-2">
<h3 class="text-lg md:text-2xl font-black text-slate-900 uppercase tracking-tighter leading-[0.95] group-hover:text-[#f39e36] transition-colors line-clamp-2">
{{ product.name }}
</h3>
<div class="mt-5 flex items-center gap-2 text-[11px] font-black uppercase tracking-[0.15em] text-slate-600 group-hover:text-[#f39e36] transition-colors">
<div class="mt-4 md:mt-5 flex items-center gap-2 text-[10px] md:text-[11px] font-black uppercase tracking-[0.15em] text-slate-600 group-hover:text-[#f39e36] transition-colors">
<span>{{ 'catalog.product.view_more'|trans }}</span>
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 transform group-hover:translate-x-2 transition-transform" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<svg xmlns="http://www.w3.org/2000/svg" class="h-3.5 w-3.5 md:h-4 md:w-4 transform group-hover:translate-x-2 transition-transform" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M17 8l4 4m0 0l-4-4m4-4H3" />
</svg>
</div>

View File

@@ -64,19 +64,44 @@
</div>
{# Grille de Tarifs Dégressifs #}
<div class="grid grid-cols-3 gap-3 mt-8 p-3 bg-slate-50 rounded-3xl border-2 border-slate-900/5">
<div class="text-center">
<p class="text-[8px] font-black text-slate-400 uppercase">1 JOUR</p>
<p class="text-sm font-black text-slate-900">{{ formule.price1j|format_currency('EUR') }}</p>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 md:gap-3 mt-8 p-4 md:p-3 bg-slate-50 rounded-2xl md:rounded-3xl border-2 border-slate-900/5">
<!-- 1 JOUR -->
<div class="text-center pb-3 md:pb-0 border-b border-slate-200 md:border-b-0">
<p class="text-[10px] md:text-[8px] font-black text-slate-400 uppercase tracking-wider">1 JOUR</p>
<p class="text-base md:text-sm font-black text-slate-900">
{% if tvaEnabled %}
{{ (formule.price1j * 1.20)|format_currency('EUR') }} <span class="text-[10px] opacity-75">TTC</span>
{% else %}
{{ formule.price1j|format_currency('EUR') }}
{% endif %}
</p>
</div>
<div class="text-center border-x border-slate-200">
<p class="text-[8px] font-black text-slate-400 uppercase">2 JOURS</p>
<p class="text-sm font-black text-blue-600">{{ formule.price2j|format_currency('EUR') }}</p>
<!-- 2 JOURS -->
<div class="text-center py-3 md:py-0 border-b border-slate-200 md:border-b-0 md:border-x">
<p class="text-[10px] md:text-[8px] font-black text-slate-400 uppercase tracking-wider">2 JOURS</p>
<p class="text-base md:text-sm font-black text-blue-600 md:text-blue-900">
{% if tvaEnabled %}
{{ (formule.price2j * 1.20)|format_currency('EUR') }} <span class="text-[10px] opacity-75">TTC</span>
{% else %}
{{ formule.price2j|format_currency('EUR') }}
{% endif %}
</p>
</div>
<div class="text-center">
<p class="text-[8px] font-black text-slate-400 uppercase">5 JOURS</p>
<p class="text-sm font-black text-emerald-600">{{ formule.price5j|format_currency('EUR') }}</p>
<!-- 5 JOURS -->
<div class="text-center pt-3 md:pt-0">
<p class="text-[10px] md:text-[8px] font-black text-slate-400 uppercase tracking-wider">5 JOURS</p>
<p class="text-base md:text-sm font-black text-emerald-600">
{% if tvaEnabled %}
{{ (formule.price5j * 1.20)|format_currency('EUR') }} <span class="text-[10px] opacity-75">TTC</span>
{% else %}
{{ formule.price5j|format_currency('EUR') }}
{% endif %}
</p>
</div>
</div>
<div class="mt-8">

View File

@@ -109,7 +109,11 @@
<div class="flex flex-col items-center md:items-start space-y-6">
{# Prix principal #}
<div class="flex flex-col md:flex-row items-center md:items-baseline gap-2 md:gap-4 text-center md:text-left">
<span class="text-6xl md:text-7xl font-black text-[#f39e36] leading-none">{{ product.priceDay|format_currency('EUR') }}</span>
{% if tvaEnabled %}
<span class="text-4xl md:text-3xl font-black text-[#f39e36] leading-none">{{ (product.priceDay*1.20)|format_currency('EUR') }} TTC</span>
{% else %}
<span class="text-4xl md:text-3xl font-black text-[#f39e36] leading-none">{{ product.priceDay|format_currency('EUR') }}</span>
{% endif %}
{% if product.category == "barnums" %}
<span class="text-[10px] md:text-sm font-bold text-slate-400 uppercase tracking-widest italic">Week-End</span>
{% else %}
@@ -121,7 +125,11 @@
<div class="flex flex-wrap justify-center md:justify-start gap-3 w-full">
{% if product.category != "barnums" %}
<div class="flex items-center gap-3 bg-slate-50 px-5 py-3 rounded-2xl border border-slate-100 shadow-sm">
<span class="text-xl md:text-2xl font-black text-slate-900 italic">+ {{ product.priceSup|format_currency('EUR') }}</span>
{% if tvaEnabled %}
<span class="text-md md:text-2xl font-black text-slate-900 italic">+ {{ (product.priceSup*1.20)|format_currency('EUR') }} TTC</span>
{% else %}
<span class="text-md md:text-2xl font-black text-slate-900 italic">+ {{ product.priceSup|format_currency('EUR') }}</span>
{% endif %}
<span class="text-[9px] font-black text-slate-400 uppercase tracking-widest italic">Jour supplémentaire</span>
</div>
{% endif %}
@@ -185,12 +193,12 @@
{# Chiffres #}
<div class="grid grid-cols-1 gap-3 md:gap-4 w-full">
<div class="flex items-center justify-between border-b border-slate-200/60 pb-3">
<span class="text-[9px] md:text-[10px] font-black text-slate-400 uppercase italic">Largeur</span>
<span class="text-lg md:text-xl font-black text-slate-900 italic">{{ product.dimW }} m</span>
<span class="text-[9px] md:text-[10px] font-black text-slate-400 uppercase italic">Longueur </span>
<span class="text-lg md:text-xl font-black text-slate-900 italic">{{ product.dimP }} m</span>
</div>
<div class="flex items-center justify-between border-b border-slate-200/60 pb-3">
<span class="text-[9px] md:text-[10px] font-black text-slate-400 uppercase italic">Profondeur</span>
<span class="text-lg md:text-xl font-black text-slate-900 italic">{{ product.dimP }} m</span>
<span class="text-[9px] md:text-[10px] font-black text-slate-400 uppercase italic">Largeur </span>
<span class="text-lg md:text-xl font-black text-slate-900 italic">{{ product.dimW }} m</span>
</div>
<div class="flex items-center justify-between">
<span class="text-[9px] md:text-[10px] font-black text-slate-400 uppercase italic">Hauteur</span>