2
0

cms.compte.php 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519
  1. <?php
  2. $jsonTarget = "/json.php?file=banque-lignes-" . core::getGet("id");
  3. if (debug::isFile("debug")) {
  4. debug::log(debug::getBadge($jsonTarget, "OUVRIR LE JSON : " . $jsonTarget), "JSON chargé en arrière plan");
  5. }
  6. $banque = banque::getInitialCompte(core::getGet("id"));
  7. $etatCompte = banque::getEtatCompte(core::getGet("id"));
  8. ?>
  9. <header class="d-flex flex-column flex-md-row align-items-md-center p-3 bg-light ">
  10. <h2 class="bd-title" id="content">
  11. <span>Etat des comptes</span>
  12. </h2>
  13. <?php
  14. if (access::ifAccesss("compte-upload")) { ?>
  15. <div class="fix-container-button-nav">
  16. <?php if ($banque["import"] == "manuel") { ?>
  17. <a href="/?p=compte-insert&add=<?php echo core::getGet("id") ?>"><button type="submit" class="btn btn-outline-success btn-sm"><?php icon::getFont(["icon" => "bi bi-file-earmark-plus"]) ?> Ajouter une ligne</button></a>
  18. <?php } else { ?>
  19. <a href="/?p=compte-upload&add=<?php echo core::getGet("id") ?>"><button type="submit" class="btn btn-outline-success btn-sm"><?php icon::getFont(["icon" => "bi bi-file-earmark-plus"]) ?> Charger un CSV</button></a>
  20. <?php } ?>
  21. </div>
  22. <?php } ?>
  23. </header>
  24. <?php
  25. echo core::filAriane(array(
  26. "current" => $banque["label"] . ((!empty($banque["compte"])) ? " [" . $banque["compte"] . "]" : NULL),
  27. "arbo" => array(
  28. "Comptes bancaires" => NULL,
  29. $banque["label"] . ((!empty($banque["compte"])) ? " [" . $banque["compte"] . "]" : NULL) => "/compte-" . core::getGet("id") . ".html"
  30. ),
  31. "refresh-json" => "banque-lignes-" . core::getGet("id")
  32. ));
  33. $lastRecord = banque::lastRecord(core::getGet("id"));
  34. if (!empty($lastRecord)) {
  35. $callout = [
  36. "type" => "info",
  37. "h4" => "Note",
  38. "p" => "La dernier import des données du compte a été réalisée le <span class=\"fw-bold\">" . core::convertDate($lastRecord) . "</span> et à cette même date le solde était de <span class=\"fw-bold\">" . banque::getEuro($etatCompte["solde"]) . "</span>",
  39. ];
  40. callout::print($callout);
  41. }
  42. ?>
  43. <!-- Panel de filtres avancés avec checkboxes multi-sélection -->
  44. <div class="card mb-3" id="advancedFiltersPanel">
  45. <div class="card-header d-flex justify-content-between align-items-center" style="cursor: pointer;" onclick="toggleAdvancedFilters()">
  46. <span><i class="bi bi-funnel"></i> Filtres avancés</span>
  47. <i class="bi bi-chevron-up" id="advancedFiltersIcon"></i>
  48. </div>
  49. <div class="card-body" id="advancedFiltersBody">
  50. <div class="row">
  51. <!-- Filtre par plage de dates -->
  52. <div class="col-lg-3 col-md-6 mb-3">
  53. <label class="form-label fw-bold">Période</label>
  54. <div class="d-flex align-items-center">
  55. <input type="date" id="dateDebut" class="form-control form-control-sm me-2">
  56. <span class="me-2">à</span>
  57. <input type="date" id="dateFin" class="form-control form-control-sm">
  58. </div>
  59. <small class="text-muted">Filtrer entre deux dates</small>
  60. </div>
  61. <!-- Filtre par mots-clés sur le label -->
  62. <div class="col-lg-3 col-md-6 mb-3">
  63. <label class="form-label fw-bold">Mots-clés dans le Label</label>
  64. <input type="text" class="form-control form-control-sm" id="search-label-keywords" placeholder="Ex: bourse enpayout">
  65. <small class="text-muted">Mots-clés séparés par des espaces</small>
  66. </div>
  67. <!-- Filtre par montant (recherche exacte ou plage) -->
  68. <div class="col-lg-3 col-md-6 mb-3">
  69. <label class="form-label fw-bold">Recherche montant</label>
  70. <div class="d-flex align-items-center">
  71. <select id="searchMontantColonne" class="form-select form-select-sm me-2" style="width: 100px;">
  72. <option value="all">Tous</option>
  73. <option value="debit">Débit</option>
  74. <option value="credit">Crédit</option>
  75. <option value="solde">Solde</option>
  76. </select>
  77. <input type="text" class="form-control form-control-sm" id="searchMontant" placeholder="Ex: 5086 ou >1000">
  78. </div>
  79. <small class="text-muted">Recherche exacte ou avec opérateur</small>
  80. </div>
  81. </div>
  82. <div class="row">
  83. <!-- Filtre par plage de montant Débit -->
  84. <div class="col-lg-3 col-md-6 mb-3">
  85. <label class="form-label fw-bold">Plage Débit</label>
  86. <div class="d-flex align-items-center">
  87. <input type="number" step="0.01" id="minDebit" class="form-control form-control-sm me-2" placeholder="Min" style="width: 100px;">
  88. <span class="me-2">à</span>
  89. <input type="number" step="0.01" id="maxDebit" class="form-control form-control-sm" placeholder="Max" style="width: 100px;">
  90. </div>
  91. </div>
  92. <!-- Filtre par plage de montant Crédit -->
  93. <div class="col-lg-3 col-md-6 mb-3">
  94. <label class="form-label fw-bold">Plage Crédit</label>
  95. <div class="d-flex align-items-center">
  96. <input type="number" step="0.01" id="minCredit" class="form-control form-control-sm me-2" placeholder="Min" style="width: 100px;">
  97. <span class="me-2">à</span>
  98. <input type="number" step="0.01" id="maxCredit" class="form-control form-control-sm" placeholder="Max" style="width: 100px;">
  99. </div>
  100. </div>
  101. <!-- Filtre par plage de Solde -->
  102. <div class="col-lg-3 col-md-6 mb-3">
  103. <label class="form-label fw-bold">Plage Solde</label>
  104. <div class="d-flex align-items-center">
  105. <input type="number" step="0.01" id="minSolde" class="form-control form-control-sm me-2" placeholder="Min" style="width: 100px;">
  106. <span class="me-2">à</span>
  107. <input type="number" step="0.01" id="maxSolde" class="form-control form-control-sm" placeholder="Max" style="width: 100px;">
  108. </div>
  109. </div>
  110. </div>
  111. <div class="mt-2">
  112. <button type="button" class="btn btn-sm btn-primary me-2" onclick="applyAdvancedFilters()">
  113. <i class="bi bi-funnel-fill"></i> Appliquer les filtres
  114. </button>
  115. <button type="button" class="btn btn-sm btn-outline-secondary" onclick="clearAllAdvancedFilters()">
  116. <i class="bi bi-x-circle"></i> Réinitialiser les filtres
  117. </button>
  118. <span class="ms-3 text-muted" id="filterInfo"></span>
  119. </div>
  120. </div>
  121. </div>
  122. <style>
  123. #advancedFiltersPanel .card-header:hover {
  124. background-color: #f8f9fa;
  125. }
  126. #search-label-keywords::placeholder {
  127. font-style: italic;
  128. color: #adb5bd;
  129. }
  130. </style>
  131. <div>
  132. <table
  133. id="table"
  134. class="table-striped table-hover table-sm"
  135. data-toggle="table"
  136. data-show-footer="true"
  137. data-buttons-align="left"
  138. data-filter-control="true"
  139. data-flat="true"
  140. data-sort-name="num"
  141. data-sort-order="desc"
  142. data-show-export="true"
  143. data-pagination="true"
  144. data-side-pagination="client"
  145. data-page-size="25"
  146. data-page-list="[25, 50, 100, 250, all]"
  147. data-url="<?php echo $jsonTarget ?>"
  148. data-filter-control-visible="false">
  149. <thead>
  150. <tr>
  151. <th data-sortable="true" data-field="num" data-width="20">#</th>
  152. <th data-sortable="true" data-field="date" data-filter-control="input" data-width="160">Date</th>
  153. <th data-sortable="true" data-field="label" data-filter-control="input">Label</th>
  154. <th data-sortable="true" data-formatter="dataFormatter" data-field="debit" data-width="150" data-footer-formatter="debitFormatter">Débit</th>
  155. <th data-sortable="true" data-formatter="dataFormatter" data-field="credit" data-width="150" data-footer-formatter="creditFormatter">Crédit</th>
  156. <th data-sortable="true" data-formatter="dataFormatter" data-field="solde" data-width="150">Solde</th>
  157. </tr>
  158. </thead>
  159. </table>
  160. </div>
  161. <script>
  162. let euro = Intl.NumberFormat('de-DE', {
  163. style: 'currency',
  164. currency: 'EUR',
  165. });
  166. // Données originales
  167. let originalData = [];
  168. // Fonction de filtre personnalisée pour les montants (accepte format décimal et euro)
  169. function filterMontant(searchValue, value, field, data) {
  170. if (!searchValue || searchValue.trim() === '') return true;
  171. // Valeur numérique brute de la donnée
  172. const numValue = parseFloat(data[field]) || 0;
  173. // Valeur absolue pour faciliter la recherche
  174. const absNumValue = Math.abs(numValue);
  175. // Normaliser la recherche : remplacer virgule par point et supprimer les espaces/symboles
  176. let searchNormalized = searchValue.trim()
  177. .replace(/\s/g, '') // Supprimer espaces
  178. .replace(/€/g, '') // Supprimer symbole euro
  179. .replace(/\./g, '') // Supprimer les points (séparateurs milliers)
  180. .replace(/,/g, '.'); // Remplacer virgule par point (décimale)
  181. // Vérifier si c'est une comparaison (>, <, >=, <=, =)
  182. let operator = null;
  183. if (searchNormalized.startsWith('>=')) {
  184. operator = '>=';
  185. searchNormalized = searchNormalized.substring(2);
  186. } else if (searchNormalized.startsWith('<=')) {
  187. operator = '<=';
  188. searchNormalized = searchNormalized.substring(2);
  189. } else if (searchNormalized.startsWith('>')) {
  190. operator = '>';
  191. searchNormalized = searchNormalized.substring(1);
  192. } else if (searchNormalized.startsWith('<')) {
  193. operator = '<';
  194. searchNormalized = searchNormalized.substring(1);
  195. } else if (searchNormalized.startsWith('=')) {
  196. operator = '=';
  197. searchNormalized = searchNormalized.substring(1);
  198. }
  199. const searchNum = parseFloat(searchNormalized);
  200. if (isNaN(searchNum)) {
  201. // Si ce n'est pas un nombre, faire une recherche textuelle sur la valeur formatée
  202. const formattedValue = euro.format(numValue).toLowerCase();
  203. return formattedValue.includes(searchValue.toLowerCase());
  204. }
  205. // Comparaison numérique
  206. if (operator) {
  207. switch (operator) {
  208. case '>':
  209. return numValue > searchNum;
  210. case '<':
  211. return numValue < searchNum;
  212. case '>=':
  213. return numValue >= searchNum;
  214. case '<=':
  215. return numValue <= searchNum;
  216. case '=':
  217. return Math.abs(numValue - searchNum) < 0.01;
  218. }
  219. }
  220. // Sans opérateur : recherche de correspondance exacte ou partielle
  221. // Correspondance exacte (avec tolérance)
  222. if (Math.abs(numValue - searchNum) < 0.01 || Math.abs(absNumValue - searchNum) < 0.01) {
  223. return true;
  224. }
  225. // Recherche partielle : vérifier si le nombre entier recherché est présent
  226. // Convertir en entier pour la comparaison (ex: 5086 dans 5086.00)
  227. const intPart = Math.floor(absNumValue);
  228. const searchInt = Math.floor(searchNum);
  229. if (intPart === searchInt) {
  230. return true;
  231. }
  232. // Recherche textuelle dans le nombre formaté
  233. const numStr = absNumValue.toFixed(2);
  234. const searchStr = searchInt.toString();
  235. return numStr.startsWith(searchStr) || intPart.toString().includes(searchStr);
  236. }
  237. function dataFormatter(value) {
  238. return euro.format(value);
  239. }
  240. function debitFormatter(data) {
  241. var total = 0;
  242. data.forEach(function(row) {
  243. total += parseFloat(row.debit);
  244. });
  245. return parseFloat(total) === 0 ? euro.format(0.00) : euro.format(total.toFixed(2));
  246. }
  247. function creditFormatter(data) {
  248. var total = 0;
  249. data.forEach(function(row) {
  250. total += parseFloat(row.credit);
  251. });
  252. return parseFloat(total) === 0 ? euro.format(0.00) : euro.format(total.toFixed(2));
  253. }
  254. // Toggle du panneau de filtres avancés
  255. function toggleAdvancedFilters() {
  256. const body = document.getElementById('advancedFiltersBody');
  257. const icon = document.getElementById('advancedFiltersIcon');
  258. if (body.style.display === 'none') {
  259. body.style.display = 'block';
  260. icon.className = 'bi bi-chevron-up';
  261. } else {
  262. body.style.display = 'none';
  263. icon.className = 'bi bi-chevron-down';
  264. }
  265. }
  266. // Convertir une date en objet Date (supporte DD/MM/YYYY et YYYY-MM-DD)
  267. function parseDate(dateStr) {
  268. if (!dateStr) return null;
  269. // Format DD/MM/YYYY
  270. if (dateStr.includes('/')) {
  271. const parts = dateStr.split('/');
  272. if (parts.length === 3) {
  273. return new Date(parseInt(parts[2]), parseInt(parts[1]) - 1, parseInt(parts[0]), 0, 0, 0);
  274. }
  275. }
  276. // Format YYYY-MM-DD (format ISO/MySQL)
  277. if (dateStr.includes('-')) {
  278. const parts = dateStr.split('-');
  279. if (parts.length === 3 && parts[0].length === 4) {
  280. return new Date(parseInt(parts[0]), parseInt(parts[1]) - 1, parseInt(parts[2]), 0, 0, 0);
  281. }
  282. }
  283. return null;
  284. }
  285. // Appliquer tous les filtres avancés
  286. function applyAdvancedFilters() {
  287. // Récupérer les dates (format ISO: YYYY-MM-DD depuis input type="date")
  288. const dateDebutStr = document.getElementById('dateDebut').value;
  289. const dateFinStr = document.getElementById('dateFin').value;
  290. let dateDebut = null;
  291. let dateFin = null;
  292. if (dateDebutStr) {
  293. const parts = dateDebutStr.split('-');
  294. dateDebut = new Date(parseInt(parts[0]), parseInt(parts[1]) - 1, parseInt(parts[2]), 0, 0, 0);
  295. }
  296. if (dateFinStr) {
  297. const parts = dateFinStr.split('-');
  298. // Mettre la date de fin à 23:59:59 pour inclure toute la journée
  299. dateFin = new Date(parseInt(parts[0]), parseInt(parts[1]) - 1, parseInt(parts[2]), 23, 59, 59);
  300. }
  301. // Fonction pour normaliser le texte (retirer accents et passer en majuscules)
  302. const normalizeText = (text) => (text || '').normalize('NFD').replace(/[\u0300-\u036f]/g, '').toUpperCase();
  303. // Récupérer les mots-clés saisis (séparés par espaces)
  304. const keywordsInput = document.getElementById('search-label-keywords').value.trim();
  305. const keywords = keywordsInput.split(/\s+/).filter(k => k.length > 0).map(k => normalizeText(k));
  306. // Récupérer la recherche de montant et la colonne sélectionnée
  307. const searchMontant = document.getElementById('searchMontant').value.trim();
  308. const searchMontantColonne = document.getElementById('searchMontantColonne').value;
  309. const minDebit = parseFloat(document.getElementById('minDebit').value);
  310. const maxDebit = parseFloat(document.getElementById('maxDebit').value);
  311. const minCredit = parseFloat(document.getElementById('minCredit').value);
  312. const maxCredit = parseFloat(document.getElementById('maxCredit').value);
  313. const minSolde = parseFloat(document.getElementById('minSolde').value);
  314. const maxSolde = parseFloat(document.getElementById('maxSolde').value);
  315. let filteredData = originalData.filter(row => {
  316. // Filtre par plage de dates
  317. if (dateDebut || dateFin) {
  318. const rowDate = parseDate(row.date);
  319. // Si pas de date valide sur la ligne et qu'on filtre par date, exclure la ligne
  320. if (!rowDate) return false;
  321. if (dateDebut && rowDate < dateDebut) return false;
  322. if (dateFin && rowDate > dateFin) return false;
  323. }
  324. // Filtre par mots-clés dans le label (TOUS les mots-clés doivent être présents)
  325. if (keywords.length > 0) {
  326. const labelNorm = normalizeText(row.label);
  327. const hasAllKeywords = keywords.every(keyword => labelNorm.includes(keyword));
  328. if (!hasAllKeywords) return false;
  329. }
  330. // Filtre par recherche de montant (selon la colonne sélectionnée)
  331. if (searchMontant) {
  332. let match = false;
  333. if (searchMontantColonne === 'all') {
  334. const matchDebit = filterMontant(searchMontant, null, 'debit', row);
  335. const matchCredit = filterMontant(searchMontant, null, 'credit', row);
  336. const matchSolde = filterMontant(searchMontant, null, 'solde', row);
  337. match = matchDebit || matchCredit || matchSolde;
  338. } else {
  339. match = filterMontant(searchMontant, null, searchMontantColonne, row);
  340. }
  341. if (!match) return false;
  342. }
  343. // Filtre par plage de débit
  344. const debit = parseFloat(row.debit) || 0;
  345. if (!isNaN(minDebit) && debit < minDebit) return false;
  346. if (!isNaN(maxDebit) && debit > maxDebit) return false;
  347. // Filtre par plage de crédit
  348. const credit = parseFloat(row.credit) || 0;
  349. if (!isNaN(minCredit) && credit < minCredit) return false;
  350. if (!isNaN(maxCredit) && credit > maxCredit) return false;
  351. // Filtre par plage de solde
  352. const solde = parseFloat(row.solde) || 0;
  353. if (!isNaN(minSolde) && solde < minSolde) return false;
  354. if (!isNaN(maxSolde) && solde > maxSolde) return false;
  355. return true;
  356. });
  357. // Recharger le tableau avec les données filtrées
  358. $('#table').bootstrapTable('load', filteredData);
  359. updateFilterInfo(filteredData.length, keywords);
  360. }
  361. // Réinitialiser tous les filtres avancés
  362. function clearAllAdvancedFilters() {
  363. // Réinitialiser les dates
  364. document.getElementById('dateDebut').value = '';
  365. document.getElementById('dateFin').value = '';
  366. // Réinitialiser les champs numériques
  367. document.getElementById('minDebit').value = '';
  368. document.getElementById('maxDebit').value = '';
  369. document.getElementById('minCredit').value = '';
  370. document.getElementById('maxCredit').value = '';
  371. document.getElementById('minSolde').value = '';
  372. document.getElementById('maxSolde').value = '';
  373. // Réinitialiser la barre de recherche des mots-clés
  374. document.getElementById('search-label-keywords').value = '';
  375. // Réinitialiser la recherche de montant
  376. document.getElementById('searchMontant').value = '';
  377. document.getElementById('searchMontantColonne').value = 'all';
  378. // Recharger les données originales
  379. $('#table').bootstrapTable('load', originalData);
  380. updateFilterInfo(originalData.length, []);
  381. // Réinitialiser aussi les filtres de la table
  382. $('#table').bootstrapTable('clearFilterControl');
  383. }
  384. // Mettre à jour l'info des filtres actifs
  385. function updateFilterInfo(count, keywords) {
  386. const infoEl = document.getElementById('filterInfo');
  387. const activeFilters = [];
  388. const dateDebut = document.getElementById('dateDebut').value;
  389. const dateFin = document.getElementById('dateFin').value;
  390. if (dateDebut || dateFin) {
  391. activeFilters.push(`Période: ${dateDebut || 'début'} à ${dateFin || 'fin'}`);
  392. }
  393. if (keywords && keywords.length > 0) {
  394. activeFilters.push(`Mots-clés: "${keywords.join(' + ')}"`);
  395. }
  396. const searchMontant = document.getElementById('searchMontant').value;
  397. if (searchMontant) {
  398. const colonneLabels = {
  399. 'all': 'Tous',
  400. 'debit': 'Débit',
  401. 'credit': 'Crédit',
  402. 'solde': 'Solde'
  403. };
  404. const colonne = document.getElementById('searchMontantColonne').value;
  405. activeFilters.push(`Montant (${colonneLabels[colonne]}): "${searchMontant}"`);
  406. }
  407. const minDebit = document.getElementById('minDebit').value;
  408. const maxDebit = document.getElementById('maxDebit').value;
  409. if (minDebit || maxDebit) {
  410. activeFilters.push(`Débit: ${minDebit || '0'} - ${maxDebit || '∞'}`);
  411. }
  412. const minCredit = document.getElementById('minCredit').value;
  413. const maxCredit = document.getElementById('maxCredit').value;
  414. if (minCredit || maxCredit) {
  415. activeFilters.push(`Crédit: ${minCredit || '0'} - ${maxCredit || '∞'}`);
  416. }
  417. const minSolde = document.getElementById('minSolde').value;
  418. const maxSolde = document.getElementById('maxSolde').value;
  419. if (minSolde || maxSolde) {
  420. activeFilters.push(`Solde: ${minSolde || '0'} - ${maxSolde || '∞'}`);
  421. }
  422. if (activeFilters.length > 0) {
  423. infoEl.innerHTML = `<strong>${count}</strong> ligne(s) affichée(s) | Filtres actifs: ${activeFilters.join(', ')}`;
  424. } else {
  425. infoEl.innerHTML = `<strong>${count || originalData.length}</strong> ligne(s) au total`;
  426. }
  427. }
  428. // Initialisation
  429. $(document).ready(function() {
  430. const $table = $('#table');
  431. // Attendre que les données soient chargées
  432. $table.on('load-success.bs.table', function(e, data) {
  433. // Sauvegarder les données originales
  434. originalData = data;
  435. updateFilterInfo(data.length, []);
  436. });
  437. // Permettre d'appliquer le filtre avec la touche Entrée sur les champs texte
  438. ['search-label-keywords', 'searchMontant'].forEach(id => {
  439. document.getElementById(id).addEventListener('keypress', function(e) {
  440. if (e.key === 'Enter') {
  441. applyAdvancedFilters();
  442. }
  443. });
  444. });
  445. });
  446. </script>