506 lines
27 KiB
HTML
506 lines
27 KiB
HTML
{% extends "base.html" %}
|
|
{% block title %}{{ mail.subject or '(sans sujet)' }} - {{ account.email }}{% endblock %}
|
|
{% block content %}
|
|
<div class="container-fluid">
|
|
<div class="mb-3">
|
|
{% if mail.folder_id %}
|
|
<a href="{{ url_for('outlook_pages.folder_view', account_id=account.id, folder_id=mail.folder_id) }}" class="btn btn-outline-secondary btn-sm">
|
|
<i class="bi bi-arrow-left"></i> Retour au dossier
|
|
</a>
|
|
{% else %}
|
|
<a href="{{ url_for('outlook_pages.index') }}" class="btn btn-outline-secondary btn-sm">
|
|
<i class="bi bi-arrow-left"></i> Retour aux emails
|
|
</a>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<div class="row">
|
|
<!-- Email à gauche -->
|
|
<div class="col-lg-8">
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<div class="d-flex justify-content-between align-items-start">
|
|
<div>
|
|
<h4 class="mb-1">{{ mail.subject or '(sans sujet)' }}</h4>
|
|
<div class="text-muted">
|
|
<strong>De:</strong> {{ mail.from_name or 'Inconnu' }}
|
|
{% if mail.from_email %}
|
|
<{{ mail.from_email }}>
|
|
{% endif %}
|
|
</div>
|
|
{% if mail.to_recipients %}
|
|
<div class="text-muted small">
|
|
<strong>À:</strong>
|
|
{% set recipients = mail.to_recipients|from_json if mail.to_recipients is string else mail.to_recipients %}
|
|
{{ recipients|join(', ') if recipients is iterable else mail.to_recipients }}
|
|
</div>
|
|
{% endif %}
|
|
{% if mail.cc_recipients %}
|
|
<div class="text-muted small">
|
|
<strong>Cc:</strong>
|
|
{% set cc = mail.cc_recipients|from_json if mail.cc_recipients is string else mail.cc_recipients %}
|
|
{{ cc|join(', ') if cc is iterable else mail.cc_recipients }}
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
<div class="text-end">
|
|
<div class="text-muted small">
|
|
{{ mail.received_at|datetime_fmt if mail.received_at else '-' }}
|
|
</div>
|
|
{% if mail.importance == 'high' %}
|
|
<span class="badge bg-danger">Priorité haute</span>
|
|
{% elif mail.importance == 'low' %}
|
|
<span class="badge bg-secondary">Priorité basse</span>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="card-body">
|
|
{% if mail.body_type == 'html' or (mail.body_content and '<' in mail.body_content[:50]) %}
|
|
<div class="email-content">
|
|
{{ mail.body_content|safe }}
|
|
</div>
|
|
{% else %}
|
|
<pre class="email-content" style="white-space: pre-wrap; font-family: inherit;">{{ mail.body_content or mail.body_preview or '(pas de contenu)' }}</pre>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<!-- Pièces jointes -->
|
|
{% if mail.has_attachments or attachments %}
|
|
<div class="card-footer">
|
|
<h6><i class="bi bi-paperclip"></i> Pièces jointes</h6>
|
|
<div id="attachments-list">
|
|
{% if attachments %}
|
|
{% for att in attachments %}
|
|
<div class="d-inline-block me-2 mb-2">
|
|
<a href="{{ url_for('outlook_sync.download_attachment', att_id=att.id) }}"
|
|
class="btn btn-outline-secondary btn-sm">
|
|
<i class="bi bi-download"></i> {{ att.name }}
|
|
<small class="text-muted">({{ (att.size / 1024)|round(1) }} KB)</small>
|
|
</a>
|
|
</div>
|
|
{% endfor %}
|
|
{% else %}
|
|
<button id="sync-attachments-btn" class="btn btn-sm btn-outline-primary" onclick="syncAttachments()">
|
|
<i class="bi bi-arrow-clockwise"></i> Charger les pièces jointes
|
|
</button>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Panneau Hermes à droite -->
|
|
<div class="col-lg-4">
|
|
<div class="card">
|
|
<div class="card-header d-flex justify-content-between align-items-center">
|
|
<span><i class="bi bi-robot"></i> Interprétation GMAO</span>
|
|
<div class="btn-group btn-group-sm">
|
|
<a href="{{ url_for('outlook_dashboard.context_config') }}" class="btn btn-outline-secondary" title="Configurer le contexte GMAO">
|
|
<i class="bi bi-gear"></i>
|
|
</a>
|
|
<button class="btn btn-outline-primary" onclick="showInterpretForm()">
|
|
<i class="bi bi-plus"></i> Nouvelle
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="card-body">
|
|
<!-- Alerte contexte GMAO -->
|
|
<div class="alert alert-info small mb-3" id="context-alert">
|
|
<i class="bi bi-info-circle"></i>
|
|
<span id="context-status">Chargement du contexte...</span>
|
|
<a href="{{ url_for('outlook_dashboard.context_config') }}" class="alert-link">Modifier</a>
|
|
</div>
|
|
|
|
<!-- Formulaire nouvelle interprétation -->
|
|
<div id="interpret-form" style="display: none;">
|
|
<div class="mb-3">
|
|
<label class="form-label">Contexte additionnel (optionnel)</label>
|
|
<textarea id="user-context" class="form-control" rows="3"
|
|
placeholder="Ajoutez un contexte spécifique pour cet email..."></textarea>
|
|
<div class="form-text">Le contexte GMAO global sera automatiquement inclus.</div>
|
|
</div>
|
|
<button id="interpret-btn" class="btn btn-primary w-100" onclick="interpretMail()">
|
|
<span class="spinner-border spinner-border-sm d-none" id="interpret-spinner"></span>
|
|
<i class="bi bi-magic"></i> Interpréter avec Hermes
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Liste des interprétations -->
|
|
<div id="interpretations-list">
|
|
{% if interpretations %}
|
|
{% for interp in interpretations %}
|
|
<div class="interpretation-card mb-3 p-3 border rounded" id="interp-{{ interp.id }}">
|
|
<div class="d-flex justify-content-between align-items-start mb-2">
|
|
<small class="text-muted">{{ interp.created_at|datetime_fmt }}</small>
|
|
<button class="btn btn-sm btn-outline-danger" onclick="deleteInterpretation({{ interp.id }})">
|
|
<i class="bi bi-trash"></i>
|
|
</button>
|
|
</div>
|
|
{% if interp.user_context %}
|
|
<div class="alert alert-info small mb-2">
|
|
<strong>Contexte:</strong> {{ interp.user_context }}
|
|
</div>
|
|
{% endif %}
|
|
|
|
{% if interp.analysis_type %}
|
|
<!-- Analyse structurée -->
|
|
<div class="analysis-structured mb-2">
|
|
{% if interp.analysis_type == 'curative' %}
|
|
<span class="badge bg-danger mb-2"><i class="bi bi-exclamation-triangle"></i> Curative</span>
|
|
{% elif interp.analysis_type == 'preventive' %}
|
|
<span class="badge bg-primary mb-2"><i class="bi bi-calendar-check"></i> Préventive</span>
|
|
{% elif interp.analysis_type == 'administrative' %}
|
|
<span class="badge bg-warning text-dark mb-2"><i class="bi bi-clipboard-check"></i> Administrative</span>
|
|
{% elif interp.analysis_type == 'formation' %}
|
|
<span class="badge bg-info text-dark mb-2"><i class="bi bi-mortarboard"></i> Formation</span>
|
|
{% else %}
|
|
<span class="badge bg-secondary mb-2"><i class="bi bi-info-circle"></i> Information</span>
|
|
{% endif %}
|
|
|
|
{% if interp.analysis_action == 'create' %}
|
|
<span class="badge bg-success mb-2"><i class="bi bi-plus-circle"></i> Nouvelle intervention</span>
|
|
{% elif interp.analysis_action == 'update' %}
|
|
<span class="badge bg-warning text-dark mb-2"><i class="bi bi-pencil"></i> Mise à jour</span>
|
|
{% else %}
|
|
<span class="badge bg-secondary mb-2"><i class="bi bi-chat"></i> Information</span>
|
|
{% endif %}
|
|
|
|
{% if interp.confidence_score %}
|
|
<span class="badge bg-info text-dark mb-2" title="Score de confiance">
|
|
Confiance: {{ '%.0f'|format(interp.confidence_score * 100) }}%
|
|
</span>
|
|
{% endif %}
|
|
|
|
{% if interp.suggested_title %}
|
|
<div class="mt-2">
|
|
<strong><i class="bi bi-chat-left-text"></i> {{ interp.suggested_title }}</strong>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<div class="row mt-2 small">
|
|
{% if interp.suggested_location %}
|
|
<div class="col-md-6">
|
|
<i class="bi bi-geo-alt"></i> <strong>Lieu:</strong> {{ interp.suggested_location }}
|
|
</div>
|
|
{% endif %}
|
|
{% if interp.suggested_equipment %}
|
|
<div class="col-md-6">
|
|
<i class="bi bi-tools"></i> <strong>Équipement:</strong> {{ interp.suggested_equipment }}
|
|
</div>
|
|
{% endif %}
|
|
{% if interp.suggested_urgency %}
|
|
<div class="col-md-6">
|
|
{% if interp.suggested_urgency == 'urgente' %}
|
|
<span class="text-danger"><i class="bi bi-exclamation-circle"></i> <strong>Urgence:</strong> Urgente</span>
|
|
{% elif interp.suggested_urgency == 'haute' %}
|
|
<span class="text-warning"><i class="bi bi-exclamation-circle"></i> <strong>Urgence:</strong> Haute</span>
|
|
{% elif interp.suggested_urgency == 'normale' %}
|
|
<span class="text-info"><i class="bi bi-exclamation-circle"></i> <strong>Urgence:</strong> Normale</span>
|
|
{% else %}
|
|
<span class="text-muted"><i class="bi bi-exclamation-circle"></i> <strong>Urgence:</strong> Basse</span>
|
|
{% endif %}
|
|
</div>
|
|
{% endif %}
|
|
{% if interp.suggested_assignee %}
|
|
<div class="col-md-6">
|
|
<i class="bi bi-person"></i> <strong>Intervenant:</strong> {{ interp.suggested_assignee }}
|
|
</div>
|
|
{% endif %}
|
|
{% if interp.suggested_date %}
|
|
<div class="col-md-6">
|
|
<i class="bi bi-calendar"></i> <strong>Date suggérée:</strong> {{ interp.suggested_date.strftime('%d/%m/%Y') }}
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
|
|
{% if interp.suggested_description %}
|
|
<div class="mt-2 small text-muted">
|
|
<strong>Description:</strong> {{ interp.suggested_description[:200] }}{% if interp.suggested_description|length > 200 %}...{% endif %}
|
|
</div>
|
|
{% endif %}
|
|
|
|
<!-- Boutons d'action -->
|
|
{% if interp.analysis_action == 'create' and interp.analysis_type == 'administrative' %}
|
|
<div class="mt-3 d-flex gap-2">
|
|
<button class="btn btn-warning btn-sm" onclick="createIntervention({{ interp.id }})">
|
|
<i class="bi bi-clipboard-check"></i> Créer la tâche
|
|
</button>
|
|
<button class="btn btn-outline-secondary btn-sm" onclick="showFullInterpretation({{ interp.id }})">
|
|
<i class="bi bi-eye"></i> Voir l'analyse complète
|
|
</button>
|
|
</div>
|
|
{% elif interp.analysis_action == 'create' %}
|
|
<div class="mt-3 d-flex gap-2">
|
|
<button class="btn btn-success btn-sm" onclick="createIntervention({{ interp.id }})">
|
|
<i class="bi bi-plus-circle"></i> Créer l'intervention
|
|
</button>
|
|
<button class="btn btn-outline-secondary btn-sm" onclick="showFullInterpretation({{ interp.id }})">
|
|
<i class="bi bi-eye"></i> Voir l'analyse complète
|
|
</button>
|
|
</div>
|
|
{% elif interp.analysis_action == 'update' %}
|
|
<div class="mt-3 d-flex gap-2">
|
|
<button class="btn btn-warning btn-sm" onclick="updateIntervention({{ interp.id }})">
|
|
<i class="bi bi-pencil"></i> Mettre à jour
|
|
</button>
|
|
<button class="btn btn-outline-secondary btn-sm" onclick="showFullInterpretation({{ interp.id }})">
|
|
<i class="bi bi-eye"></i> Voir l'analyse complète
|
|
</button>
|
|
</div>
|
|
{% else %}
|
|
{# Type information - pas d'action mais afficher le JSON #}
|
|
<div class="mt-3">
|
|
<button class="btn btn-outline-secondary btn-sm" onclick="showFullInterpretation({{ interp.id }})">
|
|
<i class="bi bi-eye"></i> Voir l'analyse complète
|
|
</button>
|
|
</div>
|
|
{% endif %}
|
|
|
|
{# Afficher la description si présente #}
|
|
{% if interp.suggested_description and interp.suggested_description != 'null' %}
|
|
<div class="mt-2 p-2 bg-light rounded small">
|
|
{{ interp.suggested_description[:300] }}{% if interp.suggested_description|length > 300 %}...{% endif %}
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
{% else %}
|
|
<!-- Ancienne interprétation non structurée -->
|
|
<div class="interpretation-text">
|
|
{{ interp.interpretation|replace('\n', '<br>')|safe }}
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
{% endfor %}
|
|
{% else %}
|
|
<div class="text-center text-muted py-4">
|
|
<i class="bi bi-lightbulb display-4"></i>
|
|
<p class="mt-2">Aucune interprétation</p>
|
|
<button class="btn btn-primary" onclick="showInterpretForm()">
|
|
<i class="bi bi-magic"></i> Interpréter ce mail
|
|
</button>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<style>
|
|
.email-content {
|
|
max-height: 60vh;
|
|
overflow-y: auto;
|
|
}
|
|
.email-content img {
|
|
max-width: 100%;
|
|
height: auto;
|
|
}
|
|
.email-content table {
|
|
max-width: 100%;
|
|
overflow-x: auto;
|
|
display: block;
|
|
}
|
|
.email-content a {
|
|
word-break: break-all;
|
|
}
|
|
.interpretation-card {
|
|
background: #f8f9fa;
|
|
}
|
|
.interpretation-text {
|
|
font-size: 0.9rem;
|
|
line-height: 1.5;
|
|
}
|
|
</style>
|
|
|
|
<script>
|
|
function showInterpretForm() {
|
|
document.getElementById('interpret-form').style.display = 'block';
|
|
document.getElementById('user-context').focus();
|
|
}
|
|
|
|
function syncAttachments() {
|
|
const btn = document.getElementById('sync-attachments-btn');
|
|
btn.disabled = true;
|
|
btn.innerHTML = '<span class="spinner-border spinner-border-sm"></span> Chargement...';
|
|
|
|
fetch('/outlook/api/sync-attachments/{{ mail.id }}', {method: 'POST'})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
location.reload();
|
|
} else {
|
|
alert('Erreur: ' + (data.error || 'Erreur inconnue'));
|
|
btn.disabled = false;
|
|
btn.innerHTML = '<i class="bi bi-arrow-clockwise"></i> Charger les pièces jointes';
|
|
}
|
|
})
|
|
.catch(error => {
|
|
alert('Erreur: ' + error.message);
|
|
btn.disabled = false;
|
|
btn.innerHTML = '<i class="bi bi-arrow-clockwise"></i> Charger les pièces jointes';
|
|
});
|
|
}
|
|
|
|
function interpretMail() {
|
|
const btn = document.getElementById('interpret-btn');
|
|
const spinner = document.getElementById('interpret-spinner');
|
|
const context = document.getElementById('user-context').value;
|
|
|
|
btn.disabled = true;
|
|
spinner.classList.remove('d-none');
|
|
|
|
fetch('/outlook/api/interpret/{{ mail.id }}', {
|
|
method: 'POST',
|
|
headers: {'Content-Type': 'application/json'},
|
|
body: JSON.stringify({context: context})
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
location.reload();
|
|
} else {
|
|
alert('Erreur: ' + (data.error || 'Erreur inconnue'));
|
|
btn.disabled = false;
|
|
spinner.classList.add('d-none');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
alert('Erreur: ' + error.message);
|
|
btn.disabled = false;
|
|
spinner.classList.add('d-none');
|
|
});
|
|
}
|
|
|
|
function deleteInterpretation(id) {
|
|
if (!confirm('Supprimer cette interprétation ?')) return;
|
|
|
|
fetch('/outlook/api/interpretation/' + id, {method: 'DELETE'})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
document.getElementById('interp-' + id).remove();
|
|
}
|
|
});
|
|
}
|
|
|
|
function showFullInterpretation(id) {
|
|
// Récupérer le JSON complet et l'afficher dans une modale
|
|
fetch('/outlook/api/interpretation/' + id)
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success && data.interpretation) {
|
|
// Créer une modale Bootstrap
|
|
let modalHtml = `
|
|
<div class="modal fade" id="jsonModal" tabindex="-1">
|
|
<div class="modal-dialog modal-lg">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">Analyse complète</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<pre style="max-height: 60vh; overflow-y: auto;">${data.interpretation}</pre>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
document.body.insertAdjacentHTML('beforeend', modalHtml);
|
|
let modal = new bootstrap.Modal(document.getElementById('jsonModal'));
|
|
modal.show();
|
|
document.getElementById('jsonModal').addEventListener('hidden.bs.modal', function() {
|
|
this.remove();
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
function createIntervention(interpId) {
|
|
// Rediriger vers le formulaire de création avec les données pré-remplies
|
|
window.location.href = '/interventions/new?from_interpretation=' + interpId;
|
|
}
|
|
|
|
function updateIntervention(interpId) {
|
|
// Chercher les interventions existantes correspondantes
|
|
fetch('/interventions/search?interpretation=' + interpId)
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.interventions && data.interventions.length > 0) {
|
|
// Afficher une liste pour choisir
|
|
let listHtml = '<ul class="list-group">';
|
|
data.interventions.forEach(inv => {
|
|
listHtml += `
|
|
<li class="list-group-item list-group-item-action" onclick="window.location.href='/interventions/${inv.id}/edit?from_interpretation=${interpId}'">
|
|
<strong>${inv.title}</strong><br>
|
|
<small class="text-muted">${inv.location || ''} - ${inv.status}</small>
|
|
</li>
|
|
`;
|
|
});
|
|
listHtml += '</ul>';
|
|
|
|
// Afficher dans une modale
|
|
let modalHtml = `
|
|
<div class="modal fade" id="interventionModal" tabindex="-1">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">Choisir une intervention à mettre à jour</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
${listHtml}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
document.body.insertAdjacentHTML('beforeend', modalHtml);
|
|
let modal = new bootstrap.Modal(document.getElementById('interventionModal'));
|
|
modal.show();
|
|
document.getElementById('interventionModal').addEventListener('hidden.bs.modal', function() {
|
|
this.remove();
|
|
});
|
|
} else {
|
|
alert('Aucune intervention existante trouvée. Créez une nouvelle intervention.');
|
|
window.location.href = '/interventions/create?from_interpretation=' + interpId;
|
|
}
|
|
});
|
|
}
|
|
|
|
// Charger le statut du contexte GMAO au chargement de la page
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
loadContextStatus();
|
|
});
|
|
|
|
function loadContextStatus() {
|
|
fetch('/outlook/api/context')
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
const statusEl = document.getElementById('context-status');
|
|
if (data.full_context && data.full_context.trim()) {
|
|
// Compter les sections remplies
|
|
const sections = [
|
|
data.establishment_name,
|
|
data.technical_context,
|
|
data.equipment_context,
|
|
data.procedure_context,
|
|
data.staff_info,
|
|
data.contact_info,
|
|
data.hermes_instructions
|
|
].filter(s => s && s.trim()).length;
|
|
|
|
statusEl.innerHTML = `<strong>Contexte GMAO configuré</strong> (${sections} section${sections > 1 ? 's' : ''} remplie${sections > 1 ? 's' : ''})`;
|
|
} else {
|
|
statusEl.innerHTML = `<strong class="text-warning">Aucun contexte GMAO configuré</strong> - `;
|
|
}
|
|
})
|
|
.catch(error => {
|
|
const statusEl = document.getElementById('context-status');
|
|
statusEl.innerHTML = '<span class="text-danger">Erreur de chargement du contexte</span>';
|
|
});
|
|
}
|
|
</script>
|
|
{% endblock %}
|