JFIF $ d d 7 * Z qn )ZP 0 !1 A "#a$2Qr D8 a Ri [f \m I ykI w0 c u F c Rı?lO7 к _f˓ [C $殷WF< _W ԣ s Kc ëI zyQy /_LK ℂ ;C",p F A : /] =H ~ , l s / 9 ć: [=/#f ; ) x { ٛE Q )~ = 𘙲r*2~ a _V = ' ku m F D } KYYC )( { * g& f `툪 r y ` = ^ c J. I ]( * `wq 1dđ #̩ ͑ 0 ; H ] u 搂 @:~ ו K L N s h } O I R* 8 :2 ! lD JV o ( 3 = M ( z Ȱ+i *N A r6K n S l ) ! J J Ӂ* % ݉?|D } d5: eP0 R; {$X' x F @ .ÊB {,WJ uQ ɲRI ;9 Q E琯 6 2fT .DU J ;*c P A\IL Nj! J ۱+ O\ ͔ ] ޒ S J ȧ c%A N ol ՎprU L Zԛe r E2= X D Xg VQeӓ k y P7 U* o m Q I s ,K `) 6\G3t?pgjr m ۛجw luGt f h 9uyP 0D;Uڽ"OXl i f $ ) &|ML 0 Z rm1 [ H XP l P R0 'G=i 2N +0 e 2 ]] 9 VT P O 7 h(F * 癈 ' = Q VZD F,d ߬ ~ T X G[ `le 69C R(!S2! P < 0 x < !1AQ "Raq 02Br #S CTb ? Ζ "]mH5WR 7 k.ۛ ! } Q~ + yԏ z |@ T20 S~ K e k *zF f^2X *( @ 8 r ? C I uI|֓>^Ex LgNU Y+{.R Ѫ τV YT D I 6 2 '8Y2 7'\T P.6 d &˦ @V q i |8 -O Ε ] ʔ U= T L 8= ;6c | !qfF3 a ů& ~$ l} 'NW U s$U k^S V : U#6w + +s &r+n ڐ { @ 29gL u " T Ù M=6 (^ "7 r}=6Yݾ lC u hquymp Ǧ G jhs ǜNl ɻ} o 7 #S6 a w 4 ! OS r D 5 7 % | ?x> L | / nD 6?/8 w # [ ) L 7 +6〼T A T g ! %5 Mm Z /c -{1_Je "| ^$ 'O & ޱմ < H vNqϡQ$ < 6 &Nݛ ֨tǻN (9 n ru~'όN sMG& ,uH Kw *Ī 5@ u6 BԒ i YN * f 0 _ GhKW u lT <nj- 0{ M L~ | /a v\ E = ?1 \ɟuq Ђ Y O a K ޡP q $ H8o Q˼EV ~ k 0 h' L Pp M -M" m^ !6l nմ 쮝C U T . ! :vMǨb h ډ Vc r @~p # !1QAa q ? AL0L x LU 4 d ; x " 16ad0 7.\ߋ 0 = P l v/ " c A Ay A|2pO8"d MV #1] = \ " ˡ ] k v I9 d ۓ & q ) ǐq E ꞅ v "P )q X &Q З non]я u ג./ ; gZ$ M QР 2M ]h ni 0 P $ێ 3CɏGjώy : m c # S_ l@ '_ \m|C 2`K _ 1 cIvGc $? PCQ S+D ? K aײ k Fr b 8 ύ NEدg ĝ U 3 6 #Ik+ v { v i ( ވ ? a e) X xy/*@Q Ǽ Ծ ) $ IWO l c G; !i k{ gOŀ L ('x T rb $ w) R $ & N1E td U3Uȉ1p M"N* (DNy d96.(jQ) X 5 c QɎMy W ? Q *!R > 6=7)Xj 5`J ] e 8%t ! + ' ! 1Q 5 !1 AQaq ё #2 "0BRb ? G t^ ## .l lQ T $ v , ,m 㵜5u bV =sY+ @d { N ! dn O < .- B; _w J t 6;Q Jd. Q c%p{ 1 ,sND dF H I0 Гo X ш e黅X ۢ F :)[ F G X ƹ / w _c MeD ,ʡc c . W D tA $ j@ : ) - #u c1< @ۗ9F ) KJ- hpP] _ x [ q B lb p ʖ w q " LFGd ƶ * s +ډ_Zc " ? % t[I P 6 J ]# =ɺ V v v CGs Gh1 > )6 |e y ? L ӣ m,4 G W Ui ` ] uJVo VDG< SB 6ϏQ @ TiU l y O U 0 kfV ~ ~}S Z @ *WU Ui ## ; s /[ = ! 7}" W N ] ' (L ! ~ y5 g9T̅J kbM' +s :S +B) v @ M j e C f j E 0 Y\Qn z G 1 д~ W o{T 9?` Rm yhsy3 !HAD ] m c1 ~ 2 L S u7x T ;j$ `} 4-> L# vzŏ I L S ֭ T { r jGK C ; b pU= - ` Bs K. S F w4 M q] Z dH S 0 ) tLg
<?php
// controllers/DevisSignatureController.php
require_once __DIR__ . '/../models/Database.php';
require_once __DIR__ . '/../models/Devis.php';
require_once __DIR__ . '/../models/Client.php';
require_once __DIR__ . '/../models/ReglagesEntreprise.php';
require_once __DIR__ . '/../models/ReglagesGestion.php';
require_once __DIR__ . '/../models/ReglagesEmails.php';
require_once __DIR__ . '/../models/DevisSignature.php';
require_once __DIR__ . '/../models/DevisLigne.php';
require_once __DIR__ . '/../models/Facture.php';
require_once __DIR__ . '/../models/FactureLigne.php';
require_once __DIR__ . '/../models/Email.php';
require_once __DIR__ . '/../models/EmailAttachment.php';
require_once __DIR__ . '/PdfFactureController.php';
require_once __DIR__ . '/../lib/EmailTemplateRenderer.php';
require_once __DIR__ . '/../lib/MailerService.php';
header('Content-Type: text/html; charset=utf-8');
$db = Database::getInstance()->getConnection();
$devisModel = new Devis();
$clientModel = new Client();
$reglagesEntreprise = new ReglagesEntreprise($db);
$infos_entreprise = $reglagesEntreprise->get();
$reglagesGestion = new ReglagesGestion($db);
$infos_gestion = $reglagesGestion->get();
$signatureModel = new DevisSignature($db);
$token = isset($_GET['token']) ? trim((string)$_GET['token']) : '';
function h(string $v): string { return htmlspecialchars($v, ENT_QUOTES, 'UTF-8'); }
function renderSignatureSnapshotHtml(array $devis, ?array $client, array $signature, array $devisLignes, array $infosEntreprise, array $infosGestion): string {
$numeroDevis = $devis['numero'] ?? '';
if ($numeroDevis === '' || $numeroDevis === null) {
$fallbackYear = date('Y', strtotime($devis['date_devis'] ?? 'now'));
$prefix = isset($infosGestion['prefixe_devis']) && $infosGestion['prefixe_devis'] !== '' ? $infosGestion['prefixe_devis'] : 'D-{year}-00';
$resolved = str_replace('{year}', $fallbackYear, $prefix);
$numeroDevis = $resolved . (string)($devis['id'] ?? '');
}
$signedAtRaw = (string)($signature['signed_at'] ?? '');
$signedAtLabel = $signedAtRaw !== '' ? date('d/m/Y H:i', strtotime($signedAtRaw)) : date('d/m/Y H:i');
$clientChoice = (string)($signature['client_choice'] ?? '');
$paymentOption = (string)($signature['payment_option'] ?? '');
$paymentLabel = 'Paiement non renseigné';
if ($clientChoice === 'bank') {
$paymentLabel = 'Paiement par virement / banque';
} elseif ($clientChoice === 'online') {
if ($paymentOption === 'acompte_manual') {
$paymentLabel = 'Paiement en ligne : acompte manuel du devis';
} elseif ($paymentOption === 'acompte_40') {
$paymentLabel = 'Paiement en ligne : acompte 40%';
} elseif ($paymentOption === 'full_ht') {
$paymentLabel = 'Paiement en ligne : 100% HT';
} elseif ($paymentOption === 'abonnement_auto') {
$paymentLabel = 'Paiement en ligne : abonnement mensuel';
} else {
$paymentLabel = 'Paiement en ligne';
}
}
$montantHt = isset($devis['montant']) ? (float)$devis['montant'] : 0.0;
$tvaActive = !empty($infosGestion['tva_active']);
$tauxTva = isset($infosGestion['taux_tva']) ? (float)$infosGestion['taux_tva'] : 20.0;
ob_start();
?>
<div style="width:210mm;min-height:297mm;margin:0 auto;background:#fff;padding:32px 40px 24px 40px;box-sizing:border-box;border-radius:12px;box-shadow:0 4px 24px #0002;font-size:0.9em;font-family:Arial,sans-serif;display:flex;flex-direction:column;">
<div style="display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:24px;">
<div style="display:flex;flex-direction:column;align-items:flex-start;max-width:50%;padding:12px 18px 12px 0;">
<?php if (!empty($infosEntreprise['logo'])): ?>
<img src="<?= h((string)$infosEntreprise['logo']) ?>" alt="Logo entreprise" style="height:75px;max-width:150px;object-fit:contain;border-radius:8px;margin-bottom:10px;display:block;">
<?php endif; ?>
<div><?= h((string)($infosEntreprise['adresse'] ?? '')) ?></div>
<div><?= h((string)($infosEntreprise['code_postal'] ?? '')) ?> <?= h((string)($infosEntreprise['ville'] ?? '')) ?></div>
<?php if (!empty($infosEntreprise['email'])): ?><div>Email : <?= h((string)$infosEntreprise['email']) ?></div><?php endif; ?>
<?php if (!empty($infosEntreprise['telephone'])): ?><div>Tél : <?= h((string)$infosEntreprise['telephone']) ?></div><?php endif; ?>
</div>
<div style="text-align:right;max-width:45%;">
<div style="font-size:1.05em;font-weight:600;margin-bottom:10px;">Signature enregistrée le <?= h($signedAtLabel) ?></div>
<div style="background:#f7f7f7;border-radius:7px;padding:12px 16px;border:1px solid #b0b4bb;">
<div style="font-weight:bold;font-size:1.08em;margin-bottom:4px;">Client</div>
<div><?= h((string)($client['nom'] ?? ($devis['client_nom'] ?? ''))) ?></div>
<div><?= h((string)($client['entreprise'] ?? '')) ?></div>
<div><?= h((string)($client['email'] ?? ($signature['email_client'] ?? ''))) ?></div>
<div><?= h((string)($client['telephone'] ?? '')) ?></div>
<div><?= h((string)($client['rue'] ?? '')) ?>, <?= h((string)($client['code_postal'] ?? '')) ?> <?= h((string)($client['ville'] ?? '')) ?></div>
</div>
</div>
</div>
<h2 style="font-size:1.3em;margin:0 0 12px 0;">Photographie de signature du devis n°<?= h((string)$numeroDevis) ?></h2>
<div style="margin-bottom:16px;padding:10px 14px;border-radius:8px;background:#e8f7ee;border:1px solid #bbf7d0;color:#166534;">
<div style="font-weight:700;margin-bottom:4px;">Devis signé</div>
<div><?= h($paymentLabel) ?></div>
<?php if (!empty($signature['ip'])): ?><div>Adresse IP : <?= h((string)$signature['ip']) ?></div><?php endif; ?>
<?php if (!empty($signature['user_agent'])): ?><div style="word-break:break-word;">Navigateur : <?= h((string)$signature['user_agent']) ?></div><?php endif; ?>
</div>
<table style="width:100%;border-collapse:collapse;margin-bottom:18px;">
<thead>
<tr style="background:#f7f7f7;">
<th style="padding:6px;border:1px solid #e0e0e0;text-align:left;">Intitulé</th>
<th style="padding:6px;border:1px solid #e0e0e0;text-align:right;">Quantité</th>
<th style="padding:6px;border:1px solid #e0e0e0;text-align:right;">Prix unitaire HT</th>
<th style="padding:6px;border:1px solid #e0e0e0;text-align:right;">Remise</th>
<th style="padding:6px;border:1px solid #e0e0e0;text-align:center;">Type</th>
<th style="padding:6px;border:1px solid #e0e0e0;text-align:right;">Total</th>
</tr>
</thead>
<tbody>
<?php foreach ($devisLignes as $ligne): ?>
<?php
$q = (float)($ligne['quantite'] ?? 1);
$pu = (float)($ligne['prix_unitaire'] ?? 0);
$rem = (float)($ligne['remise'] ?? 0);
$typeRem = (string)($ligne['type_remise'] ?? 'euro');
$totalLigne = $q * $pu;
if ($typeRem === 'euro') {
$totalLigne -= $rem;
} else {
$totalLigne -= ($totalLigne * $rem / 100);
}
if ($totalLigne < 0) {
$totalLigne = 0;
}
?>
<tr>
<td style="padding:6px;border:1px solid #e0e0e0;"><?= nl2br(h((string)($ligne['intitule'] ?? ''))) ?></td>
<td style="padding:6px;border:1px solid #e0e0e0;text-align:right;"><?= h((string)($ligne['quantite'] ?? '')) ?></td>
<td style="padding:6px;border:1px solid #e0e0e0;text-align:right;"><?= number_format($pu, 2, ',', ' ') ?> €</td>
<td style="padding:6px;border:1px solid #e0e0e0;text-align:right;"><?= $rem != 0.0 ? number_format($rem, 2, ',', ' ') : '' ?></td>
<td style="padding:6px;border:1px solid #e0e0e0;text-align:center;"><?= $rem != 0.0 ? ($typeRem === 'euro' ? '€' : '%') : '' ?></td>
<td style="padding:6px;border:1px solid #e0e0e0;text-align:right;"><?= number_format($totalLigne, 2, ',', ' ') ?> €</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<div style="display:flex;justify-content:space-between;align-items:flex-end;gap:24px;margin-top:auto;">
<div style="flex:1;max-width:50%;min-width:220px;">
<strong>Description :</strong><br>
<div style="background:#f7f7f7;padding:10px 14px;border-radius:7px;min-height:40px;"><?= nl2br(h((string)($devis['description'] ?? ''))) ?></div>
</div>
<div style="flex:1;max-width:50%;min-width:220px;display:flex;justify-content:flex-end;">
<div style="background:#f7f7f7;border-radius:7px;padding:18px 28px;min-width:260px;text-align:right;border:1px solid #b0b4bb;">
<div style="font-weight:bold;font-size:1.08em;margin-bottom:8px;">Net à payer</div>
<div><strong>Montant HT :</strong> <?= number_format($montantHt, 2, ',', ' ') ?> €</div>
<?php if ($tvaActive): ?>
<div><strong>TVA (<?= number_format($tauxTva, 2, ',', ' ') ?>%) :</strong> <?= number_format($montantHt * $tauxTva / 100, 2, ',', ' ') ?> €</div>
<div><strong>Montant TTC :</strong> <?= number_format($montantHt * (1 + $tauxTva / 100), 2, ',', ' ') ?> €</div>
<?php endif; ?>
</div>
</div>
</div>
</div>
<?php
return (string)ob_get_clean();
}
if ($token === '') {
echo '<!DOCTYPE html><html><body><p>Token manquant.</p></body></html>';
exit;
}
$signature = $signatureModel->findByToken($token);
if (!$signature) {
echo '<!DOCTYPE html><html><body><p>Signature introuvable ou lien invalide.</p></body></html>';
exit;
}
$devisId = (int)($signature['devis_id'] ?? 0);
$devis = $devisModel->getById($devisId);
if (!$devis) {
echo '<!DOCTYPE html><html><body><p>Devis introuvable.</p></body></html>';
exit;
}
$client = $clientModel->getById($devis['client_id']);
// Lignes détaillées du devis pour affichage quasi identique à l'aperçu A4
$devisLigneModel = new DevisLigne();
$devis_lignes = $devisLigneModel->getByDevisId($devisId);
$errors = [];
$done = false;
$clientChoice = null;
$paymentOption = null;
// Si la demande de signature est déjà marquée comme signée, on ne permet plus
// de modifier le choix : on affiche directement l'écran de confirmation.
$alreadySigned = isset($signature['statut']) && $signature['statut'] === 'signe';
if ($alreadySigned) {
$done = true;
$clientChoice = $signature['client_choice'] ?? null;
$paymentOption = $signature['payment_option'] ?? null;
}
// Détermine les options de paiement en ligne autorisées pour cette signature
$allowedPaymentsRaw = isset($signature['allowed_payments']) ? (string)$signature['allowed_payments'] : '';
$allowedPayments = array_filter(array_map('trim', explode(',', $allowedPaymentsRaw)));
if (!count($allowedPayments)) {
$allowedPayments = [$signature['required_payment'] ?? 'acompte_manual'];
}
$onlineAllowedPayments = array_values(array_intersect($allowedPayments, ['acompte_manual', 'acompte_40', 'full_ht', 'abonnement_auto']));
$hasBankMarker = in_array('bank', $allowedPayments, true);
$hasNoBankMarker = in_array('no_bank', $allowedPayments, true);
// Compat: anciennes signatures sans marqueur bank/no_bank -> banque autorisée.
$allowBank = $hasBankMarker || (!$hasBankMarker && !$hasNoBankMarker);
$total = isset($devis['montant']) ? (float)$devis['montant'] : 0.0;
$manualDepositAmount = 0.0;
$acompteEnabledForSignature = !empty($devis['acompte_enabled']);
$acompteTypeForSignature = (string)($devis['acompte_type'] ?? 'euro');
$acompteValeurForSignature = max(0, (float)($devis['acompte_valeur'] ?? 0));
if ($acompteEnabledForSignature) {
if ($acompteTypeForSignature === 'pourcent') {
$manualDepositAmount = $total > 0 ? ($total * $acompteValeurForSignature / 100) : 0.0;
} else {
$manualDepositAmount = $acompteValeurForSignature;
}
if ($manualDepositAmount > $total) {
$manualDepositAmount = $total;
}
}
if ($_SERVER['REQUEST_METHOD'] === 'POST' && !$alreadySigned) {
if (!isset($_POST['accept_conditions'])) {
$errors[] = 'Vous devez accepter les conditions du devis.';
}
$choice = isset($_POST['payment_choice']) ? (string)$_POST['payment_choice'] : '';
$validChoices = ['bank', 'online_acompte_manual', 'online_acompte_40', 'online_full_ht', 'online_abonnement_auto'];
if (!in_array($choice, $validChoices, true)) {
$errors[] = 'Veuillez choisir un mode de paiement.';
}
if (!$errors) {
if ($choice === 'bank') {
if (!$allowBank) {
$errors[] = 'Cette option de paiement n\'est pas disponible.';
}
$clientChoice = 'bank';
$paymentOption = null;
} else {
$clientChoice = 'online';
if ($choice === 'online_acompte_manual') {
$paymentOption = 'acompte_manual';
} elseif ($choice === 'online_acompte_40') {
$paymentOption = 'acompte_40';
} elseif ($choice === 'online_full_ht') {
$paymentOption = 'full_ht';
} elseif ($choice === 'online_abonnement_auto') {
$paymentOption = 'abonnement_auto';
}
// Si l'option choisie n'était pas proposée, on la rejette par sécurité
if ($paymentOption && !in_array($paymentOption, $onlineAllowedPayments, true)) {
$errors[] = 'Cette option de paiement n\'est pas disponible.';
}
}
}
if (!$errors) {
$ip = $_SERVER['REMOTE_ADDR'] ?? null;
$ua = $_SERVER['HTTP_USER_AGENT'] ?? null;
$signatureModel->markSigned((int)$signature['id'], $clientChoice, $ip, $ua, $paymentOption);
$signature = $signatureModel->findByToken($token) ?: $signature;
$snapshotHtml = renderSignatureSnapshotHtml($devis, $client ?: null, $signature, is_array($devis_lignes) ? $devis_lignes : [], $infos_entreprise, $infos_gestion);
$signatureModel->saveSnapshot((int)$signature['id'], $snapshotHtml);
// Met à jour le statut du devis en "Signé"
$devisModel->updateStatus($devisId, 'Signé');
$createdFactureId = 0;
$shouldSendCreatedFacture = false;
// Conversion automatique du devis en facture selon le mode de paiement choisi (40 %, 100 %, abonnement)
if ($clientChoice === 'online' && $paymentOption !== null) {
try {
$pdo = Database::getInstance()->getConnection();
$pdo->beginTransaction();
$factureModel = new Facture();
$factureLigneModel = new FactureLigne();
$totalHt = isset($devis['montant']) ? (float)$devis['montant'] : 0.0;
$acompte40 = $totalHt > 0 ? $totalHt * 0.4 : 0.0;
if ($paymentOption === 'acompte_manual') {
$montant = $manualDepositAmount;
if ($montant <= 0) {
throw new RuntimeException('Acompte manuel invalide sur le devis.');
}
$factureData = [
'client_id' => $devis['client_id'],
'date_facture' => date('Y-m-d'),
'numero' => null,
'montant' => $montant,
'montant_regle' => 0,
'statut' => 'Envoyée',
'description' => 'Acompte manuel du devis n°' . ($devis['numero'] ?? $devisId),
'remise_generale' => 0,
'type_remise_generale' => 'euro',
'mode_paiement' => 'acompte_manual',
'abonnement_duree_mois' => null,
'abonnement_auto_envoi' => 0,
'abonnement_parent_id' => null,
'generee_automatiquement' => 0,
];
$factureModel->create($factureData);
$factureId = $pdo->lastInsertId();
$intitule = 'Acompte manuel du devis n°' . ($devis['numero'] ?? $devisId);
$factureLigneModel->add($factureId, $intitule, 1, $montant, 0, 'euro', $montant);
$stmt = $pdo->prepare('UPDATE devis SET montant_facture = COALESCE(montant_facture,0) + ? WHERE id = ?');
$stmt->execute([$montant, $devisId]);
} elseif ($paymentOption === 'acompte_40') {
$montant = $acompte40;
$factureData = [
'client_id' => $devis['client_id'],
'date_facture' => date('Y-m-d'),
'numero' => null,
'montant' => $montant,
'montant_regle' => 0,
'statut' => 'Envoyée',
'description' => 'Acompte 40% du devis n°' . ($devis['numero'] ?? $devisId),
'remise_generale' => 0,
'type_remise_generale' => 'euro',
'mode_paiement' => 'acompte_40',
'abonnement_duree_mois' => null,
'abonnement_auto_envoi' => 0,
'abonnement_parent_id' => null,
'generee_automatiquement' => 0,
];
$factureModel->create($factureData);
$factureId = $pdo->lastInsertId();
$intitule = 'Acompte 40% du devis n°' . ($devis['numero'] ?? $devisId);
$factureLigneModel->add($factureId, $intitule, 1, $montant, 0, 'euro', $montant);
// Mise à jour du montant facturé sur le devis
$stmt = $pdo->prepare('UPDATE devis SET montant_facture = COALESCE(montant_facture,0) + ? WHERE id = ?');
$stmt->execute([$montant, $devisId]);
} elseif ($paymentOption === 'full_ht') {
$montant = $totalHt;
$factureData = [
'client_id' => $devis['client_id'],
'date_facture' => date('Y-m-d'),
'numero' => null,
'montant' => $montant,
'montant_regle' => 0,
'statut' => 'Envoyée',
'description' => $devis['description'] ?? ('Facture du devis n°' . ($devis['numero'] ?? $devisId)),
'remise_generale' => $devis['remise_generale'] ?? 0,
'type_remise_generale' => $devis['type_remise_generale'] ?? 'euro',
'mode_paiement' => 'full_ht',
'abonnement_duree_mois' => null,
'abonnement_auto_envoi' => 0,
'abonnement_parent_id' => null,
'generee_automatiquement' => 0,
];
$factureModel->create($factureData);
$factureId = $pdo->lastInsertId();
// Recopie des lignes du devis dans la facture
if ($devis_lignes && is_array($devis_lignes)) {
foreach ($devis_lignes as $ligne) {
$factureLigneModel->add(
$factureId,
$ligne['intitule'],
$ligne['quantite'],
$ligne['prix_unitaire'],
$ligne['remise'],
$ligne['type_remise'],
$ligne['total_ligne']
);
}
}
$stmt = $pdo->prepare('UPDATE devis SET montant_facture = COALESCE(montant_facture,0) + ? WHERE id = ?');
$stmt->execute([$montant, $devisId]);
} elseif ($paymentOption === 'abonnement_auto') {
$subscriptionMonths = null;
if (!empty($signature['subscription_months'])) {
$subscriptionMonths = (int)$signature['subscription_months'];
}
if (!$subscriptionMonths || $subscriptionMonths <= 0) {
$subscriptionMonths = 12;
}
$monthly = $totalHt > 0 ? ($totalHt / $subscriptionMonths) : 0.0;
$montant = $monthly;
$factureData = [
'client_id' => $devis['client_id'],
'date_facture' => date('Y-m-d'),
'numero' => null,
'montant' => $montant,
'montant_regle' => 0,
'statut' => 'Envoyée',
'description' => 'Abonnement mensuel (1/' . $subscriptionMonths . ') pour le devis n°' . ($devis['numero'] ?? $devisId),
'remise_generale' => 0,
'type_remise_generale' => 'euro',
'mode_paiement' => 'abonnement_mensuel',
'abonnement_duree_mois' => $subscriptionMonths,
'abonnement_auto_envoi' => 1,
'abonnement_parent_id' => null,
'generee_automatiquement' => 0,
];
$factureModel->create($factureData);
$factureId = $pdo->lastInsertId();
$createdFactureId = (int)$factureId;
$shouldSendCreatedFacture = true;
$intitule = 'Abonnement mensuel (' . number_format($montant, 2, ',', ' ') . ' € / mois) devis n°' . ($devis['numero'] ?? $devisId);
$factureLigneModel->add($factureId, $intitule, 1, $montant, 0, 'euro', $montant);
$stmt = $pdo->prepare('UPDATE devis SET montant_facture = COALESCE(montant_facture,0) + ? WHERE id = ?');
$stmt->execute([$montant, $devisId]);
}
if ($pdo->inTransaction()) {
$pdo->commit();
}
} catch (Throwable $e) {
$pdo = Database::getInstance()->getConnection();
if ($pdo->inTransaction()) {
$pdo->rollBack();
}
// En cas d'erreur de création de facture, on ne bloque pas la signature
}
}
if ($shouldSendCreatedFacture && $createdFactureId > 0) {
try {
$reglagesEmails = new ReglagesEmails($db);
$emailCfg = $reglagesEmails->get();
if (empty($emailCfg['facture_email_enabled'])) {
throw new RuntimeException('Envoi des factures par e-mail désactivé.');
}
$mailer = new MailerService($db);
$emailModel = new Email();
$emailAttachmentModel = new EmailAttachment();
$pdfFactCtrl = new PdfFactureController();
$factureModel = new Facture();
$facture = $factureModel->getById($createdFactureId);
$to = trim((string)($client['email'] ?? ''));
if ($facture && $to !== '' && filter_var($to, FILTER_VALIDATE_EMAIL)) {
$subjectTemplate = 'Votre facture n°{{numero}}';
$bodyTemplate = trim((string)($emailCfg['template_facture'] ?? ''));
if ($bodyTemplate === '') {
$bodyTemplate = '<p>Bonjour {{client_nom}},</p><p>Veuillez trouver ci-joint votre facture n°{{numero}} du {{date}}.</p>';
}
$context = EmailTemplateRenderer::buildFactureContext($facture, $client ?: null);
$subject = EmailTemplateRenderer::render($subjectTemplate, $context);
$bodyHtml = EmailTemplateRenderer::render($bodyTemplate, $context);
$attachments = [];
[$pdfContent, $pdfName] = $pdfFactCtrl->generateToString($createdFactureId);
if (is_string($pdfContent) && $pdfContent !== '') {
$attachments[] = [
'content' => $pdfContent,
'name' => $pdfName ?: 'facture.pdf',
'mime' => 'application/pdf',
];
}
$sendResult = $mailer->send($to, $subject, $bodyHtml, $attachments);
$emailModel->create([
'type' => 'sent',
'sujet' => $subject,
'corps' => $bodyHtml,
'expediteur' => $sendResult['from_email'] ?? '',
'destinataire' => $to,
'statut' => !empty($sendResult['success']) ? 'Envoyé automatiquement' : 'Erreur (envoi auto)',
'erreur' => !empty($sendResult['success']) ? null : ($sendResult['error'] ?? 'Echec de l\'envoi auto'),
'lu' => 1,
]);
$emailId = (int)$db->lastInsertId();
if ($emailId > 0) {
foreach ($attachments as $attachment) {
$emailAttachmentModel->create([
'email_id' => $emailId,
'filename' => $attachment['name'] ?? 'facture.pdf',
'mime_type' => $attachment['mime'] ?? 'application/pdf',
'content' => $attachment['content'] ?? null,
]);
}
}
}
} catch (Throwable $e) {
// On ne bloque jamais la validation de la signature si l'envoi auto échoue.
}
}
// Si le client a choisi le paiement en ligne et que Stripe est configuré,
// on tente de créer une session de paiement Stripe Checkout.
if ($clientChoice === 'online') {
$stripeSecret = trim((string)($infos_gestion['stripe_secret'] ?? ''));
if ($stripeSecret !== '' && function_exists('curl_init')) {
$scheme = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on') ? 'https://' : 'http://';
$host = $_SERVER['HTTP_HOST'] ?? '';
$scriptDir = dirname($_SERVER['SCRIPT_NAME'] ?? '/');
$appBaseDir = rtrim(dirname($scriptDir), '/');
if ($appBaseDir === '' || $appBaseDir === '.') {
$appBaseDir = '';
}
$baseUrl = $scheme . $host . $appBaseDir;
$successUrl = $baseUrl . '/controllers/DevisSignatureController.php?token=' . urlencode($token) . '&paid=1';
$cancelUrl = $baseUrl . '/controllers/DevisSignatureController.php?token=' . urlencode($token) . '&cancel=1';
$totalHt = isset($devis['montant']) ? (float)$devis['montant'] : 0.0;
$acompte40 = $totalHt > 0 ? $totalHt * 0.4 : 0.0;
$subscriptionMonths = null;
if (!empty($signature['subscription_months'])) {
$subscriptionMonths = (int)$signature['subscription_months'];
if ($subscriptionMonths <= 0) {
$subscriptionMonths = null;
}
}
// Option effective choisie par le client (fallback sur required_payment)
$requiredPayment = $paymentOption ?: (string)($signature['required_payment'] ?? 'none');
$mode = 'payment';
$unitAmount = 0;
$label = 'Paiement devis #' . ($devis['numero'] ?? $devisId);
if ($requiredPayment === 'abonnement_auto') {
// Abonnement mensuel automatique basé sur le montant HT du devis et la durée choisie
$mode = 'subscription';
$monthly = 0.0;
if ($totalHt > 0) {
$months = $subscriptionMonths ?? 12;
$monthly = $months > 0 ? ($totalHt / $months) : $totalHt;
}
$unitAmount = (int)max(1, round($monthly * 100));
$label = 'Abonnement mensuel devis #' . ($devis['numero'] ?? $devisId);
} elseif ($requiredPayment === 'full_ht') {
// Paiement intégral du montant HT en une seule fois
$mode = 'payment';
$unitAmount = (int)max(1, round($totalHt * 100));
$label = 'Paiement total devis #' . ($devis['numero'] ?? $devisId);
} elseif ($requiredPayment === 'acompte_40') {
// Acompte fixe 40 % du montant HT
$mode = 'payment';
$unitAmount = (int)max(1, round($acompte40 * 100));
$label = 'Acompte 40% devis #' . ($devis['numero'] ?? $devisId);
} else {
// Par défaut : acompte manuel du devis
$mode = 'payment';
$unitAmount = (int)max(1, round($manualDepositAmount * 100));
$label = 'Acompte manuel devis #' . ($devis['numero'] ?? $devisId);
}
if ($unitAmount > 0) {
$fields = [];
$fields['mode'] = $mode;
$fields['success_url'] = $successUrl;
$fields['cancel_url'] = $cancelUrl;
$fields['line_items[0][quantity]'] = 1;
$fields['line_items[0][price_data][currency]'] = 'eur';
$fields['line_items[0][price_data][product_data][name]'] = $label;
$fields['line_items[0][price_data][unit_amount]'] = $unitAmount;
if ($mode === 'subscription') {
$fields['line_items[0][price_data][recurring][interval]'] = 'month';
}
$ch = curl_init('https://api.stripe.com/v1/checkout/sessions');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . $stripeSecret,
],
CURLOPT_POSTFIELDS => http_build_query($fields),
]);
$resp = curl_exec($ch);
$httpCode = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode >= 200 && $httpCode < 300 && $resp) {
$json = json_decode($resp, true);
if (is_array($json) && isset($json['url'])) {
header('Location: ' . $json['url']);
exit;
}
}
}
}
}
$done = true;
}
}
$total = isset($devis['montant']) ? (float)$devis['montant'] : 0.0;
$acompte40 = $total > 0 ? $total * 0.4 : 0.0;
$subscriptionMonthsView = null;
if (!empty($signature['subscription_months'])) {
$subscriptionMonthsView = (int)$signature['subscription_months'];
if ($subscriptionMonthsView <= 0) {
$subscriptionMonthsView = null;
}
}
$ribTexte = (string)($infos_gestion['rib_texte'] ?? '');
$mentionBasDevis = (string)($infos_entreprise['mention_devis'] ?? '');
?><!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="utf-8">
<title>Signature du devis</title>
<style>
body { font-family: Arial, sans-serif; background:#f7f7f7; margin:0; padding:0; }
.container { max-width: 760px; margin: 40px auto; background:#fff; border-radius:10px; box-shadow:0 2px 16px #0002; padding:28px 32px 24px 32px; }
h1 { margin-top:0; color:#1769aa; font-size:1.4em; }
.header-row { display:flex; justify-content:space-between; align-items:flex-start; margin-bottom:22px; }
.header-left { display:flex; flex-direction:column; align-items:flex-start; max-width:55%; }
.header-left-logo { margin-bottom:8px; }
.header-left-logo img { max-height:70px; max-width:160px; object-fit:contain; border-radius:6px; }
.header-left-info { font-size:0.9em; color:#374151; }
.header-right { text-align:right; max-width:40%; font-size:0.9em; }
.header-date { font-weight:600; margin-bottom:8px; }
.header-client { background:#f7f7f7; border-radius:7px; padding:10px 12px; border:1px solid #d1d5db; }
.header-client-title { font-weight:bold; margin-bottom:4px; }
.error { background:#fdecec; color:#b42318; padding:8px 10px; border-radius:6px; margin-bottom:10px; }
.success { background:#e8f7ee; color:#186a3b; padding:10px 12px; border-radius:8px; margin-bottom:12px; }
.rib-box { background:#f3f4f6; border-radius:8px; padding:10px 12px; font-size:0.9em; white-space:pre-wrap; }
.choices { margin:12px 0; }
.payment-title { font-size:1.05em; margin:0 0 6px 0; }
.consent-block { margin:16px 0 0 0; }
.consent-block label { font-size:1.05em; font-weight:600; }
table.devis-lines { width:100%; border-collapse:collapse; margin:12px 0 10px 0; font-size:0.9em; }
table.devis-lines th, table.devis-lines td { padding:6px 8px; border:1px solid #e5e7eb; }
table.devis-lines thead tr { background:#f7f7f7; }
.totals-box-wrapper { display:flex; justify-content:flex-end; margin:8px 0 16px 0; }
.totals-box { background:#f9fafb; border-radius:8px; padding:10px 14px; border:1px solid #d1d5db; min-width:230px; text-align:right; font-size:0.92em; }
.totals-box-row { margin-bottom:4px; }
.totals-box-row strong { font-weight:600; }
button { background:#1769aa; color:#fff; border:none; border-radius:6px; padding:8px 16px; font-size:1em; cursor:pointer; }
button:hover { background:#2980b9; }
label { display:block; margin-bottom:6px; }
</style>
</head>
<body>
<div class="container">
<?php if ($done): ?>
<div class="success">
<strong>Merci.</strong><br>
Votre accord pour le devis a bien été enregistré.
</div>
<?php if ($clientChoice === 'bank'): ?>
<p>Vous avez choisi le paiement par virement ou chèque. Vous trouverez ci-dessous les coordonnées bancaires :</p>
<?php if ($ribTexte !== ''): ?>
<div class="rib-box"><?= nl2br(h($ribTexte)) ?></div>
<?php else: ?>
<p>Aucune coordonnée bancaire n'est encore configurée.</p>
<?php endif; ?>
<?php else: ?>
<p>Vous avez choisi le paiement en ligne. Un lien de paiement sécurisé pourra vous être adressé par votre interlocuteur.</p>
<?php endif; ?>
<?php else: ?>
<div class="header-row">
<div class="header-left">
<?php
$logoUrl = '';
if (!empty($infos_entreprise['logo'])) {
$logoUrl = (string)$infos_entreprise['logo'];
if ($logoUrl !== '' && $logoUrl[0] !== '/' && !preg_match('~^https?://~i', $logoUrl)) {
$logoUrl = '/' . $logoUrl;
}
}
if ($logoUrl !== ''): ?>
<div class="header-left-logo">
<img src="<?= h($logoUrl) ?>" alt="Logo entreprise">
</div>
<?php endif; ?>
<div class="header-left-info">
<?php if (!empty($infos_entreprise['nom'])): ?>
<div><strong><?= h((string)$infos_entreprise['nom']) ?></strong></div>
<?php endif; ?>
<?php if (!empty($infos_entreprise['adresse']) || !empty($infos_entreprise['code_postal']) || !empty($infos_entreprise['ville'])): ?>
<div>
<?= h((string)($infos_entreprise['adresse'] ?? '')) ?><?php if (!empty($infos_entreprise['code_postal']) || !empty($infos_entreprise['ville'])): ?><br><?= h((string)($infos_entreprise['code_postal'] ?? '')) ?> <?= h((string)($infos_entreprise['ville'] ?? '')) ?><?php endif; ?>
</div>
<?php endif; ?>
<?php if (!empty($infos_entreprise['email'])): ?>
<div>Email : <?= h((string)$infos_entreprise['email']) ?></div>
<?php endif; ?>
<?php if (!empty($infos_entreprise['telephone'])): ?>
<div>Tél : <?= h((string)$infos_entreprise['telephone']) ?></div>
<?php endif; ?>
</div>
</div>
<div class="header-right">
<div class="header-date">Date : <?= date('d/m/Y') ?></div>
<div class="header-client">
<div class="header-client-title">Client</div>
<?php if (!empty($client['nom'])): ?><div><?= h((string)$client['nom']) ?></div><?php endif; ?>
<?php if (!empty($client['entreprise'])): ?><div><?= h((string)$client['entreprise']) ?></div><?php endif; ?>
<?php if (!empty($client['email'])): ?><div><?= h((string)$client['email']) ?></div><?php endif; ?>
<?php if (!empty($client['telephone'])): ?><div><?= h((string)$client['telephone']) ?></div><?php endif; ?>
<?php if (!empty($client['rue']) || !empty($client['code_postal']) || !empty($client['ville'])): ?>
<div><?= h((string)($client['rue'] ?? '')) ?><?php if (!empty($client['rue'])): ?>, <?php endif; ?><?= h((string)($client['code_postal'] ?? '')) ?> <?= h((string)($client['ville'] ?? '')) ?></div>
<?php endif; ?>
</div>
</div>
</div>
<?php
$displayNumero = $devis['numero'] ?? '';
if ($displayNumero === '' || $displayNumero === null) {
$displayYear = date('Y', strtotime($devis['date_devis'] ?? 'now'));
$prefix = isset($infos_gestion['prefixe_devis']) && $infos_gestion['prefixe_devis'] !== '' ? $infos_gestion['prefixe_devis'] : 'D-{year}-00';
$resolved = str_replace('{year}', $displayYear, $prefix);
$displayNumero = $resolved . (string)($devis['id'] ?? '');
}
?>
<h1>Devis n° <?= h((string)$displayNumero) ?></h1>
<?php if ($devis_lignes && count($devis_lignes)): ?>
<table class="devis-lines">
<thead>
<tr>
<th>Intitulé</th>
<th>Quantité</th>
<th>Prix unitaire HT</th>
<th>Remise</th>
<th>Type</th>
<th>Total</th>
</tr>
</thead>
<tbody>
<?php foreach ($devis_lignes as $ligne): ?>
<tr>
<td><?= nl2br(h((string)($ligne['intitule'] ?? ''))) ?></td>
<td style="text-align:right;"><?= h((string)($ligne['quantite'] ?? '')) ?></td>
<td style="text-align:right;"><?= number_format((float)($ligne['prix_unitaire'] ?? 0), 2, ',', ' ') ?> €</td>
<td style="text-align:right;">
<?php
$rem = isset($ligne['remise']) ? (float)$ligne['remise'] : 0.0;
if ($rem != 0.0) {
echo number_format($rem, 2, ',', ' ');
}
?>
</td>
<td style="text-align:center;">
<?php
$rem = isset($ligne['remise']) ? (float)$ligne['remise'] : 0.0;
if ($rem != 0.0) {
echo (isset($ligne['type_remise']) && $ligne['type_remise'] === 'euro') ? '€' : '%';
}
?>
</td>
<td style="text-align:right;">
<?php
$q = isset($ligne['quantite']) ? (float)$ligne['quantite'] : 1.0;
$pu = isset($ligne['prix_unitaire']) ? (float)$ligne['prix_unitaire'] : 0.0;
$rem = isset($ligne['remise']) ? (float)$ligne['remise'] : 0.0;
$typeRem = $ligne['type_remise'] ?? 'euro';
$totalLigne = $q * $pu;
if ($rem != 0.0) {
if ($typeRem === 'euro') {
$totalLigne -= $rem;
} else {
$totalLigne -= ($totalLigne * $rem / 100.0);
}
}
if ($totalLigne < 0) {
$totalLigne = 0.0;
}
echo number_format($totalLigne, 2, ',', ' ');
?> €
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<div class="totals-box-wrapper">
<div class="totals-box">
<div class="totals-box-row"><strong>Montant HT :</strong> <?= number_format($total, 2, ',', ' ') ?> €</div>
<?php if ($manualDepositAmount > 0): ?>
<div class="totals-box-row"><strong>Acompte manuel du devis :</strong> <?= number_format($manualDepositAmount, 2, ',', ' ') ?> €</div>
<?php endif; ?>
<?php if ($acompte40 > 0 && in_array('acompte_40', $onlineAllowedPayments, true)): ?>
<div class="totals-box-row"><strong>Acompte 40% (indicatif) :</strong> <?= number_format($acompte40, 2, ',', ' ') ?> €</div>
<?php endif; ?>
</div>
</div>
<?php else: ?>
<p style="color:#888;">Aucune ligne de devis.</p>
<?php endif; ?>
<?php if ($errors): ?>
<div class="error"><?php foreach ($errors as $e) { echo h($e) . '<br>'; } ?></div>
<?php endif; ?>
<form method="post">
<div class="choices">
<p class="payment-title"><strong>Choisissez votre mode de paiement :</strong></p>
<?php
// Affiche uniquement les options en ligne autorisées
$monthsForView = $subscriptionMonthsView ?? 12;
$monthly = $total > 0 && $monthsForView > 0 ? ($total / $monthsForView) : 0.0;
if ($allowBank) {
echo '<label><input type="radio" name="payment_choice" value="bank"> Paiement par virement ou chèque</label>';
}
if (in_array('acompte_manual', $onlineAllowedPayments, true) && $manualDepositAmount > 0) {
$manualDetail = $acompteTypeForSignature === 'pourcent'
? ' (' . number_format($acompteValeurForSignature, 2, ',', ' ') . '%)'
: '';
echo '<label><input type="radio" name="payment_choice" value="online_acompte_manual"> Payer en ligne - acompte manuel du devis'.$manualDetail.' ('.number_format($manualDepositAmount, 2, ',', ' ').' €)</label>';
}
if (in_array('acompte_40', $onlineAllowedPayments, true)) {
echo '<label><input type="radio" name="payment_choice" value="online_acompte_40"> Payer en ligne - acompte 40% ('.number_format($acompte40, 2, ',', ' ').' €)</label>';
}
if (in_array('full_ht', $onlineAllowedPayments, true)) {
echo '<label><input type="radio" name="payment_choice" value="online_full_ht"> Payer en ligne - 100% du montant HT ('.number_format($total, 2, ',', ' ').' €)</label>';
}
if (in_array('abonnement_auto', $onlineAllowedPayments, true) && $monthly > 0) {
$labelMois = $monthsForView . ' mois';
echo '<label><input type="radio" name="payment_choice" value="online_abonnement_auto"> Payer en ligne - abonnement mensuel (environ '.number_format($monthly, 2, ',', ' ').' € / mois sur '.$labelMois.')</label>';
}
?>
</div>
<div class="consent-block">
<label><input type="checkbox" name="accept_conditions" value="1"> J'accepte les conditions de ce devis.</label>
</div>
<p style="margin-top:14px;"><button type="submit">Signer le devis</button></p>
<?php if ($mentionBasDevis !== ''): ?>
<p style="margin-top:16px;text-align:center;font-size:0.9em;color:#4b5563;"><?= nl2br(h($mentionBasDevis)) ?></p>
<?php endif; ?>
</form>
<?php endif; ?>
</div>
</body>
</html>