
- 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
1020 lines
40 KiB
JavaScript
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
|