first commit

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

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

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