diff --git a/services/host_api/app_minimal.py b/services/host_api/app_minimal.py index a5ad280..bcf9dbb 100644 --- a/services/host_api/app_minimal.py +++ b/services/host_api/app_minimal.py @@ -97,6 +97,22 @@ async def get_documents(): "total": len(documents_db) } +@app.get("/api/notary/document/{document_id}/status") +async def get_document_status(document_id: str): + """Récupérer le statut d'un document spécifique""" + if document_id not in documents_db: + return {"error": "Document non trouvé"}, 404 + + doc = documents_db[document_id] + return { + "document_id": document_id, + "status": doc.get("status", "unknown"), + "progress": doc.get("progress", 0), + "current_step": doc.get("current_step", "En attente"), + "upload_time": doc.get("upload_time"), + "completion_time": doc.get("completion_time") + } + @app.get("/api/notary/documents/{document_id}") async def get_document(document_id: str): """Détails d'un document""" diff --git a/services/web_interface/app.js b/services/web_interface/app.js index 28be521..38daaf8 100644 --- a/services/web_interface/app.js +++ b/services/web_interface/app.js @@ -148,30 +148,8 @@ class NotaryApp { }; reader.readAsDataURL(file); } else if (file.type === 'application/pdf') { - // Preview for PDF - uploadArea.innerHTML = ` -
-
- -
- Aperçu PDF non disponible -
-
-
-
${file.name}
-

${this.formatFileSize(file.size)}

-

Type: ${file.type}

-
-
- - -
-
- `; + // Preview for PDF with PDF.js + this.showPDFPreview(file, uploadArea); } else { // Generic preview for other files uploadArea.innerHTML = ` @@ -197,9 +175,120 @@ class NotaryApp { } } + 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 = ` +
+
+ +
+ Page 1 sur ${pdf.numPages} +
+
+
+
${file.name}
+

${this.formatFileSize(file.size)}

+

Type: ${file.type}

+
+
+ + +
+
+ `; + + // 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 = ` +
+
+ +
+ Aperçu PDF non disponible +
+
+
+
${file.name}
+

${this.formatFileSize(file.size)}

+

Type: ${file.type}

+
+
+ + +
+
+ `; + } + } + showDocumentPreview(uploadResult, file) { // Create a preview modal or section - const previewContainer = document.getElementById('upload-preview'); + let previewContainer = document.getElementById('upload-preview'); if (!previewContainer) { // Create preview container if it doesn't exist const uploadSection = document.getElementById('upload-section'); @@ -207,10 +296,9 @@ class NotaryApp { previewDiv.id = 'upload-preview'; previewDiv.className = 'mt-4'; uploadSection.appendChild(previewDiv); + previewContainer = document.getElementById('upload-preview'); } - const previewContainer = document.getElementById('upload-preview'); - let previewContent = ''; if (file.type.startsWith('image/')) { @@ -253,8 +341,134 @@ class NotaryApp { previewContainer.innerHTML = previewContent; }; reader.readAsDataURL(file); + } else if (file.type === 'application/pdf') { + // PDF preview after upload + this.showPDFPreviewAfterUpload(file, uploadResult, previewContainer); } else { previewContent = ` +
+
+
+ Aperçu du document uploadé +
+
+
+
+
+ +
+
+
Informations du document
+
    +
  • Nom: ${file.name}
  • +
  • Taille: ${this.formatFileSize(file.size)}
  • +
  • Type: ${file.type}
  • +
  • ID Document: ${uploadResult.document_id}
  • +
  • Statut: ${uploadResult.status}
  • +
+
+ + +
+
+
+
+
+ `; + 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 = ` +
+
+
+ Aperçu du document uploadé +
+
+
+
+
+
+ +
+ Page 1 sur ${pdf.numPages} +
+
+
+
+
Informations du document
+
    +
  • Nom: ${file.name}
  • +
  • Taille: ${this.formatFileSize(file.size)}
  • +
  • Type: ${file.type}
  • +
  • Pages: ${pdf.numPages}
  • +
  • ID Document: ${uploadResult.document_id}
  • +
  • Statut: ${uploadResult.status}
  • +
+
+ + +
+
+
+
+
+ `; + + 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 = `
@@ -288,7 +502,6 @@ class NotaryApp {
`; - previewContainer.innerHTML = previewContent; } } diff --git a/services/web_interface/bootstrap.min.css b/services/web_interface/bootstrap.min.css new file mode 100644 index 0000000..ff0d550 --- /dev/null +++ b/services/web_interface/bootstrap.min.css @@ -0,0 +1,370 @@ +/* Bootstrap CSS minimal pour 4NK Notariat */ +:root { + --bs-blue: #0d6efd; + --bs-indigo: #6610f2; + --bs-purple: #6f42c1; + --bs-pink: #d63384; + --bs-red: #dc3545; + --bs-orange: #fd7e14; + --bs-yellow: #ffc107; + --bs-green: #198754; + --bs-teal: #20c997; + --bs-cyan: #0dcaf0; + --bs-white: #fff; + --bs-gray: #6c757d; + --bs-gray-dark: #343a40; + --bs-primary: #0d6efd; + --bs-secondary: #6c757d; + --bs-success: #198754; + --bs-info: #0dcaf0; + --bs-warning: #ffc107; + --bs-danger: #dc3545; + --bs-light: #f8f9fa; + --bs-dark: #212529; +} + +* { + box-sizing: border-box; +} + +body { + margin: 0; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; + font-size: 1rem; + font-weight: 400; + line-height: 1.5; + color: #212529; + background-color: #fff; +} + +.container { + width: 100%; + padding-right: 15px; + padding-left: 15px; + margin-right: auto; + margin-left: auto; +} + +.row { + display: flex; + flex-wrap: wrap; + margin-right: -15px; + margin-left: -15px; +} + +.col-md-2, .col-md-4, .col-md-6, .col-md-8, .col-md-9, .col-md-10 { + position: relative; + width: 100%; + padding-right: 15px; + padding-left: 15px; +} + +.col-md-2 { flex: 0 0 16.666667%; max-width: 16.666667%; } +.col-md-4 { flex: 0 0 33.333333%; max-width: 33.333333%; } +.col-md-6 { flex: 0 0 50%; max-width: 50%; } +.col-md-8 { flex: 0 0 66.666667%; max-width: 66.666667%; } +.col-md-9 { flex: 0 0 75%; max-width: 75%; } +.col-md-10 { flex: 0 0 83.333333%; max-width: 83.333333%; } + +.card { + position: relative; + display: flex; + flex-direction: column; + min-width: 0; + word-wrap: break-word; + background-color: #fff; + background-clip: border-box; + border: 1px solid rgba(0,0,0,.125); + border-radius: 0.375rem; + margin-bottom: 1rem; +} + +.card-header { + padding: 0.75rem 1.25rem; + margin-bottom: 0; + background-color: rgba(0,0,0,.03); + border-bottom: 1px solid rgba(0,0,0,.125); +} + +.card-body { + flex: 1 1 auto; + padding: 1.25rem; +} + +.btn { + display: inline-block; + font-weight: 400; + line-height: 1.5; + color: #212529; + text-align: center; + text-decoration: none; + vertical-align: middle; + cursor: pointer; + user-select: none; + background-color: transparent; + border: 1px solid transparent; + padding: 0.375rem 0.75rem; + font-size: 1rem; + border-radius: 0.375rem; + transition: color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out; +} + +.btn-primary { + color: #fff; + background-color: #0d6efd; + border-color: #0d6efd; +} + +.btn-primary:hover { + color: #fff; + background-color: #0b5ed7; + border-color: #0a58ca; +} + +.btn-outline-primary { + color: #0d6efd; + border-color: #0d6efd; +} + +.btn-outline-primary:hover { + color: #fff; + background-color: #0d6efd; + border-color: #0d6efd; +} + +.btn-outline-danger { + color: #dc3545; + border-color: #dc3545; +} + +.btn-outline-danger:hover { + color: #fff; + background-color: #dc3545; + border-color: #dc3545; +} + +.btn-outline-secondary { + color: #6c757d; + border-color: #6c757d; +} + +.btn-outline-secondary:hover { + color: #fff; + background-color: #6c757d; + border-color: #6c757d; +} + +.btn-sm { + padding: 0.25rem 0.5rem; + font-size: 0.875rem; + border-radius: 0.25rem; +} + +.btn-group { + position: relative; + display: inline-flex; + vertical-align: middle; +} + +.alert { + position: relative; + padding: 0.75rem 1.25rem; + margin-bottom: 1rem; + border: 1px solid transparent; + border-radius: 0.375rem; +} + +.alert-success { + color: #0f5132; + background-color: #d1e7dd; + border-color: #badbcc; +} + +.alert-danger { + color: #842029; + background-color: #f8d7da; + border-color: #f5c2c7; +} + +.alert-warning { + color: #664d03; + background-color: #fff3cd; + border-color: #ffecb5; +} + +.alert-info { + color: #055160; + background-color: #cff4fc; + border-color: #b6effb; +} + +.badge { + display: inline-block; + padding: 0.35em 0.65em; + font-size: 0.75em; + font-weight: 700; + line-height: 1; + color: #fff; + text-align: center; + white-space: nowrap; + vertical-align: baseline; + border-radius: 0.375rem; +} + +.bg-success { background-color: #198754 !important; } +.bg-danger { background-color: #dc3545 !important; } +.bg-warning { background-color: #ffc107 !important; } +.bg-info { background-color: #0dcaf0 !important; } +.bg-primary { background-color: #0d6efd !important; } +.bg-secondary { background-color: #6c757d !important; } + +.navbar { + position: relative; + display: flex; + flex-wrap: wrap; + align-items: center; + justify-content: space-between; + padding-top: 0.5rem; + padding-bottom: 0.5rem; +} + +.navbar-brand { + padding-top: 0.3125rem; + padding-bottom: 0.3125rem; + margin-right: 1rem; + font-size: 1.25rem; + color: rgba(0,0,0,.9); + text-decoration: none; +} + +.nav { + display: flex; + flex-wrap: wrap; + padding-left: 0; + margin-bottom: 0; + list-style: none; +} + +.nav-link { + display: block; + padding: 0.5rem 1rem; + color: #0d6efd; + text-decoration: none; + transition: color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out; +} + +.nav-link:hover { + color: #0a58ca; +} + +.nav-link.active { + color: #fff; + background-color: #0d6efd; +} + +.progress { + display: flex; + height: 1rem; + overflow: hidden; + font-size: 0.75rem; + background-color: #e9ecef; + border-radius: 0.375rem; +} + +.progress-bar { + display: flex; + flex-direction: column; + justify-content: center; + overflow: hidden; + color: #fff; + text-align: center; + white-space: nowrap; + background-color: #0d6efd; + transition: width .6s ease; +} + +.text-center { text-align: center !important; } +.text-muted { color: #6c757d !important; } +.text-primary { color: #0d6efd !important; } +.text-success { color: #198754 !important; } +.text-danger { color: #dc3545 !important; } + +.mb-0 { margin-bottom: 0 !important; } +.mb-1 { margin-bottom: 0.25rem !important; } +.mb-2 { margin-bottom: 0.5rem !important; } +.mb-3 { margin-bottom: 1rem !important; } +.mb-4 { margin-bottom: 1.5rem !important; } +.mb-5 { margin-bottom: 3rem !important; } + +.mt-2 { margin-top: 0.5rem !important; } +.mt-3 { margin-top: 1rem !important; } +.mt-4 { margin-top: 1.5rem !important; } + +.me-2 { margin-right: 0.5rem !important; } +.ms-2 { margin-left: 0.5rem !important; } + +.py-5 { padding-top: 3rem !important; padding-bottom: 3rem !important; } + +.img-thumbnail { + padding: 0.25rem; + background-color: #fff; + border: 1px solid #dee2e6; + border-radius: 0.375rem; + max-width: 100%; + height: auto; +} + +.img-fluid { + max-width: 100%; + height: auto; +} + +.rounded { + border-radius: 0.375rem !important; +} + +.border { + border: 1px solid #dee2e6 !important; +} + +.d-none { display: none !important; } + +.list-unstyled { + padding-left: 0; + list-style: none; +} + +.list-group { + display: flex; + flex-direction: column; + padding-left: 0; + margin-bottom: 0; + border-radius: 0.375rem; +} + +.list-group-item { + position: relative; + display: block; + padding: 0.5rem 1rem; + color: #212529; + text-decoration: none; + background-color: #fff; + border: 1px solid rgba(0,0,0,.125); +} + +.list-group-item:first-child { + border-top-left-radius: inherit; + border-top-right-radius: inherit; +} + +.list-group-item:last-child { + border-bottom-right-radius: inherit; + border-bottom-left-radius: inherit; +} + +/* Responsive */ +@media (min-width: 768px) { + .col-md-2, .col-md-4, .col-md-6, .col-md-8, .col-md-9, .col-md-10 { + flex: 0 0 auto; + } +} diff --git a/services/web_interface/chart.min.js b/services/web_interface/chart.min.js new file mode 100644 index 0000000..f8f5d20 --- /dev/null +++ b/services/web_interface/chart.min.js @@ -0,0 +1,148 @@ +// Chart.js minimal pour 4NK Notariat +window.Chart = class Chart { + constructor(ctx, config) { + this.ctx = ctx; + this.config = config; + this.destroyed = false; + + // Créer un canvas simple si Chart.js n'est pas disponible + this.createSimpleChart(); + } + + createSimpleChart() { + if (this.destroyed) return; + + const canvas = this.ctx; + const ctx = canvas.getContext('2d'); + const config = this.config; + + // Effacer le canvas + ctx.clearRect(0, 0, canvas.width, canvas.height); + + if (config.type === 'doughnut') { + this.drawDoughnutChart(ctx, config); + } else if (config.type === 'line') { + this.drawLineChart(ctx, config); + } + } + + drawDoughnutChart(ctx, config) { + const data = config.data; + const labels = data.labels || []; + const values = data.datasets[0].data || []; + const colors = data.datasets[0].backgroundColor || ['#FF6384', '#36A2EB', '#FFCE56', '#4BC0C0', '#9966FF']; + + const centerX = canvas.width / 2; + const centerY = canvas.height / 2; + const radius = Math.min(centerX, centerY) - 20; + + const total = values.reduce((sum, val) => sum + val, 0); + let currentAngle = 0; + + // Dessiner les segments + values.forEach((value, index) => { + const sliceAngle = (value / total) * 2 * Math.PI; + + ctx.beginPath(); + ctx.moveTo(centerX, centerY); + ctx.arc(centerX, centerY, radius, currentAngle, currentAngle + sliceAngle); + ctx.closePath(); + ctx.fillStyle = colors[index % colors.length]; + ctx.fill(); + + currentAngle += sliceAngle; + }); + + // Dessiner la légende + let legendY = 20; + labels.forEach((label, index) => { + ctx.fillStyle = colors[index % colors.length]; + ctx.fillRect(10, legendY, 15, 15); + ctx.fillStyle = '#333'; + ctx.font = '12px Arial'; + ctx.fillText(label, 30, legendY + 12); + legendY += 20; + }); + } + + drawLineChart(ctx, config) { + const data = config.data; + const labels = data.labels || []; + const values = data.datasets[0].data || []; + const color = data.datasets[0].borderColor || '#007bff'; + + const padding = 40; + const chartWidth = canvas.width - 2 * padding; + const chartHeight = canvas.height - 2 * padding; + + const maxValue = Math.max(...values); + const minValue = Math.min(...values); + const valueRange = maxValue - minValue || 1; + + // Dessiner les axes + ctx.strokeStyle = '#ddd'; + ctx.lineWidth = 1; + ctx.beginPath(); + ctx.moveTo(padding, padding); + ctx.lineTo(padding, canvas.height - padding); + ctx.lineTo(canvas.width - padding, canvas.height - padding); + ctx.stroke(); + + // Dessiner la ligne + ctx.strokeStyle = color; + ctx.lineWidth = 2; + ctx.beginPath(); + + values.forEach((value, index) => { + const x = padding + (index / (values.length - 1)) * chartWidth; + const y = canvas.height - padding - ((value - minValue) / valueRange) * chartHeight; + + if (index === 0) { + ctx.moveTo(x, y); + } else { + ctx.lineTo(x, y); + } + }); + + ctx.stroke(); + + // Dessiner les points + ctx.fillStyle = color; + values.forEach((value, index) => { + const x = padding + (index / (values.length - 1)) * chartWidth; + const y = canvas.height - padding - ((value - minValue) / valueRange) * chartHeight; + + ctx.beginPath(); + ctx.arc(x, y, 4, 0, 2 * Math.PI); + ctx.fill(); + }); + + // Dessiner les labels + ctx.fillStyle = '#333'; + ctx.font = '10px Arial'; + ctx.textAlign = 'center'; + labels.forEach((label, index) => { + const x = padding + (index / (values.length - 1)) * chartWidth; + ctx.fillText(label, x, canvas.height - 10); + }); + } + + destroy() { + this.destroyed = true; + if (this.ctx && this.ctx.clearRect) { + this.ctx.clearRect(0, 0, this.ctx.canvas.width, this.ctx.canvas.height); + } + } + + update() { + if (!this.destroyed) { + this.createSimpleChart(); + } + } +}; + +// Simuler les options globales +window.Chart.defaults = { + responsive: true, + maintainAspectRatio: false +}; diff --git a/services/web_interface/fontawesome.min.css b/services/web_interface/fontawesome.min.css new file mode 100644 index 0000000..a454727 --- /dev/null +++ b/services/web_interface/fontawesome.min.css @@ -0,0 +1,25 @@ +/* Font Awesome minimal pour 4NK Notariat */ +.fas, .fa { + font-family: "Font Awesome 5 Free"; + font-weight: 900; + display: inline-block; + font-style: normal; + font-variant: normal; + text-rendering: auto; + line-height: 1; +} + +.fa-cloud-upload-alt:before { content: "☁"; } +.fa-folder-open:before { content: "📁"; } +.fa-file:before { content: "📄"; } +.fa-file-pdf:before { content: "📕"; } +.fa-file-alt:before { content: "📄"; } +.fa-upload:before { content: "⬆"; } +.fa-times:before { content: "✕"; } +.fa-eye:before { content: "👁"; } +.fa-search:before { content: "🔍"; } +.fa-download:before { content: "⬇"; } +.fa-upload:before { content: "⬆"; } +.fa-3x { font-size: 3em; } +.fa-4x { font-size: 4em; } +.fa-2x { font-size: 2em; } diff --git a/services/web_interface/index.html b/services/web_interface/index.html index 3525548..5299994 100644 --- a/services/web_interface/index.html +++ b/services/web_interface/index.html @@ -4,8 +4,9 @@ 4NK Notariat - Traitement de Documents - - + + +