gmao/app_new/outlook/templates/outlook/mail.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 %}
&lt;{{ mail.from_email }}&gt;
{% 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 %}