<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta content="width=device-width, initial-scale=1.0" name="viewport">
<title>
Gestion Stock -TuferGroupe-
{% block title %}{% endblock %}
</title>
<meta content="" name="description">
<meta content="" name="keywords">
<link href={{asset ('assets/img/ls.png')}} rel="icon">
<link href={{asset ('assets/img/ls.png')}} rel="apple-touch-icon">
<link href="https://fonts.gstatic.com" rel="preconnect">
<link href="https://fonts.googleapis.com/css?family=Open+Sans:300,300i,400,400i,600,600i,700,700i|Nunito:300,300i,400,400i,600,600i,700,700i|Poppins:300,300i,400,400i,500,500i,600,600i,700,700i" rel="stylesheet">
<link href={{asset ("assets/vendor/bootstrap/css/bootstrap.min.css")}} rel="stylesheet">
<link href={{asset ("assets/vendor/bootstrap-icons/bootstrap-icons.css")}} rel="stylesheet">
<link href={{asset ("assets/vendor/boxicons/css/boxicons.min.css")}} rel="stylesheet">
<link href={{asset ("assets/vendor/quill/quill.snow.css")}} rel="stylesheet">
<link href={{asset ("assets/vendor/quill/quill.bubble.css")}} rel="stylesheet">
<link href={{asset ("assets/vendor/remixicon/remixicon.css")}} rel="stylesheet">
<link href={{asset ("assets/vendor/simple-datatables/style.css")}} rel="stylesheet">
<link href={{asset ("assets/css/style.css")}} rel="stylesheet">
{% block stylesheets %}{% endblock %}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js"></script>
<style>
/* Styles pour la recherche et responsive */
.search-container {
position: relative;
width: 100%;
max-width: 500px;
}
.form-control.is-invalid:focus {
border-color: #dc3545;
box-shadow: 0 0 0 0.25rem rgba(220, 53, 69, 0.25);
}
#search-error-message {
color: #dc3545;
font-size: 0.875em;
margin-top: 5px;
animation: fadeIn 0.3s ease-in;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.autocomplete-results {
position: absolute;
z-index: 1050;
width: 100%;
max-height: 300px;
overflow-y: auto;
background-color: white;
border: 1px solid #ced4da;
border-radius: 0 0 0.375rem 0.375rem;
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
display: none;
top: 100%;
left: 0;
margin-top: -1px;
}
.autocomplete-item {
padding: 0.75rem 1rem;
cursor: pointer;
border-bottom: 1px solid #f0f0f0;
display: flex;
align-items: center;
transition: background-color 0.15s ease-in-out;
}
.autocomplete-item:last-child {
border-bottom: none;
}
.autocomplete-item:hover {
background-color: #f8f9fa;
}
.autocomplete-item i {
color: #6c757d;
width: 18px;
text-align: center;
margin-right: 10px;
}
#advancedSearchMenu {
width: 320px !important;
padding: 1.25rem;
border-radius: 0.5rem;
border: none;
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
}
#searchForm {
position: relative;
flex: 1;
max-width: 500px;
}
#activeFilters {
margin-top: 0.5rem;
font-size: 0.85rem;
color: #6c757d;
padding: 0.375rem 0.75rem;
background-color: #f8f9fa;
border-radius: 0.375rem;
display: inline-block;
border: 1px solid #e9ecef;
}
.filter-tag {
display: inline-block;
padding: 0.25rem 0.5rem;
margin-right: 0.25rem;
background-color: #e9ecef;
border-radius: 1rem;
font-size: 0.8rem;
color: #495057;
}
.filter-tag i {
margin-left: 0.25rem;
cursor: pointer;
color: #6c757d;
}
.filter-tag i:hover {
color: #dc3545;
}
/* Header responsif */
.header {
padding: 0.75rem 1rem;
}
.header .logo img {
height: 40px;
width: auto;
}
.header-nav {
align-items: center;
}
/* Sidebar responsive */
.sidebar {
transition: all 0.3s ease;
}
.nav-item .nav-link {
padding: 0.75rem 1rem;
border-radius: 0.375rem;
margin: 0.125rem 0;
transition: all 0.15s ease-in-out;
}
.nav-item .nav-link:hover {
background-color: rgba(13, 110, 253, 0.1);
color: #0d6efd;
}
.nav-item .nav-link.active {
background-color: #0d6efd;
color: white;
}
/* Panier badge */
.badge-number {
position: absolute;
top: -8px;
right: -8px;
min-width: 18px;
height: 18px;
border-radius: 50%;
font-size: 0.75rem;
display: flex;
align-items: center;
justify-content: center;
}
/* Avatar responsive */
.avatar {
width: 38px;
height: 38px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: 600;
font-size: 0.9rem;
}
/* Responsive adjustments */
@media (max-width: 992px) {
.search-container {
max-width: 100%;
}
#advancedSearchMenu {
width: 280px !important;
padding: 1rem;
}
.header .logo img {
height: 35px;
}
.avatar {
width: 34px;
height: 34px;
font-size: 0.8rem;
}
}
@media (max-width: 768px) {
.header {
padding: 0.5rem 0.75rem;
}
.search-container {
margin: 0.5rem 0;
}
#advancedSearchMenu {
width: 100% !important;
max-width: 300px;
left: 0 !important;
right: 0 !important;
margin: 0 auto;
}
.header-nav ul {
gap: 0.5rem;
}
.nav-item .nav-link {
padding: 0.5rem;
}
}
@media (max-width: 576px) {
.header .logo img {
height: 30px;
}
.avatar {
width: 30px;
height: 30px;
font-size: 0.75rem;
}
.search-container .input-group .form-control {
font-size: 0.9rem;
}
.autocomplete-item {
padding: 0.5rem 0.75rem;
font-size: 0.9rem;
}
}
/* Animation pour la sidebar sur mobile */
@media (max-width: 1199px) {
.sidebar {
left: -300px;
}
.sidebar.open {
left: 0;
}
.main {
margin-left: 0;
}
}
/* Amélioration du dropdown menu */
.dropdown-menu {
border: none;
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
border-radius: 0.5rem;
}
.dropdown-item {
padding: 0.5rem 1rem;
transition: all 0.15s ease-in-out;
}
.dropdown-item:hover {
background-color: #f8f9fa;
color: #0d6efd;
}
/* Style pour les formulaires dans le menu avancé */
#advancedSearchMenu .form-label {
font-weight: 600;
color: #495057;
margin-bottom: 0.5rem;
}
#advancedSearchMenu .form-select,
#advancedSearchMenu .form-control {
border-radius: 0.375rem;
border: 1px solid #ced4da;
transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
}
#advancedSearchMenu .form-select:focus,
#advancedSearchMenu .form-control:focus {
border-color: #86b7fe;
box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);
}
/* Footer responsive */
.footer {
padding: 1rem;
text-align: center;
}
.footer .copyright,
.footer .credits {
margin: 0.5rem 0;
}
@media (max-width: 576px) {
.footer {
font-size: 0.875rem;
}
}
</style>
</head>
<body>
<!-- ======= Header ======= -->
<header id="header" class="header fixed-top d-flex align-items-center">
{% if not is_granted('ROLE_LIVREUR') %}
<div class="d-flex align-items-center justify-content-between">
<a href="{{ path('app_dashboard') }}" class="logo d-flex align-items-center">
<img src="{{ asset('assets/img/ls.png') }}" alt="logo" class="img-fluid">
</a>
<i class="bi bi-list toggle-sidebar-btn d-xl-none ms-3"></i>
</div>
{% endif %}
{% if is_granted('ROLE_LIVREUR') %}
<div class="d-flex align-items-center justify-content-between">
<a href="{{ path('app_livreur_dashboard') }}" class="logo d-flex align-items-center">
<img src="{{ asset('assets/img/ls.png') }}" alt="logo" class="img-fluid">
</a>
<i class="bi bi-list toggle-sidebar-btn d-xl-none ms-3"></i>
</div>
{% endif %}
<div class="d-flex justify-content-center flex-grow-1 mx-2 mx-md-3">
{% if not is_granted('ROLE_LIVREUR') %}
<div class="search-container">
<form class="d-flex align-items-center w-100" method="GET" action="{{ path('produit_search') }}" id="searchForm">
<div class="input-group">
<input type="text"
name="search"
id="searchInput"
class="form-control"
placeholder="Rechercher..."
value="{{ app.request.query.get('search') }}"
autocomplete="off">
<button class="btn btn-outline-primary" type="submit" aria-label="Rechercher">
<i class="bi bi-search"></i>
</button>
<button class="btn btn-outline-secondary dropdown-toggle"
type="button"
id="advancedSearchButton"
data-bs-toggle="dropdown"
aria-expanded="false"
aria-label="Filtres avancés">
<i class="bi bi-sliders"></i>
</button>
<div class="dropdown-menu dropdown-menu-end" id="advancedSearchMenu">
<h6 class="dropdown-header fw-bold">Filtres avancés</h6>
<div class="mb-3">
<label class="form-label">Type de recherche</label>
<select name="search_type" class="form-select form-select-sm">
<option value="all" selected>Tous</option>
<option value="produits">Produits</option>
<option value="clients">Clients</option>
<option value="vendeurs">Vendeurs</option>
<option value="boutiques">Boutiques</option>
</select>
</div>
<div class="mb-3">
<label class="form-label">Trier par</label>
<select name="sort" class="form-select form-select-sm">
<option value="nom">Nom</option>
<option value="prix_asc">Prix (croissant)</option>
<option value="prix_desc">Prix (décroissant)</option>
<option value="stock">Stock disponible</option>
</select>
</div>
<button type="submit" class="btn btn-primary btn-sm w-100">
<i class="bi bi-check2"></i> Appliquer
</button>
</div>
</div>
</form>
<div id="autocompleteResults" class="autocomplete-results"></div>
</div>
{% endif %}
</div>
<nav class="header-nav ms-auto">
<ul class="d-flex align-items-center mb-0 list-unstyled">
<li class="nav-item me-2">
<a class="nav-link nav-icon position-relative" href="{{ path('app_panier') }}" aria-label="Panier">
<i class="bi bi-cart-fill fs-5"></i>
<span id="panier-badge" class="badge bg-primary badge-number"></span>
</a>
</li>
{% if app.user %}
<li class="nav-item dropdown">
<a class="nav-link nav-profile d-flex align-items-center"
href="#"
data-bs-toggle="dropdown"
aria-expanded="false">
{% set firstLetter = firstLetter(app.user) %}
<div class="avatar btn-primary d-flex align-items-center justify-content-center">
{{ firstLetter }}
</div>
</a>
<ul class="dropdown-menu dropdown-menu-end profile">
<li class="dropdown-header text-center py-2">
<h6 class="mb-1">{{ app.user.fullName }}</h6>
<small class="text-muted">{{ app.user.roles[0] }}</small>
</li>
<li><hr class="dropdown-divider"></li>
<li>
<a class="dropdown-item d-flex align-items-center" href="{{ path('app_logout') }}">
<i class="bi bi-box-arrow-right me-2"></i>
<span>Déconnexion</span>
</a>
</li>
</ul>
</li>
{% endif %}
</ul>
</nav>
</header>
<!-- End Header -->
<!-- ======= Sidebar ======= -->
{% if not is_granted('ROLE_LIVREUR') %}
<aside id="sidebar" class="sidebar">
<ul class="sidebar-nav" id="sidebar-nav">
{% if is_granted('ROLE_SUPER_ADMIN') or is_granted('ROLE_ADMIN') %}
<li class="nav-item">
<a class="nav-link collapsed" href="{{ path('app_dashboard') }}">
<i class="bi bi-grid me-2"></i>
<span>Dashboard</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link collapsed" href="{{ path('app_boutique_index') }}">
<i class="bi bi-shop me-2"></i>
<span>Boutiques</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link collapsed" href="{{ path('app_entree_stock_index') }}">
<i class="bi bi-box-arrow-in-down me-2"></i>
<span>Entrées</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link collapsed" href="{{ path('app_sortie_stock_index') }}">
<i class="bi bi-box-arrow-up me-2"></i>
<span>Sorties</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link collapsed" href="{{ path('app_client_index') }}">
<i class="bi bi-people me-2"></i>
<span>Clients</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link collapsed" href="{{ path('app_user_index') }}">
<i class="bi bi-person-gear me-2"></i>
<span>Utilisateurs</span>
</a>
</li>
{% endif %}
<li class="nav-item">
<a class="nav-link collapsed" href="{{ path('app_dashboard') }}">
<i class="bi bi-grid me-2"></i>
<span>Dashboard</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link collapsed" href="{{ path('app_categorie_index') }}">
<i class="bi bi-tags me-2"></i>
<span>Catégories</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link collapsed" href="{{ path('app_produit_index') }}">
<i class="bi bi-box-seam me-2"></i>
<span>Produits</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link collapsed" href="{{ path('app_vente_index') }}">
<i class="bi bi-cart me-2"></i>
<span>Ventes</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link collapsed" href="{{ path('app_facture_index') }}">
<i class="bi bi-receipt me-2"></i>
<span>Factures</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link collapsed" href="{{ path('app_service_index') }}">
<i class="bi bi-gear me-2"></i>
<span>Services</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link collapsed" href="{{ path('app_facture_service_index') }}">
<i class="bi bi-file-earmark-text me-2"></i>
<span>Factures Services</span>
</a>
</li>
{% if is_granted('ROLE_SUPER_ADMIN') %}
<li class="nav-item">
<a class="nav-link collapsed" href="{{ path('app_client_echeances') }}">
<i class="bi bi-cash-stack me-2"></i>
<span>Dettes Clients</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link collapsed" href="{{ path('app_recettes') }}">
<i class="bi bi-cash-coin me-2"></i>
<span>Recettes</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link collapsed" href="{{ path('app_action_log_index') }}">
<i class="bi bi-activity me-2"></i>
<span>Historique</span>
</a>
</li>
{% endif %}
</ul>
</aside>
{% endif %}
<!-- End Sidebar-->
<main id="main" class="main">
<div class="pagetitle">
<h1 class="mb-2">
{% block subtitle %}{% endblock %}
</h1>
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item">
<a href="{{ path('app_dashboard') }}">
{% block subtitle2 %}Accueil{% endblock %}
</a>
</li>
</ol>
</nav>
</div>
{% block body %}{% endblock %}
</main>
<!-- ======= Footer ======= -->
<footer id="footer" class="footer bg-light border-top">
<div class="container-fluid">
<div class="row">
<div class="col-md-6">
<div class="copyright">
© Copyright <strong><span>GestStock TuferGroupe</span></strong>.
Tous droits réservés
</div>
</div>
<div class="col-md-6">
<div class="credits text-md-end">
Développé par <a href="mailto:jsias288@gmail.com" class="text-decoration-none">jsias288@gmail.com</a>
</div>
</div>
</div>
</div>
</footer>
<!-- End Footer -->
<a href="#" class="back-to-top d-flex align-items-center justify-content-center rounded-circle">
<i class="bi bi-arrow-up-short"></i>
</a>
<!-- Vendor JS Files -->
<script src={{asset ("assets/vendor/apexcharts/apexcharts.min.js")}}></script>
<script src={{asset ("assets/vendor/bootstrap/js/bootstrap.bundle.min.js")}}></script>
<script src={{asset ("assets/vendor/chart.js/chart.umd.js")}}></script>
<script src={{asset ("assets/vendor/echarts/echarts.min.js")}}></script>
<script src={{asset ("assets/vendor/quill/quill.min.js")}}></script>
<script src={{asset ("assets/vendor/simple-datatables/simple-datatables.js")}}></script>
<script src={{asset ("assets/vendor/tinymce/tinymce.min.js")}}></script>
<script src={{asset ("assets/vendor/php-email-form/validate.js")}}></script>
<script src={{asset ("assets/js/main.js")}}></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Récupération des éléments
const searchInput = document.getElementById('searchInput');
const searchForm = document.getElementById('searchForm');
const advancedSearchButton = document.getElementById('advancedSearchButton');
const advancedSearchMenu = document.getElementById('advancedSearchMenu');
const autocompleteContainer = document.getElementById('autocompleteResults');
const AUTOCOMPLETE_URL = "{{ path('produit_autocomplete') }}";
let debounceTimer;
// VALIDATION DE LA RECHERCHE
searchForm?.addEventListener('submit', function(e) {
const searchValue = searchInput.value.trim();
if (searchValue === '' || searchValue.length < 2) {
e.preventDefault();
searchInput.classList.add('is-invalid');
searchInput.focus();
showSearchError('Veuillez saisir au moins 2 caractères pour effectuer une recherche.');
return false;
}
searchInput.classList.remove('is-invalid');
});
// Fonction pour afficher un message d'erreur
function showSearchError(message) {
const existingError = document.getElementById('search-error-message');
if (existingError) {
existingError.remove();
}
const errorDiv = document.createElement('div');
errorDiv.id = 'search-error-message';
errorDiv.className = 'invalid-feedback d-block';
errorDiv.textContent = message;
searchInput.parentNode.insertBefore(errorDiv, searchInput.nextSibling);
setTimeout(() => {
if (errorDiv && errorDiv.parentNode) {
errorDiv.remove();
}
}, 3000);
}
// Autocomplétion
searchInput?.addEventListener('input', function() {
const searchValue = this.value.trim();
if (searchValue.length >= 2) {
this.classList.remove('is-invalid');
const errorMessage = document.getElementById('search-error-message');
if (errorMessage) {
errorMessage.remove();
}
}
clearTimeout(debounceTimer);
if (this.value.length < 2) {
autocompleteContainer.innerHTML = '';
autocompleteContainer.style.display = 'none';
return;
}
debounceTimer = setTimeout(() => {
fetch(`${AUTOCOMPLETE_URL}?q=${encodeURIComponent(this.value)}`)
.then(response => response.json())
.then(data => {
autocompleteContainer.innerHTML = '';
if (data.length > 0) {
autocompleteContainer.style.display = 'block';
data.forEach(item => {
const suggestionElement = document.createElement('div');
suggestionElement.className = 'autocomplete-item';
let icon = '';
switch(item.type) {
case 'produit':
icon = '<i class="bi bi-box me-2"></i>';
break;
case 'client':
icon = '<i class="bi bi-person me-2"></i>';
break;
case 'vendeur':
icon = '<i class="bi bi-person-badge me-2"></i>';
break;
case 'boutique':
icon = '<i class="bi bi-shop me-2"></i>';
break;
}
suggestionElement.innerHTML = `${icon} <span>${item.value}</span>`;
suggestionElement.addEventListener('click', function() {
searchInput.value = item.value;
autocompleteContainer.style.display = 'none';
searchInput.classList.remove('is-invalid');
const searchTypeSelect = document.querySelector('select[name="search_type"]');
if (searchTypeSelect && item.type) {
let filterType;
switch(item.type) {
case 'produit': filterType = 'produits'; break;
case 'client': filterType = 'clients'; break;
case 'vendeur': filterType = 'vendeurs'; break;
case 'boutique': filterType = 'boutiques'; break;
default: filterType = 'all';
}
searchTypeSelect.value = filterType;
}
searchForm.submit();
});
autocompleteContainer.appendChild(suggestionElement);
});
} else {
autocompleteContainer.style.display = 'none';
}
})
.catch(error => {
console.error('Erreur lors de la récupération des suggestions:', error);
});
}, 300);
});
// Masquer les résultats d'autocomplétion lors d'un clic ailleurs
document.addEventListener('click', function(e) {
if (e.target !== searchInput && !autocompleteContainer.contains(e.target)) {
autocompleteContainer.style.display = 'none';
}
});
// Empêcher la fermeture du menu avancé lors d'un clic à l'intérieur
advancedSearchMenu?.addEventListener('click', function(e) {
e.stopPropagation();
});
});
// Fonctions existantes pour le panier et les données
$(document).ready(function() {
$(".add-to-cart").click(function(e) {
e.preventDefault();
var url = $(this).attr("href");
$.ajax({
url: url,
method: "GET",
success: function(response) {
if (response === 'error') {
alert('La quantité en stock est inférieure ou égale à la quantité d\'alerte. Impossible d\'ajouter le produit.');
} else {
$("#panier-content").html(response);
updatePanierBadge();
}
}
});
});
});
function updatePanierBadge() {
$.ajax({
url: "{{ path('count_panier') }}",
method: "GET",
success: function (response) {
$("#panier-badge").text(response.panierSize);
}
});
}
updatePanierBadge();
document.addEventListener('DOMContentLoaded', function () {
function updateSalesData(filter) {
fetch('{{ path('app_dashboard_data') }}?filter=' + filter).then(response => response.json()).then(data => {
const salesCountElement = document.querySelector('#sales-count');
const salesTitleElement = document.querySelector('#sales-title');
salesCountElement.textContent = data.nombreVentes;
switch (filter) {
case 'today': salesTitleElement.textContent = 'Ventes | Aujourd\'hui';
break;
case 'month': salesTitleElement.textContent = 'Ventes | Ce mois-ci';
break;
case 'year': salesTitleElement.textContent = 'Ventes | Cette année';
break;
default: salesTitleElement.textContent = 'Ventes | Aujourd\'hui';
break;
}
}).catch(error => console.error('Error:', error));
}
const dropdownItems = document.querySelectorAll('.sales-dropdown-item');
dropdownItems.forEach(item => {
item.addEventListener('click', function (event) {
event.preventDefault();
const filter = this.getAttribute('data-filter');
updateSalesData(filter);
});
});
updateSalesData('today');
function updateRevenueData(filter) {
fetch('{{ path('app_dashboard_data2') }}?filter=' + filter).then(response => response.json()).then(data => {
const revenueCountElement = document.querySelector('#revenue-count');
const revenueTitleElement = document.querySelector('#revenue-title');
revenueCountElement.textContent = data.totalRevenue.toFixed(2);
switch (filter) {
case 'today': revenueTitleElement.textContent = 'Revenue | Aujourd\'hui';
break;
case 'month': revenueTitleElement.textContent = 'Revenue | Ce mois-ci';
break;
case 'year': revenueTitleElement.textContent = 'Revenue | Cette année';
break;
default: revenueTitleElement.textContent = 'Revenue | Aujourd\'hui';
break;
}
}).catch(error => console.error('Error:', error));
}
const revenueDropdownItems = document.querySelectorAll('.revenue-dropdown-item');
revenueDropdownItems.forEach(item => {
item.addEventListener('click', function (event) {
event.preventDefault();
const filter = this.getAttribute('data-filter');
updateRevenueData(filter);
});
});
updateRevenueData('today');
});
</script>
{% block javascripts %}{% endblock %}
</body>
</html>