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,136 @@
{% extends 'layouts/base.html' %}
{% block title %}Suite Consultance - Ajouter un client{% 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">Ajouter un nouveau client</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('add_client') }}">
<div class="row g-3">
<!-- Informations client -->
<div class="col-12">
<h5 class="border-bottom pb-2 mb-3">Informations client</h5>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="client_name" class="form-label">Nom du client *</label>
<input type="text" class="form-control" id="client_name" name="client_name" required>
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="email" class="form-label">Email</label>
<input type="email" class="form-control" id="email" name="email">
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="telephone" class="form-label">Téléphone</label>
<input type="tel" class="form-control" id="telephone" name="telephone">
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="adresse" class="form-label">Adresse</label>
<input type="text" class="form-control" id="adresse" name="adresse">
</div>
</div>
<!-- Informations projet -->
<div class="col-12">
<h5 class="border-bottom pb-2 mb-3 mt-3">Informations projet</h5>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="project_name" class="form-label">Nom du projet *</label>
<input type="text" class="form-control" id="project_name" name="project_name" required>
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="project_type" class="form-label">Type de projet</label>
<input type="text" class="form-control" id="project_type" name="project_type">
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="deadline" class="form-label">Date limite</label>
<input type="date" class="form-control" id="deadline" name="deadline">
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="budget" class="form-label">Budget</label>
<input type="text" class="form-control" id="budget" name="budget">
</div>
</div>
<div class="col-12">
<div class="mb-3">
<label for="project_description" class="form-label">Description du projet</label>
<textarea class="form-control" id="project_description" name="project_description" rows="3"></textarea>
</div>
</div>
<div class="col-12">
<div class="mb-3">
<label for="features" class="form-label">Fonctionnalités (séparées par des virgules)</label>
<textarea class="form-control" id="features" name="features" rows="3" placeholder="Fonctionnalité 1, Fonctionnalité 2, ..."></textarea>
</div>
</div>
<!-- Informations complémentaires -->
<div class="col-12">
<h5 class="border-bottom pb-2 mb-3 mt-3">Informations complémentaires</h5>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="payment_terms" class="form-label">Conditions de paiement</label>
<input type="text" class="form-control" id="payment_terms" name="payment_terms">
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="contact_info" class="form-label">Informations de contact</label>
<input type="text" class="form-control" id="contact_info" name="contact_info">
</div>
</div>
<div class="col-12">
<div class="mb-3">
<label for="additional_info" class="form-label">Informations additionnelles</label>
<textarea class="form-control" id="additional_info" name="additional_info" rows="3"></textarea>
</div>
</div>
<div class="col-12 mt-3">
<button type="submit" class="btn btn-primary">
<i class="fas fa-save"></i> Enregistrer le client
</button>
<a href="{{ url_for('crm') }}" class="btn btn-outline-secondary">
Annuler
</a>
</div>
</div>
</form>
</div>
</div>
{% endblock %}

View file

@ -0,0 +1,129 @@
{% extends 'layouts/base.html' %}
{% block title %}Suite Consultance - Ajouter un prospect{% 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">Ajouter un nouveau prospect</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('add_prospect') }}">
<div class="row g-3">
<!-- Informations de base -->
<div class="col-12">
<h5 class="border-bottom pb-2 mb-3">Informations de base</h5>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="name" class="form-label">Nom du prospect *</label>
<input type="text" class="form-control" id="name" name="name" required>
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="company" class="form-label">Entreprise</label>
<input type="text" class="form-control" id="company" name="company">
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="email" class="form-label">Email</label>
<input type="email" class="form-control" id="email" name="email">
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="phone" class="form-label">Téléphone</label>
<input type="tel" class="form-control" id="phone" name="phone">
</div>
</div>
<!-- Source et statut -->
<div class="col-12">
<h5 class="border-bottom pb-2 mb-3 mt-3">Qualification</h5>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="source" class="form-label">Source du prospect</label>
<select class="form-select" id="source" name="source">
<option value="">-- Sélectionner une source --</option>
<option value="Site web">Site web</option>
<option value="Recommandation">Recommandation</option>
<option value="Réseaux sociaux">Réseaux sociaux</option>
<option value="Salon professionnel">Salon professionnel</option>
<option value="Publicité">Publicité</option>
<option value="Autre">Autre</option>
</select>
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="status" class="form-label">Statut</label>
<select class="form-select" id="status" name="status">
<option value="Nouveau">Nouveau</option>
<option value="Contacté">Contacté</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-md-6">
<div class="mb-3">
<label for="last_contact" class="form-label">Dernier contact</label>
<input type="date" class="form-control" id="last_contact" name="last_contact" value="{{ today }}">
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="tags" class="form-label">Tags (séparés par des virgules)</label>
<input type="text" class="form-control" id="tags" name="tags" placeholder="ex: web, urgent, pme">
</div>
</div>
<!-- Informations supplémentaires -->
<div class="col-12">
<h5 class="border-bottom pb-2 mb-3 mt-3">Informations supplémentaires</h5>
</div>
<div class="col-12">
<div class="mb-3">
<label for="notes" class="form-label">Notes</label>
<textarea class="form-control" id="notes" name="notes" rows="3"></textarea>
</div>
</div>
<div class="col-12">
<div class="mb-3">
<label for="next_action" class="form-label">Action suivante</label>
<textarea class="form-control" id="next_action" name="next_action" rows="2"></textarea>
</div>
</div>
<div class="col-12 mt-3">
<button type="submit" class="btn btn-primary">
<i class="fas fa-save"></i> Enregistrer le prospect
</button>
<a href="{{ url_for('crm') }}" class="btn btn-outline-secondary">
Annuler
</a>
</div>
</div>
</form>
</div>
</div>
{% endblock %}

View file

@ -0,0 +1,183 @@
{% extends 'layouts/base.html' %}
{% block title %}Suite Consultance - Détails du client{% endblock %}
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-4">
<h1 class="h2">Détails du client: {{ client.client_name }}</h1>
<div>
<a href="{{ url_for('crm') }}" class="btn btn-outline-secondary me-2">
<i class="fas fa-arrow-left"></i> Retour au CRM
</a>
<div class="btn-group">
<a href="{{ url_for('create_proposition') }}?client_id={{ client.client_name|lower|replace(' ', '_') }}" class="btn btn-success">
<i class="fas fa-file-contract"></i> Nouvelle proposition
</a>
<a href="{{ url_for('create_devis') }}?client_id={{ client.client_name|lower|replace(' ', '_') }}" class="btn btn-warning">
<i class="fas fa-file-invoice-dollar"></i> Nouveau devis
</a>
</div>
</div>
</div>
<div class="row">
<div class="col-md-8">
<!-- Informations principales -->
<div class="card mb-4">
<div class="card-header bg-primary text-white">
<h5 class="card-title mb-0">Informations du client</h5>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6 mb-3">
<h6 class="fw-bold">Nom</h6>
<p>{{ client.client_name }}</p>
</div>
<div class="col-md-6 mb-3">
<h6 class="fw-bold">Email</h6>
<p>{{ client.email or 'Non spécifié' }}</p>
</div>
<div class="col-md-6 mb-3">
<h6 class="fw-bold">Téléphone</h6>
<p>{{ client.telephone or 'Non spécifié' }}</p>
</div>
<div class="col-md-6 mb-3">
<h6 class="fw-bold">Adresse</h6>
<p>{{ client.adresse or 'Non spécifiée' }}</p>
</div>
</div>
</div>
</div>
<!-- Détails du projet -->
<div class="card mb-4">
<div class="card-header bg-primary text-white">
<h5 class="card-title mb-0">Détails du projet</h5>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6 mb-3">
<h6 class="fw-bold">Nom du projet</h6>
<p>{{ client.project_name }}</p>
</div>
<div class="col-md-6 mb-3">
<h6 class="fw-bold">Type de projet</h6>
<p>{{ client.project_type or 'Non spécifié' }}</p>
</div>
<div class="col-md-6 mb-3">
<h6 class="fw-bold">Date limite</h6>
<p>{{ client.deadline or 'Non spécifiée' }}</p>
</div>
<div class="col-md-6 mb-3">
<h6 class="fw-bold">Budget</h6>
<p>{{ client.budget or 'Non spécifié' }}</p>
</div>
</div>
<div class="mb-3">
<h6 class="fw-bold">Description du projet</h6>
<p>{{ client.project_description or 'Aucune description disponible' }}</p>
</div>
<div class="mb-3">
<h6 class="fw-bold">Fonctionnalités</h6>
{% if client.features %}
<ul>
{% for feature in client.features %}
<li>{{ feature.description }}</li>
{% endfor %}
</ul>
{% else %}
<p>Aucune fonctionnalité spécifiée</p>
{% endif %}
</div>
<div class="row">
<div class="col-md-6 mb-3">
<h6 class="fw-bold">Conditions de paiement</h6>
<p>{{ client.payment_terms or 'Non spécifiées' }}</p>
</div>
<div class="col-md-6 mb-3">
<h6 class="fw-bold">Informations de contact</h6>
<p>{{ client.contact_info or 'Non spécifiées' }}</p>
</div>
</div>
<div class="mb-3">
<h6 class="fw-bold">Informations additionnelles</h6>
<p>{{ client.additional_info or 'Aucune information additionnelle' }}</p>
</div>
</div>
</div>
</div>
<div class="col-md-4">
<!-- Documents liés -->
<div class="card mb-4">
<div class="card-header bg-primary text-white">
<h5 class="card-title mb-0">Documents liés</h5>
</div>
<div class="card-body">
<div class="mb-3">
<h6 class="fw-bold">Propositions</h6>
<ul class="list-group">
{% if client.propositions %}
{% for proposition in client.propositions %}
<li class="list-group-item d-flex justify-content-between align-items-center">
{{ proposition.name }}
<a href="{{ url_for('download_file', filename='propositions/' + proposition.filename) }}" class="btn btn-sm btn-outline-primary">
<i class="fas fa-download"></i>
</a>
</li>
{% endfor %}
{% else %}
<li class="list-group-item">Aucune proposition</li>
{% endif %}
</ul>
</div>
<div class="mb-3">
<h6 class="fw-bold">Devis</h6>
<ul class="list-group">
{% if client.devis %}
{% for devis in client.devis %}
<li class="list-group-item d-flex justify-content-between align-items-center">
{{ devis.name }}
<a href="{{ url_for('download_file', filename='devis/' + devis.filename) }}" class="btn btn-sm btn-outline-primary">
<i class="fas fa-download"></i>
</a>
</li>
{% endfor %}
{% else %}
<li class="list-group-item">Aucun devis</li>
{% endif %}
</ul>
</div>
</div>
</div>
<!-- Actions -->
<div class="card">
<div class="card-header bg-primary text-white">
<h5 class="card-title mb-0">Actions</h5>
</div>
<div class="card-body">
<div class="d-grid gap-2">
<a href="{{ url_for('list_client_projects', client_id=client.client_name|lower|replace(' ', '_')) }}" class="btn btn-outline-primary me-2">
<i class="fas fa-diagram-project"></i> Projets
</a>
<a href="{{ url_for('add_client_project', client_id=client.client_name|lower|replace(' ', '_')) }}" class="btn btn-primary">
<i class="fas fa-plus"></i> Nouveau projet
</a>
<button class="btn btn-outline-primary">
<i class="fas fa-edit"></i> Modifier les informations
</button>
<a href="" class="btn btn-outline-danger">
<i class="fas fa-trash-alt"></i> Supprimer le client
</a>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

283
Templates/crm/crm.html Normal file
View file

@ -0,0 +1,283 @@
{% extends 'layouts/base.html' %}
{% block title %}Suite Consultance - CRM{% 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">Gestion de la Relation Client (CRM)</h1>
<a href="{{ url_for('add_client') }}" class="btn btn-crm">
<i class="fas fa-plus"></i> Ajouter un client
</a>
</div>
<ul class="nav nav-tabs mb-4" id="crmTab" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="clients-tab" data-bs-toggle="tab" data-bs-target="#clients-tab-pane" type="button" role="tab" aria-controls="clients-tab-pane" aria-selected="true">
<i class="fas fa-user-tie"></i> Clients
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="prospects-tab" data-bs-toggle="tab" data-bs-target="#prospects-tab-pane" type="button" role="tab" aria-controls="prospects-tab-pane" aria-selected="false">
<i class="fas fa-user-plus"></i> Prospects
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="dashboard-tab" data-bs-toggle="tab" data-bs-target="#dashboard-tab-pane" type="button" role="tab" aria-controls="dashboard-tab-pane" aria-selected="false">
<i class="fas fa-chart-pie"></i> Tableau de bord
</button>
</li>
</ul>
<div class="tab-content" id="crmTabContent">
<!-- Onglet Clients -->
<div class="tab-pane fade show active" id="clients-tab-pane" role="tabpanel" aria-labelledby="clients-tab" tabindex="0">
<div class="card">
<div class="card-body">
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead>
<tr>
<th>Nom</th>
<th>Email</th>
<th>Téléphone</th>
<th>Projet</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% if clients %}
{% for client in clients %}
<tr>
<td>{{ client.name }}</td>
<td>{{ client.email }}</td>
<td>{{ client.phone }}</td>
<td>{{ client.company }}</td>
<td>
<div class="btn-group" role="group">
<a href="{{ url_for('client_details', client_id=client.name|lower|replace(' ', '_')) }}" class="btn btn-sm btn-info" title="Détails">
<i class="fas fa-eye"></i>
</a>
<a href="{{ url_for('create_proposition') }}?client_id={{ client.name|lower|replace(' ', '_') }}" class="btn btn-sm btn-success" title="Créer une proposition">
<i class="fas fa-file-contract"></i>
</a>
<a href="{{ url_for('create_devis') }}?client_id={{ client.name|lower|replace(' ', '_') }}" class="btn btn-sm btn-warning" title="Créer un devis">
<i class="fas fa-file-invoice-dollar"></i>
</a>
</div>
</td>
</tr>
{% endfor %}
{% else %}
<tr>
<td colspan="5" class="text-center">Aucun client trouvé</td>
</tr>
{% endif %}
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- Onglet Prospects -->
<div class="tab-pane fade" id="prospects-tab-pane" role="tabpanel" aria-labelledby="prospects-tab" tabindex="0">
<div class="card">
<div class="card-body">
<div class="mb-3 d-flex justify-content-between">
<div>
<a href="{{ url_for('add_prospect') }}" class="btn btn-outline-primary">
<i class="fas fa-plus"></i> Ajouter un prospect
</a>
</div>
<div>
<a href="{{ url_for('send_bulk_email') }}" class="btn btn-outline-primary me-2">
<i class="fas fa-envelope"></i> Campagne d'emails
</a>
<a href="{{ url_for('email_templates') }}" class="btn btn-outline-info me-2">
<i class="fas fa-file-alt"></i> Templates
</a>
<a href="{{ url_for('email_config') }}" class="btn btn-outline-secondary">
<i class="fas fa-cog"></i> Configuration
</a>
</div>
</div>
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead>
<tr>
<th>Nom</th>
<th>Email</th>
<th>Téléphone</th>
<th>Statut</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% if prospects %}
{% for prospect in prospects %}
<tr>
<td>{{ prospect.name }}</td>
<td>{{ prospect.email }}</td>
<td>{{ prospect.phone }}</td>
<td>
<span class="badge bg-{{ prospect.status|lower == 'nouveau' and 'primary' or (prospect.status|lower == 'contacté' and 'info' or (prospect.status|lower == 'qualifié' and 'success' or (prospect.status|lower == 'proposition' and 'warning' or 'secondary'))) }}">
{{ prospect.status }}
</span>
</td>
<td>
<div class="btn-group" role="group">
<a href="{{ url_for('prospect_details', prospect_id=prospect.id) }}" class="btn btn-sm btn-info" title="Détails">
<i class="fas fa-eye"></i>
</a>
<a href="{{ url_for('edit_prospect', prospect_id=prospect.id) }}" class="btn btn-sm btn-primary" title="Modifier">
<i class="fas fa-edit"></i>
</a>
<a href="{{ url_for('convert_prospect', prospect_id=prospect.id) }}" class="btn btn-sm btn-success" title="Convertir en client" onclick="return confirm('Êtes-vous sûr de vouloir convertir ce prospect en client ?');">
<i class="fas fa-user-check"></i>
</a>
</div>
</td>
</tr>
{% endfor %}
{% else %}
<tr>
<td colspan="5" class="text-center">Aucun prospect trouvé</td>
</tr>
{% endif %}
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- Onglet Tableau de bord -->
<div class="tab-pane fade" id="dashboard-tab-pane" role="tabpanel" aria-labelledby="dashboard-tab" tabindex="0">
<div class="row">
<div class="col-md-6 mb-4">
<div class="card h-100">
<div class="card-header bg-primary text-white">
<h5 class="card-title mb-0">Répartition clients/prospects</h5>
</div>
<div class="card-body">
<canvas id="clientProspectChart" width="400" height="300"></canvas>
<div class="row mt-4">
<div class="col-md-6 text-center">
<div class="h4 mb-0">{{ dashboard_stats.total_clients }}</div>
<div class="small text-muted">Total Clients</div>
</div>
<div class="col-md-6 text-center">
<div class="h4 mb-0">{{ dashboard_stats.total_prospects }}</div>
<div class="small text-muted">Total Prospects</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-6 mb-4">
<div class="card h-100">
<div class="card-header bg-primary text-white">
<h5 class="card-title mb-0">Statut des prospects</h5>
</div>
<div class="card-body">
<canvas id="prospectStatusChart" width="400" height="300"></canvas>
<div class="row mt-3">
{% for status, count in dashboard_stats.prospect_status.items() %}
<div class="col-md-4 text-center mb-2">
<div class="h5 mb-0">{{ count }}</div>
<div class="small text-muted">{{ status }}</div>
</div>
{% endfor %}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Graphique répartition clients/prospects
const clientProspectCtx = document.getElementById('clientProspectChart').getContext('2d');
const clientProspectChart = new Chart(clientProspectCtx, {
type: 'pie',
data: {
labels: ['Clients', 'Prospects'],
datasets: [{
data: [{{ dashboard_stats.total_clients }}, {{ dashboard_stats.total_prospects }}],
backgroundColor: [
'rgba(54, 162, 235, 0.7)',
'rgba(255, 99, 132, 0.7)'
],
borderColor: [
'rgba(54, 162, 235, 1)',
'rgba(255, 99, 132, 1)'
],
borderWidth: 1
}]
},
options: {
responsive: true,
plugins: {
legend: {
position: 'bottom'
}
}
}
});
// Graphique statut des prospects
const prospectStatusCtx = document.getElementById('prospectStatusChart').getContext('2d');
const prospectStatusChart = new Chart(prospectStatusCtx, {
type: 'bar',
data: {
labels: [{% for status in dashboard_stats.prospect_status %}'{{ status }}',{% endfor %}],
datasets: [{
label: 'Nombre de prospects',
data: [{% for status, count in dashboard_stats.prospect_status.items() %}{{ count }},{% endfor %}],
backgroundColor: [
'rgba(255, 99, 132, 0.7)',
'rgba(54, 162, 235, 0.7)',
'rgba(255, 206, 86, 0.7)',
'rgba(75, 192, 192, 0.7)',
'rgba(153, 102, 255, 0.7)'
],
borderColor: [
'rgba(255, 99, 132, 1)',
'rgba(54, 162, 235, 1)',
'rgba(255, 206, 86, 1)',
'rgba(75, 192, 192, 1)',
'rgba(153, 102, 255, 1)'
],
borderWidth: 1
}]
},
options: {
responsive: true,
scales: {
y: {
beginAtZero: true,
ticks: {
precision: 0
}
}
},
plugins: {
legend: {
display: false
}
}
}
});
});
</script>
{% endblock %}

View file

@ -0,0 +1,132 @@
{% extends 'layouts/base.html' %}
{% block title %}Suite Consultance - Modifier un prospect{% endblock %}
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-4">
<h1 class="h2">Modifier le prospect: {{ prospect.name }}</h1>
<a href="{{ url_for('prospect_details', prospect_id=prospect.id) }}" class="btn btn-outline-secondary">
<i class="fas fa-arrow-left"></i> Retour aux détails
</a>
</div>
<div class="card">
<div class="card-body">
<form method="POST" action="{{ url_for('edit_prospect', prospect_id=prospect.id) }}">
<div class="row g-3">
<!-- Informations de base -->
<div class="col-12">
<h5 class="border-bottom pb-2 mb-3">Informations de base</h5>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="name" class="form-label">Nom du prospect *</label>
<input type="text" class="form-control" id="name" name="name" value="{{ prospect.name }}" required>
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="company" class="form-label">Entreprise</label>
<input type="text" class="form-control" id="company" name="company" value="{{ prospect.company }}">
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="email" class="form-label">Email</label>
<input type="email" class="form-control" id="email" name="email" value="{{ prospect.email }}">
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="phone" class="form-label">Téléphone</label>
<input type="tel" class="form-control" id="phone" name="phone" value="{{ prospect.phone }}">
</div>
</div>
<!-- Source et statut -->
<div class="col-12">
<h5 class="border-bottom pb-2 mb-3 mt-3">Qualification</h5>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="source" class="form-label">Source du prospect</label>
<select class="form-select" id="source" name="source">
<option value="">-- Sélectionner une source --</option>
<option value="Site web" {% if prospect.source == 'Site web' %}selected{% endif %}>Site web</option>
<option value="Recommandation" {% if prospect.source == 'Recommandation' %}selected{% endif %}>Recommandation</option>
<option value="Réseaux sociaux" {% if prospect.source == 'Réseaux sociaux' %}selected{% endif %}>Réseaux sociaux</option>
<option value="Salon professionnel" {% if prospect.source == 'Salon professionnel' %}selected{% endif %}>Salon professionnel</option>
<option value="Publicité" {% if prospect.source == 'Publicité' %}selected{% endif %}>Publicité</option>
<option value="Autre" {% if prospect.source == 'Autre' %}selected{% endif %}>Autre</option>
</select>
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="status" class="form-label">Statut</label>
{# Compatibilité enum/chaîne pour le statut #}
{% set s = prospect.status %}
{% if not (s is string) %}{% set s = s.value %}{% endif %}
<select class="form-select" id="status" name="status">
<option value="Nouveau" {% if s == 'Nouveau' %}selected{% endif %}>Nouveau</option>
<option value="Contacté" {% if s == 'Contacté' %}selected{% endif %}>Contacté</option>
<option value="Relancé" {% if s == 'Relancé' %}selected{% endif %}>Relancé</option>
<option value="Qualifié" {% if s == 'Qualifié' %}selected{% endif %}>Qualifié</option>
<option value="Proposition" {% if s == 'Proposition' %}selected{% endif %}>Proposition</option>
<option value="Non intéressé" {% if s == 'Non intéressé' %}selected{% endif %}>Non intéressé</option>
</select>
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="last_contact" class="form-label">Dernier contact</label>
<input type="date" class="form-control" id="last_contact" name="last_contact" value="{{ prospect.last_contact }}">
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="tags" class="form-label">Tags (séparés par des virgules)</label>
<input type="text" class="form-control" id="tags" name="tags" value="{{ prospect.tags|join(', ') }}" placeholder="ex: web, urgent, pme">
</div>
</div>
<!-- Informations supplémentaires -->
<div class="col-12">
<h5 class="border-bottom pb-2 mb-3 mt-3">Informations supplémentaires</h5>
</div>
<div class="col-12">
<div class="mb-3">
<label for="notes" class="form-label">Notes</label>
<textarea class="form-control" id="notes" name="notes" rows="3">{{ prospect.notes }}</textarea>
</div>
</div>
<div class="col-12">
<div class="mb-3">
<label for="next_action" class="form-label">Action suivante</label>
<textarea class="form-control" id="next_action" name="next_action" rows="2">{{ prospect.next_action }}</textarea>
</div>
</div>
<div class="col-12 mt-3">
<button type="submit" class="btn btn-primary">
<i class="fas fa-save"></i> Enregistrer les modifications
</button>
<a href="{{ url_for('prospect_details', prospect_id=prospect.id) }}" class="btn btn-outline-secondary">
Annuler
</a>
</div>
</div>
</form>
</div>
</div>
{% endblock %}

View file

@ -0,0 +1,199 @@
{% extends 'layouts/base.html' %}
{% block title %}Suite Consultance - Détails du prospect{% endblock %}
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-4">
<h1 class="h2">Détails du prospect: {{ prospect.name }}</h1>
<div>
<a href="{{ url_for('crm') }}#prospects" class="btn btn-outline-secondary me-2">
<i class="fas fa-arrow-left"></i> Retour au CRM
</a>
<div class="btn-group">
<a href="{{ url_for('edit_prospect', prospect_id=prospect.id) }}" class="btn btn-primary">
<i class="fas fa-edit"></i> Modifier
</a>
<a href="{{ url_for('convert_prospect', prospect_id=prospect.id) }}" class="btn btn-success" onclick="return confirm('Êtes-vous sûr de vouloir convertir ce prospect en client ?');">
<i class="fas fa-user-check"></i> Convertir en client
</a>
{% with entity_type='prospect', entity_id=prospect.id %}
{% include "partials/task_quick_add_button.html" %}
{% endwith %}
</div>
</div>
</div>
<div class="row">
<div class="col-md-8">
<!-- Informations principales -->
<div class="card mb-4">
<div class="card-header bg-primary text-white">
<h5 class="card-title mb-0">Informations du prospect</h5>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6 mb-3">
<h6 class="fw-bold">Nom</h6>
<p>{{ prospect.name }}</p>
</div>
<div class="col-md-6 mb-3">
<h6 class="fw-bold">Entreprise</h6>
<p>{{ prospect.company or 'Non spécifiée' }}</p>
</div>
<div class="col-md-6 mb-3">
<h6 class="fw-bold">Email</h6>
<p>{{ prospect.email or 'Non spécifié' }}</p>
</div>
<div class="col-md-6 mb-3">
<h6 class="fw-bold">Téléphone</h6>
<p>{{ prospect.phone or 'Non spécifié' }}</p>
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<h6 class="fw-bold">Source</h6>
<p>{{ prospect.source or 'Non spécifiée' }}</p>
</div>
<div class="col-md-6 mb-3">
<h6 class="fw-bold">Statut</h6>
<p>
<span class="badge bg-{{ prospect.status|lower == 'nouveau' and 'primary' or (prospect.status|lower == 'contacté' and 'info' or (prospect.status|lower == 'qualifié' and 'success' or (prospect.status|lower == 'proposition' and 'warning' or 'secondary'))) }}">
{{ prospect.status }}
</span>
</p>
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<h6 class="fw-bold">Date de création</h6>
<p>{{ prospect.created_at }}</p>
</div>
<div class="col-md-6 mb-3">
<h6 class="fw-bold">Dernier contact</h6>
<p>{{ prospect.last_contact }}</p>
</div>
</div>
<div class="mb-3">
<h6 class="fw-bold">Tags</h6>
{% if prospect.tags %}
{% for tag in prospect.tags %}
<span class="badge bg-secondary me-1">{{ tag }}</span>
{% endfor %}
{% else %}
<p>Aucun tag</p>
{% endif %}
</div>
</div>
</div>
<!-- Notes et actions -->
<div class="card mb-4">
<div class="card-header bg-primary text-white">
<h5 class="card-title mb-0">Notes et actions</h5>
</div>
<div class="card-body">
<div class="mb-4">
<h6 class="fw-bold">Notes</h6>
<p>{{ prospect.notes or 'Aucune note disponible' }}</p>
</div>
<div class="mb-3">
<h6 class="fw-bold">Action suivante</h6>
<p>{{ prospect.next_action or 'Aucune action prévue' }}</p>
</div>
</div>
</div>
</div>
<div class="col-md-4">
<!-- Documents liés -->
<div class="card mb-4">
<div class="card-header bg-primary text-white">
<h5 class="card-title mb-0">Documents liés</h5>
</div>
<div class="card-body">
<div class="mb-3">
<h6 class="fw-bold">Propositions</h6>
<ul class="list-group">
{% if prospect.linked_docs and prospect.linked_docs.propositions %}
{% for proposition in prospect.linked_docs.propositions %}
<li class="list-group-item d-flex justify-content-between align-items-center">
{{ proposition.name }}
<a href="{{ url_for('download_file', filename='propositions/' + proposition.filename) }}" class="btn btn-sm btn-outline-primary">
<i class="fas fa-download"></i>
</a>
</li>
{% endfor %}
{% else %}
<li class="list-group-item">Aucune proposition</li>
{% endif %}
</ul>
</div>
</div>
</div>
<!-- Historique des emails récents -->
<div class="card mb-4">
<div class="card-header bg-primary text-white">
<h5 class="card-title mb-0">Derniers emails</h5>
</div>
<div class="card-body">
{% if email_history and email_history|length > 0 %}
<ul class="list-group">
{% for email in email_history[:5] %}
<li class="list-group-item d-flex justify-content-between align-items-center">
<div>
<span class="fw-bold">{{ email.subject }}</span>
<br>
<small class="text-muted">{{ email.timestamp|datetime }}</small>
</div>
<span class="badge bg-{% if email.success %}success{% else %}danger{% endif %}">
{% if email.success %}Envoyé{% else %}Échec{% endif %}
</span>
</li>
{% endfor %}
</ul>
{% if email_history|length > 5 %}
<div class="mt-2 text-center">
<a href="{{ url_for('prospect_email_history', prospect_id=prospect.id) }}" class="btn btn-sm btn-outline-primary">
Voir tout l'historique
</a>
</div>
{% endif %}
{% else %}
<p class="text-center">Aucun email envoyé à ce prospect.</p>
{% endif %}
</div>
</div>
<!-- Actions -->
<div class="card">
<div class="card-header bg-primary text-white">
<h5 class="card-title mb-0">Actions</h5>
</div>
<div class="card-body">
<div class="d-grid gap-2">
<a href="{{ url_for('send_prospect_email', prospect_id=prospect.id) }}" class="btn btn-outline-primary">
<i class="fas fa-envelope"></i> Envoyer un email
</a>
<a href="{{ url_for('prospect_email_history', prospect_id=prospect.id) }}" class="btn btn-outline-info">
<i class="fas fa-history"></i> Historique des emails
</a>
<a href="{{ url_for('edit_prospect', prospect_id=prospect.id) }}" class="btn btn-outline-primary">
<i class="fas fa-edit"></i> Modifier les informations
</a>
<a href="{{ url_for('delete_prospect', prospect_id=prospect.id) }}" class="btn btn-outline-danger delete-confirm">
<i class="fas fa-trash-alt"></i> Supprimer le prospect
</a>
<a href="{{ url_for('convert_prospect', prospect_id=prospect.id) }}" class="btn btn-success" onclick="return confirm('Êtes-vous sûr de vouloir convertir ce prospect en client ?');">
<i class="fas fa-user-check"></i> Convertir en client
</a>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

14
Templates/devis.json Normal file
View file

@ -0,0 +1,14 @@
{
"title": "Devis de service",
"numero": "{numero}",
"date": "{date}",
"client": {
"nom": "{client_name}",
"email": "{client_email}",
"telephone": "{client_phone}",
"adresse": "{client_adress}"
},
"services": "{services}",
"paiemnt_terms": "{payment_terms}",
"total": "{total}"
}

View file

@ -0,0 +1,218 @@
{% extends 'layouts/base.html' %}
{% block title %}Suite Consultance - Créer un devis{% endblock %}
{% block module_name %}devis{% endblock %}
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-4">
<h1 class="h2 text-devis">Créer un devis</h1>
<a href="{{ url_for('devis') }}" class="btn btn-outline-devis">
<i class="fas fa-arrow-left"></i> Retour aux devis
</a>
</div>
<div class="card">
<div class="card-body">
<form method="POST" action="{{ url_for('create_devis') }}">
<div class="row g-3">
<!-- Sélection du client -->
<div class="col-12">
<h5 class="border-bottom pb-2 mb-3">Sélection du client</h5>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="client_select" class="form-label">Sélectionner un client *</label>
<select class="form-select" id="client_select" name="client_name" required>
<option value="" selected disabled>-- Choisir un client --</option>
{% for client in clients %}
<option value="{{ client.id }}">{{ client.name }}</option>
{% endfor %}
</select>
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="numero" class="form-label">Numéro du devis *</label>
<input type="text" class="form-control" id="numero" name="numero" required>
</div>
</div>
<!-- Détails du devis -->
<div class="col-12">
<h5 class="border-bottom pb-2 mb-3 mt-3">Détails du devis</h5>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="date_emission" class="form-label">Date d'émission</label>
<input type="date" class="form-control" id="date_emission" name="date_emission" value="{{ today }}">
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="date_validite" class="form-label">Date de validité</label>
<input type="date" class="form-control" id="date_validite" name="date_validite">
</div>
</div>
<!-- Prestations -->
<div id="prestations-container">
<div class="col-12">
<h5 class="border-bottom pb-2 mb-3 mt-3">Prestations</h5>
<button type="button" id="add-prestation" class="btn btn-outline-primary mb-3">
<i class="fas fa-plus"></i> Ajouter une prestation
</button>
</div>
<div class="prestation-row mb-3">
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label class="form-label">Description</label>
<input type="text" class="form-control prestation-description" name="prestation_description[]">
</div>
</div>
<div class="col-md-2">
<div class="mb-3">
<label class="form-label">Quantité</label>
<input type="number" class="form-control prestation-quantity" name="prestation_quantity[]" value="1" min="1">
</div>
</div>
<div class="col-md-2">
<div class="mb-3">
<label class="form-label">Prix unitaire (€)</label>
<input type="number" class="form-control prestation-price" name="prestation_price[]" step="0.01" min="0">
</div>
</div>
<div class="col-md-2">
<div class="mb-3">
<label class="form-label">Total</label>
<input type="text" class="form-control prestation-total" readonly>
</div>
</div>
</div>
</div>
</div>
<!-- Totaux -->
<div class="col-12">
<div class="row justify-content-end mt-3">
<div class="col-md-4">
<div class="card">
<div class="card-body">
<div class="d-flex justify-content-between mb-2">
<span>Total HT:</span>
<span id="total-ht">0.00 €</span>
</div>
<div class="d-flex justify-content-between mb-2">
<span>TVA (20%):</span>
<span id="total-tva">0.00 €</span>
</div>
<div class="d-flex justify-content-between fw-bold">
<span>Total TTC:</span>
<span id="total-ttc">0.00 €</span>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-12 mt-3">
<button type="submit" class="btn btn-primary">
<i class="fas fa-file-download"></i> Générer le devis
</button>
<a href="{{ url_for('devis') }}" class="btn btn-outline-secondary">
Annuler
</a>
</div>
</div>
</form>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script>
// Gérer l'ajout de prestations
document.getElementById('add-prestation').addEventListener('click', function() {
const container = document.getElementById('prestations-container');
const newRow = document.querySelector('.prestation-row').cloneNode(true);
// Réinitialiser les valeurs
newRow.querySelectorAll('input').forEach(input => {
if (input.className.includes('prestation-quantity')) {
input.value = 1;
} else if (!input.className.includes('prestation-total')) {
input.value = '';
}
});
// Ajouter un bouton de suppression
const deleteBtn = document.createElement('button');
deleteBtn.type = 'button';
deleteBtn.className = 'btn btn-outline-danger btn-sm mt-1';
deleteBtn.innerHTML = '<i class="fas fa-trash"></i>';
deleteBtn.onclick = function() {
this.closest('.prestation-row').remove();
updateTotals();
};
newRow.querySelector('.row').appendChild(document.createElement('div')).appendChild(deleteBtn);
container.appendChild(newRow);
// Mettre à jour les événements pour les calculs
addCalculationEvents();
});
// Calcul des totaux
function addCalculationEvents() {
document.querySelectorAll('.prestation-quantity, .prestation-price').forEach(input => {
input.addEventListener('input', function() {
const row = this.closest('.prestation-row');
const quantity = parseFloat(row.querySelector('.prestation-quantity').value) || 0;
const price = parseFloat(row.querySelector('.prestation-price').value) || 0;
const total = quantity * price;
row.querySelector('.prestation-total').value = total.toFixed(2) + ' €';
updateTotals();
});
});
}
// Calculer les totaux généraux
function updateTotals() {
let totalHT = 0;
document.querySelectorAll('.prestation-row').forEach(row => {
const quantity = parseFloat(row.querySelector('.prestation-quantity').value) || 0;
const price = parseFloat(row.querySelector('.prestation-price').value) || 0;
totalHT += quantity * price;
});
const totalTVA = totalHT * 0.2;
const totalTTC = totalHT + totalTVA;
document.getElementById('total-ht').textContent = totalHT.toFixed(2) + ' €';
document.getElementById('total-tva').textContent = totalTVA.toFixed(2) + ' €';
document.getElementById('total-ttc').textContent = totalTTC.toFixed(2) + ' €';
}
// Initialiser les événements au chargement
addCalculationEvents();
// Définir la date du jour par défaut
if (document.getElementById('date_emission')) {
const today = new Date().toISOString().split('T')[0];
document.getElementById('date_emission').value = today;
// Date de validité par défaut (aujourd'hui + 30 jours)
const validUntil = new Date();
validUntil.setDate(validUntil.getDate() + 30);
document.getElementById('date_validite').value = validUntil.toISOString().split('T')[0];
}
</script>
{% endblock %}

View file

@ -0,0 +1,61 @@
{% extends 'layouts/base.html' %}
{% block title %}Suite Consultance - Devis{% endblock %}
{% block module_name %}devis{% endblock %}
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-4">
<h1 class="h2 text-devis">Devis clients</h1>
<a href="{{ url_for('create_devis') }}" class="btn btn-devis">
<i class="fas fa-plus"></i> Nouveau devis
</a>
</div>
<div class="card">
<div class="card-body">
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead>
<tr>
<th>Client</th>
<th>Date de création</th>
<th>Fichier</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% if devis %}
{% for d in devis %}
<tr>
<td>{{ d.client_name }}</td>
<td>{{ d.date }}</td>
<td>{{ d.filename }}</td>
<td>
<div class="btn-group" role="group">
<a href="{{ url_for('download_file', filename='devis/' + d.filename) }}" class="btn btn-sm btn-primary" title="Télécharger">
<i class="fas fa-download"></i>
</a>
<button class="btn btn-sm btn-info" title="Envoyer par email">
<i class="fas fa-envelope"></i>
</button>
<button class="btn btn-sm btn-success" title="Convertir en facture">
<i class="fas fa-file-invoice"></i>
</button>
<button class="btn btn-sm btn-danger" title="Supprimer">
<i class="fas fa-trash-alt"></i>
</button>
</div>
</td>
</tr>
{% endfor %}
{% else %}
<tr>
<td colspan="4" class="text-center">Aucun devis trouvé</td>
</tr>
{% endif %}
</tbody>
</table>
</div>
</div>
</div>
{% endblock %}

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 %}

172
Templates/index.html Normal file
View file

@ -0,0 +1,172 @@
{% extends 'layouts/base.html' %}
{% block title %}Suite Consultance - Accueil{% endblock %}
{% block content %}
<div class="jumbotron bg-light p-5 rounded">
<h1 class="display-4">Bienvenue sur Suite Consultance</h1>
<p class="lead">Solution complète pour la gestion de client et de prospects</p>
<hr class="my-4">
<p>Gérer des clients, créer des propositions commerciales et générer des devis en quelques clics.</p>
<div class="row mt-5">
<div class="col-md-4 mb-4">
<div class="card h-100 shadow-sm">
<div class="card-body text-center">
<i class="fas fa-users fa-3x text-primary mb-3"></i>
<h3 class="card-title">CRM</h3>
<p class="card-text">Gérer mes clients et prospects efficacement.</p>
<a href="{{ url_for('crm') }}" class="btn btn-primary">Accéder au CRM</a>
</div>
</div>
</div>
<div class="col-md-4 mb-4">
<div class="card h-100 shadow-sm">
<div class="card-body text-center">
<i class="fas fa-file-contract fa-3x text-primary mb-3"></i>
<h3 class="card-title">Propositions</h3>
<p class="card-text">Créer des propositions commerciales personnalisées.</p>
<a href="{{ url_for('propositions') }}" class="btn btn-primary">Gérer les propositions</a>
</div>
</div>
</div>
<div class="col-md-4 mb-4">
<div class="card h-100 shadow-sm">
<div class="card-body text-center">
<i class="fas fa-file-invoice-dollar fa-3x text-primary mb-3"></i>
<h3 class="card-title">Devis</h3>
<p class="card-text">Générer et suivre mes devis clients.</p>
<a href="{{ url_for('devis') }}" class="btn btn-primary">Gérer les devis</a>
</div>
</div>
</div>
</div>
<div class="text-center mt-4">
<a href="{{ url_for('generate_test_data') }}" class="btn btn-outline-secondary">
<i class="fas fa-vial"></i> Générer des données de test
</a>
</div>
</div> <div class="row mt-5">
<div class="col-md-6">
<div class="card">
<div class="card-header bg-primary text-white">
<h5 class="card-title mb-0">Dernières activités</h5>
</div>
<div class="card-body">
<ul class="list-group list-group-flush">
{% if recent_documents %}
{% for doc in recent_documents %}
<li class="list-group-item d-flex justify-content-between align-items-center">
<div>
<i class="fas {% if doc.type == 'Proposition' %}fa-file-contract{% else %}fa-file-invoice-dollar{% endif %} me-2"></i>
{{ doc.type }} pour {{ doc.client }}
</div>
<span class="badge bg-primary rounded-pill">{{ doc.date|datetime('%d/%m/%Y') }}</span>
</li>
{% endfor %}
{% else %}
<li class="list-group-item d-flex justify-content-between align-items-center">
Aucune activité récente
</li>
{% endif %}
</ul>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card">
<div class="card-header bg-primary text-white">
<h5 class="card-title mb-0">Statistiques</h5>
</div>
<div class="card-body">
<div class="row text-center mb-4">
<div class="col-3">
<h3 class="fw-bold text-primary">{{ clients_count }}</h3>
<p>Clients</p>
</div>
<div class="col-3">
<h3 class="fw-bold text-primary">{{ prospects_count }}</h3>
<p>Prospects</p>
</div>
<div class="col-3">
<h3 class="fw-bold text-primary">{{ propositions_count }}</h3>
<p>Propositions</p>
</div>
<div class="col-3">
<h3 class="fw-bold text-primary">{{ devis_count }}</h3>
<p>Devis</p>
</div>
</div>
{% if prospect_status %}
<h6 class="border-bottom pb-2 mb-3">Statuts des prospects</h6>
<div class="row">
{% for status, count in prospect_status.items() %}
<div class="col-md-4 mb-2 text-center">
<span class="badge bg-{{ status|lower == 'nouveau' and 'primary' or (status|lower == 'contacté' and 'info' or (status|lower == 'qualifié' and 'success' or (status|lower == 'proposition' and 'warning' or 'secondary'))) }} mb-2">{{ status }}</span>
<h5>{{ count }}</h5>
</div>
{% endfor %}
</div>
{% endif %}
</div>
</div>
</div>
</div>
<div class="row mt-4">
<div class="col-md-12">
<div class="card">
<div class="card-header bg-primary text-white">
<h5 class="card-title mb-0">Prospects récents</h5>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead>
<tr>
<th>Nom</th>
<th>Entreprise</th>
<th>Email</th>
<th>Statut</th>
<th>Dernier contact</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% if recent_prospects %}
{% for prospect in recent_prospects %}
<tr>
<td>{{ prospect.name }}</td>
<td>{{ prospect.company }}</td>
<td>{{ prospect.email }}</td>
<td>
<span class="badge bg-{{ prospect.status|lower == 'nouveau' and 'primary' or (prospect.status|lower == 'contacté' and 'info' or (prospect.status|lower == 'qualifié' and 'success' or (prospect.status|lower == 'proposition' and 'warning' or 'secondary'))) }}">
{{ prospect.status }}
</span>
</td>
<td>{{ prospect.last_contact|datetime('%d/%m/%Y') if prospect.last_contact else 'N/A' }}</td>
<td>
<a href="{{ url_for('prospect_details', prospect_id=prospect.id) }}" class="btn btn-sm btn-info" title="Détails">
<i class="fas fa-eye"></i>
</a>
</td>
</tr>
{% endfor %}
{% else %}
<tr>
<td colspan="6" class="text-center">Aucun prospect récent</td>
</tr>
{% endif %}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

324
Templates/layouts/base.html Normal file
View file

@ -0,0 +1,324 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}Suite Consultance{% endblock %}</title>
<!-- Bootstrap CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- Font Awesome -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
<!-- Custom CSS -->
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/tasks.css') }}">
{% block extra_css %}{% endblock %}
</head>
<body data-module="{% block module_name %}default{% endblock %}">
<!-- Navigation -->
<nav class="navbar navbar-expand-lg navbar-dark bg-primary
{% if request.path.startswith('/crm') %}navbar-module-crm{% endif %}
{% if request.path.startswith('/propositions') %}navbar-module-propositions{% endif %}
{% if request.path.startswith('/devis') %}navbar-module-devis{% endif %}
{% if request.path.startswith('/projects') %}navbar-module-project{% endif %}
{% if request.path.startswith('/email') %}navbar-module-email{% endif %}">
<div class="container">
<a class="navbar-brand" href="{{ url_for('index') }}">
<img src="{{ url_for('static', filename='img/logo_light.png') }}" alt="Logo" height="40" class="d-inline-block align-text-top me-2">
Suite Consultance
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav ms-auto">
<li class="nav-item">
<a class="nav-link {% if request.path == url_for('index') %}active{% endif %}" href="{{ url_for('index') }}">
<i class="fas fa-home"></i> Accueil
</a>
</li>
<li class="nav-item">
<a class="nav-link {% if request.path.startswith('/crm') %}active{% endif %}" href="{{ url_for('crm') }}">
<i class="fas fa-users"></i> CRM
</a>
</li>
<li class="nav-item">
<a class="nav-link {% if request.path.startswith('/propositions') %}active{% endif %}" href="{{ url_for('propositions') }}">
<i class="fas fa-file-contract"></i> Propositions
</a>
</li>
<li class="nav-item">
<a class="nav-link {% if request.path.startswith('/devis') %}active{% endif %}" href="{{ url_for('devis') }}">
<i class="fas fa-file-invoice-dollar"></i> Devis
</a>
</li>
<li class="nav-item">
<a class="nav-link {% if request.path.startswith('/projects') %}active{% endif %}" href="{{ url_for('projects.projects_index') }}">
<i class="fas fa-diagram-project"></i> Projets
</a>
</li>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle {% if request.path.startswith('/email') %}active{% endif %}" href="#" id="emailDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
<i class="fas fa-envelope"></i> Emails
</a>
<ul class="dropdown-menu" aria-labelledby="emailDropdown">
<li><a class="dropdown-item" href="{{ url_for('email_templates') }}">
<i class="fas fa-file-alt"></i> Templates
</a></li>
<li><a class="dropdown-item" href="{{ url_for('email_config') }}">
<i class="fas fa-cog"></i> Configuration
</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="{{ url_for('email_scraper_page') }}">
<i class="fas fa-search"></i> Scrapper d'emails
</a></li>
</ul>
</li>
</ul>
<button class="btn btn-outline-light ms-3 d-none d-lg-inline-flex" type="button" data-bs-toggle="offcanvas" data-bs-target="#tasksTodaySidebar" aria-controls="tasksTodaySidebar" title="Tâches du jour">
<i class="fas fa-list-check"></i>
<span class="badge rounded-pill bg-warning text-dark ms-1 tasks-today-badge" style="display:none;">0</span>
</button>
<a class="btn btn-outline-light ms-2 d-lg-none" data-bs-toggle="offcanvas" href="#tasksTodaySidebar" role="button" aria-controls="tasksTodaySidebar" title="Tâches du jour">
<i class="fas fa-list-check"></i>
<span class="badge rounded-pill bg-warning text-dark ms-1 tasks-today-badge" style="display:none;">0</span>
</a>
<button class="btn btn-light ms-2 d-none d-lg-inline-flex" type="button" data-bs-toggle="modal" data-bs-target="#createTaskModal" title="Nouvelle tâche">
<i class="fas fa-plus"></i>
</button>
<a class="btn btn-light ms-2 d-lg-none" data-bs-toggle="modal" href="#createTaskModal" role="button" title="Nouvelle tâche">
<i class="fas fa-plus"></i>
</a>
<button class="btn btn-outline-light ms-2" type="button" data-bs-toggle="modal" data-bs-target="#prospectSearchModal" title="Rechercher des prospects">
<i class="fas fa-magnifying-glass"></i>
</button>
<div class="form-check form-switch ms-3 text-light d-flex align-items-center">
<input class="form-check-input" type="checkbox" id="darkModeSwitch">
<label class="form-check-label ms-2" for="darkModeSwitch" id="darkModeLabel">
<i class="fas fa-sun"></i>
</label>
</div>
</div>
</div>
</nav>
<!-- Flash Messages -->
<div class="container mt-3">
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ category if category != 'error' else 'danger' }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endfor %}
{% endif %}
{% endwith %}
</div>
<!-- Main Content -->
<main class="container py-4">
<div class="row">
<div class="col-12">
{% block content %}{% endblock %}
</div>
</div>
</main>
<!-- Footer -->
<footer class="footer mt-auto py-3 bg-light">
<div class="container text-center">
<span class="text-muted">© 2025 Suite Consultance. Tous droits réservés.</span>
</div>
</footer>
{% include 'partials/tasks_sidebar.html' %}
{% include 'partials/task_create_modal.html' %}
<!-- Modal: Recherche Prospects -->
<div class="modal fade" id="prospectSearchModal" tabindex="-1" aria-labelledby="prospectSearchModalLabel" aria-hidden="true">
<div class="modal-dialog modal-xl modal-dialog-scrollable">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="prospectSearchModalLabel"><i class="fas fa-magnifying-glass me-2"></i>Recherche prospects</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Fermer"></button>
</div>
<div class="modal-body">
<form id="prospectSearchForm" class="row g-3 mb-3">
<div class="col-md-4">
<label class="form-label">Recherche globale</label>
<input type="text" name="q" class="form-control" placeholder="Nom, email, société, ville">
</div>
<div class="col-md-4">
<label class="form-label">Nom</label>
<input type="text" name="name" class="form-control" placeholder="Nom">
</div>
<div class="col-md-4">
<label class="form-label">Email</label>
<input type="email" name="email" class="form-control" placeholder="email@exemple.com">
</div>
<div class="col-md-4">
<label class="form-label">Société</label>
<input type="text" name="company" class="form-control" placeholder="Société">
</div>
<div class="col-md-4">
<label class="form-label">Statut</label>
<input type="text" name="status" class="form-control" placeholder="ex: nouveau, en cours...">
</div>
<div class="col-md-4">
<label class="form-label">Ville</label>
<input type="text" name="city" class="form-control" placeholder="Ville">
</div>
<div class="col-md-4">
<label class="form-label">Tags</label>
<input type="text" name="tags" class="form-control" placeholder="tag1, tag2">
</div>
<div class="col-md-4">
<label class="form-label">Date de création - du</label>
<input type="date" name="date_from" class="form-control">
</div>
<div class="col-md-4">
<label class="form-label">Date de création - au</label>
<input type="date" name="date_to" class="form-control">
</div>
<div class="col-12 d-flex justify-content-end">
<button type="reset" class="btn btn-outline-secondary me-2" id="resetProspectSearch">Réinitialiser</button>
<button type="submit" class="btn btn-primary"><i class="fas fa-search me-2"></i>Chercher</button>
</div>
</form>
<div id="prospectSearchSummary" class="text-muted mb-2"></div>
<div id="prospectSearchResults" class="list-group"></div>
</div>
</div>
</div>
</div>
<script>
(function(){
const form = document.getElementById('prospectSearchForm');
const results = document.getElementById('prospectSearchResults');
const summary = document.getElementById('prospectSearchSummary');
const modalEl = document.getElementById('prospectSearchModal');
function buildQuery(params) {
const usp = new URLSearchParams();
Object.entries(params).forEach(([k,v])=>{
if(v !== undefined && v !== null && String(v).trim() !== '') {
usp.append(k, String(v).trim());
}
});
return usp.toString();
}
function serializeForm() {
const data = new FormData(form);
const obj = {};
data.forEach((v, k) => {
obj[k] = v;
});
return obj;
}
function renderResults(items) {
results.innerHTML = '';
if (!items || items.length === 0) {
results.innerHTML = '<div class="text-muted">Aucun prospect trouvé.</div>';
return;
}
items.forEach(it => {
const tags = (it.tags || []).join(', ');
const created = it.created_at ? ` • Créé le ${it.created_at}` : '';
const related = Array.isArray(it.related) ? it.related : [];
const div = document.createElement('a');
div.className = 'list-group-item list-group-item-action';
div.href = it.url || '#';
div.innerHTML = `
<div class="d-flex w-100 justify-content-between">
<h6 class="mb-1">${it.name || '(Sans nom)'}</h6>
<small class="text-muted">${it.status || ''}</small>
</div>
<div class="mb-1">
<span class="me-3"><i class="fas fa-building me-1"></i>${it.company || ''}</span>
<span class="me-3"><i class="fas fa-envelope me-1"></i>${it.email || ''}</span>
<span class="me-3"><i class="fas fa-location-dot me-1"></i>${it.city || ''}</span>
${created}
</div>
${tags ? `<small class="text-muted"><i class="fas fa-tags me-1"></i>${tags}</small>` : '' }
${related.length ? `<div class="mt-2">
${related.map(r => {
const icon = r.type === 'client' ? 'fa-user' : (r.type === 'project' ? 'fa-diagram-project' : 'fa-link');
return `<a href="${r.url}" class="badge rounded-pill bg-light text-primary border me-1"><i class="fas ${icon} me-1"></i>${r.type}</a>`;
}).join('')}
</div>` : ''}
`;
results.appendChild(div);
});
}
async function performSearch(e){
if (e) e.preventDefault();
const params = serializeForm();
const qs = buildQuery(params);
summary.textContent = 'Recherche en cours...';
results.innerHTML = '<div class="text-muted">Chargement...</div>';
try {
const res = await fetch('/api/crm/prospects/search?' + qs, {headers: {'X-Requested-With':'XMLHttpRequest'}});
if (!res.ok) throw new Error('HTTP ' + res.status);
const data = await res.json();
summary.textContent = `${data.count || 0} résultat(s)`;
renderResults(data.results || []);
} catch (err) {
summary.textContent = 'Erreur lors de la recherche.';
results.innerHTML = '<div class="text-danger">Impossible deffectuer la recherche.</div>';
}
}
form.addEventListener('submit', performSearch);
document.getElementById('resetProspectSearch').addEventListener('click', function(){
// Laisser le reset natif vider le formulaire, puis effacer résultats
setTimeout(()=>{
summary.textContent = '';
results.innerHTML = '';
}, 0);
});
// Auto recherche quand la modale souvre (facultatif)
modalEl.addEventListener('shown.bs.modal', () => {
if (!results.innerHTML) {
performSearch();
}
});
})();
</script>
<!-- Bootstrap JS Bundle with Popper -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/js/bootstrap.bundle.min.js"></script>
<!-- Custom JS -->
<script src="{{ url_for('static', filename='js/main.js') }}"></script>
<script>
(function(){
async function refreshTasksBadge(){
try{
const res = await fetch('{{ url_for("tasks.today_count") }}', {headers:{'X-Requested-With':'XMLHttpRequest'}});
if(!res.ok) throw new Error('HTTP ' + res.status);
const data = await res.json();
const count = Number(data.count) || 0;
document.querySelectorAll('.tasks-today-badge').forEach(el=>{
if(count > 0){
el.textContent = count;
el.style.display = 'inline-block';
} else {
el.style.display = 'none';
}
});
}catch(e){
// silencieux
}
}
document.addEventListener('DOMContentLoaded', refreshTasksBadge);
document.addEventListener('visibilitychange', ()=>{ if(!document.hidden) refreshTasksBadge(); });
})();
</script>
{% block extra_js %}{% endblock %}
</body>
</html>

View file

@ -0,0 +1,54 @@
<div class="modal fade" id="createTaskModal" tabindex="-1" aria-labelledby="createTaskModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<form method="post" action="{{ url_for('tasks.create_task') }}" class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="createTaskModalLabel"><i class="fas fa-plus me-2"></i>Nouvelle tâche</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Fermer"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label class="form-label">Titre *</label>
<input type="text" name="title" class="form-control" required>
</div>
<div class="mb-3">
<label class="form-label">Échéance *</label>
<input type="date" name="due_date" class="form-control" value="{{ today if today is defined else '' }}" required>
</div>
<div class="mb-3">
<label class="form-label">Description</label>
<textarea name="description" class="form-control" rows="3"></textarea>
</div>
<div class="row">
<div class="mb-3 col-md-6">
<label class="form-label">Priorité</label>
<select name="priority" class="form-select">
<option value="basse">Basse</option>
<option value="normale" selected>Normale</option>
<option value="haute">Haute</option>
</select>
</div>
<div class="mb-3 col-md-6">
<label class="form-label">Type dentité</label>
<select name="entity_type" class="form-select">
<option value="">Aucune</option>
<option value="client">Client</option>
<option value="prospect">Prospect</option>
<option value="project">Projet</option>
<option value="campaign">Campagne</option>
</select>
</div>
</div>
<div class="mb-3">
<label class="form-label">ID de lentité (optionnel)</label>
<input type="text" name="entity_id" class="form-control" placeholder="cli_xxx / pros_xxx / id projet...">
</div>
</div>
<div class="modal-footer">
<a href="{{ url_for('tasks.tasks_index') }}" class="btn btn-outline-secondary">Gérer les tâches</a>
<button type="submit" class="btn btn-primary">
<i class="fas fa-save me-1"></i>Créer
</button>
</div>
</form>
</div>
</div>

View file

@ -0,0 +1,14 @@
{# Bouton réutilisable pour créer une tâche #}
{# Il lie automatiquement si entity_type et entity_id sont disponibles dans le contexte #}
{% set et = (entity_type | default(request.args.get('entity_type'))) %}
{% set eid = (entity_id | default(request.args.get('entity_id'))) %}
{% if et and eid %}
<a class="btn btn-outline-success" href="{{ url_for('tasks.quick_add_page', entity_type=et, entity_id=eid) }}">
Ajouter une tâche
</a>
{% else %}
<a class="btn btn-outline-secondary" href="{{ url_for('tasks.tasks_index') }}">
Nouvelle tâche
</a>
{% endif %}

View file

@ -0,0 +1,31 @@
<form method="post" action="{{ url_for('tasks.create_task') }}">
<input type="hidden" name="entity_type" value="{{ entity_type }}">
<input type="hidden" name="entity_id" value="{{ entity_id }}">
<div class="mb-3">
<label class="form-label">Titre</label>
<input type="text" class="form-control" name="title" placeholder="Ex: Relancer le prospect" required>
</div>
<div class="row">
<div class="col-md-4 mb-3">
<label class="form-label">Échéance</label>
<input type="date" class="form-control" name="due_date" value="{{ today }}" required>
</div>
<div class="col-md-4 mb-3">
<label class="form-label">Priorité</label>
<select class="form-select" name="priority">
<option value="basse">Basse</option>
<option value="normale" selected>Normale</option>
<option value="haute">Haute</option>
</select>
</div>
</div>
<div class="mb-3">
<label class="form-label">Description</label>
<textarea class="form-control" name="description" rows="3" placeholder="Détails éventuels..."></textarea>
</div>
<button type="submit" class="btn btn-primary">Créer la tâche</button>
</form>

View file

@ -0,0 +1,23 @@
{}
{# Partial: affiche une table de tâches à partir de la variable `tasks` #}
{% import "tasks/macros.html" as tsk %}
<div style="border:1px solid #e5e5e5;border-radius:.5rem;padding:1rem;">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:.6rem;">
<h3 style="margin:0;font-size:1.1rem;">Tâches</h3>
<div style="font-size:.9rem;color:#666;">
Total: {{ tasks|length if tasks else 0 }}
</div>
</div>
{{ tsk.render_tasks_table(tasks, show_entity=True, show_due_date=True) }}
</div>
{# Partial: affiche une table de tâches à partir de la variable `tasks` #}
{% import "tasks/macros.html" as tsk %}
<div style="border:1px solid #e5e5e5;border-radius:.5rem;padding:1rem;">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:.6rem;">
<h3 style="margin:0;font-size:1.1rem;">Tâches</h3>
<div style="font-size:.9rem;color:#666;">
Total: {{ tasks|length if tasks else 0 }}
</div>
</div>
{{ tsk.render_tasks_table(tasks, show_entity=True, show_due_date=True) }}
</div>

View file

@ -0,0 +1,74 @@
{# Sidebar offcanvas gauche pour afficher les tâches du jour #}
<div class="offcanvas offcanvas-start" tabindex="-1" id="tasksTodaySidebar" aria-labelledby="tasksTodaySidebarLabel" style="width:560px;">
<div class="offcanvas-header">
<h5 class="offcanvas-title" id="tasksTodaySidebarLabel">
<i class="fas fa-list-check me-2"></i>Tâches
</h5>
<a href="{{ url_for('tasks.tasks_index') }}" class="btn btn-warning btn-sm"><i class="fas fa-cog me-1"></i></a>
<a href="{{ url_for('tasks.email_drafts_list') }}" class="btn btn-success btn-sm"><i class="fa-solid fa-file-arrow-up"></i></a>
<button type="button" class="btn-close text-reset" data-bs-dismiss="offcanvas" aria-label="Fermer"></button>
</div>
<div class="offcanvas-body">
<ul class="nav nav-tabs mb-3">
<li class="nav-item">
<a class="nav-link active" id="today-tab" data-bs-toggle="tab" href="#today">Aujourd'hui</a>
</li>
<li class="nav-item">
<a class="nav-link" id="tomorrow-tab" data-bs-toggle="tab" href="#tomorrow">Demain</a>
</li>
</ul>
<div class="tab-content">
<div class="tab-pane fade show active" id="today">
<div id="tasksTodayContent">
<div class="placeholder">Chargement des tâches...</div>
</div>
</div>
<div class="tab-pane fade" id="tomorrow">
<div id="tasksTomorrowContent">
<div class="placeholder">Chargement des tâches...</div>
</div>
</div>
</div>
</div>
</div>
<script>
(function(){
const todayContainer = document.getElementById('tasksTodayContent');
const tomorrowContainer = document.getElementById('tasksTomorrowContent');
async function loadTasks(container, url) {
try {
const res = await fetch(url, {headers: {'X-Requested-With': 'XMLHttpRequest'}});
if (!res.ok) throw new Error('HTTP ' + res.status);
container.innerHTML = await res.text();
} catch (e) {
container.innerHTML = '<div class="text-muted">Impossible de charger les tâches.</div>';
}
}
async function loadTodayTasks() {
await loadTasks(todayContainer, '{{ url_for("tasks.today_fragment") }}');
}
async function loadTomorrowTasks() {
await loadTasks(tomorrowContainer, '{{ url_for("tasks.tomorrow_fragment") }}');
}
// Charger au premier affichage de la page
document.addEventListener('DOMContentLoaded', loadTodayTasks);
// Recharger à l'ouverture de l'offcanvas
const sidebar = document.getElementById('tasksTodaySidebar');
if (sidebar) {
sidebar.addEventListener('show.bs.offcanvas', () => {
loadTodayTasks();
loadTomorrowTasks();
});
}
// Gérer les changements d'onglets
const tomorrowTab = document.getElementById('tomorrow-tab');
tomorrowTab.addEventListener('shown.bs.tab', loadTomorrowTasks);
})();
</script>

View file

@ -0,0 +1,11 @@
{# Partial: bloc des tâches du jour, attend `today_tasks` dans le contexte #}
{% import "tasks/macros.html" as tsk %}
<div style="border:1px solid #e5e5e5;border-radius:.5rem;padding:1rem;background:#fafafa;">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:.6rem;">
<h3 style="margin:0;font-size:1.1rem;">Tâches du jour</h3>
<div style="font-size:.9rem;color:#666;">
{{ today_tasks|length if today_tasks else 0 }} tâche(s)
</div>
</div>
{{ tsk.render_tasks_table(today_tasks, show_entity=True, show_due_date=False, empty_text="Aucune tâche pour aujourd'hui") }}
</div>

View file

@ -0,0 +1,83 @@
{% extends 'layouts/base.html' %}
{% block title %}Tous les projets{% endblock %}
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-4">
<h1 class="h3">Projets</h1>
</div>
<form method="get" class="card p-3 mb-4">
<div class="row g-3">
<div class="col-md-4">
<label class="form-label">Client</label>
<select name="client_id" class="form-select" onchange="this.form.submit()">
<option value="">Tous les clients</option>
{% for c in clients %}
<option value="{{ c.id }}" {% if selected_client == c.id %}selected{% endif %}>{{ c.name }}</option>
{% endfor %}
</select>
</div>
<div class="col-md-6">
<label class="form-label">Recherche</label>
<input type="search" name="q" class="form-control" placeholder="Nom, statut, description..." value="{{ q or '' }}">
</div>
<div class="col-md-2 d-flex align-items-end">
<button class="btn btn-primary w-100" type="submit">
<i class="fas fa-search"></i> Rechercher
</button>
</div>
</div>
</form>
{% if projects and projects|length > 0 %}
<div class="table-responsive">
<table class="table table-hover align-middle">
<thead>
<tr>
<th>Projet</th>
<th>Client</th>
<th>Statut</th>
<th>Début</th>
<th>Fin</th>
<th>Budget</th>
<th class="text-end">Actions</th>
</tr>
</thead>
<tbody>
{% for p in projects %}
<tr>
<td>{{ p.name }}</td>
<td>{{ p.client_name }}</td>
<td><span class="badge bg-secondary">{{ p.status }}</span></td>
<td>{{ p.start_date or '—' }}</td>
<td>{{ p.end_date or '—' }}</td>
<td>
{% if p.budget is not none %}
{{ "%.2f"|format(p.budget) }} €
{% else %} — {% endif %}
</td>
<td class="text-end">
<a class="btn btn-sm btn-outline-primary" href="{{ url_for('project_details', client_id=p.client_id, project_id=p.id) }}">
<i class="fas fa-eye"></i>
</a>
<a class="btn btn-sm btn-outline-secondary" href="{{ url_for('edit_client_project', client_id=p.client_id, project_id=p.id) }}">
<i class="fas fa-edit"></i>
</a>
<form action="{{ url_for('delete_client_project', client_id=p.client_id, project_id=p.id) }}" method="post" class="d-inline" onsubmit="return confirm('Supprimer ce projet ?');">
<button type="submit" class="btn btn-sm btn-outline-danger">
<i class="fas fa-trash"></i>
</button>
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="alert alert-info">
Aucun projet trouvé avec ces critères.
</div>
{% endif %}
{% endblock %}

View file

@ -0,0 +1,57 @@
{% extends 'layouts/base.html' %}
{% block title %}{% if project %}Éditer le projet{% else %}Nouveau projet{% endif %} - {{ client_name }}{% endblock %}
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-4">
<h1 class="h3">{% if project %}Éditer le projet{% else %}Nouveau projet{% endif %} - {{ client_name }}</h1>
<div>
<a href="{{ url_for('list_client_projects', client_id=client_id) }}" class="btn btn-outline-secondary">
<i class="fas fa-arrow-left"></i> Retour aux projets
</a>
</div>
</div>
<form method="post" class="card p-3">
<div class="mb-3">
<label class="form-label">Nom du projet *</label>
<input type="text" name="name" class="form-control" required value="{{ project.name if project else '' }}">
</div>
<div class="row">
<div class="mb-3 col-md-4">
<label class="form-label">Statut</label>
<select name="status" class="form-select">
{% set statuses = ['Nouveau', 'En cours', 'En attente', 'Terminé', 'Annulé'] %}
{% for st in statuses %}
<option value="{{ st }}" {% if project and project.status == st %}selected{% endif %}>{{ st }}</option>
{% endfor %}
</select>
</div>
<div class="mb-3 col-md-4">
<label class="form-label">Date de début</label>
<input type="date" name="start_date" class="form-control" value="{{ project.start_date if project and project.start_date else '' }}">
</div>
<div class="mb-3 col-md-4">
<label class="form-label">Date de fin</label>
<input type="date" name="end_date" class="form-control" value="{{ project.end_date if project and project.end_date else '' }}">
</div>
</div>
<div class="mb-3">
<label class="form-label">Budget (€)</label>
<input type="number" step="0.01" name="budget" class="form-control" value="{{ project.budget if project and project.budget is not none else '' }}">
</div>
<div class="mb-3">
<label class="form-label">Description</label>
<textarea name="description" rows="5" class="form-control">{{ project.description if project else '' }}</textarea>
</div>
<div class="text-end">
<button type="submit" class="btn btn-primary">
<i class="fas fa-save"></i> Enregistrer
</button>
</div>
</form>
{% endblock %}

View file

@ -0,0 +1,68 @@
{% extends 'layouts/base.html' %}
{% block title %}Projets - {{ client_name }}{% endblock %}
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-4">
<h1 class="h3">Projets de {{ client_name }}</h1>
<div>
<a href="{{ url_for('client_details', client_id=client_id) }}" class="btn btn-outline-secondary me-2">
<i class="fas fa-user"></i> Détails client
</a>
<a href="{{ url_for('add_client_project', client_id=client_id) }}" class="btn btn-primary">
<i class="fas fa-plus"></i> Nouveau projet
</a>
</div>
</div>
{% if projects and projects|length > 0 %}
<div class="table-responsive">
<table class="table table-hover align-middle">
<thead>
<tr>
<th>Nom</th>
<th>Statut</th>
<th>Début</th>
<th>Fin</th>
<th>Budget</th>
<th class="text-end">Actions</th>
</tr>
</thead>
<tbody>
{% for project in projects %}
<tr>
<td>{{ project.name }}</td>
<td><span class="badge bg-secondary">{{ project.status }}</span></td>
<td>{{ project.start_date or '—' }}</td>
<td>{{ project.end_date or '—' }}</td>
<td>
{% if project.budget is not none %}
{{ "%.2f"|format(project.budget) }} €
{% else %}
{% endif %}
</td>
<td class="text-end">
<a class="btn btn-sm btn-outline-primary" href="{{ url_for('project_details', client_id=client_id, project_id=project.id) }}">
<i class="fas fa-eye"></i>
</a>
<a class="btn btn-sm btn-outline-secondary" href="{{ url_for('edit_client_project', client_id=client_id, project_id=project.id) }}">
<i class="fas fa-edit"></i>
</a>
<form action="{{ url_for('delete_client_project', client_id=client_id, project_id=project.id) }}" method="post" class="d-inline" onsubmit="return confirm('Supprimer ce projet ?');">
<button type="submit" class="btn btn-sm btn-outline-danger">
<i class="fas fa-trash"></i>
</button>
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="alert alert-info">
Aucun projet pour ce client. Créez-en un avec le bouton "Nouveau projet".
</div>
{% endif %}
{% endblock %}

View file

@ -0,0 +1,47 @@
{% extends 'layouts/base.html' %}
{% block title %}Projet {{ project.name }} - {{ client_name }}{% endblock %}
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-4">
<h1 class="h3">Projet: {{ project.name }}</h1>
<div>
<a href="{{ url_for('list_client_projects', client_id=client_id) }}" class="btn btn-outline-secondary me-2">
<i class="fas fa-arrow-left"></i> Retour aux projets
</a>
<a href="{{ url_for('edit_client_project', client_id=client_id, project_id=project.id) }}" class="btn btn-primary">
<i class="fas fa-edit"></i> Éditer
</a>
</div>
</div>
<div class="card">
<div class="card-body">
<dl class="row">
<dt class="col-sm-3">Statut</dt>
<dd class="col-sm-9"><span class="badge bg-secondary">{{ project.status }}</span></dd>
<dt class="col-sm-3">Date de début</dt>
<dd class="col-sm-9">{{ project.start_date or '—' }}</dd>
<dt class="col-sm-3">Date de fin</dt>
<dd class="col-sm-9">{{ project.end_date or '—' }}</dd>
<dt class="col-sm-3">Budget</dt>
<dd class="col-sm-9">
{% if project.budget is not none %}
{{ "%.2f"|format(project.budget) }} €
{% else %}
{% endif %}
</dd>
<dt class="col-sm-3">Description</dt>
<dd class="col-sm-9">{{ project.description or '—' }}</dd>
<dt class="col-sm-3">ID du projet</dt>
<dd class="col-sm-9"><code>{{ project.id }}</code></dd>
</dl>
</div>
</div>
{% endblock %}

View file

@ -0,0 +1,37 @@
{
"title": "Proposition de service",
"sections": [
{
"header": "Informations générales",
"content": [
"Client : {client_name}",
"Projet : {project_name}",
"Type : {project_type}",
"Date limite : {deadline}"
]
},
{
"header": "Description du projet",
"content": ["{project_description}"]
},
{
"header": "Fonctionnalités demandées",
"content": "{features}"
},
{
"header": "Budget",
"content": ["Montant estimé : {budget}"]
},
{
"header": "Modalités",
"content": [
"Paiement : {payment_terms}",
"Contact : {contact_info}"
]
},
{
"header": "Informations complémentaires",
"content": ["{additional_info}"]
}
]
}

View file

@ -0,0 +1,137 @@
{% extends 'layouts/base.html' %}
{% block title %}Suite Consultance - Créer une proposition{% endblock %}
{% block module_name %}propositions{% endblock %}
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-4">
<h1 class="h2 text-propositions">Créer une proposition commerciale</h1>
<a href="{{ url_for('propositions') }}" class="btn btn-outline-propositions">
<i class="fas fa-arrow-left"></i> Retour aux propositions
</a>
</div>
<div class="card">
<div class="card-body">
<form method="POST" action="{{ url_for('create_proposition') }}">
<div class="row g-3">
<!-- Informations client -->
<div class="col-12">
<h5 class="border-bottom pb-2 mb-3">Informations client</h5>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="client_name" class="form-label">Nom du client *</label>
<input type="text" class="form-control" id="client_name" name="client_name" value="{{ form_data.client_name if form_data is defined and form_data.client_name else '' }}" required>
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="email" class="form-label">Email</label>
<input type="email" class="form-control" id="email" name="email" value="{{ form_data.email if form_data is defined and form_data.email else '' }}">
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="telephone" class="form-label">Téléphone</label>
<input type="tel" class="form-control" id="telephone" name="telephone" value="{{ form_data.telephone if form_data is defined and form_data.telephone else '' }}">
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="adresse" class="form-label">Adresse</label>
<input type="text" class="form-control" id="adresse" name="adresse" value="{{ form_data.adresse if form_data is defined and form_data.adresse else '' }}">
</div>
</div>
<!-- Informations projet -->
<div class="col-12">
<h5 class="border-bottom pb-2 mb-3 mt-3">Informations projet</h5>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="project_name" class="form-label">Nom du projet *</label>
<input type="text" class="form-control" id="project_name" name="project_name" value="{{ form_data.project_name if form_data is defined and form_data.project_name else '' }}" required>
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="project_type" class="form-label">Type de projet</label>
<input type="text" class="form-control" id="project_type" name="project_type" value="{{ form_data.project_type if form_data is defined and form_data.project_type else '' }}">
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="deadline" class="form-label">Délai de livraison</label>
<input type="date" class="form-control" id="deadline" name="deadline" value="{{ form_data.deadline if form_data is defined and form_data.deadline else '' }}">
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="budget" class="form-label">Budget estimé</label>
<input type="text" class="form-control" id="budget" name="budget" value="{{ form_data.budget if form_data is defined and form_data.budget else '' }}">
</div>
</div>
<div class="col-12">
<div class="mb-3">
<label for="project_description" class="form-label">Description du projet</label>
<textarea class="form-control" id="project_description" name="project_description" rows="3">{{ form_data.project_description if form_data is defined and form_data.project_description else '' }}</textarea>
</div>
</div>
<div class="col-12">
<div class="mb-3">
<label for="features" class="form-label">Fonctionnalités principales</label>
<textarea class="form-control" id="features" name="features" rows="3" placeholder="Fonctionnalité 1, Fonctionnalité 2, ...">{{ form_data.features if form_data is defined and form_data.features and not form_data.features is iterable else '' }}</textarea>
<small class="form-text text-muted">Séparez les fonctionnalités par des virgules</small>
</div>
</div>
<!-- Informations complémentaires -->
<div class="col-12">
<h5 class="border-bottom pb-2 mb-3 mt-3">Informations complémentaires</h5>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="payment_terms" class="form-label">Conditions de paiement</label>
<input type="text" class="form-control" id="payment_terms" name="payment_terms" value="{{ form_data.payment_terms if form_data is defined and form_data.payment_terms else '' }}">
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="contact_info" class="form-label">Coordonnées</label>
<input type="text" class="form-control" id="contact_info" name="contact_info" value="{{ form_data.contact_info if form_data is defined and form_data.contact_info else '' }}">
</div>
</div>
<div class="col-12">
<div class="mb-3">
<label for="additional_info" class="form-label">Informations additionnelles</label>
<textarea class="form-control" id="additional_info" name="additional_info" rows="3">{{ form_data.additional_info if form_data is defined and form_data.additional_info else '' }}</textarea>
</div>
</div>
<div class="col-12 mt-3">
<button type="submit" class="btn btn-primary">
<i class="fas fa-file-download"></i> Générer la proposition
</button>
<a href="{{ url_for('propositions') }}" class="btn btn-outline-secondary">
Annuler
</a>
</div>
</div>
</form>
</div>
</div>
{% endblock %}

View file

@ -0,0 +1,58 @@
{% extends 'layouts/base.html' %}
{% block title %}Suite Consultance - Propositions{% endblock %}
{% block module_name %}propositions{% endblock %}
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-4">
<h1 class="h2 text-propositions">Propositions commerciales</h1>
<a href="{{ url_for('create_proposition') }}" class="btn btn-propositions">
<i class="fas fa-plus"></i> Nouvelle proposition
</a>
</div>
<div class="card">
<div class="card-body">
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead>
<tr>
<th>Client</th>
<th>Date de création</th>
<th>Fichier</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% if propositions %}
{% for proposition in propositions %}
<tr>
<td>{{ proposition.client_name }}</td>
<td>{{ proposition.date }}</td>
<td>{{ proposition.filename }}</td>
<td>
<div class="btn-group" role="group">
<a href="{{ url_for('download_file', filename='propositions/' + proposition.filename) }}" class="btn btn-sm btn-propositions" title="Télécharger">
<i class="fas fa-download"></i>
</a>
<button class="btn btn-sm btn-info" title="Envoyer par email">
<i class="fas fa-envelope"></i>
</button>
<button class="btn btn-sm btn-danger" title="Supprimer">
<i class="fas fa-trash-alt"></i>
</button>
</div>
</td>
</tr>
{% endfor %}
{% else %}
<tr>
<td colspan="4" class="text-center">Aucune proposition trouvée</td>
</tr>
{% endif %}
</tbody>
</table>
</div>
</div>
</div>
{% endblock %}

View file

@ -0,0 +1,10 @@
<a href="{{ url_for('edit_prospect', prospect_id=prospect.id) }}" class="btn btn-primary">
<i class="fas fa-edit"></i> Modifier
</a>
<a href="{{ url_for('convert_prospect', prospect_id=prospect.id) }}" class="btn btn-success" onclick="return confirm('Êtes-vous sûr de vouloir convertir ce prospect en client ?');">
<i class="fas fa-user-check"></i> Convertir en client
</a>
{% with entity_type='prospect', entity_id=prospect.id %}
{% include "partials/task_quick_add_button.html" %}
{% endwith %}
</div>

View file

@ -0,0 +1,45 @@
{% extends "layouts/base.html" %}
{% block title %}Brouillons d'emails{% endblock %}
{% block content %}
<div class="container">
<div class="d-flex justify-content-between align-items-center mb-3">
<h2 class="mb-0">Brouillons d'emails à envoyer</h2>
<div class="d-flex gap-2">
<form method="post" action="{{ url_for('tasks.email_drafts_generate') }}">
<button type="submit" class="btn btn-primary">Générer pour aujourd'hui</button>
</form>
{% include "partials/task_quick_add_button.html" %}
</div>
</div>
{% if drafts %}
{% for d in drafts %}
<div class="card mb-3">
<div class="card-body">
<div class="d-flex justify-content-between align-items-start">
<h5 class="card-title mb-0">{{ d.subject }}</h5>
<form method="post" action="{{ url_for('tasks.email_drafts_send') }}">
<input type="hidden" name="draft_id" value="{{ d.id }}">
<button type="submit" class="btn btn-success">Envoyer</button>
</form>
</div>
<p class="text-muted mt-2 mb-2">À: {{ d.to_email }}</p>
<div class="border rounded p-3 mb-2 bg-light">
{{ d.content | safe }}
</div>
<small class="text-muted">Prospect: {{ d.prospect_id }} | Template: {{ d.template_id or '-' }}</small>
</div>
</div>
{% endfor %}
{% else %}
<p>Aucun brouillon à envoyer.</p>
{% endif %}
<div class="mt-3">
<a href="{{ url_for('tasks.tasks_index') }}">&larr; Retour aux tâches</a>
</div>
</div>
{% endblock %}

10
Templates/tasks/list.html Normal file
View file

@ -0,0 +1,10 @@
{% import "tasks/macros.html" as tsk %}
<div style="max-width:1100px;margin:0 auto;padding:1rem;">
<h2 style="margin-top:0;">Liste des tâches</h2>
{{ tsk.render_tasks_table(tasks, show_entity=True, show_due_date=True) }}
</div>
{% import "tasks/macros.html" as tsk %}
<div style="max-width:1100px;margin:0 auto;padding:1rem;">
<h2 style="margin-top:0;">Liste des tâches</h2>
{{ tsk.render_tasks_table(tasks, show_entity=True, show_due_date=True) }}
</div>

413
Templates/tasks/macros.html Normal file
View file

@ -0,0 +1,413 @@
{# Macros de rendu pour les tâches #}
{% macro priority_badge(priority) -%}
{% set classes = {
'haute': 'background:#ffe5e5;color:#b30000;padding:.15rem .4rem;border-radius:.3rem;font-size:.8rem;',
'normale': 'background:#e8f1ff;color:#003a8c;padding:.15rem .4rem;border-radius:.3rem;font-size:.8rem;',
'basse': 'background:#e9f7ef;color:#1b5e20;padding:.15rem .4rem;border-radius:.3rem;font-size:.8rem;'
} %}
<span style="{{ classes.get(priority, classes['normale']) }}">{{ priority|capitalize }}</span>
{%- endmacro %}
{% macro status_badge(status) -%}
{% set classes = {
'todo': 'background:#fff3cd;color:#8a6d3b;padding:.15rem .4rem;border-radius:.3rem;font-size:.8rem;',
'done': 'background:#e6ffed;color:#155724;padding:.15rem .4rem;border-radius:.3rem;font-size:.8rem;',
'canceled': 'background:#f8d7da;color:#721c24;padding:.15rem .4rem;border-radius:.3rem;font-size:.8rem;'
} %}
{% set labels = {'todo': 'À faire', 'done': 'Fait', 'canceled': 'Annulée'} %}
<span style="{{ classes.get(status, classes['todo']) }}">{{ labels.get(status, status) }}</span>
{%- endmacro %}
{# Macros de rendu pour les tâches #}
{% macro priority_badge(priority) -%}
{% set classes = {
'haute': 'background:#ffe5e5;color:#b30000;padding:.15rem .4rem;border-radius:.3rem;font-size:.8rem;',
'normale': 'background:#e8f1ff;color:#003a8c;padding:.15rem .4rem;border-radius:.3rem;font-size:.8rem;',
'basse': 'background:#e9f7ef;color:#1b5e20;padding:.15rem .4rem;border-radius:.3rem;font-size:.8rem;'
} %}
<span style="{{ classes.get(priority, classes['normale']) }}">{{ priority|capitalize }}</span>
{%- endmacro %}
{% macro status_badge(status) -%}
{% set classes = {
'todo': 'background:#fff3cd;color:#8a6d3b;padding:.15rem .4rem;border-radius:.3rem;font-size:.8rem;',
'done': 'background:#e6ffed;color:#155724;padding:.15rem .4rem;border-radius:.3rem;font-size:.8rem;',
'canceled': 'background:#f8d7da;color:#721c24;padding:.15rem .4rem;border-radius:.3rem;font-size:.8rem;'
} %}
{% set labels = {'todo': 'À faire', 'done': 'Fait', 'canceled': 'Annulée'} %}
<span style="{{ classes.get(status, classes['todo']) }}">{{ labels.get(status, status) }}</span>
{%- endmacro %}
{% macro entity_badge(entity_type) -%}
{% if entity_type %}
{% set labels = {'client': 'Client', 'prospect': 'Prospect', 'project': 'Projet', 'campaign': 'Campagne'} %}
<span style="background:#f0f0f0;color:#333;padding:.15rem .4rem;border-radius:.3rem;font-size:.75rem;">
{{ labels.get(entity_type, entity_type|capitalize) }}
</span>
{% endif %}
{%- endmacro %}
{% macro render_task_row(task, show_entity=True, show_due_date=True) -%}
<tr style="border-bottom:1px solid #eee;">
<td style="white-space:nowrap;padding:.5rem .4rem;">
{{ status_badge(task.status) }}
</td>
<td style="padding:.5rem .4rem;">
<div style="font-weight:600;">{{ task.title }}</div>
{% if task.description %}
<div style="font-size:.9rem;color:#666;">{{ task.description }}</div>
{% endif %}
{% if task.metadata and task.metadata.ref %}
<div style="font-size:.85rem;color:#999;">Ref: {{ task.metadata.ref }}</div>
{% endif %}
</td>
{% if show_entity %}
<td style="white-space:nowrap;padding:.5rem .4rem;">
{{ entity_badge(task.entity_type) }}
{% if task.entity_id %}
<small style="color:#888;display:block;">{{ task.entity_id }}</small>
{% endif %}
</td>
{% endif %}
{% if show_due_date %}
<td style="white-space:nowrap;padding:.5rem .4rem;">
<span style="color:#333;">{{ task.due_date }}</span>
{% if task.completed_at and task.status == 'done' %}
<div style="font-size:.8rem;color:#6c757d;">Clôturée: {{ task.completed_at }}</div>
{% endif %}
</td>
{% endif %}
<td style="white-space:nowrap;text-align:right;padding:.5rem .4rem;">
{{ priority_badge(task.priority) }}
</td>
</tr>
{%- endmacro %}
{# Macros de rendu pour les tâches #}
{% macro priority_badge(priority) -%}
{% set cls = {'haute':'bg-danger','normale':'bg-primary','basse':'bg-success'} %}
<span class="badge {{ cls.get(priority, 'bg-primary') }}">{{ priority|capitalize }}</span>
{%- endmacro %}
{% macro status_badge(status) -%}
{% set cls = {'todo':'bg-warning text-dark','done':'bg-success','canceled':'bg-danger'} %}
{% set labels = {'todo': 'À faire', 'done': 'Fait', 'canceled': 'Annulée'} %}
<span class="badge {{ cls.get(status, 'bg-secondary') }}">{{ labels.get(status, status) }}</span>
{%- endmacro %}
{% macro entity_badge(entity_type) -%}
{% if entity_type %}
{% set labels = {'client': 'Client', 'prospect': 'Prospect', 'project': 'Projet', 'campaign': 'Campagne'} %}
<span class="badge bg-secondary-subtle text-dark">{{ labels.get(entity_type, entity_type|capitalize) }}</span>
{% endif %}
{%- endmacro %}
{% macro render_task_row(task, show_entity=True, show_due_date=True, show_actions=False) -%}
<tr>
<td class="text-nowrap">
{{ status_badge(task.status) }}
</td>
<td>
<div class="fw-semibold">{{ task.title }}</div>
{% if task.description %}
<div class="small text-muted">{{ task.description }}</div>
{% endif %}
{% if task.metadata and task.metadata.ref %}
<div class="small text-secondary">Ref: {{ task.metadata.ref }}</div>
{% endif %}
</td>
{% if show_entity %}
<td class="text-nowrap">
{{ entity_badge(task.entity_type) }}
{% if task.entity_id %}
<div class="small text-muted">{{ task.entity_id }}</div>
{% endif %}
</td>
{% endif %}
{% if show_due_date %}
<td class="text-nowrap">
<span>{{ task.due_date }}</span>
{% if task.completed_at and task.status == 'done' %}
<div class="small text-muted">Clôturée: {{ task.completed_at }}</div>
{% endif %}
</td>
{% endif %}
<td class="text-nowrap text-end">
{{ priority_badge(task.priority) }}
</td>
{% if show_actions %}
<td class="text-nowrap text-end">
<div class="d-flex gap-1 justify-content-end">
<button type="button"
class="btn btn-sm btn-outline-secondary btn-edit-task"
data-bs-toggle="modal"
data-bs-target="#editTaskModal"
data-id="{{ task.id }}"
data-title="{{ task.title }}"
data-due_date="{{ task.due_date }}"
data-priority="{{ task.priority }}"
data-description="{{ task.description or '' }}"
data-entity_type="{{ task.entity_type or '' }}"
data-entity_id="{{ task.entity_id or '' }}"
title="Éditer">
<i class="fas fa-pen"></i>
</button>
<div class="btn-group">
<button type="button" class="btn btn-sm btn-outline-primary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false" title="Changer le statut">
<i class="fas fa-exchange-alt"></i>
</button>
<ul class="dropdown-menu dropdown-menu-end">
<li>
<form method="post" action="{{ url_for('tasks.set_status') }}">
<input type="hidden" name="task_id" value="{{ task.id }}">
<input type="hidden" name="status" value="todo">
<button type="submit" class="dropdown-item {% if task.status=='todo' %}active{% endif %}">À faire</button>
</form>
</li>
<li>
<form method="post" action="{{ url_for('tasks.set_status') }}">
<input type="hidden" name="task_id" value="{{ task.id }}">
<input type="hidden" name="status" value="done">
<button type="submit" class="dropdown-item {% if task.status=='done' %}active{% endif %}">Fait</button>
</form>
</li>
<li>
<form method="post" action="{{ url_for('tasks.set_status') }}">
<input type="hidden" name="task_id" value="{{ task.id }}">
<input type="hidden" name="status" value="canceled">
<button type="submit" class="dropdown-item {% if task.status=='canceled' %}active{% endif %}">Annulée</button>
</form>
</li>
</ul>
</div>
<form method="post" action="{{ url_for('tasks.delete_task') }}" onsubmit="return confirm('Supprimer cette tâche ?');">
<input type="hidden" name="task_id" value="{{ task.id }}">
<button type="submit" class="btn btn-sm btn-outline-danger" title="Supprimer">
<i class="fas fa-trash"></i>
</button>
</form>
</div>
</td>
{% endif %}
</tr>
{%- endmacro %}
{% macro render_tasks_table(tasks, show_entity=True, show_due_date=True, empty_text="Aucune tâche", show_actions=False, today=None) -%}
<div class="table-responsive">
<table class="table table-hover align-middle">
{% set cols = 3 + (1 if show_entity else 0) + (1 if show_due_date else 0) + (1 if show_actions else 0) %}
<thead class="table-light">
<tr>
<th>Statut</th>
<th>Tâche</th>
{% if show_entity %}<th>Liée à</th>{% endif %}
{% if show_due_date %}<th>Échéance</th>{% endif %}
<th class="text-end">Priorité</th>
{% if show_actions %}<th class="text-end">Actions</th>{% endif %}
</tr>
</thead>
<tbody>
{% if tasks and tasks|length > 0 %}
{% for task in tasks %}
{{ render_task_row(task, show_entity=show_entity, show_due_date=show_due_date, show_actions=show_actions) }}
{% endfor %}
{% else %}
<tr>
<td colspan="{{ cols }}" class="text-muted"> {{ empty_text }} </td>
</tr>
{% endif %}
</tbody>
</table>
</div>
{%- endmacro %}
{% macro render_tasks_table(tasks, show_entity=True, show_due_date=True, empty_text="Aucune tâche", show_actions=False, today=None) -%}
<div style="overflow:auto;">
<table style="width:100%;border-collapse:collapse;">
{% set cols = 3 + (1 if show_entity else 0) + (1 if show_due_date else 0) + (1 if show_actions else 0) %}
<thead>
<tr style="text-align:left;border-bottom:1px solid #e5e5e5;">
<th style="padding:.6rem .4rem;">Statut</th>
<th style="padding:.6rem .4rem;">Tâche</th>
{% if show_entity %}<th style="padding:.6rem .4rem;">Liée à</th>{% endif %}
{% if show_due_date %}<th style="padding:.6rem .4rem;">Échéance</th>{% endif %}
<th style="padding:.6rem .4rem;text-align:right;">Priorité</th>
{% if show_actions %}<th style="padding:.6rem .4rem;text-align:right;">Actions</th>{% endif %}
</tr>
</thead>
<tbody>
{% if tasks and tasks|length > 0 %}
{% for task in tasks %}
<tr style="border-bottom:1px solid #e5e5e5;">
<td style="white-space:nowrap;padding:.5rem .4rem;">
{{ status_badge(task.status) }}
</td>
<td style="padding:.5rem .4rem;">
<div style="font-weight:600;">{{ task.title }}</div>
{% if task.description %}
<div style="font-size:.9rem;color:#666;">{{ task.description }}</div>
{% endif %}
{% if task.metadata and task.metadata.ref %}
<div style="font-size:.85rem;color:#999;">Ref: {{ task.metadata.ref }}</div>
{% endif %}
</td>
{% if show_entity %}
<td style="white-space:nowrap;padding:.5rem .4rem;">
{{ entity_badge(task.entity_type) }}
{% if task.entity_id %}
<small style="color:#888;display:block;">{{ task.entity_id }}</small>
{% endif %}
</td>
{% endif %}
{% if show_due_date %}
<td style="white-space:nowrap;padding:.5rem .4rem;">
<span style="color:#333;">{{ task.due_date }}</span>
{% if task.completed_at and task.status == 'done' %}
<div style="font-size:.8rem;color:#6c757d;">Clôturée: {{ task.completed_at }}</div>
{% endif %}
</td>
{% endif %}
<td style="white-space:nowrap;text-align:right;padding:.5rem .4rem;">
{{ priority_badge(task.priority) }}
</td>
{% if show_actions %}
<td style="white-space:nowrap;text-align:right;padding:.5rem .4rem;">
<div style="display:flex;gap:.35rem;justify-content:flex-end;">
<button type="button"
class="btn btn-sm btn-outline-secondary btn-edit-task"
data-bs-toggle="modal"
data-bs-target="#editTaskModal"
data-id="{{ task.id }}"
data-title="{{ task.title }}"
data-due_date="{{ task.due_date }}"
data-priority="{{ task.priority }}"
data-description="{{ task.description or '' }}"
data-entity_type="{{ task.entity_type or '' }}"
data-entity_id="{{ task.entity_id or '' }}"
title="Éditer">
<i class="fas fa-pen"></i>
</button>
<div class="btn-group">
<button type="button" class="btn btn-sm btn-outline-primary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false" title="Changer le statut">
<i class="fas fa-exchange-alt"></i>
</button>
<ul class="dropdown-menu dropdown-menu-end">
<li>
<form method="post" action="{{ url_for('tasks.set_status') }}">
<input type="hidden" name="task_id" value="{{ task.id }}">
<input type="hidden" name="status" value="todo">
<button type="submit" class="dropdown-item {% if task.status=='todo' %}active{% endif %}">À faire</button>
</form>
</li>
<li>
<form method="post" action="{{ url_for('tasks.set_status') }}">
<input type="hidden" name="task_id" value="{{ task.id }}">
<input type="hidden" name="status" value="done">
<button type="submit" class="dropdown-item {% if task.status=='done' %}active{% endif %}">Fait</button>
</form>
</li>
<li>
<form method="post" action="{{ url_for('tasks.set_status') }}">
<input type="hidden" name="task_id" value="{{ task.id }}">
<input type="hidden" name="status" value="canceled">
<button type="submit" class="dropdown-item {% if task.status=='canceled' %}active{% endif %}">Annulée</button>
</form>
</li>
</ul>
</div>
<form method="post" action="{{ url_for('tasks.delete_task') }}" onsubmit="return confirm('Supprimer cette tâche ?');">
<input type="hidden" name="task_id" value="{{ task.id }}">
<button type="submit" class="btn btn-sm btn-outline-danger" title="Supprimer">
<i class="fas fa-trash"></i>
</button>
</form>
</div>
</td>
{% endif %}
</tr>
{% endfor %}
{% else %}
<tr>
<td colspan="{{ cols }}" style="padding:.8rem;color:#777;">
{{ empty_text }}
</td>
</tr>
{% endif %}
</tbody>
</table>
</div>
{%- endmacro %}
{% macro entity_badge(entity_type) -%}
{% if entity_type %}
{% set labels = {'client': 'Client', 'prospect': 'Prospect', 'project': 'Projet', 'campaign': 'Campagne'} %}
<span style="background:#f0f0f0;color:#333;padding:.15rem .4rem;border-radius:.3rem;font-size:.75rem;">
{{ labels.get(entity_type, entity_type|capitalize) }}
</span>
{% endif %}
{%- endmacro %}
{% macro render_task_row(task, show_entity=True, show_due_date=True) -%}
<tr>
<td style="white-space:nowrap;">
{{ status_badge(task.status) }}
</td>
<td>
<div style="font-weight:600;">{{ task.title }}</div>
{% if task.description %}
<div style="font-size:.9rem;color:#666;">{{ task.description }}</div>
{% endif %}
{% if task.metadata and task.metadata.ref %}
<div style="font-size:.85rem;color:#999;">Ref: {{ task.metadata.ref }}</div>
{% endif %}
</td>
{% if show_entity %}
<td style="white-space:nowrap;">
{{ entity_badge(task.entity_type) }}
{% if task.entity_id %}
<small style="color:#888;display:block;">{{ task.entity_id }}</small>
{% endif %}
</td>
{% endif %}
{% if show_due_date %}
<td style="white-space:nowrap;">
<span style="color:#333;">{{ task.due_date }}</span>
{% if task.completed_at and task.status == 'done' %}
<div style="font-size:.8rem;color:#6c757d;">Clôturée: {{ task.completed_at }}</div>
{% endif %}
</td>
{% endif %}
<td style="white-space:nowrap;text-align:right;">
{{ priority_badge(task.priority) }}
</td>
</tr>
{%- endmacro %}
{% macro render_tasks_table(tasks, show_entity=True, show_due_date=True, empty_text="Aucune tâche") -%}
<div style="overflow:auto;">
<table style="width:100%;border-collapse:collapse;">
<thead>
<tr style="text-align:left;border-bottom:1px solid #e5e5e5;">
<th style="padding:.6rem .4rem;">Statut</th>
<th style="padding:.6rem .4rem;">Tâche</th>
{% if show_entity %}<th style="padding:.6rem .4rem;">Liée à</th>{% endif %}
{% if show_due_date %}<th style="padding:.6rem .4rem;">Échéance</th>{% endif %}
<th style="padding:.6rem .4rem;text-align:right;">Priorité</th>
</tr>
</thead>
<tbody>
{% if tasks and tasks|length > 0 %}
{% for task in tasks %}
{{ render_task_row(task, show_entity=show_entity, show_due_date=show_due_date) }}
{% endfor %}
{% else %}
<tr>
<td colspan="{{ 3 + (1 if show_entity else 0) + (1 if show_due_date else 0) }}" style="padding:.8rem;color:#777;">
{{ empty_text }}
</td>
</tr>
{% endif %}
</tbody>
</table>
</div>
{%- endmacro %}

127
Templates/tasks/manage.html Normal file
View file

@ -0,0 +1,127 @@
{% extends 'layouts/base.html' %}
{% block title %}Suite Consultance - Tasks{% endblock %}
{% block module_name %}Tasks{% endblock %}
{% block content %}
{% import "tasks/macros.html" as tsk %}
{% import "tasks/manage_macros.html" as tskm %}
<div class="container" style="max-width:1100px;">
<div class="d-flex justify-content-between align-items-center mb-3">
<h1 class="h4 mb-0">Gestion des tâches</h1>
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#createTaskModal">
<i class="fas fa-plus me-1"></i> Nouvelle tâche
</button>
</div>
<form class="card p-3 mb-3" method="get">
<div class="row g-2">
<div class="col-md-3">
<label class="form-label">Statut</label>
<select name="status" class="form-select" onchange="this.form.submit()">
<option value="" {% if not status %}selected{% endif %}>Tous</option>
<option value="todo" {% if status=='todo' %}selected{% endif %}>À faire</option>
<option value="done" {% if status=='done' %}selected{% endif %}>Faites</option>
<option value="canceled" {% if status=='canceled' %}selected{% endif %}>Annulées</option>
</select>
</div>
<div class="col-md-3">
<label class="form-label">Type dentité</label>
<select name="entity_type" class="form-select">
<option value="" {% if not entity_type %}selected{% endif %}>Toutes</option>
<option value="client" {% if entity_type=='client' %}selected{% endif %}>Client</option>
<option value="prospect" {% if entity_type=='prospect' %}selected{% endif %}>Prospect</option>
<option value="project" {% if entity_type=='project' %}selected{% endif %}>Projet</option>
<option value="campaign" {% if entity_type=='campaign' %}selected{% endif %}>Campagne</option>
</select>
</div>
<div class="col-md-4">
<label class="form-label">ID entité</label>
<input type="text" name="entity_id" class="form-control" value="{{ entity_id or '' }}" placeholder="Filtrer par ID">
</div>
<div class="col-md-2 d-flex align-items-end">
<button class="btn btn-outline-secondary w-100" type="submit">
<i class="fas fa-filter me-1"></i> Filtrer
</button>
</div>
</div>
</form>
<div class="card p-3">
{{ tskm.render_tasks_table_actions(tasks, show_entity=True, show_due_date=True) }}
</div>
</div>
{# Modal de création global #}
{% include 'partials/task_create_modal.html' %}
{# Modal d'édition de tâche #}
<div class="modal fade" id="editTaskModal" tabindex="-1" aria-labelledby="editTaskModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<form method="post" action="{{ url_for('tasks.update_task') }}" class="modal-content">
<input type="hidden" name="task_id" id="edit_task_id">
<div class="modal-header">
<h5 class="modal-title" id="editTaskModalLabel"><i class="fas fa-pen me-2"></i>Éditer la tâche</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Fermer"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label class="form-label">Titre *</label>
<input type="text" name="title" id="edit_title" class="form-control" required>
</div>
<div class="mb-3">
<label class="form-label">Échéance *</label>
<input type="date" name="due_date" id="edit_due_date" class="form-control" required>
</div>
<div class="mb-3">
<label class="form-label">Description</label>
<textarea name="description" id="edit_description" class="form-control" rows="3"></textarea>
</div>
<div class="row">
<div class="mb-3 col-md-6">
<label class="form-label">Priorité</label>
<select name="priority" id="edit_priority" class="form-select">
<option value="basse">Basse</option>
<option value="normale">Normale</option>
<option value="haute">Haute</option>
</select>
</div>
<div class="mb-3 col-md-6">
<label class="form-label">Type dentité</label>
<select name="entity_type" id="edit_entity_type" class="form-select">
<option value="">Aucune</option>
<option value="client">Client</option>
<option value="prospect">Prospect</option>
<option value="project">Projet</option>
<option value="campaign">Campagne</option>
</select>
</div>
</div>
<div class="mb-3">
<label class="form-label">ID de lentité (optionnel)</label>
<input type="text" name="entity_id" id="edit_entity_id" class="form-control" placeholder="cli_xxx / pros_xxx / id projet...">
</div>
</div>
<div class="modal-footer">
<button class="btn btn-primary" type="submit"><i class="fas fa-save me-1"></i> Enregistrer</button>
</div>
</form>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script>
document.addEventListener('click', function(e){
const btn = e.target.closest('.btn-edit-task');
if (!btn) return;
document.getElementById('edit_task_id').value = btn.dataset.id || '';
document.getElementById('edit_title').value = btn.dataset.title || '';
document.getElementById('edit_due_date').value = btn.dataset.due_date || '';
document.getElementById('edit_priority').value = btn.dataset.priority || 'normale';
document.getElementById('edit_description').value = btn.dataset.description || '';
document.getElementById('edit_entity_type').value = btn.dataset.entity_type || '';
document.getElementById('edit_entity_id').value = btn.dataset.entity_id || '';
});
</script>
{% endblock %}

View file

@ -0,0 +1,133 @@
{# Macros dédiés à la page de gestion des tâches (évite les collisions de signatures) #}
{% macro _priority_badge(priority) -%}
{% set cls = {'haute':'bg-danger','normale':'bg-primary','basse':'bg-success'} %}
<span class="badge {{ cls.get(priority, 'bg-primary') }}">{{ priority|capitalize }}</span>
{%- endmacro %}
{% macro _status_badge(status) -%}
{% set cls = {'todo':'bg-warning text-dark','done':'bg-success','canceled':'bg-danger'} %}
{% set labels = {'todo': 'À faire', 'done': 'Fait', 'canceled': 'Annulée'} %}
<span class="badge {{ cls.get(status, 'bg-secondary') }}">{{ labels.get(status, status) }}</span>
{%- endmacro %}
{% macro _entity_badge(entity_type) -%}
{% if entity_type %}
{% set labels = {'client': 'Client', 'prospect': 'Prospect', 'project': 'Projet', 'campaign': 'Campagne'} %}
<span class="badge bg-secondary-subtle text-dark">{{ labels.get(entity_type, entity_type|capitalize) }}</span>
{% endif %}
{%- endmacro %}
{% macro render_tasks_table_actions(tasks, show_entity=True, show_due_date=True, empty_text="Aucune tâche") -%}
<div class="table-responsive">
<table class="table table-hover align-middle">
{% set cols = 3 + (1 if show_entity else 0) + (1 if show_due_date else 0) + 1 %}
<thead class="table-light">
<tr>
<th>Statut</th>
<th>Tâche</th>
{% if show_entity %}<th>Liée à</th>{% endif %}
{% if show_due_date %}<th>Échéance</th>{% endif %}
<th class="text-end">Priorité</th>
<th class="text-end">Actions</th>
</tr>
</thead>
<tbody>
{% if tasks and tasks|length > 0 %}
{% for task in tasks %}
<tr>
<td class="text-nowrap">
{{ _status_badge(task.status) }}
</td>
<td>
<div class="fw-semibold">{{ task.title }}</div>
{% if task.description %}
<div class="small text-muted">{{ task.description }}</div>
{% endif %}
{% if task.metadata and task.metadata.ref %}
<div class="small text-secondary">Ref: {{ task.metadata.ref }}</div>
{% endif %}
</td>
{% if show_entity %}
<td class="text-nowrap">
{{ _entity_badge(task.entity_type) }}
{% if task.entity_id %}
<div class="small text-muted">{{ task.entity_id }}</div>
{% endif %}
</td>
{% endif %}
{% if show_due_date %}
<td class="text-nowrap">
<span>{{ task.due_date }}</span>
{% if task.completed_at and task.status == 'done' %}
<div class="small text-muted">Clôturée: {{ task.completed_at }}</div>
{% endif %}
</td>
{% endif %}
<td class="text-nowrap text-end">
{{ _priority_badge(task.priority) }}
</td>
<td class="text-nowrap text-end">
<div class="d-flex gap-1 justify-content-end">
<button type="button"
class="btn btn-sm btn-outline-secondary btn-edit-task"
data-bs-toggle="modal"
data-bs-target="#editTaskModal"
data-id="{{ task.id }}"
data-title="{{ task.title }}"
data-due_date="{{ task.due_date }}"
data-priority="{{ task.priority }}"
data-description="{{ task.description or '' }}"
data-entity_type="{{ task.entity_type or '' }}"
data-entity_id="{{ task.entity_id or '' }}"
title="Éditer">
<i class="fas fa-pen"></i>
</button>
<div class="btn-group">
<button type="button" class="btn btn-sm btn-outline-primary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false" title="Changer le statut">
<i class="fas fa-exchange-alt"></i>
</button>
<ul class="dropdown-menu dropdown-menu-end">
<li>
<form method="post" action="{{ url_for('tasks.set_status') }}">
<input type="hidden" name="task_id" value="{{ task.id }}">
<input type="hidden" name="status" value="todo">
<button type="submit" class="dropdown-item {% if task.status=='todo' %}active{% endif %}">À faire</button>
</form>
</li>
<li>
<form method="post" action="{{ url_for('tasks.set_status') }}">
<input type="hidden" name="task_id" value="{{ task.id }}">
<input type="hidden" name="status" value="done">
<button type="submit" class="dropdown-item {% if task.status=='done' %}active{% endif %}">Fait</button>
</form>
</li>
<li>
<form method="post" action="{{ url_for('tasks.set_status') }}">
<input type="hidden" name="task_id" value="{{ task.id }}">
<input type="hidden" name="status" value="canceled">
<button type="submit" class="dropdown-item {% if task.status=='canceled' %}active{% endif %}">Annulée</button>
</form>
</li>
</ul>
</div>
<form method="post" action="{{ url_for('tasks.delete_task') }}" onsubmit="return confirm('Supprimer cette tâche ?');">
<input type="hidden" name="task_id" value="{{ task.id }}">
<button type="submit" class="btn btn-sm btn-outline-danger" title="Supprimer">
<i class="fas fa-trash"></i>
</button>
</form>
</div>
</td>
</tr>
{% endfor %}
{% else %}
<tr>
<td colspan="{{ cols }}" class="text-muted"> {{ empty_text }} </td>
</tr>
{% endif %}
</tbody>
</table>
</div>
{%- endmacro %}

View file

@ -0,0 +1,16 @@
{% extends "layouts/base.html" %}
{% block title %}Nouvelle tâche liée{% endblock %}
{% block content %}
<div class="container">
<h2>Ajouter une tâche liée</h2>
<p class="text-muted">Cette tâche sera liée à l'entité: {{ entity_type }} (ID: {{ entity_id }})</p>
{% include "partials/task_quick_add_form.html" %}
<div class="mt-3">
<a href="{{ url_for('tasks.tasks_index') }}">&larr; Retour aux tâches</a>
</div>
</div>
{% endblock %}

View file

@ -0,0 +1,10 @@
{% import "tasks/macros.html" as tsk %}
<div style="max-width:1100px;margin:0 auto;padding:1rem;">
<h2 style="margin-top:0;">Tâches du jour</h2>
{{ tsk.render_tasks_table(today_tasks, show_entity=True, show_due_date=False, empty_text="Aucune tâche pour aujourd'hui") }}
</div>
{% import "tasks/macros.html" as tsk %}
<div style="max-width:1100px;margin:0 auto;padding:1rem;">
<h2 style="margin-top:0;">Tâches du jour</h2>
{{ tsk.render_tasks_table(today_tasks, show_entity=True, show_due_date=False, empty_text="Aucune tâche pour aujourd'hui") }}
</div>