first commit

This commit is contained in:
mrtoine 2025-09-20 13:18:04 +02:00
commit e6c52820cd
227 changed files with 16156 additions and 0 deletions

View file

@ -0,0 +1,323 @@
{% extends 'layouts/base.html' %}
{% block title %}Suite Consultance - Campagne d'email{% endblock %}
{% block module_name %}crm{% endblock %}
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-4">
<h1 class="h2 text-crm">Campagne d'email aux prospects</h1>
<a href="{{ url_for('crm') }}" class="btn btn-outline-crm">
<i class="fas fa-arrow-left"></i> Retour au CRM
</a>
</div>
<div class="card">
<div class="card-body">
<form method="POST" action="{{ url_for('send_bulk_email') }}">
<div class="row g-3">
<!-- Sélection des prospects -->
<div class="col-12">
<h5 class="border-bottom pb-2 mb-3">Sélection des prospects</h5>
</div>
<div class="col-md-6">
<div class="mb-3">
<label class="form-label">Filtre par statut</label>
<div class="d-flex flex-wrap gap-2">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="status_all" name="status_all" checked>
<label class="form-check-label" for="status_all">
Tous
</label>
</div>
<div class="form-check">
<input class="form-check-input status-option" type="checkbox" id="status_nouveau" name="status[]" value="Nouveau">
<label class="form-check-label" for="status_nouveau">
Nouveau
</label>
</div>
<div class="form-check">
<input class="form-check-input status-option" type="checkbox" id="status_contacte" name="status[]" value="Contacté">
<label class="form-check-label" for="status_contacte">
Contacté
</label>
</div>
<div class="form-check">
<input class="form-check-input status-option" type="checkbox" id="status_relance" name="status[]" value="Relancé">
<label class="form-check-label" for="status_relance">
Relancé
</label>
</div>
<div class="form-check">
<input class="form-check-input status-option" type="checkbox" id="status_qualifie" name="status[]" value="Qualifié">
<label class="form-check-label" for="status_qualifie">
Qualifié
</label>
</div>
<div class="form-check">
<input class="form-check-input status-option" type="checkbox" id="status_proposition" name="status[]" value="Proposition">
<label class="form-check-label" for="status_proposition">
Proposition
</label>
</div>
</div>
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label class="form-label">Filtre par tags</label>
<input type="text" class="form-control" id="tags_filter" name="tags" placeholder="ex: web, urgent, pme">
<small class="text-muted">Séparer les tags par des virgules</small>
</div>
</div>
<div class="col-12">
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead>
<tr>
<th width="50">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="select_all_prospects" checked>
</div>
</th>
<th>Nom</th>
<th>Email</th>
<th>Statut</th>
<th>Tags</th>
</tr>
</thead>
<tbody id="prospects_table">
{% for prospect in prospects %}
{# Convertit le statut en chaîne si enum #}
{% set s = prospect.status %}
{% if not (s is string) %}{% set s = s.value %}{% endif %}
<tr class="prospect-row" data-status="{{ s }}" data-tags="{{ prospect.tags|join(',') }}">
<td>
<div class="form-check">
<input class="form-check-input prospect-select" type="checkbox" name="prospect_ids[]" value="{{ prospect.id }}" checked>
</div>
</td>
<td>{{ prospect.name }}</td>
<td>{{ prospect.email }}</td>
<td>
{# Mapping badge #}
{% set cls = 'secondary' %}
{% if s|lower == 'nouveau' %}
{% set cls = 'primary' %}
{% elif s|lower == 'contacté' or s|lower == 'contacte' %}
{% set cls = 'info' %}
{% elif s|lower == 'qualifié' or s|lower == 'qualifie' %}
{% set cls = 'success' %}
{% elif s|lower == 'proposition' %}
{% set cls = 'warning' %}
{% endif %}
<span class="badge bg-{{ cls }}">{{ s }}</span>
</td>
<td>
{% for tag in prospect.tags %}
<span class="badge bg-secondary">{{ tag }}</span>
{% endfor %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<!-- Contenu de l'email -->
<div class="col-12">
<h5 class="border-bottom pb-2 mb-3 mt-3">Contenu de l'email</h5>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="template_id" class="form-label">Template</label>
<select class="form-select" id="template_id" name="template_id">
<option value="">Email personnalisé</option>
{% for template in templates %}
<option value="{{ template.id }}">{{ template.name }}</option>
{% endfor %}
</select>
</div>
</div>
<div class="col-12">
<div class="mb-3">
<label for="subject" class="form-label">Sujet *</label>
<input type="text" class="form-control" id="subject" name="subject" required>
</div>
</div>
<div class="col-12">
<div class="mb-3">
<label for="body" class="form-label">Contenu *</label>
<textarea class="form-control" id="body" name="body" rows="10" required></textarea>
<small class="text-muted">Variables disponibles: {{nom}}, {{entreprise}}, {{email}}. Vous pouvez utiliser du texte formaté HTML.</small>
</div>
</div>
<div class="col-12">
<div class="mb-3">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="update_status" name="update_status" checked>
<label class="form-check-label" for="update_status">
Mettre à jour le statut des prospects
</label>
</div>
</div>
</div>
<div class="col-md-6" id="status_selection">
<div class="mb-3">
<label for="new_status" class="form-label">Nouveau statut</label>
<select class="form-select" id="new_status" name="new_status">
<option value="Contacté">Contacté</option>
<option value="Relancé">Relancé</option>
<option value="Qualifié">Qualifié</option>
<option value="Proposition">Proposition</option>
<option value="Non intéressé">Non intéressé</option>
</select>
</div>
</div>
<div class="col-12 mt-3">
<button type="submit" class="btn btn-primary">
<i class="fas fa-paper-plane"></i> Envoyer la campagne
</button>
<a href="{{ url_for('crm') }}" class="btn btn-outline-secondary">
Annuler
</a>
</div>
</div>
</form>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script>
document.addEventListener('DOMContentLoaded', function() {
const templateSelect = document.getElementById('template_id');
const subjectInput = document.getElementById('subject');
const bodyInput = document.getElementById('body');
const updateStatusCheckbox = document.getElementById('update_status');
const statusSelection = document.getElementById('status_selection');
const selectAllProspects = document.getElementById('select_all_prospects');
const statusAllCheckbox = document.getElementById('status_all');
const statusOptions = document.querySelectorAll('.status-option');
const prospectRows = document.querySelectorAll('.prospect-row');
const tagsFilter = document.getElementById('tags_filter');
// Gestion du choix de template
templateSelect.addEventListener('change', function() {
const templateId = this.value;
if (templateId) {
// Charger le contenu du template
fetch(`/api/email_template/${templateId}`)
.then(response => response.json())
.then(data => {
if (data.success) {
subjectInput.value = data.template.subject;
bodyInput.value = data.template.content;
}
});
}
});
// Afficher/masquer le sélecteur de statut
updateStatusCheckbox.addEventListener('change', function() {
statusSelection.style.display = this.checked ? 'block' : 'none';
});
// Initialiser l'affichage du sélecteur de statut
statusSelection.style.display = updateStatusCheckbox.checked ? 'block' : 'none';
// Sélectionner/désélectionner tous les prospects
selectAllProspects.addEventListener('change', function() {
document.querySelectorAll('.prospect-select:not(:disabled)').forEach(checkbox => {
checkbox.checked = this.checked;
});
});
// Gestion des filtres par statut
statusAllCheckbox.addEventListener('change', function() {
if (this.checked) {
statusOptions.forEach(option => {
option.checked = false;
option.disabled = true;
});
// Afficher tous les prospects
prospectRows.forEach(row => {
row.style.display = '';
row.querySelector('.prospect-select').disabled = false;
});
} else {
statusOptions.forEach(option => {
option.disabled = false;
});
// Appliquer les filtres
applyFilters();
}
});
// Gestion des options de statut individuelles
statusOptions.forEach(option => {
option.addEventListener('change', function() {
applyFilters();
});
});
// Gestion du filtre par tags
tagsFilter.addEventListener('input', function() {
applyFilters();
});
// Fonction pour appliquer les filtres
function applyFilters() {
// Désactiver "Tous" si une option spécifique est choisie
if (Array.from(statusOptions).some(opt => opt.checked)) {
statusAllCheckbox.checked = false;
}
// Récupérer les statuts sélectionnés
const selectedStatuses = Array.from(statusOptions)
.filter(option => option.checked)
.map(option => option.value);
// Récupérer les tags saisis
const tags = tagsFilter.value.toLowerCase().split(',').map(tag => tag.trim()).filter(tag => tag);
// Appliquer les filtres aux lignes
prospectRows.forEach(row => {
const rowStatus = row.getAttribute('data-status');
const rowTags = row.getAttribute('data-tags').toLowerCase().split(',');
let statusMatch = selectedStatuses.length === 0 || selectedStatuses.includes(rowStatus);
let tagsMatch = tags.length === 0 || tags.some(tag => rowTags.includes(tag));
if (statusAllCheckbox.checked) {
statusMatch = true;
}
if (statusMatch && tagsMatch) {
row.style.display = '';
row.querySelector('.prospect-select').disabled = false;
} else {
row.style.display = 'none';
row.querySelector('.prospect-select').disabled = true;
row.querySelector('.prospect-select').checked = false;
}
});
// Mettre à jour "Sélectionner tout" si aucun prospect n'est visible
const visibleProspects = document.querySelectorAll('.prospect-row[style=""]');
selectAllProspects.disabled = visibleProspects.length === 0;
}
});
</script>
{% endblock %}

141
Templates/email/config.html Normal file
View file

@ -0,0 +1,141 @@
{% extends 'layouts/base.html' %}
{% block title %}Suite Consultance - Configuration Email{% endblock %}
{% block module_name %}crm{% endblock %}
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-4">
<h1 class="h2 text-crm">Configuration Email</h1>
<a href="{{ url_for('crm') }}" class="btn btn-outline-crm">
<i class="fas fa-arrow-left"></i> Retour au CRM
</a>
</div>
<div class="card">
<div class="card-body">
<form method="POST" action="{{ url_for('email_config') }}">
<div class="row g-3">
<!-- Configuration SMTP -->
<div class="col-12">
<h5 class="border-bottom pb-2 mb-3">Configuration SMTP</h5>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="smtp_server" class="form-label">Serveur SMTP *</label>
<input type="text" class="form-control" id="smtp_server" name="smtp_server" value="{{ config.smtp_server }}" required>
<small class="text-muted">Exemple: smtp.gmail.com</small>
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="smtp_port" class="form-label">Port SMTP *</label>
<input type="number" class="form-control" id="smtp_port" name="smtp_port" value="{{ config.smtp_port }}" required>
<small class="text-muted">Exemple: 587 (TLS) ou 465 (SSL)</small>
</div>
</div>
<!-- Authentification -->
<div class="col-12">
<h5 class="border-bottom pb-2 mb-3 mt-3">Authentification</h5>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="username" class="form-label">Adresse email *</label>
<input type="email" class="form-control" id="username" name="username" value="{{ config.username }}" required>
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="password" class="form-label">Mot de passe *</label>
<input type="password" class="form-control" id="password" name="password" value="{{ config.password }}" required>
<small class="text-muted">Pour Gmail, utilisez un "mot de passe d'application"</small>
</div>
</div>
<!-- Information d'expéditeur -->
<div class="col-12">
<h5 class="border-bottom pb-2 mb-3 mt-3">Informations d'expéditeur</h5>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="sender_name" class="form-label">Nom d'expéditeur *</label>
<input type="text" class="form-control" id="sender_name" name="sender_name" value="{{ config.sender_name }}" required>
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="sender_email" class="form-label">Email d'expéditeur *</label>
<input type="email" class="form-control" id="sender_email" name="sender_email" value="{{ config.sender_email }}" required>
<small class="text-muted">Doit généralement correspondre à l'adresse email d'authentification</small>
</div>
</div>
<div class="col-12 mt-3">
<button type="submit" class="btn btn-primary">
<i class="fas fa-save"></i> Enregistrer la configuration
</button>
<a href="{{ url_for('crm') }}" class="btn btn-outline-secondary">
Annuler
</a>
<button type="button" id="test_config" class="btn btn-info float-end">
<i class="fas fa-vial"></i> Tester la configuration
</button>
</div>
</div>
</form>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script>
document.addEventListener('DOMContentLoaded', function() {
// Test de la configuration
document.getElementById('test_config').addEventListener('click', function() {
// Récupérer les données du formulaire
const formData = {
smtp_server: document.getElementById('smtp_server').value,
smtp_port: document.getElementById('smtp_port').value,
username: document.getElementById('username').value,
password: document.getElementById('password').value,
sender_name: document.getElementById('sender_name').value,
sender_email: document.getElementById('sender_email').value
};
// Vérifier que tous les champs sont remplis
for (const key in formData) {
if (!formData[key]) {
alert('Veuillez remplir tous les champs avant de tester la configuration.');
return;
}
}
// Envoyer une requête pour tester la configuration
fetch('/api/test_email_config', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(formData)
})
.then(response => response.json())
.then(data => {
if (data.success) {
alert('Test réussi ! La configuration email fonctionne correctement.');
} else {
alert('Échec du test : ' + data.error);
}
})
.catch(error => {
alert('Erreur lors du test : ' + error);
});
});
});
</script>
{% endblock %}

View file

@ -0,0 +1,128 @@
{% extends 'layouts/base.html' %}
{% block title %}Suite Consultance - {% if template %}Modifier{% else %}Créer{% endif %} un template d'email{% endblock %}
{% block module_name %}crm{% endblock %}
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-4">
<h1 class="h2 text-crm">{% if template %}Modifier{% else %}Créer{% endif %} un template d'email</h1>
<a href="{{ url_for('email_templates') }}" class="btn btn-outline-crm">
<i class="fas fa-arrow-left"></i> Retour aux templates
</a>
</div>
<div class="card">
<div class="card-body">
<form method="POST" action="{{ url_for('edit_email_template', template_id=template.id) if template else url_for('create_email_template') }}">
<div class="row g-3">
<!-- Informations du template -->
<div class="col-12">
<h5 class="border-bottom pb-2 mb-3">Informations du template</h5>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="name" class="form-label">Nom du template *</label>
<input type="text" class="form-control" id="name" name="name" value="{{ template.name if template else '' }}" required>
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="description" class="form-label">Description</label>
<input type="text" class="form-control" id="description" name="description" value="{{ template.description if template else '' }}">
</div>
</div>
<!-- Contenu du template -->
<div class="col-12">
<h5 class="border-bottom pb-2 mb-3 mt-3">Contenu du template</h5>
</div>
<div class="col-12">
<div class="mb-3">
<label for="subject" class="form-label">Sujet *</label>
<input type="text" class="form-control" id="subject" name="subject" value="{{ template.subject if template else '' }}" required>
</div>
</div>
<div class="col-12">
<div class="mb-3">
<label for="content" class="form-label">Contenu *</label>
<textarea class="form-control" id="content" name="content" rows="15" required>{{ template.content if template else '' }}</textarea>
<small class="text-muted">Variables disponibles: {{name}}, {{company}}, {{email}}, {{phone}}. Vous pouvez utiliser du texte formaté HTML.</small>
</div>
</div>
<div class="col-12 mt-3">
<button type="submit" class="btn btn-primary">
<i class="fas fa-save"></i> {% if template %}Mettre à jour{% else %}Enregistrer{% endif %} le template
</button>
<a href="{{ url_for('email_templates') }}" class="btn btn-outline-secondary">
Annuler
</a>
</div>
</div>
</form>
</div>
</div>
<!-- Aperçu du template -->
<div class="card mt-4">
<div class="card-header bg-info text-white">
<h5 class="card-title mb-0">Aperçu du template</h5>
</div>
<div class="card-body">
<div class="mb-3">
<h6>Sujet</h6>
<p id="preview_subject" class="border p-2">{{ template.subject if template else 'Votre sujet apparaîtra ici' }}</p>
</div>
<div>
<h6>Contenu</h6>
<div id="preview_content" class="border p-3">
{{ template.content|safe if template else '<p>Votre contenu apparaîtra ici</p>' }}
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script>
document.addEventListener('DOMContentLoaded', function() {
const subjectInput = document.getElementById('subject');
const contentInput = document.getElementById('content');
const previewSubject = document.getElementById('preview_subject');
const previewContent = document.getElementById('preview_content');
// Mise à jour de l'aperçu lors de la saisie
subjectInput.addEventListener('input', updatePreview);
contentInput.addEventListener('input', updatePreview);
function updatePreview() {
// Mettre à jour le sujet
previewSubject.textContent = subjectInput.value || 'Votre sujet apparaîtra ici';
// Mettre à jour le contenu
let content = contentInput.value || '<p>Votre contenu apparaîtra ici</p>';
// Remplacer les variables par des valeurs d'exemple
const replacements = {
'{{name}}': 'Nom du prospect',
'{{company}}': 'Entreprise du prospect',
'{{email}}': 'email@exemple.com',
'{{phone}}': '01 23 45 67 89'
};
for (const [variable, value] of Object.entries(replacements)) {
content = content.replace(new RegExp(variable, 'g'), value);
}
previewContent.innerHTML = content;
}
// Initialiser l'aperçu au chargement
updatePreview();
});
</script>
{% endblock %}

View file

@ -0,0 +1,112 @@
{% extends 'layouts/base.html' %}
{% block title %}Suite Consultance - Historique des emails{% endblock %}
{% block module_name %}crm{% endblock %}
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-4">
<h1 class="h2 text-crm">Historique des emails de {{ prospect.name }}</h1>
<a href="{{ url_for('prospect_details', prospect_id=prospect.id) }}" class="btn btn-outline-crm">
<i class="fas fa-arrow-left"></i> Retour au prospect
</a>
</div>
<div class="card">
<div class="card-body">
{% if emails %}
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead>
<tr>
<th>Date</th>
<th>Sujet</th>
<th>Statut</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for email in emails %}
<tr>
<td>{{ email.timestamp|datetime }}</td>
<td>{{ email.subject }}</td>
<td>
{% if email.success %}
<span class="badge bg-success">Envoyé</span>
{% else %}
<span class="badge bg-danger">Échec</span>
{% endif %}
</td>
<td>
<button type="button" class="btn btn-sm btn-info view-email" data-email='{{ email|tojson }}'>
<i class="fas fa-eye"></i> Voir
</button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="alert alert-info">
Aucun email n'a été envoyé à ce prospect.
</div>
{% endif %}
</div>
</div>
<!-- Modal de visualisation d'email -->
<div class="modal fade" id="emailModal" tabindex="-1" aria-labelledby="emailModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="emailModalLabel">Détails de l'email</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<h6>Date d'envoi</h6>
<p id="email_date"></p>
</div>
<div class="mb-3">
<h6>Destinataire</h6>
<p id="email_recipient"></p>
</div>
<div class="mb-3">
<h6>Sujet</h6>
<p id="email_subject"></p>
</div>
<div>
<h6>Contenu</h6>
<div id="email_content" class="border p-3"></div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Fermer</button>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script>
document.addEventListener('DOMContentLoaded', function() {
// Afficher les détails de l'email dans le modal
document.querySelectorAll('.view-email').forEach(button => {
button.addEventListener('click', function() {
const emailData = JSON.parse(this.getAttribute('data-email'));
// Remplir les champs du modal
document.getElementById('email_date').textContent = new Date(emailData.timestamp).toLocaleString();
document.getElementById('email_recipient').textContent = emailData.to;
document.getElementById('email_subject').textContent = emailData.subject;
document.getElementById('email_content').innerHTML = emailData.content;
// Afficher le modal
const emailModal = new bootstrap.Modal(document.getElementById('emailModal'));
emailModal.show();
});
});
});
</script>
{% endblock %}

View file

@ -0,0 +1,96 @@
{% extends "layouts/base.html" %}
{% block title %}Nouveau scraping d'emails{% endblock %}
{% block content %}
<div class="container-fluid">
<div class="row">
<div class="col-md-8 offset-md-2">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">
<i class="fas fa-search"></i> Nouveau scraping d'emails
</h5>
</div>
<div class="card-body">
<form method="POST" id="scrapingForm">
<div class="mb-3">
<label for="url" class="form-label">URL à scraper *</label>
<input type="url" class="form-control" id="url" name="url"
placeholder="https://exemple.com" required>
<div class="form-text">
Entrez l'URL complète du site web à analyser pour extraire les adresses email.
</div>
</div>
<div class="row">
<div class="col-md-12">
<div class="mb-3">
<label for="max_pages" class="form-label">Nombre de pages à scraper</label>
<select class="form-select" id="max_pages" name="max_pages">
<option value="1">1 page</option>
<option value="3" selected>3 pages</option>
<option value="5">5 pages</option>
<option value="10">10 pages</option>
<option value="20">20 pages</option>
<option value="50">50 pages</option>
</select>
<div class="form-text">
Nombre de pages à analyser avec gestion automatique de la pagination.
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="mb-3">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="auto_create_prospects"
name="auto_create_prospects" checked>
<label class="form-check-label" for="auto_create_prospects">
Créer automatiquement des prospects
</label>
<div class="form-text">
Les contacts trouvés seront automatiquement ajoutés comme nouveaux prospects.
</div>
</div>
</div>
<div class="alert alert-info">
<i class="fas fa-info-circle"></i>
<strong>Informations importantes :</strong>
<ul class="mb-0 mt-2">
<li>Le scrappeur analyse les pages avec gestion automatique de la pagination</li>
<li>Spécialement conçu pour les annuaires d'entreprises et pages de résultats</li>
<li>Extraction automatique des données : nom, entreprise, email, téléphone, localité</li>
<li>Un délai de 2 secondes est appliqué entre chaque page pour éviter la surcharge</li>
<li>Respectez les politiques du site web et les conditions d'utilisation</li>
</ul>
</div>
<div class="d-flex justify-content-between">
<a href="{{ url_for('email_scraper_page') }}" class="btn btn-secondary">
<i class="fas fa-arrow-left"></i> Retour
</a>
<button type="submit" class="btn btn-primary" id="submitBtn">
<i class="fas fa-search"></i> Lancer le scraping
</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
<script>
document.getElementById('scrapingForm').addEventListener('submit', function(e) {
const submitBtn = document.getElementById('submitBtn');
submitBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Scraping en cours...';
submitBtn.disabled = true;
});
</script>
{% endblock %}

View file

@ -0,0 +1,91 @@
{% extends "layouts/base.html" %}
{% block title %}Scrapping d'emails{% endblock %}
{% block content %}
<div class="container-fluid">
<div class="row">
<div class="col-12">
<div class="d-flex justify-content-between align-items-center mb-4">
<h1 class="h3 mb-0">Scrapping d'emails</h1>
<a href="{{ url_for('new_email_scraping') }}" class="btn btn-primary">
<i class="fas fa-plus"></i> Nouveau scraping
</a>
</div>
{% if scrapings %}
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">Historique des scrapings</h5>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>URL</th>
<th>Emails trouvés</th>
<th>Pages scrapées</th>
<th>Date</th>
<th>Statut</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for scraping in scrapings %}
<tr>
<td>
<a href="{{ scraping.url }}" target="_blank" class="text-decoration-none">
{{ scraping.url[:50] }}{% if scraping.url|length > 50 %}...{% endif %}
</a>
</td>
<td>
<span class="badge bg-primary">{{ scraping.emails_count }}</span>
</td>
<td>
<span class="badge bg-info">{{ scraping.pages_count }}</span>
</td>
<td>
{{ scraping.start_time | datetime('%d/%m/%Y %H:%M') }}
</td>
<td>
{% if scraping.errors_count > 0 %}
<span class="badge bg-warning">{{ scraping.errors_count }} erreur(s)</span>
{% else %}
<span class="badge bg-success">Succès</span>
{% endif %}
</td>
<td>
<div class="btn-group btn-group-sm">
<a href="{{ url_for('scraping_results', filename=scraping.filename) }}"
class="btn btn-outline-primary">
<i class="fas fa-eye"></i> Voir
</a>
<a href="{{ url_for('delete_scraping', filename=scraping.filename) }}"
class="btn btn-outline-danger"
onclick="return confirm('Êtes-vous sûr de vouloir supprimer ce scraping ?')">
<i class="fas fa-trash"></i>
</a>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{% else %}
<div class="text-center py-5">
<i class="fas fa-envelope fa-3x text-muted mb-3"></i>
<h4 class="text-muted">Aucun scraping d'email effectué</h4>
<p class="text-muted">Commencez par créer votre premier scraping d'emails.</p>
<a href="{{ url_for('new_email_scraping') }}" class="btn btn-primary">
<i class="fas fa-plus"></i> Nouveau scraping
</a>
</div>
{% endif %}
</div>
</div>
</div>
{% endblock %}

View file

@ -0,0 +1,289 @@
{% extends "layouts/base.html" %}
{% block title %}Résultats du scraping{% endblock %}
{% block content %}
<div class="container-fluid">
<div class="row">
<div class="col-12">
<div class="d-flex justify-content-between align-items-center mb-4">
<h1 class="h3 mb-0">Résultats du scraping</h1>
<a href="{{ url_for('email_scraper_page') }}" class="btn btn-secondary">
<i class="fas fa-arrow-left"></i> Retour
</a>
</div>
<!-- Informations générales -->
<div class="row mb-4">
<div class="col-md-3">
<div class="card text-center">
<div class="card-body">
<h3 class="text-primary">{{ results.contacts|length }}</h3>
<p class="text-muted mb-0">Contacts trouvés</p>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card text-center">
<div class="card-body">
<h3 class="text-info">{{ results.pages_scraped|length }}</h3>
<p class="text-muted mb-0">Pages scrapées</p>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card text-center">
<div class="card-body">
<h3 class="text-warning">{{ results.errors|length }}</h3>
<p class="text-muted mb-0">Erreurs</p>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card text-center">
<div class="card-body">
<small class="text-muted">Durée</small>
<p class="mb-0">
{% if results.start_time and results.end_time %}
{% set start = results.start_time | parse_datetime %}
{% set end = results.end_time | parse_datetime %}
{{ ((end - start).total_seconds() / 60) | round(1) }} min
{% else %}
N/A
{% endif %}
</p>
</div>
</div>
</div>
</div>
<!-- URL source -->
<div class="card mb-4">
<div class="card-header">
<h5 class="card-title mb-0">URL source</h5>
</div>
<div class="card-body">
<a href="{{ results.url }}" target="_blank" class="text-decoration-none">
{{ results.url }}
</a>
</div>
</div>
<!-- Contacts trouvés -->
{% if results.contacts %}
<div class="card mb-4">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="card-title mb-0">Contacts trouvés ({{ results.contacts|length }})</h5>
<div>
<button type="button" class="btn btn-sm btn-outline-primary" onclick="selectAllContacts()">
Tout sélectionner
</button>
<button type="button" class="btn btn-sm btn-primary" onclick="createProspectsFromSelected()">
Créer prospects sélectionnés
</button>
</div>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th width="40"></th>
<th>Email</th>
<th>Nom</th>
<th>Entreprise</th>
<th>Téléphone</th>
<th>Localité</th>
</tr>
</thead>
<tbody>
{% for contact in results.contacts %}
<tr>
<td>
<div class="form-check">
<input class="form-check-input contact-checkbox" type="checkbox"
value="{{ contact.email }}" id="contact_{{ loop.index }}">
</div>
</td>
<td>
<strong>{{ contact.email }}</strong>
</td>
<td>
{% if contact.first_name or contact.last_name %}
{{ contact.first_name }} {{ contact.last_name }}
{% elif contact.name %}
{{ contact.name }}
{% else %}
<span class="text-muted">-</span>
{% endif %}
</td>
<td>
{% if contact.company %}
{{ contact.company }}
{% else %}
<span class="text-muted">-</span>
{% endif %}
</td>
<td>
{% if contact.phone %}
{{ contact.phone }}
{% else %}
<span class="text-muted">-</span>
{% endif %}
</td>
<td>
{% if contact.location %}
{{ contact.location }}
{% else %}
<span class="text-muted">-</span>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{% endif %}
<!-- Pages scrapées -->
<div class="card mb-4">
<div class="card-header">
<h5 class="card-title mb-0">Pages scrapées ({{ results.pages_scraped|length }})</h5>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-sm">
<thead>
<tr>
<th>URL</th>
<th>Niveau</th>
<th>Contacts trouvés</th>
<th>Statut</th>
<th>Heure</th>
</tr>
</thead>
<tbody>
{% for page in results.pages_scraped %}
<tr class="{% if page.status == 'error' %}table-danger{% endif %}">
<td>
<a href="{{ page.url }}" target="_blank" class="text-decoration-none">
{{ page.url[:60] }}{% if page.url|length > 60 %}...{% endif %}
</a>
</td>
<td>
<span class="badge bg-secondary">{{ page.depth }}</span>
</td>
<td>
{% if page.contacts_found %}
<span class="badge bg-primary">{{ page.contacts_found }}</span>
{% if page.contacts and page.contacts|length > 0 %}
<small class="text-muted d-block">
{% for contact in page.contacts[:3] %}
{{ contact.email }}{% if not loop.last %}, {% endif %}
{% endfor %}
{% if page.contacts|length > 3 %}...{% endif %}
</small>
{% endif %}
{% else %}
<span class="badge bg-light text-dark">0</span>
{% endif %}
</td>
<td>
{% if page.status == 'success' %}
<span class="badge bg-success">Succès</span>
{% else %}
<span class="badge bg-danger">Erreur</span>
{% if page.error %}
<small class="text-danger d-block">{{ page.error }}</small>
{% endif %}
{% endif %}
</td>
<td>
<small class="text-muted">
{{ page.timestamp | datetime('%H:%M:%S') }}
</small>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
<!-- Erreurs -->
{% if results.errors %}
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0 text-danger">Erreurs ({{ results.errors|length }})</h5>
</div>
<div class="card-body">
{% for error in results.errors %}
<div class="alert alert-danger mb-2">{{ error }}</div>
{% endfor %}
</div>
</div>
{% endif %}
</div>
</div>
</div>
<script>
function selectAllContacts() {
const checkboxes = document.querySelectorAll('.contact-checkbox');
const allChecked = Array.from(checkboxes).every(cb => cb.checked);
checkboxes.forEach(cb => {
cb.checked = !allChecked;
});
}
function createProspectsFromSelected() {
const selectedEmails = Array.from(document.querySelectorAll('.contact-checkbox:checked'))
.map(cb => cb.value);
if (selectedEmails.length === 0) {
alert('Veuillez sélectionner au moins un contact');
return;
}
if (!confirm(`Créer ${selectedEmails.length} prospect(s) à partir des contacts sélectionnés ?`)) {
return;
}
fetch('/api/scraping/create_prospects', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
filename: '{{ filename }}',
emails: selectedEmails
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
let message = `${data.created} prospect(s) créé(s) avec succès`;
if (data.existing > 0) {
message += ` (${data.existing} contact(s) déjà existant(s))`;
}
alert(message);
// Décocher les contacts traités
document.querySelectorAll('.contact-checkbox:checked').forEach(cb => {
cb.checked = false;
});
} else {
alert(`Erreur: ${data.error}`);
}
})
.catch(error => {
console.error('Erreur:', error);
alert('Erreur lors de la création des prospects');
});
}
</script>
{% endblock %}

View file

@ -0,0 +1,155 @@
{% extends 'layouts/base.html' %}
{% block title %}Suite Consultance - Envoyer un email{% endblock %}
{% block module_name %}crm{% endblock %}
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-4">
<h1 class="h2 text-crm">Envoyer un email à {{ prospect.name }}</h1>
<a href="{{ url_for('prospect_details', prospect_id=prospect.id) }}" class="btn btn-outline-crm">
<i class="fas fa-arrow-left"></i> Retour au prospect
</a>
</div>
<div class="card">
<div class="card-body">
<form method="POST" action="{{ url_for('send_prospect_email', prospect_id=prospect.id) }}">
<div class="row g-3">
<!-- Informations du destinataire -->
<div class="col-12">
<h5 class="border-bottom pb-2 mb-3">Destinataire</h5>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="to_email" class="form-label">Email du destinataire</label>
<input type="email" class="form-control" id="to_email" name="to_email" value="{{ prospect.email }}" required>
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="template_id" class="form-label">Template</label>
<select class="form-select" id="template_id" name="template_id">
<option value="">Email personnalisé</option>
{% for template in templates %}
<option value="{{ template.id }}">{{ template.name }}</option>
{% endfor %}
</select>
</div>
</div>
<!-- Contenu de l'email -->
<div class="col-12">
<h5 class="border-bottom pb-2 mb-3 mt-3">Contenu de l'email</h5>
</div>
<div class="col-12">
<div class="mb-3">
<label for="subject" class="form-label">Sujet *</label>
<input type="text" class="form-control" id="subject" name="subject" required>
</div>
</div>
<div class="col-12">
<div class="mb-3">
<label for="body" class="form-label">Contenu *</label>
<textarea class="form-control" id="body" name="body" rows="10" required></textarea>
<small class="text-muted">Vous pouvez utiliser du texte formaté HTML.</small>
</div>
</div>
<div class="col-12">
<div class="mb-3">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="update_status" name="update_status" checked>
<label class="form-check-label" for="update_status">
Mettre à jour le statut du prospect
</label>
</div>
</div>
</div>
<div class="col-md-6" id="status_selection">
<div class="mb-3">
<label for="new_status" class="form-label">Nouveau statut</label>
<select class="form-select" id="new_status" name="new_status">
<option value="Contacté" {% if prospect.status == 'Contacté' %}selected{% endif %}>Contacté</option>
<option value="Qualifié" {% if prospect.status == 'Qualifié' %}selected{% endif %}>Qualifié</option>
<option value="Proposition" {% if prospect.status == 'Proposition' %}selected{% endif %}>Proposition</option>
<option value="Non intéressé" {% if prospect.status == 'Non intéressé' %}selected{% endif %}>Non intéressé</option>
</select>
</div>
</div>
<div class="col-12 mt-3">
<button type="submit" class="btn btn-primary">
<i class="fas fa-paper-plane"></i> Envoyer l'email
</button>
<a href="{{ url_for('prospect_details', prospect_id=prospect.id) }}" class="btn btn-outline-secondary">
Annuler
</a>
</div>
</div>
</form>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script>
document.addEventListener('DOMContentLoaded', function() {
const templateSelect = document.getElementById('template_id');
const subjectInput = document.getElementById('subject');
const bodyInput = document.getElementById('body');
const updateStatusCheckbox = document.getElementById('update_status');
const statusSelection = document.getElementById('status_selection');
// Gestion du choix de template
templateSelect.addEventListener('change', function() {
const templateId = this.value;
if (templateId) {
// Charger le contenu du template
fetch(`/api/email_template/${templateId}`)
.then(response => response.json())
.then(data => {
if (data.success) {
const template = data.template;
// Remplacer les variables dans le sujet et le contenu
let subject = template.subject;
let content = template.content;
// Variables de remplacement
const replacements = {
'name': '{{ prospect.name }}',
'company': '{{ prospect.company }}',
'email': '{{ prospect.email }}',
'phone': '{{ prospect.phone }}'
};
// Appliquer les remplacements
for (const [key, value] of Object.entries(replacements)) {
// Escape $ character for Jinja by using string concatenation instead of template literals
const placeholder = "{{" + key + "}}";
subject = subject.replace(new RegExp(placeholder, 'g'), value);
content = content.replace(new RegExp(placeholder, 'g'), value);
}
subjectInput.value = subject;
bodyInput.value = content;
}
});
}
});
// Afficher/masquer le sélecteur de statut
updateStatusCheckbox.addEventListener('change', function() {
statusSelection.style.display = this.checked ? 'block' : 'none';
});
// Initialiser l'affichage du sélecteur de statut
statusSelection.style.display = updateStatusCheckbox.checked ? 'block' : 'none';
});
</script>
{% endblock %}

View file

@ -0,0 +1,60 @@
{% extends 'layouts/base.html' %}
{% block title %}Suite Consultance - Templates d'email{% endblock %}
{% block module_name %}crm{% endblock %}
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-4">
<h1 class="h2 text-crm">Templates d'email</h1>
<div>
<a href="{{ url_for('crm') }}" class="btn btn-outline-crm me-2">
<i class="fas fa-arrow-left"></i> Retour au CRM
</a>
<a href="{{ url_for('create_email_template') }}" class="btn btn-primary">
<i class="fas fa-plus"></i> Créer un template
</a>
</div>
</div>
<div class="card">
<div class="card-body">
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead>
<tr>
<th>Nom</th>
<th>Sujet</th>
<th>Description</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% if templates %}
{% for template in templates %}
<tr>
<td>{{ template.name }}</td>
<td>{{ template.subject }}</td>
<td>{{ template.description }}</td>
<td>
<div class="btn-group" role="group">
<a href="{{ url_for('edit_email_template', template_id=template.id) }}" class="btn btn-sm btn-primary" title="Modifier">
<i class="fas fa-edit"></i>
</a>
<a href="{{ url_for('delete_email_template', template_id=template.id) }}" class="btn btn-sm btn-danger delete-confirm" title="Supprimer" onclick="return confirm('Êtes-vous sûr de vouloir supprimer ce template ?');">
<i class="fas fa-trash-alt"></i>
</a>
</div>
</td>
</tr>
{% endfor %}
{% else %}
<tr>
<td colspan="4" class="text-center">Aucun template trouvé</td>
</tr>
{% endif %}
</tbody>
</table>
</div>
</div>
</div>
{% endblock %}