IFSACTIONPLAN / glodeactioniplanv2.html
MMOON's picture
Upload glodeactioniplanv2.html
d2ff234 verified
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Assistant VisiPilot pour Plan d'Actions IFS</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<style>
/* General Styles */
body {
font-family: 'Inter', 'Roboto', sans-serif;
margin: 0;
padding: 0;
background-color: #f8fafd;
color: #333;
line-height: 1.6;
}
.container {
max-width: 1200px;
margin: 25px auto;
padding: 30px;
background-color: #ffffff;
border-radius: 12px;
box-shadow: 0 5px 25px rgba(0, 0, 0, 0.1);
}
/* Banner */
.banner {
background-image: url('https://raw.githubusercontent.com/M00N69/BUSCAR/main/logo%2002%20copie.jpg');
background-size: cover;
height: 120px;
background-position: center;
margin-bottom: 0;
border-radius: 10px 10px 0 0;
position: relative;
background-blend-mode: overlay;
background-color: rgba(26, 115, 232, 0.05);
}
.banner-overlay {
display: none;
}
/* Header sous la bannière */
.header-section {
background: linear-gradient(135deg, #1a73e8 0%, #004080 100%);
color: white;
text-align: center;
padding: 25px 20px;
margin-bottom: 30px;
border-radius: 0 0 10px 10px;
}
.main-header {
font-size: 32px;
font-weight: 700;
color: white;
margin-bottom: 8px;
letter-spacing: -0.5px;
}
.main-subtitle {
color: rgba(255, 255, 255, 0.9);
font-size: 18px;
margin: 0;
font-weight: 400;
}
/* Expander */
.expander-header {
background-color: #e8f0fe;
border: 1px solid #c5dafc;
border-radius: 8px;
padding: 18px 25px;
cursor: pointer;
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
font-weight: 500;
color: #1a73e8;
transition: background-color 0.3s ease, box-shadow 0.2s ease;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05);
}
.expander-header:hover {
background-color: #d2e3fc;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
}
.expander-header::after {
content: '▼';
margin-left: 15px;
transition: transform 0.3s ease;
font-size: 0.9em;
}
.expander-header.expanded::after {
content: '▲';
transform: rotate(180deg);
}
.expander-content {
background-color: #f0f6ff;
border: 1px solid #d2e3fc;
border-top: none;
border-radius: 0 0 8px 8px;
padding: 25px;
margin-top: -15px;
display: none;
overflow: hidden;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.03);
}
.expander-content ol {
padding-left: 25px;
margin-top: 15px;
}
.expander-content li {
margin-bottom: 10px;
}
.expander-content strong {
color: #004080;
}
/* Form Elements */
.form-group {
margin-bottom: 25px;
padding: 15px;
background-color: #f7f9fc;
border-radius: 8px;
border: 1px solid #e0e0e0;
}
.form-group label {
display: block;
margin-bottom: 10px;
font-weight: 500;
color: #5f6368;
font-size: 1.1em;
}
.form-input {
width: calc(100% - 24px);
padding: 12px;
border: 1px solid #dadce0;
border-radius: 8px;
font-size: 16px;
box-sizing: border-box;
transition: border-color 0.3s ease, box-shadow 0.3s ease;
}
.form-input:focus {
border-color: #1a73e8;
box-shadow: 0 0 0 2px rgba(26, 115, 232, 0.2);
outline: none;
}
/* File Drop Zone */
.file-drop-zone {
border: 2px dashed #dadce0;
border-radius: 12px;
padding: 32px;
text-align: center;
transition: all 0.3s ease;
cursor: pointer;
background: linear-gradient(135deg, #fafbfc 0%, #f8fafd 100%);
}
.file-drop-zone:hover {
border-color: #1a73e8;
background: linear-gradient(135deg, #e8f0fe 0%, #f0f6ff 100%);
transform: translateY(-2px);
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
}
.file-drop-icon {
font-size: 48px;
color: #1a73e8;
opacity: 0.7;
margin-bottom: 16px;
}
/* Buttons */
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 12px 24px;
border: none;
border-radius: 8px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.3s ease;
text-transform: uppercase;
letter-spacing: 0.5px;
min-height: 44px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
}
.btn:hover {
transform: translateY(-2px);
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.15);
}
.btn:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none !important;
}
.btn-primary {
background: linear-gradient(135deg, #004080 0%, #1a73e8 100%);
color: white;
}
.btn-success {
background: linear-gradient(135deg, #34a853 0%, #2e8b4e 100%);
color: white;
}
.btn-danger {
background: linear-gradient(135deg, #ea4335 0%, #d93025 100%);
color: white;
}
.btn-small {
padding: 8px 16px;
font-size: 12px;
min-height: 36px;
}
/* Messages */
.message {
padding: 16px 20px;
border-radius: 8px;
margin-bottom: 20px;
display: none;
align-items: center;
gap: 12px;
font-weight: 500;
}
.message.show {
display: flex;
}
.message-success {
background: #e6f4ea;
color: #1e8e3e;
border: 1px solid #c8e6c9;
}
.message-error {
background: #fce8e6;
color: #c5221f;
border: 1px solid #f9bdbb;
}
/* Data Table */
.data-table-container {
background: #ffffff;
border-radius: 12px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
overflow: hidden;
margin-top: 35px;
display: none;
}
.data-table-header {
background: linear-gradient(135deg, #004080 0%, #1a73e8 100%);
color: white;
padding: 20px 24px;
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 12px;
}
.data-table-title {
font-size: 20px;
font-weight: 600;
}
.data-table {
width: 100%;
border-collapse: collapse;
}
.data-table th,
.data-table td {
border: 1px solid #e8eaed;
padding: 15px;
text-align: left;
vertical-align: top;
font-size: 15px;
}
.data-table th {
background-color: #f0f4f7;
color: #3c4043;
font-weight: 500;
}
.data-table tbody tr:nth-child(even) {
background-color: #fcfdff;
}
.data-table tbody tr:hover {
background-color: #f5f8fc;
}
.requirement-link {
color: #1a73e8;
text-decoration: none;
font-weight: 500;
transition: color 0.2s ease;
}
.requirement-link:hover {
color: #004080;
text-decoration: underline;
}
/* Bulk Generation */
.bulk-generation-container {
margin-top: 16px;
padding: 16px;
background: linear-gradient(135deg, #e8f0fe 0%, #f0f6ff 100%);
border: 1px solid #1a73e8;
border-radius: 8px;
text-align: center;
}
.generation-options {
display: flex;
gap: 12px;
align-items: center;
justify-content: center;
margin-top: 12px;
flex-wrap: wrap;
}
.option-select {
padding: 4px 8px;
border: 1px solid #dadce0;
border-radius: 4px;
font-size: 11px;
background: #ffffff;
}
.bulk-progress {
display: none;
margin-top: 16px;
padding: 16px;
background: #ffffff;
border-radius: 8px;
border: 1px solid #e8eaed;
}
.bulk-progress.show {
display: block;
}
.progress-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.progress-counter {
background: #1a73e8;
color: white;
padding: 4px 12px;
border-radius: 12px;
font-size: 12px;
font-weight: 500;
}
.progress-bar-container {
background: #e0e0e0;
border-radius: 8px;
height: 8px;
overflow: hidden;
margin-bottom: 12px;
}
.progress-bar {
background: linear-gradient(90deg, #004080 0%, #1a73e8 100%);
height: 100%;
width: 0%;
transition: width 0.3s ease;
}
.current-item {
font-size: 13px;
color: #5f6368;
display: flex;
align-items: center;
gap: 8px;
}
.loading-spinner {
display: inline-block;
width: 16px;
height: 16px;
border: 2px solid rgba(26, 115, 232, 0.3);
border-radius: 50%;
border-top-color: #1a73e8;
animation: spin 1s ease-in-out infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
/* Modal */
.modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.6);
z-index: 1000;
justify-content: center;
align-items: center;
padding: 20px;
backdrop-filter: blur(4px);
}
.modal-content {
background: #ffffff;
border-radius: 12px;
max-width: 900px;
width: 100%;
max-height: 90vh;
overflow: hidden;
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.2);
}
.modal-header {
background: linear-gradient(135deg, #004080 0%, #1a73e8 100%);
color: white;
padding: 24px;
display: flex;
justify-content: space-between;
align-items: center;
}
.modal-close {
background: none;
border: none;
color: white;
font-size: 24px;
cursor: pointer;
padding: 4px;
border-radius: 50%;
transition: background-color 0.2s ease;
}
.modal-close:hover {
background: rgba(255, 255, 255, 0.1);
}
.modal-body {
padding: 24px;
max-height: 60vh;
overflow-y: auto;
}
.modal-field {
margin-bottom: 20px;
}
.modal-field-label {
font-weight: 600;
color: #3c4043;
margin-bottom: 8px;
display: block;
}
.modal-textarea {
width: 100%;
min-height: 120px;
padding: 16px;
border: 2px solid #dadce0;
border-radius: 8px;
font-family: inherit;
font-size: 14px;
resize: vertical;
display: none;
transition: border-color 0.3s ease, box-shadow 0.3s ease;
}
.modal-textarea:focus {
border-color: #1a73e8;
box-shadow: 0 0 0 3px rgba(26, 115, 232, 0.1);
outline: none;
}
.textarea-toggle {
background: #ffffff;
border: 1px solid #dadce0;
border-radius: 6px;
padding: 8px 12px;
cursor: pointer;
color: #1a73e8;
font-size: 13px;
font-weight: 500;
display: inline-flex;
align-items: center;
gap: 6px;
transition: all 0.2s ease;
}
.textarea-toggle:hover {
background: #f8fafd;
border-color: #1a73e8;
}
.modal-actions {
padding: 24px;
border-top: 1px solid #e8eaed;
display: flex;
justify-content: flex-end;
gap: 12px;
}
.recommendation-content {
background: #ffffff;
border: 1px solid #e0e0e0;
padding: 15px;
border-radius: 8px;
min-height: 80px;
overflow-y: auto;
max-height: 250px;
margin-bottom: 10px;
color: #333;
box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.05);
}
/* Responsive */
@media (max-width: 768px) {
.container {
margin: 15px;
padding: 20px;
}
.banner {
height: 80px;
}
.main-header {
font-size: 24px;
}
.main-subtitle {
font-size: 16px;
}
.data-table th:nth-child(2),
.data-table td:nth-child(2) {
display: none;
}
.modal-content {
width: 95%;
}
.modal-body {
padding: 16px;
}
.modal-actions {
padding: 16px;
flex-direction: column;
}
}
</style>
</head>
<body>
<div class="banner"></div>
<div class="container">
<div class="header-section">
<h1 class="main-header">Assistant VisiPilot pour Plan d'Actions IFS</h1>
<p class="main-subtitle">Génération intelligente de plans d'actions IFS Food 8</p>
</div>
<div class="expander">
<div class="expander-header" id="howToUseHeader">
<span>Comment utiliser cette application</span>
</div>
<div class="expander-content" id="howToUseContent">
<p><strong>Étapes d'utilisation:</strong></p>
<ol>
<li><strong>Saisissez votre clé API Groq:</strong> Entrez votre clé API dans le champ dédié.</li>
<li><strong>Téléchargez votre plan d'actions IFS v8:</strong> Glissez-déposez votre fichier Excel.</li>
<li><strong>Générez des recommandations:</strong> Utilisez les boutons individuels ou la génération en lot.</li>
<li><strong>Affinez les propositions:</strong> Cliquez sur le numéro d'exigence pour éditer.</li>
<li><strong>Exportez en PDF:</strong> Générez un rapport complet.</li>
</ol>
</div>
</div>
<div class="form-group">
<label for="groqApiKey">Votre clé API Groq :</label>
<input type="password" id="groqApiKey" class="form-input" placeholder="Entrez votre clé API Groq">
</div>
<div class="form-group">
<label>Téléchargez votre plan d'action (fichier Excel) :</label>
<div class="file-drop-zone" id="fileDropZone">
<input type="file" id="fileInput" accept=".xlsx" style="display: none;">
<div class="file-drop-content">
<span class="material-icons file-drop-icon">upload_file</span>
<div>Glissez-déposez votre fichier Excel ici</div>
<div>ou cliquez pour sélectionner (.xlsx)</div>
</div>
</div>
</div>
<div class="message message-success" id="successMessage">
<span class="material-icons">check_circle</span>
<span id="successText"></span>
</div>
<div class="message message-error" id="errorMessage">
<span class="material-icons">error</span>
<span id="errorText"></span>
</div>
<div class="data-table-container" id="dataTableContainer">
<div class="data-table-header">
<h2 class="data-table-title">Plan d'Action IFS</h2>
<div style="display: flex; gap: 12px; align-items: center;">
<button class="btn btn-success" id="bulkGenerateBtn">
<span class="material-icons">auto_awesome</span>
Générer toutes les recommandations
</button>
<button class="btn btn-danger" id="exportPdfBtn">
<span class="material-icons">picture_as_pdf</span>
Exporter PDF
</button>
</div>
</div>
<div class="bulk-generation-container">
<div style="margin-bottom: 12px;">
<strong>Génération en lot :</strong> Générez automatiquement toutes les recommandations manquantes.
</div>
<div class="generation-options">
<div style="display: flex; align-items: center; gap: 6px;">
<label for="delaySelect">Délai :</label>
<select class="option-select" id="delaySelect">
<option value="5">5 secondes</option>
<option value="10" selected>10 secondes</option>
<option value="15">15 secondes</option>
</select>
</div>
</div>
<div class="bulk-progress" id="bulkProgress">
<div class="progress-header">
<div>Génération en cours...</div>
<div class="progress-counter" id="progressCounter">0 / 0</div>
</div>
<div class="progress-bar-container">
<div class="progress-bar" id="progressBar"></div>
</div>
<div class="current-item" id="currentItem">En attente...</div>
</div>
</div>
<table class="data-table" id="dataTable">
<thead>
<tr>
<th>Numéro d'exigence</th>
<th>Exigence IFS Food 8</th>
<th>Constat détaillé</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="dataTableBody">
</tbody>
</table>
</div>
</div>
<div class="modal" id="detailModal">
<div class="modal-content">
<div class="modal-header">
<h2>Détail de la non-conformité</h2>
<button class="modal-close" id="modalClose">
<span class="material-icons">close</span>
</button>
</div>
<div class="modal-body">
<div class="modal-field">
<label class="modal-field-label">Numéro d'exigence:</label>
<div id="modalReqNum"></div>
</div>
<div class="modal-field">
<label class="modal-field-label">Exigence IFS Food 8:</label>
<div id="modalReqText"></div>
</div>
<div class="modal-field">
<label class="modal-field-label">Constat détaillé:</label>
<div id="modalExplanation"></div>
</div>
<div class="modal-field">
<label class="modal-field-label">Recommandation IA:</label>
<div class="recommendation-content" id="modalRenderedRecommendation"></div>
</div>
<div class="modal-field">
<label class="modal-field-label">Modifier la recommandation:</label>
<button class="textarea-toggle" id="textareaToggle">
<span class="material-icons">edit</span>
Modifier
</button>
<textarea class="modal-textarea" id="modalTextarea"></textarea>
</div>
</div>
<div class="modal-actions">
<button class="btn btn-primary" id="modalCancel">Fermer</button>
<button class="btn btn-success" id="modalGenerate">
<span class="material-icons">auto_awesome</span>
Générer/Améliorer
</button>
</div>
</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.17.0/xlsx.full.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/html2pdf.js/0.9.3/html2pdf.bundle.min.js"></script>
<script>
// Configuration
var CONFIG = {
GROQ_API_ENDPOINT: "https://api.groq.com/openai/v1/chat/completions",
GROQ_MODEL: "openai/gpt-oss-120b",
GUIDE_CSV_URL: "https://raw.githubusercontent.com/M00N69/Action-planGroq/main/Guide%20Checklist_IFS%20Food%20V%208%20-%20CHECKLIST.csv",
EXPECTED_HEADERS: ["requirementNo", "requirementText", "requirementExplanation"],
HEADER_ROW_INDEX: 11,
DATA_START_INDEX: 13
};
// État global
var AppState = {
apiKey: '',
actionPlanData: [],
guideData: [],
recommendations: {},
currentModalIndex: -1,
isLoading: false,
bulkGeneration: {
isRunning: false,
currentIndex: 0,
total: 0,
completed: 0,
failed: []
}
};
// Éléments DOM
var DOM = {
apiKeyInput: document.getElementById('groqApiKey'),
fileDropZone: document.getElementById('fileDropZone'),
fileInput: document.getElementById('fileInput'),
successMessage: document.getElementById('successMessage'),
errorMessage: document.getElementById('errorMessage'),
successText: document.getElementById('successText'),
errorText: document.getElementById('errorText'),
dataTableContainer: document.getElementById('dataTableContainer'),
dataTableBody: document.getElementById('dataTableBody'),
bulkGenerateBtn: document.getElementById('bulkGenerateBtn'),
bulkProgress: document.getElementById('bulkProgress'),
progressBar: document.getElementById('progressBar'),
progressCounter: document.getElementById('progressCounter'),
currentItem: document.getElementById('currentItem'),
delaySelect: document.getElementById('delaySelect'),
exportPdfBtn: document.getElementById('exportPdfBtn'),
detailModal: document.getElementById('detailModal'),
modalClose: document.getElementById('modalClose'),
modalCancel: document.getElementById('modalCancel'),
modalGenerate: document.getElementById('modalGenerate'),
modalReqNum: document.getElementById('modalReqNum'),
modalReqText: document.getElementById('modalReqText'),
modalExplanation: document.getElementById('modalExplanation'),
modalRenderedRecommendation: document.getElementById('modalRenderedRecommendation'),
modalTextarea: document.getElementById('modalTextarea'),
textareaToggle: document.getElementById('textareaToggle'),
howToUseHeader: document.getElementById('howToUseHeader'),
howToUseContent: document.getElementById('howToUseContent')
};
// Utilitaires
var Utils = {
showMessage: function(type, text) {
var messageEl = DOM[type + 'Message'];
var textEl = DOM[type + 'Text'];
textEl.textContent = text;
messageEl.classList.add('show');
setTimeout(function() {
messageEl.classList.remove('show');
}, 5000);
},
parseCSV: function(csvText) {
var lines = csvText.split('\n');
var headers = lines[0].split(';').map(function(h) {
return h.trim();
});
var data = [];
for (var i = 1; i < lines.length; i++) {
var currentLine = lines[i].split(';');
if (currentLine.length === headers.length) {
var row = {};
headers.forEach(function(header, index) {
row[header] = currentLine[index] ? currentLine[index].trim() : '';
});
data.push(row);
}
}
return data;
}
};
// Gestionnaire API
var APIManager = {
loadGuideData: function() {
fetch(CONFIG.GUIDE_CSV_URL)
.then(function(response) {
if (!response.ok) throw new Error('HTTP ' + response.status);
return response.text();
})
.then(function(csvText) {
AppState.guideData = Utils.parseCSV(csvText);
console.log('Guide IFS chargé:', AppState.guideData.length, 'entrées');
})
.catch(function(error) {
console.error('Erreur chargement guide:', error);
Utils.showMessage('error', 'Impossible de charger le guide IFS');
});
},
getGuideInfo: function(reqNumber) {
if (!AppState.guideData.length || !reqNumber) return null;
return AppState.guideData.find(function(row) {
return row['NUM_REQ'] && reqNumber && String(row['NUM_REQ']).includes(String(reqNumber));
});
},
generateRecommendation: function(index, isRegeneration) {
return new Promise(function(resolve, reject) {
if (!AppState.apiKey) {
Utils.showMessage('error', 'Veuillez configurer votre clé API Groq');
reject(new Error('Pas de clé API'));
return;
}
var nonConformity = AppState.actionPlanData[index];
if (!nonConformity) {
Utils.showMessage('error', 'Non-conformité introuvable');
reject(new Error('Non-conformité introuvable'));
return;
}
var guideInfo = APIManager.getGuideInfo(nonConformity["Numéro d'exigence"]);
var prompt = "En tant qu'expert en sécurité alimentaire et IFS Food 8, analysez cette non-conformité :\n\n" +
"Numéro d'exigence: " + nonConformity["Numéro d'exigence"] + "\n" +
"Description: " + nonConformity["Exigence IFS Food 8"] + "\n" +
"Constat détaillé: " + nonConformity["Explication (par l'auditeur/l'évaluateur)"] + "\n\n";
if (guideInfo && guideInfo['Good practice']) {
prompt += "Référence Guide IFS v8:\n" +
"Bonnes pratiques: " + (guideInfo['Good practice'] || 'Non disponible') + "\n" +
"Éléments à vérifier: " + (guideInfo['Elements to check'] || 'Non disponible') + "\n\n";
}
prompt += "Fournissez une recommandation structurée en Markdown incluant:\n\n" +
"## Correction Immédiate\n" +
"## Type de Preuve Requise\n" +
"## Cause Probable\n" +
"## Actions Correctives\n" +
"## Conclusion et Bonnes Pratiques";
var messages = [
{
role: "system",
content: "Vous êtes un expert en sécurité alimentaire et en norme IFS Food 8. Fournissez des recommandations précises et actionables."
},
{
role: "user",
content: prompt
}
];
fetch(CONFIG.GROQ_API_ENDPOINT, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + AppState.apiKey
},
body: JSON.stringify({
messages: messages,
model: CONFIG.GROQ_MODEL,
temperature: 0.7,
max_tokens: 2000
})
})
.then(function(response) {
if (!response.ok) {
throw new Error('API Error ' + response.status);
}
return response.json();
})
.then(function(data) {
var recommendation = data.choices[0].message.content;
AppState.recommendations[index] = recommendation;
resolve(recommendation);
})
.catch(function(error) {
console.error('Erreur génération recommandation:', error);
Utils.showMessage('error', 'Erreur: ' + error.message);
reject(error);
});
});
}
};
// Gestionnaire de fichiers
var FileManager = {
init: function() {
DOM.fileDropZone.addEventListener('click', function() {
DOM.fileInput.click();
});
DOM.fileDropZone.addEventListener('dragover', FileManager.handleDragOver);
DOM.fileDropZone.addEventListener('dragleave', FileManager.handleDragLeave);
DOM.fileDropZone.addEventListener('drop', FileManager.handleDrop);
DOM.fileInput.addEventListener('change', FileManager.handleFileSelect);
},
handleDragOver: function(e) {
e.preventDefault();
DOM.fileDropZone.style.borderColor = '#1a73e8';
DOM.fileDropZone.style.background = 'linear-gradient(135deg, #e8f0fe 0%, #f0f6ff 100%)';
},
handleDragLeave: function(e) {
e.preventDefault();
DOM.fileDropZone.style.borderColor = '#dadce0';
DOM.fileDropZone.style.background = 'linear-gradient(135deg, #fafbfc 0%, #f8fafd 100%)';
},
handleDrop: function(e) {
e.preventDefault();
DOM.fileDropZone.style.borderColor = '#dadce0';
DOM.fileDropZone.style.background = 'linear-gradient(135deg, #fafbfc 0%, #f8fafd 100%)';
var files = e.dataTransfer.files;
if (files.length > 0) {
FileManager.processFile(files[0]);
}
},
handleFileSelect: function(e) {
var file = e.target.files[0];
if (file && file.name.endsWith('.xlsx')) {
FileManager.processFile(file);
} else {
Utils.showMessage('error', 'Veuillez sélectionner un fichier Excel (.xlsx)');
}
},
processFile: function(file) {
var reader = new FileReader();
reader.onload = function(e) {
try {
FileManager.parseExcelFile(e.target.result);
} catch (error) {
console.error('Erreur lecture fichier:', error);
Utils.showMessage('error', 'Erreur lors de la lecture du fichier Excel');
}
};
reader.readAsArrayBuffer(file);
},
parseExcelFile: function(arrayBuffer) {
var workbook = XLSX.read(arrayBuffer, { type: 'array' });
var sheetName = workbook.SheetNames[0];
var worksheet = workbook.Sheets[sheetName];
var allRows = XLSX.utils.sheet_to_json(worksheet, { header: 1, raw: false });
if (allRows.length < CONFIG.DATA_START_INDEX + 1) {
Utils.showMessage('error', 'Le fichier Excel ne contient pas suffisamment de données');
return;
}
var headerRow = allRows[CONFIG.HEADER_ROW_INDEX] || [];
var columnMap = FileManager.mapColumns(headerRow);
if (!columnMap) return;
var dataRows = allRows.slice(CONFIG.DATA_START_INDEX);
var processedData = FileManager.processDataRows(dataRows, columnMap);
if (!processedData.length) {
Utils.showMessage('error', 'Aucune donnée valide trouvée');
return;
}
AppState.actionPlanData = processedData;
DataTableManager.render();
Utils.showMessage('success', 'Fichier chargé avec succès! ' + processedData.length + ' non-conformités trouvées.');
},
mapColumns: function(headerRow) {
var columnMap = { reqNumber: -1, reqText: -1, explanation: -1 };
headerRow.forEach(function(cell, index) {
var cellValue = String(cell || '').trim();
if (cellValue === CONFIG.EXPECTED_HEADERS[0]) {
columnMap.reqNumber = index;
} else if (cellValue === CONFIG.EXPECTED_HEADERS[1]) {
columnMap.reqText = index;
} else if (cellValue === CONFIG.EXPECTED_HEADERS[2]) {
columnMap.explanation = index;
}
});
var missingColumns = [];
if (columnMap.reqNumber === -1) missingColumns.push('requirementNo');
if (columnMap.reqText === -1) missingColumns.push('requirementText');
if (columnMap.explanation === -1) missingColumns.push('requirementExplanation');
if (missingColumns.length > 0) {
Utils.showMessage('error', 'Colonnes manquantes: ' + missingColumns.join(', '));
return null;
}
return columnMap;
},
processDataRows: function(dataRows, columnMap) {
return dataRows
.map(function(row) {
return {
"Numéro d'exigence": String(row[columnMap.reqNumber] || '').trim(),
"Exigence IFS Food 8": String(row[columnMap.reqText] || '').trim(),
"Explication (par l'auditeur/l'évaluateur)": String(row[columnMap.explanation] || '').trim()
};
})
.filter(function(item) {
return item["Numéro d'exigence"] && item["Exigence IFS Food 8"];
});
}
};
// Gestionnaire de la table
var DataTableManager = {
render: function() {
DOM.dataTableBody.innerHTML = '';
AppState.actionPlanData.forEach(function(item, index) {
DataTableManager.createDataRow(item, index);
});
DOM.dataTableContainer.style.display = 'block';
},
createDataRow: function(item, index) {
var row = document.createElement('tr');
row.innerHTML =
'<td><a href="#" class="requirement-link" data-index="' + index + '">' +
item["Numéro d'exigence"] + '</a></td>' +
'<td>' + item["Exigence IFS Food 8"] + '</td>' +
'<td>' + item["Explication (par l'auditeur/l'évaluateur)"] + '</td>' +
'<td><button class="btn btn-primary btn-small generate-btn" data-index="' + index + '">' +
'<span class="material-icons">auto_awesome</span> Générer</button></td>';
DOM.dataTableBody.appendChild(row);
row.querySelector('.requirement-link').addEventListener('click', function(e) {
e.preventDefault();
ModalManager.open(index);
});
row.querySelector('.generate-btn').addEventListener('click', function() {
DataTableManager.generateRecommendation(index);
});
},
generateRecommendation: function(index) {
APIManager.generateRecommendation(index)
.then(function(recommendation) {
if (recommendation) {
Utils.showMessage('success', 'Recommandation générée avec succès!');
}
})
.catch(function(error) {
console.error('Erreur génération:', error);
});
}
};
// Gestionnaire de génération en lot
var BulkGenerationManager = {
init: function() {
DOM.bulkGenerateBtn.addEventListener('click', function(event) {
event.preventDefault();
event.stopPropagation();
BulkGenerationManager.startBulkGeneration();
});
},
startBulkGeneration: function() {
if (!AppState.apiKey) {
Utils.showMessage('error', 'Veuillez configurer votre clé API Groq');
return;
}
if (AppState.actionPlanData.length === 0) {
Utils.showMessage('error', 'Aucun plan d\'action chargé');
return;
}
var itemsToProcess = AppState.actionPlanData
.map(function(item, index) {
return { item: item, index: index };
})
.filter(function(obj) {
return !AppState.recommendations[obj.index];
});
if (itemsToProcess.length === 0) {
Utils.showMessage('success', 'Toutes les recommandations ont déjà été générées !');
return;
}
var delay = parseInt(DOM.delaySelect.value) * 1000;
var estimatedTime = Math.round((itemsToProcess.length * (delay + 5000)) / 60000);
var confirmed = confirm(
'Vous allez générer ' + itemsToProcess.length + ' recommandations.\n' +
'Délai entre chaque génération: ' + (delay/1000) + 's\n' +
'Temps estimé: ' + estimatedTime + ' minutes\n\n' +
'Continuer ?'
);
if (!confirmed) return;
AppState.bulkGeneration = {
isRunning: true,
currentIndex: 0,
total: itemsToProcess.length,
completed: 0,
failed: [],
itemsToProcess: itemsToProcess
};
BulkGenerationManager.updateUI(true);
BulkGenerationManager.processBulk(itemsToProcess, delay);
},
processBulk: function(itemsToProcess, delay) {
var processNext = function(i) {
if (i >= itemsToProcess.length || !AppState.bulkGeneration.isRunning) {
BulkGenerationManager.completeBulkGeneration();
return;
}
var obj = itemsToProcess[i];
AppState.bulkGeneration.currentIndex = i;
BulkGenerationManager.updateProgress();
BulkGenerationManager.updateCurrentItem(obj.item["Numéro d'exigence"]);
APIManager.generateRecommendation(obj.index)
.then(function(recommendation) {
if (recommendation) {
AppState.bulkGeneration.completed++;
} else {
AppState.bulkGeneration.failed.push(obj.item["Numéro d'exigence"]);
}
})
.catch(function(error) {
console.error('Erreur pour ' + obj.item["Numéro d'exigence"] + ':', error);
AppState.bulkGeneration.failed.push(obj.item["Numéro d'exigence"]);
})
.finally(function() {
if (i < itemsToProcess.length - 1) {
setTimeout(function() {
processNext(i + 1);
}, delay);
} else {
processNext(i + 1);
}
});
};
processNext(0);
},
updateUI: function(isRunning) {
DOM.bulkGenerateBtn.disabled = isRunning;
DOM.bulkProgress.classList.toggle('show', isRunning);
if (isRunning) {
DOM.bulkGenerateBtn.innerHTML = '<span class="loading-spinner"></span> Génération en cours...';
} else {
DOM.bulkGenerateBtn.innerHTML = '<span class="material-icons">auto_awesome</span> Générer toutes les recommandations';
}
},
updateProgress: function() {
var current = AppState.bulkGeneration.currentIndex;
var total = AppState.bulkGeneration.total;
var percentage = total > 0 ? ((current + 1) / total) * 100 : 0;
DOM.progressBar.style.width = percentage + '%';
DOM.progressCounter.textContent = (current + 1) + ' / ' + total;
},
updateCurrentItem: function(reqNumber) {
DOM.currentItem.innerHTML =
'<span class="loading-spinner"></span> Génération pour l\'exigence ' + reqNumber + '...';
},
completeBulkGeneration: function() {
var completed = AppState.bulkGeneration.completed;
var failed = AppState.bulkGeneration.failed;
var total = AppState.bulkGeneration.total;
AppState.bulkGeneration.isRunning = false;
BulkGenerationManager.updateUI(false);
var message = 'Génération terminée : ' + completed + '/' + total + ' recommandations générées';
if (failed.length > 0) {
message += '\nÉchecs : ' + failed.join(', ');
Utils.showMessage('error', message);
} else {
message += ' avec succès !';
Utils.showMessage('success', message);
}
DOM.currentItem.innerHTML =
'<span class="material-icons" style="color: #34a853;">check_circle</span> Génération terminée !';
setTimeout(function() {
DOM.bulkProgress.classList.remove('show');
}, 3000);
}
};
// Gestionnaire de modal
var ModalManager = {
init: function() {
DOM.modalClose.addEventListener('click', ModalManager.close);
DOM.modalCancel.addEventListener('click', ModalManager.close);
DOM.modalGenerate.addEventListener('click', ModalManager.generateRecommendation);
DOM.textareaToggle.addEventListener('click', ModalManager.toggleTextarea);
DOM.detailModal.addEventListener('click', function(e) {
if (e.target === DOM.detailModal) ModalManager.close();
});
},
toggleTextarea: function() {
var isVisible = DOM.modalTextarea.style.display !== 'none';
if (isVisible) {
DOM.modalTextarea.style.display = 'none';
DOM.textareaToggle.innerHTML = '<span class="material-icons">edit</span> Modifier';
} else {
DOM.modalTextarea.style.display = 'block';
DOM.textareaToggle.innerHTML = '<span class="material-icons">close</span> Fermer';
DOM.modalTextarea.focus();
}
},
open: function(index) {
AppState.currentModalIndex = index;
var item = AppState.actionPlanData[index];
DOM.modalReqNum.textContent = item["Numéro d'exigence"];
DOM.modalReqText.textContent = item["Exigence IFS Food 8"];
DOM.modalExplanation.textContent = item["Explication (par l'auditeur/l'évaluateur)"];
var recommendation = AppState.recommendations[index];
if (recommendation) {
DOM.modalTextarea.value = recommendation;
DOM.modalRenderedRecommendation.innerHTML = marked.parse(recommendation);
} else {
DOM.modalTextarea.value = '';
DOM.modalRenderedRecommendation.innerHTML = '<p>Aucune recommandation générée.</p>';
}
DOM.modalTextarea.style.display = 'none';
DOM.textareaToggle.innerHTML = '<span class="material-icons">edit</span> Modifier';
DOM.detailModal.style.display = 'flex';
},
close: function() {
DOM.detailModal.style.display = 'none';
AppState.currentModalIndex = -1;
},
generateRecommendation: function() {
if (AppState.currentModalIndex === -1) return;
APIManager.generateRecommendation(AppState.currentModalIndex, DOM.modalTextarea.value.trim() !== '')
.then(function(recommendation) {
if (recommendation) {
DOM.modalTextarea.value = recommendation;
DOM.modalRenderedRecommendation.innerHTML = marked.parse(recommendation);
Utils.showMessage('success', 'Recommandation générée/améliorée avec succès!');
}
})
.catch(function(error) {
console.error('Erreur génération modal:', error);
});
}
};
// Gestionnaire d'export PDF
var PDFManager = {
init: function() {
DOM.exportPdfBtn.addEventListener('click', PDFManager.exportToPDF);
},
exportToPDF: function() {
var pdfContainer = document.createElement('div');
pdfContainer.style.fontFamily = 'Arial, sans-serif';
pdfContainer.style.fontSize = '11px';
pdfContainer.style.padding = '15px';
pdfContainer.style.background = 'white';
var title = document.createElement('h1');
title.style.textAlign = 'center';
title.style.color = '#004080';
title.style.marginBottom = '20px';
title.style.fontSize = '18px';
title.textContent = 'Rapport de Plan d\'Actions IFS avec Recommandations IA';
pdfContainer.appendChild(title);
AppState.actionPlanData.forEach(function(item, index) {
var recommendation = AppState.recommendations[index];
if (!recommendation) return;
var section = document.createElement('div');
section.style.marginBottom = '25px';
section.style.border = '1px solid #e0e0e0';
section.style.borderRadius = '8px';
section.style.padding = '15px';
section.style.background = '#fafafa';
var header = document.createElement('div');
header.style.background = 'linear-gradient(135deg, #004080, #1a73e8)';
header.style.color = 'white';
header.style.padding = '10px 15px';
header.style.margin = '-15px -15px 15px -15px';
header.style.borderRadius = '7px 7px 0 0';
header.innerHTML = '<h2 style="margin: 0; font-size: 14px;">Exigence ' +
item["Numéro d'exigence"] + ' - ' + item["Exigence IFS Food 8"] + '</h2>';
section.appendChild(header);
var constatDiv = document.createElement('div');
constatDiv.style.marginBottom = '15px';
constatDiv.style.padding = '10px';
constatDiv.style.background = 'white';
constatDiv.style.borderLeft = '4px solid #ea4335';
constatDiv.innerHTML = '<strong style="color: #ea4335;">Constat:</strong><br>' +
item["Explication (par l'auditeur/l'évaluateur)"];
section.appendChild(constatDiv);
var recommendationDiv = document.createElement('div');
recommendationDiv.style.background = 'white';
recommendationDiv.style.padding = '15px';
recommendationDiv.style.borderRadius = '6px';
recommendationDiv.style.border = '1px solid #d0d0d0';
recommendationDiv.innerHTML =
'<h3 style="color: #1a73e8; margin-top: 0; font-size: 12px;">Recommandation IA</h3>' +
'<div style="font-size: 10px;">' + marked.parse(recommendation) + '</div>';
section.appendChild(recommendationDiv);
pdfContainer.appendChild(section);
});
var options = {
margin: [10, 10, 10, 10],
filename: 'Plan_Action_IFS_' + new Date().toISOString().split('T')[0] + '.pdf',
image: { type: 'jpeg', quality: 0.95 },
html2canvas: { scale: 1.5, logging: false, useCORS: true },
jsPDF: { unit: 'mm', format: 'a4', orientation: 'portrait' }
};
Utils.showMessage('success', 'Génération du PDF en cours...');
html2pdf().from(pdfContainer).set(options).save()
.then(function() {
Utils.showMessage('success', 'PDF généré et téléchargé avec succès!');
})
.catch(function(error) {
console.error('Erreur export PDF:', error);
Utils.showMessage('error', 'Erreur lors de la génération du PDF');
});
}
};
// Gestionnaire UI
var UIManager = {
init: function() {
DOM.apiKeyInput.addEventListener('input', function(e) {
var key = e.target.value.trim();
if (key) {
AppState.apiKey = key;
Utils.showMessage('success', 'Clé API configurée avec succès');
}
});
DOM.howToUseHeader.addEventListener('click', function() {
var isExpanded = DOM.howToUseContent.style.display === 'block';
DOM.howToUseContent.style.display = isExpanded ? 'none' : 'block';
if (isExpanded) {
DOM.howToUseHeader.classList.remove('expanded');
} else {
DOM.howToUseHeader.classList.add('expanded');
}
});
}
};
// Initialisation
document.addEventListener('DOMContentLoaded', function() {
console.log('Initialisation Assistant VisiPilot IFS');
marked.setOptions({
gfm: true,
breaks: true,
sanitize: false
});
UIManager.init();
FileManager.init();
ModalManager.init();
PDFManager.init();
BulkGenerationManager.init();
APIManager.loadGuideData();
console.log('Application initialisée');
});
</script>
</body>
</html>