first commit
This commit is contained in:
commit
e6c52820cd
227 changed files with 16156 additions and 0 deletions
136
Templates/crm/add_client.html
Normal file
136
Templates/crm/add_client.html
Normal 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 %}
|
||||
129
Templates/crm/add_prospect.html
Normal file
129
Templates/crm/add_prospect.html
Normal 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 %}
|
||||
183
Templates/crm/client_details.html
Normal file
183
Templates/crm/client_details.html
Normal 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
283
Templates/crm/crm.html
Normal 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 %}
|
||||
132
Templates/crm/edit_prospect.html
Normal file
132
Templates/crm/edit_prospect.html
Normal 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 %}
|
||||
199
Templates/crm/prospect_details.html
Normal file
199
Templates/crm/prospect_details.html
Normal 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
14
Templates/devis.json
Normal 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}"
|
||||
}
|
||||
218
Templates/devis/create_devis.html
Normal file
218
Templates/devis/create_devis.html
Normal 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 %}
|
||||
61
Templates/devis/devis.html
Normal file
61
Templates/devis/devis.html
Normal 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 %}
|
||||
323
Templates/email/bulk_email.html
Normal file
323
Templates/email/bulk_email.html
Normal 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
141
Templates/email/config.html
Normal 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 %}
|
||||
128
Templates/email/edit_template.html
Normal file
128
Templates/email/edit_template.html
Normal 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 %}
|
||||
112
Templates/email/email_history.html
Normal file
112
Templates/email/email_history.html
Normal 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 %}
|
||||
96
Templates/email/new_scraping.html
Normal file
96
Templates/email/new_scraping.html
Normal 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 %}
|
||||
91
Templates/email/scraper.html
Normal file
91
Templates/email/scraper.html
Normal 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 %}
|
||||
289
Templates/email/scraping_results.html
Normal file
289
Templates/email/scraping_results.html
Normal 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 %}
|
||||
155
Templates/email/send_email.html
Normal file
155
Templates/email/send_email.html
Normal 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 %}
|
||||
60
Templates/email/templates.html
Normal file
60
Templates/email/templates.html
Normal 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
172
Templates/index.html
Normal 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
324
Templates/layouts/base.html
Normal 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 d’effectuer 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 s’ouvre (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>
|
||||
54
Templates/partials/task_create_modal.html
Normal file
54
Templates/partials/task_create_modal.html
Normal 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 d’entité</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 l’entité (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>
|
||||
14
Templates/partials/task_quick_add_button.html
Normal file
14
Templates/partials/task_quick_add_button.html
Normal 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 %}
|
||||
31
Templates/partials/task_quick_add_form.html
Normal file
31
Templates/partials/task_quick_add_form.html
Normal 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>
|
||||
23
Templates/partials/tasks_list.html
Normal file
23
Templates/partials/tasks_list.html
Normal 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>
|
||||
74
Templates/partials/tasks_sidebar.html
Normal file
74
Templates/partials/tasks_sidebar.html
Normal 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>
|
||||
11
Templates/partials/tasks_today_block.html
Normal file
11
Templates/partials/tasks_today_block.html
Normal 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>
|
||||
83
Templates/projects/all_projects.html
Normal file
83
Templates/projects/all_projects.html
Normal 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 %}
|
||||
57
Templates/projects/edit_project.html
Normal file
57
Templates/projects/edit_project.html
Normal 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 %}
|
||||
68
Templates/projects/list_projects.html
Normal file
68
Templates/projects/list_projects.html
Normal 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 %}
|
||||
47
Templates/projects/project_details.html
Normal file
47
Templates/projects/project_details.html
Normal 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 %}
|
||||
37
Templates/proposition_commercial.json
Normal file
37
Templates/proposition_commercial.json
Normal 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}"]
|
||||
}
|
||||
]
|
||||
}
|
||||
137
Templates/propositions/create_proposition.html
Normal file
137
Templates/propositions/create_proposition.html
Normal 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 %}
|
||||
58
Templates/propositions/propositions.html
Normal file
58
Templates/propositions/propositions.html
Normal 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 %}
|
||||
10
Templates/prospect_details.html
Normal file
10
Templates/prospect_details.html
Normal 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>
|
||||
45
Templates/tasks/email_drafts.html
Normal file
45
Templates/tasks/email_drafts.html
Normal 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') }}">← Retour aux tâches</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
10
Templates/tasks/list.html
Normal file
10
Templates/tasks/list.html
Normal 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
413
Templates/tasks/macros.html
Normal 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
127
Templates/tasks/manage.html
Normal 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 d’entité</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 d’entité</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 l’entité (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 %}
|
||||
133
Templates/tasks/manage_macros.html
Normal file
133
Templates/tasks/manage_macros.html
Normal 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 %}
|
||||
16
Templates/tasks/quick_add.html
Normal file
16
Templates/tasks/quick_add.html
Normal 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') }}">← Retour aux tâches</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
10
Templates/tasks/today.html
Normal file
10
Templates/tasks/today.html
Normal 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>
|
||||
Loading…
Add table
Add a link
Reference in a new issue