IFSNEOREVIEWV3 / file-handler.js
MMOON's picture
Upload 4 files
f150763 verified
class FileHandler {
constructor(state, uiManager, dataProcessor) {
this.state = state;
this.uiManager = uiManager; // Inject UIManager
this.dataProcessor = dataProcessor; // Inject DataProcessor
this.state.subscribe(this.onStateChange.bind(this));
}
setUIManager(uiManager) {
this.uiManager = uiManager;
}
setDataProcessor(dataProcessor) {
this.dataProcessor = dataProcessor;
}
onStateChange(newState) {
// Handle state changes here
console.log('FileHandler State changed:', newState);
}
loadFile() {
const fileInput = document.getElementById('fileInputInternal');
if (fileInput) fileInput.click();
}
handleFileUpload(event) {
const file = event.target.files[0];
if (!file) return;
const fileName = file.name.toLowerCase();
// Detection Logic
const isIFSFile = fileName.endsWith('.ifs');
const isIFSRFile = fileName.endsWith('.ifsr');
const isIFSPFile = fileName.endsWith('.ifsp');
const isExcelFile = fileName.endsWith('.xlsx') || fileName.endsWith('.xls');
if (!isIFSFile && !isIFSRFile && !isIFSPFile && !isExcelFile) {
this.uiManager.showError('❌ Type de fichier non supporté !\n\n✅ Formats acceptés :\n• .ifs (nouveau dossier depuis NEO)\n• .ifsr (travail en cours)\n• .ifsp (package)\n• .xlsx (Plan d\'Actions)');
return;
}
// Handle Excel Import specifically
if (isExcelFile) {
if (!this.state.get().auditData) {
this.uiManager.showError("⚠️ Veuillez d'abord charger un dossier d'audit (.ifs) avant d'importer le plan d'actions.");
event.target.value = null;
return;
}
// Delegate to importActionPlanExcel BUT importActionPlanExcel expects an event with target.files
// We can reuse the event, or call processExcel directly if we read it here.
// Better to call importActionPlanExcel directly passing the event
this.importActionPlanExcel(event);
return;
}
this.uiManager.showLoading(true);
this.uiManager.simulateProgress();
const reader = new FileReader();
reader.onload = (e) => {
try {
const content = e.target.result;
const data = JSON.parse(content);
if (isIFSFile) {
this.processNewIFSFile(data);
} else if (isIFSRFile) {
this.loadWorkInProgress(data);
} else if (isIFSPFile) {
this.loadCollaborativePackage(data);
}
} catch (error) {
console.error('Error during file processing:', error);
this.uiManager.showError(`Erreur lors du traitement du fichier : ${error.message}`);
this.uiManager.showLoading(false);
this.uiManager.resetToUploadState();
} finally {
event.target.value = null;
}
};
reader.onerror = (error) => {
console.error('File reading error:', error);
this.uiManager.showError('Erreur lors de la lecture du fichier');
this.uiManager.showLoading(false);
this.uiManager.resetToUploadState();
event.target.value = null;
};
reader.readAsText(file);
}
processNewIFSFile(data) {
console.log('🎯 PROCESSING NEW IFS FILE');
this.uiManager.setPartialView(false); // Ensure full view
this.state.setState({
auditData: data,
checklistData: [],
companyProfileData: {},
conversations: {},
requirementNumberMapping: {},
packageVersion: 1,
certificationDecisionData: {},
dossierReviewState: {},
currentSession: {
id: `IFS-${Date.now()}`,
name: 'Nouvel Audit',
created: new Date(),
lastModified: new Date(),
data: data
}
});
if (!data?.data?.modules?.food_8) {
this.uiManager.showError('Format de fichier IFS non valide.');
this.uiManager.resetToUploadState();
return;
}
const food8 = data.data.modules.food_8;
this.dataProcessor.processAuditDataLogic(food8);
}
loadWorkInProgress(workData) {
console.log('📂 LOADING WORK IN PROGRESS');
this.uiManager.setPartialView(false); // Ensure full view
try {
if (!workData.auditData || !workData.version) {
throw new Error('Format de fichier IFSR invalide');
}
const conversations = this.migrateConversationKeys(workData.conversations || {});
this.state.setState({
auditData: workData.auditData,
checklistData: workData.checklistData || [],
companyProfileData: workData.companyProfileData || {},
conversations: conversations,
requirementNumberMapping: workData.requirementNumberMapping || {},
packageVersion: workData.packageVersion || 1,
certificationDecisionData: workData.certificationDecisionData || {},
dossierReviewState: workData.dossierReviewState || {}
});
if (workData.currentMode && workData.currentMode !== this.state.get().currentMode) {
this.uiManager.selectMode(workData.currentMode, false);
}
const companyName = workData.companyName || 'Société inconnue';
const coid = workData.coid || 'COID-XXXX';
this.state.setState({
currentSession: {
id: workData.sessionId || `IFS-Loaded-${Date.now()}`,
name: `Audit ${companyName} (rechargé)`,
created: workData.savedDate ? new Date(workData.savedDate) : new Date(),
lastModified: new Date(),
data: workData.auditData
}
});
this.uiManager.updateCurrentAuditName(`IFS Reviewer - ${companyName}`);
this.uiManager.updateSessionInfo(coid);
if (this.state.get().auditData?.data?.modules?.food_8?.result?.overall) {
const overallResult = this.state.get().auditData.data.modules.food_8.result.overall;
this.uiManager.updateElementText('overallScore', overallResult.percent.toFixed(1) + '%');
}
this.uiManager.updateElementText('totalRequirements', this.state.get().checklistData.length);
this.uiManager.updateElementText('conformCount', this.state.get().checklistData.filter(item => item.score === 'A').length);
this.uiManager.updateElementText('nonConformCount', this.state.get().checklistData.filter(item => ['B', 'C', 'D'].includes(item.score)).length);
const totalComments = this.dataProcessor.getTotalCommentsCount();
this.uiManager.showSuccess(`📂 Travail rechargé : ${companyName} (${totalComments} commentaires sauvegardés)`);
this.finalizeDataLoad();
} catch (error) {
console.error('Error loading work in progress:', error);
this.uiManager.showError('Erreur lors du chargement : ' + error.message);
this.uiManager.resetToUploadState();
}
}
migrateConversationKeys(conversations) {
if (!conversations) return {};
const newConversations = { ...conversations };
let keysChanged = false;
for (const key in newConversations) {
// Migrer les anciennes clés 'req-' ou 'nc-' vers 'ckl-' (Canal Constat)
if (key.startsWith('req-') || key.startsWith('nc-')) {
const uuid = key.startsWith('req-') ? key.replace('req-', '') : key.replace('nc-', '');
const newKey = `ckl-${uuid}`;
if (!newConversations[newKey]) {
newConversations[newKey] = newConversations[key];
delete newConversations[key];
keysChanged = true;
}
}
}
if (keysChanged) {
console.log('✅ Migrated old conversation keys to the new dual-channel format (ckl-).');
}
return newConversations;
}
loadCollaborativePackage(packageData) {
console.log('📦 LOADING COLLABORATIVE PACKAGE');
try {
if (!packageData.version || !packageData.packageType) {
throw new Error('Format de package IFSP invalide');
}
this.uiManager.setPartialView(packageData.isPartial || false);
const expectedMode = packageData.packageType === 'REVIEWER_TO_AUDITOR' ? 'auditor' : 'reviewer';
if (this.state.get().currentMode !== expectedMode) {
if (confirm(`Ce package est destiné au mode ${expectedMode === 'auditor' ? 'Auditeur' : 'Reviewer'}. Voulez-vous changer de mode ?`)) {
this.uiManager.selectMode(expectedMode, false);
}
}
this.state.setState({
auditData: packageData.auditData,
checklistData: packageData.checklistData || [],
companyProfileData: packageData.companyProfileData || {},
requirementNumberMapping: packageData.requirementNumberMapping || {},
});
if (packageData.conversations) {
const migratedConversations = this.migrateConversationKeys(packageData.conversations);
this.mergeConversations(migratedConversations);
}
this.state.setState({
packageVersion: packageData.packageVersion || 1,
});
const companyName = packageData.companyName || 'Société inconnue';
const coid = packageData.coid || 'COID-XXXX';
this.state.setState({
currentSession: {
id: `IFSP-${coid}-v${this.state.get().packageVersion}`,
name: `Package ${companyName} v${this.state.get().packageVersion}`,
created: new Date(packageData.createdDate),
lastModified: new Date(),
data: packageData.auditData
}
});
this.uiManager.updateCurrentAuditName(`IFS Reviewer - ${companyName} (Package v${this.state.get().packageVersion})`);
this.uiManager.updateSessionInfo(coid);
this.uiManager.updateElementText('totalRequirements', this.state.get().checklistData.length);
this.uiManager.updateElementText('conformCount', this.state.get().checklistData.filter(item => item.score === 'A').length);
this.uiManager.updateElementText('nonConformCount', this.state.get().checklistData.filter(item => ['B', 'C', 'D'].includes(item.score)).length);
if (this.state.get().auditData?.data?.modules?.food_8?.result?.overall) {
const overallResult = this.state.get().auditData.data.modules.food_8.result.overall;
this.uiManager.updateElementText('overallScore', overallResult.percent.toFixed(1) + '%');
}
const newComments = this.countNewComments(packageData);
this.uiManager.showSuccess(`📦 Package collaboratif chargé : ${companyName} v${this.state.get().packageVersion} (${newComments} nouveaux commentaires)`);
this.finalizeDataLoad();
} catch (error) {
console.error('Error loading collaborative package:', error);
this.uiManager.showError('Erreur lors du chargement du package : ' + error.message);
this.uiManager.resetToUploadState();
}
}
mergeConversations(newConversations) {
const currentConversations = { ...this.state.get().conversations };
for (const fieldId in newConversations) {
if (!currentConversations[fieldId]) {
currentConversations[fieldId] = newConversations[fieldId];
} else {
const existingIds = new Set(currentConversations[fieldId].thread.map(msg => msg.id));
const newMessages = newConversations[fieldId].thread.filter(msg => !existingIds.has(msg.id));
currentConversations[fieldId].thread = [...currentConversations[fieldId].thread, ...newMessages];
currentConversations[fieldId].thread.sort((a, b) => new Date(a.date) - new Date(b.date));
currentConversations[fieldId].lastActivity = newConversations[fieldId].lastActivity;
}
}
this.state.setState({ conversations: currentConversations });
}
countNewComments(packageData) {
let newCount = 0;
const otherMode = this.state.get().currentMode === 'reviewer' ? 'auditor' : 'reviewer';
for (const fieldId in packageData.conversations) {
const conversation = packageData.conversations[fieldId];
conversation.thread.forEach(message => {
if (message.author === otherMode && message.status === 'pending') {
newCount++;
}
});
}
return newCount;
}
finalizeDataLoad() {
this.uiManager.showLoading(false);
this.uiManager.showResults(true);
setTimeout(() => {
this.dataProcessor.renderCompanyProfile();
this.dataProcessor.renderChecklistTable();
this.dataProcessor.renderNonConformitiesTable();
this.dataProcessor.refreshAllCounters();
}, 100);
}
createNewSession() {
if (this.state.get().auditData && !confirm("⚠️ Créer un nouveau dossier ? Le travail non sauvegardé sera perdu.")) {
return;
}
this.state.setState({
auditData: null,
checklistData: [],
companyProfileData: {},
conversations: {},
requirementNumberMapping: {},
packageVersion: 1,
certificationDecisionData: {},
dossierReviewState: {},
currentSession: {
id: null,
name: 'Nouveau Dossier',
created: new Date(),
lastModified: new Date(),
data: null
}
});
this.uiManager.resetToUploadState();
this.uiManager.resetUI();
this.uiManager.switchTab('profil');
this.uiManager.showSuccess('✅ Nouveau dossier prêt. Chargez un fichier .ifs, .ifsr ou .ifsp.');
}
saveWorkInProgress() {
if (!this.state.get().auditData) {
this.uiManager.showError('❌ Aucune donnée à sauvegarder. Chargez un fichier .ifs d\'abord.');
return;
}
try {
const companyName = this.state.get().companyProfileData['Nom du site à auditer'] || 'Société inconnue';
const coid = this.state.get().companyProfileData['N° COID du portail'] || 'COID-XXXX';
const workPackage = {
version: '2.0',
packageType: 'WORK_IN_PROGRESS',
savedDate: new Date().toISOString(),
companyName,
coid,
currentMode: this.state.get().currentMode,
auditData: this.state.get().auditData,
checklistData: this.state.get().checklistData,
companyProfileData: this.state.get().companyProfileData,
conversations: this.state.get().conversations,
requirementNumberMapping: this.state.get().requirementNumberMapping,
packageVersion: this.state.get().packageVersion,
certificationDecisionData: this.state.get().certificationDecisionData,
dossierReviewState: this.state.get().dossierReviewState,
stats: {
totalComments: this.dataProcessor.getTotalCommentsCount(),
totalRequirements: this.state.get().checklistData.length,
progressPercentage: this.dataProcessor.calculateProgressPercentage()
}
};
this.downloadFile(workPackage, `TRAVAIL_${coid}_${this.sanitizeFileName(companyName)}_${this.getDateStamp()}.ifsr`);
this.state.setState({ hasUnsavedChanges: false });
this.uiManager.showSuccess(`✅ Travail sauvegardé`);
} catch (error) {
console.error('Error saving work:', error);
this.uiManager.showError('❌ Erreur sauvegarde : ' + error.message);
}
}
createPackage() {
try {
const {
companyProfileData,
conversations,
checklistData,
auditData,
requirementNumberMapping,
currentMode
} = this.state.get();
const companyName = companyProfileData['Nom du site à auditer'] || 'Société inconnue';
const coid = companyProfileData['N° COID du portail'] || 'COID-XXXX';
const packageType = currentMode === 'reviewer' ? 'REVIEWER_TO_AUDITOR' : 'AUDITOR_TO_REVIEWER';
const newPackageVersion = this.state.get().packageVersion + 1;
this.state.setState({ packageVersion: newPackageVersion });
const packageData = {
version: '2.1', // Version updated
packageType: packageType,
packageVersion: newPackageVersion,
isPartial: false,
createdBy: currentMode,
createdDate: new Date().toISOString(),
companyName,
coid,
auditData: auditData,
checklistData: checklistData,
companyProfileData: companyProfileData,
requirementNumberMapping: requirementNumberMapping,
conversations: conversations,
metadata: {
totalFields: Object.keys(conversations).length,
totalComments: this.dataProcessor.getTotalCommentsCount(),
lastExchangeDate: new Date().toISOString()
}
};
const filename = `PACKAGE_${packageType}_${coid}_${this.sanitizeFileName(companyName)}_v${newPackageVersion}_${this.getDateStamp()}.ifsp`;
this.downloadFile(packageData, filename);
this.state.setState({ hasUnsavedChanges: false });
this.uiManager.showSuccess(`📦 Package partiel créé : ${filename}`);
this.uiManager.closePackageModal();
} catch (error) {
console.error('Error creating package:', error);
this.uiManager.showError('❌ Erreur création package : ' + error.message);
}
}
addNeoUpdateToPackage() {
const description = document.getElementById('neoUpdateDescription').value.trim();
const fileInput = document.getElementById('newNeoFile');
const file = fileInput?.files?.[0];
if (!description && !file) {
this.uiManager.showError('Veuillez décrire les modifications ou joindre un fichier.');
return;
}
const createComment = (fileData = null) => {
const neoUpdateComment = {
id: generateUUID(),
author: 'auditor',
content: `🔄 MISE À JOUR NEO: ${description}`,
date: new Date().toISOString(),
status: 'pending',
isNeoUpdate: true,
version: this.state.get().packageVersion,
file: fileData // { name: '...', content: '...' }
};
const profileFieldId = 'profile-nom-du-site-à-auditer';
this.dataProcessor.addCommentToConversation(profileFieldId, neoUpdateComment);
this.uiManager.showSuccess('🔄 Mise à jour NEO ajoutée au package');
this.uiManager.closeNeoUpdateModal();
this.dataProcessor.refreshAllCounters();
};
if (file) {
const reader = new FileReader();
reader.onload = (e) => {
createComment({
name: file.name,
content: e.target.result
});
};
reader.readAsText(file);
} else {
createComment();
}
}
exportExcel() {
if (!this.state.get().auditData) {
this.uiManager.showError('Aucune donnée à exporter.');
return;
}
try {
const wb = XLSX.utils.book_new();
this.addSummarySheet(wb);
this.addProfileSheet(wb);
this.addNCSheet(wb);
this.addChecklistSheet(wb);
this.addCommentsSheet(wb);
const companyName = this.state.get().companyProfileData['Nom du site à auditer'] || 'audit';
const coid = this.state.get().companyProfileData['N° COID du portail'] || 'COID';
const filename = `RAPPORT_IFS_${coid}_${this.sanitizeFileName(companyName)}_${this.getDateStamp()}.xlsx`;
XLSX.writeFile(wb, filename);
this.state.setState({ hasUnsavedChanges: false });
this.uiManager.showSuccess(`📊 Rapport Excel généré : ${filename}`);
} catch (error) {
console.error('Error exporting Excel:', error);
this.uiManager.showError('❌ Erreur export Excel : ' + error.message);
}
}
addSummarySheet(wb) {
const certData = this.state.get().certificationDecisionData || {};
const summary = [
['Rapport Importé depuis IFS Reviewer', ''],
['', ''],
['DÉCISION DE CERTIFICATION', ''],
['Date de décision', certData.date || 'Non renseignée'],
['Responsable', certData.maker || 'Non renseigné'],
['Résultat', certData.result || 'Non renseigné'],
['Synthèse / Commentaire', certData.comments || 'Aucun'],
['', ''],
['RÉSUMÉ AUDIT', ''],
['Information Clé', 'Valeur'],
['Entreprise', this.state.get().companyProfileData['Nom du site à auditer'] || 'N/A'],
['COID', this.state.get().companyProfileData['N° COID du portail'] || 'N/A'],
['Score global IFS', document.getElementById('overallScore')?.textContent || '0%'],
['Total exigences', this.state.get().checklistData.length],
['Conformités (A)', this.state.get().checklistData.filter(i => i.score === 'A').length],
['Score B', this.state.get().checklistData.filter(i => i.score === 'B').length],
['Score C', this.state.get().checklistData.filter(i => i.score === 'C').length],
['Score D', this.state.get().checklistData.filter(i => i.score === 'D').length],
['Total commentaires', this.dataProcessor.getTotalCommentsCount()],
['Mode de travail', this.state.get().currentMode],
['Version package', this.state.get().packageVersion]
];
const ws = XLSX.utils.aoa_to_sheet(summary);
ws['!cols'] = [{ width: 30 }, { width: 30 }];
XLSX.utils.book_append_sheet(wb, ws, "RÉSUMÉ");
}
addProfileSheet(wb) {
const profileData = [['Champ', 'Valeur', 'Commentaires']];
Object.entries(this.state.get().companyProfileData).forEach(([field, value]) => {
const fieldId = `profile-${this.dataProcessor.sanitizeFieldId(field)}`;
const comments = this.dataProcessor.getCommentsText(fieldId);
profileData.push([field, value || 'N/A', comments]);
});
const ws = XLSX.utils.aoa_to_sheet(profileData);
ws['!cols'] = [{ width: 40 }, { width: 40 }, { width: 60 }];
XLSX.utils.book_append_sheet(wb, ws, "PROFIL ENTREPRISE");
}
addNCSheet(wb) {
const ncData = [['N° Exigence', 'Score', 'Explication', 'Détail', 'Commentaires']];
this.state.get().checklistData
.filter(item => ['B', 'C', 'D', 'NA'].includes(item.score))
.forEach(item => {
const fieldId = `nc-${item.uuid}`;
const comments = this.dataProcessor.getCommentsText(fieldId);
ncData.push([
item.requirementNumber,
item.score,
item.explanation || '-',
item.detailedExplanation || '-',
comments
]);
});
const ws = XLSX.utils.aoa_to_sheet(ncData);
ws['!cols'] = [{ width: 15 }, { width: 15 }, { width: 45 }, { width: 45 }, { width: 60 }];
XLSX.utils.book_append_sheet(wb, ws, "NON-CONFORMITÉS");
}
addChecklistSheet(wb) {
const checklistSheetData = [['N° Exigence', 'Chapitre', 'Score', 'Explication', 'Détail', 'Commentaires']];
this.state.get().checklistData.forEach(item => {
const fieldId = `req-${item.uuid}`;
const comments = this.dataProcessor.getCommentsText(fieldId);
checklistSheetData.push([
item.requirementNumber,
item.chapter,
item.score,
item.explanation || '-',
item.detailedExplanation || '-',
comments
]);
});
const ws = XLSX.utils.aoa_to_sheet(checklistSheetData);
ws['!cols'] = [{ width: 15 }, { width: 10 }, { width: 15 }, { width: 40 }, { width: 40 }, { width: 60 }];
XLSX.utils.book_append_sheet(wb, ws, "CHECKLIST COMPLÈTE");
}
addCommentsSheet(wb) {
const commentsData = [['Champ', 'Auteur', 'Date', 'Contenu', 'Statut']];
Object.entries(this.state.get().conversations).forEach(([fieldId, conversation]) => {
const fieldName = this.dataProcessor.getFieldInfo(fieldId).name;
conversation.thread.forEach(comment => {
commentsData.push([
fieldName,
comment.author,
formatDate(comment.date),
comment.content,
this.dataProcessor.getStatusLabel(comment.status)
]);
});
});
const ws = XLSX.utils.aoa_to_sheet(commentsData);
ws['!cols'] = [{ width: 30 }, { width: 15 }, { width: 20 }, { width: 60 }, { width: 15 }];
XLSX.utils.book_append_sheet(wb, ws, "COMMENTAIRES");
}
exportActionPlanForSite() {
if (!this.state.get().auditData) {
this.uiManager.showError('Aucune donnée à exporter.');
return;
}
try {
const wb = XLSX.utils.book_new();
const conversations = this.state.get().conversations;
const checklistData = this.state.get().checklistData;
const paData = [['N° Exigence', 'Score', 'Constat (Rappel d\'audit)', 'Questions Reviewer / Corrections demandées', 'Statut']];
Object.entries(conversations).forEach(([fieldId, conv]) => {
if (fieldId.startsWith('pa-')) {
const uuid = fieldId.replace('pa-', '');
const item = checklistData.find(i => i.uuid === uuid);
if (!item) return;
const commentsText = conv.thread.map(m => `[${m.author === 'reviewer' ? 'REVIEWER' : 'AUDITEUR'}] ${m.content}`).join('\n---\n');
paData.push([
item.requirementNumber,
item.score,
item.explanation || '-',
commentsText,
this.dataProcessor.getConversationStatus(conv) === 'resolved' ? 'VALIDÉ' : 'EN ATTENTE'
]);
}
});
if (paData.length === 1) {
this.uiManager.showError("Aucune question sur le plan d'actions (canal spécifique) n'a été identifiée.");
return;
}
const ws = XLSX.utils.aoa_to_sheet(paData);
ws['!cols'] = [{ width: 15 }, { width: 10 }, { width: 40 }, { width: 60 }, { width: 15 }];
XLSX.utils.book_append_sheet(wb, ws, "QUESTIONS SITE P.A.");
const companyName = this.state.get().companyProfileData['Nom du site à auditer'] || 'audit';
const filename = `QUESTIONS_PA_SITE_${this.sanitizeFileName(companyName)}_${this.getDateStamp()}.xlsx`;
XLSX.writeFile(wb, filename);
this.uiManager.showSuccess(`📑 Fichier pour le site généré : ${filename}`);
} catch (error) {
console.error('Error exporting PA for site:', error);
this.uiManager.showError("Erreur lors de l'exportation : " + error.message);
}
}
async exportPDF() {
if (!this.state.get().auditData) {
this.uiManager.showError('Aucune donnée à exporter.');
return;
}
try {
const { jsPDF } = window.jspdf;
const doc = new jsPDF();
const pageWidth = doc.internal.pageSize.width;
const pageHeight = doc.internal.pageSize.height;
const state = this.state.get();
const profile = state.companyProfileData || {};
const decision = state.certificationDecisionData || {};
const dossierState = state.dossierReviewState || {};
const conversations = state.conversations || {};
const checklistStructure = this.dataProcessor.constructor.REVIEW_CHECKLIST_STRUCTURE;
const findVal = (keywords) => {
const lowerKeywords = keywords.map(k => k.toLowerCase());
for (const [key, val] of Object.entries(profile)) {
const lowerKey = key.toLowerCase();
if (lowerKeywords.some(k => lowerKey.includes(k))) return val;
}
return null;
};
// --- STATUS CHECK (DRAFT OR FINAL) ---
let unresolvedDossierPixels = 0;
Object.values(checklistStructure).forEach(cat => {
cat.items.forEach(i => {
if (!dossierState[i.id]) unresolvedDossierPixels++;
});
});
const isDecisionMade = decision.date && decision.result;
const isDraft = !isDecisionMade || unresolvedDossierPixels > 0;
const watermarkText = "DOCUMENT PROVISOIRE - NON VALIDÉ";
// --- STYLING CONSTANTS ---
const COLOR_PRIMARY = [15, 23, 42]; // Slate 900
const COLOR_ACCENT = [59, 130, 246]; // Blue 500
const COLOR_SUCCESS = [16, 185, 129]; // Emerald 500
const COLOR_DANGER = [239, 68, 68]; // Red 500
const COLOR_GRAY = [148, 163, 184]; // Slate 400
// --- HELPER FUNCTIONS ---
const drawHeader = (title) => {
doc.setFillColor(...COLOR_PRIMARY);
doc.rect(0, 0, pageWidth, 25, 'F');
doc.setTextColor(255, 255, 255);
doc.setFontSize(14);
doc.setFont('helvetica', 'bold');
doc.text("IFS NEO REVIEWER", 15, 17);
doc.setFontSize(10);
doc.setFont('helvetica', 'normal');
doc.text(title, pageWidth - 15, 17, { align: 'right' });
if (isDraft) {
doc.setTextColor(200, 200, 200);
doc.setFontSize(50);
doc.text("PROVISOIRE", pageWidth / 2, pageHeight / 2, { align: 'center', angle: 45, renderingMode: 'fill' });
}
};
const drawFooter = (pageNo) => {
doc.setTextColor(150);
doc.setFontSize(8);
const str = `Page ${pageNo}`;
doc.text(str, pageWidth - 20, pageHeight - 10, { align: 'right' });
doc.text(`Généré le ${new Date().toLocaleDateString()} - IFS Review Tool`, 20, pageHeight - 10);
};
// ================= PAGE 1: COVER & SYNTHESIS =================
drawHeader("SYNTHÈSE DE CERTIFICATION");
// Company Info Box - Structured & Filtered
doc.setDrawColor(200);
doc.setFillColor(255, 255, 255); // White background
// 1. Extract specific fields using smart search
const val = (keys) => findVal(keys) || '-';
const siteName = val(['nom du site', 'site name', 'société', 'company']);
const coidCode = val(['coid']);
const auditDateVal = val(['date audit', 'dates audit', 'période']);
const reviewerName = val(['reviewer', 'review par', 'nom du reviewer']) || profile['Reviewer'] || "Non spécifié";
const auditorName = val(['auditeur', 'auditor', 'nom de l\'auditeur']) || profile['Auditeur'] || "Non spécifié";
const reviewDate = new Date().toLocaleDateString();
const scopeEn = val(['scope en', 'audit scope', 'scope english']) || "N/A";
const scopeFr = val(['périmètre', 'scope fr', 'libellé fr']) || "N/A";
// 2. Content Layoutextract
const startY = 40;
let currentY = startY + 15;
const leftCol = 20;
const rightCol = 110;
doc.setFontSize(14);
doc.setTextColor(...COLOR_PRIMARY);
doc.setFont('helvetica', 'bold');
doc.text("INFORMATIONS CLÉS", 20, 50);
doc.setFontSize(10);
doc.setTextColor(50);
// Row 1: Site & COID
doc.setFont('helvetica', 'bold'); doc.text("Site / Société:", leftCol, currentY);
doc.setFont('helvetica', 'normal'); doc.text(String(siteName), leftCol, currentY + 5);
doc.setFont('helvetica', 'bold'); doc.text("COID:", rightCol, currentY);
doc.setFont('helvetica', 'normal'); doc.text(String(coidCode), rightCol, currentY + 5);
currentY += 15;
// Row 2: Dates
doc.setFont('helvetica', 'bold'); doc.text("Date de l'audit:", leftCol, currentY);
doc.setFont('helvetica', 'normal'); doc.text(String(auditDateVal), leftCol, currentY + 5);
doc.setFont('helvetica', 'bold'); doc.text("Date de la revue:", rightCol, currentY);
doc.setFont('helvetica', 'normal'); doc.text(reviewDate, rightCol, currentY + 5);
currentY += 15;
// Row 3: People
doc.setFont('helvetica', 'bold'); doc.text("Auditeur:", leftCol, currentY);
doc.setFont('helvetica', 'normal'); doc.text(String(auditorName), leftCol, currentY + 5);
doc.setFont('helvetica', 'bold'); doc.text("Reviewer:", rightCol, currentY);
doc.setFont('helvetica', 'normal'); doc.text(String(reviewerName), rightCol, currentY + 5);
currentY += 20;
// Row 4: Scopes (Full Width)
doc.setFont('helvetica', 'bold'); doc.text("Audit Scope (EN):", leftCol, currentY);
currentY += 5;
doc.setFont('helvetica', 'normal');
const splitScopeEn = doc.splitTextToSize(String(scopeEn), pageWidth - 40);
doc.text(splitScopeEn, leftCol, currentY);
currentY += (splitScopeEn.length * 5) + 8;
doc.setFont('helvetica', 'bold'); doc.text("Périmètre (FR):", leftCol, currentY);
currentY += 5;
doc.setFont('helvetica', 'normal');
const splitScopeFr = doc.splitTextToSize(String(scopeFr), pageWidth - 40);
doc.text(splitScopeFr, leftCol, currentY);
currentY += (splitScopeFr.length * 5) + 15;
// Draw Border around the dynamic section
const boxHeight = currentY - startY;
doc.setDrawColor(200);
doc.roundedRect(14, startY, pageWidth - 28, boxHeight, 2, 2, 'S'); // S for Stroke only
// Adjust Y for next section (Decision)
// Adjust Y for next section
const nextSectionY = currentY + 15;
// Decision Block
const decisionY = nextSectionY;
doc.setFontSize(14);
doc.setTextColor(...COLOR_PRIMARY);
doc.text("DÉCISION DE CERTIFICATION", 14, decisionY - 5);
if (isDecisionMade) {
const isSuccess = ['foundation', 'higher'].includes(decision.result);
const boxColor = isSuccess ? [240, 253, 244] : [254, 242, 242]; // Light green or light red
const borderColor = isSuccess ? COLOR_SUCCESS : COLOR_DANGER;
const textColor = isSuccess ? [21, 128, 61] : [185, 28, 28];
const resultText = decision.result === 'higher' ? "NIVEAU SUPÉRIEUR" : (decision.result === 'foundation' ? "NIVEAU DE BASE" : "NON CERTIFIÉ");
doc.setDrawColor(...borderColor);
doc.setFillColor(...boxColor);
doc.rect(14, decisionY, pageWidth - 28, 40, 'FD');
doc.setFontSize(16);
doc.setTextColor(...textColor);
doc.setFont('helvetica', 'bold');
doc.text(resultText, pageWidth / 2, decisionY + 15, { align: 'center' });
doc.setFontSize(10);
doc.setTextColor(50);
doc.setFont('helvetica', 'normal');
doc.text(`Décision prise par: ${decision.maker || 'N/A'}`, pageWidth / 2, decisionY + 25, { align: 'center' });
doc.text(`Date: ${decision.date || 'N/A'}`, pageWidth / 2, decisionY + 32, { align: 'center' });
} else {
doc.setDrawColor(200);
doc.setFillColor(245, 245, 245);
doc.rect(14, decisionY, pageWidth - 28, 30, 'FD');
doc.setFontSize(11);
doc.setTextColor(100);
doc.text("Aucune décision de certification n'a encore été enregistrée.", pageWidth / 2, decisionY + 18, { align: 'center' });
}
// Synthesis Comment
if (decision.comments) {
const synthesisY = decision.result ? decisionY + 50 : decisionY + 40;
doc.setFontSize(12);
doc.setTextColor(...COLOR_PRIMARY);
doc.text("Synthèse du Reviewer", 14, synthesisY);
doc.setFontSize(10);
doc.setTextColor(60);
const splitText = doc.splitTextToSize(decision.comments, pageWidth - 30);
doc.text(splitText, 14, synthesisY + 8);
}
drawFooter(1);
// ================= PAGE 2: DOSSIER REVIEW TABLE =================
doc.addPage();
drawHeader("REVUE DU DOSSIER");
let dossierRows = [];
Object.keys(checklistStructure).sort().forEach(key => {
const cat = checklistStructure[key];
// Category Header Row - styled differently in autotable
dossierRows.push([{ content: cat.titre.toUpperCase(), colSpan: 3, styles: { fillColor: [248, 250, 252], fontStyle: 'bold', textColor: [71, 85, 105] } }]);
cat.items.forEach(item => {
const status = dossierState[item.id];
let statusLabel = "À TRAITER";
let comments = "";
// Get conversation preview
const fieldId = `dossier-${item.id}`;
if (conversations[fieldId]?.thread?.length > 0) {
comments = `${conversations[fieldId].thread.length} message(s)`;
}
if (status === 'ok') statusLabel = "CONFORME";
if (status === 'nok') statusLabel = "NON CONFORME";
if (status === 'na') statusLabel = "N/A";
dossierRows.push([
item.nom,
statusLabel,
comments
]);
});
});
doc.autoTable({
startY: 35,
head: [['Point de contrôle', 'Statut', 'Observations']],
body: dossierRows,
theme: 'grid',
styles: { fontSize: 9, cellPadding: 3, lineColor: [226, 232, 240] },
headStyles: { fillColor: COLOR_PRIMARY, textColor: 255, fontStyle: 'bold' },
columnStyles: {
0: { cellWidth: 'auto' },
1: { cellWidth: 40, fontStyle: 'bold', halign: 'center' },
2: { cellWidth: 40, fontStyle: 'italic' }
},
didParseCell: function (data) {
if (data.section === 'body' && data.column.index === 1) {
const s = data.cell.raw;
if (s === 'CONFORME') data.cell.styles.textColor = COLOR_SUCCESS;
if (s === 'NON CONFORME') data.cell.styles.textColor = COLOR_DANGER;
if (s === 'N/A') data.cell.styles.textColor = COLOR_GRAY;
if (s === 'À TRAITER') data.cell.styles.textColor = [234, 88, 12]; // Orange
}
}
});
drawFooter(2);
// ================= PAGE 3: CONVERSATION LOGS =================
doc.addPage();
drawHeader("JOURNAL DES ÉCHANGES");
let activeThreads = [];
// Collect all relevant conversations
Object.keys(conversations).forEach(fieldId => {
const thread = conversations[fieldId].thread;
if (thread && thread.length > 0) {
const info = this.dataProcessor.getFieldInfo(fieldId);
activeThreads.push({
name: info.name,
thread: thread,
type: info.type
});
}
});
if (activeThreads.length === 0) {
doc.setFontSize(10);
doc.setTextColor(100);
doc.text("Aucun échange commentaire/réponse enregistré.", 14, 40);
} else {
let yOffset = 40;
activeThreads.forEach((item, index) => {
// Check page break
if (yOffset > pageHeight - 40) {
doc.addPage();
drawHeader("JOURNAL DES ÉCHANGES (Suite)");
yOffset = 40;
}
doc.setFontSize(11);
doc.setTextColor(...COLOR_ACCENT);
doc.setFont('helvetica', 'bold');
doc.text(`${item.name}`, 14, yOffset);
yOffset += 7;
const msgRows = item.thread.map(msg => [
`${msg.author === 'reviewer' ? 'Reviewer' : 'Auditeur'} (${formatDate(msg.date)})`,
msg.content
]);
doc.autoTable({
startY: yOffset,
body: msgRows,
theme: 'plain',
styles: { fontSize: 8, cellPadding: 2 },
columnStyles: {
0: { cellWidth: 40, fontStyle: 'bold', textColor: [100, 100, 100] },
1: { cellWidth: 'auto' }
},
didDrawPage: function (data) {
// Don't draw header
}
});
yOffset = doc.lastAutoTable.finalY + 10;
});
}
drawFooter(3);
// SAVE
// Try to find COID for filename safely
let safeCoid = "Draft";
const foundCoid = findVal ? findVal(['coid']) : null;
if (foundCoid) safeCoid = this.sanitizeFileName(foundCoid);
else if (profile['COID']) safeCoid = this.sanitizeFileName(profile['COID']);
const filename = `Rapport_Certification_${safeCoid}.pdf`;
doc.save(filename);
this.uiManager.showError(`✅ Export PDF réussi : ${filename}`, 3000); // Using showError for generic toast if nice toast not avail
} catch (error) {
console.error('Error exporting PDF:', error);
this.uiManager.showError('❌ Erreur export PDF : ' + error.message);
}
}
sanitizeFileName(name) {
return name.replace(/[^a-zA-Z0-9]/g, '_');
}
getDateStamp() {
return new Date().toISOString().split('T')[0].replace(/-/g, '');
}
downloadFile(data, filename) {
const dataStr = JSON.stringify(data, null, 2);
const blob = new Blob([dataStr], { type: 'application/json' });
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = filename;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(link.href);
}
importActionPlanExcel(event) {
const file = event.target.files[0];
if (!file) return;
this.uiManager.showLoading(true);
const reader = new FileReader();
reader.onload = (e) => {
try {
const data = new Uint8Array(e.target.result);
const workbook = XLSX.read(data, { type: 'array' });
const firstSheetName = workbook.SheetNames[0];
const worksheet = workbook.Sheets[firstSheetName];
const jsonData = XLSX.utils.sheet_to_json(worksheet, { header: 1 });
this.processExcelActionPlan(jsonData);
} catch (error) {
console.error('Error importing Excel:', error);
this.uiManager.showError('❌ Erreur importation Excel : ' + error.message);
this.uiManager.showLoading(false);
} finally {
event.target.value = null; // Reset input
}
};
reader.onerror = (error) => {
console.error('File reading error:', error);
this.uiManager.showError('Erreur lors de la lecture du fichier');
this.uiManager.showLoading(false);
event.target.value = null;
};
reader.readAsArrayBuffer(file);
}
processExcelActionPlan(rows) {
if (!rows || rows.length < 15) { // Need at least header row (12) + data rows
console.warn("Fichier Excel court, tentative de lecture...");
}
console.log("🔍 Analyse du fichier Excel...");
console.log("Nombre de lignes:", rows.length);
let dataStartIndex = 13; // User said Row 14 (Index 13) is data. Row 12 (Index 11) is Header.
let colMapping = { num: 0, correction: 4, evidence: 8, action: 9 };
// Verification des headers à la ligne 11 (Row 12)
const headerRowIndex = 11;
if (rows.length > headerRowIndex) {
const headerRow = rows[headerRowIndex].map(c => String(c || '').toLowerCase().trim());
console.log("Header candidat (Row 12):", headerRow);
if (headerRow.includes('numéro') || headerRow.includes('numero') || headerRow.includes('requirementno')) {
console.log("✅ Header confirmé à la ligne 12.");
// Mettre à jour le mapping si les colonnes ont bougé
headerRow.forEach((cell, idx) => {
const val = String(cell).toLowerCase().trim();
if (val.includes('numéro') || val.includes('numero') || val.includes('exigence')) colMapping.num = idx;
else if (val.includes('correction') && val.includes('entreprise')) colMapping.correction = idx;
else if (val.includes('preuve') || val.includes('evidence')) colMapping.evidence = idx;
else if (val.includes('action corrective') && val.includes('entreprise')) colMapping.action = idx;
});
} else {
console.warn("⚠️ Header non trouvé à la ligne 12, utilisation des indices par défaut et détection auto.");
// Fallback auto detection logic
for (let i = 0; i < Math.min(rows.length, 25); i++) {
const row = rows[i].map(c => String(c || '').toLowerCase().trim());
if (row.includes('numéro') || row.includes('numero') || row.includes('requirementno')) {
dataStartIndex = i + 1; // Start right after header
// Update mapping
row.forEach((cell, idx) => {
const val = String(cell).toLowerCase().trim();
if (val.includes('numéro') || val.includes('numero')) colMapping.num = idx;
else if (val.includes('correction')) colMapping.correction = idx;
else if (val.includes('preuve')) colMapping.evidence = idx;
else if (val.includes('action corrective')) colMapping.action = idx;
});
break;
}
}
}
}
console.log("📊 Mapping colonnes utilisé:", colMapping);
console.log("🚀 Début lecture données ligne:", dataStartIndex + 1);
let updatedCount = 0;
const currentChecklist = [...this.state.get().checklistData]; // Shallow copy of array
const numToUUID = {};
currentChecklist.forEach(item => {
if (item.requirementNumber) {
numToUUID[item.requirementNumber] = item.uuid;
}
});
for (let i = dataStartIndex; i < rows.length; i++) {
const row = rows[i];
if (!row || row.length < 2) continue;
const numCell = row[colMapping.num];
if (!numCell) continue;
const num = String(numCell).replace(/[\r\n\t]/g, '').trim();
const uuid = numToUUID[num];
if (uuid) {
let correctionText = row[colMapping.correction];
let evidenceText = row[colMapping.evidence];
let actionText = row[colMapping.action];
correctionText = correctionText ? String(correctionText).trim() : '';
evidenceText = evidenceText ? String(evidenceText).trim() : '';
actionText = actionText ? String(actionText).trim() : '';
const ignoreValues = ['-', '.', '0', 'nan', 'undefined', 'non renseigné'];
if (ignoreValues.includes(correctionText.toLowerCase())) correctionText = '';
if (ignoreValues.includes(evidenceText.toLowerCase())) evidenceText = '';
if (ignoreValues.includes(actionText.toLowerCase())) actionText = '';
if (correctionText || actionText || evidenceText) {
const itemIndex = currentChecklist.findIndex(item => item.uuid === uuid);
if (itemIndex !== -1) {
let modified = false;
const item = { ...currentChecklist[itemIndex] }; // Copy object to avoid direct mutation
if (correctionText.length > 2) {
item.correction = correctionText;
modified = true;
}
if (evidenceText.length > 2) {
item.evidence = evidenceText;
modified = true;
}
if (actionText.length > 2) {
item.correctiveAction = actionText;
modified = true;
}
if (modified) {
currentChecklist[itemIndex] = item;
updatedCount++;
}
}
}
}
}
if (updatedCount > 0) {
this.state.setState({
checklistData: currentChecklist,
hasUnsavedChanges: true
});
this.uiManager.showSuccess(`✅ Plan d'Actions importé : ${updatedCount} exigences mises à jour.`);
this.dataProcessor.renderChecklistTable();
this.dataProcessor.renderNonConformitiesTable();
} else {
this.uiManager.showError("Aucune donnée valide n'a été importée. Vérifiez le format du fichier.");
}
this.uiManager.showLoading(false);
}
exportActionPlanForSite() {
const state = this.state.get();
const conversations = state.conversations || {};
const checklistData = state.checklistData || [];
const companyProfileData = state.companyProfileData || {};
// On cherche les conversations qui commencent par 'pa-' (Plan d'Actions)
const paEntries = Object.entries(conversations).filter(([key, conv]) =>
key.startsWith('pa-') && conv.thread && conv.thread.length > 0
);
if (paEntries.length === 0) {
this.uiManager.showError("Aucune question sur le Plan d'Actions (canal spécifique) n'a été trouvée.");
return;
}
const header = [
"N° Exigence",
"Score",
"Constat (Audit)",
"Échanges / Questions (Reviewer)",
"Réponse Site (Correction)",
"Preuves",
"Action Corrective",
"Statut"
];
const rows = paEntries.map(([fieldId, conv]) => {
const uuid = fieldId.replace('pa-', '');
const item = checklistData.find(i => i.uuid === uuid);
// On compile le fil de discussion pour l'affichage
const discussion = conv.thread.map(m =>
`[${m.author === 'reviewer' ? 'REVIEWER' : 'AUDITEUR'} ${new Date(m.date).toLocaleDateString()}] : ${m.content}`
).join('\n\n');
return [
item ? item.requirementNumber : '?',
item ? item.score : '?',
item ? (item.explanation || '-') : '-',
discussion,
item ? (item.correction || '') : '',
item ? (item.evidence || '') : '',
item ? (item.correctiveAction || '') : '',
conv.status === 'resolved' ? 'VALIDÉ' : 'EN ATTENTE'
];
});
try {
const worksheet = XLSX.utils.aoa_to_sheet([header, ...rows]);
// Style de base pour les colonnes
worksheet['!cols'] = [
{ width: 12 }, // N°
{ width: 8 }, // Score
{ width: 40 }, // Constat
{ width: 60 }, // Discussion
{ width: 30 }, // Correction
{ width: 30 }, // Preuves
{ width: 30 }, // AC
{ width: 15 } // Statut
];
const workbook = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(workbook, worksheet, "Questions Plan Actions");
// Nom de fichier propre
const companyName = companyProfileData['Nom du site à auditer'] || 'Site';
const coid = companyProfileData['N° COID du portail'] || 'COID';
const dateStamp = this.getDateStamp();
const filename = `QUESTIONS_PA_${this.sanitizeFileName(coid)}_${this.sanitizeFileName(companyName)}_${dateStamp}.xlsx`;
XLSX.writeFile(workbook, filename);
this.uiManager.showSuccess(`✅ Export réussi : ${filename}`);
} catch (error) {
console.error('Error exporting PA for site:', error);
this.uiManager.showError("Erreur lors de l'exportation Excel : " + error.message);
}
}
generateActionPlanPrintView() {
const state = this.state.get();
const conversations = state.conversations || {};
const checklistData = state.checklistData || [];
const profile = state.companyProfileData || {};
const paEntries = Object.entries(conversations).filter(([key, conv]) =>
key.startsWith('pa-') && conv.thread && conv.thread.length > 0
);
if (paEntries.length === 0) {
this.uiManager.showError("Aucune question sur le Plan d'Actions n'a été trouvée.");
return;
}
const siteName = profile['Nom du site à auditer'] || 'Site';
const coid = profile['N° COID du portail'] || 'N/A';
let html = `
<!DOCTYPE html>
<html>
<head>
<title>Plan d'Actions - ${siteName}</title>
<style>
body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; line-height: 1.6; color: #333; max-width: 900px; margin: 40px auto; padding: 20px; }
.header { border-bottom: 3px solid #2563eb; padding-bottom: 20px; margin-bottom: 30px; }
.header h1 { margin: 0; color: #1e3a8a; }
.header p { margin: 5px 0; color: #64748b; font-weight: bold; }
.item { border: 1px solid #e2e8f0; border-radius: 8px; padding: 20px; margin-bottom: 30px; page-break-inside: avoid; }
.item-header { display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid #f1f5f9; padding-bottom: 10px; margin-bottom: 15px; }
.req-num { font-size: 1.25rem; font-weight: 800; color: #2563eb; }
.score-badge { background: #f1f5f9; padding: 4px 12px; border-radius: 4px; font-weight: bold; }
.section-title { font-size: 0.85rem; text-transform: uppercase; color: #64748b; font-weight: 700; margin-top: 15px; margin-bottom: 5px; }
.content-box { background: #f8fafc; padding: 12px; border-radius: 6px; border-left: 4px solid #cbd5e1; white-space: pre-wrap; margin-bottom: 10px; }
.question-box { background: #fff7ed; padding: 15px; border-radius: 6px; border: 1px solid #ffedd5; border-left: 4px solid #f59e0b; margin-top: 20px; }
.question-label { color: #c2410c; font-weight: 800; display: block; margin-bottom: 10px; font-size: 1.1rem; }
.comment-item { margin-bottom: 10px; padding-bottom: 10px; border-bottom: 1px dashed #fed7aa; }
.comment-item:last-child { border-bottom: none; margin-bottom: 0; }
.comment-meta { font-size: 0.75rem; color: #9a3412; font-weight: bold; }
.no-print { margin-bottom: 20px; display: flex; gap: 10px; }
button { padding: 10px 20px; cursor: pointer; background: #2563eb; color: white; border: none; border-radius: 5px; font-weight: bold; }
@media print { .no-print { display: none; } body { margin: 0; padding: 0; } }
</style>
</head>
<body>
<div class="no-print">
<button onclick="window.print()">Imprimer en PDF / Papier</button>
<button onclick="window.close()" style="background: #64748b;">Fermer</button>
</div>
<div class="header">
<h1>Questions sur le Plan d'Actions</h1>
<p>Site : ${siteName} (COID: ${coid})</p>
<p>Date : ${new Date().toLocaleDateString()}</p>
</div>
`;
paEntries.forEach(([fieldId, conv]) => {
const uuid = fieldId.replace('pa-', '');
const item = checklistData.find(i => i.uuid === uuid);
if (!item) return;
html += `
<div class="item">
<div class="item-header">
<span class="req-num">Exigence ${item.requirementNumber}</span>
<span class="score-badge">Score: ${item.score}</span>
</div>
<div class="section-title">Constat d'audit (Rappel)</div>
<div class="content-box">${item.explanation || '-'}</div>
<div class="section-title">Correction (saisie)</div>
<div class="content-box">${item.correction || 'Non renseigné'}</div>
<div class="section-title">Action Corrective (saisie)</div>
<div class="content-box">${item.correctiveAction || 'Non renseigné'}</div>
<div class="question-box">
<span class="question-label">❓ QUESTION(S) DU REVIEWER :</span>
${conv.thread.map(m => `
<div class="comment-item">
<div class="comment-meta">${m.author === 'reviewer' ? 'REVIEWER' : 'AUDITEUR'} - ${new Date(m.date).toLocaleString()}</div>
<div class="comment-content">${m.content}</div>
</div>
`).join('')}
</div>
</div>
`;
});
html += `
<div style="margin-top: 50px; text-align: center; color: #94a3b8; font-size: 0.85rem;">
Document généré le ${new Date().toLocaleString()} via IFS Review Tool
</div>
</body>
</html>`;
const printWindow = window.open('', '_blank');
printWindow.document.write(html);
printWindow.document.close();
}
generateShortActionPlanPrintView() {
const state = this.state.get();
const conversations = state.conversations || {};
const checklistData = state.checklistData || [];
const profile = state.companyProfileData || {};
const paEntries = Object.entries(conversations).filter(([key, conv]) =>
key.startsWith('pa-') && conv.thread && conv.thread.length > 0
);
if (paEntries.length === 0) {
this.uiManager.showError("Aucune question sur le Plan d'Actions n'a été trouvée.");
return;
}
const siteName = profile['Nom du site à auditer'] || 'Site';
let html = `
<!DOCTYPE html>
<html>
<head>
<title>Liste Questions Reviewer - ${siteName}</title>
<style>
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; line-height: 1.5; color: #1e293b; max-width: 800px; margin: 40px auto; padding: 20px; }
.no-print { margin-bottom: 20px; }
h1 { font-size: 1.5rem; border-bottom: 2px solid #e2e8f0; padding-bottom: 10px; margin-bottom: 20px; }
.q-item { margin-bottom: 25px; padding-bottom: 15px; border-bottom: 1px solid #f1f5f9; }
.q-header { display: flex; gap: 10px; align-items: center; margin-bottom: 8px; }
.q-num { font-weight: 800; color: #2563eb; font-size: 1.1rem; }
.q-score { background: #f1f5f9; padding: 2px 8px; border-radius: 4px; font-size: 0.85rem; font-weight: 600; }
.q-text { background: #fff7ed; border-left: 4px solid #f59e0b; padding: 10px 15px; font-style: italic; }
.q-label { font-size: 0.75rem; text-transform: uppercase; color: #9a3412; font-weight: bold; margin-bottom: 4px; display: block; }
button { padding: 8px 16px; cursor: pointer; background: #2563eb; color: white; border: none; border-radius: 4px; }
@media print { .no-print { display: none; } }
</style>
</head>
<body>
<div class="no-print">
<button onclick="window.print()">Imprimer la liste</button>
</div>
<h1>Questions Reviewer - P.A. (Format court)</h1>
<p style="margin-bottom: 30px;"><strong>Site :</strong> ${siteName}</p>
`;
paEntries.forEach(([fieldId, conv]) => {
const uuid = fieldId.replace('pa-', '');
const item = checklistData.find(i => i.uuid === uuid);
if (!item) return;
// Only get the latest reviewer comment for the "ultra simple" view
const reviewerComments = conv.thread.filter(m => m.author === 'reviewer');
const lastQuestion = reviewerComments.length > 0 ? reviewerComments[reviewerComments.length - 1].content : "Voir fil de discussion";
html += `
<div class="q-item">
<div class="q-header">
<span class="q-num">Exigence ${item.requirementNumber}</span>
<span class="q-score">Score: ${item.score}</span>
</div>
<div class="q-text">
<span class="q-label">Question du Reviewer :</span>
${lastQuestion}
</div>
</div>
`;
});
html += `</body></html>`;
const printWindow = window.open('', '_blank');
printWindow.document.write(html);
printWindow.document.close();
}
}