fork download
  1. <?php
  2. /**
  3.  * Single-file French Quote & Invoice Generator
  4.  *
  5.  * This script handles two things:
  6.  * 1. If accessed via a POST request, it generates a PDF.
  7.  * 2. If accessed via a GET request, it displays the HTML interface.
  8.  */
  9.  
  10. if ($_SERVER['REQUEST_METHOD'] === 'POST') {
  11. // --- MODE 1: PDF GENERATION ---
  12.  
  13. require('lib/fpdf/fpdf.php');
  14.  
  15. // We need a custom class to create a header and footer
  16. class PDF extends FPDF
  17. {
  18. private $docData;
  19.  
  20. function __construct($orientation = 'P', $unit = 'mm', $size = 'A4', $data = []) {
  21. parent::__construct($orientation, $unit, $size);
  22. $this->docData = $data;
  23. }
  24.  
  25. function Header() {
  26. $this->SetFont('Arial', 'B', 20);
  27. $this->Cell(0, 10, utf8_decode(strtoupper($this->docData['doc_type'])), 0, 1, 'L');
  28. $this->SetFont('Arial', '', 12);
  29. $this->Cell(0, 7, utf8_decode($this->docData['doc']['number']), 0, 1, 'L');
  30. $this->Ln(15);
  31.  
  32. $this->SetFont('Arial', 'B', 10);
  33. $this->Cell(95, 7, utf8_decode($this->docData['company']['name']), 0, 0, 'L');
  34. $this->Cell(95, 7, utf8_decode($this->docData['client']['name']), 0, 1, 'R');
  35.  
  36. $this->SetFont('Arial', '', 10);
  37. $yPos = $this->GetY();
  38. $this->MultiCell(95, 5, utf8_decode($this->docData['company']['address']), 0, 'L');
  39. $this->SetXY(115, $yPos); // Set X to the right column
  40. $this->MultiCell(85, 5, utf8_decode($this->docData['client']['address']), 0, 'L');
  41.  
  42. // Use GetY from the longest MultiCell to set the next position correctly
  43. $yPosAfterAddress = $this->GetY();
  44.  
  45. $this->SetY($yPosAfterAddress);
  46. $this->Ln(2);
  47. if(!empty($this->docData['company']['siret'])) $this->Cell(95, 5, utf8_decode('SIRET : ' . $this->docData['company']['siret']), 0, 1, 'L');
  48. if(!empty($this->docData['company']['vat'])) $this->Cell(95, 5, utf8_decode('N° TVA : ' . $this->docData['company']['vat']), 0, 1, 'L');
  49.  
  50. $this->Ln(10);
  51. $this->SetFont('Arial', '', 10);
  52. $this->Cell(0, 5, utf8_decode('Date d\'émission : ' . date("d/m/Y", strtotime($this->docData['doc']['date']))), 0, 1, 'R');
  53. if (!empty($this->docData['doc']['due_date'])) {
  54. $this->Cell(0, 5, utf8_decode('Date d\'échéance : ' . date("d/m/Y", strtotime($this->docData['doc']['due_date']))), 0, 1, 'R');
  55. }
  56. $this->Ln(15);
  57. }
  58.  
  59. function Footer() {
  60. $this->SetY(-30);
  61. if (!empty($this->docData['notes'])) {
  62. $this->SetFont('Arial','',9);
  63. $this->Cell(0, 5, 'Notes :', 0, 1, 'L');
  64. $this->MultiCell(0, 5, utf8_decode($this->docData['notes']), 0, 'L');
  65. }
  66. $this->SetY(-15);
  67. $this->SetFont('Arial','I',8);
  68. $this->Cell(0,10, 'Page '.$this->PageNo().'/{nb}',0,0,'C');
  69. }
  70. }
  71.  
  72. $json = file_get_contents('php://input');
  73. $data = json_decode($json, true);
  74.  
  75. if ($data === null) {
  76. http_response_code(400);
  77. die('Invalid JSON');
  78. }
  79.  
  80. $pdf = new PDF('P', 'mm', 'A4', $data);
  81. $pdf->AliasNbPages();
  82. $pdf->AddPage();
  83.  
  84. $pdf->SetFont('Arial', 'B', 10);
  85. $pdf->SetFillColor(230, 230, 230);
  86. $pdf->Cell(100, 8, 'Description', 1, 0, 'L', true);
  87. $pdf->Cell(20, 8, utf8_decode('Qté'), 1, 0, 'C', true);
  88. $pdf->Cell(35, 8, 'P.U. HT', 1, 0, 'C', true);
  89. $pdf->Cell(35, 8, 'Total HT', 1, 1, 'C', true);
  90.  
  91. $pdf->SetFont('Arial', '', 10);
  92. $currencySymbol = utf8_decode('€');
  93. foreach ($data['items'] as $item) {
  94. $pdf->Cell(100, 8, utf8_decode($item['description']), 1, 0, 'L');
  95. $pdf->Cell(20, 8, $item['quantity'], 1, 0, 'R');
  96. $pdf->Cell(35, 8, number_format((float)$item['price'], 2, ',', ' ') . ' ' . $currencySymbol, 1, 0, 'R');
  97. $pdf->Cell(35, 8, number_format((float)$item['total'], 2, ',', ' ') . ' ' . $currencySymbol, 1, 1, 'R');
  98. }
  99.  
  100. $pdf->Ln(10);
  101. $pdf->SetFont('Arial', '', 10);
  102. $totalsX = 120;
  103. $totalsLabelWidth = 35;
  104. $totalsValueWidth = 45;
  105.  
  106. $pdf->Cell($totalsX, 8, '', 0, 0);
  107. $pdf->Cell($totalsLabelWidth, 8, 'Total HT', 1, 0, 'L');
  108. $pdf->Cell($totalsValueWidth, 8, number_format((float)$data['totals']['subtotal'], 2, ',', ' ') . ' ' . $currencySymbol, 1, 1, 'R');
  109.  
  110. $pdf->Cell($totalsX, 8, '', 0, 0);
  111. $pdf->Cell($totalsLabelWidth, 8, 'TVA (' . $data['totals']['vat_rate'] . '%)', 1, 0, 'L');
  112. $pdf->Cell($totalsValueWidth, 8, number_format((float)$data['totals']['vat_total'], 2, ',', ' ') . ' ' . $currencySymbol, 1, 1, 'R');
  113.  
  114. $pdf->SetFont('Arial', 'B', 12);
  115. $pdf->Cell($totalsX, 10, '', 0, 0);
  116. $pdf->Cell($totalsLabelWidth, 10, 'Total TTC', 1, 0, 'L', true);
  117. $pdf->Cell($totalsValueWidth, 10, number_format((float)$data['totals']['total_ttc'], 2, ',', ' ') . ' ' . $currencySymbol, 1, 1, 'R', true);
  118.  
  119. $filename = strtoupper($data['doc_type']) . '-' . preg_replace('/[^a-zA-Z0-9-]/', '', $data['doc']['number']) . '.pdf';
  120. $pdf->Output('D', $filename);
  121.  
  122. // Stop execution to prevent HTML from being sent
  123. }
  124.  
  125. // --- MODE 2: HTML INTERFACE ---
  126. ?>
  127. <!DOCTYPE html>
  128. <html lang="fr" data-theme="light">
  129. <head>
  130. <meta charset="UTF-8">
  131. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  132. <title>Générateur de Devis et Factures</title>
  133. <link rel="stylesheet" href="https://c...content-available-to-author-only...r.net/npm/@picocss/pico@1/css/pico.min.css">
  134. <style>
  135. /* Embedded custom CSS */
  136. body {
  137. padding-bottom: 5rem;
  138. }
  139. .table-container {
  140. overflow-x: auto;
  141. }
  142. table th:last-child,
  143. table td:last-child {
  144. text-align: right;
  145. }
  146. table input[type="number"] {
  147. min-width: 80px;
  148. text-align: right;
  149. }
  150. .totals-section {
  151. text-align: right;
  152. padding-top: 1rem;
  153. border-left: 1px solid var(--pico-muted-border-color);
  154. padding-left: 1rem;
  155. }
  156. .totals-section p {
  157. margin-bottom: 0.5rem;
  158. }
  159. .totals-section strong {
  160. margin-right: 1rem;
  161. }
  162. .notes-section {
  163. padding-right: 1rem;
  164. }
  165. .form-actions {
  166. margin-top: 2rem;
  167. display: flex;
  168. justify-content: flex-end;
  169. gap: 1rem;
  170. }
  171. .remove-item {
  172. padding: 0.25rem 0.5rem;
  173. line-height: 1;
  174. }
  175. </style>
  176. </head>
  177. <body>
  178. <main class="container">
  179. <header>
  180. <h1>Générateur de Devis & Factures</h1>
  181. <p>Toutes les données sont sauvegardées localement dans votre navigateur. Aucune information n'est envoyée à un serveur.</p>
  182. </header>
  183.  
  184. <form id="invoice-form">
  185. <fieldset>
  186. <legend>Type de document</legend>
  187. <label for="doc-type-quote">
  188. <input type="radio" id="doc-type-quote" name="doc_type" value="Devis" checked>
  189. Devis
  190. </label>
  191. <label for="doc-type-invoice">
  192. <input type="radio" id="doc-type-invoice" name="doc_type" value="Facture">
  193. Facture
  194. </label>
  195. </fieldset>
  196.  
  197. <div class="grid">
  198. <article>
  199. <h3 id="company-title">Votre Entreprise</h3>
  200. <label for="company_name">Nom de l'entreprise</label>
  201. <input type="text" id="company_name" name="company_name" required>
  202. <label for="company_address">Adresse</label>
  203. <textarea id="company_address" name="company_address" rows="3"></textarea>
  204. <div class="grid">
  205. <label for="company_siret">SIRET <input type="text" id="company_siret" name="company_siret"></label>
  206. <label for="company_vat">N° TVA <input type="text" id="company_vat" name="company_vat"></label>
  207. </div>
  208. <button type="button" id="save-company-info" class="secondary">Enregistrer mes informations</button>
  209. </article>
  210.  
  211. <article>
  212. <h3>Client</h3>
  213. <label for="client_name">Nom du client</label>
  214. <input type="text" id="client_name" name="client_name" required>
  215. <label for="client_address">Adresse du client</label>
  216. <textarea id="client_address" name="client_address" rows="3"></textarea>
  217. </article>
  218. </div>
  219.  
  220. <article>
  221. <div class="grid">
  222. <label for="doc_number"><span id="doc-type-label">Numéro de Devis</span>
  223. <input type="text" id="doc_number" name="doc_number" required>
  224. </label>
  225. <label for="doc_date">Date
  226. <input type="date" id="doc_date" name="doc_date" required>
  227. </label>
  228. <label for="doc_due_date">Date d'échéance
  229. <input type="date" id="doc_due_date" name="doc_due_date">
  230. </label>
  231. </div>
  232. </article>
  233.  
  234. <article>
  235. <h3>Lignes de prestation</h3>
  236. <div class="table-container">
  237. <table>
  238. <thead>
  239. <tr>
  240. <th>Description</th>
  241. <th>Qté</th>
  242. <th>P.U. HT</th>
  243. <th>Total HT</th>
  244. <th></th>
  245. </tr>
  246. </thead>
  247. <tbody id="item-list"></tbody>
  248. </table>
  249. </div>
  250. <button type="button" id="add-item" class="secondary">Ajouter une ligne</button>
  251. </article>
  252.  
  253. <div class="grid">
  254. <div class="notes-section">
  255. <label for="notes">Notes / Conditions de paiement</label>
  256. <textarea id="notes" name="notes" rows="4">Paiement à réception de la facture.</textarea>
  257. </div>
  258. <article class="totals-section">
  259. <div class="grid">
  260. <label for="vat_rate">Taux de TVA (%)</label>
  261. <input type="number" id="vat_rate" name="vat_rate" value="20" step="0.1" required>
  262. </div>
  263. <p><strong>Total HT :</strong> <span id="subtotal">0.00</span> €</p>
  264. <p><strong>TVA :</strong> <span id="vat-total">0.00</span> €</p>
  265. <p><strong>Total TTC :</strong> <span id="total-ttc">0.00</span> €</p>
  266. </article>
  267. </div>
  268.  
  269. <footer class="form-actions">
  270. <button type="submit" id="generate-pdf">Générer le PDF</button>
  271. <button type="button" id="reset-form" class="secondary outline">Réinitialiser</button>
  272. </footer>
  273. </form>
  274. </main>
  275.  
  276. <script>
  277. // --- Embedded JavaScript ---
  278. document.addEventListener('DOMContentLoaded', () => {
  279. const form = document.getElementById('invoice-form');
  280. const itemList = document.getElementById('item-list');
  281. const addItemBtn = document.getElementById('add-item');
  282. const saveCompanyInfoBtn = document.getElementById('save-company-info');
  283. const resetFormBtn = document.getElementById('reset-form');
  284. const subtotalEl = document.getElementById('subtotal');
  285. const vatTotalEl = document.getElementById('vat-total');
  286. const totalTtcEl = document.getElementById('total-ttc');
  287. const docTypeRadios = document.querySelectorAll('input[name="doc_type"]');
  288. const docTypeLabel = document.getElementById('doc-type-label');
  289. const docNumberInput = document.getElementById('doc_number');
  290. const companyTitle = document.getElementById('company-title');
  291.  
  292. const calculateTotals = () => {
  293. let subtotal = 0;
  294. itemList.querySelectorAll('tr').forEach(row => {
  295. const quantity = parseFloat(row.querySelector('.quantity').value) || 0;
  296. const price = parseFloat(row.querySelector('.price').value) || 0;
  297. const rowTotal = quantity * price;
  298. row.querySelector('.row-total').textContent = rowTotal.toFixed(2);
  299. subtotal += rowTotal;
  300. });
  301. const vatRate = parseFloat(document.getElementById('vat_rate').value) || 0;
  302. const vatTotal = subtotal * (vatRate / 100);
  303. const totalTtc = subtotal + vatTotal;
  304. subtotalEl.textContent = subtotal.toFixed(2);
  305. vatTotalEl.textContent = vatTotal.toFixed(2);
  306. totalTtcEl.textContent = totalTtc.toFixed(2);
  307. };
  308.  
  309. const addLineItem = () => {
  310. const row = document.createElement('tr');
  311. row.innerHTML = `
  312. <td><input type="text" class="description" placeholder="Description de la prestation"></td>
  313. <td><input type="number" class="quantity" value="1" step="any"></td>
  314. <td><input type="number" class="price" value="0.00" step="any"></td>
  315. <td><span class="row-total">0.00</span> €</td>
  316. <td><button type="button" class="remove-item secondary outline">×</button></td>
  317. `;
  318. itemList.appendChild(row);
  319. row.querySelector('.remove-item').addEventListener('click', () => {
  320. row.remove();
  321. calculateTotals();
  322. });
  323. };
  324.  
  325. const updateDocType = () => {
  326. const selectedType = document.querySelector('input[name="doc_type"]:checked').value;
  327. docTypeLabel.textContent = `Numéro de ${selectedType}`;
  328. const prefix = selectedType === 'Devis' ? 'DE' : 'FA';
  329. const currentVal = docNumberInput.value;
  330. if (!currentVal.startsWith('DE-') && !currentVal.startsWith('FA-') || currentVal === '') {
  331. const year = new Date().getFullYear();
  332. docNumberInput.value = `${prefix}-${year}-001`;
  333. }
  334. };
  335.  
  336. const saveCompanyInfo = () => {
  337. const companyInfo = {
  338. name: document.getElementById('company_name').value,
  339. address: document.getElementById('company_address').value,
  340. siret: document.getElementById('company_siret').value,
  341. vat: document.getElementById('company_vat').value,
  342. };
  343. localStorage.setItem('companyInfo', JSON.stringify(companyInfo));
  344. companyTitle.textContent = 'Votre Entreprise (Enregistré)';
  345. setTimeout(() => companyTitle.textContent = 'Votre Entreprise', 2000);
  346. };
  347.  
  348. const loadCompanyInfo = () => {
  349. const companyInfo = JSON.parse(localStorage.getItem('companyInfo'));
  350. if (companyInfo) {
  351. document.getElementById('company_name').value = companyInfo.name || '';
  352. document.getElementById('company_address').value = companyInfo.address || '';
  353. document.getElementById('company_siret').value = companyInfo.siret || '';
  354. document.getElementById('company_vat').value = companyInfo.vat || '';
  355. }
  356. };
  357.  
  358. const resetForm = () => {
  359. if(confirm("Voulez-vous vraiment réinitialiser le formulaire ? Les informations de votre entreprise resteront enregistrées.")) {
  360. const companyInfo = JSON.parse(localStorage.getItem('companyInfo'));
  361. form.reset();
  362. localStorage.setItem('companyInfo', JSON.stringify(companyInfo));
  363. loadCompanyInfo();
  364. itemList.innerHTML = '';
  365. addLineItem();
  366. document.getElementById('doc_date').valueAsDate = new Date();
  367. updateDocType();
  368. calculateTotals();
  369. }
  370. }
  371.  
  372. const generatePDF = async (e) => {
  373. e.preventDefault();
  374. const items = Array.from(itemList.querySelectorAll('tr')).map(row => ({
  375. description: row.querySelector('.description').value,
  376. quantity: row.querySelector('.quantity').value,
  377. price: row.querySelector('.price').value,
  378. total: parseFloat(row.querySelector('.row-total').textContent)
  379. }));
  380. const formData = {
  381. doc_type: document.querySelector('input[name="doc_type"]:checked').value,
  382. company: { name: document.getElementById('company_name').value, address: document.getElementById('company_address').value, siret: document.getElementById('company_siret').value, vat: document.getElementById('company_vat').value },
  383. client: { name: document.getElementById('client_name').value, address: document.getElementById('client_address').value },
  384. doc: { number: document.getElementById('doc_number').value, date: document.getElementById('doc_date').value, due_date: document.getElementById('doc_due_date').value },
  385. items: items,
  386. totals: { subtotal: subtotalEl.textContent, vat_rate: document.getElementById('vat_rate').value, vat_total: vatTotalEl.textContent, total_ttc: totalTtcEl.textContent },
  387. notes: document.getElementById('notes').value
  388. };
  389. const pdfButton = document.getElementById('generate-pdf');
  390. pdfButton.setAttribute('aria-busy', 'true');
  391. pdfButton.textContent = 'Génération...';
  392. try {
  393. const response = await fetch('', { // Post to the same file
  394. method: 'POST',
  395. headers: { 'Content-Type': 'application/json' },
  396. body: JSON.stringify(formData)
  397. });
  398. if (!response.ok) throw new Error(`Erreur du serveur: ${response.statusText}`);
  399. const blob = await response.blob();
  400. const url = window.URL.createObjectURL(blob);
  401. const a = document.createElement('a');
  402. a.style.display = 'none';
  403. a.href = url;
  404. a.download = `${formData.doc_type.toUpperCase()}-${formData.doc.number.replace(/[^a-zA-Z0-9-]/g, '')}.pdf`;
  405. document.body.appendChild(a);
  406. a.click();
  407. window.URL.revokeObjectURL(url);
  408. a.remove();
  409. } catch (error) {
  410. console.error('Erreur lors de la génération du PDF:', error);
  411. alert('Une erreur est survenue lors de la génération du PDF.');
  412. } finally {
  413. pdfButton.removeAttribute('aria-busy');
  414. pdfButton.textContent = 'Générer le PDF';
  415. }
  416. };
  417.  
  418. addItemBtn.addEventListener('click', addLineItem);
  419. form.addEventListener('input', calculateTotals);
  420. form.addEventListener('submit', generatePDF);
  421. saveCompanyInfoBtn.addEventListener('click', saveCompanyInfo);
  422. resetFormBtn.addEventListener('click', resetForm);
  423. docTypeRadios.forEach(radio => radio.addEventListener('change', updateDocType));
  424.  
  425. // --- Initialisation ---
  426. loadCompanyInfo();
  427. addLineItem();
  428. calculateTotals();
  429. document.getElementById('doc_date').valueAsDate = new Date();
  430. updateDocType();
  431. });
  432. </script>
  433. </body>
  434. </html>
Success #stdin #stdout #stderr 0.03s 25652KB
stdin
Standard input is empty
stdout
<!DOCTYPE html>
<html lang="fr" data-theme="light">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Générateur de Devis et Factures</title>
    <link rel="stylesheet" href="https://c...content-available-to-author-only...r.net/npm/@picocss/pico@1/css/pico.min.css">
    <style>
        /* Embedded custom CSS */
        body {
            padding-bottom: 5rem;
        }
        .table-container {
            overflow-x: auto;
        }
        table th:last-child,
        table td:last-child {
            text-align: right;
        }
        table input[type="number"] {
            min-width: 80px;
            text-align: right;
        }
        .totals-section {
            text-align: right;
            padding-top: 1rem;
            border-left: 1px solid var(--pico-muted-border-color);
            padding-left: 1rem;
        }
        .totals-section p {
            margin-bottom: 0.5rem;
        }
        .totals-section strong {
            margin-right: 1rem;
        }
        .notes-section {
            padding-right: 1rem;
        }
        .form-actions {
            margin-top: 2rem;
            display: flex;
            justify-content: flex-end;
            gap: 1rem;
        }
        .remove-item {
            padding: 0.25rem 0.5rem;
            line-height: 1;
        }
    </style>
</head>
<body>
    <main class="container">
        <header>
            <h1>Générateur de Devis & Factures</h1>
            <p>Toutes les données sont sauvegardées localement dans votre navigateur. Aucune information n'est envoyée à un serveur.</p>
        </header>

        <form id="invoice-form">
            <fieldset>
                <legend>Type de document</legend>
                <label for="doc-type-quote">
                    <input type="radio" id="doc-type-quote" name="doc_type" value="Devis" checked>
                    Devis
                </label>
                <label for="doc-type-invoice">
                    <input type="radio" id="doc-type-invoice" name="doc_type" value="Facture">
                    Facture
                </label>
            </fieldset>

            <div class="grid">
                <article>
                    <h3 id="company-title">Votre Entreprise</h3>
                    <label for="company_name">Nom de l'entreprise</label>
                    <input type="text" id="company_name" name="company_name" required>
                    <label for="company_address">Adresse</label>
                    <textarea id="company_address" name="company_address" rows="3"></textarea>
                    <div class="grid">
                        <label for="company_siret">SIRET <input type="text" id="company_siret" name="company_siret"></label>
                        <label for="company_vat">N° TVA <input type="text" id="company_vat" name="company_vat"></label>
                    </div>
                     <button type="button" id="save-company-info" class="secondary">Enregistrer mes informations</button>
                </article>

                <article>
                    <h3>Client</h3>
                    <label for="client_name">Nom du client</label>
                    <input type="text" id="client_name" name="client_name" required>
                    <label for="client_address">Adresse du client</label>
                    <textarea id="client_address" name="client_address" rows="3"></textarea>
                </article>
            </div>
            
            <article>
                <div class="grid">
                    <label for="doc_number"><span id="doc-type-label">Numéro de Devis</span>
                        <input type="text" id="doc_number" name="doc_number" required>
                    </label>
                    <label for="doc_date">Date
                        <input type="date" id="doc_date" name="doc_date" required>
                    </label>
                    <label for="doc_due_date">Date d'échéance
                        <input type="date" id="doc_due_date" name="doc_due_date">
                    </label>
                </div>
            </article>

            <article>
                <h3>Lignes de prestation</h3>
                <div class="table-container">
                    <table>
                        <thead>
                            <tr>
                                <th>Description</th>
                                <th>Qté</th>
                                <th>P.U. HT</th>
                                <th>Total HT</th>
                                <th></th>
                            </tr>
                        </thead>
                        <tbody id="item-list"></tbody>
                    </table>
                </div>
                <button type="button" id="add-item" class="secondary">Ajouter une ligne</button>
            </article>

            <div class="grid">
                <div class="notes-section">
                     <label for="notes">Notes / Conditions de paiement</label>
                     <textarea id="notes" name="notes" rows="4">Paiement à réception de la facture.</textarea>
                </div>
                <article class="totals-section">
                    <div class="grid">
                        <label for="vat_rate">Taux de TVA (%)</label>
                        <input type="number" id="vat_rate" name="vat_rate" value="20" step="0.1" required>
                    </div>
                    <p><strong>Total HT :</strong> <span id="subtotal">0.00</span> €</p>
                    <p><strong>TVA :</strong> <span id="vat-total">0.00</span> €</p>
                    <p><strong>Total TTC :</strong> <span id="total-ttc">0.00</span> €</p>
                </article>
            </div>

            <footer class="form-actions">
                <button type="submit" id="generate-pdf">Générer le PDF</button>
                <button type="button" id="reset-form" class="secondary outline">Réinitialiser</button>
            </footer>
        </form>
    </main>

    <script>
        // --- Embedded JavaScript ---
        document.addEventListener('DOMContentLoaded', () => {
            const form = document.getElementById('invoice-form');
            const itemList = document.getElementById('item-list');
            const addItemBtn = document.getElementById('add-item');
            const saveCompanyInfoBtn = document.getElementById('save-company-info');
            const resetFormBtn = document.getElementById('reset-form');
            const subtotalEl = document.getElementById('subtotal');
            const vatTotalEl = document.getElementById('vat-total');
            const totalTtcEl = document.getElementById('total-ttc');
            const docTypeRadios = document.querySelectorAll('input[name="doc_type"]');
            const docTypeLabel = document.getElementById('doc-type-label');
            const docNumberInput = document.getElementById('doc_number');
            const companyTitle = document.getElementById('company-title');

            const calculateTotals = () => {
                let subtotal = 0;
                itemList.querySelectorAll('tr').forEach(row => {
                    const quantity = parseFloat(row.querySelector('.quantity').value) || 0;
                    const price = parseFloat(row.querySelector('.price').value) || 0;
                    const rowTotal = quantity * price;
                    row.querySelector('.row-total').textContent = rowTotal.toFixed(2);
                    subtotal += rowTotal;
                });
                const vatRate = parseFloat(document.getElementById('vat_rate').value) || 0;
                const vatTotal = subtotal * (vatRate / 100);
                const totalTtc = subtotal + vatTotal;
                subtotalEl.textContent = subtotal.toFixed(2);
                vatTotalEl.textContent = vatTotal.toFixed(2);
                totalTtcEl.textContent = totalTtc.toFixed(2);
            };

            const addLineItem = () => {
                const row = document.createElement('tr');
                row.innerHTML = `
                    <td><input type="text" class="description" placeholder="Description de la prestation"></td>
                    <td><input type="number" class="quantity" value="1" step="any"></td>
                    <td><input type="number" class="price" value="0.00" step="any"></td>
                    <td><span class="row-total">0.00</span> €</td>
                    <td><button type="button" class="remove-item secondary outline">×</button></td>
                `;
                itemList.appendChild(row);
                row.querySelector('.remove-item').addEventListener('click', () => {
                    row.remove();
                    calculateTotals();
                });
            };

            const updateDocType = () => {
                const selectedType = document.querySelector('input[name="doc_type"]:checked').value;
                docTypeLabel.textContent = `Numéro de ${selectedType}`;
                const prefix = selectedType === 'Devis' ? 'DE' : 'FA';
                const currentVal = docNumberInput.value;
                if (!currentVal.startsWith('DE-') && !currentVal.startsWith('FA-') || currentVal === '') {
                    const year = new Date().getFullYear();
                    docNumberInput.value = `${prefix}-${year}-001`;
                }
            };

            const saveCompanyInfo = () => {
                const companyInfo = {
                    name: document.getElementById('company_name').value,
                    address: document.getElementById('company_address').value,
                    siret: document.getElementById('company_siret').value,
                    vat: document.getElementById('company_vat').value,
                };
                localStorage.setItem('companyInfo', JSON.stringify(companyInfo));
                companyTitle.textContent = 'Votre Entreprise (Enregistré)';
                setTimeout(() => companyTitle.textContent = 'Votre Entreprise', 2000);
            };

            const loadCompanyInfo = () => {
                const companyInfo = JSON.parse(localStorage.getItem('companyInfo'));
                if (companyInfo) {
                    document.getElementById('company_name').value = companyInfo.name || '';
                    document.getElementById('company_address').value = companyInfo.address || '';
                    document.getElementById('company_siret').value = companyInfo.siret || '';
                    document.getElementById('company_vat').value = companyInfo.vat || '';
                }
            };
            
            const resetForm = () => {
                if(confirm("Voulez-vous vraiment réinitialiser le formulaire ? Les informations de votre entreprise resteront enregistrées.")) {
                    const companyInfo = JSON.parse(localStorage.getItem('companyInfo'));
                    form.reset();
                    localStorage.setItem('companyInfo', JSON.stringify(companyInfo));
                    loadCompanyInfo();
                    itemList.innerHTML = '';
                    addLineItem();
                    document.getElementById('doc_date').valueAsDate = new Date();
                    updateDocType();
                    calculateTotals();
                }
            }

            const generatePDF = async (e) => {
                e.preventDefault();
                const items = Array.from(itemList.querySelectorAll('tr')).map(row => ({
                    description: row.querySelector('.description').value,
                    quantity: row.querySelector('.quantity').value,
                    price: row.querySelector('.price').value,
                    total: parseFloat(row.querySelector('.row-total').textContent)
                }));
                const formData = {
                    doc_type: document.querySelector('input[name="doc_type"]:checked').value,
                    company: { name: document.getElementById('company_name').value, address: document.getElementById('company_address').value, siret: document.getElementById('company_siret').value, vat: document.getElementById('company_vat').value },
                    client: { name: document.getElementById('client_name').value, address: document.getElementById('client_address').value },
                    doc: { number: document.getElementById('doc_number').value, date: document.getElementById('doc_date').value, due_date: document.getElementById('doc_due_date').value },
                    items: items,
                    totals: { subtotal: subtotalEl.textContent, vat_rate: document.getElementById('vat_rate').value, vat_total: vatTotalEl.textContent, total_ttc: totalTtcEl.textContent },
                    notes: document.getElementById('notes').value
                };
                const pdfButton = document.getElementById('generate-pdf');
                pdfButton.setAttribute('aria-busy', 'true');
                pdfButton.textContent = 'Génération...';
                try {
                    const response = await fetch('', { // Post to the same file
                        method: 'POST',
                        headers: { 'Content-Type': 'application/json' },
                        body: JSON.stringify(formData)
                    });
                    if (!response.ok) throw new Error(`Erreur du serveur: ${response.statusText}`);
                    const blob = await response.blob();
                    const url = window.URL.createObjectURL(blob);
                    const a = document.createElement('a');
                    a.style.display = 'none';
                    a.href = url;
                    a.download = `${formData.doc_type.toUpperCase()}-${formData.doc.number.replace(/[^a-zA-Z0-9-]/g, '')}.pdf`;
                    document.body.appendChild(a);
                    a.click();
                    window.URL.revokeObjectURL(url);
                    a.remove();
                } catch (error) {
                    console.error('Erreur lors de la génération du PDF:', error);
                    alert('Une erreur est survenue lors de la génération du PDF.');
                } finally {
                    pdfButton.removeAttribute('aria-busy');
                    pdfButton.textContent = 'Générer le PDF';
                }
            };

            addItemBtn.addEventListener('click', addLineItem);
            form.addEventListener('input', calculateTotals);
            form.addEventListener('submit', generatePDF);
            saveCompanyInfoBtn.addEventListener('click', saveCompanyInfo);
            resetFormBtn.addEventListener('click', resetForm);
            docTypeRadios.forEach(radio => radio.addEventListener('change', updateDocType));

            // --- Initialisation ---
            loadCompanyInfo();
            addLineItem();
            calculateTotals();
            document.getElementById('doc_date').valueAsDate = new Date();
            updateDocType();
        });
    </script>
</body>
</html>
stderr
PHP Notice:  Undefined index: REQUEST_METHOD in /home/I6dqNV/prog.php on line 10