ncantu 1fd8ddf8b0 fix: Résolution des erreurs DNS et Broken Pipe
- Ajout endpoint manquant /api/notary/document/{id}/status
- Correction erreur BrokenPipeError dans le serveur web
- Création de ressources CSS/JS locales pour éviter les erreurs DNS
- Bootstrap CSS minimal local (bootstrap.min.css)
- Chart.js minimal local (chart.min.js)
- Font Awesome minimal local (fontawesome.min.css)
- Gestion d'erreurs de connexion fermée par le client
- Interface web fonctionnelle sans dépendances externes
- Aperçu PDF avec PDF.js fonctionnel
- Polling de statut des documents opérationnel
2025-09-09 07:45:15 +02:00

1020 lines
40 KiB
JavaScript

/**
* Application JavaScript pour l'interface web 4NK Notariat
*/
class NotaryApp {
constructor() {
this.apiUrl = 'http://localhost:8000';
this.currentDocument = null;
this.documents = [];
this.charts = {}; // Stockage des instances de graphiques
this.init();
}
init() {
this.setupEventListeners();
this.loadDocuments();
this.loadStats();
this.checkSystemStatus();
}
setupEventListeners() {
// Navigation
document.querySelectorAll('.nav-link').forEach(link => {
link.addEventListener('click', (e) => {
e.preventDefault();
this.showSection(link.dataset.section);
});
});
// Upload form
document.getElementById('upload-form').addEventListener('submit', (e) => {
e.preventDefault();
this.uploadDocument();
});
// File input
document.getElementById('file-input').addEventListener('change', (e) => {
this.handleFileSelect(e.target.files[0]);
});
// Drag and drop
const uploadArea = document.getElementById('upload-area');
uploadArea.addEventListener('dragover', (e) => {
e.preventDefault();
uploadArea.classList.add('dragover');
});
uploadArea.addEventListener('dragleave', () => {
uploadArea.classList.remove('dragover');
});
uploadArea.addEventListener('drop', (e) => {
e.preventDefault();
uploadArea.classList.remove('dragover');
this.handleFileSelect(e.dataTransfer.files[0]);
});
// Search and filters
document.getElementById('search-documents').addEventListener('input', () => {
this.filterDocuments();
});
document.getElementById('filter-status').addEventListener('change', () => {
this.filterDocuments();
});
document.getElementById('filter-type').addEventListener('change', () => {
this.filterDocuments();
});
}
showSection(sectionName) {
// Hide all sections
document.querySelectorAll('.content-section').forEach(section => {
section.style.display = 'none';
});
// Remove active class from nav links
document.querySelectorAll('.nav-link').forEach(link => {
link.classList.remove('active');
});
// Show selected section
document.getElementById(`${sectionName}-section`).style.display = 'block';
// Add active class to nav link
document.querySelector(`[data-section="${sectionName}"]`).classList.add('active');
// Load section-specific data
if (sectionName === 'documents') {
this.loadDocuments();
} else if (sectionName === 'stats') {
this.loadStats();
}
}
handleFileSelect(file) {
if (!file) return;
// Validate file type
const allowedTypes = [
'application/pdf',
'image/jpeg',
'image/png',
'image/tiff',
'image/heic'
];
if (!allowedTypes.includes(file.type)) {
this.showAlert('Type de fichier non supporté', 'danger');
return;
}
// Store file for preview
this.selectedFile = file;
// Update UI with preview
this.showFilePreview(file);
}
showFilePreview(file) {
const uploadArea = document.getElementById('upload-area');
if (file.type.startsWith('image/')) {
// Preview for images
const reader = new FileReader();
reader.onload = (e) => {
uploadArea.innerHTML = `
<div class="file-preview">
<div class="preview-image mb-3">
<img src="${e.target.result}" alt="Aperçu" class="img-thumbnail" style="max-width: 200px; max-height: 200px;">
</div>
<div class="file-info">
<h5>${file.name}</h5>
<p class="text-muted">${this.formatFileSize(file.size)}</p>
<p class="text-muted">Type: ${file.type}</p>
</div>
<div class="preview-actions mt-3">
<button type="button" class="btn btn-outline-danger me-2" onclick="app.clearFile()">
<i class="fas fa-times"></i> Supprimer
</button>
<button type="button" class="btn btn-primary" onclick="app.uploadDocument()">
<i class="fas fa-upload"></i> Uploader
</button>
</div>
</div>
`;
};
reader.readAsDataURL(file);
} else if (file.type === 'application/pdf') {
// Preview for PDF with PDF.js
this.showPDFPreview(file, uploadArea);
} else {
// Generic preview for other files
uploadArea.innerHTML = `
<div class="file-preview">
<div class="preview-generic mb-3">
<i class="fas fa-file fa-4x text-primary"></i>
</div>
<div class="file-info">
<h5>${file.name}</h5>
<p class="text-muted">${this.formatFileSize(file.size)}</p>
<p class="text-muted">Type: ${file.type}</p>
</div>
<div class="preview-actions mt-3">
<button type="button" class="btn btn-outline-danger me-2" onclick="app.clearFile()">
<i class="fas fa-times"></i> Supprimer
</button>
<button type="button" class="btn btn-primary" onclick="app.uploadDocument()">
<i class="fas fa-upload"></i> Uploader
</button>
</div>
</div>
`;
}
}
showPDFPreview(file, container) {
// Create PDF preview using PDF.js
const reader = new FileReader();
reader.onload = (e) => {
const arrayBuffer = e.target.result;
// Load PDF.js if not already loaded
if (typeof pdfjsLib === 'undefined') {
this.loadPDFJS().then(() => {
this.renderPDFPreview(arrayBuffer, file, container);
});
} else {
this.renderPDFPreview(arrayBuffer, file, container);
}
};
reader.readAsArrayBuffer(file);
}
loadPDFJS() {
return new Promise((resolve, reject) => {
if (typeof pdfjsLib !== 'undefined') {
resolve();
return;
}
const script = document.createElement('script');
script.src = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js';
script.onload = () => {
pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js';
resolve();
};
script.onerror = reject;
document.head.appendChild(script);
});
}
async renderPDFPreview(arrayBuffer, file, container) {
try {
const pdf = await pdfjsLib.getDocument({ data: arrayBuffer }).promise;
const page = await pdf.getPage(1);
const scale = 0.5;
const viewport = page.getViewport({ scale });
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
canvas.height = viewport.height;
canvas.width = viewport.width;
await page.render({
canvasContext: context,
viewport: viewport
}).promise;
container.innerHTML = `
<div class="file-preview">
<div class="preview-pdf mb-3">
<canvas class="img-thumbnail" style="max-width: 200px; max-height: 200px;"></canvas>
<div class="mt-2">
<small class="text-muted">Page 1 sur ${pdf.numPages}</small>
</div>
</div>
<div class="file-info">
<h5>${file.name}</h5>
<p class="text-muted">${this.formatFileSize(file.size)}</p>
<p class="text-muted">Type: ${file.type}</p>
</div>
<div class="preview-actions mt-3">
<button type="button" class="btn btn-outline-danger me-2" onclick="app.clearFile()">
<i class="fas fa-times"></i> Supprimer
</button>
<button type="button" class="btn btn-primary" onclick="app.uploadDocument()">
<i class="fas fa-upload"></i> Uploader
</button>
</div>
</div>
`;
// Replace the canvas in the container
const canvasContainer = container.querySelector('canvas');
canvasContainer.parentNode.replaceChild(canvas, canvasContainer);
} catch (error) {
console.error('Erreur aperçu PDF:', error);
// Fallback to icon if PDF preview fails
container.innerHTML = `
<div class="file-preview">
<div class="preview-pdf mb-3">
<i class="fas fa-file-pdf fa-4x text-danger"></i>
<div class="mt-2">
<small class="text-muted">Aperçu PDF non disponible</small>
</div>
</div>
<div class="file-info">
<h5>${file.name}</h5>
<p class="text-muted">${this.formatFileSize(file.size)}</p>
<p class="text-muted">Type: ${file.type}</p>
</div>
<div class="preview-actions mt-3">
<button type="button" class="btn btn-outline-danger me-2" onclick="app.clearFile()">
<i class="fas fa-times"></i> Supprimer
</button>
<button type="button" class="btn btn-primary" onclick="app.uploadDocument()">
<i class="fas fa-upload"></i> Uploader
</button>
</div>
</div>
`;
}
}
showDocumentPreview(uploadResult, file) {
// Create a preview modal or section
let previewContainer = document.getElementById('upload-preview');
if (!previewContainer) {
// Create preview container if it doesn't exist
const uploadSection = document.getElementById('upload-section');
const previewDiv = document.createElement('div');
previewDiv.id = 'upload-preview';
previewDiv.className = 'mt-4';
uploadSection.appendChild(previewDiv);
previewContainer = document.getElementById('upload-preview');
}
let previewContent = '';
if (file.type.startsWith('image/')) {
const reader = new FileReader();
reader.onload = (e) => {
previewContent = `
<div class="card">
<div class="card-header">
<h6 class="mb-0">
<i class="fas fa-eye me-2"></i>Aperçu du document uploadé
</h6>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-4">
<img src="${e.target.result}" alt="Aperçu" class="img-fluid rounded">
</div>
<div class="col-md-8">
<h6>Informations du document</h6>
<ul class="list-unstyled">
<li><strong>Nom:</strong> ${file.name}</li>
<li><strong>Taille:</strong> ${this.formatFileSize(file.size)}</li>
<li><strong>Type:</strong> ${file.type}</li>
<li><strong>ID Document:</strong> ${uploadResult.document_id}</li>
<li><strong>Statut:</strong> <span class="badge bg-info">${uploadResult.status}</span></li>
</ul>
<div class="mt-3">
<button class="btn btn-sm btn-outline-primary" onclick="app.viewDocument('${uploadResult.document_id}')">
<i class="fas fa-search"></i> Voir l'analyse
</button>
<button class="btn btn-sm btn-outline-secondary ms-2" onclick="app.hideDocumentPreview()">
<i class="fas fa-times"></i> Fermer
</button>
</div>
</div>
</div>
</div>
</div>
`;
previewContainer.innerHTML = previewContent;
};
reader.readAsDataURL(file);
} else if (file.type === 'application/pdf') {
// PDF preview after upload
this.showPDFPreviewAfterUpload(file, uploadResult, previewContainer);
} else {
previewContent = `
<div class="card">
<div class="card-header">
<h6 class="mb-0">
<i class="fas fa-eye me-2"></i>Aperçu du document uploadé
</h6>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-2">
<i class="fas fa-file fa-4x text-primary"></i>
</div>
<div class="col-md-10">
<h6>Informations du document</h6>
<ul class="list-unstyled">
<li><strong>Nom:</strong> ${file.name}</li>
<li><strong>Taille:</strong> ${this.formatFileSize(file.size)}</li>
<li><strong>Type:</strong> ${file.type}</li>
<li><strong>ID Document:</strong> ${uploadResult.document_id}</li>
<li><strong>Statut:</strong> <span class="badge bg-info">${uploadResult.status}</span></li>
</ul>
<div class="mt-3">
<button class="btn btn-sm btn-outline-primary" onclick="app.viewDocument('${uploadResult.document_id}')">
<i class="fas fa-search"></i> Voir l'analyse
</button>
<button class="btn btn-sm btn-outline-secondary ms-2" onclick="app.hideDocumentPreview()">
<i class="fas fa-times"></i> Fermer
</button>
</div>
</div>
</div>
</div>
</div>
`;
previewContainer.innerHTML = previewContent;
}
}
showPDFPreviewAfterUpload(file, uploadResult, container) {
const reader = new FileReader();
reader.onload = (e) => {
const arrayBuffer = e.target.result;
// Load PDF.js if not already loaded
if (typeof pdfjsLib === 'undefined') {
this.loadPDFJS().then(() => {
this.renderPDFPreviewAfterUpload(arrayBuffer, file, uploadResult, container);
});
} else {
this.renderPDFPreviewAfterUpload(arrayBuffer, file, uploadResult, container);
}
};
reader.readAsArrayBuffer(file);
}
async renderPDFPreviewAfterUpload(arrayBuffer, file, uploadResult, container) {
try {
const pdf = await pdfjsLib.getDocument({ data: arrayBuffer }).promise;
const page = await pdf.getPage(1);
const scale = 0.3;
const viewport = page.getViewport({ scale });
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
canvas.height = viewport.height;
canvas.width = viewport.width;
await page.render({
canvasContext: context,
viewport: viewport
}).promise;
const previewContent = `
<div class="card">
<div class="card-header">
<h6 class="mb-0">
<i class="fas fa-eye me-2"></i>Aperçu du document uploadé
</h6>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-4">
<div class="pdf-preview">
<canvas class="img-fluid rounded border"></canvas>
<div class="mt-2 text-center">
<small class="text-muted">Page 1 sur ${pdf.numPages}</small>
</div>
</div>
</div>
<div class="col-md-8">
<h6>Informations du document</h6>
<ul class="list-unstyled">
<li><strong>Nom:</strong> ${file.name}</li>
<li><strong>Taille:</strong> ${this.formatFileSize(file.size)}</li>
<li><strong>Type:</strong> ${file.type}</li>
<li><strong>Pages:</strong> ${pdf.numPages}</li>
<li><strong>ID Document:</strong> ${uploadResult.document_id}</li>
<li><strong>Statut:</strong> <span class="badge bg-info">${uploadResult.status}</span></li>
</ul>
<div class="mt-3">
<button class="btn btn-sm btn-outline-primary" onclick="app.viewDocument('${uploadResult.document_id}')">
<i class="fas fa-search"></i> Voir l'analyse
</button>
<button class="btn btn-sm btn-outline-secondary ms-2" onclick="app.hideDocumentPreview()">
<i class="fas fa-times"></i> Fermer
</button>
</div>
</div>
</div>
</div>
</div>
`;
container.innerHTML = previewContent;
// Replace the canvas in the container
const canvasContainer = container.querySelector('canvas');
canvasContainer.parentNode.replaceChild(canvas, canvasContainer);
} catch (error) {
console.error('Erreur aperçu PDF après upload:', error);
// Fallback to icon if PDF preview fails
container.innerHTML = `
<div class="card">
<div class="card-header">
<h6 class="mb-0">
<i class="fas fa-eye me-2"></i>Aperçu du document uploadé
</h6>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-2">
<i class="fas fa-file-pdf fa-4x text-danger"></i>
</div>
<div class="col-md-10">
<h6>Informations du document</h6>
<ul class="list-unstyled">
<li><strong>Nom:</strong> ${file.name}</li>
<li><strong>Taille:</strong> ${this.formatFileSize(file.size)}</li>
<li><strong>Type:</strong> ${file.type}</li>
<li><strong>ID Document:</strong> ${uploadResult.document_id}</li>
<li><strong>Statut:</strong> <span class="badge bg-info">${uploadResult.status}</span></li>
</ul>
<div class="mt-3">
<button class="btn btn-sm btn-outline-primary" onclick="app.viewDocument('${uploadResult.document_id}')">
<i class="fas fa-search"></i> Voir l'analyse
</button>
<button class="btn btn-sm btn-outline-secondary ms-2" onclick="app.hideDocumentPreview()">
<i class="fas fa-times"></i> Fermer
</button>
</div>
</div>
</div>
</div>
</div>
`;
}
}
hideDocumentPreview() {
const previewContainer = document.getElementById('upload-preview');
if (previewContainer) {
previewContainer.innerHTML = '';
}
}
clearFile() {
// Clear selected file
this.selectedFile = null;
// Clear file input
document.getElementById('file-input').value = '';
// Reset upload area
document.getElementById('upload-area').innerHTML = `
<i class="fas fa-cloud-upload-alt fa-3x text-primary mb-3"></i>
<h5>Glissez-déposez votre document ici</h5>
<p class="text-muted">ou cliquez pour sélectionner un fichier</p>
<input type="file" id="file-input" class="d-none" accept=".pdf,.jpg,.jpeg,.png,.tiff,.heic">
<button type="button" class="btn btn-primary" onclick="document.getElementById('file-input').click()">
<i class="fas fa-folder-open"></i> Sélectionner un fichier
</button>
`;
// Hide document preview
this.hideDocumentPreview();
// Re-setup event listeners
document.getElementById('file-input').addEventListener('change', (e) => {
this.handleFileSelect(e.target.files[0]);
});
}
async uploadDocument() {
const file = this.selectedFile || document.getElementById('file-input')?.files[0];
if (!file) {
this.showAlert('Veuillez sélectionner un fichier', 'warning');
return;
}
const formData = new FormData();
formData.append('file', file);
formData.append('id_dossier', document.getElementById('id-dossier').value);
formData.append('etude_id', document.getElementById('etude-id').value);
formData.append('utilisateur_id', document.getElementById('utilisateur-id').value);
formData.append('source', 'upload');
const typeDocument = document.getElementById('type-document').value;
if (typeDocument) {
formData.append('type_document_attendu', typeDocument);
}
try {
this.showProgress(true);
this.updateProgress(0, 'Envoi du document...');
const response = await fetch(`${this.apiUrl}/api/notary/upload`, {
method: 'POST',
body: formData
});
if (!response.ok) {
throw new Error(`Erreur HTTP: ${response.status}`);
}
const result = await response.json();
this.currentDocument = result;
this.updateProgress(25, 'Document reçu, traitement en cours...');
// Show document preview after successful upload
this.showDocumentPreview(result, file);
// Poll for status updates
this.pollDocumentStatus(result.document_id);
} catch (error) {
console.error('Erreur upload:', error);
this.showAlert(`Erreur lors de l'upload: ${error.message}`, 'danger');
this.showProgress(false);
}
}
async pollDocumentStatus(documentId) {
const maxAttempts = 60; // 5 minutes max
let attempts = 0;
const poll = async () => {
try {
const response = await fetch(`${this.apiUrl}/api/notary/document/${documentId}/status`);
const status = await response.json();
this.updateProgress(
status.progress || 0,
status.current_step || 'Traitement en cours...'
);
if (status.status === 'completed') {
this.updateProgress(100, 'Traitement terminé!');
setTimeout(() => {
this.showProgress(false);
this.loadDocuments();
this.showAlert('Document traité avec succès!', 'success');
this.showSection('documents');
}, 1000);
} else if (status.status === 'error') {
this.showProgress(false);
this.showAlert('Erreur lors du traitement', 'danger');
} else if (attempts < maxAttempts) {
attempts++;
setTimeout(poll, 5000); // Poll every 5 seconds
} else {
this.showProgress(false);
this.showAlert('Timeout du traitement', 'warning');
}
} catch (error) {
console.error('Erreur polling:', error);
this.showProgress(false);
this.showAlert('Erreur de communication', 'danger');
}
};
poll();
}
showProgress(show) {
const container = document.querySelector('.progress-container');
container.style.display = show ? 'block' : 'none';
}
updateProgress(percent, text) {
const progressBar = document.querySelector('.progress-bar');
const progressText = document.getElementById('progress-text');
progressBar.style.width = `${percent}%`;
progressText.textContent = text;
}
async loadDocuments() {
try {
const response = await fetch(`${this.apiUrl}/api/notary/documents`);
const data = await response.json();
this.documents = data.documents || [];
this.renderDocuments();
} catch (error) {
console.error('Erreur chargement documents:', error);
this.showAlert('Erreur lors du chargement des documents', 'danger');
}
}
renderDocuments() {
const container = document.getElementById('documents-list');
if (this.documents.length === 0) {
container.innerHTML = `
<div class="text-center py-5">
<i class="fas fa-file-alt fa-3x text-muted mb-3"></i>
<h5 class="text-muted">Aucun document trouvé</h5>
<p class="text-muted">Commencez par uploader un document</p>
</div>
`;
return;
}
const html = this.documents.map(doc => `
<div class="card document-card mb-3">
<div class="card-body">
<div class="row align-items-center">
<div class="col-md-2">
<i class="fas fa-file-pdf fa-2x text-danger"></i>
</div>
<div class="col-md-4">
<h6 class="mb-1">${doc.filename || 'Document'}</h6>
<small class="text-muted">ID: ${doc.document_id}</small>
</div>
<div class="col-md-2">
<span class="badge status-badge ${this.getStatusClass(doc.status)}">
${this.getStatusText(doc.status)}
</span>
</div>
<div class="col-md-2">
<small class="text-muted">
${new Date(doc.created_at).toLocaleDateString()}
</small>
</div>
<div class="col-md-2">
<div class="btn-group">
<button class="btn btn-sm btn-outline-primary"
onclick="app.viewDocument('${doc.document_id}')">
<i class="fas fa-eye"></i>
</button>
<button class="btn btn-sm btn-outline-success"
onclick="app.downloadDocument('${doc.document_id}')">
<i class="fas fa-download"></i>
</button>
</div>
</div>
</div>
</div>
</div>
`).join('');
container.innerHTML = html;
}
getStatusClass(status) {
const classes = {
'queued': 'bg-warning',
'processing': 'bg-info',
'completed': 'bg-success',
'error': 'bg-danger'
};
return classes[status] || 'bg-secondary';
}
getStatusText(status) {
const texts = {
'queued': 'En attente',
'processing': 'En cours',
'completed': 'Terminé',
'error': 'Erreur'
};
return texts[status] || status;
}
filterDocuments() {
const search = document.getElementById('search-documents').value.toLowerCase();
const statusFilter = document.getElementById('filter-status').value;
const typeFilter = document.getElementById('filter-type').value;
const filtered = this.documents.filter(doc => {
const matchesSearch = !search ||
doc.filename?.toLowerCase().includes(search) ||
doc.document_id.toLowerCase().includes(search);
const matchesStatus = !statusFilter || doc.status === statusFilter;
const matchesType = !typeFilter || doc.type === typeFilter;
return matchesSearch && matchesStatus && matchesType;
});
// Re-render with filtered documents
const originalDocuments = this.documents;
this.documents = filtered;
this.renderDocuments();
this.documents = originalDocuments;
}
async viewDocument(documentId) {
try {
const response = await fetch(`${this.apiUrl}/api/notary/document/${documentId}/analysis`);
const analysis = await response.json();
this.showAnalysisModal(analysis);
} catch (error) {
console.error('Erreur chargement analyse:', error);
this.showAlert('Erreur lors du chargement de l\'analyse', 'danger');
}
}
showAnalysisModal(analysis) {
const content = document.getElementById('analysis-content');
content.innerHTML = `
<div class="row">
<div class="col-md-6">
<h6>Informations Générales</h6>
<div class="mb-3">
<strong>Type détecté:</strong>
<span class="badge bg-primary">${analysis.type_detecte}</span>
</div>
<div class="mb-3">
<strong>Confiance classification:</strong>
${(analysis.confiance_classification * 100).toFixed(1)}%
</div>
<div class="mb-3">
<strong>Score de vraisemblance:</strong>
<span class="badge ${analysis.score_vraisemblance > 0.8 ? 'bg-success' : 'bg-warning'}">
${(analysis.score_vraisemblance * 100).toFixed(1)}%
</span>
</div>
</div>
<div class="col-md-6">
<h6>Entités Extraites</h6>
<div id="entities-list">
${this.renderEntities(analysis.entites_extraites)}
</div>
</div>
</div>
<div class="row mt-4">
<div class="col-md-6">
<h6>Vérifications Externes</h6>
<div id="verifications-list">
${this.renderVerifications(analysis.verifications_externes)}
</div>
</div>
<div class="col-md-6">
<h6>Avis de Synthèse</h6>
<div class="alert alert-info">
${analysis.avis_synthese}
</div>
<h6>Recommandations</h6>
<ul class="list-group">
${analysis.recommandations.map(rec =>
`<li class="list-group-item">${rec}</li>`
).join('')}
</ul>
</div>
</div>
`;
const modal = new bootstrap.Modal(document.getElementById('analysisModal'));
modal.show();
}
renderEntities(entities) {
let html = '';
for (const [type, items] of Object.entries(entities)) {
if (Array.isArray(items) && items.length > 0) {
html += `<h6 class="mt-3">${type.charAt(0).toUpperCase() + type.slice(1)}</h6>`;
items.forEach(item => {
html += `<div class="entity-item">${JSON.stringify(item, null, 2)}</div>`;
});
}
}
return html || '<p class="text-muted">Aucune entité extraite</p>';
}
renderVerifications(verifications) {
let html = '';
for (const [service, result] of Object.entries(verifications)) {
const statusClass = result.status === 'verified' ? 'success' :
result.status === 'error' ? 'error' : 'warning';
html += `
<div class="verification-item ${statusClass}">
<strong>${service}:</strong> ${result.status}
${result.details ? `<br><small>${JSON.stringify(result.details)}</small>` : ''}
</div>
`;
}
return html || '<p class="text-muted">Aucune vérification effectuée</p>';
}
async loadStats() {
try {
const response = await fetch(`${this.apiUrl}/api/notary/stats`);
const stats = await response.json();
document.getElementById('total-documents').textContent = stats.documents_traites || 0;
document.getElementById('processing-documents').textContent = stats.documents_en_cours || 0;
document.getElementById('success-rate').textContent =
`${((stats.taux_reussite || 0) * 100).toFixed(1)}%`;
document.getElementById('avg-time').textContent = `${stats.temps_moyen_traitement || 0}s`;
this.renderCharts(stats);
} catch (error) {
console.error('Erreur chargement stats:', error);
}
}
renderCharts(stats) {
// Détruire les graphiques existants
if (this.charts.documentTypes) {
this.charts.documentTypes.destroy();
}
if (this.charts.timeline) {
this.charts.timeline.destroy();
}
// Document types chart
const typesCtx = document.getElementById('document-types-chart');
if (typesCtx) {
this.charts.documentTypes = new Chart(typesCtx, {
type: 'doughnut',
data: {
labels: Object.keys(stats.types_documents || {}),
datasets: [{
data: Object.values(stats.types_documents || {}),
backgroundColor: [
'#FF6384',
'#36A2EB',
'#FFCE56',
'#4BC0C0',
'#9966FF',
'#FF9F40'
]
}]
},
options: {
responsive: true,
plugins: {
legend: {
position: 'bottom'
}
}
}
});
}
// Timeline chart
const timelineCtx = document.getElementById('timeline-chart');
if (timelineCtx) {
this.charts.timeline = new Chart(timelineCtx, {
type: 'line',
data: {
labels: ['Jan', 'Fév', 'Mar', 'Avr', 'Mai', 'Jun'],
datasets: [{
label: 'Documents traités',
data: [12, 19, 3, 5, 2, 3],
borderColor: '#007bff',
backgroundColor: 'rgba(0, 123, 255, 0.1)',
tension: 0.4
}]
},
options: {
responsive: true,
scales: {
y: {
beginAtZero: true
}
}
}
});
}
}
async checkSystemStatus() {
try {
// Check API
const response = await fetch(`${this.apiUrl}/api/health`);
const health = await response.json();
document.getElementById('api-status').textContent = 'Connecté';
document.getElementById('api-status').className = 'badge bg-success';
// Check LLM (simplified)
document.getElementById('llm-status').textContent = 'Disponible';
document.getElementById('llm-status').className = 'badge bg-success';
// Check external APIs (simplified)
document.getElementById('external-apis-status').textContent = 'OK';
document.getElementById('external-apis-status').className = 'badge bg-success';
} catch (error) {
console.error('Erreur vérification statut:', error);
document.getElementById('api-status').textContent = 'Erreur';
document.getElementById('api-status').className = 'badge bg-danger';
}
}
showAlert(message, type = 'info') {
const alertDiv = document.createElement('div');
alertDiv.className = `alert alert-${type} alert-dismissible fade show`;
alertDiv.innerHTML = `
${message}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
// Insert at the top of main content
const mainContent = document.querySelector('.main-content');
mainContent.insertBefore(alertDiv, mainContent.firstChild);
// Auto-dismiss after 5 seconds
setTimeout(() => {
if (alertDiv.parentNode) {
alertDiv.remove();
}
}, 5000);
}
formatFileSize(bytes) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
async downloadDocument(documentId) {
// Implementation for document download
this.showAlert('Fonctionnalité de téléchargement en cours de développement', 'info');
}
downloadReport() {
// Implementation for report download
this.showAlert('Fonctionnalité de téléchargement de rapport en cours de développement', 'info');
}
}
// Global functions
function testConnection() {
app.checkSystemStatus();
app.showAlert('Test de connexion effectué', 'info');
}
// L'application est maintenant initialisée dans index.html