- 1<?php
- 2session_start();
- 3ini_set('display_errors', 1);
- 4ini_set('display_startup_errors', 1);
- 5error_reporting(E_ALL);
- 6
- 7require_once __DIR__ . '/../app/Support/helpers.php';
- 8require_once __DIR__ . '/../app/Core/Database.php';
- 9require_once __DIR__ . '/../app/Core/View.php';
- 10require_once __DIR__ . '/../app/Repositories/InvoiceRepository.php';
- 11require_once __DIR__ . '/../app/Services/VatCalculator.php';
- 12require_once __DIR__ . '/../app/Services/InvoiceBuilder.php';
- 13require_once __DIR__ . '/../app/Services/PdfService.php';
- 14require_once __DIR__ . '/../app/Services/ExcelGenerator.php';
- 15require_once __DIR__ . '/../app/Controllers/DashboardController.php';
- 16require_once __DIR__ . '/../app/Controllers/InvoiceController.php';
- 17
- 18use App\Core\Database;
- 19use App\Repositories\InvoiceRepository;
- 20use App\Services\VatCalculator;
- 21use App\Services\InvoiceBuilder;
- 22use App\Services\PdfService;
- 23use App\Services\ExcelGenerator;
- 24use App\Controllers\DashboardController;
- 25use App\Controllers\InvoiceController;
- 26
- 27$config = require __DIR__ . '/../config/database.php';
- 28$pdo = Database::connect($config);
- 29
- 30$repository = new InvoiceRepository($pdo);
- 31$calculator = new VatCalculator(0.20);
- 32$builder = new InvoiceBuilder($calculator);
- 33$pdfService = new PdfService();
- 34$excelService = new ExcelGenerator();
- 35$dashboardController = new DashboardController($repository);
- 36$invoiceController = new InvoiceController($repository, $calculator, $builder, $pdfService, $excelService);
- 37
- 38$action = $_GET['action'] ?? 'dashboard';
- 39$dashboardYear = isset($_GET['year']) ? (int)$_GET['year'] : (int)date('Y');
- 40
- 41if ($_SERVER['REQUEST_METHOD'] === 'POST') {
- 42 if ($action === 'store') $invoiceController->store();
- 43 if ($action === 'update') $invoiceController->update();
- 44 if ($action === 'form') {
- 45 include __DIR__ . '/../app/Views/invoices/form.php';
- 46 exit;
- 47}
- 48
- 49if ($action === 'edit') {
- 50 $edit = $repository->find((int)($_GET['id'] ?? 0));
- 51 include __DIR__ . '/../app/Views/invoices/form.php';
- 52 exit;
- 53}
- 54
- 55
- 56 if ($action === 'delete') $invoiceController->delete();
- 57 if ($action === 'tva') $invoiceController->selectedVat();
- 58}
- 59
- 60if ($action === 'pdf') {
- 61 $invoiceController->pdf();
- 62 exit;
- 63}
- 64
- 65if ($action === 'csv') {
- 66 // Filtres liste
- 67$listYear = isset($_GET['year']) ? (int)$_GET['year'] : (int)date('Y');
- 68$listPage = isset($_GET['page']) ? max(1,(int)$_GET['page']) : 1;
- 69$listSort = isset($_GET['sort']) ? $_GET['sort'] : 'date_desc';
- 70$listQuarter = isset($_GET['quarter']) ? trim($_GET['quarter']) : '';
- 71$listQ = isset($_GET['q']) ? trim($_GET['q']) : '';
- 72$perPage = 20;
- 73$totalInv = $repository->countSearch($listYear, $listQuarter, $listQ);
- 74$pages = max(1, (int)ceil($totalInv / $perPage));
- 75if ($listPage > $pages) $listPage = $pages;
- 76$invoices = $repository->search($listYear, $listPage, $perPage, $listSort, $listQuarter, $listQ);
- 77$availYears = $repository->availableYears();
- 78 header('Content-Type: text/csv; charset=UTF-8');
- 79 header('Content-Disposition: attachment; filename="factures.csv"');
- 80 $out = fopen('php://output', 'w');
- 81 fputcsv($out, ['FACTURE', 'NUMERO', 'DATE', 'TRIMESTRE', 'ANNEE'], ';');
- 82 fputcsv($out, ['LIGNE', 'DESIGNATION', 'QTE', 'PU HT', 'TOTAL HT'], ';');
- 83 fputcsv($out, [], ';');
- 84 foreach ($invoices as $inv) {
- 85 fputcsv($out, ['FACTURE', $inv['invoice_number'], $inv['invoice_date'], $inv['quarter'], $inv['year']], ';');
- 86 $it = $pdo->prepare('SELECT * FROM invoice_items WHERE invoice_id=:id ORDER BY id ASC');
- 87 $it->execute(['id' => $inv['id']]);
- 88 foreach ($it->fetchAll() as $item) {
- 89 fputcsv($out, ['LIGNE', $item['description'], $item['quantity'], $item['unit_price_ht'], $item['line_total_ht']], ';');
- 90 }
- 91 fputcsv($out, ['TOTAL HT', $inv['total_ht']], ';');
- 92 fputcsv($out, ['TVA 20%', $inv['total_vat']], ';');
- 93 fputcsv($out, ['TOTAL TTC', $inv['total_ttc']], ';');
- 94 fputcsv($out, [], ';');
- 95 }
- 96 fclose($out);
- 97 exit;
- 98}
- 99
- 100if ($action === 'excel') {
- 101 $xid = isset($_GET['id']) ? (int)$_GET['id'] : 0;
- 102 if ($xid > 0) {
- 103 ob_end_clean();
- 104 header('Location: /TV@/dlxls.php?id=' . $xid);
- 105 exit;
- 106 }
- 107 if (!empty($_POST['invoice_ids'])) {
- 108 ob_end_clean();
- 109 require_once dirname(__DIR__) . '/dlxls.php';
- 110 exit;
- 111 }
- 112 echo '<div class="container py-4"><div class="alert alert-warning">Selectionnez au moins une facture.</div></div>';
- 113 exit;
- 114}
- 115
- 116$invoices = $repository->all();
- 117$selectedTotals = $_SESSION['selected_totals'] ?? null;
- 118unset($_SESSION['selected_totals']);
- 119
- 120$edit = null;
- 121if ($action === 'edit') {
- 122 $edit = $repository->find((int)($_GET['id'] ?? 0));
- 123}
- 124
- 125$yearInvoices = array_values(array_filter($invoices, fn($inv) => (int)$inv['year'] === $dashboardYear));
- 126$yearTotals = ['c' => count($yearInvoices), 'ht' => 0, 'vat' => 0, 'ttc' => 0];
- 127foreach ($yearInvoices as $inv) {
- 128 $yearTotals['ht'] += (float)$inv['total_ht'];
- 129 $yearTotals['vat'] += (float)$inv['total_vat'];
- 130 $yearTotals['ttc'] += (float)$inv['total_ttc'];
- 131}
- 132$quarters = $repository->quarterSummary($dashboardYear);
- 133$latest = array_slice($yearInvoices, 0, 10);
- 134$nextNumber = $repository->nextNumber();
- 135?>
- 136<!doctype html>
- 137<html lang="fr">
- 138<head>
- 139<meta charset="utf-8">
- 140<meta name="viewport" content="width=device-width, initial-scale=1">
- 141<title>Facturation TVA</title>
- 142<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
- 143<link rel="stylesheet" href="assets/css/style.css">
- 144</head>
- 145<body class="bg-light">
- 146<div class="container py-4">
- 147 <div class="card card-soft mb-4">
- 148 <div class="card-body d-flex justify-content-between align-items-center flex-wrap gap-2">
- 149 <div>
- 150 <h1 class="h4 mb-1">Facturation TVA</h1>
- 151 <div class="text-muted">Dashboard, factures, TVA, PDF, CSV, Excel</div>
- 152 </div>
- 153 <div class="d-flex gap-2 flex-wrap">
- 154 <a href="?action=dashboard&year=<?= (int)$dashboardYear ?>" class="btn btn-outline-primary btn-sm">Dashboard</a>
- 155 <a href="?action=list" class="btn btn-outline-primary btn-sm">Factures</a>
- 156 <a href="?action=form" class="btn btn-primary btn-sm">Nouvelle facture</a>
- 157 <a href="?action=csv" class="btn btn-outline-success btn-sm">Export CSV</a>
- 158 <a href="?action=excel" class="btn btn-success btn-sm">Export Excel</a>
- 159 </div>
- 160 </div>
- 161 </div>
- 162
- 163 <?php if (!empty($_SESSION['success'])): ?>
- 164 <div class="alert alert-success"><?= e($_SESSION['success']); unset($_SESSION['success']); ?></div>
- 165 <?php endif; ?>
- 166
- 167 <?php if (!empty($_SESSION['error'])): ?>
- 168 <div class="alert alert-danger"><?= e($_SESSION['error']); unset($_SESSION['error']); ?></div>
- 169 <?php endif; ?>
- 170
- 171 <?php if ($action === 'dashboard'): ?>
- 172 <div class="card card-soft mb-4">
- 173 <div class="card-body">
- 174 <div class="d-flex justify-content-between align-items-center flex-wrap gap-2 mb-3">
- 175 <h2 class="h5 mb-0">Dashboard <?= (int)$dashboardYear ?></h2>
- 176 <form method="get" class="d-flex gap-2 align-items-center">
- 177 <input type="hidden" name="action" value="dashboard">
- 178 <select name="year" class="form-select form-select-sm" style="width:auto">
- 179 <?php for ($y = date('Y'); $y >= date('Y') - 5; $y--): ?>
- 180 <option value="<?= (int)$y ?>" <?= $dashboardYear == $y ? 'selected' : '' ?>><?= (int)$y ?></option>
- 181 <?php endfor; ?>
- 182 </select>
- 183 <button class="btn btn-sm btn-primary" type="submit">Voir</button>
- 184 </form>
- 185 </div>
- 186
- 187 <div class="row g-3 mb-4">
- 188 <div class="col-md-3"><div class="card card-soft"><div class="card-body"><div class="text-muted">Factures <?= (int)$dashboardYear ?></div><div class="fs-4 fw-bold"><?= (int)$yearTotals['c'] ?></div></div></div></div>
- 189 <div class="col-md-3"><div class="card card-soft"><div class="card-body"><div class="text-muted">Total HT</div><div class="fs-4 fw-bold"><?= money($yearTotals['ht']) ?></div></div></div></div>
- 190 <div class="col-md-3"><div class="card card-soft"><div class="card-body"><div class="text-muted">TVA</div><div class="fs-4 fw-bold"><?= money($yearTotals['vat']) ?></div></div></div></div>
- 191 <div class="col-md-3"><div class="card card-soft"><div class="card-body"><div class="text-muted">TTC</div><div class="fs-4 fw-bold"><?= money($yearTotals['ttc']) ?></div></div></div></div>
- 192 </div>
- 193
- 194 <div class="table-responsive">
- 195 <table class="table table-sm align-middle mb-0">
- 196 <thead><tr><th>Trimestre</th><th>Factures</th><th>HT</th><th>TVA</th><th>TTC</th></tr></thead>
- 197 <tbody>
- 198 <?php foreach($quarters as $q): ?>
- 199 <tr>
- 200 <td><?= e($q['quarter']) ?></td>
- 201 <td><?= (int)$q['c'] ?></td>
- 202 <td><?= money($q['ht']) ?></td>
- 203 <td><?= money($q['vat']) ?></td>
- 204 <td><?= money($q['ttc']) ?></td>
- 205 </tr>
- 206 <?php endforeach; ?>
- 207 </tbody>
- 208 </table>
- 209 </div>
- 210 </div>
- 211 </div>
- 212
- 213 <div class="card card-soft">
- 214 <div class="card-body">
- 215 <h2 class="h5 mb-3">Dernières factures de l’année <?= (int)$dashboardYear ?></h2>
- 216 <div class="table-responsive">
- 217 <table class="table table-sm align-middle">
- 218 <thead><tr><th>N°</th><th>Date</th><th>Trimestre</th><th>TVA</th><th>TTC</th></tr></thead>
- 219 <tbody>
- 220 <?php foreach($latest as $inv): ?>
- 221 <tr>
- 222 <td><?= e($inv['invoice_number']) ?></td>
- 223 <td><?= e($inv['invoice_date']) ?></td>
- 224 <td><?= e($inv['quarter']) ?></td>
- 225 <td><?= money($inv['total_vat']) ?></td>
- 226 <td><?= money($inv['total_ttc']) ?></td>
- 227 </tr>
- 228 <?php endforeach; ?>
- 229 </tbody>
- 230 </table>
- 231 </div>
- 232 </div>
- 233 </div>
- 234
- 235<?php elseif ($action === 'list'): ?>
- 236<?php
- 237$listYear = isset($_GET['year']) ? (int)$_GET['year'] : (int)date('Y');
- 238$listPage = isset($_GET['page']) ? max(1,(int)$_GET['page']) : 1;
- 239$listSort = isset($_GET['sort']) ? $_GET['sort'] : 'date_desc';
- 240$listQuarter = isset($_GET['quarter']) ? trim($_GET['quarter']) : '';
- 241$listQ = isset($_GET['q']) ? trim($_GET['q']) : '';
- 242$perPage = 20;
- 243$totalInv = $repository->countSearch($listYear, $listQuarter, $listQ);
- 244$pages = max(1,(int)ceil($totalInv / $perPage));
- 245if ($listPage > $pages) $listPage = $pages;
- 246$invoices = $repository->search($listYear, $listPage, $perPage, $listSort, $listQuarter, $listQ);
- 247$availYears = $repository->availableYears();
- 248$baseParams = http_build_query(array_filter(["action"=>"list","year"=>$listYear,"sort"=>$listSort,"quarter"=>$listQuarter,"q"=>$listQ],fn($v)=>$v!==" "&&$v!==null&&$v!==0));
- 249?>
- 250
- 251<div class="card card-soft mb-3">
- 252 <div class="card-body py-2">
- 253 <form method="get" id="filterForm" class="row g-2 align-items-end">
- 254 <input type="hidden" name="action" value="list">
- 255 <div class="col-auto">
- 256 <label class="form-label mb-1 small text-muted">Année</label>
- 257 <select name="year" class="form-select form-select-sm" style="width:100px" onchange="document.getElementById('filterForm').submit()">
- 258 <?php foreach ($availYears as $y): ?>
- 259 <option value="<?= (int)$y ?>" <?= (int)$y===$listYear?"selected":"" ?>><?= (int)$y ?></option>
- 260 <?php endforeach; ?>
- 261 </select>
- 262 </div>
- 263 <div class="col-auto">
- 264 <label class="form-label mb-1 small text-muted">Trimestre</label>
- 265 <select name="quarter" class="form-select form-select-sm" style="width:120px" onchange="document.getElementById('filterForm').submit()">
- 266 <option value="" <?= $listQuarter===""?"selected":"" ?>>Tous</option>
- 267 <option value="Q1" <?= $listQuarter==="Q1"?"selected":"" ?>>T1 (Jan-Mar)</option>
- 268 <option value="Q2" <?= $listQuarter==="Q2"?"selected":"" ?>>T2 (Avr-Jun)</option>
- 269 <option value="Q3" <?= $listQuarter==="Q3"?"selected":"" ?>>T3 (Jul-Sep)</option>
- 270 <option value="Q4" <?= $listQuarter==="Q4"?"selected":"" ?>>T4 (Oct-Dec)</option>
- 271 </select>
- 272 </div>
- 273 <div class="col-auto">
- 274 <label class="form-label mb-1 small text-muted">Trier</label>
- 275 <select name="sort" class="form-select form-select-sm" style="width:150px" onchange="document.getElementById('filterForm').submit()">
- 276 <option value="date_desc" <?= $listSort==="date_desc"?"selected":"" ?>>Date décroissante</option>
- 277 <option value="date_asc" <?= $listSort==="date_asc"?"selected":"" ?>>Date croissante</option>
- 278 </select>
- 279 </div>
- 280 <div class="col-auto">
- 281 <label class="form-label mb-1 small text-muted">Recherche article</label>
- 282 <div class="input-group input-group-sm">
- 283 <input type="text" name="q" class="form-control" placeholder="Ex: béton, dalle..." value="<?= htmlspecialchars($listQ) ?>" style="width:180px">
- 284 <button type="submit" class="btn btn-primary btn-sm">OK</button>
- 285 </div>
- 286 </div>
- 287 <div class="col-auto">
- 288 <label class="form-label mb-1 d-block"> </label>
- 289 <?php if ($listQuarter!==" "||$listQ!==" "||$listSort!=="date_desc"): ?>
- 290 <a href="?action=list&year=<?= $listYear ?>" class="btn btn-sm btn-outline-danger">Effacer</a>
- 291 <?php endif; ?>
- 292 </div>
- 293 <div class="col-auto ms-auto">
- 294 <label class="form-label mb-1 d-block"> </label>
- 295 <span class="text-muted small"><?= $totalInv ?> facture(s)</span>
- 296 </div>
- 297 </form>
- 298 </div>
- 299</div>
- 300
- 301<div class="card card-soft">
- 302 <div class="card-body p-0">
- 303 <form method="post" action="?action=tva" id="invForm">
- 304 <div class="d-flex align-items-center gap-2 px-3 py-2 border-bottom bg-light rounded-top">
- 305 <div class="form-check mb-0">
- 306 <input class="form-check-input" type="checkbox" id="chkAll" onchange="toggleAll(this)">
- 307 <label class="form-check-label small" for="chkAll">Tout sélectionner</label>
- 308 </div>
- 309 <button type="button" class="btn btn-sm btn-outline-secondary" onclick="deselectAll()">Désélectionner tout</button>
- 310 <span class="badge bg-secondary" id="selCount">0 sélectionné(s)</span>
- 311 <div class="ms-auto d-flex gap-2">
- 312 <button type="submit" formaction="?action=tva" class="btn btn-sm btn-outline-info">TVA</button>
- 313 <button type="submit" formaction="/TV@/dlxls.php" class="btn btn-sm btn-success">Export Excel</button>
- 314 </div>
- 315 </div>
- 316 <div class="table-responsive">
- 317 <table class="table table-sm align-middle mb-0">
- 318 <thead class="table-light">
- 319 <tr>
- 320 <th style="width:36px"></th>
- 321 <th>N°</th>
- 322 <th>Client</th>
- 323 <th>Date</th>
- 324 <th>Trimestre</th>
- 325 <th class="text-end">HT</th>
- 326 <th class="text-end">TVA</th>
- 327 <th class="text-end">TTC</th>
- 328 <th class="text-center">Actions</th>
- 329 </tr>
- 330 </thead>
- 331 <tbody>
- 332 <?php if (empty($invoices)): ?>
- 333 <tr><td colspan="9" class="text-center text-muted py-4">Aucune facture trouvée</td></tr>
- 334 <?php else: ?>
- 335 <?php foreach ($invoices as $inv): ?>
- 336 <tr>
- 337 <td><input type="checkbox" name="invoice_ids[]" value="<?= $inv["id"] ?>" class="form-check-input chk" onchange="saveSelection();updateCount()"></td>
- 338 <td><strong><?= e($inv["invoice_number"]) ?></strong></td>
- 339 <td class="text-truncate" style="max-width:150px"><?= e($inv["client_name"]??"") ?></td>
- 340 <td><?= e($inv["invoice_date"]) ?></td>
- 341 <td><span class="badge bg-light text-dark border"><?= e($inv["quarter"]) ?></span></td>
- 342 <td class="text-end"><?= money($inv["total_ht"]) ?></td>
- 343 <td class="text-end"><?= money($inv["total_vat"]) ?></td>
- 344 <td class="text-end fw-semibold"><?= money($inv["total_ttc"]) ?></td>
- 345 <td class="text-center">
- 346 <a href="?action=edit&id=<?= $inv["id"] ?>" class="btn btn-sm btn-outline-primary py-0 px-1">Modifier</a>
- 347 <a href="/TV@/genpdf.php?id=<?= $inv["id"] ?>" target="_blank" class="btn btn-sm btn-outline-danger py-0 px-1">PDF</a>
- 348 <a href="/TV@/dlxls.php?id=<?= $inv["id"] ?>" class="btn btn-sm btn-outline-success py-0 px-1">XLS</a>
- 349 <a href="?action=delete&id=<?= $inv["id"] ?>" class="btn btn-sm btn-outline-secondary py-0 px-1" onclick="return confirm('Supprimer ?')">Suppr.</a>
- 350 </td>
- 351 </tr>
- 352 <?php endforeach; ?>
- 353 <?php endif; ?>
- 354 </tbody>
- 355 </table>
- 356 </div>
- 357 <?php if ($pages>1): ?>
- 358 <nav class="d-flex justify-content-center align-items-center py-3 gap-1 flex-wrap">
- 359 <?php if ($listPage>1): ?><a href="?<?= $baseParams ?>&page=<?= $listPage-1 ?>" class="btn btn-sm btn-outline-secondary">«</a><?php endif; ?>
- 360 <?php $ps=max(1,$listPage-3);$pe=min($pages,$listPage+3); ?>
- 361 <?php if($ps>1): ?><a href="?<?= $baseParams ?>&page=1" class="btn btn-sm btn-outline-secondary">1</a><?php if($ps>2): ?><span class="px-1 text-muted">...</span><?php endif; endif; ?>
- 362 <?php for($p=$ps;$p<=$pe;$p++): ?>
- 363 <a href="?<?= $baseParams ?>&page=<?= $p ?>" class="btn btn-sm <?= $p===$listPage?"btn-primary":"btn-outline-secondary" ?>"><?= $p ?></a>
- 364 <?php endfor; ?>
- 365 <?php if($pe<$pages): ?><?php if($pe<$pages-1): ?><span class="px-1 text-muted">...</span><?php endif; ?><a href="?<?= $baseParams ?>&page=<?= $pages ?>" class="btn btn-sm btn-outline-secondary"><?= $pages ?></a><?php endif; ?>
- 366 <?php if($listPage<$pages): ?><a href="?<?= $baseParams ?>&page=<?= $listPage+1 ?>" class="btn btn-sm btn-outline-secondary">»</a><?php endif; ?>
- 367 <small class="text-muted ms-2">Page <?= $listPage ?>/<?= $pages ?></small>
- 368 </nav>
- 369 <?php endif; ?>
- 370 </form>
- 371 </div>
- 372</div>
- 373<script>
- 374const _SK="inv_sel";
- 375function _getS(){try{return JSON.parse(sessionStorage.getItem(_SK)||"[]")}catch(e){return[]}}
- 376function saveSelection(){const s=_getS();document.querySelectorAll(".chk").forEach(c=>{const id=c.value;if(c.checked&&!s.includes(id))s.push(id);else if(!c.checked){const i=s.indexOf(id);if(i>-1)s.splice(i,1);}});sessionStorage.setItem(_SK,JSON.stringify(s));updateCount();}
- 377function restoreSelection(){const s=_getS();document.querySelectorAll(".chk").forEach(c=>c.checked=s.includes(c.value));updateCount();syncChkAll();}
- 378function updateCount(){document.getElementById("selCount").textContent=_getS().length+" sélectionné(s)";}
- 379function toggleAll(m){document.querySelectorAll(".chk").forEach(c=>c.checked=m.checked);saveSelection();}
- 380function deselectAll(){sessionStorage.removeItem(_SK);document.querySelectorAll(".chk").forEach(c=>c.checked=false);document.getElementById("chkAll").checked=false;updateCount();}
- 381function syncChkAll(){const a=document.querySelectorAll(".chk").length,c=document.querySelectorAll(".chk:checked").length;const el=document.getElementById("chkAll");el.checked=a>0&&c===a;el.indeterminate=c>0&&c<a;}
- 382document.getElementById("invForm").addEventListener("submit",function(e){const btn=e.submitter;const s=_getS();this.querySelectorAll("input[name=\"invoice_ids[]\"]").forEach(e=>e.remove());s.forEach(id=>{const i=document.createElement("input");i.type="hidden";i.name="invoice_ids[]";i.value=id;this.appendChild(i);});if(btn&&btn.getAttribute("formaction")){this.action=btn.getAttribute("formaction");}});
- 383document.addEventListener("DOMContentLoaded",()=>restoreSelection());
- 384</script>
- 385<?php endif; ?>
- 386</div>
- 387</body>
- 388</html>
Paste brut