<?php
namespace App\Controller\Admin;
use App\Doctrine\Type\ClientType;
use App\Entity\Activity;
use App\Entity\Comment;
use App\Entity\Document;
use App\Entity\User;
use App\Form\AdministationChangePwType;
use App\Form\UserType;
use App\Repository\UserRepository;
use App\Repository\SupplierRepository;
use App\Service\GlobalVariables;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Form\Form;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\HttpFoundation\JsonResponse;
use App\Form\UserAddType;
use App\Entity\Link;
use App\Form\FilterUserType;
use App\Repository\AddressRepository;
use App\Entity\Address;
use App\Form\AddressType;
use App\Form\AddressUserType;
use App\Form\AddCodePromotionType;
use App\Repository\DocumentRepository;
use App\Service\RightService;
use App\Form\AdministationType;
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
use App\Service\ActivityService;
use Symfony\Component\Form\Extension\Core\Type\DateTimeType;
use Symfony\Component\Form\Extension\Core\Type\TimeType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use App\Form\ManageBlockType;
use App\Service\DailyBlockService;
use Psr\Log\LoggerInterface;
use App\Repository\UserPromotionHistoryRepository;
use App\Repository\PromotionRepository;
use App\Entity\UserPromotionHistory;
use Doctrine\DBAL\Connection;
use App\Entity\Category;
/**
* @Route("/user")
*/
class UserController extends AbstractController {
use AccessTrait;
private $addressRepository;
private $userRepository;
private $documentRepository;
private $rightService;
private $activityService;
public function __construct(
AddressRepository $addressRepository,
UserRepository $userRepository,
SupplierRepository $supplierRepository,
DocumentRepository $documentRepository,
RightService $rightService,
ActivityService $activityService,
GlobalVariables $globalVariables
) {
$this->addressRepository = $addressRepository;
$this->userRepository = $userRepository;
$this->supplierRepository = $supplierRepository;
$this->documentRepository = $documentRepository;
$this->rightService = $rightService;
$this->activityService= $activityService;
$this->globalVariables = $globalVariables;
}
/**
* @Route("/show/{id}", name="user_show", methods={"GET","POST"}, options = { "expose" = true})
*/
public function show(Request $request, User $user,EntityManagerInterface $em,DocumentRepository $documentRepository,UserPromotionHistoryRepository $historyRepo): Response {
$this->hasRight($request,'USERS');
$form = $this->createForm(AddCodePromotionType::class, null);
$form->handleRequest($request);
if( $form->isSubmitted() && $form->isValid()) {
$em->flush();
return $this->redirect($this->generateUrl('user_show', ['id' => $user->getId()]));
}
$cmdEnAttente = $user->getDocuments()->filter(function($element) {
return in_array($element->getStatus(), ['en-attente']) and $element->getType() =='commande';
})->count();
$cmdAccepte = $user->getDocuments()->filter(function($element) {
return in_array($element->getStatus(), ['accepte']) and $element->getType() =='commande';
})->count();
$cmdExpedie = $user->getDocuments()->filter(function($element) {
return in_array($element->getStatus(), ['expedie']) and $element->getType() =='commande';
})->count();
$cmdLivre = $user->getDocuments()->filter(function($element) {
return in_array($element->getStatus(), ['livre']) and $element->getType() =='commande';
})->count();
$cmdAnnule = $user->getDocuments()->filter(function($element) {
return in_array($element->getStatus(), ['annule']) and $element->getType() =='commande';
})->count();
$cmdEnRetour = $user->getDocuments()->filter(function($element) {
return in_array($element->getStatus(), ['retourne','retour-en-cours']) and $element->getType() =='commande';
})->count();
$echangeEnAttente = $user->getDocuments()->filter(function($element) {
return $element->getStatus() =='en-attente' and $element->getType() =='echange';
})->count();
$nbCmdTotal = $user->getDocuments()->filter(function($element) {
return $element->getType() =='commande';
})->count();
$nbBeTotal = $user->getDocuments()->filter(function($element) {
return $element->getType() =='echange';
})->count();
$nbFacTotal = $user->getDocuments()->filter(function($element) {
return $element->getType() =='facture';
})->count();
$totalAchat = $user->getDocuments()->filter(function($element) {
return in_array($element->getStatus(), ['expedie','livre','paye','facture']) && $element->getType() == 'commande';
})->map(function($element) {
return $element->getTotalAmountTtc() -$element->getDeliveryTotal();
})->reduce(function($carry, $item) {
return $carry + $item;
}, 0);
$categoriesStat = $this->getDoctrine()->getRepository(Document::class)->countByProductCategory($user);
$statsParSource = $this->getDoctrine()->getRepository(Document::class)->countBySource($user);
$cmdByYear = $this->getDoctrine()->getRepository(Document::class)->countByYear($user);
$lastCommande = $em->getRepository(Document::class)->createQueryBuilder('d')
->select('MAX(d.createdAt)')
->where('d.client = :client')
->andWhere('d.type = :type')
->setParameter('client', $user)
->setParameter('type', 'commande')
->getQuery()
->getSingleScalarResult();
$lastCommandeDate = $lastCommande ? new \DateTime($lastCommande) : null;
$daysSinceLastCommande = $lastCommandeDate ? (new \DateTime())->diff($lastCommandeDate)->days : null;
$averageDelay = $documentRepository->getAverageDelayBetweenOrders($user);
$averageAOV = $documentRepository->getAverageOrderValue($user, 'ttc');
$promo = $user->getPromotion();
$usedCount = null;
if ($promo) { $usedCount = $documentRepository->countValidUsesByClient($promo, $user); }
$userPromotionHistories = $historyRepo->findBy(
['user' => $user],
['startedAt' => 'DESC']
);
//$statsParSource = $documentRepository->countSource($user);
//dump($cmdByYear); die;
return $this->render('@admin/user/fiche_client.html.twig', [
'user' => $user,
'form' => $form->createView(),
'statistiques' => compact('cmdEnAttente','cmdAccepte','cmdExpedie','cmdLivre','cmdAnnule','cmdEnRetour','echangeEnAttente','nbCmdTotal','nbFacTotal','nbBeTotal','totalAchat'),
'categoriesStat' => $categoriesStat,
'statsParSource' => $statsParSource,
'cmdByYear' => $cmdByYear,
'lastCommandeDate' => $lastCommandeDate,
'daysSinceLastCommande' => $daysSinceLastCommande,
'promoUsedCount' => $usedCount,
'userPromotionHistories' => $userPromotionHistories,
'averageDelay' => $averageDelay,
'averageAOV' => $averageAOV,
]);
}
/**
* @Route("/listuser/{type}", name="list_user", methods="GET|POST", options = { "expose" = true})
*/
public function listData($type, Request $request, UserRepository $users) {
$clientType = $request->get('clientType');
if ($clientType === null) {
// Log ou message d'erreur
error_log('clientType non transmis');
} else {
error_log('clientType transmis: ' . $clientType);
}
//$clientType = $request->get('form')['clientType'] ?? null;
//dump($clientType);
// dump($request->query->all());
$pagination = $request->get('pagination');
$page = $pagination['page'] - 1;
$limit = $pagination['perpage'];
$dateB = null;
if( $request->get('dateBefore')) {
$dateBefore = new \DateTime($request->get('dateBefore'));
$dateB = $dateBefore->format('Y-m-d h:m:s');
}
$dateA = null;
if( $request->get('dateAfter')) {
$dateAfter = new \DateTime($request->get('dateAfter'));
$dateA = $dateAfter->format('Y-m-d h:m:s');
}
$sortField=($request->get('sort')&&$type== "client")?$request->get('sort')["field"]:'createdAt';
$sortType=$request->get('sort')?$request->get('sort')["sort"]:'DESC';
//$count = $users->countUsers($request->get('username'), $request->get('phone'), $type, $request->get('email'), $dateB, $dateA, $request->get('region'), $request->get('clientType'), $request->get('city'));
$count = $users->countUsers($request->get('username'), $request->get('phone'), $type, $request->get('email'),$dateB, $dateA, $request->get('region'), $request->get('clientType'),$request->get('city'));
$output = array(
'data' => array(),
'meta' => array(
'page' => $pagination['page'],
'perpage' => $limit,
"pages" => ceil($count / $limit),
"total" => $count,
)
);
if($type== "client"){
$entities = $users->clientList($page, $limit, $request->get('username'), $request->get('phone'), $type, $request->get('email'), $dateB, $dateA, $request->get('region'), $request->get('clientType'), $request->get('city'),$sortField,$sortType);
foreach ($entities as $entity) {
$data = [
'id' => $entity["customer"]->getId(),
'userName' => $entity["customer"]->getUsername(),
'firstName' => $entity["customer"]->getCivility() . ' ' . $entity["customer"]->getFirstName() ,
'phone' => $entity["customer"]->getPhone() == "" ? 'Pas mentionné' : $entity["customer"]->getPhone(),
'email' => $entity["customer"]->getEmail() == "" ? 'Pas mentionné' : $entity["customer"]->getEmail(),
'adress' => $entity["customer"]->getAdress(),
'region' => $entity["customer"]->getRegion(),
'clientType' => $entity["customer"]->getClientType(),
'city' => $entity["customer"]->getCity(),
'description' => $entity["customer"]->getDescription() ?? 'Pas mentionné',
'createdAt' => $entity["customer"]->getCreatedAt()->format('d/m/Y'),
'total_Achat'=>$entity["total_Achat"],
'date_last_cmd'=>$this->convertDate($entity["date_last_cmd"]),
'nbCmdTotal' => $entity["nbCmdTotal"],
'cmdOK' => $entity["cmdOK"],
'cmdEnRetour' => $entity["cmdEnRetour"],
'cmdAnnulee' => $entity["cmdAnnulee"],
'nbEchangeTotal' => $entity["nbEchangeTotal"],
'actions' => 'actions'
];
//dump($entity["customer"]->getClientType());
$output['data'][]=$data;
}
}else{
$entities = $users->search($page, $limit, $request->get('username'), $request->get('phone'), $type, $request->get('email'), $dateB, $dateA, $request->get('region'), $request->get('clientType'), $request->get('city'),$sortField,$sortType);
foreach ($entities as $entity) {
$data = [
'id' => $entity->getId(),
'name' => $entity->getCivility() . ' ' . $entity->getFirstName() ,
'phone' => $entity->getPhone() == "" ? 'Pas mentionné' : $entity->getPhone(),
'email' => $entity->getEmail() == "" ? 'Pas mentionné' : $entity->getEmail(),
'description' => $entity->getDescription() ?? 'Pas mentionné',
'clientType' => $entity->getClientType(),
'createdAt' => $entity->getCreatedAt()->format('d/m/Y'),
'actions' => 'actions'
];
$output['data'][]=$data;
}
}
return new JsonResponse($output);
}
/**
* @Route("/index/{type}", name="user_index", methods={"GET"})
*/
public function index($type,Request $request): Response {
$rights = $this->rightService->getAllRights($this->getUser());
if( !in_array('USERS', $rights)) {
$request->getSession()->getFlashBag()->add('danger', "Accès refusé");
return $this->redirect($this->generateUrl('index'));
}
$form = $this->createForm(FilterUserType::class, null, ['is_client_filter' => true,]);
return $this->render('@admin/user/list_users.html.twig', [
'form' => $form->createView(),
'type' => $type,
'rights' => $rights
]);
}
public function modal(Request $request): Response {
$user = new User();
$form = $this->createForm(UserAddType::class, $user);
return $this->render('@admin/user/_formAddUserModal.html.twig', [
'user' => $user,
'form' => $form->createView(),
]);
/*$request->getSession()->getFlashBag()->add('danger', "Accès refusé");
return $this->redirect($this->generateUrl('index'));*/
}
/**
* @Route("/new", name="user_new", methods={"GET","POST"})
*/
public function new(Request $request,EntityManagerInterface $em): Response {
$rights = $this->rightService->getAllRights($this->getUser());
if( in_array('USERS_CREATE', $rights)) {
$user = new User();
$form = $this->createForm(UserType::class, $user);
$form->handleRequest($request);
if( $form->isSubmitted() && $form->isValid()) {
$em->persist($user);
$em->flush();
return $this->redirectToRoute('user_index');
}
return $this->render('@admin/user/new.html.twig', [
'user' => $user,
'form' => $form->createView(),
'rights' => $rights
]);
} else {
$request->getSession()->getFlashBag()->add('danger', "Accès refusé");
return $this->redirect($this->generateUrl('index'));
}
}
/**
* @Route("/add", name="add_user", methods={"GET","POST"}, options={"expose"=true})
*/
public function add(Request $request, UserRepository $users, EntityManagerInterface $em): JsonResponse
{
$rights = $this->rightService->getAllRights($this->getUser());
if (!in_array('USERS_CREATE', $rights)) {
$request->getSession()->getFlashBag()->add('danger', "Accès refusé");
// Pour un appel AJAX, on peut aussi renvoyer un JSON d'erreur si besoin
return new JsonResponse([
'success' => false,
'message' => "Accès refusé.",
]);
}
$type = $request->get('type');
$civility = $request->get('civility');
$username = trim((string) $request->get('username'));
$phone = trim((string) $request->get('phone'));
// 1) Validation des champs obligatoires
if (!$civility || !$username || !$phone) {
$message = 'Veuillez renseigner tous les champs obligatoires : Civilité, Référence et Téléphone.';
if (!$civility) {
$message = 'La civilité est obligatoire.';
} elseif (!$username) {
$message = 'La référence est obligatoire.';
} elseif (!$phone) {
$message = 'Le téléphone est obligatoire.';
}
return new JsonResponse([
'success' => false,
'message' => $message,
]);
}
// 2) Unicité téléphone / référence
$existUsername = $users->findOneBy(['username' => $username]);
$existPhone = $users->findOneBy(['phone' => $phone]);
if ($existPhone) {
return new JsonResponse([
'success' => false,
'message' => "Téléphone que vous avez entré existe déjà.",
]);
}
if ($existUsername) {
return new JsonResponse([
'success' => false,
'message' => "Référence que vous avez entré existe déjà.",
]);
}
// 3) Création du User
$user = $this->createUser($request);
$em->persist($user);
// 4) Liens réseaux sociaux (structure existante)
$linksData = $request->get('links', []);
foreach ($linksData as $linkData) {
if (!empty($linkData['link'])) {
$link = new Link();
$link->setIcon($linkData['icon'] ?? '')
->setLink($linkData['link'])
->setUser($user);
$em->persist($link);
}
}
$em->flush();
// 5) Flashs de confirmation (en plus du JSON)
if ($type === 'client') {
$request->getSession()->getFlashBag()->add('success', "Client ajouté avec succès");
} elseif ($type === 'prospect') {
$request->getSession()->getFlashBag()->add('success', "Prospect ajouté avec succès");
} elseif ($type === 'contact') {
$request->getSession()->getFlashBag()->add('success', "Contact ajouté avec succès");
}
// 6) Redirection côté JS
if ($request->get('save') === 'save') {
return new JsonResponse([
'success' => true,
'path' => $this->generateUrl('user_show', ['id' => $user->getId()]),
]);
}
return new JsonResponse([
'success' => true,
'path' => $this->generateUrl('new_document_commande', [
'category' => 'client',
'id' => $user->getId(),
]),
]);
}
/**
* @Route("/delete/{id}", name="user_delete", methods={"GET","DELETE"}, options={"expose"=true})
*/
public function delete_user(Request $request, User $user, EntityManagerInterface $em): JsonResponse
{
$this->hasRight($request, 'USERS_DELETE');
// Vérifie si le client a des documents de type "commande"
$hasCommandes = $user->getDocuments()->exists(function($key, $doc) {
return $doc->getType() === 'commande';
});
if ($hasCommandes) {
return new JsonResponse([
'success' => false,
'message' => "Impossible de supprimer ce client : il possède encore des commandes. Veuillez d'abord annuler et supprimer toutes ses commandes."
], Response::HTTP_BAD_REQUEST);
}
// Suppression du client (aucune commande liée)
$em->remove($user);
$em->flush();
return new JsonResponse([
'success' => true,
'message' => "Client supprimé avec succès."
]);
}
/**
* @Route("/utilisateur/{id}", name="user_profile", methods={"GET"}, options = { "expose" = true})
*/
public function userProfile(User $user): Response
{
$this->denyAccessUnlessGranted('ROLE_ADMIN'); // ou ta méthode hasRight
return $this->render('@admin/user/fiche_user.html.twig', [
'user' => $user,
]);
}
/**
* @Route("/edit/{id}", name="user_edit", methods={"GET","POST"}, options={"expose"=true})
*/
/*
public function edit(Request $request, User $user,EntityManagerInterface $em): Response {
$this->hasRight($request,'USERS_UPDATE');
$form = $this->createForm(UserType::class, $user);
$form->handleRequest($request);
if( $form->isSubmitted() && $form->isValid()) {
$em->flush();
return $this->redirectToRoute('user_index');
}
return $this->render('@admin/user/edit.html.twig', [
'user' => $user,
'form' => $form->createView(),
]);
}
*/
public function createUser($request) {
$user = new User();
$user->setCivility($request->get('civility'))
->setAdress($request->get('address'))
->setCity($request->get('city'))
->setCountry($request->get('country'))
->setFirstName($request->get('firstName'))
->setPhone(trim($request->get('phone')))
//->setRegion($request->get('region'))
->setRegion($request->request->get('region') ?? '')
->setSecondPhone($request->get('second_phone'))
->setType($request->get('type'))
->setUsername($request->get('username'))
->setClientType(ClientType::TYPE_NOUVEAU_CLIENT)
->setZip($request->get('zip'))
->setCreatedAt(new \DateTime('now'));
return $user;
}
/**
* @Route("/search", name="search_client", methods="GET|POST", options = { "expose" = true})
*/
public function searchItem(Request $request, UserRepository $users) {
$query = $request->get('query');
$deleteEspace = str_replace(' ', '', $query);
if( is_numeric($deleteEspace)) $query = $deleteEspace;
$entities = $users->search(0, 10, $query, null, 'client');
foreach ($entities as $entity) {
// dd($entity->getDocuments()->first());
$output[] = [
'id' => $entity->getId(),
'username' => $entity->getUsername(),
'name' => $entity->getCivility() . ' ' . $entity->getFirstName() ,
'phone' => $entity->getPhone(),
'email' => $entity->getEmail(),
'address' => $entity->getCity().','.$entity->getRegion(),
'cmdEnAttente' => $entity->getDocuments()->filter(function($element) {
return in_array($element->getStatus(), ['en-attente']) and $element->getType() =='commande';
})->count(),
'cmdAccepte' => $entity->getDocuments()->filter(function($element) {
return in_array($element->getStatus(), ['accepte','']) and $element->getType() =='commande';
})->count(),
'cmdAnnule' => $entity->getDocuments()->filter(function($element) {
return in_array($element->getStatus(), ['annule']) and $element->getType() =='commande';
})->count(),
'cmdEnRetour' => $entity->getDocuments()->filter(function($element) {
return in_array($element->getStatus(), ['retourne','retour-en-cours']) and $element->getType() =='commande';
})->count(),
'echangeEnAttente' => $entity->getDocuments()->filter(function($element) {
return $element->getStatus() =='en-attente' and $element->getType() =='echange';
})->count(),
'nbCmdTotal' => $entity->getDocuments()->filter(function($element) {
return $element->getType() =='commande';
})->count(),
'nbEchangeTotal' => $entity->getDocuments()->filter(function($element) {
return $element->getType() =='echange';
})->count(),
'created_at' => $entity->getCreatedAt()->format('d/m/Y H:i'),
'actions' => 'actions',
];
}
if( !$entities) {
$output = [];
}
return new JsonResponse($output);
}
/**
* @Route("/search2", name="search_client2", methods="GET|POST", options = { "expose" = true})
*/
public function searchItem2(Request $request, UserRepository $users) {
$query = $request->get('query');
$deleteEspace = str_replace(' ', '', $query);
if( is_numeric($deleteEspace))
$query = $deleteEspace;
$entities = $users->search(0, 10, $query, null, 'client');
$output='<div class="m-list-search__results">';
foreach ($entities as $entity) {
$output .= '<a href="/admin/user/show/'.$entity->getId().'" class="m-list-search__result-item">
<span class="m-list-search__result-item-pic">
<img class="m--img-rounded" src="https://ui-avatars.com/api/?name=' . $entity->getFirstName() .'" title="">
</span>
<span class="m-list-search__result-item-text">
'.$entity->getCivility() . ' ' . $entity->getFirstName() .'
<span class="float-right">'.$entity->getPhone().'</span>
</span>
</a>';
}
$output .='</div>';
if( !$entities)
$output ="";
return new Response($output);
}
/**
* @Route("/address/{type}/{id}", name="modal_addres_user", methods={"GET","POST"}, options={"expose"=true})
*/
public function modalAdressClient(Request $request, $type, $id = null,EntityManagerInterface $em): Response {
//todo ; update hasRight in trait to accepte multi value.
$rights = $this->rightService->getAllRights($this->getUser());
if( !in_array('USERS_UPDATE', $rights) || !in_array('USERS_CREATE', $rights)) {
$request->getSession()->getFlashBag()->add('danger', "Accès refusé");
return $this->redirect($this->generateUrl('index'));
}
if( $type == 'new') {
$address = new Address();
$client = $this->userRepository->find($id);
$form = $this->createForm(AddressType::class, $address);
} else if( $type == 'principal') {
$client = $this->userRepository->find($id);
$form = $this->createForm(AddressUserType::class, $client);
} else {
$address = $this->addressRepository->find($id);
$client = $address->getUser();
$form = $this->createForm(AddressType::class, $address);
}
$form->handleRequest($request);
//dump($client,$type,$id);die;
if( $form->isSubmitted() && $request->isMethod('POST')) {
if( $type == 'new') {
$address->setCountry($request->get('address')['country'])
//->setRegion($request->get('address')['region'])
->setRegion($request->request->get('address')['region'] ?? '')
->setUser($client);
$em->persist($address);
} else if( $type == 'principal')
$client->setCountry($request->get('address_user')['country'])
->setRegion($request->get('address_user')['region']);
else
$address->setCountry($request->get('address')['country'])
->setRegion($request->get('address')['region']);
$em->flush();
$request->getSession()->getFlashBag()->add('success', "Adresse modifié avec succès");
return $this->redirect($this->generateUrl('user_show', ['id' => $client->getId()]));
}
return $this->render('@admin/includes/modals/_formAddressModal.html.twig', [
'form' => $form->createView(),
'idAddress' => $id,
'type' => $type,
'categorie' => 'client'
]);
}
/**
* @Route("/supplier/address/modal/{id}", name="modal_address_supplier", methods={"GET", "POST"}, options={"expose"=true})
*/
public function modalAddressSupplier($id, Request $request, EntityManagerInterface $em): Response
{
$rights = $this->rightService->getAllRights($this->getUser());
if (!in_array('USERS_UPDATE', $rights) || !in_array('USERS_CREATE', $rights)) {
$request->getSession()->getFlashBag()->add('danger', "Accès refusé");
return $this->redirect($this->generateUrl('index'));
}
$type = $request->get('type'); // <-- à ne pas oublier
$supplier = null;
if ($type == 'new') {
$address = new Address();
$supplier = $this->supplierRepository->find($id);
$form = $this->createForm(AddressType::class, $address);
} elseif ($type == 'principal') {
$supplier = $this->supplierRepository->find($id);
$form = $this->createForm(AddressUserType::class, $supplier);
} else {
$address = $this->addressRepository->find($id);
$supplier = $address ? $address->getSupplier() : null;
$form = $this->createForm(AddressType::class, $address);
}
// dump($supplier,$type,$id);die;
$form->handleRequest($request);
if ($form->isSubmitted() && $request->isMethod('POST')) {
if ($type == 'new') {
$address->setCountry($request->get('address')['country'] ?? '')
->setRegion($request->request->get('address')['region'] ?? '')
->setSupplier($supplier);
$em->persist($address);
} elseif ($type == 'principal') {
$supplier->setCountry($request->get('address_user')['country'] ?? '')
->setRegion($request->get('address_user')['region'] ?? '');
} else {
$address->setCountry($request->get('address')['country'] ?? '')
->setRegion($request->get('address')['region'] ?? '');
}
$em->flush();
$request->getSession()->getFlashBag()->add('success', "Adresse modifiée avec succès");
return $this->redirect($this->generateUrl('supplier_show', ['id' => $supplier->getId()]));
}
return $this->render('@admin/includes/modals/_formAddressModal.html.twig', [
'form' => $form->createView(),
'idAddress' => $id,
'type' => $type,
'categorie' => 'fournisseur'
]);
}
/**
* @Route("/modal/edit/{id}/{document}", name="modal_edit_user", methods={"GET","POST"}, options={"expose"=true}, defaults={"document"=NULL})
*/
public function modalEditUser(Request $request, User $user,?Document $document,EntityManagerInterface $em): Response {
$this->hasRight($request,'USERS_UPDATE');
$form = $this->createForm(UserType::class, $user);
$form->handleRequest($request);
//$entityDocument = ($document)?$this->documentRepository->find($document): null;
if( $form->isSubmitted()) {
$user->setCountry($request->request->get('user')['country']);
$user->setRegion($request->request->get('user')['region']);
// Nettoyer les liens vides
foreach ($user->getLinks() as $link) {
if (empty($link->getLink()) && empty($link->getIcon())) {
$user->removeLink($link);
$em->remove($link);
}
}
if( $document) {
$document->setAdress($user->getAdress() . ' ' . $user->getCity() . ' ' . $user->getRegion() . ' ' . $user->getCountry());
if( $document->getDocument())
$document->getDocument()->setAdress($user->getAdress() . ' ' . $user->getCity() . ' ' . $user->getRegion() . ' ' . $user->getCountry());
}
$em->flush();
if( $document)
return $this->redirect($this->generateUrl('document_show', ['id' => $document->getId()]));
else
return $this->redirect($this->generateUrl('user_show', ['id' => $user->getId()]));
}
if( $document) {
return $this->render('@admin/user/_formEditUserModal.html.twig', [
'form' => $form->createView(),
'user' => $user,
'document' => $document
]);
} else {
return $this->render('@admin/user/_formEditUserModal.html.twig', [
'form' => $form->createView(),
'user' => $user
]);
}
}
/**
* @Route("/address/delete/{id}", name="delete_user_address", methods={"POST"})
*/
public function deleteUserAddress(AddressRepository $addressRepository, EntityManagerInterface $em, int $id): JsonResponse
{
$address = $addressRepository->find($id);
if (!$address) {
return new JsonResponse(['success' => false, 'message' => 'Adresse introuvable.'], 404);
}
$em->remove($address);
$em->flush();
return new JsonResponse(['success' => true]);
}
/**
* @Route("/administration/index", name="administration_index", methods={"GET"})
*/
public function indexAdministration(Request $request): Response {
// Vérification du rôle
if (!$this->isGranted('ROLE_SUPER_ADMIN')) {
$this->addFlash('danger', "Vous n'avez pas le droit d'accéder à la gestion des utilisateurs.");
return $this->redirectToRoute('index'); // Redirection vers la page d'accueil admin
}
$rights=$this->hasRight($request,'ADMINISTRATION');
$form = $this->createForm(FilterUserType::class, null, ['is_client_filter' => false, ]);
return $this->render('@admin/user/administrationIndex.html.twig', [
'form' => $form->createView(),
'rights' => $rights
]);
}
/**
* @Route("/listadministration", name="list_administration", methods="GET|POST", options={"expose"=true})
*/
public function listAdministrationData(Request $request, UserRepository $users) {
$pagination = $request->get('pagination');
$page = $pagination['page'] - 1;
$limit = $pagination['perpage'];
$dateB = $request->get('dateBefore') ? (new \DateTime($request->get('dateBefore')))->format('d/m/Y H:i:s') : null;
$dateA = $request->get('dateAfter') ? (new \DateTime($request->get('dateAfter')))->format('d/m/Y H:i:s') : null;
$entities = $users->search($page, $limit,
$request->get('username'),
$request->get('phone'),
'user',
$request->get('email'),
$dateB,
$dateA,
$request->get('region')
);
$count = $users->countUsers(
$request->get('username'),
$request->get('phone'),
'user',
$request->get('email'),
$dateB,
$dateA,
$request->get('region')
);
$output = [
'data' => [],
'meta' => [
'page' => $pagination['page'],
'perpage' => $limit,
'pages' => ceil($count / $limit),
'total' => $count,
]
];
foreach ($entities as $entity) {
$output['data'][] = [
'id' => $entity->getId(),
'username' => $entity->getUserName(),
'name' => trim($entity->getFirstName()),
'email' => $entity->getEmail(),
'phone' => $entity->getPhone() ?? '',
'group' => $entity->getGroupUser() ? $entity->getGroupUser()->getName() : null,
'poste' => $entity->getUserPost() ? $entity->getUserPost()->getName() : null,
'roles' => $entity->getRoles(), // tableau JSON natif
'isBlocked' => $entity->isBlocked() ? 1 : 0,
'createdAt' => $entity->getCreatedAt()
? $entity->getCreatedAt()->format('Y-m-d H:i:s')
: null,
];
}
return new JsonResponse($output);
}
/**
* @Route("/administration/manage-block/{id}", name="modal_manage_block", methods={"GET","POST"}, options={"expose"=true})
*/
public function modalManageBlock(Request $request, User $user, EntityManagerInterface $em, DailyBlockService $svc): Response
{
$form = $this->createForm(\App\Form\ManageBlockType::class, $user, ['method' => 'POST']);
if ($request->isMethod('POST')) {
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
// Récupère le format UI: daily[mon][enabled|start1|end1|start2|end2]
$dailyRaw = $request->request->get('daily', []);
// ✅ Convertit UI -> format normalisé (mon..sun => [ {start,end}, ... ])
$normalized = $svc->fromUiDaily(is_array($dailyRaw) ? $dailyRaw : []);
// (option) si blocage permanent, on peut forcer l'effacement du quotidien
// if ($user->isBlocked()) { $normalized = ['mon'=>[], 'tue'=>[], 'wed'=>[], 'thu'=>[], 'fri'=>[], 'sat'=>[], 'sun'=>[]]; }
// Sauvegarde: choisis UNE propriété (JSON string ou array JSON)
if (method_exists($user, 'setDailyBlocks')) {
$user->setDailyBlocks($svc->toJson($normalized)); // string JSON
} elseif (method_exists($user, 'setBlockedHours')) {
$user->setBlockedHours($normalized); // array (colonne JSON)
}
$em->flush();
if ($request->isXmlHttpRequest()) {
return $this->json(['status' => 'ok']);
}
$this->addFlash('success', 'Blocage mis à jour avec succès.');
return $this->redirectToRoute('administration_index');
}
// form invalide → renvoyer le fragment
return $this->render('@admin/user/_formManageBlockModal.html.twig', [
'form' => $form->createView(),
'user' => $user,
'dailyJson' => $svc->toJson($svc->buildUserDailyBlocks($user)),
]);
}
// GET (ou re-affichage initial)
$userDailyBlocks = $svc->buildUserDailyBlocks($user);
return $this->render('@admin/user/_formManageBlockModal.html.twig', [
'form' => $form->createView(),
'user' => $user,
'dailyJson' => $svc->toJson($userDailyBlocks),
]);
}
/**
* @Route("/administration/new", name="modal_new_administration", methods={"GET","POST"}, options={"expose"=true})
*/
public function modalNewAdministration(Request $request, UserPasswordEncoderInterface $passwordEncoder,EntityManagerInterface $em): Response {
$this->hasRight($request,'ADMINISTRATION');
$user = new User();
$form = $this->createForm(AdministationType::class, $user, ['is_edit' => false]);
$form->handleRequest($request);
if( $form->isSubmitted() && $request->isMethod('POST')) {
$user->setCreatedAt(new \DateTime('now'));
// encode the plain password
$user->setPassword(
$passwordEncoder->encodePassword(
$user,
$form->get('password')->getData()
)
);
$user->setRoles(["ROLE_ADMIN"]);
$user->setType('user');
$em->persist($user);
$em->flush();
return $this->redirect($this->generateUrl('administration_index'));
}
return $this->render('@admin/user/_formNewAdministrationModal.html.twig', [
'form' => $form->createView(),
'user' => $user
]);
}
/**
* @Route("/administration/edit/{id}", name="modal_edit_administration", methods={"GET","POST"}, options={"expose"=true})
*/
public function modalEditAdministration(Request $request, User $user, UserPasswordEncoderInterface $passwordEncoder, EntityManagerInterface $em ): Response {
$this->hasRight($request,'ADMINISTRATION');
$form = $this->createForm(AdministationType::class, $user, ['is_edit' => true]);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$em->flush();
return $this->redirectToRoute('administration_index');
}
return $this->render('@admin/user/_formEditAdministrationModal.html.twig', [
'form' => $form->createView(),
'user' => $user
]);
}
/**
* @Route("/administration/update_password/{id}", name="modal_update_pw_administration", methods={"GET","POST"}, options={"expose"=true})
*/
public function modalEditPwAdministration(Request $request, User $user, UserPasswordEncoderInterface $passwordEncoder,EntityManagerInterface $em): Response {
$this->hasRight($request,'ADMINISTRATION');
$form = $this->createForm(AdministationChangePwType::class, $user);
if( $request->isMethod('POST'))
{
$form->handleRequest($request);
if( !$form->isValid()) {//&& $request->isMethod('POST')
return new JsonResponse(array(
'result' => 0,
'message' => 'Invalid form',
'data' => $this->getErrorMessages($form)
));
}else{
$user->setPassword(
$passwordEncoder->encodePassword(
$user,
$form->get('password')->getData()
)
);
$em->persist($user);
$em->flush();
$this->activityService->addActivity('warning',$this->getUser()->getUserIdentifier(). " a changé le mot de passe de l'utilisateur ".$user->getUserIdentifier(), null, $this->getUser(), 'user');
return new JsonResponse(array(
'result' => 1,
'message' => 'Le mot de passe de '.$user->getUsername().' a été mis à jour avec succès',
'data' => ''));
}
}
return $this->render('@admin/user/_formEditPwModal.html.twig', [
'form' => $form->createView(),
'user' => $user
]);
}
public function recentDangerActivities(Request $request,EntityManagerInterface $em): Response
{
$repository = $em->getRepository(Activity::class);
//dump($repository->findBy(['type' => 'danger']));die;
return $this->render('@admin/includes/_topnavDangerActivities.html.twig',[
'activities' => $repository->findBy(['type' => 'danger'],['createdAt'=>'DESC'])
]);
}
// Generate an array contains a key -> value with the errors where the key is the name of the form field
protected function getErrorMessages(Form $form)
{
$errors = array();
foreach ($form->getErrors() as $key => $error) {
$errors[] = $error->getMessage();
}
foreach ($form->all() as $child) {
if( !$child->isValid())
$errors[$child->getName()] = $this->getErrorMessages($child);
}
return $errors;
}
/**
* @Route("/new_comment/{id}", name="client_comment_new", methods={"GET","POST"})
*/
public function newComment(Request $request, $id,EntityManagerInterface $em): Response {
$comment = new Comment();
if( $request->isMethod('POST') && $request->get('form_comment')) {
// dd($request);
$comment->setDescription($request->get('form_comment')['comment']);
$comment->setUser($this->getUser());
$comment->setCreateAt(new \DateTime('now'));
$client = $this->userRepository->find($id);
$comment->setClient($client);
$em->persist($comment);
$em->flush();
return $this->redirectToRoute("user_show", ['id' => $id,"tab"=>"comments"]);
}
return false;
}
/**
* @Route("/edit_comment/{id}/{comment}", name="client_comment_edit", methods={"GET","POST"})
*/
public function editComment(Request $request, $id, Comment $comment,EntityManagerInterface $em): Response {
if( $request->isMethod('POST') && $request->get('form_comment')) {
$comment->setDescription($request->get('form_comment')['comment']);
$comment->setUser($this->getUser());
$comment->setCreateAt(new \DateTime('now'));
$em->flush();
return $this->redirectToRoute("user_show", ['id' => $id,"tab"=>"comments"]);
}
return false;
}
private function convertDate($originalDate)
{
$date = \DateTime::createFromFormat('Y-m-d H:i:s', $originalDate);
if ($date === false) {
return ("-");
}
$formattedDate = $date->format('d/m/Y');
return $formattedDate;
}
/**
* @Route("/admin/user/{id}/promotion", name="user_apply_promotion", methods={"POST"}, options={"expose"=true})
*/
public function applyClientPromo(Request $request, User $user, EntityManagerInterface $em, UserPromotionHistoryRepository $historyRepo): Response {
//$em->refresh($user);
$tab = $request->request->get('tab', 'promotion');
$now = new \DateTimeImmutable();
$operator = $this->getUser()?->getUserIdentifier() ?? $this->getUser()->getFirstName();
// {# #} Recréer le même formulaire que dans show()
$form = $this->createForm(AddCodePromotionType::class);
$form->handleRequest($request);
//dump($user->getPromotion()); die;
if (!$form->isSubmitted()) {
$this->addFlash('danger', 'Formulaire non soumis.');
return $this->redirectToRoute('user_show', [
'id' => $user->getId(),
'tab' => $tab,
]);
}
/** @var Promotion|null $promotion */
$promotion = $form->get('promotion')->getData();
// {# #} Aucun code choisi
if (!$promotion) {
$this->addFlash('warning', 'Veuillez choisir un code promotionnel.');
return $this->redirectToRoute('user_show', [
'id' => $user->getId(),
'tab' => $tab,
]);
}
// {# #} Vérifier type client
if ($promotion->getType() !== 'client') {
$this->addFlash('danger', 'Ce code promotionnel n’est pas destiné aux clients.');
return $this->redirectToRoute('user_show', [
'id' => $user->getId(),
'tab' => $tab,
]);
}
// {# #} Vérifier période
if ($promotion->getStartAt() > $now || $promotion->getEndAt() < $now) {
$this->addFlash('danger', 'Ce code n’est pas valable actuellement.');
return $this->redirectToRoute('user_show', [
'id' => $user->getId(),
'tab' => $tab,
]);
}
// {# #} Vérifier quantité
if ($promotion->getTotalQuantity() !== null && $promotion->getQuantityUser() >= $promotion->getTotalQuantity()) {
$this->addFlash('danger', 'Ce code a atteint sa limite d’utilisation.');
return $this->redirectToRoute('user_show', [
'id' => $user->getId(),
'tab' => $tab,
]);
}
//dump($user->getPromotion()); die;
// {# #} Vérifier qu’il n’y a pas déjà une promo en cours
if ($user->getPromotion()) {
$this->addFlash('danger', 'Ce client possède déjà une promotion active. Veuillez la stopper avant d’en appliquer une nouvelle.');
return $this->redirectToRoute('user_show', [
'id' => $user->getId(),
'tab' => $tab,
]);
}
// {# #} Appliquer la promotion
$user->setPromotion($promotion);
// Historique
$history = new UserPromotionHistory();
$history
->setUser($user)
->setPromotion($promotion)
->setStartedAt($now)
->setStartedBy($operator);
$em->persist($history);
// Incrémenter le compteur d’utilisation du code
if ($promotion->getQuantityUser() !== null) {
$promotion->setQuantityUser($promotion->getQuantityUser() + 1);
}
$em->flush();
$this->addFlash('success', 'Code promotionnel appliqué avec succès.');
return $this->redirectToRoute('user_show', [
'id' => $user->getId(),
'tab' => 'promotion',
]);
}
/**
* @Route("/admin/user/{id}/end-promotion", name="user_end_promotion", methods={"POST"})
*/
public function endPromotion(User $user, Request $request, EntityManagerInterface $em, UserPromotionHistoryRepository $historyRepo) {
$promo = $user->getPromotion();
if ($promo) {
// récupérer la dernière ligne d'historique non terminée
$last = $historyRepo->findOneBy(
['user' => $user, 'promotion' => $promo, 'endedAt' => null],
['startedAt' => 'DESC']
);
if ($last) {
$now = new \DateTimeImmutable();
$operator = $this->getUser();
$last->setEndedAt($now);
$last->setEndedBy($operator ? $operator->getFirstName() : 'Système');
// motif optionnel venant de la modale
$reason = $request->request->get('form_end_reason');
$note = $request->request->get('form_end_note');
$last->setEndReason($reason ?: null);
$last->setEndNote($note ?: null);
}
// désactiver le code promo du client
$user->setPromotion(null);
$em->flush();
}
$this->addFlash('success', 'Le code promotionnel a été retiré pour ce client.');
return $this->redirectToRoute('user_show', [
'id' => $user->getId(),
'tab' => 'promotion'
]);
}
/**
* @Route("/admin/user/{id}/stats/first-last-year", name="client_stats_first_last_year", methods={"GET"})
*/
public function clientFirstLastYear(User $user, Connection $conn): JsonResponse
{
$row = $conn->fetchAssociative("
SELECT MIN(YEAR(d.created_at)) AS start_year, MAX(YEAR(d.created_at)) AS end_year
FROM document d
WHERE d.category='client' AND d.type='commande' AND d.client_id=:uid
", ['uid' => $user->getId()]);
$current = (int)date('Y');
$start = ($row && $row['start_year']) ? (int)$row['start_year'] : $current;
$end = ($row && $row['end_year']) ? (int)$row['end_year'] : $current;
if ($start > $end) { $start = $end; }
return $this->json(['start_year' => $start, 'end_year' => $end]);
}
/**
* @Route("/admin/user/{id}/stats/sales-trend", name="client_stats_sales_trend", methods={"GET"})
*/
public function clientSalesTrend(User $user, Request $request, EntityManagerInterface $em): JsonResponse
{
$start = $request->query->get('startDate');
$end = $request->query->get('endDate');
$basis = strtolower($request->query->get('priceBasis', 'ht')); // ht | ttc
$granularity = strtolower($request->query->get('granularity', 'month')); // day | month | year
$conn = $em->getConnection();
$groupFormat = match ($granularity) {
'day' => '%Y-%m-%d',
'week' => '%Y-%u',
'year' => '%Y',
default => '%Y-%m'
};
$caExpr = ($basis === 'ttc')
? "COALESCE(d.total_amount_ttc,0) - COALESCE(d.delivery_company_total, 0)"
: "COALESCE(d.total_amount_ttc,0) - COALESCE(d.total_tva,0) - COALESCE(d.delivery_company_total, 0)";
$netSales = "($caExpr - COALESCE(d.delivery_company_total,0))";
$cogsSub = ($basis === 'ttc')
? "
SELECT SUM(pdv.quantity * COALESCE(COALESCE(dv.buying_price_ht, p.buying_price_ht, 0) * (1 + COALESCE(t.number,0)/100), 0))
FROM document_declination_produit pdv
JOIN produit_declination_value dv ON dv.id = pdv.produit_declination_value_id
JOIN produit p ON p.id = dv.produit_id
LEFT JOIN tva t ON t.id = pdv.tva_id
WHERE pdv.document_id = d.id
"
: "
SELECT SUM(pdv.quantity * COALESCE(COALESCE(dv.buying_price_ht, p.buying_price_ht, 0), 0))
FROM document_declination_produit pdv
JOIN produit_declination_value dv ON dv.id = pdv.produit_declination_value_id
JOIN produit p ON p.id = dv.produit_id
WHERE pdv.document_id = d.id
";
$sql = "
SELECT
DATE_FORMAT(d.created_at, :groupFormat) AS periode,
ROUND(SUM($netSales), 2) AS ca_ht,
SUM(COALESCE(d.total_tva, 0)) AS tva,
ROUND(SUM($netSales - COALESCE(($cogsSub), 0)), 2) AS marge,
COUNT(DISTINCT d.id) AS orders
FROM document d
WHERE d.client_id = :uid
AND d.type = 'commande'
AND d.category = 'client'
AND d.status IN ('expedie', 'livre', 'paye', 'facture')
";
$params = [
'uid' => $user->getId(),
'groupFormat' => $groupFormat,
];
// ✅ Ajout conditionnel du filtre de période
if (!empty($start)) {
$sql .= " AND d.created_at >= :start";
$params['start'] = $start . ' 00:00:00';
}
if (!empty($end)) {
$sql .= " AND d.created_at <= :end";
$params['end'] = $end . ' 23:59:59';
}
$sql .= " GROUP BY periode ORDER BY periode ASC";
$rows = $conn->prepare($sql)->executeQuery($params)->fetchAllAssociative();
// Traitement post-requête
foreach ($rows as &$r) {
$r['ca_ht'] = (float) ($r['ca_ht'] ?? 0);
$r['tva'] = (float) ($r['tva'] ?? 0);
$r['marge'] = (float) ($r['marge'] ?? 0);
$r['orders'] = (int) ($r['orders'] ?? 0);
$r['ca_ttc'] = $r['ca_ht'] + $r['tva'];
$r['marge_percent'] = $r['ca_ht'] > 0 ? round($r['marge'] / $r['ca_ht'] * 100, 2) : 0.0;
}
unset($r);
return new JsonResponse($rows);
}
/**
* @Route("/admin/user/{id}/stats/by-source", name="client_stats_by_source", methods={"GET"})
*/
public function clientSalesBySource(User $user, Request $request, Connection $conn, EntityManagerInterface $em): JsonResponse
{
$start = $request->query->get('startDate');
$end = $request->query->get('endDate');
$basis = strtolower($request->query->get('priceBasis', 'ht'));
$metric = strtolower($request->query->get('metric', 'orders')); // orders | ca | margin | ca_margin
/* --- CA net --- */
$netSales = ($basis === 'ttc')
? "COALESCE(d.total_amount_ttc,0) - COALESCE(d.delivery_company_total,0)"
: "COALESCE(d.total_amount_ttc,0) - COALESCE(d.total_tva,0) - COALESCE(d.delivery_company_total,0)";
/* --- COGS document --- */
$cogsSub = ($basis === 'ttc')
? "
SELECT SUM(pdv.quantity * COALESCE(COALESCE(dv.buying_price_ht, p.buying_price_ht, 0) * (1 + COALESCE(t.number,0)/100),0))
FROM document_declination_produit pdv
JOIN produit_declination_value dv ON dv.id = pdv.produit_declination_value_id
JOIN produit p ON p.id = dv.produit_id
LEFT JOIN tva t ON t.id = pdv.tva_id
WHERE pdv.document_id = d.id
"
: "
SELECT SUM(pdv.quantity * COALESCE(COALESCE(dv.buying_price_ht, p.buying_price_ht, 0),0))
FROM document_declination_produit pdv
JOIN produit_declination_value dv ON dv.id = pdv.produit_declination_value_id
JOIN produit p ON p.id = dv.produit_id
WHERE pdv.document_id = d.id
";
/* --- SQL --- */
$sql = "
SELECT
s.name AS name,
COALESCE(COUNT(DISTINCT d.id), 0) AS orders,
COALESCE(SUM($netSales), 0) AS ca,
COALESCE(SUM($netSales - COALESCE(($cogsSub), 0)), 0) AS margin
FROM source s
LEFT JOIN document d
ON d.source_id = s.id
AND d.client_id = :uid
AND d.type = 'commande'
AND d.status IN ('expedie', 'livre', 'paye', 'facture')
";
$params = ['uid' => $user->getId()];
// ✅ Ajout conditionnel du filtre de période
if (!empty($start)) {
$sql .= " AND d.created_at >= :start";
$params['start'] = $start . ' 00:00:00';
}
if (!empty($end)) {
$sql .= " AND d.created_at <= :end";
$params['end'] = $end . ' 23:59:59';
}
$sql .= "
GROUP BY s.id, s.name
ORDER BY orders DESC, s.name ASC
";
$rows = $conn->prepare($sql)->executeQuery($params)->fetchAllAssociative();
$labels = array_column($rows, 'name');
// ✅ Récupération des couleurs personnalisées
$colorMap = $em->getRepository(\App\Entity\Source::class)->findColorMap();
$colors = [];
foreach ($labels as $lab) {
$key = trim(mb_strtolower((string)$lab, 'UTF-8'));
$colors[] = $colorMap[$key] ?? null;
}
// ✅ CA + Marge
if ($metric === 'ca_margin') {
return new JsonResponse([
'labels' => $labels,
'ca_values' => array_map('floatval', array_column($rows, 'ca')),
'margin_values' => array_map('floatval', array_column($rows, 'margin')),
'colors' => $colors,
]);
}
// ✅ Cas standard
$key = match ($metric) {
'orders' => 'orders',
'margin' => 'margin',
default => 'ca',
};
return new JsonResponse([
'labels' => $labels,
'values' => array_map(fn($r) => (float)($r[$key] ?? 0), $rows),
'colors' => $colors,
]);
}
/**
* @Route("/admin/user/{id}/stats/by-category", name="client_stats_by_category", methods={"GET"})
*/
public function getClientSalesByCategory(Request $request, int $id, Connection $conn): JsonResponse
{
$start = $request->query->get('startDate');
$end = $request->query->get('endDate');
$basis = strtolower($request->query->get('priceBasis', 'ht')); // ht | ttc
$metric = strtolower($request->query->get('metric', 'qty')); // qty | orders | ca | margin | ca_margin
$netSales = ($basis === 'ttc')
? "COALESCE(d.total_amount_ttc,0) - COALESCE(d.delivery_company_total,0)"
: "COALESCE(d.total_amount_ttc,0) - COALESCE(d.total_tva,0) - COALESCE(d.delivery_company_total,0)";
$sql = "
SELECT
c.name AS name,
COUNT(DISTINCT d.id) AS orders,
SUM(pdv.quantity) AS qty,
SUM($netSales) AS ca,
SUM(
pdv.quantity *
COALESCE(
COALESCE(p.buying_price_ht, 0) * (1 + COALESCE(t.number, 0)/100),
0
)
) AS cost
FROM document d
JOIN document_declination_produit pdv ON pdv.document_id = d.id
JOIN produit_declination_value dv ON dv.id = pdv.produit_declination_value_id
JOIN produit p ON p.id = dv.produit_id
LEFT JOIN category c ON c.id = p.categories_id
LEFT JOIN tva t ON t.id = pdv.tva_id
WHERE d.client_id = :clientId
AND d.type = 'commande'
AND d.status IN ('expedie', 'livre', 'paye', 'facture')
";
$params = ['clientId' => $id];
// ✅ Application conditionnelle du filtre temporel
if (!empty($start)) {
$sql .= " AND d.created_at >= :start";
$params['start'] = $start . ' 00:00:00';
}
if (!empty($end)) {
$sql .= " AND d.created_at <= :end";
$params['end'] = $end . ' 23:59:59';
}
$sql .= "
GROUP BY c.id, c.name
ORDER BY qty DESC, c.name ASC
";
$rows = $conn->prepare($sql)->executeQuery($params)->fetchAllAssociative();
$labels = array_column($rows, 'name');
// ✅ Cas spécial : CA + Marge
if ($metric === 'ca_margin') {
return new JsonResponse([
'labels' => $labels,
'ca_values' => array_map(fn($r) => (float)($r['ca'] ?? 0), $rows),
'margin_values' => array_map(fn($r) =>
(float)($r['ca'] ?? 0) - (float)($r['cost'] ?? 0), $rows),
]);
}
// ✅ Cas standards : qty | orders | ca | margin
$values = match ($metric) {
'qty' => array_map(fn($r) => (float)($r['qty'] ?? 0), $rows),
'orders' => array_map(fn($r) => (float)($r['orders'] ?? 0), $rows),
'margin' => array_map(fn($r) =>
(float)($r['ca'] ?? 0) - (float)($r['cost'] ?? 0), $rows),
default => array_map(fn($r) => (float)($r['ca'] ?? 0), $rows),
};
return new JsonResponse([
'labels' => $labels,
'values' => $values,
]);
}
/**
* @Route("/admin/client/{id}/stats/by-status", name="client_stats_by_status", methods={"GET"})
*/
public function clientStatsByStatus(User $user, Request $request, Connection $conn): JsonResponse
{
$start = $request->query->get('startDate');
$end = $request->query->get('endDate');
$metric = strtolower($request->query->get('metric', 'orders')); // orders | ca | margin | ca+margin
$basis = strtolower($request->query->get('priceBasis', 'ht'));
$netSales = ($basis === 'ttc')
? "COALESCE(d.total_amount_ttc, 0) - COALESCE(d.delivery_company_total, 0)"
: "COALESCE(d.total_amount_ttc, 0) - COALESCE(d.total_tva, 0) - COALESCE(d.delivery_company_total, 0)";
$cogsSub = ($basis === 'ttc')
? "
SELECT SUM(pdv.quantity * COALESCE(COALESCE(dv.buying_price_ht, p.buying_price_ht, 0) * (1 + COALESCE(t.number,0)/100), 0))
FROM document_declination_produit pdv
JOIN produit_declination_value dv ON dv.id = pdv.produit_declination_value_id
JOIN produit p ON p.id = dv.produit_id
LEFT JOIN tva t ON t.id = pdv.tva_id
WHERE pdv.document_id = d.id
"
: "
SELECT SUM(pdv.quantity * COALESCE(COALESCE(dv.buying_price_ht, p.buying_price_ht, 0), 0))
FROM document_declination_produit pdv
JOIN produit_declination_value dv ON dv.id = pdv.produit_declination_value_id
JOIN produit p ON p.id = dv.produit_id
WHERE pdv.document_id = d.id
";
$sql = "
SELECT
d.status AS statut,
COUNT(*) AS orders,
SUM($netSales) AS ca,
SUM($netSales - COALESCE(($cogsSub), 0)) AS margin
FROM document d
WHERE d.client_id = :clientId
AND d.type = 'commande'
";
$params = ['clientId' => $user->getId()];
// ✅ Ajout des filtres dynamiques de période
if (!empty($start)) {
$sql .= " AND d.created_at >= :start";
$params['start'] = $start . ' 00:00:00';
}
if (!empty($end)) {
$sql .= " AND d.created_at <= :end";
$params['end'] = $end . ' 23:59:59';
}
$sql .= " GROUP BY d.status ORDER BY orders DESC";
$rows = $conn->prepare($sql)->executeQuery($params)->fetchAllAssociative();
$labels = array_column($rows, 'statut');
if ($metric === 'ca+margin') {
return new JsonResponse([
'labels' => $labels,
'ca_values' => array_map(fn($r) => (float)($r['ca'] ?? 0), $rows),
'margin_values' => array_map(fn($r) =>
(float)($r['ca'] ?? 0) - (float)($r['margin'] ?? 0), $rows),
]);
}
$key = match ($metric) {
'ca' => 'ca',
'margin' => 'margin',
default => 'orders'
};
return new JsonResponse([
'labels' => $labels,
'values' => array_map(fn($r) => (float)($r[$key] ?? 0), $rows),
]);
}
/**
* @Route("/admin/client/{id}/stats/repurchase-delay", name="client_stats_repurchase_delay", methods={"GET"})
*/
public function getRepurchaseDelay(int $id, Request $request, EntityManagerInterface $em): JsonResponse
{
$start = $request->query->get('startDate');
$end = $request->query->get('endDate');
$granularity = $request->query->get('granularity', 'month');
$format = $granularity === 'day' ? '%Y-%m-%d' : '%Y-%m';
$params = [
'clientId' => $id,
'format' => $format,
];
// Construction dynamique du filtre date
$dateFilter = '';
if (!empty($start)) {
$dateFilter .= " AND d.created_at >= :start";
$params['start'] = $start . ' 00:00:00';
}
if (!empty($end)) {
$dateFilter .= " AND d.created_at <= :end";
$params['end'] = $end . ' 23:59:59';
}
// Sous-requête avec lag() : calcule le délai entre deux commandes successives
$innerSql = "
SELECT
DATE_FORMAT(d.created_at, :format) AS periode,
DATEDIFF(d.created_at, LAG(d.created_at) OVER (ORDER BY d.created_at)) AS delay
FROM document d
WHERE d.client_id = :clientId
AND d.type = 'commande'
AND d.status IN ('expedie','livre','paye','facture')
$dateFilter
";
// Requête principale : moyenne du délai par période
$sql = "
SELECT
periode,
ROUND(AVG(delay), 1) AS avg_delay
FROM (
$innerSql
) sub
WHERE delay IS NOT NULL
GROUP BY periode
ORDER BY periode
";
$stmt = $em->getConnection()->prepare($sql);
$rows = $stmt->executeQuery($params)->fetchAllAssociative();
return $this->json([
'labels' => array_column($rows, 'periode'),
'values' => array_map(fn($r) => (float)($r['avg_delay'] ?? 0), $rows),
]);
}
/**
* @Route("/admin/client/{id}/stats/aov", name="client_stats_aov", methods={"GET"})
*/
public function getClientAverageOrderValueChart(Request $request, User $user, Connection $conn): JsonResponse {
$start = $request->query->get('startDate');
$end = $request->query->get('endDate');
$granularity = $request->query->get('granularity', 'month');
$basis = strtolower($request->query->get('priceBasis', 'ht'));
$groupFormat = match ($granularity) {
'day' => '%Y-%m-%d',
'week' => '%Y-%u',
'month' => '%Y-%m',
'year' => '%Y',
default => '%Y-%m'
};
$netSalesExpr = $basis === 'ttc'
? "COALESCE(d.total_amount_ttc, 0) - COALESCE(d.delivery_company_total, 0)"
: "COALESCE(d.total_amount_ttc, 0) - COALESCE(d.total_tva, 0) - COALESCE(d.delivery_company_total, 0)";
$params = [
'uid' => $user->getId(),
'groupFormat' => $groupFormat,
];
$dateFilter = '';
if (!empty($start)) {
$dateFilter .= " AND d.created_at >= :start";
$params['start'] = $start . ' 00:00:00';
}
if (!empty($end)) {
$dateFilter .= " AND d.created_at <= :end";
$params['end'] = $end . ' 23:59:59';
}
$sql = "
SELECT
DATE_FORMAT(d.created_at, :groupFormat) AS period,
ROUND(SUM($netSalesExpr) / COUNT(DISTINCT d.id), 2) AS aov
FROM document d
WHERE d.client_id = :uid
AND d.type = 'commande'
AND d.status IN ('expedie', 'livre', 'paye', 'facture')
$dateFilter
GROUP BY period
ORDER BY period ASC
";
$res = $conn->prepare($sql)->executeQuery($params);
$rows = method_exists($res, 'fetchAllAssociative') ? $res->fetchAllAssociative() : $res->fetchAll();
return new JsonResponse([
'labels' => array_column($rows, 'period'),
'values' => array_map(fn($r) => (float)$r['aov'], $rows),
]);
}
/**
* @Route("/admin/user/{id}/stats/exchange", name="client_stats_exchange", methods={"GET"})
*/
public function clientExchangeRate(int $id, Request $request, Connection $conn): JsonResponse {
$start = $request->query->get('startDate');
$end = $request->query->get('endDate');
$granularity = strtolower($request->query->get('granularity', 'month'));
$groupFormat = match ($granularity) {
'day' => '%Y-%m-%d',
'week' => '%Y-%u',
'year' => '%Y',
default => '%Y-%m'
};
$params = [
'clientId' => $id,
'groupFormat' => $groupFormat,
];
$dateFilter = '';
if (!empty($start)) {
$dateFilter .= " AND d.created_at >= :start";
$params['start'] = $start . ' 00:00:00';
}
if (!empty($end)) {
$dateFilter .= " AND d.created_at <= :end";
$params['end'] = $end . ' 23:59:59';
}
$sql = "
SELECT
DATE_FORMAT(d.created_at, :groupFormat) AS periode,
COUNT(DISTINCT CASE
WHEN d.type = 'commande' AND d.status IN ('expedie','livre','paye','facture') THEN d.id
END) AS valid_orders,
COUNT(DISTINCT CASE
WHEN d.type = 'echange' AND d.status != 'annule' THEN d.id
END) AS exchanges
FROM document d
WHERE d.client_id = :clientId
AND (
(d.type = 'commande' AND d.status IN ('expedie','livre','paye','facture'))
OR (d.type = 'echange' AND d.status != 'annule')
)
$dateFilter
GROUP BY DATE_FORMAT(d.created_at, :groupFormat)
ORDER BY DATE_FORMAT(d.created_at, :groupFormat) ASC
";
$rows = $conn->prepare($sql)->executeQuery($params)->fetchAllAssociative();
$labels = [];
$exchanges = [];
$totals = [];
$rates = [];
foreach ($rows as $r) {
$valid = (int)($r['valid_orders'] ?? 0);
$exchange = (int)($r['exchanges'] ?? 0);
$rate = $valid > 0 ? round($exchange / $valid * 100, 1) : 0;
$labels[] = $r['periode'];
$exchanges[] = $exchange;
$totals[] = $valid;
$rates[] = $rate;
}
return new JsonResponse([
'labels' => $labels,
'exchanges' => $exchanges,
'totals' => $totals,
'rates' => $rates
]);
}
/**
* @Route("/admin/user/{id}/stats/by-declination", name="client_stats_by_declination", methods={"GET"})
*/
public function clientStatsByDeclination(int $id, Request $request, Connection $conn): JsonResponse {
$start = $request->query->get('startDate');
$end = $request->query->get('endDate');
$params = ['clientId' => $id];
$dateFilter = '';
if (!empty($start)) {
$dateFilter .= " AND d.created_at >= :start";
$params['start'] = $start . ' 00:00:00';
}
if (!empty($end)) {
$dateFilter .= " AND d.created_at <= :end";
$params['end'] = $end . ' 23:59:59';
}
$sql = "
SELECT
dcl.name AS declinaison,
v.name AS valeur,
SUM(ddp.quantity) AS total_qty
FROM document d
JOIN document_declination_produit ddp ON ddp.document_id = d.id
JOIN produit_declination_value pdv ON ddp.produit_declination_value_id = pdv.id
JOIN group_declination_value gdv ON gdv.produit_declination_id = pdv.id
JOIN value_declination v ON v.id = gdv.value_id
JOIN declination dcl ON dcl.id = v.declination_id
WHERE d.client_id = :clientId
AND d.type = 'commande'
AND d.status IN ('expedie', 'livre', 'paye', 'facture')
$dateFilter
GROUP BY dcl.name, v.name
ORDER BY dcl.name ASC, total_qty DESC
";
$rows = $conn->prepare($sql)->executeQuery($params)->fetchAllAssociative();
$dataGrouped = []; // [declinaison => [[label, value], ...]]
foreach ($rows as $r) {
$declinaison = $r['declinaison'];
$valeur = $r['valeur'];
$qty = (int)$r['total_qty'];
if (!isset($dataGrouped[$declinaison])) {
$dataGrouped[$declinaison] = [];
}
$dataGrouped[$declinaison][] = [
'label' => $valeur,
'value' => $qty
];
}
return new JsonResponse([
'data' => $dataGrouped, // => { "Couleur": [...], "Taille": [...] }
]);
}
/**
* @Route("/admin/user/{id}/stats/repeated-products", name="client_stats_repeated_products", methods={"GET"})
*/
public function getClientRepeatedProducts(int $id, Request $request, Connection $conn): JsonResponse {
$start = $request->query->get('startDate');
$end = $request->query->get('endDate');
$limit = (int)$request->query->get('limit', 10);
if ($limit < 1) $limit = 10;
$params = ['clientId' => $id];
$dateFilter = '';
if (!empty($start)) {
$dateFilter .= " AND d.created_at >= :start";
$params['start'] = $start . ' 00:00:00';
}
if (!empty($end)) {
$dateFilter .= " AND d.created_at <= :end";
$params['end'] = $end . ' 23:59:59';
}
$sql = "
SELECT
p.name AS label,
COUNT(d.id) AS total_orders,
COUNT(DISTINCT d.id) AS unique_orders
FROM document d
JOIN document_declination_produit pdv ON pdv.document_id = d.id
JOIN produit_declination_value dv ON dv.id = pdv.produit_declination_value_id
JOIN produit p ON p.id = dv.produit_id
WHERE d.client_id = :clientId
AND d.type = 'commande'
AND d.status IN ('expedie','livre','paye','facture')
$dateFilter
GROUP BY p.id, p.name
HAVING COUNT(DISTINCT d.id) >= 2
ORDER BY total_orders DESC
LIMIT $limit
";
$rows = $conn->prepare($sql)->executeQuery($params)->fetchAllAssociative();
return new JsonResponse([
'labels' => array_column($rows, 'label'),
'values' => array_map(fn($r) => (int)($r['total_orders'] ?? 0), $rows),
]);
}
/**
* @Route("/admin/user/{id}/stats/payment-modes", name="client_stats_payment_modes", methods={"GET"})
*/
public function clientStatsPaymentModes(int $id, Request $request, Connection $conn): JsonResponse
{
$start = $request->query->get('startDate');
$end = $request->query->get('endDate');
$basis = strtolower($request->query->get('priceBasis', 'ht')); // ht | ttc
$priceField = $basis === 'ttc'
? '(d.total_amount_ttc - COALESCE(d.delivery_company_total, 0))'
: '(d.total_amount_ttc - COALESCE(d.total_tva, 0) - COALESCE(d.delivery_company_total, 0))';
$sql = "
SELECT
COALESCE(d.payment_method, 'Inconnu') AS mode,
COUNT(*) AS orders,
SUM($priceField) AS total
FROM document d
WHERE d.client_id = :clientId
AND d.type = 'commande'
AND d.status IN ('expedie','livre','paye','facture')
";
$params = ['clientId' => $id];
if (!empty($start)) {
$sql .= " AND d.created_at >= :start";
$params['start'] = $start . ' 00:00:00';
}
if (!empty($end)) {
$sql .= " AND d.created_at <= :end";
$params['end'] = $end . ' 23:59:59';
}
$sql .= " GROUP BY mode ORDER BY orders DESC";
$rows = $conn->prepare($sql)->executeQuery($params)->fetchAllAssociative();
return new JsonResponse([
'labels' => array_column($rows, 'mode'),
'orders' => array_map(fn($r) => (int) $r['orders'], $rows),
'totals' => array_map(fn($r) => round((float) $r['total'], 2), $rows),
]);
}
/**
* @Route("/admin/user/{id}/stats/promo-vs-full", name="client_stats_promo_vs_full", methods={"GET"})
*/
public function getPromoVsFullPriceStats(int $id, Request $request, Connection $conn): JsonResponse
{
$start = $request->query->get('startDate');
$end = $request->query->get('endDate');
$basis = strtolower($request->query->get('priceBasis', 'ht')); // ht | ttc
// ✅ Base de calcul propre (hors remise et livraison)
$priceField = $basis === 'ttc'
? '(d.total_amount_ttc - COALESCE(d.delivery_company_total, 0))'
: '(d.total_amount_ttc - COALESCE(d.total_tva, 0) - COALESCE(d.delivery_company_total, 0))';
$params = ['clientId' => $id];
$dateFilter = '';
if (!empty($start)) {
$dateFilter .= " AND d.created_at >= :start";
$params['start'] = $start . ' 00:00:00';
}
if (!empty($end)) {
$dateFilter .= " AND d.created_at <= :end";
$params['end'] = $end . ' 23:59:59';
}
$sql = "
SELECT
SUM(CASE WHEN d.discount > 0 THEN $priceField ELSE 0 END) AS promo_total,
SUM(CASE WHEN d.discount = 0 OR d.discount IS NULL THEN $priceField ELSE 0 END) AS full_total
FROM document d
WHERE d.client_id = :clientId
AND d.type = 'commande'
AND d.status IN ('expedie','livre','paye','facture')
$dateFilter
";
$row = $conn->prepare($sql)->executeQuery($params)->fetchAssociative();
$promoTotal = (float)($row['promo_total'] ?? 0);
$fullTotal = (float)($row['full_total'] ?? 0);
$total = $promoTotal + $fullTotal;
return new JsonResponse([
'labels' => ['Promo', 'Plein tarif'],
'values' => [$promoTotal, $fullTotal],
'percent' => [
$total > 0 ? round($promoTotal / $total * 100, 1) : 0,
$total > 0 ? round($fullTotal / $total * 100, 1) : 0
]
]);
}
}