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