INSPECTION / index.html
MMOON's picture
Update index.html
e8cd2a5 verified
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Application d'Inspection Qualité v2.1.0</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf-autotable/3.5.23/jspdf.plugin.autotable.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.9.1/chart.min.js"></script>
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
body {
font-family: 'Inter', sans-serif;
background-color: #f4f7f6;
transition: margin-left 0.3s;
}
.modal {
display: none;
position: fixed;
z-index: 1000;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0,0,0,0.5);
align-items: center;
justify-content: center;
}
.modal-content {
background-color: #fff;
margin: auto;
padding: 2rem;
border-radius: 0.5rem;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
width: 90%;
max-width: 600px;
max-height: 90vh;
overflow-y: auto;
}
#toast-container {
position: fixed;
bottom: 1rem;
right: 1rem;
z-index: 1050;
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.toast {
padding: 0.75rem 1.25rem;
border-radius: 0.375rem;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
opacity: 0;
transition: opacity 0.5s ease-in-out, transform 0.3s ease-in-out;
max-width: 300px;
transform: translateX(100%);
}
.toast.show {
opacity: 1;
transform: translateX(0);
}
.toast-success {
background-color: #d1fae5;
color: #065f46;
border-left: 4px solid #10b981;
}
.toast-error {
background-color: #fee2e2;
color: #991b1b;
border-left: 4px solid #ef4444;
}
.toast-info {
background-color: #dbeafe;
color: #1e40af;
border-left: 4px solid #3b82f6;
}
.toast-warning {
background-color: #fef3c7;
color: #92400e;
border-left: 4px solid #f59e0b;
}
#sidebar {
height: 100%;
width: 0;
position: fixed;
z-index: 1001;
top: 0;
left: 0;
background-color: #1e3a8a;
overflow-x: hidden;
transition: 0.3s;
padding-top: 60px;
box-shadow: 2px 0 5px rgba(0,0,0,0.2);
}
#sidebar a {
padding: 10px 15px 10px 20px;
text-decoration: none;
font-size: 16px;
color: #e0f2fe;
display: block;
white-space: nowrap;
transition: 0.2s;
border-left: 4px solid transparent;
}
#sidebar a:hover {
background-color: #1d4ed8;
color: white;
}
#sidebar a.active {
background-color: #2563eb;
color: white;
font-weight: 600;
border-left: 4px solid white;
}
#sidebar .close-btn {
position: absolute;
top: 10px;
right: 15px;
font-size: 24px;
color: white;
background: none;
border: none;
cursor: pointer;
}
#sidebar .close-btn:hover {
color: #bfdbfe;
}
.sidebar-divider {
border-top: 1px solid rgba(255,255,255,0.2);
margin: 15px 20px;
}
#main-content {
transition: margin-left 0.3s;
padding: 0;
}
#loading-indicator {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(255, 255, 255, 0.85);
z-index: 2000;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
backdrop-filter: blur(2px);
}
#loading-indicator i {
font-size: 3.5rem;
color: #3b82f6;
margin-bottom: 1.5rem;
}
#loading-indicator p {
font-size: 1.1rem;
color: #1e3a8a;
font-weight: 500;
}
#menu-toggle {
position: fixed;
top: 1rem;
left: 1rem;
z-index: 1002;
width: 40px;
height: 40px;
background-color: #1e3a8a;
color: white;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
border: none;
transition: background-color 0.2s, left 0.3s;
}
#menu-toggle:hover {
background-color: #2563eb;
}
.welcome-card {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.feature-card {
transition: transform 0.2s;
}
.feature-card:hover {
transform: translateY(-2px);
}
.workflow-step {
position: relative;
}
.workflow-step::after {
content: '';
position: absolute;
top: 50%;
right: -20px;
width: 40px;
height: 2px;
background: #e5e7eb;
}
.workflow-step:last-child::after {
display: none;
}
.kpi-card {
background: white;
border-radius: 0.5rem;
padding: 1.5rem;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
border-left: 4px solid;
}
.kpi-card.success {
border-left-color: #10b981;
}
.kpi-card.info {
border-left-color: #3b82f6;
}
.kpi-card.warning {
border-left-color: #f59e0b;
}
.kpi-card.danger {
border-left-color: #ef4444;
}
.chart-container {
background: white;
border-radius: 0.5rem;
padding: 1.5rem;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
height: 400px; /* CORRECTION: Hauteur fixe pour les graphiques */
position: relative;
}
.chart-container canvas {
max-height: 300px !important; /* CORRECTION: Hauteur max pour les canvas */
}
.chart-container h3 {
margin-bottom: 1rem;
height: 2rem; /* Hauteur fixe pour le titre */
}
/* NOUVEAU: Boutons de statut colorés */
.status-buttons {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
}
.status-button {
padding: 0.5rem 1rem;
border: 2px solid transparent;
border-radius: 0.375rem;
cursor: pointer;
transition: all 0.2s;
font-size: 0.875rem;
font-weight: 500;
min-width: 4rem;
text-align: center;
}
.status-button.conforme {
background-color: #f0fdf4;
color: #15803d;
border-color: #bbf7d0;
}
.status-button.conforme.active {
background-color: #16a34a;
color: white;
border-color: #16a34a;
}
.status-button.non-conforme {
background-color: #fef2f2;
color: #dc2626;
border-color: #fecaca;
}
.status-button.non-conforme.active {
background-color: #dc2626;
color: white;
border-color: #dc2626;
}
.status-button.hors-plage {
background-color: #fff7ed;
color: #ea580c;
border-color: #fed7aa;
}
.status-button.hors-plage.active {
background-color: #ea580c;
color: white;
border-color: #ea580c;
}
.status-button.na {
background-color: #f9fafb;
color: #6b7280;
border-color: #d1d5db;
}
.status-button.na.active {
background-color: #6b7280;
color: white;
border-color: #6b7280;
}
.status-button:hover {
transform: translateY(-1px);
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
@media (max-width: 768px) {
#main-content {
padding-left: 0 !important;
}
#menu-toggle {
left: 0.5rem;
}
.workflow-step::after {
display: none;
}
.chart-container {
height: 350px; /* Hauteur réduite sur mobile */
}
.status-buttons {
justify-content: center;
}
}
</style>
</head>
<body>
<!-- Menu latéral -->
<div id="sidebar">
<button class="close-btn" onclick="closeSidebar()">×</button>
<a href="#" id="sidebar-welcome" class="active" onclick="event.preventDefault(); UI.switchView('welcome')">
<i class="fas fa-home mr-2 w-5 text-center"></i> Accueil
</a>
<a href="#" id="sidebar-models" onclick="event.preventDefault(); UI.switchView('models')">
<i class="fas fa-clipboard-list mr-2 w-5 text-center"></i> Gestion Modèles
</a>
<a href="#" id="sidebar-inspections" onclick="event.preventDefault(); UI.switchView('inspections')">
<i class="fas fa-tasks mr-2 w-5 text-center"></i> Inspections
</a>
<a href="#" id="sidebar-dashboard" onclick="event.preventDefault(); UI.switchView('dashboard')">
<i class="fas fa-chart-line mr-2 w-5 text-center"></i> Tableau de Bord
</a>
<div class="sidebar-divider"></div>
<a href="#" onclick="event.preventDefault(); UI.showHelpModal()">
<i class="fas fa-question-circle mr-2 w-5 text-center"></i> Aide
</a>
<div class="sidebar-divider"></div>
<a href="#" onclick="event.preventDefault(); DataManager.confirmClearAllData()">
<i class="fas fa-trash-alt mr-2 w-5 text-center text-red-300"></i> <span class="text-red-300">Tout Effacer</span>
</a>
<div class="mt-auto text-white text-xs p-4 text-center absolute bottom-0 w-full">
<div>App Inspection Qualité</div>
<div class="text-blue-200 mt-1">v2.1.0</div>
</div>
</div>
<!-- Bouton menu -->
<button id="menu-toggle" onclick="openSidebar()">
<i class="fas fa-bars"></i>
</button>
<!-- Contenu principal -->
<div id="main-content">
<header class="bg-gradient-to-r from-blue-600 to-blue-800 text-white p-4 shadow-lg sticky top-0 z-50">
<div class="flex justify-between items-center">
<div class="pl-12">
<h1 class="text-xl sm:text-2xl font-bold">Application d'Inspection Qualité</h1>
<p class="text-xs sm:text-sm text-blue-100">Gestion checklists & inspections hors-ligne.</p>
</div>
</div>
</header>
<nav class="bg-white shadow-sm mb-6 sticky top-[72px] z-40">
<div class="max-w-7xl mx-auto px-2 sm:px-6 lg:px-8">
<div class="flex justify-center h-16">
<div class="flex space-x-4 sm:space-x-8">
<button id="nav-welcome" class="nav-button border-b-2 border-blue-500 text-blue-600 inline-flex items-center px-1 pt-1 text-sm font-medium" onclick="UI.switchView('welcome')">
<i class="fas fa-home mr-1 sm:mr-2"></i> Accueil
</button>
<button id="nav-models" class="nav-button border-b-2 border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700 inline-flex items-center px-1 pt-1 text-sm font-medium" onclick="UI.switchView('models')">
<i class="fas fa-clipboard-list mr-1 sm:mr-2"></i> Modèles
</button>
<button id="nav-inspections" class="nav-button border-b-2 border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700 inline-flex items-center px-1 pt-1 text-sm font-medium" onclick="UI.switchView('inspections')">
<i class="fas fa-tasks mr-1 sm:mr-2"></i> Inspections
</button>
<button id="nav-dashboard" class="nav-button border-b-2 border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700 inline-flex items-center px-1 pt-1 text-sm font-medium" onclick="UI.switchView('dashboard')">
<i class="fas fa-chart-line mr-1 sm:mr-2"></i> Tableau de Bord
</button>
</div>
</div>
</div>
</nav>
<main class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 pb-24">
<!-- Section Accueil -->
<section id="welcome-section" class="space-y-8">
<div class="welcome-card rounded-lg p-8 text-center">
<h2 class="text-3xl font-bold mb-4">Bienvenue dans l'Application d'Inspection Qualité</h2>
<p class="text-lg opacity-90 mb-6">Gérez vos inspections qualité de manière simple et efficace, même hors-ligne</p>
<div class="flex flex-col sm:flex-row gap-4 justify-center">
<button onclick="UI.switchView('models')" class="bg-white text-blue-600 px-6 py-3 rounded-lg font-semibold hover:bg-blue-50 transition duration-200">
<i class="fas fa-rocket mr-2"></i>Commencer
</button>
<button onclick="UI.showHelpModal()" class="bg-blue-500 text-white px-6 py-3 rounded-lg font-semibold hover:bg-blue-400 transition duration-200">
<i class="fas fa-question-circle mr-2"></i>Guide d'utilisation
</button>
</div>
</div>
<!-- Workflow -->
<div class="bg-white rounded-lg shadow-md p-6">
<h3 class="text-2xl font-semibold text-gray-800 mb-6 text-center">Comment ça fonctionne ?</h3>
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
<div class="workflow-step text-center">
<div class="bg-blue-100 w-16 h-16 rounded-full flex items-center justify-center mx-auto mb-4">
<i class="fas fa-file-import text-2xl text-blue-600"></i>
</div>
<h4 class="text-lg font-semibold mb-2">1. Importez votre modèle</h4>
<p class="text-gray-600">Importez un fichier Excel (.xlsx) contenant vos points de contrôle et paramètres d'inspection</p>
</div>
<div class="workflow-step text-center">
<div class="bg-green-100 w-16 h-16 rounded-full flex items-center justify-center mx-auto mb-4">
<i class="fas fa-clipboard-check text-2xl text-green-600"></i>
</div>
<h4 class="text-lg font-semibold mb-2">2. Réalisez l'inspection</h4>
<p class="text-gray-600">Remplissez votre checklist point par point, ajoutez commentaires et photos selon les besoins</p>
</div>
<div class="workflow-step text-center">
<div class="bg-purple-100 w-16 h-16 rounded-full flex items-center justify-center mx-auto mb-4">
<i class="fas fa-chart-pie text-2xl text-purple-600"></i>
</div>
<h4 class="text-lg font-semibold mb-2">3. Analysez les résultats</h4>
<p class="text-gray-600">Consultez les rapports détaillés et le tableau de bord pour analyser vos performances</p>
</div>
</div>
</div>
<!-- Features -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<div class="feature-card bg-white rounded-lg shadow-md p-6">
<div class="flex items-center mb-4">
<i class="fas fa-wifi-off text-2xl text-blue-600 mr-3"></i>
<h4 class="text-lg font-semibold">Fonctionnement hors-ligne</h4>
</div>
<p class="text-gray-600">Travaillez sans connexion Internet. Toutes vos données sont sauvegardées localement.</p>
</div>
<div class="feature-card bg-white rounded-lg shadow-md p-6">
<div class="flex items-center mb-4">
<i class="fas fa-camera text-2xl text-green-600 mr-3"></i>
<h4 class="text-lg font-semibold">Photos et commentaires</h4>
</div>
<p class="text-gray-600">Ajoutez des photos et commentaires détaillés pour documenter vos observations.</p>
</div>
<div class="feature-card bg-white rounded-lg shadow-md p-6">
<div class="flex items-center mb-4">
<i class="fas fa-file-export text-2xl text-purple-600 mr-3"></i>
<h4 class="text-lg font-semibold">Export et partage</h4>
</div>
<p class="text-gray-600">Exportez vos rapports en PDF et partagez vos inspections via des packages ZIP.</p>
</div>
<div class="feature-card bg-white rounded-lg shadow-md p-6">
<div class="flex items-center mb-4">
<i class="fas fa-cogs text-2xl text-orange-600 mr-3"></i>
<h4 class="text-lg font-semibold">Types de paramètres variés</h4>
</div>
<p class="text-gray-600">Supporte texte, numérique, dates, listes déroulantes, cases à cocher et statuts.</p>
</div>
<div class="feature-card bg-white rounded-lg shadow-md p-6">
<div class="flex items-center mb-4">
<i class="fas fa-chart-line text-2xl text-red-600 mr-3"></i>
<h4 class="text-lg font-semibold">Tableau de bord</h4>
</div>
<p class="text-gray-600">Visualisez vos performances avec des graphiques et indicateurs clés.</p>
</div>
<div class="feature-card bg-white rounded-lg shadow-md p-6">
<div class="flex items-center mb-4">
<i class="fas fa-shield-alt text-2xl text-teal-600 mr-3"></i>
<h4 class="text-lg font-semibold">Sécurité alimentaire</h4>
</div>
<p class="text-gray-600">Conçu spécifiquement pour les audits et inspections en industrie alimentaire.</p>
</div>
</div>
<!-- Tips -->
<div class="bg-yellow-50 border-l-4 border-yellow-400 p-6 rounded-lg">
<h4 class="text-lg font-semibold text-yellow-800 mb-3">
<i class="fas fa-lightbulb mr-2"></i>Conseils pour bien commencer
</h4>
<ul class="text-yellow-700 space-y-2">
<li><i class="fas fa-check-circle mr-2 text-yellow-600"></i>Préparez votre fichier Excel avec les colonnes requises : ID_Point, Categorie, PointDeControle, TypeParametre</li>
<li><i class="fas fa-check-circle mr-2 text-yellow-600"></i>Utilisez les nouveaux boutons colorés pour les statuts : Conforme (vert), Non Conforme (rouge), Hors Plage (orange), N.A. (gris)</li>
<li><i class="fas fa-check-circle mr-2 text-yellow-600"></i>Exportez régulièrement vos inspections pour sauvegarder vos données</li>
<li><i class="fas fa-check-circle mr-2 text-yellow-600"></i>Utilisez le tableau de bord pour suivre vos tendances et identifier les points d'amélioration</li>
</ul>
</div>
</section>
<!-- Section Modèles -->
<section id="models-section" class="hidden space-y-6">
<h2 class="text-xl font-semibold text-gray-700">Gestion Modèles Checklists</h2>
<div class="bg-white p-6 rounded-lg shadow border border-gray-200">
<h3 class="text-lg font-medium mb-4 text-gray-800">Importer Nouveau Modèle</h3>
<!-- Bouton pour télécharger template -->
<div class="mb-4 p-4 bg-blue-50 border border-blue-200 rounded-lg">
<div class="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-3">
<div>
<h4 class="font-medium text-blue-800 mb-1">📥 Template Excel</h4>
<p class="text-sm text-blue-600">Téléchargez le modèle Excel avec les colonnes requises</p>
</div>
<button id="download-template-button" class="px-4 py-2 bg-blue-600 text-white font-semibold rounded-lg shadow-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-400 flex items-center">
<i class="fas fa-download mr-2"></i>Télécharger Template
</button>
</div>
</div>
<div class="flex flex-col sm:flex-row items-start sm:items-center gap-3">
<input type="file" id="import-excel-input" accept=".xlsx" class="block w-full text-sm text-gray-600 border border-gray-300 rounded-md p-2 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"/>
<button id="import-excel-button" class="w-full sm:w-auto px-4 py-2 bg-blue-600 text-white font-semibold rounded-lg shadow-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-400 focus:ring-opacity-75 disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center" disabled>
<i class="fas fa-file-import mr-2"></i>Importer Modèle
</button>
</div>
<p class="mt-3 text-xs text-gray-500">
Fichier Excel (.xlsx). Requis: ID_Point, Categorie, PointDeControle, TypeParametre.
Types supportés: Texte, Numérique, Date, Liste Déroulante, Case à cocher, Statut Seulement.
</p>
</div>
<div class="bg-white p-6 rounded-lg shadow border border-gray-200">
<h3 class="text-lg font-medium mb-4 text-gray-800">Modèles Existants</h3>
<div id="checklist-models-list" class="space-y-3">
<p class="text-gray-500 italic text-center py-4">Chargement...</p>
</div>
</div>
</section>
<!-- Section Inspections -->
<section id="inspections-section" class="hidden space-y-6">
<h2 class="text-xl font-semibold text-gray-700">Inspections</h2>
<div class="bg-white p-6 rounded-lg shadow border border-gray-200">
<h3 class="text-lg font-medium mb-4 text-gray-800">Démarrer Nouvelle Inspection</h3>
<div class="space-y-4">
<div>
<label for="new-inspection-model" class="block text-sm font-medium text-gray-700 mb-1">Choisir modèle :</label>
<select id="new-inspection-model" class="mt-1 block w-full pl-3 pr-10 py-2 text-base border-gray-300 focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm rounded-md shadow-sm appearance-none bg-white" disabled>
<option value="">Chargement...</option>
</select>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label for="inspector-name" class="block text-sm font-medium text-gray-700 mb-1">Nom inspecteur :</label>
<input type="text" id="inspector-name" placeholder="Entrez votre nom" class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm">
</div>
<div>
<label for="inspection-site" class="block text-sm font-medium text-gray-700 mb-1">Site/Département :</label>
<input type="text" id="inspection-site" placeholder="Ex: Site Nord, Département Production..." class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm">
</div>
</div>
<button id="start-inspection-button" class="w-full px-4 py-2 bg-green-600 text-white font-semibold rounded-lg shadow-md hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-green-400 focus:ring-opacity-75 disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center" disabled>
<i class="fas fa-play mr-2"></i>Démarrer Inspection
</button>
</div>
</div>
<div class="bg-white p-6 rounded-lg shadow border border-gray-200">
<h3 class="text-lg font-medium mb-4 text-gray-800">Inspections Sauvegardées</h3>
<div id="inspections-list" class="space-y-4">
<p class="text-gray-500 italic text-center py-4">Chargement...</p>
</div>
</div>
</section>
<!-- Section Interface d'Inspection -->
<section id="inspection-interface-section" class="hidden space-y-6">
<div class="flex items-center justify-between mb-6">
<div>
<h2 class="text-2xl font-semibold text-gray-700" id="current-inspection-title">Inspection en cours</h2>
<p class="text-sm text-gray-500" id="current-inspection-info">Informations de l'inspection</p>
</div>
<div class="flex gap-3">
<button id="save-inspection-progress" class="px-4 py-2 bg-blue-600 text-white font-semibold rounded-lg shadow-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-400 flex items-center">
<i class="fas fa-save mr-2"></i>Sauvegarder
</button>
<button id="finish-inspection" class="px-4 py-2 bg-green-600 text-white font-semibold rounded-lg shadow-md hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-green-400 flex items-center">
<i class="fas fa-check mr-2"></i>Terminer
</button>
<button id="exit-inspection" class="px-4 py-2 bg-gray-600 text-white font-semibold rounded-lg shadow-md hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-gray-400 flex items-center">
<i class="fas fa-times mr-2"></i>Quitter
</button>
</div>
</div>
<!-- Barre de progression -->
<div class="bg-white p-4 rounded-lg shadow border border-gray-200">
<div class="flex justify-between items-center mb-2">
<span class="text-sm font-medium text-gray-700">Progression</span>
<span class="text-sm text-gray-500" id="progress-text">0/0 points</span>
</div>
<div class="w-full bg-gray-200 rounded-full h-2">
<div id="progress-bar" class="bg-blue-600 h-2 rounded-full transition-all duration-300" style="width: 0%"></div>
</div>
</div>
<!-- Points de contrôle -->
<div id="inspection-points-container" class="space-y-4">
<!-- Les points seront générés dynamiquement -->
</div>
</section>
<!-- Section Tableau de Bord -->
<section id="dashboard-section" class="hidden space-y-6">
<div class="flex flex-col sm:flex-row justify-between items-start sm:items-center mb-6 gap-4">
<h2 class="text-2xl font-semibold text-gray-700">Tableau de Bord</h2>
<button id="export-dashboard-pdf" class="px-4 py-2 bg-purple-600 text-white font-semibold rounded-lg shadow-md hover:bg-purple-700 focus:outline-none focus:ring-2 focus:ring-purple-400 flex items-center">
<i class="fas fa-file-pdf mr-2"></i>Exporter PDF
</button>
</div>
<!-- Filtres -->
<div class="bg-white p-6 rounded-lg shadow border border-gray-200">
<h3 class="text-lg font-medium mb-4 text-gray-800">Filtres</h3>
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
<div>
<label for="dashboard-model-filter" class="block text-sm font-medium text-gray-700 mb-1">Modèle :</label>
<select id="dashboard-model-filter" class="block w-full pl-3 pr-10 py-2 text-base border-gray-300 focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm rounded-md shadow-sm appearance-none bg-white">
<option value="">Tous les modèles</option>
</select>
</div>
<div>
<label for="dashboard-site-filter" class="block text-sm font-medium text-gray-700 mb-1">Site/Département :</label>
<select id="dashboard-site-filter" class="block w-full pl-3 pr-10 py-2 text-base border-gray-300 focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm rounded-md shadow-sm appearance-none bg-white">
<option value="">Tous les sites</option>
</select>
</div>
<div>
<label for="dashboard-date-from" class="block text-sm font-medium text-gray-700 mb-1">Du :</label>
<input type="date" id="dashboard-date-from" class="block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm">
</div>
<div>
<label for="dashboard-date-to" class="block text-sm font-medium text-gray-700 mb-1">Au :</label>
<input type="date" id="dashboard-date-to" class="block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm">
</div>
</div>
<div class="mt-4">
<button id="apply-dashboard-filters" class="px-6 py-2 bg-blue-600 text-white font-semibold rounded-lg shadow-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-400">
<i class="fas fa-filter mr-2"></i>Appliquer les filtres
</button>
</div>
</div>
<!-- KPIs -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<div class="kpi-card success">
<div class="flex items-center justify-between">
<div>
<p class="text-sm font-medium text-gray-600">Conformité Globale</p>
<p id="kpi-conformity" class="text-2xl font-bold text-green-600">0%</p>
</div>
<i class="fas fa-check-circle text-3xl text-green-500"></i>
</div>
<p id="kpi-conformity-change" class="text-xs text-gray-500 mt-2">Aucune donnée</p>
</div>
<div class="kpi-card info">
<div class="flex items-center justify-between">
<div>
<p class="text-sm font-medium text-gray-600">Total Inspections</p>
<p id="kpi-total-inspections" class="text-2xl font-bold text-blue-600">0</p>
</div>
<i class="fas fa-clipboard-list text-3xl text-blue-500"></i>
</div>
<p id="kpi-inspections-change" class="text-xs text-gray-500 mt-2">Aucune donnée</p>
</div>
<div class="kpi-card warning">
<div class="flex items-center justify-between">
<div>
<p class="text-sm font-medium text-gray-600">Hors Plage</p>
<p id="kpi-out-of-range" class="text-2xl font-bold text-orange-600">0</p>
</div>
<i class="fas fa-exclamation-triangle text-3xl text-orange-500"></i>
</div>
<p id="kpi-out-of-range-change" class="text-xs text-gray-500 mt-2">Aucune donnée</p>
</div>
<div class="kpi-card danger">
<div class="flex items-center justify-between">
<div>
<p class="text-sm font-medium text-gray-600">Non Conformes</p>
<p id="kpi-non-conform" class="text-2xl font-bold text-red-600">0</p>
</div>
<i class="fas fa-times-circle text-3xl text-red-500"></i>
</div>
<p id="kpi-non-conform-change" class="text-xs text-gray-500 mt-2">Aucune donnée</p>
</div>
</div>
<!-- Graphiques -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div class="chart-container">
<h3 class="text-lg font-semibold mb-4">Évolution de la Conformité</h3>
<div style="position: relative; height: 300px;">
<canvas id="conformity-trend-chart"></canvas>
</div>
</div>
<div class="chart-container">
<h3 class="text-lg font-semibold mb-4">Répartition par Statut</h3>
<div style="position: relative; height: 300px;">
<canvas id="status-distribution-chart"></canvas>
</div>
</div>
</div>
<div class="chart-container">
<h3 class="text-lg font-semibold mb-4">Top 5 Non-Conformités par Catégorie</h3>
<div style="position: relative; height: 300px;">
<canvas id="top-non-conformities-chart"></canvas>
</div>
</div>
<!-- Tableau -->
<div class="bg-white rounded-lg shadow border border-gray-200">
<div class="px-6 py-4 border-b border-gray-200">
<h3 class="text-lg font-semibold">Inspections Détaillées</h3>
</div>
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Date</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Modèle</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Site</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Inspecteur</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Conformité</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Non-Conformes</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Hors Plage</th>
</tr>
</thead>
<tbody id="dashboard-inspections-table" class="bg-white divide-y divide-gray-200">
<tr>
<td colspan="7" class="px-6 py-4 text-center text-gray-500">Aucune inspection trouvée</td>
</tr>
</tbody>
</table>
</div>
</div>
</section>
</main>
</div>
<!-- Modaux -->
<div id="modal" class="modal" onclick="UI.closeModalOnClickOutside(event)">
<div class="modal-content">
<div class="flex justify-between items-center mb-4 border-b pb-2">
<h3 id="modal-title" class="text-xl font-semibold text-gray-800">Titre</h3>
<button onclick="UI.closeModal()" class="text-gray-400 hover:text-gray-600 text-2xl font-bold">×</button>
</div>
<div id="modal-body" class="text-gray-700 mb-6">Contenu</div>
<div id="modal-footer" class="mt-6 text-right space-x-3 border-t pt-4"></div>
</div>
</div>
<div id="toast-container"></div>
<div id="loading-indicator" style="display: none;">
<i class="fas fa-spinner fa-spin"></i>
<p id="loading-message">Chargement...</p>
</div>
<script>
(function() {
'use strict';
// Constants
const MODELS_KEY = 'checklistApp_Models_v2';
const INSPECTIONS_KEY = 'checklistApp_Inspections_v2';
const INSPECTOR_NAME_KEY = 'checklistApp_InspectorName_v2';
// Global variables
let currentModelData = {};
let currentInspectionsData = {};
let lastUsedInspectorName = '';
let dashboardCharts = {};
// Utilities
const generateId = () => 'id_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
const formatDate = (dateString) => {
if (!dateString) return 'N/A';
try {
const date = new Date(dateString);
if (isNaN(date.getTime())) return 'Date invalide';
return date.toLocaleString('fr-FR', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
} catch (e) {
console.error("Erreur date:", dateString, e);
return 'Date invalide';
}
};
// TemplateManager pour créer et télécharger le template Excel
window.TemplateManager = {
createExcelTemplate() {
const templateData = [
{
'ID_Point': 'TEMP_001',
'Categorie': 'Température',
'PointDeControle': 'Température chambre froide positive',
'Description': 'Contrôle de la température de la chambre froide positive',
'CritereAcceptation': 'Entre 0°C et +4°C',
'TypeParametre': 'Numérique',
'OptionsParametre': '',
'ModeleCommentaire': 'Mesurer avec thermomètre étalonné toutes les 4h'
},
{
'ID_Point': 'TEMP_002',
'Categorie': 'Température',
'PointDeControle': 'Température chambre froide négative',
'Description': 'Contrôle de la température de la chambre froide négative',
'CritereAcceptation': 'Inférieure à -18°C',
'TypeParametre': 'Numérique',
'OptionsParametre': '',
'ModeleCommentaire': 'Vérifier l\'étalonnage du thermomètre'
},
{
'ID_Point': 'HYG_001',
'Categorie': 'Hygiène Personnel',
'PointDeControle': 'Port des équipements de protection',
'Description': 'Vérification du port correct des EPI',
'CritereAcceptation': 'Tous les EPI requis portés correctement',
'TypeParametre': 'Statut Seulement',
'OptionsParametre': '',
'ModeleCommentaire': 'Charlotte, blouse, chaussures de sécurité'
},
{
'ID_Point': 'HYG_002',
'Categorie': 'Hygiène Personnel',
'PointDeControle': 'Lavage des mains',
'Description': 'Procédure de lavage des mains respectée',
'CritereAcceptation': 'Procédure complète de 30 secondes minimum',
'TypeParametre': 'Case à cocher',
'OptionsParametre': 'Savonnage adequat;Rinçage complet;Séchage avec papier;Désinfection',
'ModeleCommentaire': 'Observer la procédure complète'
},
{
'ID_Point': 'NETT_001',
'Categorie': 'Nettoyage',
'PointDeControle': 'État de propreté des surfaces',
'Description': 'Contrôle visuel de la propreté des surfaces de travail',
'CritereAcceptation': 'Aucune souillure visible',
'TypeParametre': 'Liste Déroulante',
'OptionsParametre': 'Excellent;Bon;Moyen;Insuffisant;Inacceptable',
'ModeleCommentaire': 'Examiner les plans de travail, équipements'
},
{
'ID_Point': 'DOC_001',
'Categorie': 'Documentation',
'PointDeControle': 'Enregistrements de nettoyage',
'Description': 'Vérification de la tenue des registres',
'CritereAcceptation': 'Tous les enregistrements à jour',
'TypeParametre': 'Texte',
'OptionsParametre': '',
'ModeleCommentaire': 'Vérifier les 7 derniers jours'
},
{
'ID_Point': 'TRA_001',
'Categorie': 'Traçabilité',
'PointDeControle': 'Date limite de consommation',
'Description': 'Contrôle des DLC sur les produits stockés',
'CritereAcceptation': 'Aucun produit périmé',
'TypeParametre': 'Date',
'OptionsParametre': '',
'ModeleCommentaire': 'Contrôler un échantillon représentatif'
}
];
return templateData;
},
downloadTemplate() {
console.log('Début téléchargement template Excel...');
try {
if (typeof XLSX === 'undefined') {
throw new Error('XLSX (SheetJS) non disponible');
}
const templateData = this.createExcelTemplate();
console.log('Données template créées:', templateData.length, 'lignes');
const worksheet = XLSX.utils.json_to_sheet(templateData);
// Définir la largeur des colonnes
const columnWidths = [
{ wpx: 80 }, // ID_Point
{ wpx: 120 }, // Categorie
{ wpx: 200 }, // PointDeControle
{ wpx: 250 }, // Description
{ wpx: 200 }, // CritereAcceptation
{ wpx: 120 }, // TypeParametre
{ wpx: 200 }, // OptionsParametre
{ wpx: 250 } // ModeleCommentaire
];
worksheet['!cols'] = columnWidths;
const workbook = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(workbook, worksheet, 'Template_Checklist');
// Ajouter une feuille d'instructions
const instructionsData = [
{ 'Colonne': 'ID_Point', 'Description': 'Identifiant unique du point de contrôle', 'Obligatoire': 'OUI', 'Exemple': 'TEMP_001' },
{ 'Colonne': 'Categorie', 'Description': 'Catégorie du point de contrôle', 'Obligatoire': 'OUI', 'Exemple': 'Température' },
{ 'Colonne': 'PointDeControle', 'Description': 'Nom du point de contrôle', 'Obligatoire': 'OUI', 'Exemple': 'Température chambre froide' },
{ 'Colonne': 'Description', 'Description': 'Description détaillée (optionnel)', 'Obligatoire': 'NON', 'Exemple': 'Contrôle de la température...' },
{ 'Colonne': 'CritereAcceptation', 'Description': 'Critères d\'acceptation (optionnel)', 'Obligatoire': 'NON', 'Exemple': 'Entre 0°C et +4°C' },
{ 'Colonne': 'TypeParametre', 'Description': 'Type de paramètre à saisir', 'Obligatoire': 'OUI', 'Exemple': 'Numérique' },
{ 'Colonne': 'OptionsParametre', 'Description': 'Options pour listes (séparées par ;)', 'Obligatoire': 'NON', 'Exemple': 'Bon;Moyen;Mauvais' },
{ 'Colonne': 'ModeleCommentaire', 'Description': 'Commentaire modèle (optionnel)', 'Obligatoire': 'NON', 'Exemple': 'Mesurer toutes les 4h' }
];
const instructionsSheet = XLSX.utils.json_to_sheet(instructionsData);
instructionsSheet['!cols'] = [
{ wpx: 120 },
{ wpx: 300 },
{ wpx: 80 },
{ wpx: 200 }
];
XLSX.utils.book_append_sheet(workbook, instructionsSheet, 'Instructions');
// Ajouter une feuille sur les types de paramètres
const typesData = [
{ 'Type': 'Texte', 'Description': 'Saisie libre de texte', 'Utilisation': 'Commentaires, observations' },
{ 'Type': 'Numérique', 'Description': 'Saisie de nombres (température, pH, etc.)', 'Utilisation': 'Mesures, quantités' },
{ 'Type': 'Date', 'Description': 'Saisie de dates', 'Utilisation': 'DLC, dates de contrôle' },
{ 'Type': 'Liste Déroulante', 'Description': 'Choix dans une liste prédéfinie', 'Utilisation': 'États, niveaux de qualité' },
{ 'Type': 'Case à cocher', 'Description': 'Multiple sélections possibles', 'Utilisation': 'Vérifications multiples' },
{ 'Type': 'Statut Seulement', 'Description': 'Conforme/Non Conforme/Hors Plage/N.A.', 'Utilisation': 'Conformité simple avec boutons colorés' }
];
const typesSheet = XLSX.utils.json_to_sheet(typesData);
typesSheet['!cols'] = [
{ wpx: 120 },
{ wpx: 250 },
{ wpx: 200 }
];
XLSX.utils.book_append_sheet(workbook, typesSheet, 'Types_Parametres');
console.log('Workbook créé, tentative d\'écriture...');
XLSX.writeFile(workbook, 'Template_Checklist_Inspection.xlsx');
console.log('Fichier Excel écrit avec succès');
UI.showToast('Template Excel téléchargé avec succès !', 'success');
} catch (error) {
console.error('Erreur téléchargement template:', error);
UI.showToast('Erreur lors du téléchargement du template: ' + error.message, 'error');
}
}
};
// InspectionInterface pour gérer la saisie des inspections
window.InspectionInterface = {
currentInspectionId: null,
continueInspection(inspectionId) {
this.currentInspectionId = inspectionId;
const inspection = currentInspectionsData[inspectionId];
if (!inspection) {
UI.showToast('Inspection non trouvée.', 'error');
return;
}
const model = currentModelData[inspection.modelId];
if (!model) {
UI.showToast('Modèle de l\'inspection non trouvé.', 'error');
return;
}
this.loadInspectionInterface(inspection, model);
UI.switchView('inspection-interface');
},
loadInspectionInterface(inspection, model) {
// Mettre à jour le titre et les infos
document.getElementById('current-inspection-title').textContent = model.name || 'Inspection';
document.getElementById('current-inspection-info').textContent =
`Inspecteur: ${inspection.inspectorName} | Site: ${inspection.site} | Démarrée: ${formatDate(inspection.startTime)}`;
// Calculer et afficher la progression
this.updateProgress(inspection, model);
// Générer les points de contrôle
this.generateInspectionPoints(inspection, model);
},
updateProgress(inspection, model) {
const progress = InspectionManager.calculateProgress(inspection);
const progressText = document.getElementById('progress-text');
const progressBar = document.getElementById('progress-bar');
if (progressText) {
progressText.textContent = `${progress.completed}/${progress.total} points (${progress.percentage.toFixed(0)}%)`;
}
if (progressBar) {
progressBar.style.width = progress.percentage + '%';
}
},
generateInspectionPoints(inspection, model) {
const container = document.getElementById('inspection-points-container');
if (!container) return;
container.innerHTML = '';
// Grouper par catégorie
const pointsByCategory = {};
model.points.forEach(point => {
const category = point.Categorie || 'Général';
if (!pointsByCategory[category]) {
pointsByCategory[category] = [];
}
pointsByCategory[category].push(point);
});
// Créer les sections par catégorie
Object.keys(pointsByCategory).sort().forEach(category => {
const categorySection = document.createElement('div');
categorySection.className = 'bg-white rounded-lg shadow border border-gray-200';
const categoryHeader = document.createElement('div');
categoryHeader.className = 'bg-gray-50 px-6 py-3 border-b border-gray-200 cursor-pointer';
categoryHeader.innerHTML = `<h3 class="text-lg font-semibold text-gray-800 flex items-center">
<i class="fas fa-chevron-down mr-2 transition-transform"></i>
${category}
<span class="ml-auto text-sm text-gray-500">${pointsByCategory[category].length} points</span>
</h3>`;
const categoryContent = document.createElement('div');
categoryContent.className = 'p-6 space-y-6';
// Ajouter les points de cette catégorie
pointsByCategory[category].forEach(point => {
const pointElement = this.createPointElement(point, inspection);
categoryContent.appendChild(pointElement);
});
// Toggle collapse/expand
categoryHeader.addEventListener('click', () => {
const icon = categoryHeader.querySelector('i');
if (categoryContent.style.display === 'none') {
categoryContent.style.display = 'block';
icon.style.transform = 'rotate(0deg)';
} else {
categoryContent.style.display = 'none';
icon.style.transform = 'rotate(-90deg)';
}
});
categorySection.appendChild(categoryHeader);
categorySection.appendChild(categoryContent);
container.appendChild(categorySection);
});
},
createPointElement(point, inspection) {
const result = inspection.results[point.ID_Point] || { status: 'pending', value: '', comment: '', photos: [] };
const pointDiv = document.createElement('div');
pointDiv.className = 'border border-gray-200 rounded-lg p-4 bg-gray-50';
pointDiv.innerHTML = `
<div class="mb-3">
<h4 class="font-semibold text-gray-800">${point.PointDeControle}</h4>
<p class="text-sm text-gray-600 mt-1">${point.Description || ''}</p>
${point.CritereAcceptation ? `<p class="text-sm text-blue-600 mt-1"><strong>Critère:</strong> ${point.CritereAcceptation}</p>` : ''}
${point.ModeleCommentaire ? `<p class="text-xs text-gray-500 italic mt-1">${point.ModeleCommentaire}</p>` : ''}
</div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Valeur/Résultat :</label>
${this.createInputField(point, result)}
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Statut :</label>
${point.TypeParametre === 'Statut Seulement' ?
this.createStatusButtons(point, result) :
`<select data-point="${point.ID_Point}" data-field="status" class="point-input block w-full pl-3 pr-10 py-2 text-base border-gray-300 focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm rounded-md">
<option value="pending" ${result.status === 'pending' ? 'selected' : ''}>En attente</option>
<option value="Conforme" ${result.status === 'Conforme' ? 'selected' : ''}>Conforme</option>
<option value="Non Conforme" ${result.status === 'Non Conforme' ? 'selected' : ''}>Non Conforme</option>
<option value="Hors Plage" ${result.status === 'Hors Plage' ? 'selected' : ''}>Hors Plage</option>
<option value="N/A" ${result.status === 'N/A' ? 'selected' : ''}>N/A</option>
</select>`
}
</div>
</div>
<div class="mt-4">
<label class="block text-sm font-medium text-gray-700 mb-2">Commentaire :</label>
<textarea data-point="${point.ID_Point}" data-field="comment" class="point-input block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm" rows="2" placeholder="Ajouter un commentaire...">${result.comment || ''}</textarea>
</div>
<div class="mt-4">
<label class="block text-sm font-medium text-gray-700 mb-2">Photos :</label>
<div class="space-y-3">
<div class="flex flex-wrap gap-2">
<button type="button" onclick="InspectionInterface.openCamera('${point.ID_Point}')" class="px-3 py-2 bg-blue-500 text-white text-sm rounded shadow hover:bg-blue-600 flex items-center">
<i class="fas fa-camera mr-2"></i>Prendre Photo
</button>
<button type="button" onclick="InspectionInterface.selectPhoto('${point.ID_Point}')" class="px-3 py-2 bg-green-500 text-white text-sm rounded shadow hover:bg-green-600 flex items-center">
<i class="fas fa-image mr-2"></i>Sélectionner Photo
</button>
</div>
<input type="file" id="photo-input-${point.ID_Point}" accept="image/*" style="display: none;" multiple>
<div id="photos-container-${point.ID_Point}" class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-2">
${this.renderPhotos(result.photos || [], point.ID_Point)}
</div>
</div>
</div>
`;
return pointDiv;
},
// NOUVEAU: Créer des boutons de statut colorés
createStatusButtons(point, result) {
const statuses = [
{ value: 'Conforme', label: 'C', class: 'conforme', title: 'Conforme' },
{ value: 'Non Conforme', label: 'NC', class: 'non-conforme', title: 'Non Conforme' },
{ value: 'Hors Plage', label: 'HP', class: 'hors-plage', title: 'Hors Plage' },
{ value: 'N/A', label: 'NA', class: 'na', title: 'Non Applicable' }
];
return `
<div class="status-buttons">
${statuses.map(status => `
<button type="button"
class="status-button ${status.class} ${result.status === status.value ? 'active' : ''}"
data-point="${point.ID_Point}"
data-status="${status.value}"
title="${status.title}"
onclick="InspectionInterface.setStatus('${point.ID_Point}', '${status.value}')">
${status.label}
</button>
`).join('')}
</div>
`;
},
// NOUVEAU: Fonction pour définir le statut via les boutons
setStatus(pointId, status) {
if (!this.currentInspectionId) return;
const inspection = currentInspectionsData[this.currentInspectionId];
if (!inspection) return;
if (!inspection.results[pointId]) {
inspection.results[pointId] = { status: 'pending', value: '', comment: '', photos: [] };
}
inspection.results[pointId].status = status;
// Mettre à jour l'affichage des boutons
const buttons = document.querySelectorAll(`button[data-point="${pointId}"]`);
buttons.forEach(button => {
button.classList.remove('active');
if (button.dataset.status === status) {
button.classList.add('active');
}
});
// Sauvegarder automatiquement
setTimeout(() => {
this.saveProgress();
}, 500);
},
renderPhotos(photos, pointId) {
if (!photos || photos.length === 0) {
return '<p class="text-gray-500 text-sm col-span-full">Aucune photo ajoutée</p>';
}
return photos.map((photo, index) => `
<div class="relative group">
<img src="${photo.data}" alt="Photo ${index + 1}" class="w-full h-20 object-cover rounded border cursor-pointer hover:opacity-75" onclick="InspectionInterface.viewPhoto('${photo.data}', '${photo.name}')">
<button type="button" onclick="InspectionInterface.removePhoto('${pointId}', ${index})" class="absolute top-1 right-1 bg-red-500 text-white rounded-full w-5 h-5 flex items-center justify-center text-xs opacity-0 group-hover:opacity-100 transition-opacity">
<i class="fas fa-times"></i>
</button>
<p class="text-xs text-gray-600 mt-1 truncate">${photo.name}</p>
</div>
`).join('');
},
openCamera(pointId) {
const input = document.getElementById(`photo-input-${pointId}`);
if (input) {
input.setAttribute('capture', 'camera');
input.click();
input.onchange = (event) => this.handlePhotoSelection(event, pointId);
}
},
selectPhoto(pointId) {
const input = document.getElementById(`photo-input-${pointId}`);
if (input) {
input.removeAttribute('capture');
input.click();
input.onchange = (event) => this.handlePhotoSelection(event, pointId);
}
},
handlePhotoSelection(event, pointId) {
const files = event.target.files;
if (!files || files.length === 0) return;
const inspection = currentInspectionsData[this.currentInspectionId];
if (!inspection) return;
if (!inspection.results[pointId]) {
inspection.results[pointId] = { status: 'pending', value: '', comment: '', photos: [] };
}
if (!inspection.results[pointId].photos) {
inspection.results[pointId].photos = [];
}
Array.from(files).forEach(file => {
if (file.type.startsWith('image/')) {
const reader = new FileReader();
reader.onload = (e) => {
const photo = {
name: file.name,
data: e.target.result,
timestamp: new Date().toISOString(),
size: file.size
};
inspection.results[pointId].photos.push(photo);
// Limiter à 10 photos maximum par point
if (inspection.results[pointId].photos.length > 10) {
inspection.results[pointId].photos = inspection.results[pointId].photos.slice(-10);
}
// Mettre à jour l'affichage
this.updatePhotosDisplay(pointId, inspection.results[pointId].photos);
// Sauvegarder
DataManager.saveData(INSPECTIONS_KEY, currentInspectionsData);
UI.showToast('Photo ajoutée avec succès', 'success');
};
reader.readAsDataURL(file);
} else {
UI.showToast('Veuillez sélectionner un fichier image valide', 'warning');
}
});
// Reset input
event.target.value = '';
},
updatePhotosDisplay(pointId, photos) {
const container = document.getElementById(`photos-container-${pointId}`);
if (container) {
container.innerHTML = this.renderPhotos(photos, pointId);
}
},
removePhoto(pointId, photoIndex) {
const inspection = currentInspectionsData[this.currentInspectionId];
if (!inspection || !inspection.results[pointId] || !inspection.results[pointId].photos) return;
inspection.results[pointId].photos.splice(photoIndex, 1);
this.updatePhotosDisplay(pointId, inspection.results[pointId].photos);
DataManager.saveData(INSPECTIONS_KEY, currentInspectionsData);
UI.showToast('Photo supprimée', 'success');
},
viewPhoto(photoData, photoName) {
const modalContent = `
<div class="text-center">
<img src="${photoData}" alt="${photoName}" class="max-w-full max-h-96 mx-auto rounded-lg shadow-lg">
<p class="mt-4 text-sm text-gray-600">${photoName}</p>
<div class="mt-4 flex justify-center gap-3">
<button onclick="InspectionInterface.downloadPhoto('${photoData}', '${photoName}')" class="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600">
<i class="fas fa-download mr-2"></i>Télécharger
</button>
</div>
</div>
`;
UI.showModal('Aperçu Photo', modalContent, [
{ text: 'Fermer', class: 'bg-gray-200 hover:bg-gray-300 text-gray-700', action: UI.closeModal }
]);
},
downloadPhoto(photoData, photoName) {
const link = document.createElement('a');
link.href = photoData;
link.download = photoName || 'photo_inspection.jpg';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
UI.showToast('Photo téléchargée', 'success');
},
createInputField(point, result) {
const baseClasses = 'point-input block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm';
switch (point.TypeParametre) {
case 'Numérique':
return `<input type="number" step="any" data-point="${point.ID_Point}" data-field="value" class="${baseClasses}" placeholder="Entrer une valeur numérique" value="${result.value || ''}">`;
case 'Date':
return `<input type="date" data-point="${point.ID_Point}" data-field="value" class="${baseClasses}" value="${result.value || ''}">`;
case 'Liste Déroulante':
const options = point.OptionsParametre ? point.OptionsParametre.split(';') : [];
let selectHtml = `<select data-point="${point.ID_Point}" data-field="value" class="${baseClasses}">
<option value="">-- Sélectionner --</option>`;
options.forEach(option => {
const selected = result.value === option.trim() ? 'selected' : '';
selectHtml += `<option value="${option.trim()}" ${selected}>${option.trim()}</option>`;
});
selectHtml += '</select>';
return selectHtml;
case 'Case à cocher':
const checkOptions = point.OptionsParametre ? point.OptionsParametre.split(';') : [];
const selectedValues = result.value ? result.value.split(',') : [];
let checkboxHtml = '<div class="space-y-2">';
checkOptions.forEach((option, index) => {
const checked = selectedValues.includes(option.trim()) ? 'checked' : '';
checkboxHtml += `
<label class="flex items-center">
<input type="checkbox" data-point="${point.ID_Point}" data-field="value" data-option="${option.trim()}" class="point-checkbox mr-2" ${checked}>
<span class="text-sm">${option.trim()}</span>
</label>`;
});
checkboxHtml += '</div>';
return checkboxHtml;
case 'Statut Seulement':
return `<div class="text-sm text-gray-500 italic">Utilisez les boutons colorés ci-contre →</div>`;
default: // Texte
return `<input type="text" data-point="${point.ID_Point}" data-field="value" class="${baseClasses}" placeholder="Entrer du texte" value="${result.value || ''}">`;
}
},
saveProgress() {
if (!this.currentInspectionId) return;
const inspection = currentInspectionsData[this.currentInspectionId];
if (!inspection) return;
// Sauvegarder tous les champs modifiés
document.querySelectorAll('.point-input').forEach(input => {
const pointId = input.dataset.point;
const field = input.dataset.field;
if (!inspection.results[pointId]) {
inspection.results[pointId] = { status: 'pending', value: '', comment: '', photos: [] };
}
if (field === 'value' && input.type === 'checkbox') {
// Gérer les cases à cocher
const checkboxes = document.querySelectorAll(`input[data-point="${pointId}"][data-field="value"]`);
const selectedValues = [];
checkboxes.forEach(cb => {
if (cb.checked) {
selectedValues.push(cb.dataset.option);
}
});
inspection.results[pointId][field] = selectedValues.join(',');
} else {
inspection.results[pointId][field] = input.value;
}
});
DataManager.saveData(INSPECTIONS_KEY, currentInspectionsData);
// Mettre à jour la progression
const model = currentModelData[inspection.modelId];
if (model) {
this.updateProgress(inspection, model);
}
UI.showToast('Progression sauvegardée.', 'success');
},
finishInspection() {
if (!this.currentInspectionId) return;
const inspection = currentInspectionsData[this.currentInspectionId];
if (!inspection) return;
// Sauvegarder d'abord
this.saveProgress();
// Marquer comme terminée
inspection.status = 'completed';
inspection.endTime = new Date().toISOString();
DataManager.saveData(INSPECTIONS_KEY, currentInspectionsData);
UI.showToast('Inspection terminée avec succès !', 'success');
UI.switchView('inspections');
UI.displayInspections();
DashboardManager.updateDashboard();
},
exitInspection() {
UI.showModal(
'Quitter l\'inspection',
'<p>Voulez-vous sauvegarder vos modifications avant de quitter ?</p>',
[
{ text: 'Quitter sans sauvegarder', class: 'bg-gray-200 hover:bg-gray-300 text-gray-700', action: () => { UI.closeModal(); UI.switchView('inspections'); } },
{ text: 'Sauvegarder et quitter', class: 'bg-blue-600 hover:bg-blue-700 text-white', action: () => { this.saveProgress(); UI.closeModal(); UI.switchView('inspections'); } }
]
);
}
};
const formatDateShort = (dateString) => {
if (!dateString) return 'N/A';
try {
const date = new Date(dateString);
if (isNaN(date.getTime())) return 'Date invalide';
return date.toLocaleDateString('fr-FR');
} catch (e) {
return 'Date invalide';
}
};
// DataManager
window.DataManager = {
saveData(key, data) {
try {
localStorage.setItem(key, JSON.stringify(data));
console.log('Sauvé: ' + key);
} catch (e) {
console.error('Erreur save ' + key + ':', e);
let message = "Erreur sauvegarde.";
if (e.name === 'QuotaExceededError' || (e.code && (e.code === 22 || e.code === 1014))) {
message = "Erreur: Stockage local plein.";
UI.showModal("Stockage Plein", "Stockage local navigateur plein. Supprimez inspections/modèles.");
}
UI.showToast(message, 'error', 5000);
}
},
loadData(key, defaultValue) {
if (defaultValue === undefined) defaultValue = {};
try {
const data = localStorage.getItem(key);
if (data === null) {
console.log('Pas de data ' + key);
return defaultValue;
}
console.log('Chargé: ' + key);
return JSON.parse(data);
} catch (e) {
console.error('Erreur load ' + key + ':', e);
UI.showToast('Erreur chargement (' + key + ').', 'error');
return defaultValue;
}
},
migrateData() {
const oldInspections = this.loadData('checklistApp_Inspections_v1', {});
const oldModels = this.loadData('checklistApp_Models_v1', {});
if (Object.keys(oldInspections).length > 0 || Object.keys(oldModels).length > 0) {
console.log("Migration des données v1 vers v2...");
Object.keys(oldModels).forEach(modelId => {
if (!currentModelData[modelId]) {
currentModelData[modelId] = oldModels[modelId];
}
});
Object.keys(oldInspections).forEach(inspectionId => {
if (!currentInspectionsData[inspectionId]) {
const inspection = oldInspections[inspectionId];
inspection.site = inspection.site || 'Site non spécifié';
currentInspectionsData[inspectionId] = inspection;
}
});
this.saveData(MODELS_KEY, currentModelData);
this.saveData(INSPECTIONS_KEY, currentInspectionsData);
UI.showToast("Données migrées vers la version 2.1", 'info', 4000);
}
},
confirmClearAllData() {
UI.showModal(
"Confirmer suppression totale",
"<p class='text-red-600 font-semibold'>ATTENTION !</p><p class='mt-2'>Suppression de <strong>tous les modèles et inspections</strong>.</p><p class='mt-2 font-bold'>Action IRREVERSIBLE.</p><p class='mt-3'>Continuer ?</p>",
[
{ text: "Annuler", class: "bg-gray-200 hover:bg-gray-300 text-gray-700", action: UI.closeModal },
{ text: "Oui, Tout Supprimer", class: "bg-red-600 hover:bg-red-700 text-white font-bold", action: this.clearAllData.bind(this) }
]
);
},
clearAllData() {
try {
localStorage.removeItem(MODELS_KEY);
localStorage.removeItem(INSPECTIONS_KEY);
localStorage.removeItem(INSPECTOR_NAME_KEY);
localStorage.removeItem('checklistApp_Models_v1');
localStorage.removeItem('checklistApp_Inspections_v1');
localStorage.removeItem('checklistApp_InspectorName_v1');
currentModelData = {};
currentInspectionsData = {};
lastUsedInspectorName = '';
document.getElementById('inspector-name').value = '';
document.getElementById('inspection-site').value = '';
UI.displayModels();
UI.displayInspections();
DashboardManager.updateDashboard();
UI.switchView('welcome');
UI.closeModal();
UI.showToast("Données effacées.", 'success', 4000);
} catch (e) {
console.error("Erreur suppression:", e);
UI.showToast("Erreur suppression.", 'error');
}
},
saveInspectorName(name) {
lastUsedInspectorName = name;
this.saveData(INSPECTOR_NAME_KEY, name);
},
loadInspectorName() {
lastUsedInspectorName = this.loadData(INSPECTOR_NAME_KEY, '');
document.getElementById('inspector-name').value = lastUsedInspectorName;
}
};
// UI Object
window.UI = {
sections: ['welcome-section', 'models-section', 'inspections-section', 'inspection-interface-section', 'dashboard-section'],
navButtons: ['nav-welcome', 'nav-models', 'nav-inspections', 'nav-dashboard'],
sidebarLinks: ['sidebar-welcome', 'sidebar-models', 'sidebar-inspections', 'sidebar-dashboard'],
switchView(viewId) {
console.log('Switch view: ' + viewId);
this.sections.forEach(id => {
const element = document.getElementById(id);
if (element) element.classList.add('hidden');
});
const targetSection = document.getElementById(viewId + '-section');
if (targetSection) targetSection.classList.remove('hidden');
this.navButtons.forEach(id => {
const button = document.getElementById(id);
if (button) {
button.classList.remove('border-blue-500', 'text-blue-600', 'font-semibold');
button.classList.add('border-transparent', 'text-gray-500', 'hover:border-gray-300', 'hover:text-gray-700');
if (id === 'nav-' + viewId) {
button.classList.remove('border-transparent', 'text-gray-500', 'hover:border-gray-300', 'hover:text-gray-700');
button.classList.add('border-blue-500', 'text-blue-600', 'font-semibold');
}
}
});
this.sidebarLinks.forEach(id => {
const link = document.getElementById(id);
if (link) {
link.classList.remove('active');
if (id === 'sidebar-' + viewId) {
link.classList.add('active');
}
}
});
if (viewId === 'dashboard') {
// Attendre que le DOM soit bien mis à jour et Chart.js soit prêt
setTimeout(() => {
DashboardManager.initializeDashboard();
}, 300);
}
// Retourner à la liste des inspections si on quitte l'interface d'inspection
if (InspectionInterface.currentInspectionId && viewId !== 'inspection-interface') {
InspectionInterface.currentInspectionId = null;
}
window.scrollTo(0, 0);
window.closeSidebar();
},
showModal(title, content, buttons) {
if (!buttons) buttons = [];
document.getElementById('modal-title').textContent = title;
document.getElementById('modal-body').innerHTML = content;
const footer = document.getElementById('modal-footer');
footer.innerHTML = '';
if (buttons.length === 0) {
const closeButton = document.createElement('button');
closeButton.textContent = 'Fermer';
closeButton.className = 'px-4 py-2 bg-gray-200 text-gray-700 rounded-md hover:bg-gray-300 focus:outline-none focus:ring-2 focus:ring-gray-400';
closeButton.onclick = UI.closeModal;
footer.appendChild(closeButton);
} else {
buttons.forEach(btnInfo => {
const button = document.createElement('button');
button.textContent = btnInfo.text;
button.className = 'px-4 py-2 rounded-md focus:outline-none focus:ring-2 focus:ring-opacity-75 ' + (btnInfo.class || 'bg-blue-500 hover:bg-blue-600 text-white');
if (!btnInfo.class || btnInfo.class.indexOf('focus:ring-') === -1) {
button.classList.add('focus:ring-blue-400');
}
button.onclick = btnInfo.action;
footer.appendChild(button);
});
}
document.getElementById('modal').style.display = 'flex';
},
closeModal() {
document.getElementById('modal').style.display = 'none';
},
closeModalOnClickOutside(event) {
if (event.target === document.getElementById('modal')) {
UI.closeModal();
}
},
showHelpModal() {
const helpContent = '<h4 class="font-semibold text-lg mb-3">Guide d\'utilisation (v2.1.0)</h4>' +
'<div class="mb-4">' +
'<h5 class="font-medium mb-2">🏠 Page d\'accueil</h5>' +
'<p class="text-sm text-gray-600 mb-2">Présentation générale et guide de démarrage rapide.</p>' +
'</div>' +
'<div class="mb-4">' +
'<h5 class="font-medium mb-2">📋 Gestion des modèles</h5>' +
'<p class="text-sm text-gray-600 mb-2">Importez vos checklists depuis Excel (.xlsx).</p>' +
'<ul class="text-xs text-gray-500 list-disc list-inside">' +
'<li>Colonnes requises : ID_Point, Categorie, PointDeControle, TypeParametre</li>' +
'<li>Types supportés : Texte, Numérique, Date, Liste Déroulante, Case à cocher, Statut Seulement</li>' +
'</ul>' +
'</div>' +
'<div class="mb-4">' +
'<h5 class="font-medium mb-2">✅ Inspections</h5>' +
'<p class="text-sm text-gray-600 mb-2">Créez et gérez vos inspections avec la nouvelle interface de statuts colorés.</p>' +
'<div class="flex gap-2 mt-2">' +
'<span class="px-2 py-1 bg-green-100 text-green-800 rounded text-xs">C = Conforme</span>' +
'<span class="px-2 py-1 bg-red-100 text-red-800 rounded text-xs">NC = Non Conforme</span>' +
'<span class="px-2 py-1 bg-orange-100 text-orange-800 rounded text-xs">HP = Hors Plage</span>' +
'<span class="px-2 py-1 bg-gray-100 text-gray-800 rounded text-xs">NA = Non Applicable</span>' +
'</div>' +
'</div>' +
'<div class="mb-4">' +
'<h5 class="font-medium mb-2">📊 Tableau de bord</h5>' +
'<p class="text-sm text-gray-600 mb-2">Analysez vos performances avec des graphiques optimisés et indicateurs clés.</p>' +
'</div>' +
'<p class="text-sm font-medium text-orange-600 mt-4">⚠️ Données stockées localement - Exportez régulièrement !</p>' +
'<p class="text-xs text-gray-500 mt-4">Version 2.1.0 - Améliorations graphiques et interface</p>';
this.showModal("Guide d'utilisation", helpContent);
},
showToast(message, type, duration) {
if (!type) type = 'info';
if (!duration) duration = 3500;
const toastContainer = document.getElementById('toast-container');
if (!toastContainer) return;
const toast = document.createElement('div');
toast.className = 'toast toast-' + type;
let iconClass = 'fas fa-info-circle';
if (type === 'success') iconClass = 'fas fa-check-circle';
else if (type === 'error') iconClass = 'fas fa-exclamation-triangle';
else if (type === 'warning') iconClass = 'fas fa-exclamation-circle';
toast.innerHTML = '<i class="' + iconClass + ' mr-2"></i> ' + message;
toastContainer.appendChild(toast);
requestAnimationFrame(function() {
requestAnimationFrame(function() {
toast.classList.add('show');
});
});
setTimeout(function() {
toast.classList.remove('show');
setTimeout(function() {
if (toast.parentElement === toastContainer) {
toast.remove();
}
}, 600);
}, duration);
},
setLoading(isLoading, message) {
if (!message) message = "Chargement...";
const indicator = document.getElementById('loading-indicator');
const msgElement = document.getElementById('loading-message');
if (!indicator || !msgElement) return;
if (isLoading) {
msgElement.textContent = message;
indicator.style.display = 'flex';
} else {
setTimeout(function() {
indicator.style.display = 'none';
}, 150);
}
},
displayModels() {
const listContainer = document.getElementById('checklist-models-list');
const selectDropdown = document.getElementById('new-inspection-model');
const dashboardModelFilter = document.getElementById('dashboard-model-filter');
listContainer.innerHTML = '';
selectDropdown.innerHTML = '<option value="" disabled selected>-- Sélectionnez un modèle --</option>';
dashboardModelFilter.innerHTML = '<option value="">Tous les modèles</option>';
const modelIds = Object.keys(currentModelData).sort(function(a, b) {
const nameA = currentModelData[a].name || '';
const nameB = currentModelData[b].name || '';
if (nameA.toLowerCase() < nameB.toLowerCase()) return -1;
if (nameA.toLowerCase() > nameB.toLowerCase()) return 1;
return new Date(currentModelData[b].importDate) - new Date(currentModelData[a].importDate);
});
if (modelIds.length === 0) {
listContainer.innerHTML = '<p class="text-gray-500 italic text-center py-4">Aucun modèle trouvé.</p>';
selectDropdown.innerHTML = '<option value="" disabled selected>Aucun modèle disponible</option>';
selectDropdown.disabled = true;
} else {
modelIds.forEach(function(modelId) {
const model = currentModelData[modelId];
const modelName = model.name || 'Modèle importé ' + formatDate(model.importDate);
const pointCount = (model.points && model.points.length) || 0;
const listItem = document.createElement('div');
listItem.className = 'flex flex-col sm:flex-row justify-between items-start sm:items-center p-3 bg-gray-50 rounded border border-gray-200 hover:bg-gray-100 transition duration-150 gap-2';
listItem.innerHTML = '<div><span class="font-medium text-gray-800">' + modelName + '</span><span class="text-xs text-gray-500 ml-2">(' + pointCount + ' points)</span><p class="text-xs text-gray-400">ID: ' + modelId + '</p></div><button onclick="ModelManager.confirmDeleteModel(\'' + modelId + '\')" class="flex-shrink-0 px-3 py-1 bg-red-100 text-red-700 text-xs font-medium rounded hover:bg-red-200 hover:text-red-800 transition duration-150 self-end sm:self-center"><i class="fas fa-trash-alt mr-1"></i>Supprimer</button>';
listContainer.appendChild(listItem);
const option = document.createElement('option');
option.value = modelId;
option.textContent = modelName + ' (' + pointCount + ' points)';
selectDropdown.appendChild(option);
const dashboardOption = document.createElement('option');
dashboardOption.value = modelId;
dashboardOption.textContent = modelName;
dashboardModelFilter.appendChild(dashboardOption);
});
selectDropdown.disabled = false;
}
selectDropdown.dispatchEvent(new Event('change'));
this.updateStartButtonState();
},
displayInspections() {
const listContainer = document.getElementById('inspections-list');
listContainer.innerHTML = '';
const inspectionIds = Object.keys(currentInspectionsData).sort(function(a, b) {
return new Date(currentInspectionsData[b].startTime) - new Date(currentInspectionsData[a].startTime);
});
if (inspectionIds.length === 0) {
listContainer.innerHTML = '<p class="text-gray-500 italic text-center py-4">Aucune inspection trouvée.</p>';
return;
}
inspectionIds.forEach(function(id) {
const inspection = currentInspectionsData[id];
if (!inspection) return;
const model = currentModelData[inspection.modelId];
const modelName = model ? (model.name || 'ID: ' + inspection.modelId) : 'Modèle Supprimé/Inconnu';
const statusText = inspection.status === 'completed' ? 'Terminée' : 'En cours';
const statusColor = inspection.status === 'completed' ? 'text-green-600 bg-green-100' : 'text-yellow-600 bg-yellow-100';
const progress = InspectionManager.calculateProgress(inspection);
const isModelMissing = !model;
const site = inspection.site || 'Site non spécifié';
const listItem = document.createElement('div');
listItem.className = 'p-4 bg-white rounded-lg shadow border border-gray-200 flex flex-col sm:flex-row justify-between items-start sm:items-center gap-3' + (isModelMissing ? ' opacity-70 border-l-4 border-red-400' : '');
listItem.innerHTML = '<div class="flex-grow">' +
'<p class="font-semibold text-lg ' + (isModelMissing ? 'text-red-600' : 'text-blue-700') + '">' + modelName + '</p>' +
'<p class="text-sm text-gray-600">Inspecteur: <span class="font-medium">' + (inspection.inspectorName || 'N/A') + '</span></p>' +
'<p class="text-sm text-gray-600">Site: <span class="font-medium">' + site + '</span></p>' +
'<p class="text-sm text-gray-500">Démarrée: ' + formatDate(inspection.startTime) + '</p>' +
'<div class="flex items-center gap-2 mt-1">' +
'<span class="text-xs font-medium px-2 py-0.5 rounded-full ' + statusColor + '">' + statusText + '</span>' +
(inspection.status !== 'completed' ? '<span class="text-xs text-gray-500">(' + progress.percentage.toFixed(0) + '% complété)</span>' : '') +
'</div>' +
(inspection.status === 'completed' ? '<p class="text-sm text-gray-500 mt-1">Terminée: ' + formatDate(inspection.endTime) + '</p>' : '') +
(isModelMissing ? '<p class="text-xs text-red-500 mt-1 font-semibold"><i class="fas fa-exclamation-triangle mr-1"></i>Modèle manquant !</p>' : '') +
'</div>' +
'<div class="flex flex-wrap gap-2 mt-2 sm:mt-0 self-end sm:self-center flex-shrink-0">' +
(inspection.status !== 'completed' && !isModelMissing ?
'<button onclick="InspectionInterface.continueInspection(\'' + id + '\')" class="px-3 py-1.5 bg-blue-500 text-white text-xs rounded shadow hover:bg-blue-700 flex items-center" title="Continuer"><i class="fas fa-play mr-1"></i> Continuer</button>' : '') +
'<button onclick="InspectionManager.confirmDeleteInspection(\'' + id + '\')" class="px-3 py-1.5 bg-red-500 text-white text-xs rounded shadow hover:bg-red-700 flex items-center" title="Supprimer"><i class="fas fa-trash-alt mr-1"></i> Suppr.</button>' +
'</div>';
listContainer.appendChild(listItem);
});
},
updateStartButtonState() {
const modelSelect = document.getElementById('new-inspection-model');
const inspectorInput = document.getElementById('inspector-name');
const siteInput = document.getElementById('inspection-site');
const startButton = document.getElementById('start-inspection-button');
if (modelSelect && inspectorInput && siteInput && startButton) {
startButton.disabled = !modelSelect.value || !inspectorInput.value.trim() || !siteInput.value.trim();
}
}
};
// ModelManager
window.ModelManager = {
handleImport(event) {
const file = event.target.files[0];
if (!file) return;
const importButton = document.getElementById('import-excel-button');
importButton.disabled = true;
UI.setLoading(true, "Import modèle...");
const reader = new FileReader();
reader.onload = function(e) {
try {
const data = new Uint8Array(e.target.result);
const workbook = XLSX.read(data, { type: 'array' });
const sheetName = workbook.SheetNames[0];
const worksheet = workbook.Sheets[sheetName];
const json = XLSX.utils.sheet_to_json(worksheet, { raw: false, defval: "" });
if (json.length === 0) {
throw new Error("Fichier Excel vide.");
}
const requiredColumns = ['ID_Point', 'Categorie', 'PointDeControle', 'TypeParametre'];
const firstRow = json[0];
const missingColumns = requiredColumns.filter(function(col) {
return !(col in firstRow);
});
if (missingColumns.length > 0) {
throw new Error('Colonnes manquantes: ' + missingColumns.join(', '));
}
const uniqueIds = new Set();
const processedPoints = json.map(function(row, rowIndex) {
const idPoint = String(row.ID_Point || '').trim();
if (!idPoint) {
throw new Error('ID_Point manquant ligne ' + (rowIndex + 2) + '.');
}
if (uniqueIds.has(idPoint)) {
throw new Error('ID_Point "' + idPoint + '" dupliqué (ligne ' + (rowIndex + 2) + ').');
}
uniqueIds.add(idPoint);
const typeParametre = String(row.TypeParametre || '').trim();
const validTypes = ['Texte', 'Numérique', 'Date', 'Liste Déroulante', 'Case à cocher', 'Statut Seulement'];
if (!typeParametre || validTypes.indexOf(typeParametre) === -1) {
throw new Error('TypeParametre invalide ("' + typeParametre + '") ligne ' + (rowIndex + 2) + '. Valides: ' + validTypes.join(', '));
}
return {
ID_Point: idPoint,
Categorie: String(row.Categorie || 'Général').trim(),
PointDeControle: String(row.PointDeControle || '').trim() || 'Point ' + idPoint,
Description: String(row.Description || '').trim(),
CritereAcceptation: String(row.CritereAcceptation || '').trim(),
TypeParametre: typeParametre,
OptionsParametre: String(row.OptionsParametre || '').trim(),
ModeleCommentaire: String(row.ModeleCommentaire || '').trim()
};
});
const modelId = generateId();
const newModel = {
id: modelId,
name: file.name.replace(/\.xlsx$/i, ''),
importDate: new Date().toISOString(),
points: processedPoints
};
currentModelData[modelId] = newModel;
DataManager.saveData(MODELS_KEY, currentModelData);
UI.displayModels();
UI.showToast('Modèle "' + newModel.name + '" importé (' + newModel.points.length + ' points).', 'success');
} catch (error) {
console.error("Erreur import Excel:", error);
UI.showModal("Erreur Importation Excel", 'Erreur: ' + error.message);
UI.showToast("Échec import modèle.", 'error');
} finally {
UI.setLoading(false);
document.getElementById('import-excel-input').value = '';
document.getElementById('import-excel-button').disabled = true;
}
};
reader.onerror = function() {
UI.setLoading(false);
UI.showToast("Erreur lecture fichier Excel.", 'error');
document.getElementById('import-excel-input').value = '';
document.getElementById('import-excel-button').disabled = true;
};
reader.readAsArrayBuffer(file);
},
confirmDeleteModel(modelId) {
const model = currentModelData[modelId];
if (!model) return;
const modelName = model.name || 'ID: ' + modelId;
const usedByInspections = Object.values(currentInspectionsData).filter(function(insp) {
return insp.modelId === modelId;
});
let message = '<p>Supprimer modèle "<strong>' + modelName + '</strong>" ?</p>';
if (usedByInspections.length > 0) {
message += '<p class=\'mt-3 text-orange-600 font-semibold\'><i class="fas fa-exclamation-triangle mr-1"></i>Utilisé par ' + usedByInspections.length + ' inspection(s).</p>';
}
message += '<p class=\'mt-3 font-bold\'>Action irréversible.</p>';
UI.showModal(
"Confirmer Suppression Modèle",
message,
[
{ text: "Annuler", class: "bg-gray-200 hover:bg-gray-300 text-gray-700", action: UI.closeModal },
{ text: "Oui, Supprimer", class: "bg-red-600 hover:bg-red-700 text-white", action: function() { ModelManager.deleteModel(modelId); } }
]
);
},
deleteModel(modelId) {
const model = currentModelData[modelId];
if (!model) return;
const modelName = model.name || 'ID: ' + modelId;
delete currentModelData[modelId];
DataManager.saveData(MODELS_KEY, currentModelData);
UI.displayModels();
UI.displayInspections();
DashboardManager.updateDashboard();
UI.closeModal();
UI.showToast('Modèle "' + modelName + '" supprimé.', 'success');
console.log('Modèle ' + modelId + ' supprimé.');
}
};
// InspectionManager
window.InspectionManager = {
startInspection() {
const modelId = document.getElementById('new-inspection-model').value;
const inspectorName = document.getElementById('inspector-name').value.trim();
const site = document.getElementById('inspection-site').value.trim();
if (!modelId) {
UI.showToast("Sélectionnez modèle.", 'warning');
return;
}
if (!inspectorName) {
UI.showToast("Entrez nom inspecteur.", 'warning');
document.getElementById('inspector-name').focus();
return;
}
if (!site) {
UI.showToast("Entrez site/département.", 'warning');
document.getElementById('inspection-site').focus();
return;
}
const model = currentModelData[modelId];
if (!model || !model.points) {
UI.showToast("Erreur: Modèle invalide.", 'error');
return;
}
const inspectionId = generateId();
const initialResults = {};
model.points.forEach(function(point) {
initialResults[point.ID_Point] = { status: 'pending', value: '', comment: '', photos: [] };
});
const newInspection = {
id: inspectionId,
modelId: modelId,
inspectorName: inspectorName,
site: site,
startTime: new Date().toISOString(),
endTime: null,
status: 'in-progress',
results: initialResults
};
currentInspectionsData[inspectionId] = newInspection;
DataManager.saveData(INSPECTIONS_KEY, currentInspectionsData);
DataManager.saveInspectorName(inspectorName);
UI.showToast('Inspection démarrée: ' + (model.name || modelId), 'success');
// Clear form
document.getElementById('new-inspection-model').value = '';
document.getElementById('inspection-site').value = '';
UI.updateStartButtonState();
// Rediriger vers l'interface d'inspection
InspectionInterface.continueInspection(inspectionId);
},
confirmDeleteInspection(inspectionId) {
const inspection = currentInspectionsData[inspectionId];
if (!inspection) return;
const model = currentModelData[inspection.modelId];
const modelName = model ? model.name : 'ID Modèle: ' + inspection.modelId;
const inspector = inspection.inspectorName || 'N/A';
const site = inspection.site || 'Site non spécifié';
const date = formatDate(inspection.startTime);
const message = '<p>Supprimer inspection pour "<strong>' + modelName + '</strong>" ?</p>' +
'<p class="text-sm text-gray-600 mt-1">Inspecteur: ' + inspector + '</p>' +
'<p class="text-sm text-gray-600">Site: ' + site + '</p>' +
'<p class="text-sm text-gray-600">Date: ' + date + '</p>' +
'<p class="mt-3 font-bold text-red-600">Action irréversible.</p>';
UI.showModal(
"Confirmer Suppression Inspection",
message,
[
{ text: "Annuler", class: "bg-gray-200 hover:bg-gray-300 text-gray-700", action: UI.closeModal },
{ text: "Oui, Supprimer", class: "bg-red-600 hover:bg-red-700 text-white", action: function() { InspectionManager.deleteInspection(inspectionId); } }
]
);
},
deleteInspection(inspectionId) {
if (!currentInspectionsData[inspectionId]) return;
const inspection = currentInspectionsData[inspectionId];
const modelName = (currentModelData[inspection.modelId] && currentModelData[inspection.modelId].name) || 'ID: ' + inspection.modelId;
delete currentInspectionsData[inspectionId];
DataManager.saveData(INSPECTIONS_KEY, currentInspectionsData);
UI.displayInspections();
DashboardManager.updateDashboard();
UI.closeModal();
UI.showToast('Inspection "' + modelName + '" supprimée.', 'success');
},
calculateProgress(inspection) {
if (!inspection || !inspection.modelId || !currentModelData[inspection.modelId]) {
return { total: 0, completed: 0, percentage: 0 };
}
const model = currentModelData[inspection.modelId];
const totalPoints = (model.points && model.points.length) || 0;
let completedPoints = 0;
if (totalPoints > 0 && inspection.results) {
model.points.forEach(function(point) {
const result = inspection.results[point.ID_Point];
if (result && result.status && result.status !== 'pending') {
completedPoints++;
}
});
}
const percentage = totalPoints > 0 ? (completedPoints / totalPoints) * 100 : 0;
return { total: totalPoints, completed: completedPoints, percentage: percentage };
}
};
// DashboardManager avec corrections pour les graphiques
window.DashboardManager = {
initializeDashboard() {
this.populateFilters();
this.updateDashboard();
},
populateFilters() {
const modelFilter = document.getElementById('dashboard-model-filter');
const siteFilter = document.getElementById('dashboard-site-filter');
modelFilter.innerHTML = '<option value="">Tous les modèles</option>';
Object.values(currentModelData).forEach(function(model) {
const option = document.createElement('option');
option.value = model.id;
option.textContent = model.name || 'Modèle ' + model.id;
modelFilter.appendChild(option);
});
siteFilter.innerHTML = '<option value="">Tous les sites</option>';
const sites = [];
Object.values(currentInspectionsData).forEach(function(insp) {
const site = insp.site || 'Site non spécifié';
if (sites.indexOf(site) === -1) {
sites.push(site);
}
});
sites.sort().forEach(function(site) {
const option = document.createElement('option');
option.value = site;
option.textContent = site;
siteFilter.appendChild(option);
});
const today = new Date();
const thirtyDaysAgo = new Date(today);
thirtyDaysAgo.setDate(today.getDate() - 30);
document.getElementById('dashboard-date-from').value = thirtyDaysAgo.toISOString().split('T')[0];
document.getElementById('dashboard-date-to').value = today.toISOString().split('T')[0];
},
getFilteredInspections() {
const modelFilter = document.getElementById('dashboard-model-filter').value;
const siteFilter = document.getElementById('dashboard-site-filter').value;
const dateFrom = document.getElementById('dashboard-date-from').value;
const dateTo = document.getElementById('dashboard-date-to').value;
return Object.values(currentInspectionsData).filter(function(inspection) {
if (modelFilter && inspection.modelId !== modelFilter) return false;
const inspectionSite = inspection.site || 'Site non spécifié';
if (siteFilter && inspectionSite !== siteFilter) return false;
if (dateFrom || dateTo) {
const inspectionDate = new Date(inspection.startTime);
if (dateFrom && inspectionDate < new Date(dateFrom)) return false;
if (dateTo && inspectionDate > new Date(dateTo + 'T23:59:59')) return false;
}
return true;
});
},
updateDashboard() {
const filteredInspections = this.getFilteredInspections();
this.updateKPIs(filteredInspections);
this.updateCharts(filteredInspections);
this.updateInspectionsTable(filteredInspections);
},
updateKPIs(inspections) {
const completedInspections = inspections.filter(function(insp) {
return insp.status === 'completed';
});
if (completedInspections.length === 0) {
document.getElementById('kpi-conformity').textContent = '0%';
document.getElementById('kpi-total-inspections').textContent = '0';
document.getElementById('kpi-out-of-range').textContent = '0';
document.getElementById('kpi-non-conform').textContent = '0';
['kpi-conformity-change', 'kpi-inspections-change', 'kpi-out-of-range-change', 'kpi-non-conform-change'].forEach(function(id) {
document.getElementById(id).textContent = 'Aucune donnée';
});
return;
}
let totalPoints = 0, conformePoints = 0, nonConformePoints = 0, horsPlagePoints = 0;
completedInspections.forEach(function(inspection) {
const model = currentModelData[inspection.modelId];
if (!model) return;
model.points.forEach(function(point) {
const result = inspection.results[point.ID_Point];
if (result && result.status && result.status !== 'pending') {
totalPoints++;
switch (result.status) {
case 'Conforme': conformePoints++; break;
case 'Non Conforme': nonConformePoints++; break;
case 'Hors Plage': horsPlagePoints++; break;
}
}
});
});
const conformityRate = totalPoints > 0 ? ((conformePoints / totalPoints) * 100) : 0;
document.getElementById('kpi-conformity').textContent = conformityRate.toFixed(1) + '%';
document.getElementById('kpi-total-inspections').textContent = completedInspections.length.toString();
document.getElementById('kpi-out-of-range').textContent = horsPlagePoints.toString();
document.getElementById('kpi-non-conform').textContent = nonConformePoints.toString();
document.getElementById('kpi-conformity-change').textContent = 'Sur ' + totalPoints + ' points analysés';
document.getElementById('kpi-inspections-change').textContent = (inspections.length - completedInspections.length) + ' en cours';
document.getElementById('kpi-out-of-range-change').textContent = totalPoints > 0 ? ((horsPlagePoints / totalPoints) * 100).toFixed(1) + '% du total' : '';
document.getElementById('kpi-non-conform-change').textContent = totalPoints > 0 ? ((nonConformePoints / totalPoints) * 100).toFixed(1) + '% du total' : '';
},
updateCharts(inspections) {
const completedInspections = inspections.filter(function(insp) {
return insp.status === 'completed';
});
// Utiliser bind pour conserver le contexte et s'assurer que les graphiques sont créés après que les conteneurs soient visibles
setTimeout(() => {
this.createConformityTrendChart.call(this, completedInspections);
this.createStatusDistributionChart.call(this, completedInspections);
this.createTopNonConformitiesChart.call(this, completedInspections);
}, 100);
},
// CORRECTION: Fonction améliorée pour les graphiques avec gestion des dimensions
createConformityTrendChart(inspections) {
const ctx = document.getElementById('conformity-trend-chart');
if (!ctx) {
console.warn('Canvas conformity-trend-chart non trouvé');
return;
}
// Vérifier que Chart.js est chargé
if (typeof Chart === 'undefined') {
console.error('Chart.js non chargé');
ctx.parentNode.innerHTML = '<p class="text-gray-500 text-center py-8">Chart.js non disponible</p>';
return;
}
const dateGroups = {};
inspections.forEach(function(inspection) {
const date = new Date(inspection.startTime).toISOString().split('T')[0];
if (!dateGroups[date]) {
dateGroups[date] = { total: 0, conforme: 0 };
}
const model = currentModelData[inspection.modelId];
if (!model) return;
model.points.forEach(function(point) {
const result = inspection.results[point.ID_Point];
if (result && result.status && result.status !== 'pending') {
dateGroups[date].total++;
if (result.status === 'Conforme') {
dateGroups[date].conforme++;
}
}
});
});
const sortedDates = Object.keys(dateGroups).sort();
let labels, data;
if (sortedDates.length === 0) {
labels = ['Aucune donnée'];
data = [0];
} else {
labels = sortedDates.map(function(date) {
return new Date(date).toLocaleDateString('fr-FR');
});
data = sortedDates.map(function(date) {
const group = dateGroups[date];
return group.total > 0 ? (group.conforme / group.total) * 100 : 0;
});
}
// Détruire le graphique existant
if (dashboardCharts.conformityTrend) {
try {
dashboardCharts.conformityTrend.destroy();
dashboardCharts.conformityTrend = null;
} catch (e) {
console.warn('Erreur destruction graphique conformité:', e);
}
}
try {
// CORRECTION: Configuration améliorée pour éviter les débordements
dashboardCharts.conformityTrend = new Chart(ctx, {
type: 'line',
data: {
labels: labels,
datasets: [{
label: 'Taux de Conformité (%)',
data: data,
borderColor: '#10b981',
backgroundColor: 'rgba(16, 185, 129, 0.1)',
borderWidth: 2,
fill: true,
tension: 0.1
}]
},
options: {
responsive: true,
maintainAspectRatio: false, // CORRECTION: Important pour contrôler la taille
animation: {
duration: 300 // Réduction de l'animation
},
interaction: {
intersect: false,
mode: 'index'
},
scales: {
y: {
beginAtZero: true,
max: 100,
ticks: {
callback: function(value) {
return value + '%';
}
}
}
},
plugins: {
legend: {
display: false
},
tooltip: {
callbacks: {
label: function(context) {
return context.parsed.y.toFixed(1) + '% de conformité';
}
}
}
}
}
});
console.log('Graphique conformité créé avec succès');
} catch (error) {
console.error('Erreur création graphique conformité:', error);
ctx.parentNode.innerHTML = '<p class="text-red-500 text-center py-8">Erreur chargement graphique conformité</p>';
}
},
createStatusDistributionChart(inspections) {
const ctx = document.getElementById('status-distribution-chart');
if (!ctx) {
console.warn('Canvas status-distribution-chart non trouvé');
return;
}
if (typeof Chart === 'undefined') {
console.error('Chart.js non chargé');
ctx.parentNode.innerHTML = '<p class="text-gray-500 text-center py-8">Chart.js non disponible</p>';
return;
}
let conforme = 0, nonConforme = 0, horsPlage = 0, na = 0;
inspections.forEach(function(inspection) {
const model = currentModelData[inspection.modelId];
if (!model) return;
model.points.forEach(function(point) {
const result = inspection.results[point.ID_Point];
if (result && result.status) {
switch (result.status) {
case 'Conforme': conforme++; break;
case 'Non Conforme': nonConforme++; break;
case 'Hors Plage': horsPlage++; break;
case 'N/A': na++; break;
}
}
});
});
const total = conforme + nonConforme + horsPlage + na;
if (total === 0) {
// Afficher un message si pas de données
ctx.parentNode.innerHTML = '<div style="position: relative; height: 300px; display: flex; align-items: center; justify-content: center;"><p class="text-gray-500 text-center">Aucune donnée à afficher</p></div>';
return;
}
if (dashboardCharts.statusDistribution) {
try {
dashboardCharts.statusDistribution.destroy();
dashboardCharts.statusDistribution = null;
} catch (e) {
console.warn('Erreur destruction graphique distribution:', e);
}
}
try {
dashboardCharts.statusDistribution = new Chart(ctx, {
type: 'doughnut',
data: {
labels: ['Conforme', 'Non Conforme', 'Hors Plage', 'N/A'],
datasets: [{
data: [conforme, nonConforme, horsPlage, na],
backgroundColor: ['#10b981', '#ef4444', '#f97316', '#6b7280'],
borderWidth: 2,
borderColor: '#ffffff'
}]
},
options: {
responsive: true,
maintainAspectRatio: false, // CORRECTION
animation: {
duration: 300
},
plugins: {
legend: {
position: 'bottom',
labels: {
padding: 15,
usePointStyle: true
}
},
tooltip: {
callbacks: {
label: function(context) {
const label = context.label || '';
const value = context.parsed;
const percentage = ((value / total) * 100).toFixed(1);
return label + ': ' + value + ' (' + percentage + '%)';
}
}
}
}
}
});
console.log('Graphique distribution créé avec succès');
} catch (error) {
console.error('Erreur création graphique distribution:', error);
ctx.parentNode.innerHTML = '<div style="position: relative; height: 300px; display: flex; align-items: center; justify-content: center;"><p class="text-red-500 text-center">Erreur chargement graphique distribution</p></div>';
}
},
createTopNonConformitiesChart(inspections) {
const ctx = document.getElementById('top-non-conformities-chart');
if (!ctx) {
console.warn('Canvas top-non-conformities-chart non trouvé');
return;
}
if (typeof Chart === 'undefined') {
console.error('Chart.js non chargé');
ctx.parentNode.innerHTML = '<p class="text-gray-500 text-center py-8">Chart.js non disponible</p>';
return;
}
const categoryNonConformities = {};
inspections.forEach(function(inspection) {
const model = currentModelData[inspection.modelId];
if (!model) return;
model.points.forEach(function(point) {
const result = inspection.results[point.ID_Point];
if (result && (result.status === 'Non Conforme' || result.status === 'Hors Plage')) {
const category = point.Categorie || 'Non classé';
categoryNonConformities[category] = (categoryNonConformities[category] || 0) + 1;
}
});
});
const sortedCategories = Object.entries(categoryNonConformities)
.sort(function(a, b) { return b[1] - a[1]; })
.slice(0, 5);
let labels, data;
if (sortedCategories.length === 0) {
ctx.parentNode.innerHTML = '<div style="position: relative; height: 300px; display: flex; align-items: center; justify-content: center;"><p class="text-gray-500 text-center">Aucune non-conformité détectée</p></div>';
return;
} else {
labels = sortedCategories.map(function(item) { return item[0]; });
data = sortedCategories.map(function(item) { return item[1]; });
}
if (dashboardCharts.topNonConformities) {
try {
dashboardCharts.topNonConformities.destroy();
dashboardCharts.topNonConformities = null;
} catch (e) {
console.warn('Erreur destruction graphique non-conformités:', e);
}
}
try {
dashboardCharts.topNonConformities = new Chart(ctx, {
type: 'bar',
data: {
labels: labels,
datasets: [{
label: 'Non-Conformités',
data: data,
backgroundColor: '#ef4444',
borderColor: '#dc2626',
borderWidth: 1
}]
},
options: {
responsive: true,
maintainAspectRatio: false, // CORRECTION
animation: {
duration: 300
},
scales: {
y: {
beginAtZero: true,
ticks: {
stepSize: 1,
callback: function(value) {
return Number.isInteger(value) ? value : '';
}
}
}
},
plugins: {
legend: {
display: false
},
tooltip: {
callbacks: {
label: function(context) {
return context.parsed.y + ' non-conformité(s)';
}
}
}
}
}
});
console.log('Graphique non-conformités créé avec succès');
} catch (error) {
console.error('Erreur création graphique non-conformités:', error);
ctx.parentNode.innerHTML = '<div style="position: relative; height: 300px; display: flex; align-items: center; justify-content: center;"><p class="text-red-500 text-center">Erreur chargement graphique non-conformités</p></div>';
}
},
updateInspectionsTable(inspections) {
const tbody = document.getElementById('dashboard-inspections-table');
tbody.innerHTML = '';
if (inspections.length === 0) {
tbody.innerHTML = '<tr><td colspan="7" class="px-6 py-4 text-center text-gray-500">Aucune inspection trouvée</td></tr>';
return;
}
inspections.sort(function(a, b) {
return new Date(b.startTime) - new Date(a.startTime);
});
inspections.forEach(function(inspection) {
const model = currentModelData[inspection.modelId];
const modelName = model ? (model.name || 'Modèle ' + inspection.modelId) : 'Modèle supprimé';
const site = inspection.site || 'Site non spécifié';
let conforme = 0, nonConforme = 0, horsPlage = 0, totalPoints = 0;
if (model && inspection.status === 'completed') {
model.points.forEach(function(point) {
const result = inspection.results[point.ID_Point];
if (result && result.status && result.status !== 'pending') {
totalPoints++;
switch (result.status) {
case 'Conforme': conforme++; break;
case 'Non Conforme': nonConforme++; break;
case 'Hors Plage': horsPlage++; break;
}
}
});
}
const conformityRate = totalPoints > 0 ? ((conforme / totalPoints) * 100).toFixed(1) : 'N/A';
const row = document.createElement('tr');
row.className = 'hover:bg-gray-50';
row.innerHTML = '<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">' + formatDateShort(inspection.startTime) + '</td>' +
'<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">' + modelName + '</td>' +
'<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">' + site + '</td>' +
'<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">' + (inspection.inspectorName || 'N/A') + '</td>' +
'<td class="px-6 py-4 whitespace-nowrap text-sm"><span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ' +
(inspection.status === 'completed' ?
(parseFloat(conformityRate) >= 90 ? 'bg-green-100 text-green-800' :
parseFloat(conformityRate) >= 70 ? 'bg-yellow-100 text-yellow-800' : 'bg-red-100 text-red-800')
: 'bg-gray-100 text-gray-800') + '">' +
(inspection.status === 'completed' ? conformityRate + '%' : 'En cours') + '</span></td>' +
'<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">' + nonConforme + '</td>' +
'<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">' + horsPlage + '</td>';
tbody.appendChild(row);
});
}
};
// Event Listeners Setup
function setupEventListeners() {
// Import Excel file input
const importExcelInput = document.getElementById('import-excel-input');
const importExcelButton = document.getElementById('import-excel-button');
if (importExcelInput && importExcelButton) {
importExcelInput.addEventListener('change', function(event) {
importExcelButton.disabled = !event.target.files.length;
});
importExcelButton.addEventListener('click', function() {
if (importExcelInput.files.length > 0) {
ModelManager.handleImport({ target: { files: importExcelInput.files } });
}
});
}
// Template download button
const downloadTemplateButton = document.getElementById('download-template-button');
if (downloadTemplateButton) {
downloadTemplateButton.addEventListener('click', function() {
TemplateManager.downloadTemplate();
});
}
// New inspection form
const modelSelect = document.getElementById('new-inspection-model');
const inspectorInput = document.getElementById('inspector-name');
const siteInput = document.getElementById('inspection-site');
const startButton = document.getElementById('start-inspection-button');
if (modelSelect) {
modelSelect.addEventListener('change', UI.updateStartButtonState);
}
if (inspectorInput) {
inspectorInput.addEventListener('input', UI.updateStartButtonState);
}
if (siteInput) {
siteInput.addEventListener('input', UI.updateStartButtonState);
}
if (startButton) {
startButton.addEventListener('click', InspectionManager.startInspection);
}
// Inspection interface controls
const saveProgressButton = document.getElementById('save-inspection-progress');
const finishInspectionButton = document.getElementById('finish-inspection');
const exitInspectionButton = document.getElementById('exit-inspection');
if (saveProgressButton) {
saveProgressButton.addEventListener('click', () => InspectionInterface.saveProgress());
}
if (finishInspectionButton) {
finishInspectionButton.addEventListener('click', () => InspectionInterface.finishInspection());
}
if (exitInspectionButton) {
exitInspectionButton.addEventListener('click', () => InspectionInterface.exitInspection());
}
// Dashboard filters
const applyFiltersButton = document.getElementById('apply-dashboard-filters');
if (applyFiltersButton) {
applyFiltersButton.addEventListener('click', () => DashboardManager.updateDashboard());
}
// Export PDF button
const exportPdfButton = document.getElementById('export-dashboard-pdf');
if (exportPdfButton) {
exportPdfButton.addEventListener('click', function() {
UI.showToast('Export PDF sera implémenté dans une future version.', 'info');
});
}
// Auto-save lors de la saisie dans l'interface d'inspection
document.addEventListener('input', function(event) {
if (event.target.classList.contains('point-input')) {
// Auto-save après un court délai pour éviter trop de sauvegardes
clearTimeout(window.autoSaveTimeout);
window.autoSaveTimeout = setTimeout(() => {
if (InspectionInterface.currentInspectionId) {
InspectionInterface.saveProgress();
}
}, 2000); // 2 secondes de délai
}
});
document.addEventListener('change', function(event) {
if (event.target.classList.contains('point-input') || event.target.classList.contains('point-checkbox')) {
// Sauvegarde immédiate pour les changements de sélection
if (InspectionInterface.currentInspectionId) {
setTimeout(() => {
InspectionInterface.saveProgress();
}, 500);
}
}
});
// Keyboard shortcuts
document.addEventListener('keydown', function(event) {
// Ctrl+S pour sauvegarder dans l'interface d'inspection
if (event.ctrlKey && event.key === 's' && InspectionInterface.currentInspectionId) {
event.preventDefault();
InspectionInterface.saveProgress();
}
// Escape pour quitter l'interface d'inspection
if (event.key === 'Escape' && InspectionInterface.currentInspectionId) {
InspectionInterface.exitInspection();
}
});
}
// Sidebar functions
window.openSidebar = function() {
const sidebar = document.getElementById('sidebar');
const mainContent = document.getElementById('main-content');
const menuToggle = document.getElementById('menu-toggle');
if (sidebar) sidebar.style.width = '280px';
if (mainContent) mainContent.style.marginLeft = '280px';
if (menuToggle) menuToggle.style.left = '290px';
};
window.closeSidebar = function() {
const sidebar = document.getElementById('sidebar');
const mainContent = document.getElementById('main-content');
const menuToggle = document.getElementById('menu-toggle');
if (sidebar) sidebar.style.width = '0';
if (mainContent) mainContent.style.marginLeft = '0';
if (menuToggle) menuToggle.style.left = '1rem';
};
// Initialize application
function initializeApplication() {
console.log('Initialisation de l\'application d\'inspection qualité v2.1.0');
try {
// Vérifier les dépendances
if (typeof XLSX === 'undefined') {
console.warn('XLSX (SheetJS) non chargé - Import Excel indisponible');
}
if (typeof Chart === 'undefined') {
console.warn('Chart.js non chargé - Graphiques indisponibles');
}
// Load data from localStorage
currentModelData = DataManager.loadData(MODELS_KEY, {});
currentInspectionsData = DataManager.loadData(INSPECTIONS_KEY, {});
// Migrate data if needed
DataManager.migrateData();
// Load inspector name
DataManager.loadInspectorName();
// Setup event listeners
setupEventListeners();
// Initialize UI
UI.displayModels();
UI.displayInspections();
// Set initial view to welcome
UI.switchView('welcome');
console.log('Application initialisée avec succès');
console.log('Modèles chargés:', Object.keys(currentModelData).length);
console.log('Inspections chargées:', Object.keys(currentInspectionsData).length);
} catch (error) {
console.error('Erreur lors de l\'initialisation:', error);
UI.showToast('Erreur d\'initialisation de l\'application.', 'error');
}
}
// Start application when DOM is loaded
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initializeApplication);
} else {
initializeApplication();
}
// Global error handler
window.addEventListener('error', function(event) {
console.error('Erreur globale:', event.error);
UI.showToast('Erreur inattendue. Consultez la console.', 'error');
});
// Handle unhandled promise rejections
window.addEventListener('unhandledrejection', function(event) {
console.error('Promise rejection non gérée:', event.reason);
UI.showToast('Erreur de traitement. Consultez la console.', 'error');
});
})();
</script>
</body>
</html>