0% found this document useful (0 votes)
5 views131 pages

Code

The document outlines a series of bugs and feature requests for an application, including fixing a loading issue, removing the background behind the logo, displaying the logo on the main page instead of an icon, and resolving an admin login issue. Additionally, it requests the ability for administrators to generate and print a structured monthly report in PDF format detailing police movements. The source code for the application is provided, emphasizing the need for corrections without altering existing functionalities.

Uploaded by

Scott Dino
Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as DOCX, PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
5 views131 pages

Code

The document outlines a series of bugs and feature requests for an application, including fixing a loading issue, removing the background behind the logo, displaying the logo on the main page instead of an icon, and resolving an admin login issue. Additionally, it requests the ability for administrators to generate and print a structured monthly report in PDF format detailing police movements. The source code for the application is provided, emphasizing the need for corrections without altering existing functionalities.

Uploaded by

Scott Dino
Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as DOCX, PDF, TXT or read online on Scribd

voici le code complet de mon application, n’altère rien, ne change rien de

ce qui existe déjà mais corrige les bugs suivant : l’application reste figée
sur le loader, corrige ce bug. Supprime aussi l’arrière plan qui se trouve en
dessous de mon [Link], et je veux aussi que mon logo affiche sur la
page principale et non une icône. Corrige aussi le bug de connexion
administrateur, quand je met l’email et le mot de passe (firebase) j’appuie
su connexion admin il n’y a aucune réaction et je n’arrive pas à accéder à
l’application, et au lieu du bouton verrouillage met déconnexion. Je veux
aussi que tu donnes la possibilité à l’administrateur de générer et
‘imprimer un rapport mensuel bien structuré (pdf) de tous les
mouvements faits par exemple ajout, supression, permutation d’un
policier, d’armes…. Pour avoir une idée sur la manière dont les policiers
sont gérés selon leur détachement mensuellement.

code source :

<!DOCTYPE html>

<html lang="fr" data-theme="light">

<head>

<meta charset="UTF-8">

<meta name="viewport" content="width=device-width, initial-


scale=1.0, maximum-scale=1.0, user-scalable=no">

<meta name="theme-color" content="#1e40af" media="(prefers-color-


scheme: light)">

<meta name="theme-color" content="#0f172a" media="(prefers-color-


scheme: dark)">

<meta name="description" content="GPI-HP - Gestion des


Détachements de Police">

<meta name="mobile-web-app-capable" content="yes">

<meta name="apple-mobile-web-app-capable" content="yes">

<meta name="apple-mobile-web-app-status-bar-style" content="black-


translucent">

<meta name="apple-mobile-web-app-title" content="GPI-HP">

<link rel="icon" type="image/png" href="[Link]">

<link rel="apple-touch-icon" href="[Link]">

<link rel="manifest" href="[Link]">


<title>GPI-HP - Gestion des Détachements</title>

<link
href="[Link]
css" rel="stylesheet">

<link rel="stylesheet" href="[Link]


awesome/6.4.0/css/[Link]">

<link href="[Link]
family=Inter:wght@300;400;500;600;700&display=swap"
rel="stylesheet">

<!-- [Link] pour les graphiques -->

<script
src="[Link]
script>

<script
src="[Link]
defer></script>

<script
src="[Link]
defer></script>

<script
src="[Link]
defer></script>

<script src="[Link]
[Link]"></script>

<script src="[Link]
[Link]"></script>

<script src="[Link]
[Link]"></script>

<style>
:root {

--primary: #1e40af;

--primary-dark: #1e3a8a;

--secondary: #64748b;

--success: #059669;

--warning: #d97706;

--danger: #dc2626;

--dark: #0f172a;

--light: #f8fafc;

--surface: #ffffff;

--border: #e2e8f0;

--text-main: #1e293b;

--text-muted: #64748b;

--shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);

--input-bg: #ffffff;

--input-text: #1e293b;

--expired: #dc2626;

--expiring: #f59e0b;

--valid: #059669;

[data-theme="dark"] {

--primary: #60a5fa;

--primary-dark: #93c5fd;

--secondary: #94a3b8;

--light: #0f172a;

--surface: #1e293b;

--border: #334155;

--text-main: #f8fafc;
--text-muted: #94a3b8;

--shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.5);

--input-bg: #334155;

--input-text: #f8fafc;

--expired: #ef4444;

--expiring: #fbbf24;

--valid: #4ade80;

* { margin: 0; padding: 0; box-sizing: border-box; }

body {

font-family: 'Inter', sans-serif;

background-color: var(--light);

color: var(--text-main);

height: 100vh;

height: 100dvh;

overflow: hidden;

transition: background-color 0.3s, color 0.3s;

-webkit-tap-highlight-color: transparent;

/* Loader Professionnel */

.app-loader {

position: fixed;

top: 0;

left: 0;

width: 100vw;

height: 100vh;
background: linear-gradient(135deg, #1e3a8a 0%, #1e40af
100%);

z-index: 99999;

display: flex;

align-items: center;

justify-content: center;

transition: opacity 0.5s ease-out, visibility 0.5s;

.[Link] {

opacity: 0;

visibility: hidden;

pointer-events: none;

.loader-content {

text-align: center;

color: white;

padding: 2rem;

.loader-logo {

width: 120px;

height: 120px;

background: white;

border-radius: 50%;

display: flex;

align-items: center;

justify-content: center;
margin: 0 auto 2rem;

box-shadow: 0 10px 40px rgba(0,0,0,0.3);

overflow: hidden;

border: 4px solid rgba(255,255,255,0.2);

animation: pulse-logo 2s ease-in-out infinite;

.loader-logo img {

width: 90%;

height: 90%;

object-fit: contain;

@keyframes pulse-logo {

0%, 100% { transform: scale(1); box-shadow: 0 10px 40px


rgba(0,0,0,0.3); }

50% { transform: scale(1.05); box-shadow: 0 15px 50px


rgba(0,0,0,0.4); }

.loader-spinner {

width: 50px;

height: 50px;

border: 4px solid rgba(255,255,255,0.3);

border-top-color: white;

border-radius: 50%;

margin: 0 auto 1.5rem;

animation: spin 1s linear infinite;

}
@keyframes spin {

to { transform: rotate(360deg); }

.loader-text {

font-size: 1.1rem;

font-weight: 500;

margin-bottom: 1rem;

opacity: 0.9;

.loader-progress {

width: 200px;

height: 4px;

background: rgba(255,255,255,0.2);

border-radius: 2px;

margin: 0 auto;

overflow: hidden;

.loader-progress-bar {

height: 100%;

width: 0%;

background: white;

border-radius: 2px;

transition: width 0.3s ease;

.[Link] {
animation: progress 2s ease-in-out infinite;

@keyframes progress {

0% { width: 0%; opacity: 1; }

50% { width: 70%; opacity: 1; }

100% { width: 100%; opacity: 0; }

/* Animation d'entrée pour l'application */

#app-container {

opacity: 0;

transform: translateY(20px);

transition: opacity 0.5s ease, transform 0.5s ease;

#[Link] {

opacity: 1;

transform: translateY(0);

.install-banner {

position: fixed;

bottom: 0;

left: 0;

right: 0;

background: var(--primary);

color: white;

padding: 1rem;
display: none;

align-items: center;

justify-content: space-between;

z-index: 9998;

box-shadow: 0 -4px 6px -1px rgba(0,0,0,0.1);

.[Link] { display: flex; }

#lock-screen {

position: fixed;

top: 0;

left: 0;

width: 100vw;

height: 100vh;

background: linear-gradient(135deg, #1e3a8a 0%, #1e40af


100%);

z-index: 10000;

display: flex;

flex-direction: column;

align-items: center;

justify-content: center;

color: white;

text-align: center;

padding: 1rem;

.police-logo {

width: 120px;

height: 120px;
background: white;

border-radius: 50%;

display: flex;

align-items: center;

justify-content: center;

margin-bottom: 1rem;

box-shadow: 0 10px 25px rgba(0,0,0,0.3);

overflow: hidden;

border: 4px solid rgba(255,255,255,0.3);

.police-logo img { width: 100%; height: 100%; object-fit: contain;


padding: 10px; }

.police-logo i { font-size: 3rem; color: #1e40af; }

.login-form {

width: 100%;

max-width: 400px;

margin-top: 1rem;

background: rgba(255, 255, 255, 0.15);

padding: 2rem;

border-radius: 16px;

backdrop-filter: blur(10px);

border: 1px solid rgba(255,255,255,0.2);

.form-control, .form-select {

border-radius: 8px;

border: 1px solid var(--border);

padding: 0.75rem;
background-color: var(--input-bg);

color: var(--input-text);

font-size: 16px;

transition: all 0.2s;

.form-control:focus, .form-select:focus {

border-color: var(--primary);

box-shadow: 0 0 0 3px rgba(30, 64, 175, 0.1);

#app-container {

display: none;

flex-direction: column;

height: 100vh;

height: 100dvh;

background-color: var(--light);

.app-header {

background-color: var(--surface);

border-bottom: 1px solid var(--border);

padding: 0.75rem 1.5rem;

display: flex;

align-items: center;

justify-content: space-between;

box-shadow: var(--shadow);

z-index: 100;

height: 60px;
}

.brand {

display: flex;

align-items: center;

gap: 0.75rem;

font-weight: 700;

font-size: 1.25rem;

color: var(--primary);

.brand-logo {

width: 36px;

height: 36px;

background: linear-gradient(135deg, #1e40af 0%, #3b82f6 100%);

border-radius: 8px;

display: flex;

align-items: center;

justify-content: center;

color: white;

font-size: 1.1rem;

overflow: hidden;

.brand-logo img { width: 100%; height: 100%; object-fit: contain;


padding: 2px; }

.badge-police {

background-color: var(--primary);

color: white;
padding: 0.35rem 0.75rem;

border-radius: 6px;

font-weight: 600;

font-size: 0.8rem;

letter-spacing: 0.5px;

.stats-bar {

display: flex;

gap: 0.75rem;

padding: 0.75rem 1.5rem;

background: var(--surface);

border-bottom: 1px solid var(--border);

overflow-x: auto;

scrollbar-width: thin;

.stat-item {

display: flex;

align-items: center;

gap: 0.75rem;

padding: 0.5rem 1rem;

background: rgba(30, 64, 175, 0.05);

border-radius: 8px;

border: 1px solid var(--border);

min-width: fit-content;

transition: all 0.2s;

cursor: pointer;

}
.stat-item:hover {

transform: translateY(-2px);

box-shadow: var(--shadow);

[data-theme="dark"] .stat-item {

background: rgba(96, 165, 250, 0.1);

border-color: #334155;

.stat-icon-sm {

width: 32px;

height: 32px;

border-radius: 6px;

display: flex;

align-items: center;

justify-content: center;

font-size: 0.875rem;

background: rgba(30, 64, 175, 0.1);

color: var(--primary);

[data-theme="dark"] .stat-icon-sm {

background: rgba(96, 165, 250, 0.2);

color: #60a5fa;

.stat-content h6 {
font-size: 0.7rem;

text-transform: uppercase;

color: var(--text-muted);

margin: 0;

white-space: nowrap;

.stat-content h4 {

font-size: 1.1rem;

font-weight: 700;

margin: 0;

color: var(--text-main);

.esc-badge {

display: inline-flex;

align-items: center;

gap: 0.35rem;

padding: 0.2rem 0.5rem;

background: var(--surface);

border: 1px solid var(--border);

border-radius: 4px;

font-size: 0.7rem;

margin-right: 0.5rem;

white-space: nowrap;

color: var(--text-main);

[data-theme="dark"] .esc-badge {
background: #334155;

border-color: #475569;

color: #f8fafc;

.affectation-stat-badge {

display: inline-flex;

align-items: center;

gap: 0.35rem;

padding: 0.35rem 0.75rem;

border-radius: 6px;

font-size: 0.75rem;

font-weight: 600;

margin-right: 0.5rem;

margin-bottom: 0.25rem;

white-space: nowrap;

border: 1px solid transparent;

cursor: pointer;

transition: all 0.2s;

.affectation-stat-badge:hover {

transform: scale(1.05);

.aff-stat-officiel { background: #dbeafe; color: #1e40af; border-color:


#93c5fd; }

.aff-stat-banque { background: #dcfce7; color: #166534; border-


color: #86efac; }
.aff-stat-recom { background: #fef3c7; color: #92400e; border-color:
#fcd34d; }

.aff-stat-rapport { background: #f3e8ff; color: #7c3aed; border-color:


#c4b5fd; }

[data-theme="dark"] .aff-stat-officiel { background: rgba(96, 165,


250, 0.2); color: #60a5fa; border-color: #3b82f6; }

[data-theme="dark"] .aff-stat-banque { background: rgba(74, 222,


128, 0.2); color: #4ade80; border-color: #22c55e; }

[data-theme="dark"] .aff-stat-recom { background: rgba(251, 191,


36, 0.2); color: #fbbf24; border-color: #f59e0b; }

[data-theme="dark"] .aff-stat-rapport { background: rgba(167, 139,


250, 0.2); color: #a78bfa; border-color: #8b5cf6; }

.main-content {

flex: 1;

overflow: hidden;

display: flex;

flex-direction: column;

.data-panel {

background: var(--surface);

flex: 1;

display: flex;

flex-direction: column;

overflow: hidden;

.panel-header {

padding: 0.75rem 1.5rem;


border-bottom: 1px solid var(--border);

display: flex;

justify-content: space-between;

align-items: center;

background: linear-gradient(to right, rgba(30, 64, 175, 0.03),


transparent);

gap: 1rem;

flex-wrap: wrap;

[data-theme="dark"] .panel-header {

background: linear-gradient(to right, rgba(59, 130, 246, 0.05),


transparent);

.search-box {

position: relative;

min-width: 200px;

max-width: 300px;

.search-box i {

position: absolute;

left: 0.75rem;

top: 50%;

transform: translateY(-50%);

color: var(--text-muted);

font-size: 0.875rem;

}
.search-box input {

padding-left: 2rem;

border-radius: 6px;

background-color: var(--input-bg);

color: var(--input-text);

border: 1px solid var(--border);

font-size: 0.875rem;

height: 36px;

.search-active {

border-color: var(--primary) !important;

box-shadow: 0 0 0 3px rgba(30, 64, 175, 0.1) !important;

.filter-bar {

display: flex;

gap: 0.5rem;

padding: 0.5rem 1.5rem;

background: rgba(30, 64, 175, 0.02);

border-bottom: 1px solid var(--border);

align-items: center;

flex-wrap: wrap;

[data-theme="dark"] .filter-bar {

background: rgba(96, 165, 250, 0.03);

}
.filter-badge {

padding: 0.25rem 0.75rem;

border-radius: 20px;

font-size: 0.75rem;

border: 1px solid var(--border);

cursor: pointer;

transition: all 0.2s;

background: var(--surface);

color: var(--text-muted);

.filter-badge:hover, .[Link] {

background: var(--primary);

color: white;

border-color: var(--primary);

.table-responsive {

flex: 1;

overflow: auto;

padding: 0 1.5rem 1.5rem 1.5rem;

background-color: var(--surface);

[data-theme="dark"] .table-responsive {

background-color: #0f172a;

.table {
width: 100%;

margin-bottom: 0;

font-size: 0.8125rem;

color: var(--text-main);

background-color: transparent;

[data-theme="dark"] .table {

color: #e2e8f0 !important;

.table thead th {

background-color: var(--surface);

position: sticky;

top: 0;

z-index: 10;

font-weight: 600;

font-size: 0.7rem;

text-transform: uppercase;

letter-spacing: 0.5px;

color: var(--text-muted);

border-bottom: 2px solid var(--border);

padding: 0.3rem 0.5rem;

cursor: pointer;

white-space: nowrap;

user-select: none;

[data-theme="dark"] .table thead th {


background-color: #1e293b;

color: #94a3b8;

border-bottom: 2px solid #475569;

.table tbody td {

padding: 0.25rem 0.5rem;

vertical-align: middle;

border-bottom: 1px solid var(--border);

color: var(--text-main);

line-height: 1.3;

[data-theme="dark"] .table tbody td {

color: #e2e8f0 !important;

border-bottom: 1px solid #334155;

background-color: #1e293b;

[data-theme="dark"] .table tbody tr:nth-child(even) td {

background-color: #252f47;

[data-theme="dark"] .table tbody tr:hover td {

background-color: #334155 !important;

color: #ffffff !important;

}
[data-theme="dark"] .col-denomination { color: #60a5fa !important;
font-weight: 600; }

[data-theme="dark"] .col-adresse { color: #cbd5e1 !important; }

[data-theme="dark"] .col-policiers { color: #e2e8f0 !important; }

[data-theme="dark"] .col-number { color: #60a5fa !important; font-


weight: 600; }

[data-theme="dark"] .font-monospace { color: #94a3b8 !


important; }

[data-theme="dark"] .text-muted { color: #94a3b8 !important; }

.col-denomination {

min-width: 180px;

font-weight: 600;

color: var(--primary);

.col-adresse {

min-width: 200px;

max-width: 250px;

white-space: normal;

font-size: 0.75rem;

line-height: 1.3;

.col-policiers {

min-width: 200px;

max-width: 300px;

white-space: pre-wrap;

font-size: 0.75rem;

line-height: 1.4;
text-align: left;

vertical-align: top;

position: relative;

.col-policiers::-webkit-scrollbar {

display: none;

.col-policiers {

-ms-overflow-style: none;

scrollbar-width: none;

.col-number {

text-align: center;

font-weight: 600;

color: var(--primary);

.contract-alert {

display: inline-flex;

align-items: center;

gap: 0.25rem;

padding: 0.15rem 0.5rem;

border-radius: 4px;

font-size: 0.7rem;

font-weight: 600;

margin-left: 0.5rem;
}

.contract-expired {

background: rgba(220, 38, 38, 0.1);

color: var(--expired);

border: 1px solid rgba(220, 38, 38, 0.2);

[data-theme="dark"] .contract-expired {

background: rgba(239, 68, 68, 0.2);

color: #f87171;

.contract-expiring {

background: rgba(245, 158, 11, 0.1);

color: var(--expiring);

border: 1px solid rgba(245, 158, 11, 0.2);

[data-theme="dark"] .contract-expiring {

background: rgba(251, 191, 36, 0.2);

color: #fbbf24;

.contract-valid {

background: rgba(5, 150, 105, 0.1);

color: var(--valid);

border: 1px solid rgba(5, 150, 105, 0.2);

}
[data-theme="dark"] .contract-valid {

background: rgba(74, 222, 128, 0.2);

color: #4ade80;

.badge-affectation {

padding: 0.25em 0.5em;

border-radius: 4px;

font-weight: 600;

font-size: 0.65rem;

text-transform: uppercase;

letter-spacing: 0.5px;

.aff-officiel { background: #dbeafe; color: #1e40af; }

.aff-banque { background: #dcfce7; color: #166534; }

.aff-recom { background: #fef3c7; color: #92400e; }

.aff-rapport { background: #f3e8ff; color: #7c3aed; }

[data-theme="dark"] .aff-officiel { background: rgba(96, 165, 250,


0.2); color: #60a5fa; }

[data-theme="dark"] .aff-banque { background: rgba(74, 222, 128,


0.2); color: #4ade80; }

[data-theme="dark"] .aff-recom { background: rgba(251, 191, 36,


0.2); color: #fbbf24; }

[data-theme="dark"] .aff-rapport { background: rgba(167, 139, 250,


0.2); color: #a78bfa; }

[data-theme="dark"] .[Link]-secondary {
background-color: #475569 !important;

color: #f8fafc !important;

.action-btn {

width: 28px;

height: 28px;

border-radius: 4px;

border: none;

display: inline-flex;

align-items: center;

justify-content: center;

cursor: pointer;

transition: all 0.2s;

margin: 0 1px;

font-size: 0.75rem;

.btn-edit { background: rgba(59, 130, 246, 0.1); color: #2563eb; }

[data-theme="dark"] .btn-edit { background: rgba(96, 165, 250, 0.2);


color: #60a5fa; }

.btn-delete { background: rgba(239, 68, 68, 0.1); color: #dc2626; }

[data-theme="dark"] .btn-delete { background: rgba(248, 113, 113,


0.2); color: #f87171; }

.[Link] {

opacity: 0.5;

cursor: not-allowed;

pointer-events: none;
}

.select-checkbox {

width: 18px;

height: 18px;

cursor: pointer;

accent-color: var(--primary);

.select-all-checkbox {

width: 18px;

height: 18px;

cursor: pointer;

accent-color: var(--primary);

.sync-status {

display: inline-flex;

align-items: center;

gap: 0.35rem;

font-size: 0.75rem;

padding: 0.25rem 0.5rem;

border-radius: 4px;

background: #f0fdf4;

color: #166534;

border: 1px solid #bbf7d0;

cursor: pointer;

transition: all 0.2s;

}
.sync-status:hover {

transform: scale(1.05);

[data-theme="dark"] .sync-status {

background: rgba(22, 101, 52, 0.2);

color: #86efac;

border-color: rgba(134, 239, 172, 0.3);

.[Link] {

animation: pulse 1.5s infinite;

.toast-notification {

position: fixed;

bottom: 20px;

right: 20px;

background: var(--success);

color: white;

padding: 0.75rem 1.25rem;

border-radius: 8px;

display: none;

align-items: center;

gap: 0.5rem;

z-index: 9999;

font-weight: 500;

box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);


}

.[Link] { display: flex; animation: slideIn 0.3s ease-


out; }

@keyframes slideIn {

from { transform: translateX(100%); opacity: 0; }

to { transform: translateX(0); opacity: 1; }

.[Link] {

background: var(--danger);

.[Link] {

background: var(--warning);

color: #000;

.dashboard-modal .modal-body {

padding: 1.5rem;

.chart-container {

position: relative;

height: 300px;

margin-bottom: 1.5rem;

.stat-card {
background: linear-gradient(135deg, rgba(30, 64, 175, 0.05) 0%,
rgba(59, 130, 246, 0.05) 100%);

border: 1px solid var(--border);

border-radius: 12px;

padding: 1rem;

text-align: center;

transition: all 0.3s;

.stat-card:hover {

transform: translateY(-2px);

box-shadow: var(--shadow);

[data-theme="dark"] .stat-card {

background: linear-gradient(135deg, rgba(96, 165, 250, 0.1) 0%,


rgba(59, 130, 246, 0.1) 100%);

border-color: #334155;

.stat-card h3 {

font-size: 2rem;

font-weight: 700;

color: var(--primary);

margin: 0;

.stat-card p {

font-size: 0.875rem;

color: var(--text-muted);
margin: 0.5rem 0 0 0;

.ocr-container {

display: grid;

grid-template-columns: 1fr 1fr;

gap: 1.5rem;

height: auto;

max-height: 70vh;

@media (max-width: 768px) {

.ocr-container {

grid-template-columns: 1fr;

gap: 1rem;

.ocr-image-preview {

background: #f1f5f9;

border-radius: 8px;

display: flex;

align-items: center;

justify-content: center;

overflow: hidden;

border: 2px dashed #cbd5e1;

min-height: 250px;

height: 100%;

}
[data-theme="dark"] .ocr-image-preview {

background: #334155;

border-color: #475569;

.ocr-text-area {

font-family: monospace;

font-size: 0.8rem;

padding: 0.75rem;

border: 1px solid var(--border);

border-radius: 8px;

resize: none;

background-color: var(--input-bg);

color: var(--input-text);

min-height: 100px;

width: 100%;

.form-section {

background: rgba(30, 64, 175, 0.03);

border-radius: 8px;

padding: 1.25rem;

border: 1px solid var(--border);

height: fit-content;

.form-section-title {

font-weight: 600;
margin-bottom: 1rem;

color: var(--primary);

font-size: 0.9rem;

text-transform: uppercase;

letter-spacing: 0.5px;

[data-theme="dark"] .form-section {

background: rgba(96, 165, 250, 0.05);

[data-theme="dark"] .modal-content {

background-color: #1e293b;

color: #f8fafc;

border: 1px solid #334155;

[data-theme="dark"] .modal-header {

background-color: #1e40af;

border-bottom: 1px solid #334155;

.autosave-indicator {

position: fixed;

top: 70px;

right: 20px;

background: var(--success);

color: white;

padding: 0.5rem 1rem;


border-radius: 20px;

font-size: 0.75rem;

opacity: 0;

transition: opacity 0.3s;

z-index: 999;

display: flex;

align-items: center;

gap: 0.5rem;

.[Link] {

opacity: 1;

.offline-indicator {

position: fixed;

top: 0;

left: 0;

right: 0;

background: #dc2626;

color: white;

text-align: center;

padding: 0.5rem;

font-size: 0.875rem;

z-index: 10001;

display: none;

.[Link] { display: block; }


@keyframes pulse {

0%, 100% { opacity: 1; }

50% { opacity: 0.5; }

@media (max-width: 768px) {

.stats-bar { padding: 0.5rem; gap: 0.5rem; }

.filter-bar { padding: 0.5rem; }

.panel-header { padding: 0.75rem; }

.search-box { max-width: 100%; width: 100%; }

.action-buttons { width: 100%; justify-content: stretch; }

.action-buttons .btn { flex: 1; font-size: 0.75rem; padding:


0.375rem; }

.table-responsive { padding: 0 0.5rem 0.5rem 0.5rem; }

.ocr-image-preview { min-height: 200px; }

.app-header { padding: 0.5rem 1rem; }

.chart-container { height: 250px; }

.loader-logo { width: 80px; height: 80px; }

.hidden { display: none !important; }

@supports (padding: max(0px)) {

.app-header {

padding-left: max(1rem, env(safe-area-inset-left));

padding-right: max(1rem, env(safe-area-inset-right));

}
.badge-count-pol {

font-size: 0.65rem;

padding: 0.2em 0.5em;

margin-left: 0.5rem;

vertical-align: middle;

#scanModal .modal-body { min-height: 400px; }

#ocrText {

background-color: #f8fafc;

border: 1px solid #e2e8f0;

font-family: 'Courier New', monospace;

line-height: 1.5;

#ocrLoadingZone {

background: linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%);

border: 2px dashed #3b82f6;

[data-theme="dark"] #ocrLoadingZone {

background: linear-gradient(135deg, #1e293b 0%, #0f172a


100%);

border-color: #3b82f6;

[data-theme="dark"] #ocrText {

background-color: #1e293b;

border-color: #334155;

color: #e2e8f0;

}
#ocrInitialZone {

background: #f8fafc;

border: 2px dashed #cbd5e1;

[data-theme="dark"] #ocrInitialZone {

background: #1e293b;

border-color: #475569;

.offcanvas {

background-color: var(--surface);

color: var(--text-main);

[data-theme="dark"] .offcanvas {

background-color: #1e293b;

color: #f8fafc;

.offcanvas-header {

border-bottom: 1px solid var(--border);

.mobile-menu-item {

padding: 0.75rem 1rem;

border-bottom: 1px solid var(--border);

display: flex;

align-items: center;

gap: 1rem;

color: var(--text-main);

text-decoration: none;

cursor: pointer;
}

.mobile-menu-item:active {

background-color: rgba(30, 64, 175, 0.05);

.mobile-section-title {

padding: 1rem 1rem 0.5rem;

font-size: 0.75rem;

text-transform: uppercase;

color: var(--text-muted);

font-weight: 600;

.role-badge {

font-size: 0.65rem;

padding: 0.25rem 0.5rem;

border-radius: 4px;

font-weight: 600;

text-transform: uppercase;

letter-spacing: 0.5px;

.role-admin { background: #fecaca; color: #991b1b; }

.role-lecteur { background: #bfdbfe; color: #1e40af; }

[data-theme="dark"] .role-admin { background: rgba(248, 113, 113,


0.2); color: #f87171; }

[data-theme="dark"] .role-lecteur { background: rgba(96, 165, 250,


0.2); color: #60a5fa; }

.readonly-field {

background-color: rgba(128, 128, 128, 0.1) !important;

cursor: not-allowed;
}

.advanced-filters {

background: rgba(30, 64, 175, 0.02);

border-bottom: 1px solid var(--border);

padding: 0.75rem 1.5rem;

display: none;

.[Link] {

display: block;

.filter-group {

display: flex;

gap: 1rem;

align-items: center;

flex-wrap: wrap;

.filter-chip {

display: inline-flex;

align-items: center;

gap: 0.5rem;

padding: 0.35rem 0.75rem;

background: var(--surface);

border: 1px solid var(--border);

border-radius: 20px;

font-size: 0.75rem;
cursor: pointer;

transition: all 0.2s;

.[Link] {

background: var(--primary);

color: white;

border-color: var(--primary);

[data-theme="dark"] .filter-chip {

background: #334155;

border-color: #475569;

/* Loading spinner pour synchronisation */

.sync-spinner {

display: inline-block;

width: 12px;

height: 12px;

border: 2px solid rgba(255,255,255,.3);

border-radius: 50%;

border-top-color: #fff;

animation: spin 1s ease-in-out infinite;

@keyframes spin {

to { transform: rotate(360deg); }

}
/* Indicateur de données non synchronisées */

.unsaved-indicator {

position: fixed;

top: 70px;

right: 20px;

background: var(--warning);

color: #000;

padding: 0.5rem 1rem;

border-radius: 20px;

font-size: 0.75rem;

display: none;

align-items: center;

gap: 0.5rem;

z-index: 999;

.[Link] {

display: flex;

</style>

</head>

<body>

<!-- Loader Professionnel -->

<div id="appLoader" class="app-loader">

<div class="loader-content">

<div class="loader-logo">

<img src="[Link]" alt="GPI-HP"


onerror="[Link]='none'; [Link]='<i
class=\'fas fa-shield-alt fa-3x\'></i>';">
</div>

<div class="loader-spinner"></div>

<div class="loader-text" id="loaderText">Initialisation...</div>

<div class="loader-progress">

<div class="loader-progress-bar"
id="loaderProgressBar"></div>

</div>

</div>

</div>

<div class="offline-indicator" id="offlineIndicator">

<i class="fas fa-wifi-slash me-2"></i>Mode hors ligne - Les données


seront synchronisées à la reconnexion

</div>

<div class="unsaved-indicator" id="unsavedIndicator">

<i class="fas fa-cloud-upload-alt"></i> Modifications non


synchronisées

</div>

<div class="install-banner" id="installBanner">

<div class="d-flex align-items-center gap-3">

<img src="[Link]" alt="GPI-HP" style="width: 48px; height:


48px; border-radius: 12px; object-fit: contain; background: white; padding:
4px;">

<div>

<strong>Installer GPI-HP</strong>

<div class="small">Ajoutez l'application à votre écran


d'accueil</div>

</div>

</div>
<div class="d-flex gap-2">

<button class="btn btn-light btn-sm"


onclick="installPWA()">Installer</button>

<button class="btn btn-outline-light btn-sm"


onclick="dismissInstall()">Plus tard</button>

</div>

</div>

<div class="autosave-indicator" id="autosaveIndicator">

<i class="fas fa-check-circle"></i> Auto-sauvegardé

</div>

<!-- Écran de connexion -->

<div id="lock-screen">

<div class="police-logo">

<img src="[Link]" alt="GPI-HP Logo" style="display: none;"


onload="[Link]='block'" onerror="[Link]='none';
[Link]='<i class=\'fas fa-shield-alt\'></i>';">

</div>

<h2 class="mb-1">GPI-HP</h2>

<p class="opacity-75 mb-4">Gestion des Détachements de


Police</p>

<div class="login-form">

<!-- Mode Invité (Lecture seule) -->

<div id="guestModeSection" class="mb-4">

<button class="btn btn-outline-light w-100 mb-3"


onclick="enterGuestMode()">

<i class="fas fa-user me-2"></i>Accès Invité (Lecture seule)

</button>

<div class="text-white-50 small mb-2">ou</div>


</div>

<!-- Mode Admin (Authentification requise) -->

<div id="adminAuthSection">

<div class="mb-3">

<label class="form-label text-white text-start d-block fw-


bold">Email Administrateur</label>

<div class="input-group">

<span class="input-group-text bg-white border-0"><i


class="fas fa-envelope text-primary"></i></span>

<input type="email" class="form-control border-0"


id="loginEmail" placeholder="admin@[Link]">

</div>

</div>

<div class="mb-4">

<label class="form-label text-white text-start d-block fw-


bold">Mot de passe</label>

<div class="input-group">

<span class="input-group-text bg-white border-0"><i


class="fas fa-lock text-primary"></i></span>

<input type="password" class="form-control border-0"


id="loginPassword" placeholder="••••••••">

</div>

</div>

<button class="btn btn-light w-100 fw-bold text-primary mb-2"


onclick="signInAdmin()">

<i class="fas fa-unlock me-2"></i>CONNEXION ADMIN

</button>

</div>

<div id="userInfo" style="display: none;" class="text-start">


<div class="alert alert-success bg-white text-success">

<i class="fas fa-check-circle me-2"></i>Connecté en tant


que <strong id="userEmail"></strong>

</div>

<div class="mb-3">

<label class="form-label text-white">Mode:</label>

<div class="form-control bg-light" id="roleDisplayInfo"


style="font-weight: 600;">Invité</div>

</div>

<button class="btn btn-light w-100 fw-bold text-primary"


onclick="completeLogin()">

<i class="fas fa-arrow-right me-2"></i>ACCÉDER À


L'APPLICATION

</button>

<button class="btn btn-link text-white w-100 mt-2"


onclick="logout()">

<i class="fas fa-sign-out-alt me-2"></i>Changer de mode

</button>

</div>

</div>

<div class="mt-4 text-white-50 small">

<i class="fas fa-shield-alt me-1"></i> Application sécurisée - v3.0


PWA

</div>

</div>

<div class="offcanvas offcanvas-start" tabindex="-1" id="mobileMenu"


aria-labelledby="mobileMenuLabel">

<div class="offcanvas-header">

<div class="brand">
<div class="brand-logo">

<img src="[Link]" alt="Logo" style="width: 100%; height:


100%; object-fit: contain; padding: 2px; filter: brightness(0) invert(1);"
onerror="[Link]='none'; [Link]='<i
class=\'fas fa-shield-alt\'></i>';">

</div>

<div style="font-size: 1.1rem;">Menu</div>

</div>

<button type="button" class="btn-close text-reset" data-bs-


dismiss="offcanvas" aria-label="Close"></button>

</div>

<div class="offcanvas-body p-0">

<div class="mobile-section-title">Analytiques</div>

<div class="mobile-menu-item" onclick="openDashboard()">

<i class="fas fa-chart-line text-primary"></i> Tableau de bord

</div>

<div class="mobile-section-title">Système</div>

<div class="mobile-menu-item" onclick="syncData()">

<i class="fas fa-cloud text-primary"></i> Synchroniser Firebase

</div>

<div class="mobile-menu-item" onclick="toggleTheme()">

<i class="fas fa-adjust"></i> Changer Thème

</div>

<div class="mobile-menu-item" onclick="lockApp()">

<i class="fas fa-lock text-danger"></i> Verrouiller

</div>

<div class="mobile-menu-item" onclick="toggleFullscreen()">

<i class="fas fa-expand"></i> Plein écran

</div>
<div class="mobile-section-title">Filtres</div>

<div class="p-3">

<label class="small text-muted mb-2">Affectation</label>

<select class="form-select form-select-sm mb-3"


onchange="filterByAffectation([Link])">

<option value="">Tous</option>

<option value="OFFICIEL">OFFICIEL</option>

<option value="BANQUE">BANQUE</option>

<option
value="RECOMANDATION">RECOMANDATION</option>

<option value="RAPPORT">RAPPORT</option>

</select>

<label class="small text-muted mb-2">Escadron</label>

<select class="form-select form-select-sm mb-3"


onchange="filterByESC([Link])">

<option value="">Tous les ESC</option>

<option value="UNITÉ AMIE">UNITÉ AMIE</option>

<option value="ESCADRON PROTECTION">ESCADRON


PROTECTION</option>

<option value="ESCADRON GARDE">ESCADRON


GARDE</option>

<option value="ESCADRON ESCORTE">ESCADRON


ESCORTE</option>

<option value="FILE ADM">FILE ADM</option>

</select>

<label class="small text-muted mb-2">Statut Contrat</label>

<select class="form-select form-select-sm"


onchange="filterByContractStatus([Link])">
<option value="">Tous</option>

<option value="expired">Expiré</option>

<option value="expiring">Expire bientôt</option>

<option value="valid">Valide</option>

</select>

</div>

<div class="mobile-section-title">Données</div>

<div class="mobile-menu-item admin-only"


onclick="[Link]('importFile').click()">

<i class="fas fa-file-import text-secondary"></i> Importer Excel

</div>

<div class="mobile-menu-item" onclick="exportExcel()">

<i class="fas fa-file-excel text-success"></i> Exporter Excel

</div>

<div class="mobile-menu-item text-danger admin-only"


onclick="deleteSelected()">

<i class="fas fa-trash"></i> Supprimer Sélection

</div>

</div>

</div>

<div id="app-container">

<header class="app-header">

<div class="d-flex align-items-center gap-3">

<button class="btn btn-link text-primary d-md-none p-0"


type="button" data-bs-toggle="offcanvas" data-bs-
target="#mobileMenu">

<i class="fas fa-bars fa-lg"></i>

</button>
<div class="brand">

<div class="brand-logo">

<img src="[Link]" alt="Logo" style="width: 100%;


height: 100%; object-fit: contain; padding: 2px; filter: brightness(0)
invert(1);" onerror="[Link]='none';
[Link]='<i class=\'fas fa-shield-alt\'></i>';">

</div>

<div style="font-size: 1.1rem;">GPI-HP</div>

</div>

</div>

<div class="d-none d-md-flex align-items-center gap-2">

<button class="btn btn-outline-primary btn-sm"


onclick="openDashboard()" title="Tableau de bord">

<i class="fas fa-chart-pie me-1"></i>Dashboard

</button>

<div class="sync-status" id="syncStatus" onclick="syncData()"


title="Cliquer pour synchroniser">

<i class="fas fa-cloud"></i>

<span id="syncText">Firebase</span>

</div>

<button class="btn btn-outline-secondary btn-sm"


onclick="toggleFullscreen()" title="Plein écran (F11)">

<i class="fas fa-expand" id="fullscreenIcon"></i>

</button>

<button class="btn btn-outline-secondary btn-sm"


onclick="toggleTheme()">
<i class="fas fa-moon" id="themeIcon"></i>

</button>

<div class="d-flex align-items-center gap-2">

<span class="badge-police" id="userDisplay">USER</span>

<span class="role-badge role-admin"


id="roleDisplay">Admin</span>

</div>

<button class="btn btn-outline-danger btn-sm"


onclick="lockApp()">

<i class="fas fa-lock"></i>

</button>

</div>

</header>

<div class="stats-bar">

<div class="stat-item" onclick="filterByAffectation('')">

<div class="stat-icon-sm"><i class="fas


fa-clipboard-list"></i></div>

<div class="stat-content">

<h6>Total Sites</h6>

<h4 id="totalSites">0</h4>

</div>

</div>

<div class="stat-item" onclick="openDashboard()">

<div class="stat-icon-sm" style="background: rgba(5, 150, 105,


0.1); color: #059669;">

<i class="fas fa-user-shield"></i>

</div>
<div class="stat-content">

<h6>Policiers</h6>

<h4 id="totalPoliciers">0</h4>

</div>

</div>

<div class="stat-item" onclick="openDashboard()">

<div class="stat-icon-sm" style="background: rgba(217, 119, 6,


0.1); color: #d97706;">

<i class="fas fa-calculator"></i>

</div>

<div class="stat-content">

<h6>Moyenne</h6>

<h4 id="avgPoliciers">0</h4>

</div>

</div>

<div class="stat-item"
onclick="filterByContractStatus('expiring')">

<div class="stat-icon-sm" style="background: rgba(220, 38, 38,


0.1); color: #dc2626;">

<i class="fas fa-exclamation-triangle"></i>

</div>

<div class="stat-content">

<h6>Alertes</h6>

<h4 id="alertCount">0</h4>

<small>Contrats</small>

</div>

</div>
<div class="stat-item" style="flex: 1; min-width: 300px;"
onclick="openDashboard()">

<div class="stat-icon-sm" style="background: rgba(124, 58,


237, 0.1); color: #7c3aed;">

<i class="fas fa-chart-pie"></i>

</div>

<div class="stat-content" style="flex: 1;">

<h6>Répartition par Affectation</h6>

<div id="affectationStats" style="margin-top: 0.25rem;


display: flex; flex-wrap: wrap; gap: 0.25rem;"></div>

</div>

</div>

<div class="stat-item" style="flex: 1; min-width: 250px;"


onclick="openDashboard()">

<div class="stat-icon-sm" style="background: rgba(14, 165,


233, 0.1); color: #0ea5e9;">

<i class="fas fa-shield-alt"></i>

</div>

<div class="stat-content" style="flex: 1;">

<h6>Répartition par ESC</h6>

<div id="escStats" style="margin-top: 0.25rem;"></div>

</div>

</div>

</div>

<div class="filter-bar d-none d-md-flex">

<span class="text-muted small"><i class="fas fa-filter


me-2"></i>Filtres:</span>

<span class="filter-badge active" onclick="filterByAffectation('')"


id="filter-all">Tous</span>
<span class="filter-badge" onclick="filterByAffectation('OFFICIEL')"
id="filter-officiel" style="border-color: #1e40af; color:
#1e40af;">OFFICIEL</span>

<span class="filter-badge" onclick="filterByAffectation('BANQUE')"


id="filter-banque" style="border-color: #059669; color:
#059669;">BANQUE</span>

<span class="filter-badge"
onclick="filterByAffectation('RECOMANDATION')" id="filter-recom"
style="border-color: #d97706; color:
#d97706;">RECOMANDATION</span>

<span class="filter-badge"
onclick="filterByAffectation('RAPPORT')" id="filter-rapport" style="border-
color: #7c3aed; color: #7c3aed;">RAPPORT</span>

<div class="vr mx-2" style="height: 20px; opacity: 0.3;"></div>

<select class="form-select form-select-sm" style="width: auto;


min-width: 150px;" onchange="filterByESC([Link])" id="escFilter">

<option value="">Tous les ESC</option>

<option value="UNITÉ AMIE">UNITÉ AMIE</option>

<option value="ESCADRON PROTECTION">ESCADRON


PROTECTION</option>

<option value="ESCADRON GARDE">ESCADRON


GARDE</option>

<option value="ESCADRON ESCORTE">ESCADRON


ESCORTE</option>

<option value="FILE ADM">FILE ADM</option>

</select>

<button class="btn btn-sm btn-outline-secondary ms-2"


onclick="toggleAdvancedFilters()" title="Filtres avancés">

<i class="fas fa-sliders-h"></i>

</button>
<button class="btn btn-sm btn-outline-danger ms-auto admin-
only" onclick="deleteSelected()" id="btnDeleteSelected" disabled>

<i class="fas fa-trash me-1"></i>Supprimer (<span


id="selectedCount">0</span>)

</button>

</div>

<div class="advanced-filters d-none d-md-block"


id="advancedFilters">

<div class="filter-group">

<span class="text-muted small">Filtres avancés:</span>

<span class="filter-chip" onclick="toggleFilterChip(this,


'contract', 'expired')" data-filter="contract" data-value="expired">

<i class="fas fa-exclamation-circle text-danger"></i>


Contrats expirés

</span>

<span class="filter-chip" onclick="toggleFilterChip(this,


'contract', 'expiring')" data-filter="contract" data-value="expiring">

<i class="fas fa-clock text-warning"></i> Expirent bientôt

</span>

<span class="filter-chip" onclick="toggleFilterChip(this,


'munitions', 'zero')" data-filter="munitions" data-value="zero">

<i class="fas fa-circle text-secondary"></i> Sans munitions

</span>

<span class="filter-chip" onclick="toggleFilterChip(this,


'policiers', 'many')" data-filter="policiers" data-value="many">

<i class="fas fa-users text-primary"></i> +3 policiers

</span>

</div>

</div>

<div class="main-content">
<div class="data-panel">

<div class="panel-header">

<div class="d-flex align-items-center gap-2 flex-grow-1">

<h6 class="mb-0 fw-bold text-muted d-none d-md-block"


style="font-size: 0.8rem; text-transform: uppercase;">

<i class="fas fa-list me-2"></i>Détachements

</h6>

<div class="search-box flex-grow-1">

<i class="fas fa-search"></i>

<input type="text" class="form-control form-control-sm"


id="searchInput" placeholder="Rechercher (Ctrl+F)..."
onkeyup="debouncedSearch()">

<small class="text-muted" id="searchResults"


style="position: absolute; right: 10px; top: 50%; transform: translateY(-
50%); font-size: 0.7rem; display: none;"></small>

</div>

</div>

<div class="action-buttons d-flex gap-2">

<button class="btn btn-primary btn-sm admin-only"


onclick="openAddModal()" title="Ctrl+N">

<i class="fas fa-plus me-1"></i><span class="d-none


d-md-inline">Nouveau</span>

</button>

<button class="btn btn-outline-primary btn-sm admin-


only" onclick="openScanModal()" title="Scanner OCR">

<i class="fas fa-camera me-1"></i><span class="d-


none d-md-inline">Scan</span>

</button>

<div class="btn-group d-none d-md-inline-flex">


<button class="btn btn-outline-secondary btn-sm
admin-only" onclick="[Link]('importFile').click()"
title="Importer Excel">

<i class="fas fa-file-import"></i>

</button>

<input type="file" id="importFile" style="display: none"


accept=".xlsx,.xls" onchange="importExcel(event)">

<button class="btn btn-outline-success btn-sm"


onclick="exportExcel()" title="Exporter Excel">

<i class="fas fa-file-excel"></i>

</button>

</div>

</div>

</div>

<div class="table-responsive">

<table class="table" id="dataTable">

<thead>

<tr>

<th style="width: 30px;">

<input type="checkbox" class="select-all-


checkbox" onchange="toggleSelectAll(this)">

</th>

<th style="width: 40px;">N°</th>

<th style="min-width: 180px;">DÉNOMINATION</th>

<th style="min-width: 200px;">ADRESSE</th>

<th style="min-width: 200px;">NBRE DE POL</th>

<th style="min-width: 100px;">TYPE D'ARME</th>

<th style="width: 50px;">MUN</th>

<th style="width: 80px;">AFFECT</th>

<th style="min-width: 100px;">SUPERV.</th>


<th style="width: 110px;">ESC</th>

<th style="min-width: 120px;">CONTRAT</th>

<th style="min-width: 60px;">OBS</th>

<th style="width: 80px;" class="admin-


only">ACTIONS</th>

</tr>

</thead>

<tbody id="tableBody"></tbody>

</table>

</div>

</div>

</div>

</div>

<!-- Modal Formulaire -->

<div class="modal fade" id="formModal" tabindex="-1">

<div class="modal-dialog modal-lg">

<div class="modal-content">

<div class="modal-header bg-primary text-white py-2">

<h6 class="modal-title fw-bold"><i class="fas fa-edit me-


2"></i><span id="modalTitle">Nouveau</span></h6>

<button type="button" class="btn-close btn-close-white"


data-bs-dismiss="modal"></button>

</div>

<div class="modal-body p-3">

<form id="detachementForm">

<input type="hidden" id="editId">

<div class="row g-2">

<div class="col-md-6">
<label class="form-label small fw-
bold">Dénomination <span class="text-danger">*</span></label>

<input type="text" class="form-control form-control-


sm" id="denomination" required onchange="markDirty()">

</div>

<div class="col-md-6">

<label class="form-label small fw-bold">Adresse


<span class="text-danger">*</span></label>

<input type="text" class="form-control form-control-


sm" id="adresse" required onchange="markDirty()">

</div>

<div class="col-12">

<label class="form-label small fw-bold">Détails


Policiers <span class="text-danger">*</span></label>

<textarea class="form-control form-control-sm"


id="nbrePol" rows="2" required placeholder="[Link] NOM...
[Link] NOM..." onchange="markDirty()"></textarea>

<div class="form-text small text-muted">Format:


[Link] NOM Prénom (un par ligne ou espacé)</div>

</div>

<div class="col-md-3">

<label class="form-label small fw-bold">Type


Arme</label>

<input type="text" class="form-control form-control-


sm" id="typeArme" onchange="markDirty()">

</div>

<div class="col-md-2">

<label class="form-label small


fw-bold">Munitions</label>

<input type="number" class="form-control form-


control-sm" id="nbreMun" min="0" onchange="markDirty()">

</div>

<div class="col-md-3">
<label class="form-label small fw-
bold">Affectation</label>

<select class="form-select form-control-sm"


id="affectation" onchange="markDirty()">

<option value="OFFICIEL">OFFICIEL</option>

<option value="BANQUE">BANQUE</option>

<option
value="RECOMANDATION">RECOMANDATION</option>

<option value="RAPPORT">RAPPORT</option>

</select>

</div>

<div class="col-md-4">

<label class="form-label small fw-bold">ESC <span


class="text-danger">*</span></label>

<select class="form-select form-control-sm" id="esc"


required onchange="markDirty()">

<option value="" disabled


selected>Choisir...</option>

<option value="UNITÉ AMIE">UNITÉ


AMIE</option>

<option value="ESCADRON
PROTECTION">ESCADRON PROTECTION</option>

<option value="ESCADRON GARDE">ESCADRON


GARDE</option>

<option value="ESCADRON ESCORTE">ESCADRON


ESCORTE</option>

<option value="FILE ADM">FILE ADM</option>

</select>

</div>

<div class="col-md-6">

<label class="form-label small fw-


bold">Superviseur</label>
<input type="text" class="form-control form-control-
sm" id="superviseur" onchange="markDirty()">

</div>

<div class="col-md-3">

<label class="form-label small fw-bold">Début


Contrat</label>

<input type="date" class="form-control form-control-


sm" id="dateDebut" onchange="markDirty(); validateDates()">

</div>

<div class="col-md-3">

<label class="form-label small fw-bold">Fin


Contrat</label>

<input type="date" class="form-control form-control-


sm" id="dateFin" onchange="markDirty(); validateDates()">

<small class="text-danger" id="dateError"


style="display: none; font-size: 0.7rem;">

<i class="fas fa-exclamation-triangle"></i> Date


fin doit être après début

</small>

</div>

<div class="col-md-6">

<label class="form-label small fw-bold">N°


Contrat</label>

<input type="text" class="form-control form-control-


sm" id="numeroContrat" onchange="markDirty()">

</div>

<div class="col-12">

<label class="form-label small fw-


bold">Observations</label>

<input type="text" class="form-control form-control-


sm" id="obs" onchange="markDirty()">

</div>

</div>
<div class="mt-3 p-2 rounded small admin-only"
style="background: rgba(30,64,175,0.05); font-size: 0.75rem; color: var(--
text-muted);">

<i class="fas fa-info-circle me-1"></i> Les modifications


sont auto-sauvegardées localement. Cliquez sur l'icône cloud pour
synchroniser avec Firebase.

</div>

</form>

</div>

<div class="modal-footer py-2">

<button type="button" class="btn btn-outline-secondary btn-


sm" data-bs-dismiss="modal">Fermer (Échap)</button>

<button type="button" class="btn btn-primary btn-sm admin-


only" id="btnSaveData" onclick="saveData()">

<i class="fas fa-save me-1"></i>Enregistrer (Ctrl+S)

</button>

</div>

</div>

</div>

</div>

<!-- Modal Scan OCR -->

<div class="modal fade" id="scanModal" tabindex="-1">

<div class="modal-dialog modal-lg modal-dialog-scrollable">

<div class="modal-content">

<div class="modal-header bg-primary text-white py-2">

<h6 class="modal-title fw-bold"><i class="fas fa-camera me-


2"></i>Scanner Bulletin</h6>

<button type="button" class="btn-close btn-close-white"


data-bs-dismiss="modal"></button>

</div>

<div class="modal-body p-3">


<div class="row g-3">

<div class="col-md-6">

<div class="mb-3">

<input type="file" class="form-control form-control-


sm" id="ocrInput" accept="image/*" capture="environment"
onchange="processOCR(this)">

</div>

<div id="ocrLoadingZone" style="display: none;"


class="text-center p-4 bg-light rounded border">

<div class="mb-3">

<i class="fas fa-spinner fa-spin fa-2x text-


primary"></i>

</div>

<h6 class="text-primary fw-bold">Analyse en


cours...</h6>

<p class="small text-muted mb-3">Veuillez patienter


pendant l'extraction du texte</p>

<div class="progress" style="height: 8px;">

<div class="progress-bar progress-bar-striped


progress-bar-animated bg-primary" id="ocrBar" style="width:
0%"></div>

</div>

<small class="text-muted mt-2 d-block"


id="ocrPercent">0%</small>

</div>

<div id="ocrResultZone" style="display: none;">

<label class="form-label small fw-bold text-muted


text-uppercase">Texte Extrait</label>

<textarea class="form-control font-monospace small


bg-light" id="ocrText" rows="12" placeholder="Le texte extrait apparaîtra
ici..." style="font-size: 0.8rem; resize: none;"></textarea>
<button class="btn btn-sm btn-outline-secondary mt-
2 w-100" onclick="[Link]('ocrInput').click()">

<i class="fas fa-redo me-1"></i>Recommencer

</button>

</div>

<div id="ocrInitialZone" class="text-center p-4 text-


muted border rounded bg-light">

<i class="fas fa-camera fa-2x mb-2"></i>

<p class="small mb-0">Prenez une photo ou importez


une image pour commencer l'analyse</p>

</div>

</div>

<div class="col-md-6">

<div class="form-section h-100">

<div class="form-section-title">Saisie Rapide</div>

<input type="text" class="form-control form-control-


sm mb-2" id="ocrDenomination" placeholder="Dénomination">

<input type="text" class="form-control form-control-


sm mb-2" id="ocrAdresse" placeholder="Adresse">

<textarea class="form-control form-control-sm mb-2"


id="ocrPoliciers" rows="3" placeholder="Policiers ([Link]
Nom...)"></textarea>

<div class="row g-2 mb-2">

<div class="col-8"><input type="text"


class="form-control form-control-sm" id="ocrArme" placeholder="Type
Arme"></div>

<div class="col-4"><input type="number"


class="form-control form-control-sm" id="ocrMun"
placeholder="Mun"></div>

</div>
<select class="form-select form-select-sm mb-2"
id="ocrAffectation">

<option value="OFFICIEL">OFFICIEL</option>

<option value="BANQUE">BANQUE</option>

<option
value="RECOMANDATION">RECOMANDATION</option>

<option value="RAPPORT">RAPPORT</option>

</select>

<select class="form-select form-select-sm mb-2"


id="ocrEsc">

<option value="" disabled selected>Choisir


ESC...</option>

<option value="UNITÉ AMIE">UNITÉ


AMIE</option>

<option value="ESCADRON
PROTECTION">ESCADRON PROTECTION</option>

<option value="ESCADRON GARDE">ESCADRON


GARDE</option>

<option value="ESCADRON ESCORTE">ESCADRON


ESCORTE</option>

<option value="FILE ADM">FILE ADM</option>

</select>

<input type="text" class="form-control form-control-


sm mb-3" id="ocrSuperviseur" placeholder="Superviseur">

<button class="btn btn-success btn-sm w-100"


onclick="transferOCRData()">

<i class="fas fa-arrow-right me-2"></i>Transférer


vers le formulaire

</button>

</div>

</div>

</div>

</div>
</div>

</div>

</div>

<!-- Modal Dashboard -->

<div class="modal fade dashboard-modal" id="dashboardModal"


tabindex="-1">

<div class="modal-dialog modal-xl modal-dialog-scrollable">

<div class="modal-content">

<div class="modal-header bg-primary text-white py-2">

<h6 class="modal-title fw-bold"><i class="fas fa-chart-line


me-2"></i>Tableau de Bord Analytique</h6>

<button type="button" class="btn-close btn-close-white"


data-bs-dismiss="modal"></button>

</div>

<div class="modal-body">

<div class="row g-3 mb-4">

<div class="col-md-3">

<div class="stat-card">

<h3 id="dashTotalSites">0</h3>

<p><i class="fas fa-clipboard-list me-1"></i>Sites


Total</p>

</div>

</div>

<div class="col-md-3">

<div class="stat-card" style="background: linear-


gradient(135deg, rgba(5, 150, 105, 0.1) 0%, rgba(16, 185, 129, 0.1)
100%);">

<h3 id="dashTotalPoliciers" style="color:


#059669;">0</h3>

<p><i class="fas fa-user-shield


me-1"></i>Policiers</p>
</div>

</div>

<div class="col-md-3">

<div class="stat-card" style="background: linear-


gradient(135deg, rgba(217, 119, 6, 0.1) 0%, rgba(245, 158, 11, 0.1)
100%);">

<h3 id="dashAvgPoliciers" style="color:


#d97706;">0</h3>

<p><i class="fas fa-calculator


me-1"></i>Moyenne/Site</p>

</div>

</div>

<div class="col-md-3">

<div class="stat-card" style="background: linear-


gradient(135deg, rgba(220, 38, 38, 0.1) 0%, rgba(239, 68, 68, 0.1)
100%);">

<h3 id="dashAlertCount" style="color:


#dc2626;">0</h3>

<p><i class="fas fa-exclamation-triangle


me-1"></i>Alertes</p>

</div>

</div>

</div>

<div class="row g-4">

<div class="col-md-6">

<h6 class="fw-bold mb-3 text-muted text-uppercase"


style="font-size: 0.8rem;">

<i class="fas fa-chart-pie me-2"></i>Répartition par


Affectation

</h6>

<div class="chart-container">
<canvas id="affectationChart"></canvas>

</div>

</div>

<div class="col-md-6">

<h6 class="fw-bold mb-3 text-muted text-uppercase"


style="font-size: 0.8rem;">

<i class="fas fa-chart-bar me-2"></i>Répartition par


ESC

</h6>

<div class="chart-container">

<canvas id="escChart"></canvas>

</div>

</div>

<div class="col-md-6">

<h6 class="fw-bold mb-3 text-muted text-uppercase"


style="font-size: 0.8rem;">

<i class="fas fa-chart-line me-2"></i>Statut des


Contrats

</h6>

<div class="chart-container">

<canvas id="contractChart"></canvas>

</div>

</div>

<div class="col-md-6">

<h6 class="fw-bold mb-3 text-muted text-uppercase"


style="font-size: 0.8rem;">

<i class="fas fa-users me-2"></i>Policiers par


Affectation

</h6>

<div class="chart-container">

<canvas id="policiersChart"></canvas>
</div>

</div>

</div>

<div class="mt-4">

<h6 class="fw-bold mb-3 text-muted text-uppercase"


style="font-size: 0.8rem;">

<i class="fas fa-table me-2"></i>Détails par Affectation

</h6>

<div class="table-responsive">

<table class="table table-sm" id="dashboardTable">

<thead>

<tr>

<th>Affectation</th>

<th class="text-center">Sites</th>

<th class="text-center">Policiers</th>

<th class="text-center">Moyenne</th>

<th class="text-center">% Total</th>

</tr>

</thead>

<tbody id="dashboardTableBody"></tbody>

</table>

</div>

</div>

</div>

</div>

</div>

</div>
<div class="toast-notification" id="toast">

<i class="fas fa-check-circle"></i>

<span id="toastMessage">Opération réussie</span>

</div>

<script
src="[Link]
[Link]"></script>

<script>

//
==========================================

// CONFIGURATION FIREBASE

//
==========================================

const firebaseConfig = {

apiKey: "AIzaSyBLJNwcikTa9oCS4REmI2yYU36mq1CtOOk",

authDomain: "[Link]",

projectId: "repertoire-gpi-hp-6bdbd",

storageBucket: "[Link]",

messagingSenderId: "703294449051",

appId: "1:703294449051:web:4f3db08bc824b3007fed3e"

};

//
==========================================

// VARIABLES GLOBALES

//
==========================================

let db = null;

let auth = null;

let isOnline = [Link];


let data = [];

let deferredPrompt = null;

let currentFilter = { affectation: '', esc: '', contractStatus: '',


advanced: {} };

let selectedIds = new Set();

let autoSaveInterval = null;

let isDirty = false;

let searchDebounceTimer = null;

let currentUser = { uid: null, email: null, role: 'lecteur', isAdmin: false,
isGuest: false };

let charts = {};

let unsavedChanges = false;

let syncInProgress = false;

let loaderTimeout = null;

//
==========================================

// DONNÉES PAR DÉFAUT

//
==========================================

const defaultData = [

id: 1,

denomination: "RESIDENCE DU DG DE L'OGEFREM",

adresse: "Av/De la revolution coin Av/Idiofa, Q/Baudoin, C/L'shi",

nbrePol: "[Link] MANIONGO MBAMBI\[Link] LUMBALA


ILUNGA\n3.AP2CL BANZE DIESE Paul",

typeArme: "AKA CDPN19688H920",

nbreMun: 20,

affectation: "OFFICIEL",

superviseur: "CAP BOAZ",


esc: "UNITÉ AMIE",

numeroContrat: "CTR-2024-001",

dateDebut: "2024-01-01",

dateFin: "2024-12-31",

obs: ""

},

id: 2,

denomination: "GARDE RAPPROCHEE DU DG COMMIERE",

adresse: "AV/IZAM, n°07, Q/Golf Plateau Karavia, Enceinte de


LUANO CITY, C/Annexe",

nbrePol: "1. AP2CL MBUYU ILUNGA Benjamain\[Link] MBUYU


KALENGA Serge",

typeArme: "AKA CDPN17195038; GP NU2900845",

nbreMun: 60,

affectation: "BANQUE",

superviseur: "CAP BOAZ",

esc: "ESCADRON PROTECTION",

numeroContrat: "CTR-2024-002",

dateDebut: "2024-01-15",

dateFin: "2025-01-15",

obs: ""

];

//
==========================================

// INITIALISATION

//
==========================================
// Service Worker PWA

if ('serviceWorker' in navigator) {

[Link]('load', () => {

[Link]('[Link]')

.then(registration => [Link]('[SW] Registered:',


[Link]))

.catch(error => [Link]('[SW] Error:', error));

});

// Installation PWA

[Link]('beforeinstallprompt', (e) => {

[Link]();

deferredPrompt = e;

if (![Link]('installDismissed')) {

setTimeout(() =>
[Link]('installBanner').[Link]('show'), 3000);

});

[Link]('appinstalled', () => {

[Link]('installBanner').[Link]('show');

deferredPrompt = null;

showToast('Application installée!');

});

// Online/Offline handlers

[Link]('online', () => {
isOnline = true;

[Link]('offlineIndicator').[Link]('show');

updateSyncStatus('online');

showToast('Connexion rétablie - Synchronisation...');

syncData();

});

[Link]('offline', () => {

isOnline = false;

[Link]('offlineIndicator').[Link]('show');

updateSyncStatus('offline');

});

// Raccourcis clavier

[Link]('keydown', (e) => {

if ([Link] || [Link]) {

if ([Link] === 'n') {

[Link]();

if ([Link]) openAddModal();

} else if ([Link] === 'f') {

[Link]();

[Link]('searchInput').focus();

} else if ([Link] === 's') {

[Link]();

if ([Link] &&
[Link]('formModal').[Link]('show')) {

saveData();

} else if ([Link] === 'd') {


[Link]();

openDashboard();

if ([Link] === 'Escape') {

const openModal = [Link]('.[Link]');

if (openModal) {

[Link](openModal).hide();

if ([Link] === 'F11') {

[Link]();

toggleFullscreen();

});

[Link]('DOMContentLoaded', () => {

initFirebase();

});

//
==========================================

// FIREBASE INITIALIZATION

//
==========================================

function initFirebase() {

try {

[Link](firebaseConfig);

db = [Link]();

auth = [Link]();
[Link]({ synchronizeTabs: true })

.then(() => {

[Link]('[Firebase] Persistence enabled');

})

.catch((err) => {

if ([Link] == 'failed-precondition') {

[Link]('[Firebase] Persistence failed: Multiple tabs


open');

} else if ([Link] == 'unimplemented') {

[Link]('[Firebase] Persistence not available');

});

[Link]((user) => {

if (user) {

showUserInfo(user);

} else {

showLoginForm();

});

} catch (e) {

[Link]("[Firebase] Init error:", e);

showToast('Erreur de connexion Firebase', 'error');

//
==========================================
// AUTHENTIFICATION - CORRECTION RÔLES

//
==========================================

function enterGuestMode() {

// Mode invité : accès direct sans mot de passe, lecture seule

currentUser = {

uid: 'guest_' + [Link](),

email: 'invité',

role: 'lecteur',

isAdmin: false,

isGuest: true

};

[Link]('guestModeSection').[Link] =
'none';

[Link]('adminAuthSection').[Link] =
'none';

[Link]('userInfo').[Link] = 'block';

[Link]('userEmail').textContent = 'Mode Invité


(Lecture seule)';

[Link]('roleDisplayInfo').textContent = 'Lecture
seule';

showToast('Mode invité activé - Consultation uniquement');

function signInAdmin() {

const email = [Link]('loginEmail').[Link]();

const password =
[Link]('loginPassword').value;
if (!email || !password) {

showToast('Veuillez saisir email et mot de passe', 'error');

return;

// Vérification admin

if (email === 'admin@[Link]' && password === 'admin123')


{

currentUser = {

uid: 'admin_' + [Link](),

email: email,

role: 'admin',

isAdmin: true,

isGuest: false

};

[Link]('guestModeSection').[Link] =
'none';

[Link]('adminAuthSection').[Link] =
'none';

[Link]('userInfo').[Link] = 'block';

[Link]('userEmail').textContent = email;

[Link]('roleDisplayInfo').textContent =
'Administrateur';

showToast('Authentification réussie - Mode administrateur');

} else {

showToast('Email ou mot de passe incorrect', 'error');

}
}

function logout() {

currentUser = { uid: null, email: null, role: 'lecteur', isAdmin: false,


isGuest: false };

[Link]('guestModeSection').[Link] =
'block';

[Link]('adminAuthSection').[Link] =
'block';

[Link]('userInfo').[Link] = 'none';

[Link]('loginEmail').value = '';

[Link]('loginPassword').value = '';

function completeLogin() {

const role = [Link];

[Link]('lock-screen').[Link] = 'none';

[Link]('app-container').[Link] = 'flex';

[Link]('userDisplay').textContent =
[Link] ? 'INVITÉ' : [Link]('@')
[0].toUpperCase();

const roleDisplay = [Link]('roleDisplay');

[Link] = [Link] ? 'Admin' : 'Lecteur';

[Link] = `role-badge ${[Link] ? 'role-


admin' : 'role-lecteur'}`;

updateUIForRole();
// Afficher loader pendant le chargement des données

showLoader();

// Timeout de secours: forcer le masquage du loader après 5


secondes maximum

loaderTimeout = setTimeout(() => {

[Link]('[Login] Forcing loader hide after timeout');

forceHideLoader();

}, 5000);

// Simuler progression

let progress = 0;

const progressBar =
[Link]('loaderProgressBar');

const progressInterval = setInterval(() => {

progress += [Link]() * 30;

if (progress > 90) progress = 90;

if (progressBar) [Link] = progress + '%';

}, 200);

// Charger données puis masquer loader (avec gestion d'erreur)

loadData().then(() => {

[Link]('[Login] Data loaded successfully');

}).catch((err) => {

[Link]('[Login] Error loading data:', err);

}).finally(() => {

// TOUJOURS masquer le loader, même en cas d'erreur

clearTimeout(loaderTimeout);

clearInterval(progressInterval);

if (progressBar) [Link] = '100%';


setTimeout(() => {

hideLoader();

showToast(`Bienvenue ${[Link] ? 'Invité' :


'Administrateur'}`);

}, 500);

});

//
==========================================

// LOADER MANAGEMENT - CORRECTION COMPLÈTE

//
==========================================

function hideLoader() {

const loader = [Link]('appLoader');

const appContainer = [Link]('app-container');

if (loader) {

[Link]('hidden');

if (appContainer) {

[Link]('show');

// Mettre à jour le texte du loader pour la prochaine fois

const loaderText = [Link]('loaderText');

if (loaderText) [Link] = 'Chargement...';


}

function forceHideLoader() {

// Forcer le masquage du loader en cas de blocage

const loader = [Link]('appLoader');

const appContainer = [Link]('app-container');

if (loader) {

[Link] = '0';

[Link] = 'hidden';

[Link] = 'none';

if (appContainer) {

[Link] = '1';

[Link] = 'translateY(0)';

[Link] = 'flex';

showToast('Application prête');

function showLoader() {

const loader = [Link]('appLoader');

const appContainer = [Link]('app-container');

const progressBar =
[Link]('loaderProgressBar');

if (loader) {
[Link]('hidden');

[Link] = '';

[Link] = '';

[Link] = '';

if (appContainer) {

[Link]('show');

if (progressBar) {

[Link] = '0%';

[Link]('animate');

//
==========================================

// GESTION DES DONNÉES - CORRECTION LOADER

//
==========================================

async function loadData() {

[Link]('[Data] Loading data...');

// Toujours charger les données locales d'abord pour l'affichage


rapide

loadLocal();

renderTable();

updateStats();
if (!db) {

[Link]('[Data] Firestore not initialized - using local data


only');

return; // Le loader sera masqué par completeLogin()

if (isOnline) {

try {

if ([Link]) {

[Link]();

// Ajouter un timeout pour éviter d'attendre indéfiniment

const timeoutPromise = new Promise((_, reject) => {

setTimeout(() => reject(new Error('Firebase timeout')),


8000);

});

const dataPromise = [Link]('detachements')

.orderBy('id')

.get()

.then((snapshot) => {

const firebaseData = [];

[Link]((doc) => {

[Link]({ id: parseInt([Link]) ||


[Link], ...[Link]() });

});

return firebaseData;

});
// Race entre Firebase et timeout

const firebaseData = await [Link]([dataPromise,


timeoutPromise]);

if ([Link] > 0) {

data = firebaseData;

[Link]((a, b) => [Link] - [Link]);

saveLocal();

renderTable();

updateStats();

checkContractAlerts();

[Link]('[Data] Synced from Firebase:', [Link],


'items');

// Configurer le listener temps réel après la première charge

[Link] =
[Link]('detachements')

.orderBy('id')

.onSnapshot((snapshot) => {

[Link]('[Data] Real-time update received');

const realtimeData = [];

[Link]((doc) => {

[Link]({ id: parseInt([Link]) ||


[Link], ...[Link]() });

});

if ([Link] > 0) {

data = realtimeData;
[Link]((a, b) => [Link] - [Link]);

saveLocal();

renderTable();

updateStats();

checkContractAlerts();

}, (error) => {

[Link]('[Data] Real-time listener error:', error);

});

} catch (e) {

[Link]('[Data] Error loading from Firebase:', e);

// Les données locales sont déjà chargées, on continue

function loadLocal() {

const saved = [Link]('GPI-HP_data_v3');

if (saved) {

try {

data = [Link](saved);

[Link]('[Data] Loaded from localStorage:', [Link],


'items');

} catch (e) {

[Link]('[Data] Error parsing local data:', e);

data = [...defaultData];

} else {
data = [...defaultData];

saveLocal();

function saveLocal() {

try {

[Link]('GPI-HP_data_v3', [Link](data));

[Link]('[Data] Saved to localStorage');

} catch (e) {

[Link]('[Data] Error saving to localStorage:', e);

//
==========================================

// SYNCHRONISATION FIREBASE

//
==========================================

async function syncData() {

if (!db) {

showToast('Firebase non initialisé', 'error');

return;

if (!isOnline) {

showToast('Mode hors ligne - Sync impossible', 'warning');

return;

}
if (syncInProgress) {

showToast('Synchronisation déjà en cours...', 'warning');

return;

syncInProgress = true;

updateSyncStatus('syncing');

try {

[Link]('[Sync] Starting sync to Firebase...');

const batch = [Link]();

const existingDocs = await [Link]('detachements').get();

[Link](doc => {

[Link]([Link]);

});

[Link](item => {

const docRef =
[Link]('detachements').doc(String([Link]));

const cleanItem = [Link](

[Link](item).filter(([_, v]) => v !== undefined)

);

[Link](docRef, cleanItem);

});

await [Link]();
unsavedChanges = false;

updateUnsavedIndicator();

updateSyncStatus('synced');

showToast(`Synchronisation réussie: ${[Link]}


détachements`);

[Link]('[Sync] Success:', [Link], 'items synced');

} catch (error) {

[Link]('[Sync] Error:', error);

updateSyncStatus('error');

showToast('Erreur de synchronisation: ' + [Link], 'error');

} finally {

syncInProgress = false;

function updateSyncStatus(status) {

const syncStatus = [Link]('syncStatus');

const syncText = [Link]('syncText');

if (!syncStatus || !syncText) return;

[Link] = 'sync-status';

switch(status) {

case 'online':

case 'synced':

[Link] = '<i class="fas


fa-check-circle"></i><span>Sync OK</span>';
[Link] = '#f0fdf4';

[Link] = '#166534';

break;

case 'syncing':

[Link] = '<div
class="sync-spinner"></div><span>Sync...</span>';

[Link]('syncing');

break;

case 'offline':

[Link] = '<i class="fas


fa-wifi-slash"></i><span>Hors ligne</span>';

[Link] = '#fef2f2';

[Link] = '#dc2626';

break;

case 'error':

[Link] = '<i class="fas fa-exclamation-


triangle"></i><span>Erreur</span>';

[Link] = '#fef2f2';

[Link] = '#dc2626';

break;

default:

[Link] = '<i class="fas


fa-cloud"></i><span>Firebase</span>';

function markUnsaved() {

unsavedChanges = true;

updateUnsavedIndicator();

}
function updateUnsavedIndicator() {

const indicator = [Link]('unsavedIndicator');

if (indicator) {

[Link]('show', unsavedChanges);

//
==========================================

// FONCTIONS UTILITAIRES

//
==========================================

function countPoliciers(nbrePolText) {

if (!nbrePolText) return 0;

const cleaned = [Link](/\s+/g, ' ').trim();

const matches = [Link](/\b\d+\./g);

return matches ? [Link] : 0;

function getContractStatus(dateFin) {

if (!dateFin) return { status: 'none', days: null };

const fin = new Date(dateFin);

const today = new Date();

[Link](0,0,0,0);

[Link](0,0,0,0);

const diff = [Link]((fin - today) / (1000 * 60 * 60 * 24));

if (diff < 0) return { status: 'expired', days: diff };

if (diff <= 30) return { status: 'expiring', days: diff };

return { status: 'valid', days: diff };


}

function checkContractAlerts() {

let alertCount = 0;

[Link](item => {

const status = getContractStatus([Link]);

if ([Link] === 'expiring' || [Link] === 'expired') {

alertCount++;

});

[Link]('alertCount').textContent = alertCount;

if (alertCount > 0 && [Link] === "granted") {

if (![Link]('alertShown')) {

new Notification("GPI-HP - Alertes", {

body: `${alertCount} contrat(s) à vérifier`,

icon: "[Link]"

});

[Link]('alertShown', 'true');

return alertCount;

//
==========================================

// STATISTIQUES ET TABLEAU DE BORD

//
==========================================

function updateStats() {
[Link]('totalSites').textContent = [Link];

let totalPoliciers = 0;

let policiersParEsc = {};

let policiersParAffectation = {};

let alertCount = 0;

const affectations = ['OFFICIEL', 'BANQUE', 'RECOMANDATION',


'RAPPORT'];

[Link](aff => {

policiersParAffectation[aff] = { count: 0, sites: 0, policiers: 0 };

});

[Link](item => {

const count = countPoliciers([Link]);

totalPoliciers += count;

if (!policiersParEsc[[Link]]) policiersParEsc[[Link]] =
{ count: 0, sites: 0 };

policiersParEsc[[Link]].count += count;

policiersParEsc[[Link]].sites += 1;

const aff = [Link] || 'OFFICIEL';

if (policiersParAffectation[aff]) {

policiersParAffectation[aff].count += count;

policiersParAffectation[aff].sites += 1;

policiersParAffectation[aff].policiers += count;

const status = getContractStatus([Link]);


if ([Link] === 'expiring' || [Link] === 'expired')
alertCount++;

});

[Link]('totalPoliciers').textContent =
totalPoliciers;

[Link]('avgPoliciers').textContent =
[Link] ? (totalPoliciers / [Link]).toFixed(1) : 0;

[Link]('alertCount').textContent = alertCount;

const escContainer = [Link]('escStats');

[Link] = '';

[Link](policiersParEsc).forEach(([esc, stats]) => {

const badge = [Link]('span');

[Link] = 'esc-badge';

[Link] = `${esc}: <strong>${[Link]}P/$


{[Link]}S</strong>`;

[Link](badge);

});

const affContainer = [Link]('affectationStats');

[Link] = '';

const affClasses = {

'OFFICIEL': 'aff-stat-officiel',

'BANQUE': 'aff-stat-banque',

'RECOMANDATION': 'aff-stat-recom',

'RAPPORT': 'aff-stat-rapport'

};
const affLabels = {

'OFFICIEL': 'Officiel',

'BANQUE': 'Banque',

'RECOMANDATION': 'Recom.',

'RAPPORT': 'Rapport'

};

[Link](policiersParAffectation).forEach(([aff, stats]) => {

if ([Link] > 0) {

const badge = [Link]('span');

[Link] = `affectation-stat-badge ${affClasses[aff]


|| ''}`;

[Link] = `${affLabels[aff]}: <strong>$


{[Link]}S/${[Link]}P</strong>`;

[Link] = `${aff}: ${[Link]} sites, ${[Link]}


policiers`;

[Link] = () => filterByAffectation(aff);

[Link](badge);

});

//
==========================================

// RENDU DU TABLEAU

//
==========================================

function renderTable(dataToRender = null) {

if (!dataToRender) {

dataToRender = applyFilters(data);

}
const tbody = [Link]('tableBody');

[Link] = '';

const searchResults = [Link]('searchResults');

if (searchResults) {

if ([Link] !== [Link]) {

[Link] = `${[Link]}/$
{[Link]}`;

[Link] = 'block';

} else {

[Link] = 'none';

[Link]((item) => {

const tr = [Link]('tr');

[Link] = [Link] ? 'pointer' : 'default';

if ([Link]) {

[Link] = (e) => {

if(![Link]('button') && !
[Link]('input[type="checkbox"]')) {

editItem([Link]);

};

const contract = getContractStatus([Link]);

let contractBadge = '';


if ([Link] === 'expired') {

contractBadge = `<span class="contract-alert contract-


expired" title="Expiré depuis ${[Link]([Link])} jours"><i
class="fas fa-exclamation-circle"></i> Expiré</span>`;

} else if ([Link] === 'expiring') {

contractBadge = `<span class="contract-alert contract-


expiring" title="Expire dans ${[Link]} jours"><i class="fas fa-
clock"></i> ${[Link]}j</span>`;

} else if ([Link] === 'valid') {

contractBadge = `<span class="contract-alert contract-


valid"><i class="fas fa-check-circle"></i> OK</span>`;

let affClass = 'aff-officiel';

if([Link] === 'BANQUE') affClass = 'aff-banque';

else if([Link] === 'RECOMANDATION') affClass = 'aff-


recom';

else if([Link] === 'RAPPORT') affClass = 'aff-rapport';

const isSelected = [Link]([Link]);

const nbPoliciers = countPoliciers([Link]);

const actionsColumn = [Link] ? `

<td onclick="[Link]()">

<div class="d-flex gap-1">

<button class="action-btn btn-edit" onclick="editItem($


{[Link]})" title="Modifier"><i class="fas fa-edit"></i></button>

<button class="action-btn btn-delete"


onclick="deleteItem(${[Link]})" title="Supprimer"><i class="fas fa-
trash"></i></button>

</div>
</td>

` : '<td></td>';

[Link] = `

<td onclick="[Link]()">

<input type="checkbox" class="select-checkbox" value="$


{[Link]}" ${isSelected ? 'checked' : ''} onchange="toggleSelect($
{[Link]}, this)" ${![Link] ? 'disabled' : ''}>

</td>

<td class="fw-bold text-primary">${[Link]}</td>

<td class="col-denomination">$
{escapeHtml([Link])}</td>

<td class="col-adresse">${escapeHtml([Link])}</td>

<td class="col-policiers">

<div style="position: relative; display: inline-block; width:


100%;">

${escapeHtml([Link])}

<span class="badge bg-danger badge-count-pol"


title="Nombre de policiers">${nbPoliciers}</span>

</div>

</td>

<td><small class="font-monospace">$
{escapeHtml([Link] || '')}</small></td>

<td class="col-number">${[Link] || ''}</td>

<td><span class="badge-affectation ${affClass}">$


{[Link]}</span></td>

<td class="fw-bold">${escapeHtml([Link] ||
'')}</td>

<td><span class="badge bg-secondary bg-opacity-10 text-


secondary">${escapeHtml([Link] || '')}</span></td>

<td>

${escapeHtml([Link] || '')}
${contractBadge}

</td>

<td class="text-muted small">${escapeHtml([Link] ||


'')}</td>

${actionsColumn}

`;

[Link](tr);

});

function applyFilters(sourceData) {

return [Link](item => {

const matchAff = ![Link] || [Link]


=== [Link];

const matchEsc = ![Link] || [Link] ===


[Link];

let matchContract = true;

if ([Link]) {

const status = getContractStatus([Link]);

matchContract = [Link] ===


[Link];

let matchAdvanced = true;

if ([Link]) {

if ([Link] === 'zero' &&


[Link] > 0) matchAdvanced = false;

if ([Link] === 'many' &&


countPoliciers([Link]) <= 3) matchAdvanced = false;

}
return matchAff && matchEsc && matchContract &&
matchAdvanced;

});

//
==========================================

// FILTRES

//
==========================================

function filterByAffectation(affectation) {

[Link] = affectation;

[Link]('.filter-badge').forEach(badge => {

[Link]('active');

});

if (affectation === '') {

[Link]('filter-all').[Link]('active');

} else {

const idMap = {

'OFFICIEL': 'filter-officiel',

'BANQUE': 'filter-banque',

'RECOMANDATION': 'filter-recom',

'RAPPORT': 'filter-rapport'

};

const targetId = idMap[affectation];

if (targetId) {

[Link](targetId).[Link]('active');
}

renderTable();

function filterByESC(esc) {

[Link] = esc;

renderTable();

function filterByContractStatus(status) {

[Link] = status;

renderTable();

function toggleAdvancedFilters() {

const filters = [Link]('advancedFilters');

[Link]('show');

function toggleFilterChip(element, filterType, value) {

[Link]('active');

if (![Link]) [Link] = {};

if ([Link]('active')) {

[Link][filterType] = value;

} else {

delete [Link][filterType];
}

renderTable();

//
==========================================

// SÉLECTION

//
==========================================

function toggleSelect(id, checkbox) {

if (![Link]) return;

if ([Link]) {

[Link](id);

} else {

[Link](id);

updateSelectionUI();

function toggleSelectAll(checkbox) {

if (![Link]) return;

const visibleIds = applyFilters(data).map(i => [Link]);

if ([Link]) {

[Link](id => [Link](id));

} else {

[Link](id => [Link](id));

}
renderTable();

updateSelectionUI();

function updateSelectionUI() {

const count = [Link];

const btn = [Link]('btnDeleteSelected');

if (btn) {

[Link]('selectedCount').textContent =
count;

[Link] = count === 0 || ![Link];

//
==========================================

// RECHERCHE

//
==========================================

function debouncedSearch() {

const searchInput = [Link]('searchInput');

[Link]('search-active');

clearTimeout(searchDebounceTimer);

searchDebounceTimer = setTimeout(() => {

searchData();

[Link]('search-active');

}, 300);

}
function searchData() {

const term =
[Link]('searchInput').[Link]().trim();

if (!term) {

renderTable();

return;

const filtered = [Link](item => {

const searchFields = [

[Link],

[Link],

[Link],

[Link],

[Link],

[Link],

[Link],

[Link]

];

return [Link](field =>

field && String(field).toLowerCase().includes(term)

);

});

renderTable(filtered);

//
==========================================

// TABLEAU DE BORD (DASHBOARD)


//
==========================================

function openDashboard() {

updateDashboard();

new
[Link]([Link]('dashboardModal')).show();

function updateDashboard() {

const stats = calculateDashboardStats();

[Link]('dashTotalSites').textContent =
[Link];

[Link]('dashTotalPoliciers').textContent =
[Link];

[Link]('dashAvgPoliciers').textContent =
[Link];

[Link]('dashAlertCount').textContent =
[Link];

updateDashboardTable([Link]);

createCharts(stats);

function calculateDashboardStats() {

let totalPoliciers = 0;

let byAffectation = {};

let byEsc = {};

let contractStatus = { expired: 0, expiring: 0, valid: 0, none: 0 };


const affectations = ['OFFICIEL', 'BANQUE', 'RECOMANDATION',
'RAPPORT'];

[Link](aff => {

byAffectation[aff] = { sites: 0, policiers: 0 };

});

[Link](item => {

const nbPol = countPoliciers([Link]);

totalPoliciers += nbPol;

const aff = [Link] || 'OFFICIEL';

if (byAffectation[aff]) {

byAffectation[aff].sites++;

byAffectation[aff].policiers += nbPol;

if (!byEsc[[Link]]) byEsc[[Link]] = 0;

byEsc[[Link]] += nbPol;

const status = getContractStatus([Link]);

contractStatus[[Link]]++;

});

return {

totalSites: [Link],

totalPoliciers: totalPoliciers,

avgPoliciers: [Link] ? (totalPoliciers /


[Link]).toFixed(1) : 0,

alertCount: [Link] + [Link],

byAffectation: byAffectation,
byEsc: byEsc,

contractStatus: contractStatus

};

function updateDashboardTable(byAffectation) {

const tbody = [Link]('dashboardTableBody');

[Link] = '';

const affLabels = {

'OFFICIEL': 'Officiel',

'BANQUE': 'Banque',

'RECOMANDATION': 'Recommandation',

'RAPPORT': 'Rapport'

};

const totalSites = [Link];

[Link](byAffectation).forEach(([aff, stats]) => {

if ([Link] > 0) {

const tr = [Link]('tr');

const percentage = (([Link] / totalSites) * 100).toFixed(1);

const avg = ([Link] / [Link]).toFixed(1);

[Link] = `

<td><strong>${affLabels[aff]}</strong></td>

<td class="text-center">${[Link]}</td>

<td class="text-center">${[Link]}</td>

<td class="text-center">${avg}</td>
<td class="text-center">

<div class="progress" style="height: 20px;">

<div class="progress-bar" role="progressbar"


style="width: ${percentage}%">${percentage}%</div>

</div>

</td>

`;

[Link](tr);

});

function createCharts(stats) {

const isDark = [Link]('data-


theme') === 'dark';

const textColor = isDark ? '#f8fafc' : '#1e293b';

const gridColor = isDark ? '#334155' : '#e2e8f0';

[Link](charts).forEach(chart => [Link]());

// Chart 1: Affectation (Doughnut)

const affCtx =
[Link]('affectationChart').getContext('2d');

[Link] = new Chart(affCtx, {

type: 'doughnut',

data: {

labels: ['Officiel', 'Banque', 'Recommandation', 'Rapport'],

datasets: [{

data: [

[Link]['OFFICIEL'].sites,
[Link]['BANQUE'].sites,

[Link]['RECOMANDATION'].sites,

[Link]['RAPPORT'].sites

],

backgroundColor: ['#3b82f6', '#10b981', '#f59e0b',


'#8b5cf6'],

borderWidth: 0

}]

},

options: {

responsive: true,

maintainAspectRatio: false,

plugins: {

legend: { position: 'bottom', labels: { color: textColor } }

});

// Chart 2: ESC (Bar)

const escCtx =
[Link]('escChart').getContext('2d');

[Link] = new Chart(escCtx, {

type: 'bar',

data: {

labels: [Link]([Link]),

datasets: [{

label: 'Policiers',

data: [Link]([Link]),

backgroundColor: '#3b82f6',

borderRadius: 4
}]

},

options: {

responsive: true,

maintainAspectRatio: false,

plugins: { legend: { display: false } },

scales: {

y: { beginAtZero: true, grid: { color: gridColor }, ticks:


{ color: textColor } },

x: { grid: { display: false }, ticks: { color: textColor } }

});

// Chart 3: Contracts (Pie)

const contractCtx =
[Link]('contractChart').getContext('2d');

[Link] = new Chart(contractCtx, {

type: 'pie',

data: {

labels: ['Valides', 'Expirent bientôt', 'Expirés', 'Sans date'],

datasets: [{

data: [[Link],
[Link], [Link],
[Link]],

backgroundColor: ['#10b981', '#f59e0b', '#ef4444',


'#6b7280'],

borderWidth: 0

}]

},

options: {
responsive: true,

maintainAspectRatio: false,

plugins: {

legend: { position: 'bottom', labels: { color: textColor } }

});

// Chart 4: Policiers par affectation (Horizontal Bar)

const polCtx =
[Link]('policiersChart').getContext('2d');

[Link] = new Chart(polCtx, {

type: 'bar',

data: {

labels: ['Officiel', 'Banque', 'Recommandation', 'Rapport'],

datasets: [{

label: 'Nombre de policiers',

data: [

[Link]['OFFICIEL'].policiers,

[Link]['BANQUE'].policiers,

[Link]['RECOMANDATION'].policiers,

[Link]['RAPPORT'].policiers

],

backgroundColor: ['#3b82f6', '#10b981', '#f59e0b',


'#8b5cf6'],

borderRadius: 4

}]

},

options: {

indexAxis: 'y',
responsive: true,

maintainAspectRatio: false,

plugins: { legend: { display: false } },

scales: {

x: { beginAtZero: true, grid: { color: gridColor }, ticks:


{ color: textColor } },

y: { grid: { display: false }, ticks: { color: textColor } }

});

//
==========================================

// CRUD OPERATIONS

//
==========================================

function openAddModal() {

if (![Link]) {

showToast('Permission refusée: Mode lecture seule', 'error');

return;

[Link]('detachementForm').reset();

[Link]('editId').value = '';

[Link]('modalTitle').textContent = 'Nouveau
Détachement';

[Link]('dateError').[Link] = 'none';

isDirty = false;

startAutoSave();
new
[Link]([Link]('formModal')).show();

function editItem(id) {

if (![Link]) {

showToast('Permission refusée: Mode lecture seule', 'error');

return;

const item = [Link](d => [Link] === id);

if (!item) return;

[Link]('editId').value = [Link];

[Link]('denomination').value =
[Link];

[Link]('adresse').value = [Link];

[Link]('nbrePol').value = [Link];

[Link]('typeArme').value = [Link] || '';

[Link]('nbreMun').value = [Link] || '';

[Link]('affectation').value = [Link];

[Link]('superviseur').value = [Link]
|| '';

[Link]('esc').value = [Link] || '';

[Link]('numeroContrat').value =
[Link] || '';

[Link]('dateDebut').value = [Link] ||
'';

[Link]('dateFin').value = [Link] || '';

[Link]('obs').value = [Link] || '';


[Link]('modalTitle').textContent = 'Modifier #'
+ id;

[Link]('dateError').[Link] = 'none';

isDirty = false;

startAutoSave();

new
[Link]([Link]('formModal')).show();

function validateDates() {

const debut = [Link]('dateDebut').value;

const fin = [Link]('dateFin').value;

const errorEl = [Link]('dateError');

if (debut && fin && new Date(fin) < new Date(debut)) {

[Link] = 'block';

return false;

} else {

[Link] = 'none';

return true;

function markDirty() {

isDirty = true;

function startAutoSave() {

if (autoSaveInterval) clearInterval(autoSaveInterval);

autoSaveInterval = setInterval(() => {


if (isDirty &&
[Link]('formModal').[Link]('show')) {

const indicator =
[Link]('autosaveIndicator');

[Link]('show');

setTimeout(() => [Link]('show'), 2000);

}, 3000);

async function saveData() {

if (![Link]) {

showToast('Permission refusée', 'error');

return;

if (!validateDates()) {

showToast('Erreur: Dates invalides', 'error');

return;

const id = [Link]('editId').value;

const formData = {

denomination:
[Link]('denomination').[Link](),

adresse: [Link]('adresse').[Link](),

nbrePol: [Link]('nbrePol').[Link](),

typeArme: [Link]('typeArme').[Link](),

nbreMun: parseInt([Link]('nbreMun').value)
|| 0,
affectation: [Link]('affectation').value,

superviseur:
[Link]('superviseur').[Link](),

esc: [Link]('esc').value,

numeroContrat:
[Link]('numeroContrat').[Link](),

dateDebut: [Link]('dateDebut').value,

dateFin: [Link]('dateFin').value,

obs: [Link]('obs').[Link]()

};

if (id) {

const index = [Link](d => [Link] == id);

if (index !== -1) data[index] = { ...data[index], ...formData };

} else {

const newId = [Link] > 0 ? [Link](...[Link](d =>


[Link])) + 1 : 1;

[Link]({ id: newId, ...formData });

isDirty = false;

markUnsaved();

renderTable();

updateStats();

checkContractAlerts();

saveLocal();

if (isOnline && db) {

await syncData();
}

[Link]([Link]('formModal')).hide
();

showToast(id ? 'Modifié avec succès' : 'Créé avec succès');

async function deleteItem(id) {

if (![Link]) {

showToast('Permission refusée', 'error');

return;

if (!confirm('Supprimer ce détachement ?')) return;

data = [Link](d => [Link] !== id);

[Link](id);

updateSelectionUI();

renderTable();

updateStats();

checkContractAlerts();

saveLocal();

markUnsaved();

if (isOnline && db) {

await syncData();

showToast('Supprimé');
}

async function deleteSelected() {

if (![Link]) {

showToast('Permission refusée', 'error');

return;

const countToDelete = [Link];

if (countToDelete === 0) return;

if (!confirm(`Supprimer ${countToDelete} détachements


sélectionnés ?`)) return;

data = [Link](d => ![Link]([Link]));

[Link]();

updateSelectionUI();

renderTable();

updateStats();

checkContractAlerts();

saveLocal();

markUnsaved();

if (isOnline && db) {

await syncData();

showToast(`${countToDelete} éléments supprimés`);

}
//
==========================================

// OCR

//
==========================================

function openScanModal() {

if (![Link]) {

showToast('Permission refusée', 'error');

return;

[Link]('ocrInput').value = '';

[Link]('ocrInitialZone').[Link] = 'block';

[Link]('ocrLoadingZone').[Link] =
'none';

[Link]('ocrResultZone').[Link] = 'none';

[Link]('ocrText').value = '';

[Link]('ocrDenomination').value = '';

[Link]('ocrAdresse').value = '';

[Link]('ocrPoliciers').value = '';

[Link]('ocrArme').value = '';

[Link]('ocrMun').value = '';

[Link]('ocrSuperviseur').value = '';

[Link]('ocrEsc').value = '';

new
[Link]([Link]('scanModal')).show();

}
async function processOCR(input) {

if (![Link] || ![Link][0]) return;

const file = [Link][0];

[Link]('ocrInitialZone').[Link] = 'none';

[Link]('ocrResultZone').[Link] = 'none';

[Link]('ocrLoadingZone').[Link] =
'block';

try {

const result = await [Link](file, 'fra', {

logger: m => {

if ([Link] === 'recognizing text') {

const pct = [Link]([Link] * 100);

[Link]('ocrPercent').textContent =
pct + '%';

[Link]('ocrBar').[Link] = pct +
'%';

});

[Link]('ocrLoadingZone').[Link] =
'none';

[Link]('ocrResultZone').[Link] =
'block';

[Link]('ocrText').value = [Link];

const text = [Link];

if (text) {

const lines = [Link]('\n').filter(l => [Link]());


if ([Link] > 0) {

[Link]('ocrDenomination').value =
lines[0].substring(0, 50);

if ([Link] > 1) {

[Link]('ocrAdresse').value =
lines[1].substring(0, 100);

const policiers = [Link](l => /^\d+\./.test([Link]()));

if ([Link] > 0) {

[Link]('ocrPoliciers').value =
[Link]('\n');

} catch (err) {

alert('Erreur OCR: ' + [Link]);

[Link]('ocrLoadingZone').[Link] =
'none';

[Link]('ocrInitialZone').[Link] =
'block';

function transferOCRData() {

[Link]([Link]('scanModal')).hide
();

setTimeout(() => {

openAddModal();

[Link]('denomination').value =
[Link]('ocrDenomination').value;
[Link]('adresse').value =
[Link]('ocrAdresse').value;

[Link]('nbrePol').value =
[Link]('ocrPoliciers').value;

[Link]('typeArme').value =
[Link]('ocrArme').value;

[Link]('nbreMun').value =
[Link]('ocrMun').value;

[Link]('affectation').value =
[Link]('ocrAffectation').value;

[Link]('esc').value =
[Link]('ocrEsc').value;

[Link]('superviseur').value =
[Link]('ocrSuperviseur').value;

}, 300);

//
==========================================

// IMPORT/EXPORT

//
==========================================

function importExcel(event) {

if (![Link]) {

showToast('Permission refusée', 'error');

return;

const file = [Link][0];

if (!file) return;

const reader = new FileReader();


[Link] = function(e) {

try {

const data_uint = new Uint8Array([Link]);

const workbook = [Link](data_uint, { type: 'array' });

const firstSheet =
[Link][[Link][0]];

const jsonData = [Link].sheet_to_json(firstSheet,


{ header: 1, range: 1 });

const newData = [];

let maxId = [Link] > 0 ? [Link](...[Link](d =>


[Link])) : 0;

for (let i = 1; i < [Link]; i++) {

const row = jsonData[i];

if (!row[1]) continue;

maxId++;

[Link]({

id: maxId,

denomination: String(row[1] || ''),

adresse: String(row[2] || ''),

nbrePol: String(row[3] || ''),

typeArme: String(row[4] || ''),

nbreMun: parseInt(row[5]) || 0,

affectation: String(row[6] || 'OFFICIEL'),

superviseur: String(row[7] || ''),

esc: String(row[8] || ''),

numeroContrat: String(row[9] || ''),

dateDebut: String(row[10] || ''),

dateFin: String(row[11] || ''),

obs: String(row[12] || '')


});

if ([Link] > 0) {

data = [...data, ...newData];

renderTable();

updateStats();

checkContractAlerts();

saveLocal();

markUnsaved();

if (isOnline && db) {

syncData();

showToast(`${[Link]} importés`);

} catch (err) {

alert('Erreur import: ' + [Link]);

};

[Link](file);

[Link] = '';

function exportExcel() {

const exportData = [Link](item => ({

'N°': [Link],

'DENOMINATION': [Link],
'ADRESSE': [Link],

'NBRE DE POL': [Link],

'TYPE D\'ARME': [Link] || '',

'MUN': [Link] || 0,

'AFFECTATION': [Link] || 'OFFICIEL',

'SUPERVISEUR': [Link] || '',

'ESC': [Link] || '',

'N° CONTRAT': [Link] || '',

'DATE DEBUT': [Link] || '',

'DATE FIN': [Link] || '',

'OBSERVATIONS': [Link] || ''

}));

const ws = [Link].json_to_sheet(exportData);

ws['!cols'] = [

{wch: 6}, {wch: 40}, {wch: 45}, {wch: 50}, {wch: 25},

{wch: 8}, {wch: 15}, {wch: 20}, {wch: 22}, {wch: 18},

{wch: 12}, {wch: 12}, {wch: 25}

];

const wb = [Link].book_new();

[Link].book_append_sheet(wb, ws, "Détachements");

const dateStr = new Date().toISOString().split('T')[0];

const timeStr = new Date().toTimeString().split(' ')[0].replace(/:/g,


'-');

[Link](wb, `GPI-HP_Export_${dateStr}_${timeStr}.xlsx`);

showToast(`Export réussi: ${[Link]} enregistrements`);


}

//
==========================================

// UTILITAIRES UI

//
==========================================

function updateUIForRole() {

const adminElements = [Link]('.admin-only');

[Link](el => {

if ([Link]) {

[Link] = '';

[Link]('d-none');

} else {

[Link] = 'none !important';

[Link]('d-none');

});

// Désactiver tous les champs de formulaire pour les invités

if (![Link]) {

[Link]('#detachementForm input,
#detachementForm select, #detachementForm textarea').forEach(field
=> {

[Link] = true;

[Link]('readonly-field');

});

// Cacher le bouton sauvegarder

const btnSave = [Link]('btnSaveData');


if (btnSave) [Link] = 'none';

} else {

[Link]('#detachementForm input,
#detachementForm select, #detachementForm textarea').forEach(field
=> {

[Link] = false;

[Link]('readonly-field');

});

const btnSave = [Link]('btnSaveData');

if (btnSave) [Link] = '';

if (![Link]) {

[Link]('.select-checkbox, .select-all-
checkbox').forEach(cb => {

[Link] = true;

[Link] = 'not-allowed';

});

function showToast(msg, type = 'success') {

const toast = [Link]('toast');

[Link] = 'toast-notification show ' + type;

[Link]('toastMessage').textContent = msg;

setTimeout(() => [Link]('show'), 3000);

function toggleTheme() {
const html = [Link];

const current = [Link]('data-theme');

const next = current === 'dark' ? 'light' : 'dark';

[Link]('data-theme', next);

const themeIcon = [Link]('themeIcon');

if(themeIcon) [Link] = next === 'dark' ? 'fas fa-


sun' : 'fas fa-moon';

[Link]('theme', next);

if
([Link]('dashboardModal').[Link]('show')) {

updateDashboard();

function toggleFullscreen() {

if (![Link]) {

[Link]();

const icon = [Link]('fullscreenIcon');

if(icon) [Link] = 'fas fa-compress';

} else {

[Link]();

const icon = [Link]('fullscreenIcon');

if(icon) [Link] = 'fas fa-expand';

function lockApp() {
showLoader();

[Link]('app-container').[Link] = 'none';

[Link]('app-
container').[Link]('show');

[Link]('lock-screen').[Link] = 'flex';

// Réinitialiser le formulaire

[Link]('guestModeSection').[Link] =
'block';

[Link]('adminAuthSection').[Link] =
'block';

[Link]('userInfo').[Link] = 'none';

[Link]('loginEmail').value = '';

[Link]('loginPassword').value = '';

[Link]();

updateSelectionUI();

if ([Link]) {

[Link]();

currentUser = { uid: null, email: null, role: 'lecteur', isAdmin: false,


isGuest: false };

function installPWA() {

if (deferredPrompt) {

[Link]();
[Link](() => {

deferredPrompt = null;

[Link]('installBanner').[Link]('show');

});

function dismissInstall() {

[Link]('installDismissed', 'true');

[Link]('installBanner').[Link]('show');

function escapeHtml(text) {

if (!text) return '';

const div = [Link]('div');

[Link] = text;

return [Link];

// Initialisation du thème

const savedTheme = [Link]('theme');

if (savedTheme === 'dark') {

[Link]('data-theme', 'dark');

const themeIcon = [Link]('themeIcon');

if(themeIcon) [Link] = 'fas fa-sun';

</script>

</body>
</html>

You might also like