Wizard: 2 cases génériques Prix marketing / Prix original (barré) sur toutes les lignes + aperçu client WYSIWYG

- Prix original (regular_price) disponible sur lignes récurrentes ET ponctuelles (avant: ponctuelles seulement)
- Si Prix original > Prix marketing → barré affiché (sommaire + récap + aperçu live dans l'éditeur)
- Forfait récurrent barré remonte au contrat (monthly_regular) → page d'acceptation affiche <s>99.95</s> 79.95/mois
- Cohérent avec install (install_regular 360→240). Aucun impact CRTC (rabais go-forward, jamais récupéré)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
louispaulb 2026-06-03 18:31:54 -04:00
parent 4b2e6fb698
commit e2104c93f2
3 changed files with 35 additions and 14 deletions

View File

@ -582,11 +582,11 @@
<span class="sommaire-sep">·</span>
<span class="text-grey-6">{{ item.contract_months }} mois</span>
</template>
<template v-if="item.billing === 'onetime' && item.regular_price > item.rate">
<template v-if="item.regular_price > item.rate">
<span class="sommaire-sep">·</span>
<q-chip dense square color="orange-1" text-color="orange-9"
icon="celebration" size="sm" style="margin:0">
Promo {{ ((item.regular_price - item.rate) * item.qty).toFixed(2) }}$
Promo {{ ((item.regular_price - item.rate) * item.qty).toFixed(2) }}${{ item.billing === 'recurring' ? '/mois' : '' }}
</q-chip>
</template>
</div>
@ -623,13 +623,14 @@
</div>
</div>
<div class="row q-col-gutter-sm q-mt-xs items-center">
<div class="col-3">
<div :class="item.billing === 'recurring' ? 'col-2' : 'col-3'">
<q-input v-model.number="item.qty" dense outlined type="number" min="1"
label="Qté" :input-style="{ fontSize: '0.82rem' }" />
</div>
<div class="col-5">
<div :class="item.billing === 'recurring' ? 'col-4' : 'col-5'">
<q-input v-model.number="item.rate" dense outlined type="number" step="0.01"
label="Prix $" :input-style="{ fontSize: '0.82rem' }">
label="Prix marketing $" :input-style="{ fontSize: '0.82rem' }"
title="Prix réellement facturé / affiché au client">
<template v-slot:append>
<button type="button" class="billing-pill" :class="billingPillClass(item)"
@click.stop="cycleBilling(item)"
@ -640,19 +641,32 @@
</template>
</q-input>
</div>
<div class="col-4">
<q-input v-if="item.billing === 'recurring'" v-model.number="item.contract_months"
<div v-if="item.billing === 'recurring'" class="col-3">
<q-input v-model.number="item.contract_months"
dense outlined type="number" label="Mois" title="Durée du contrat en mois"
:input-style="{ fontSize: '0.78rem', textAlign: 'center' }" />
<q-input v-else v-model.number="item.regular_price" dense outlined type="number" step="0.01"
label="Prix régulier (si promo)" placeholder="0 = sans promo"
:input-style="{ fontSize: '0.75rem' }">
</div>
<div :class="item.billing === 'recurring' ? 'col-3' : 'col-4'">
<q-input v-model.number="item.regular_price" dense outlined type="number" step="0.01"
label="Prix original" placeholder="vide = aucun"
:input-style="{ fontSize: '0.75rem' }"
title="Prix barré affiché au client. Laisser vide (ou ≤ prix marketing) = aucun prix barré.">
<template v-slot:append>
<q-icon name="local_offer" size="14px" color="orange-7" />
</template>
</q-input>
</div>
</div>
<!-- Aperçu client WYSIWYG : le prix marketing barre le prix original -->
<div v-if="Number(item.regular_price) > Number(item.rate)" class="q-mt-xs"
style="font-size:0.78rem;display:flex;align-items:center;gap:6px;flex-wrap:wrap">
<span class="text-grey-6">Aperçu client :</span>
<span style="text-decoration:line-through;color:#9ca3af">{{ Number(item.regular_price).toFixed(2) }}$</span>
<span class="text-weight-bold text-green-8">{{ Number(item.rate).toFixed(2) }}${{ item.billing === 'recurring' ? '/mois' : '' }}</span>
<q-chip dense square color="orange-1" text-color="orange-9" size="sm" style="margin:0">
{{ ((Number(item.regular_price) - Number(item.rate)) * (item.qty || 1)).toFixed(2) }}${{ item.billing === 'recurring' ? '/mois' : '' }}
</q-chip>
</div>
<!-- Bundle steps (installation pill) kept here so editing a
service still lets the rep tweak which install steps come
@ -716,9 +730,9 @@
<q-icon :name="item.billing === 'recurring' ? 'autorenew' : 'shopping_cart'" size="14px"
:color="item.billing === 'recurring' ? 'orange-7' : 'indigo-6'" />
<span class="col">{{ item.qty }}x {{ item.item_name }}</span>
<span v-if="item.billing === 'onetime' && item.regular_price > item.rate"
<span v-if="item.regular_price > item.rate"
class="text-caption text-orange-8" style="text-decoration:line-through;opacity:0.7">
{{ (item.qty * item.regular_price).toFixed(2) }}$
{{ (item.qty * item.regular_price).toFixed(2) }}${{ item.billing === 'recurring' ? '/mois' : '' }}
</span>
<span class="text-weight-bold">{{ (item.qty * item.rate).toFixed(2) }}${{ item.billing === 'recurring' ? '/mois' : '' }}</span>
</div>

View File

@ -244,6 +244,9 @@ export function useWizardPublish ({ props, emit, state }) {
try {
const durationMonths = Math.max(...recurringItems.map(i => i.contract_months || 12))
const monthlyRate = recurringItems.reduce((s, i) => s + (i.qty * i.rate), 0)
// Prix original (barré) du forfait : somme des prix originaux si > marketing, sinon le marketing.
const monthlyRegular = recurringItems.reduce(
(s, i) => s + (i.qty * ((i.regular_price || 0) > i.rate ? i.regular_price : i.rate)), 0)
const benefits = onetimeItems
.filter(i => (i.regular_price || 0) > i.rate)
.map(i => ({
@ -261,6 +264,7 @@ export function useWizardPublish ({ props, emit, state }) {
contract_type: contractType,
duration_months: durationMonths,
monthly_rate: monthlyRate,
monthly_regular: monthlyRegular > monthlyRate ? monthlyRegular : 0,
service_location: serviceLocation,
quotation: orderDocName,
start_date: today,

View File

@ -186,6 +186,7 @@ async function handle (req, res, method, path) {
end_date: endDate,
duration_months: duration,
monthly_rate: body.monthly_rate || 0,
monthly_regular: body.monthly_regular || 0,
install_fee: body.install_fee || 0,
install_regular: body.install_regular || 0,
service_location: body.service_location || '',
@ -942,6 +943,8 @@ function renderAcceptancePage (contract, token) {
const installFee = contract.install_fee || 0
const installRegular = contract.install_regular || 0
const installMonthly = installFee ? round2(installFee / duration) : 0
const monthlyRegular = contract.monthly_regular || 0
const mr = monthlyRegular > (contract.monthly_rate || 0)
return `<!DOCTYPE html><html lang="fr"><head>
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
@ -999,7 +1002,7 @@ tfoot td{font-weight:700;color:#00733a;background:#f4fff6;border-top:1px solid #
</div>
<div class="field"><span class="label">Client</span><span class="value">${escapeHtml(contract.customer_name || contract.customer || '')}</span></div>
<div class="field"><span class="label">Mensualité</span><span class="value">${contract.monthly_rate || 0} $/mois <span style="color:#9ca3af;font-weight:400;font-size:12px">(+taxes)</span></span></div>
<div class="field"><span class="label">Mensualité</span><span class="value">${mr ? `<s style="color:#9ca3af;font-weight:400">${monthlyRegular} $</s> ` : ''}${contract.monthly_rate || 0} $/mois <span style="color:#9ca3af;font-weight:400;font-size:12px">(+taxes)</span></span></div>
<div class="field"><span class="label">Durée</span><span class="value">${duration} mois</span></div>
<div class="field"><span class="label">Début prévu</span><span class="value">${contract.start_date || 'À déterminer'}</span></div>
@ -1007,7 +1010,7 @@ tfoot td{font-weight:700;color:#00733a;background:#f4fff6;border-top:1px solid #
<h3>Détail mensuel</h3>
<table>
<tbody>
<tr><td>Forfait</td><td class="r">${contract.monthly_rate || 0} $</td></tr>
<tr><td>Forfait${mr ? ` (<s style="color:#9ca3af">${monthlyRegular} $</s>)` : ''}</td><td class="r">${contract.monthly_rate || 0} $</td></tr>
<tr><td>Installation ${installRegular > installFee ? `(<s style="color:#9ca3af">${installRegular} $</s> ${installFee} $)` : `(${installFee} $)`} financement ${duration} mois</td><td class="r">+${installMonthly} $</td></tr>
<tr><td>Crédit installation nouveau client (${duration} mois)</td><td class="r" style="color:#019547">${installMonthly} $</td></tr>
</tbody>