{# templates/upload/review.html.twig #}
{% extends 'admin/baseAdmin.html.twig' %}
{% block title %}Validation IA - Relecture & Correction{% endblock %}
{% block stylesheets %}
{{ parent() }}
<style>
:root {
--ai-primary: linear-gradient(135deg, #1e40af, #3b82f6);
--ai-success: linear-gradient(135deg, #10b981, #34d399);
--ai-warning: linear-gradient(135deg, #f59e0b, #fbbf24);
--ai-danger: linear-gradient(135deg, #dc2626, #ef4444);
--ai-info: linear-gradient(135deg, #0ea5e9, #38bdf8);
--ai-processing: linear-gradient(135deg, #8b5cf6, #a78bfa);
}
.review-container {
background: linear-gradient(180deg, #f8fafc 0%, #f1f5f9 100%);
min-height: 100vh;
}
.header-gradient {
background: var(--ai-primary);
color: white;
border-radius: 20px;
padding: 2rem 2.5rem;
margin-bottom: 2rem;
position: relative;
overflow: hidden;
}
.header-gradient::before {
content: '';
position: absolute;
top: -50%;
right: -20%;
width: 300px;
height: 300px;
background: rgba(255, 255, 255, 0.1);
border-radius: 50%;
filter: blur(40px);
}
.ai-badge {
background: linear-gradient(135deg, #8b5cf6, #a78bfa);
color: white;
padding: 0.5rem 1rem;
border-radius: 50px;
font-weight: 700;
font-size: 0.85rem;
display: inline-flex;
align-items: center;
gap: 0.5rem;
}
.card-ai {
border: none;
border-radius: 20px;
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.08);
background: white;
overflow: hidden;
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.card-ai:hover {
transform: translateY(-5px);
box-shadow: 0 20px 40px -10px rgba(0, 0, 0, 0.12);
}
.card-header-ai {
background: var(--ai-primary);
color: white;
border: none;
padding: 1.5rem 2rem;
border-radius: 20px 20px 0 0 !important;
}
.card-header-secondary {
background: linear-gradient(135deg, #374151, #4b5563);
color: white;
border: none;
padding: 1.5rem 2rem;
border-radius: 20px 20px 0 0 !important;
}
.stat-card-ai {
background: white;
border-radius: 16px;
padding: 1.5rem;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05);
border-left: 5px solid;
height: 100%;
position: relative;
overflow: hidden;
}
.stat-card-ai::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 3px;
background: inherit;
opacity: 0.3;
}
.stat-card-ai.primary { border-left-color: #3b82f6; }
.stat-card-ai.success { border-left-color: #10b981; }
.stat-card-ai.warning { border-left-color: #f59e0b; }
.stat-card-ai.info { border-left-color: #0ea5e9; }
.stat-number-ai {
font-size: 2.5rem;
font-weight: 900;
line-height: 1;
margin-bottom: 0.5rem;
}
.stat-number-ai.primary { color: #3b82f6; }
.stat-number-ai.success { color: #10b981; }
.stat-number-ai.warning { color: #f59e0b; }
.stat-number-ai.info { color: #0ea5e9; }
.stat-icon-ai {
position: absolute;
right: 1.5rem;
top: 1.5rem;
font-size: 2rem;
opacity: 0.2;
}
.document-preview-container-ai {
border: 3px solid white;
border-radius: 16px;
overflow: hidden;
background: white;
height: 280px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: 0 10px 30px -5px rgba(0, 0, 0, 0.1);
position: relative;
}
.document-preview-container-ai:hover {
transform: translateY(-8px) scale(1.02);
box-shadow: 0 25px 50px -12px rgba(59, 130, 246, 0.25);
border-color: #3b82f6;
}
.document-preview-container-ai:hover::after {
content: '🔍 Cliquez pour agrandir';
position: absolute;
bottom: 0;
left: 0;
right: 0;
background: rgba(59, 130, 246, 0.9);
color: white;
text-align: center;
padding: 0.5rem;
font-size: 0.85rem;
font-weight: 600;
}
.document-preview-ai {
max-height: 100%;
max-width: 100%;
object-fit: contain;
transition: transform 0.4s ease;
}
.document-preview-container-ai:hover .document-preview-ai {
transform: scale(1.05);
}
.pdf-icon-ai {
font-size: 5rem;
color: #dc2626;
filter: drop-shadow(0 10px 15px rgba(220, 38, 38, 0.2));
transition: all 0.4s ease;
}
.document-preview-container-ai:hover .pdf-icon-ai {
transform: rotate(-5deg) scale(1.1);
filter: drop-shadow(0 15px 25px rgba(220, 38, 38, 0.3));
}
.ai-process-steps {
background: linear-gradient(135deg, #f0f9ff, #e0f2fe);
border-radius: 16px;
padding: 2rem;
border-left: 5px solid #0ea5e9;
position: relative;
overflow: hidden;
}
.ai-process-steps::before {
content: 'IA';
position: absolute;
top: 1rem;
right: 1rem;
font-size: 3rem;
font-weight: 900;
color: rgba(14, 165, 233, 0.1);
}
.process-step {
display: flex;
align-items: flex-start;
gap: 1rem;
margin-bottom: 1.5rem;
padding: 1rem;
background: white;
border-radius: 12px;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05);
transition: transform 0.2s ease;
}
.process-step:hover {
transform: translateX(5px);
}
.step-number {
width: 36px;
height: 36px;
background: var(--ai-primary);
color: white;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: 900;
flex-shrink: 0;
}
.table-container-ai {
border-radius: 16px;
overflow: hidden;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.08);
}
.table-header-ai {
background: linear-gradient(135deg, #374151, #4b5563);
color: white;
border: none;
position: sticky;
top: 0;
z-index: 10;
}
.table-header-ai th {
border: none;
font-weight: 700;
padding: 1.25rem 1rem;
text-transform: uppercase;
letter-spacing: 0.5px;
font-size: 0.85rem;
}
.table-subheader-ai {
background: #f3f4f6;
border-bottom: 2px solid #e5e7eb;
}
.table-subheader-ai th {
font-size: 0.8rem;
color: #6b7280;
font-weight: 600;
padding: 0.75rem 1rem;
}
.table-row-ai {
background: white;
border-bottom: 1px solid #f3f4f6;
transition: all 0.2s ease;
}
.table-row-ai:hover {
background: #f9fafb;
transform: translateY(-1px);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
z-index: 1;
position: relative;
}
.table-cell-ai {
padding: 1rem;
vertical-align: middle;
border: none;
}
.form-control-ai {
border: 2px solid #e5e7eb;
border-radius: 12px;
padding: 0.75rem 1rem;
font-size: 0.95rem;
transition: all 0.2s ease;
background: white;
}
.form-control-ai:focus {
border-color: #3b82f6;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
transform: translateY(-1px);
}
.time-cell-ai {
background: #f8fafc;
min-width: 100px;
}
.confidence-indicator {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.4rem 0.8rem;
border-radius: 50px;
font-size: 0.8rem;
font-weight: 700;
}
.confidence-high { background: #d1fae5; color: #065f46; }
.confidence-medium { background: #fef3c7; color: #92400e; }
.confidence-low { background: #fee2e2; color: #991b1b; }
.btn-ai-primary {
background: var(--ai-primary);
color: white;
border: none;
padding: 0.875rem 2rem;
border-radius: 12px;
font-weight: 700;
display: inline-flex;
align-items: center;
gap: 0.75rem;
transition: all 0.3s ease;
}
.btn-ai-primary:hover {
transform: translateY(-2px);
box-shadow: 0 10px 25px -5px rgba(59, 130, 246, 0.3);
color: white;
}
.btn-ai-success {
background: var(--ai-success);
color: white;
border: none;
padding: 0.875rem 2rem;
border-radius: 12px;
font-weight: 700;
display: inline-flex;
align-items: center;
gap: 0.75rem;
transition: all 0.3s ease;
}
.btn-ai-success:hover {
transform: translateY(-2px);
box-shadow: 0 10px 25px -5px rgba(16, 185, 129, 0.3);
color: white;
}
.btn-ai-outline {
border: 2px solid #e5e7eb;
background: white;
color: #374151;
padding: 0.875rem 2rem;
border-radius: 12px;
font-weight: 700;
display: inline-flex;
align-items: center;
gap: 0.75rem;
transition: all 0.3s ease;
}
.btn-ai-outline:hover {
border-color: #3b82f6;
color: #3b82f6;
transform: translateY(-2px);
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1);
}
.ai-validation-alert {
background: linear-gradient(135deg, #fef3c7, #fde68a);
border: none;
border-left: 5px solid #f59e0b;
border-radius: 12px;
padding: 1.5rem;
}
.extraction-highlight {
background: linear-gradient(120deg, #dbeafe 0%, #dbeafe 100%);
animation: highlightPulse 2s ease-in-out infinite;
border-radius: 8px;
padding: 2px 4px;
font-weight: 600;
}
@keyframes highlightPulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.8; }
}
.modal-fullscreen-ai .modal-content {
border-radius: 0;
background: #1f2937;
}
.modal-header-ai {
background: #374151;
border-bottom: 1px solid #4b5563;
}
.modal-title-ai {
color: white;
font-weight: 700;
}
.ai-correction-tag {
position: absolute;
top: -10px;
right: 20px;
background: #dc2626;
color: white;
padding: 0.5rem 1rem;
border-radius: 0 0 10px 10px;
font-size: 0.75rem;
font-weight: 700;
z-index: 10;
}
@media (max-width: 768px) {
.header-gradient {
padding: 1.5rem;
border-radius: 16px;
}
.stat-number-ai {
font-size: 2rem;
}
.document-preview-container-ai {
height: 220px;
}
.table-responsive {
font-size: 0.85rem;
}
}
</style>
{% endblock %}
{% block body %}
<div class="container-fluid py-4 review-container">
<!-- En-tête avec IA en vedette -->
<div class="header-gradient">
<div class="row align-items-center">
<div class="col-md-8">
<div class="d-flex align-items-center gap-3 mb-3">
<h1 class="h2 fw-bold mb-0">🔄 Validation IA</h1>
<span class="ai-badge">
<i class="fas fa-robot"></i>
Extraction automatisée
</span>
{# {% if declaration.extractionConfidence %}
<span class="confidence-indicator confidence-{{ declaration.extractionConfidence > 80 ? 'high' : declaration.extractionConfidence > 60 ? 'medium' : 'low' }}">
<i class="fas fa-{{ declaration.extractionConfidence > 80 ? 'chart-line' : declaration.extractionConfidence > 60 ? 'chart-bar' : 'exclamation-triangle' }}"></i>
Fiabilité : {{ declaration.extractionConfidence }}%
</span>
{% endif %} #}
</div>
<p class="mb-0 opacity-90 fs-5">Relecture & correction des données extraites automatiquement par notre IA</p>
</div>
<div class="col-md-4 text-md-end mt-3 mt-md-0">
<div class="d-flex flex-column flex-md-row gap-2 justify-content-md-end">
<span class="info-badge bg-opacity-20 text-white d-inline-flex align-items-center">
<i class="fas fa-file me-2"></i>
{{ batch.originalFilename|length > 25 ? batch.originalFilename[:25] ~ '...' : batch.originalFilename }}
</span>
<a href="{{ path('upload_list') }}" class="btn btn-light btn-sm px-3">
<i class="fas fa-arrow-left me-2"></i>Retour
</a>
</div>
</div>
</div>
</div>
<!-- Cartes statistiques IA -->
<div class="row mb-5">
<div class="col-xl-3 col-md-6 mb-4">
<div class="stat-card-ai primary">
<i class="fas fa-list-ul stat-icon-ai primary"></i>
<div class="stat-number-ai primary">{{ ligneCount }}</div>
<div class="stat-label text-muted mb-2">Lignes détectées</div>
<div class="small text-success">
<i class="fas fa-check-circle me-1"></i>
{{ declaration.correctLines|default(ligneCount) }} correctes
</div>
</div>
</div>
<div class="col-xl-3 col-md-6 mb-4">
<div class="stat-card-ai success">
<i class="fas fa-table stat-icon-ai success"></i>
<div class="stat-number-ai success">{{ declaration.tablesDetected|default('1') }}</div>
<div class="stat-label text-muted mb-2">Structures analysées</div>
<div class="small text-info">
<i class="fas fa-brain me-1"></i>
Reconnaissance IA
</div>
</div>
</div>
<div class="col-xl-3 col-md-6 mb-4">
<div class="stat-card-ai warning">
<i class="fas fa-edit stat-icon-ai warning"></i>
<div class="stat-number-ai warning">{{ declaration.lignes|length }}</div>
<div class="stat-label text-muted mb-2">Lignes à vérifier</div>
<div class="small text-warning">
<i class="fas fa-clock me-1"></i>
{{ declaration.pendingCorrections|default('0') }} corrections
</div>
</div>
</div>
<div class="col-xl-3 col-md-6 mb-4">
<div class="stat-card-ai info">
<i class="fas fa-bolt stat-icon-ai info"></i>
<div class="stat-number-ai info">{{ declaration.processingTime|default('15') }}s</div>
<div class="stat-label text-muted mb-2">Temps de traitement</div>
<div class="small text-primary">
<i class="fas fa-rocket me-1"></i>
Analyse ultra-rapide
</div>
</div>
</div>
</div>
<!-- Document et processus IA -->
<div class="row mb-5">
<div class="col-lg-5 mb-4">
<div class="card-ai h-100">
<div class="card-header-ai">
<h5 class="mb-0">
<i class="fas fa-file-image me-2"></i>
Document original
</h5>
</div>
<div class="card-body p-4">
<!-- Aperçu interactif -->
<div class="document-preview-container-ai mb-4"
data-bs-toggle="modal"
data-bs-target="#documentModal"
title="Cliquez pour visualiser en haute résolution">
{% if batch.extension in ['jpg','jpeg','png','heic','webp','gif','bmp'] %}
<img src="{{ asset('uploads/' ~ batch.storedFilename) }}"
class="document-preview-ai"
alt="Document original analysé par IA"
id="documentPreviewAi">
{% else %}
<div class="text-center">
<i class="fas fa-file-pdf pdf-icon-ai"></i>
<p class="text-dark mt-3 mb-1 fw-semibold">Document PDF analysé</p>
<small class="text-muted">Cliquez pour explorer</small>
</div>
{% endif %}
</div>
<!-- Métriques du document -->
<div class="row g-3">
<div class="col-6">
<div class="bg-light rounded p-3 text-center">
<div class="text-muted small mb-1">Format</div>
<div class="fw-bold text-dark">{{ batch.extension|upper }}</div>
</div>
</div>
<div class="col-6">
<div class="bg-light rounded p-3 text-center">
<div class="text-muted small mb-1">Taille</div>
<div class="fw-bold text-dark">{{ (batch.fileSize / 1024 / 1024)|number_format(2) }} Mo</div>
</div>
</div>
<div class="col-6">
<div class="bg-light rounded p-3 text-center">
<div class="text-muted small mb-1">Date</div>
<div class="fw-bold text-dark">{{ batch.uploadedAt|date('d/m/Y') }}</div>
</div>
</div>
<div class="col-6">
<div class="bg-light rounded p-3 text-center">
<div class="text-muted small mb-1">Heure</div>
<div class="fw-bold text-dark">{{ batch.uploadedAt|date('H:i') }}</div>
</div>
</div>
</div>
<!-- Actions document -->
<div class="d-flex gap-2 mt-4">
<a href="{{ asset('uploads/' ~ batch.storedFilename) }}"
class="btn btn-ai-outline flex-grow-1"
target="_blank"
download="{{ batch.originalFilename }}">
<i class="fas fa-download"></i>
Télécharger
</a>
<button type="button"
class="btn btn-ai-primary"
data-bs-toggle="modal"
data-bs-target="#documentModal">
<i class="fas fa-expand"></i>
Plein écran
</button>
</div>
</div>
</div>
</div>
<div class="col-lg-7 mb-4">
<div class="card-ai h-100">
<div class="card-header-ai">
<h5 class="mb-0">
<i class="fas fa-brain me-2"></i>
Processus d'extraction IA
</h5>
</div>
<div class="card-body p-4">
<div class="ai-process-steps mb-4">
<h6 class="text-dark mb-4 fw-bold">
<i class="fas fa-microchip me-2 text-info"></i>
Comment notre IA a analysé votre document :
</h6>
<div class="process-step">
<div class="step-number">1</div>
<div>
<h6 class="text-dark mb-1 fw-semibold">OCR Avancé</h6>
<p class="text-muted mb-0 small">Reconnaissance optique des caractères pour extraire le texte des images</p>
</div>
</div>
<div class="process-step">
<div class="step-number">2</div>
<div>
<h6 class="text-dark mb-1 fw-semibold">Écriture manuscrite</h6>
<p class="text-muted mb-0 small">Reconnaissance intelligente de l'écriture manuscrite avec 95% de précision</p>
</div>
</div>
<div class="process-step">
<div class="step-number">3</div>
<div>
<h6 class="text-dark mb-1 fw-semibold">Structure des tableaux</h6>
<p class="text-muted mb-0 small">Détection automatique de la structure et des relations entre cellules</p>
</div>
</div>
<div class="process-step">
<div class="step-number">4</div>
<div>
<h6 class="text-dark mb-1 fw-semibold">Validation intelligente</h6>
<p class="text-muted mb-0 small">Vérification cohérence des données et détection des anomalies</p>
</div>
</div>
</div>
<!-- Indicateur de performance IA -->
{# {% if declaration.extractionStats %}
<div class="border rounded p-3 mb-3">
<div class="d-flex justify-content-between align-items-center mb-2">
<span class="text-dark fw-semibold">
<i class="fas fa-chart-bar me-2 text-primary"></i>
Performance de l'extraction
</span>
<span class="badge bg-primary">{{ declaration.extractionConfidence|default('85') }}%</span>
</div>
<div class="progress" style="height: 8px;">
<div class="progress-bar bg-primary" style="width: {{ declaration.extractionConfidence|default('85') }}%"></div>
</div>
<div class="d-flex justify-content-between mt-2">
<small class="text-muted">{{ declaration.extractionStats.words|default('0') }} mots détectés</small>
<small class="text-muted">{{ declaration.extractionStats.tables|default('0') }} tableaux analysés</small>
</div>
</div>
{% endif %} #}
<!-- Alerte validation -->
<div class="ai-validation-alert">
<div class="d-flex">
<div class="flex-shrink-0">
<i class="fas fa-user-check text-warning fa-2x"></i>
</div>
<div class="flex-grow-1 ms-3">
<h6 class="alert-heading fw-bold mb-2">Validation humaine requise</h6>
<p class="mb-0 small">Notre IA a extrait les données avec précision, mais votre expertise est essentielle pour valider et corriger si nécessaire.</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- FORMULAIRE PRINCIPAL -->
<form method="post" action="{{ path('upload_review', {id: batch.id}) }}" id="reviewFormAi" class="position-relative">
<div class="ai-correction-tag">
<i class="fas fa-edit me-1"></i>Mode correction
</div>
<!-- Informations conducteur -->
<div class="card-ai mb-4">
<div class="card-header-secondary">
<h5 class="mb-0">
<i class="fas fa-user-tie me-2"></i>
Informations du conducteur
<small class="opacity-75 ms-2 fw-normal">(Extraites automatiquement)</small>
</h5>
</div>
<div class="card-body p-4">
<div class="row g-3">
<div class="col-md-6">
<label class="form-label fw-bold text-dark mb-2 d-flex align-items-center">
Nom du conducteur
<span class="extraction-highlight ms-2">IA</span>
</label>
<input type="text" name="nom_client" class="form-control-ai"
value="{{ declaration.nomClient }}"
placeholder="Nom et prénom du conducteur"
required>
<small class="text-muted mt-1 d-block">
<i class="fas fa-robot me-1"></i>
Détecté automatiquement depuis le document
</small>
</div>
<div class="col-md-3">
<label class="form-label fw-bold text-dark mb-2">Immatriculation</label>
<input type="text" name="immatriculation" class="form-control-ai"
value="{{ declaration.immatriculation }}"
placeholder="AA-123-BB">
</div>
<div class="col-md-3">
<label class="form-label fw-bold text-dark mb-2">N° licence</label>
<input type="text" name="numero_licence" class="form-control-ai"
value="{{ declaration.numeroLicence }}"
placeholder="N° licence">
</div>
<div class="col-md-4">
<label class="form-label fw-bold text-dark mb-2">Agence</label>
<input type="text" name="agence" class="form-control-ai"
value="{{ declaration.agence }}"
placeholder="Agence rattachée">
</div>
<div class="col-md-4">
<label class="form-label fw-bold text-dark mb-2">Date début</label>
<input type="date" name="date_debut" class="form-control-ai"
value="{{ declaration.dateDebut ? declaration.dateDebut|date('Y-m-d') : '' }}">
</div>
<div class="col-md-4">
<label class="form-label fw-bold text-dark mb-2">Date fin</label>
<input type="date" name="date_fin" class="form-control-ai"
value="{{ declaration.dateFin ? declaration.dateFin|date('Y-m-d') : '' }}">
</div>
</div>
</div>
</div>
<!-- Tableau des lignes journalières -->
<div class="card-ai mb-4">
<div class="card-header-secondary d-flex justify-content-between align-items-center">
<div>
<h5 class="mb-0">
<i class="fas fa-calendar-alt me-2"></i>
Lignes journalières extraites
<span class="badge bg-primary ms-2">{{ ligneCount }} lignes</span>
</h5>
<p class="mb-0 small text-light opacity-75 mt-1">
<i class="fas fa-magic me-1"></i>
Tableau restructuré automatiquement par l'IA
</p>
</div>
<button type="submit" name="add_line" value="1" class="btn btn-light">
<i class="fas fa-plus-circle me-2"></i>
Ajouter ligne
</button>
</div>
<div class="card-body p-0">
<div class="table-container-ai">
<div class="table-responsive">
<table class="table table-bordered mb-0">
<thead>
<tr class="table-header-ai">
<th width="130" class="text-center">📅 Date</th>
<th width="160">📍 Départ</th>
<th width="100" class="text-center">🚗 KM départ</th>
<th colspan="2" class="text-center">🌅 Matin</th>
<th colspan="2" class="text-center">☀️ Midi</th>
<th colspan="2" class="text-center">🌙 Soir</th>
<th colspan="2" class="text-center">⏰ Autre</th>
<th width="160">📍 Arrivée</th>
<th width="100" class="text-center">🚗 KM arrivée</th>
<th width="130" class="text-center">➕ Temps annexes</th>
<th width="130" class="text-center">⏱️ Total</th>
<th width="200">💬 Commentaires</th>
<th width="80" class="text-center">⚙️ Actions</th>
</tr>
<tr class="table-subheader-ai">
<th></th><th></th><th></th>
<th class="text-center"><small>Début</small></th>
<th class="text-center"><small>Fin</small></th>
<th class="text-center"><small>Début</small></th>
<th class="text-center"><small>Fin</small></th>
<th class="text-center"><small>Début</small></th>
<th class="text-center"><small>Fin</small></th>
<th class="text-center"><small>Début</small></th>
<th class="text-center"><small>Fin</small></th>
<th></th><th></th><th></th><th></th><th></th>
</tr>
</thead>
<tbody>
{% for ligne in declaration.lignes %}
<tr class="table-row-ai">
<td class="table-cell-ai">
<input type="date" name="lignes[{{ loop.index0 }}][date]"
class="form-control-ai"
value="{{ ligne.dateJour|date('Y-m-d') }}"
required>
</td>
<td class="table-cell-ai">
<input type="text" name="lignes[{{ loop.index0 }}][lieu_depart]"
class="form-control-ai"
value="{{ ligne.lieuDepart }}"
placeholder="Lieu de départ">
</td>
<td class="table-cell-ai">
<input type="number" name="lignes[{{ loop.index0 }}][km_depart]"
class="form-control-ai text-center"
value="{{ ligne.kmDepart }}"
step="0.1"
placeholder="0.0">
</td>
<!-- Matin -->
<td class="table-cell-ai time-cell-ai">
<input type="time" name="lignes[{{ loop.index0 }}][tranche_1_matin_debut]"
class="form-control-ai"
value="{{ ligne.tranche1MatinDebut }}">
</td>
<td class="table-cell-ai time-cell-ai">
<input type="time" name="lignes[{{ loop.index0 }}][tranche_1_matin_fin]"
class="form-control-ai"
value="{{ ligne.tranche1MatinFin }}">
</td>
<!-- Midi -->
<td class="table-cell-ai time-cell-ai">
<input type="time" name="lignes[{{ loop.index0 }}][tranche_2_midi_debut]"
class="form-control-ai"
value="{{ ligne.tranche2MidiDebut }}">
</td>
<td class="table-cell-ai time-cell-ai">
<input type="time" name="lignes[{{ loop.index0 }}][tranche_2_midi_fin]"
class="form-control-ai"
value="{{ ligne.tranche2MidiFin }}">
</td>
<!-- Soir -->
<td class="table-cell-ai time-cell-ai">
<input type="time" name="lignes[{{ loop.index0 }}][tranche_3_soir_debut]"
class="form-control-ai"
value="{{ ligne.tranche3SoirDebut }}">
</td>
<td class="table-cell-ai time-cell-ai">
<input type="time" name="lignes[{{ loop.index0 }}][tranche_3_soir_fin]"
class="form-control-ai"
value="{{ ligne.tranche3SoirFin }}">
</td>
<!-- Autre -->
<td class="table-cell-ai time-cell-ai">
<input type="time" name="lignes[{{ loop.index0 }}][tranche_4_autre_debut]"
class="form-control-ai"
value="{{ ligne.tranche4AutreDebut }}">
</td>
<td class="table-cell-ai time-cell-ai">
<input type="time" name="lignes[{{ loop.index0 }}][tranche_4_autre_fin]"
class="form-control-ai"
value="{{ ligne.tranche4AutreFin }}">
</td>
<td class="table-cell-ai">
<input type="text" name="lignes[{{ loop.index0 }}][lieu_arrivee]"
class="form-control-ai"
value="{{ ligne.lieuArrivee }}"
placeholder="Lieu d'arrivée">
</td>
<td class="table-cell-ai">
<input type="number" name="lignes[{{ loop.index0 }}][km_arrivee]"
class="form-control-ai text-center"
value="{{ ligne.kmArrivee }}"
step="0.1"
placeholder="0.0">
</td>
<td class="table-cell-ai">
<input type="text" name="lignes[{{ loop.index0 }}][temps_annexes]"
class="form-control-ai text-center"
value="{{ ligne.tempsAnnexes }}"
placeholder="00:00">
</td>
<td class="table-cell-ai">
<input type="text" name="lignes[{{ loop.index0 }}][temps_total]"
class="form-control-ai text-center fw-bold"
value="{{ ligne.tempsTotal }}"
placeholder="00:00">
</td>
<td class="table-cell-ai">
<textarea name="lignes[{{ loop.index0 }}][commentaires]"
class="form-control-ai"
rows="1"
placeholder="Notes éventuelles...">{{ ligne.commentaires }}</textarea>
</td>
<td class="table-cell-ai text-center">
{% if ligneCount > 1 %}
<button type="submit" name="delete_line" value="{{ ligne.id }}"
class="btn btn-outline-danger btn-sm"
onclick="return confirmAI(this)"
title="Supprimer cette ligne"
data-line-date="{{ ligne.dateJour|date('d/m/Y') }}">
<i class="fas fa-trash-alt"></i>
</button>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- Actions finales -->
<div class="card-ai">
<div class="card-body p-4">
<div class="row align-items-center">
<div class="col-md-6 mb-3 mb-md-0">
<div class="d-flex align-items-center gap-2">
<div class="flex-shrink-0">
<i class="fas fa-shield-alt fa-2x text-primary opacity-75"></i>
</div>
<div class="flex-grow-1">
<h6 class="mb-1 fw-bold">Validation sécurisée</h6>
<p class="mb-0 small text-muted">Toutes les modifications sont tracées et sécurisées</p>
</div>
</div>
</div>
<div class="col-md-6">
<div class="d-flex flex-column flex-md-row gap-3 justify-content-md-end">
<a href="{{ path('upload_list') }}" class="btn btn-ai-outline">
<i class="fas fa-arrow-left me-2"></i>
Retour
</a>
<button type="submit" name="save_draft" value="1" class="btn btn-ai-primary">
<i class="fas fa-save me-2"></i>
Sauvegarder brouillon
</button>
<button type="button"
onclick="validateFinalAI()"
class="btn btn-ai-success">
<i class="fas fa-check-double me-2"></i>
Valider définitivement
</button>
</div>
</div>
</div>
</div>
</div>
</form>
</div>
<!-- Modal document plein écran -->
<div class="modal fade modal-fullscreen-ai" id="documentModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-fullscreen">
<div class="modal-content">
<div class="modal-header modal-header-ai">
<h5 class="modal-title modal-title-ai">
<i class="fas fa-file-{% if batch.extension == 'pdf' %}pdf{% else %}image{% endif %} me-2"></i>
{{ batch.originalFilename }}
</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body p-0">
{% if batch.extension == 'pdf' %}
<embed src="{{ asset('uploads/' ~ batch.storedFilename) }}"
type="application/pdf"
class="w-100 h-100">
{% else %}
<div class="d-flex align-items-center justify-content-center w-100 h-100">
<img src="{{ asset('uploads/' ~ batch.storedFilename) }}"
alt="{{ batch.originalFilename }}"
class="img-fluid"
style="max-height: 90vh; object-fit: contain;">
</div>
{% endif %}
<div class="preview-actions">
<a href="{{ asset('uploads/' ~ batch.storedFilename) }}"
class="btn btn-primary"
target="_blank"
download="{{ batch.originalFilename }}">
<i class="fas fa-download me-1"></i>
Télécharger
</a>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
<i class="fas fa-times me-1"></i>
Fermer
</button>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block javascripts %}
{{ parent() }}
<script>
// Fonction de validation finale améliorée
function validateFinalAI() {
const form = document.getElementById('reviewFormAi');
const validateBtn = form.querySelector('button[name="validate_all"]') ||
form.querySelector('[onclick*="validateFinalAI"]');
// Vérifier les champs obligatoires
const requiredFields = form.querySelectorAll('[required]');
const emptyFields = [];
requiredFields.forEach(field => {
if (!field.value.trim()) {
emptyFields.push(field);
field.classList.add('is-invalid');
}
});
if (emptyFields.length > 0) {
// Afficher une notification moderne
if (typeof Swal !== 'undefined') {
Swal.fire({
icon: 'warning',
title: 'Champs manquants',
html: `
<div class="text-start">
<p>Veuillez remplir les <strong>${emptyFields.length}</strong> champ(s) obligatoire(s) :</p>
<ul class="mb-0">
${emptyFields.slice(0, 3).map(field =>
`<li>${field.previousElementSibling?.textContent || 'Champ requis'}</li>`
).join('')}
${emptyFields.length > 3 ? '<li>... et ' + (emptyFields.length - 3) + ' autre(s)</li>' : ''}
</ul>
</div>
`,
confirmButtonText: 'Corriger',
confirmButtonColor: '#3b82f6'
}).then(() => {
emptyFields[0].scrollIntoView({ behavior: 'smooth', block: 'center' });
emptyFields[0].focus();
});
} else {
alert(`Veuillez remplir ${emptyFields.length} champ(s) obligatoire(s) avant de valider.`);
emptyFields[0].focus();
}
return false;
}
// Confirmation avec statistiques
const lineCount = {{ ligneCount }};
const totalHours = calculateTotalHours();
if (typeof Swal !== 'undefined') {
Swal.fire({
title: '✅ Validation définitive',
html: `
<div class="text-start">
<p>Êtes-vous sûr de vouloir valider définitivement cette déclaration ?</p>
<div class="alert alert-light border mt-3">
<div class="row">
<div class="col-6">
<small class="text-muted">Lignes :</small><br>
<strong>${lineCount}</strong>
</div>
<div class="col-6">
<small class="text-muted">Période :</small><br>
<strong>${form.querySelector('[name="date_debut"]').value} → ${form.querySelector('[name="date_fin"]').value}</strong>
</div>
</div>
</div>
<p class="text-danger small mt-3">
<i class="fas fa-exclamation-triangle me-1"></i>
Cette action est irréversible.
</p>
</div>
`,
icon: 'question',
showCancelButton: true,
confirmButtonText: 'Oui, valider',
cancelButtonText: 'Annuler',
confirmButtonColor: '#10b981',
cancelButtonColor: '#6b7280',
reverseButtons: true
}).then((result) => {
if (result.isConfirmed) {
// Créer un bouton temporaire pour la validation
const tempInput = document.createElement('input');
tempInput.type = 'hidden';
tempInput.name = 'validate_all';
tempInput.value = '1';
form.appendChild(tempInput);
// Afficher un loader
Swal.fire({
title: 'Validation en cours...',
text: 'Enregistrement des données validées',
icon: 'info',
showConfirmButton: false,
allowOutsideClick: false,
didOpen: () => {
Swal.showLoading();
// Soumettre après un délai
setTimeout(() => form.submit(), 1000);
}
});
}
});
} else {
if (confirm(`Valider définitivement ${lineCount} lignes ? Cette action est irréversible.`)) {
const tempInput = document.createElement('input');
tempInput.type = 'hidden';
tempInput.name = 'validate_all';
tempInput.value = '1';
form.appendChild(tempInput);
form.submit();
}
}
}
// Calculer le total des heures
function calculateTotalHours() {
const timeInputs = document.querySelectorAll('input[type="time"]');
let totalMinutes = 0;
timeInputs.forEach(input => {
if (input.value) {
const [hours, minutes] = input.value.split(':').map(Number);
totalMinutes += hours * 60 + minutes;
}
});
const hours = Math.floor(totalMinutes / 60);
const minutes = totalMinutes % 60;
return `${hours}h${minutes.toString().padStart(2, '0')}`;
}
// Confirmation améliorée pour la suppression
function confirmAI(button) {
const lineDate = button.dataset.lineDate || 'cette ligne';
if (typeof Swal !== 'undefined') {
Swal.fire({
title: 'Supprimer la ligne ?',
html: `
<div class="text-start">
<p>Vous allez supprimer la ligne du <strong>${lineDate}</strong>.</p>
<p class="text-danger small">
<i class="fas fa-exclamation-triangle me-1"></i>
Cette action ne peut pas être annulée.
</p>
</div>
`,
icon: 'warning',
showCancelButton: true,
confirmButtonText: 'Oui, supprimer',
cancelButtonText: 'Annuler',
confirmButtonColor: '#dc2626',
cancelButtonColor: '#6b7280',
reverseButtons: true
}).then((result) => {
if (result.isConfirmed) {
// Soumettre le formulaire
button.form.submit();
}
});
return false;
} else {
return confirm(`Supprimer la ligne du ${lineDate} ?`);
}
}
// Amélioration de l'UX des champs
document.addEventListener('DOMContentLoaded', function() {
const form = document.getElementById('reviewFormAi');
// Highlight automatique des champs corrigés
const inputs = form.querySelectorAll('input, textarea');
inputs.forEach(input => {
const originalValue = input.value;
input.addEventListener('change', function() {
if (this.value !== originalValue) {
this.style.borderColor = '#10b981';
this.style.boxShadow = '0 0 0 3px rgba(16, 185, 129, 0.1)';
// Ajouter un petit badge "modifié"
const parent = this.parentElement;
if (!parent.querySelector('.modified-badge')) {
const badge = document.createElement('span');
badge.className = 'modified-badge position-absolute';
badge.innerHTML = '<i class="fas fa-pencil-alt"></i>';
badge.style.top = '5px';
badge.style.right = '5px';
badge.style.color = '#10b981';
badge.style.fontSize = '0.7rem';
badge.style.zIndex = '5';
parent.style.position = 'relative';
parent.appendChild(badge);
}
}
});
});
// Navigation clavier améliorée
const tableInputs = form.querySelectorAll('.table-container-ai input, .table-container-ai textarea');
tableInputs.forEach((input, index) => {
input.addEventListener('keydown', function(e) {
if (e.key === 'Enter' && !e.shiftKey && this.tagName !== 'TEXTAREA') {
e.preventDefault();
const nextIndex = index + (e.ctrlKey ? 10 : 1); // Ctrl+Entrée pour sauter 10 champs
if (nextIndex < tableInputs.length) {
tableInputs[nextIndex].focus();
}
} else if (e.key === 'Tab' && !e.shiftKey) {
if (index === tableInputs.length - 1) {
e.preventDefault();
form.querySelector('button[name="save_draft"]').focus();
}
}
});
});
// Calcul automatique du temps total
function setupTimeCalculation() {
const timeCells = form.querySelectorAll('.time-cell-ai input[type="time"]');
timeCells.forEach(cell => {
cell.addEventListener('change', function() {
const row = this.closest('tr');
const timeInputs = row.querySelectorAll('input[type="time"]');
let totalMinutes = 0;
// Calculer le temps total entre début et fin pour chaque tranche
for (let i = 0; i < timeInputs.length; i += 2) {
const start = timeInputs[i].value;
const end = timeInputs[i + 1]?.value;
if (start && end) {
const [startHours, startMinutes] = start.split(':').map(Number);
const [endHours, endMinutes] = end.split(':').map(Number);
const diffMinutes = (endHours * 60 + endMinutes) - (startHours * 60 + startMinutes);
if (diffMinutes > 0) {
totalMinutes += diffMinutes;
}
}
}
// Mettre à jour le champ temps total
if (totalMinutes > 0) {
const hours = Math.floor(totalMinutes / 60);
const minutes = totalMinutes % 60;
const totalCell = row.querySelector('input[name$="[temps_total]"]');
if (totalCell) {
totalCell.value = `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}`;
}
}
});
});
}
setupTimeCalculation();
// Auto-complétion pour les lieux récurrents
const locationInputs = form.querySelectorAll('input[placeholder*="Lieu"]');
const commonLocations = ['Dépôt', 'Garage', 'Siège', 'Client', 'Chantier'];
locationInputs.forEach(input => {
const datalist = document.createElement('datalist');
datalist.id = `locations-${Math.random().toString(36).substr(2, 9)}`;
commonLocations.forEach(location => {
const option = document.createElement('option');
option.value = location;
datalist.appendChild(option);
});
document.body.appendChild(datalist);
input.setAttribute('list', datalist.id);
input.setAttribute('autocomplete', 'on');
});
// Modal amélioré
const documentModal = document.getElementById('documentModal');
if (documentModal) {
documentModal.addEventListener('shown.bs.modal', function() {
// Zoom interactif pour les images
const img = this.querySelector('img');
if (img) {
img.style.cursor = 'zoom-in';
let isZoomed = false;
img.addEventListener('click', function() {
if (!isZoomed) {
this.style.transform = 'scale(1.5)';
this.style.cursor = 'zoom-out';
} else {
this.style.transform = 'scale(1)';
this.style.cursor = 'zoom-in';
}
isZoomed = !isZoomed;
});
}
});
}
// Afficher un message de bienvenue pour la première visite
if (!localStorage.getItem('reviewTutorialShown')) {
setTimeout(() => {
if (typeof Swal !== 'undefined') {
Swal.fire({
title: '👋 Bienvenue dans l\'interface de relecture IA',
html: `
<div class="text-start">
<p>Notre intelligence artificielle a extrait automatiquement les données de votre document.</p>
<div class="alert alert-info mt-3">
<h6><i class="fas fa-lightbulb me-2"></i>Conseils :</h6>
<ul class="mb-0 small">
<li>Vérifiez les champs mis en évidence</li>
<li>Comparez avec le document original</li>
<li>Utilisez Tab/Entrée pour naviguer rapidement</li>
</ul>
</div>
</div>
`,
icon: 'info',
confirmButtonText: 'Commencer',
confirmButtonColor: '#3b82f6'
});
localStorage.setItem('reviewTutorialShown', 'true');
}
}, 1000);
}
});
</script>
{% endblock %}