src/Controller/HomeController.php line 49

Open in your IDE?
  1. <?php
  2. namespace App\Controller;
  3. use App\Entity\Bondesortie;
  4. use App\Entity\Client;
  5. use App\Entity\Devis;
  6. use App\Entity\User;
  7. use App\Form\AdminEditType;
  8. use App\Form\ClientType;
  9. use App\Form\EmployeType;
  10. use App\Form\RegistrationFormType;
  11. use App\Form\TechnicienType;
  12. use App\Repository\BondesortieRepository;
  13. use App\Repository\ClientRepository;
  14. use App\Repository\DevisRepository;
  15. use App\Repository\InterventionRepository;
  16. use App\Repository\UserRepository;
  17. use Doctrine\ORM\EntityManagerInterface;
  18. use DateTimeImmutable;
  19. use DateTimeInterface;
  20. use NumberFormatter;
  21. use PhpOffice\PhpSpreadsheet\Spreadsheet;
  22. use PhpOffice\PhpSpreadsheet\Style\Alignment;
  23. use PhpOffice\PhpSpreadsheet\Style\Border;
  24. use PhpOffice\PhpSpreadsheet\Style\Fill;
  25. use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
  26. use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
  27. use Symfony\Bridge\Twig\Mime\TemplatedEmail;
  28. use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
  29. use Symfony\Component\DomCrawler\Crawler;
  30. use Symfony\Component\HttpClient\HttpClient;
  31. use Symfony\Component\HttpFoundation\File\Exception\FileException;
  32. use Symfony\Component\HttpFoundation\File\File;
  33. use Symfony\Component\HttpFoundation\JsonResponse;
  34. use Symfony\Component\HttpFoundation\Request;
  35. use Symfony\Component\HttpFoundation\Response;
  36. use Symfony\Component\HttpFoundation\ResponseHeaderBag;
  37. use Symfony\Component\HttpFoundation\StreamedResponse;
  38. use Symfony\Component\Mailer\MailerInterface;
  39. use Symfony\Component\Mime\Address;
  40. use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
  41. use Symfony\Component\Routing\Annotation\Route;
  42. use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
  43. class HomeController extends AbstractController
  44. {
  45.     #[Route('admin/home', name: 'app_home')]
  46.     public function scraping(DevisRepository $devisRepository, BondesortieRepository $bondesortieRepository, ClientRepository $clientRepository, Request $request): Response
  47.     {
  48.         return $this->render('home/index.html.twig', $this->buildDashboardData(
  49.             $devisRepository,
  50.             $bondesortieRepository,
  51.             $clientRepository,
  52.             $request
  53.         ));
  54.     }
  55.     #[Route('admin/home/export-excel', name: 'app_home_export_excel', methods: ['GET'])]
  56.     public function exportExcel(DevisRepository $devisRepository, BondesortieRepository $bondesortieRepository, ClientRepository $clientRepository, Request $request): StreamedResponse
  57.     {
  58.         $data = $this->buildDashboardData(
  59.             $devisRepository,
  60.             $bondesortieRepository,
  61.             $clientRepository,
  62.             $request
  63.         );
  64.         $spreadsheet = $this->createDashboardSpreadsheet($data);
  65.         $writer = new Xlsx($spreadsheet);
  66.         $filename = $this->buildDashboardExportFilename($data['filters']);
  67.         $response = new StreamedResponse(static function () use ($writer): void {
  68.             $writer->save('php://output');
  69.         });
  70.         $response->headers->set('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
  71.         $response->headers->set('Content-Disposition', $response->headers->makeDisposition(
  72.             ResponseHeaderBag::DISPOSITION_ATTACHMENT,
  73.             $filename
  74.         ));
  75.         return $response;
  76.     }
  77.     private function buildDashboardData(DevisRepository $devisRepository, BondesortieRepository $bondesortieRepository, ClientRepository $clientRepository, Request $request): array
  78.     {
  79.         $selectedClientId = trim((string) $request->query->get('client', ''));
  80.         $selectedDateFrom = trim((string) $request->query->get('date_from', ''));
  81.         $selectedDateTo = trim((string) $request->query->get('date_to', ''));
  82.         $selectedDateRange = trim((string) $request->query->get('dateRangePicker', ''));
  83.         $selectedType = trim((string) $request->query->get('type', ''));
  84.         $selectedUnpaidOnly = in_array((string) $request->query->get('unpaid_only', ''), ['1', 'true', 'on', 'yes'], true);
  85.         if ($selectedDateRange !== '') {
  86.             [$selectedDateFrom, $selectedDateTo] = $this->parseDashboardDateRange($selectedDateRange);
  87.         } else {
  88.             $selectedDateRange = $this->buildDashboardDateRangeValue($selectedDateFrom, $selectedDateTo);
  89.         }
  90.         $selectedClient = $selectedClientId !== '' ? $clientRepository->find($selectedClientId) : null;
  91.         if ($selectedClient instanceof Client && $this->isExcludedFromDashboard($this->formatClientLabel($selectedClient))) {
  92.             // An old dashboard URL must not restore an excluded client filter.
  93.             $selectedClient = null;
  94.             $selectedClientId = '';
  95.         }
  96.         $dateFrom = $this->createDateFilter($selectedDateFrom, false);
  97.         $dateTo = $this->createDateFilter($selectedDateTo, true);
  98.         $stats = $this->createDashboardStats();
  99.         if ($selectedType !== '' && !isset($stats[$selectedType])) {
  100.             $selectedType = '';
  101.         }
  102.         $devisStatuts = $selectedType === ''
  103.             ? ['Facture', 'BL', 'BLP']
  104.             : (in_array($selectedType, ['Facture', 'BL', 'BLP'], true) ? [$selectedType] : []);
  105.         $devis = $devisStatuts !== []
  106.             ? $this->findDashboardDevis($devisRepository, $selectedClient, $dateFrom, $dateTo, $devisStatuts)
  107.             : [];
  108.         $ajustementsFactures = $this->findDashboardInvoiceAdjustments($devisRepository, $devis);
  109.         $bondesorties = $selectedType === '' || $selectedType === 'BS'
  110.             ? $this->findDashboardBondesorties($bondesortieRepository, $selectedClient, $dateFrom, $dateTo)
  111.             : [];
  112.         $documents = [];
  113.         foreach ($devis as $devi) {
  114.             $this->addDevisDashboardDocument(
  115.                 $devi,
  116.                 $stats,
  117.                 $documents,
  118.                 $selectedUnpaidOnly,
  119.                 $ajustementsFactures[(string) $devi->getId()] ?? 0.0
  120.             );
  121.         }
  122.         foreach ($bondesorties as $bondesortie) {
  123.             $this->addBondesortieDashboardDocument($bondesortie, $stats, $documents, $selectedUnpaidOnly);
  124.         }
  125.         usort($documents, static function (array $left, array $right): int {
  126.             return $right['dateTimestamp'] <=> $left['dateTimestamp'];
  127.         });
  128.         $totals = [
  129.             'count' => array_sum(array_column($stats, 'count')),
  130.             'total' => array_sum(array_column($stats, 'total')),
  131.             'paid' => array_sum(array_column($stats, 'paid')),
  132.             'remaining' => array_sum(array_column($stats, 'remaining')),
  133.         ];
  134.         return [
  135.             'clients' => array_values(array_filter(
  136.                 $clientRepository->findBy([], ['raisonsocial' => 'ASC', 'nom' => 'ASC', 'prenom' => 'ASC']),
  137.                 fn (Client $client): bool => !$this->isExcludedFromDashboard($this->formatClientLabel($client))
  138.             )),
  139.             'filters' => [
  140.                 'client' => $selectedClientId,
  141.                 'client_label' => $selectedClient instanceof Client ? $this->formatClientLabel($selectedClient) : 'Tous les clients',
  142.                 'date_from' => $selectedDateFrom,
  143.                 'date_to' => $selectedDateTo,
  144.                 'date_range' => $selectedDateRange,
  145.                 'type' => $selectedType,
  146.                 'type_label' => $selectedType !== '' ? $stats[$selectedType]['shortLabel'] : 'Tous les types',
  147.                 'unpaid_only' => $selectedUnpaidOnly,
  148.                 'payment_label' => $selectedUnpaidOnly ? 'Non payes seulement' : 'Tous les paiements',
  149.             ],
  150.             'stats' => $stats,
  151.             'totals' => $totals,
  152.             'documents' => $documents,
  153.             'chart' => [
  154.                 'labels' => array_values(array_map(static fn (array $item): string => $item['shortLabel'], $stats)),
  155.                 'remaining' => array_values(array_map(static fn (array $item): float => round($item['remaining'], 3), $stats)),
  156.                 'paid' => array_values(array_map(static fn (array $item): float => round($item['paid'], 3), $stats)),
  157.                 'total' => array_values(array_map(static fn (array $item): float => round($item['total'], 3), $stats)),
  158.             ],
  159.         ];
  160.     }
  161.     private function parseDashboardDateRange(string $dateRange): array
  162.     {
  163.         if (str_contains($dateRange, ' to ')) {
  164.             $parts = explode(' to ', $dateRange);
  165.         } elseif (str_contains($dateRange, ' au ')) {
  166.             $parts = explode(' au ', $dateRange);
  167.         } else {
  168.             $parts = [$dateRange];
  169.         }
  170.         $dateFrom = $this->normalizeDashboardDateString($parts[0] ?? '');
  171.         $dateTo = $this->normalizeDashboardDateString($parts[1] ?? ($parts[0] ?? ''));
  172.         return [$dateFrom, $dateTo];
  173.     }
  174.     private function normalizeDashboardDateString(string $date): string
  175.     {
  176.         $date = trim($date);
  177.         if ($date === '') {
  178.             return '';
  179.         }
  180.         $months = [
  181.             'janv' => 'Jan', 'janvier' => 'Jan',
  182.             'fevr' => 'Feb', 'fevrier' => 'Feb', 'févr' => 'Feb', 'février' => 'Feb',
  183.             'mars' => 'Mar',
  184.             'avr' => 'Apr', 'avril' => 'Apr',
  185.             'mai' => 'May',
  186.             'juin' => 'Jun',
  187.             'juil' => 'Jul', 'juillet' => 'Jul',
  188.             'aout' => 'Aug', 'août' => 'Aug',
  189.             'sept' => 'Sep', 'septembre' => 'Sep',
  190.             'oct' => 'Oct', 'octobre' => 'Oct',
  191.             'nov' => 'Nov', 'novembre' => 'Nov',
  192.             'dec' => 'Dec', 'decembre' => 'Dec', 'déc' => 'Dec', 'décembre' => 'Dec',
  193.         ];
  194.         $lowerDate = mb_strtolower($date);
  195.         $mappedDate = strtr($lowerDate, $months);
  196.         $formats = ['Y-m-d', 'd-m-Y', 'd/m/Y', 'd M, Y', 'd M Y'];
  197.         foreach ($formats as $format) {
  198.             $dateTime = DateTimeImmutable::createFromFormat($format, $format === 'Y-m-d' || $format === 'd-m-Y' || $format === 'd/m/Y' ? $date : $mappedDate);
  199.             if ($dateTime instanceof DateTimeImmutable) {
  200.                 return $dateTime->format('Y-m-d');
  201.             }
  202.         }
  203.         return '';
  204.     }
  205.     private function buildDashboardDateRangeValue(string $dateFrom, string $dateTo): string
  206.     {
  207.         if ($dateFrom !== '' && $dateTo !== '') {
  208.             return $dateFrom.' to '.$dateTo;
  209.         }
  210.         if ($dateFrom !== '') {
  211.             return $dateFrom;
  212.         }
  213.         return $dateTo;
  214.     }
  215.     private function createDashboardSpreadsheet(array $data): Spreadsheet
  216.     {
  217.         $spreadsheet = new Spreadsheet();
  218.         $sheet = $spreadsheet->getActiveSheet();
  219.         $sheet->setTitle('Reste a payer');
  220.         $filters = $data['filters'];
  221.         $documents = $data['documents'];
  222.         $totals = $data['totals'];
  223.         $sheet->mergeCells('A1:G1');
  224.         $sheet->setCellValue('A1', 'Tableau de bord des restes a payer');
  225.         $sheet->getStyle('A1')->getFont()->setBold(true)->setSize(16)->getColor()->setRGB('FFFFFF');
  226.         $sheet->getStyle('A1')->getAlignment()->setHorizontal(Alignment::HORIZONTAL_CENTER);
  227.         $sheet->getStyle('A1:G1')->getFill()->setFillType(Fill::FILL_SOLID)->getStartColor()->setRGB('405189');
  228.         $sheet->mergeCells('A2:G2');
  229.         $sheet->setCellValue('A2', sprintf(
  230.             'Type: %s | Client: %s | Periode: %s | Paiement: %s',
  231.             $filters['type_label'],
  232.             $filters['client_label'],
  233.             $this->formatDashboardPeriodLabel($filters['date_from'], $filters['date_to']),
  234.             $filters['payment_label']
  235.         ));
  236.         $sheet->getStyle('A2')->getFont()->setItalic(true)->getColor()->setRGB('495057');
  237.         $sheet->getStyle('A2')->getAlignment()->setHorizontal(Alignment::HORIZONTAL_CENTER);
  238.         $headers = ['Type', 'Numero', 'Date', 'Client', 'Total TTC', 'Paye', 'Reste a payer'];
  239.         $sheet->fromArray($headers, null, 'A4');
  240.         $sheet->getStyle('A4:G4')->getFont()->setBold(true)->getColor()->setRGB('000000');
  241.         $sheet->getStyle('A4:G4')->getFill()->setFillType(Fill::FILL_SOLID)->getStartColor()->setRGB('D0E3FA');
  242.         $sheet->getStyle('A4:G4')->getAlignment()->setHorizontal(Alignment::HORIZONTAL_CENTER);
  243.         $row = 5;
  244.         foreach ($documents as $document) {
  245.             $sheet->setCellValue("A$row", $document['typeLabel']);
  246.             $sheet->setCellValue("B$row", $document['number']);
  247.             if ($document['date'] instanceof DateTimeInterface) {
  248.                 $sheet->setCellValue("C$row", \PhpOffice\PhpSpreadsheet\Shared\Date::PHPToExcel($document['date']));
  249.             } else {
  250.                 $sheet->setCellValue("C$row", '');
  251.             }
  252.             $sheet->setCellValue("D$row", $document['client']);
  253.             $sheet->setCellValue("E$row", round((float) $document['total'], 3));
  254.             $sheet->setCellValue("F$row", round((float) $document['paid'], 3));
  255.             $sheet->setCellValue("G$row", round((float) $document['remaining'], 3));
  256.             if ($row % 2 === 0) {
  257.                 $sheet->getStyle("A$row:G$row")->getFill()->setFillType(Fill::FILL_SOLID)->getStartColor()->setRGB('F8F9FA');
  258.             }
  259.             $row++;
  260.         }
  261.         $totalRow = $row;
  262.         $sheet->setCellValue("A$totalRow", 'Total');
  263.         $sheet->mergeCells("A$totalRow:D$totalRow");
  264.         $sheet->setCellValue("E$totalRow", round((float) $totals['total'], 3));
  265.         $sheet->setCellValue("F$totalRow", round((float) $totals['paid'], 3));
  266.         $sheet->setCellValue("G$totalRow", round((float) $totals['remaining'], 3));
  267.         $sheet->getStyle("A$totalRow:G$totalRow")->getFont()->setBold(true)->getColor()->setRGB('9C0006');
  268.         $sheet->getStyle("A$totalRow:G$totalRow")->getFill()->setFillType(Fill::FILL_SOLID)->getStartColor()->setRGB('FFD9D9');
  269.         $sheet->getStyle("A$totalRow:D$totalRow")->getAlignment()->setHorizontal(Alignment::HORIZONTAL_CENTER);
  270.         $lastRow = max($totalRow, 5);
  271.         $sheet->getStyle("C5:C$lastRow")->getNumberFormat()->setFormatCode('dd/mm/yyyy');
  272.         $sheet->getStyle("E5:G$lastRow")->getNumberFormat()->setFormatCode('#,##0.000');
  273.         $sheet->getStyle("A4:G$lastRow")->getBorders()->getAllBorders()->setBorderStyle(Border::BORDER_THIN)->getColor()->setRGB('E9EBEC');
  274.         $sheet->getStyle("E5:G$lastRow")->getAlignment()->setHorizontal(Alignment::HORIZONTAL_RIGHT);
  275.         $sheet->getStyle("A4:G$lastRow")->getAlignment()->setVertical(Alignment::VERTICAL_CENTER);
  276.         foreach (range('A', 'G') as $column) {
  277.             $sheet->getColumnDimension($column)->setAutoSize(true);
  278.         }
  279.         $sheet->freezePane('A5');
  280.         $sheet->setAutoFilter("A4:G$lastRow");
  281.         return $spreadsheet;
  282.     }
  283.     private function formatDashboardPeriodLabel(string $dateFrom, string $dateTo): string
  284.     {
  285.         if ($dateFrom === '' && $dateTo === '') {
  286.             return 'Toutes les dates';
  287.         }
  288.         if ($dateFrom !== '' && $dateTo !== '') {
  289.             return $this->formatFilterDate($dateFrom).' au '.$this->formatFilterDate($dateTo);
  290.         }
  291.         if ($dateFrom !== '') {
  292.             return 'A partir du '.$this->formatFilterDate($dateFrom);
  293.         }
  294.         return 'Jusqu au '.$this->formatFilterDate($dateTo);
  295.     }
  296.     private function formatFilterDate(string $date): string
  297.     {
  298.         $dateTime = DateTimeImmutable::createFromFormat('Y-m-d', $date);
  299.         return $dateTime instanceof DateTimeImmutable ? $dateTime->format('d/m/Y') : $date;
  300.     }
  301.     private function buildDashboardExportFilename(array $filters): string
  302.     {
  303.         $parts = [
  304.             'reste_a_payer',
  305.             $filters['type'] !== '' ? $filters['type'] : 'tous_types',
  306.             $filters['client_label'] !== 'Tous les clients' ? $filters['client_label'] : 'tous_clients',
  307.         ];
  308.         if ($filters['date_from'] !== '' || $filters['date_to'] !== '') {
  309.             $parts[] = ($filters['date_from'] !== '' ? str_replace('-', '', $filters['date_from']) : 'debut')
  310.                 .'_au_'
  311.                 .($filters['date_to'] !== '' ? str_replace('-', '', $filters['date_to']) : 'fin');
  312.         } else {
  313.             $parts[] = 'toutes_dates';
  314.         }
  315.         if ($filters['unpaid_only']) {
  316.             $parts[] = 'non_payes';
  317.         }
  318.         $filename = strtolower(implode('_', $parts));
  319.         $filename = preg_replace('/[^a-z0-9_]+/i', '_', $filename) ?? 'reste_a_payer';
  320.         $filename = trim($filename, '_');
  321.         return $filename.'.xlsx';
  322.     }
  323.     private function createDateFilter(string $date, bool $endOfDay): ?DateTimeImmutable
  324.     {
  325.         if ($date === '') {
  326.             return null;
  327.         }
  328.         $dateTime = DateTimeImmutable::createFromFormat('Y-m-d', $date);
  329.         if (!$dateTime instanceof DateTimeImmutable) {
  330.             return null;
  331.         }
  332.         return $endOfDay ? $dateTime->setTime(23, 59, 59) : $dateTime->setTime(0, 0, 0);
  333.     }
  334.     /**
  335.      * @return Devis[]
  336.      */
  337.     private function findDashboardDevis(DevisRepository $devisRepository, ?Client $client, ?DateTimeInterface $dateFrom, ?DateTimeInterface $dateTo, array $statuts): array
  338.     {
  339.         $queryBuilder = $devisRepository->createQueryBuilder('d')
  340.             ->leftJoin('d.client', 'c')
  341.             ->addSelect('c')
  342.             ->leftJoin('d.paymentdevis', 'paymentdevis')
  343.             ->addSelect('paymentdevis')
  344.             ->andWhere('d.statut IN (:statuts)')
  345.             ->setParameter('statuts', $statuts)
  346.             ->orderBy('d.date', 'DESC')
  347.             ->addOrderBy('d.datecreation', 'DESC')
  348.             ->addOrderBy('d.id', 'DESC');
  349.         if ($client instanceof Client) {
  350.             $queryBuilder
  351.                 ->andWhere('d.client = :client')
  352.                 ->setParameter('client', $client);
  353.         }
  354.         if ($dateFrom instanceof DateTimeInterface) {
  355.             $queryBuilder
  356.                 ->andWhere('COALESCE(d.date, d.datecreation) >= :dateFrom')
  357.                 ->setParameter('dateFrom', $dateFrom);
  358.         }
  359.         if ($dateTo instanceof DateTimeInterface) {
  360.             $queryBuilder
  361.                 ->andWhere('COALESCE(d.date, d.datecreation) <= :dateTo')
  362.                 ->setParameter('dateTo', $dateTo);
  363.         }
  364.         return $queryBuilder->getQuery()->getResult();
  365.     }
  366.     /**
  367.      * Returns the total of valid returns and credit notes for each displayed invoice.
  368.      *
  369.      * Adjustments are deliberately not constrained by the dashboard date range: that
  370.      * range selects the invoices to report, while the displayed amount must remain
  371.      * the current net amount of each selected invoice.
  372.      *
  373.      * @param Devis[] $devis
  374.      * @return array<string, float>
  375.      */
  376.     private function findDashboardInvoiceAdjustments(DevisRepository $devisRepository, array $devis): array
  377.     {
  378.         $invoiceIds = [];
  379.         foreach ($devis as $devisItem) {
  380.             if ($devisItem->getStatut() === 'Facture' && $devisItem->getId() !== null) {
  381.                 $invoiceIds[] = (string) $devisItem->getId();
  382.             }
  383.         }
  384.         $invoiceIds = array_values(array_unique($invoiceIds));
  385.         if ($invoiceIds === []) {
  386.             return [];
  387.         }
  388.         $adjustments = $devisRepository->createQueryBuilder('adjustment')
  389.             ->andWhere(
  390.                 '(adjustment.statut = :returnStatus AND adjustment.frtour IN (:invoiceIds))'
  391.                 .' OR (adjustment.statut = :creditNoteStatus AND adjustment.fav IN (:invoiceIds))'
  392.             )
  393.             ->setParameter('returnStatus', 'Facture-R')
  394.             ->setParameter('creditNoteStatus', 'Facture-A')
  395.             ->setParameter('invoiceIds', $invoiceIds)
  396.             ->getQuery()
  397.             ->getResult();
  398.         $totalsByInvoice = [];
  399.         $processedAdjustmentIds = [];
  400.         foreach ($adjustments as $adjustment) {
  401.             $adjustmentId = $adjustment->getId();
  402.             if ($adjustmentId === null || isset($processedAdjustmentIds[$adjustmentId])) {
  403.                 continue;
  404.             }
  405.             $invoiceId = $adjustment->getStatut() === 'Facture-R'
  406.                 ? $adjustment->getFrtour()
  407.                 : $adjustment->getFav();
  408.             if ($invoiceId === null || !in_array((string) $invoiceId, $invoiceIds, true)) {
  409.                 continue;
  410.             }
  411.             $processedAdjustmentIds[$adjustmentId] = true;
  412.             $totalsByInvoice[(string) $invoiceId] = ($totalsByInvoice[(string) $invoiceId] ?? 0.0)
  413.                 + $this->firstPositiveAmount([$adjustment->getTotalttc(), $adjustment->getTotal()]);
  414.         }
  415.         return $totalsByInvoice;
  416.     }
  417.     /**
  418.      * @return Bondesortie[]
  419.      */
  420.     private function findDashboardBondesorties(BondesortieRepository $bondesortieRepository, ?Client $client, ?DateTimeInterface $dateFrom, ?DateTimeInterface $dateTo): array
  421.     {
  422.         $queryBuilder = $bondesortieRepository->createQueryBuilder('b')
  423.             ->leftJoin('b.client', 'c')
  424.             ->addSelect('c')
  425.             ->orderBy('b.date', 'DESC')
  426.             ->addOrderBy('b.datecreation', 'DESC')
  427.             ->addOrderBy('b.id', 'DESC');
  428.         if ($client instanceof Client) {
  429.             $queryBuilder
  430.                 ->andWhere('b.client = :client')
  431.                 ->setParameter('client', $client);
  432.         }
  433.         if ($dateFrom instanceof DateTimeInterface) {
  434.             $queryBuilder
  435.                 ->andWhere('COALESCE(b.date, b.datecreation) >= :dateFrom')
  436.                 ->setParameter('dateFrom', $dateFrom);
  437.         }
  438.         if ($dateTo instanceof DateTimeInterface) {
  439.             $queryBuilder
  440.                 ->andWhere('COALESCE(b.date, b.datecreation) <= :dateTo')
  441.                 ->setParameter('dateTo', $dateTo);
  442.         }
  443.         return $queryBuilder->getQuery()->getResult();
  444.     }
  445.     private function createDashboardStats(): array
  446.     {
  447.         return [
  448.             'Facture' => [
  449.                 'label' => 'Factures',
  450.                 'shortLabel' => 'Facture',
  451.                 'route' => 'app_devis_liste_facture',
  452.                 'color' => 'primary',
  453.                 'icon' => 'ri-file-list-3-line',
  454.                 'count' => 0,
  455.                 'total' => 0.0,
  456.                 'paid' => 0.0,
  457.                 'remaining' => 0.0,
  458.             ],
  459.             'BL' => [
  460.                 'label' => 'Bons de livraison',
  461.                 'shortLabel' => 'BL',
  462.                 'route' => 'app_devis_liste_bl',
  463.                 'color' => 'success',
  464.                 'icon' => 'ri-truck-line',
  465.                 'count' => 0,
  466.                 'total' => 0.0,
  467.                 'paid' => 0.0,
  468.                 'remaining' => 0.0,
  469.             ],
  470.             'BLP' => [
  471.                 'label' => 'BL particuliers',
  472.                 'shortLabel' => 'BLP',
  473.                 'route' => 'app_devis_particulier_index',
  474.                 'color' => 'warning',
  475.                 'icon' => 'ri-user-received-2-line',
  476.                 'count' => 0,
  477.                 'total' => 0.0,
  478.                 'paid' => 0.0,
  479.                 'remaining' => 0.0,
  480.             ],
  481.             'BS' => [
  482.                 'label' => 'Bons de sortie',
  483.                 'shortLabel' => 'BS',
  484.                 'route' => 'app_bondesortie_index',
  485.                 'color' => 'danger',
  486.                 'icon' => 'ri-archive-drawer-line',
  487.                 'count' => 0,
  488.                 'total' => 0.0,
  489.                 'paid' => 0.0,
  490.                 'remaining' => 0.0,
  491.             ],
  492.         ];
  493.     }
  494.     private function addDevisDashboardDocument(
  495.         Devis $devis,
  496.         array &$stats,
  497.         array &$documents,
  498.         bool $unpaidOnly,
  499.         float $invoiceAdjustments = 0.0
  500.     ): void
  501.     {
  502.         $type = $devis->getStatut();
  503.         if (!isset($stats[$type])) {
  504.             return;
  505.         }
  506.         if ($this->isExcludedFromDashboard($this->getDevisClientLabel($devis))) {
  507.             return;
  508.         }
  509.         // Do not use montantNet here: it is a persisted cache maintained by the
  510.         // invoice workflow. Calculating from the original TTC prevents a return or
  511.         // credit note from being deducted a second time.
  512.         $total = $this->firstPositiveAmount([$devis->getTotalttc(), $devis->getTotal()]);
  513.         // Some legacy invoices only have the net cache populated. Preserve their
  514.         // existing display behaviour when no original TTC is available.
  515.         if ($total === 0.0) {
  516.             $total = $this->firstPositiveAmount([$devis->getMontantNet()]);
  517.         }
  518.         if ($type === 'Facture') {
  519.             $total = max(0.0, $total - $invoiceAdjustments);
  520.         }
  521.         $payment = $devis->getPaymentdevis()->first();
  522.         $remaining = $total;
  523.         if ($payment !== false) {
  524.             // "reste" is the actual outstanding balance. "montant" may still contain
  525.             // the original invoice amount on legacy payments even when the balance is zero.
  526.             $remaining = $this->firstAvailableAmount([$payment->getReste(), $payment->getMontant()], $total);
  527.         }
  528.         $remaining = max(0.0, $remaining);
  529.         if (!$this->shouldKeepDashboardDocument($remaining, $unpaidOnly)) {
  530.             return;
  531.         }
  532.         $paid = max(0.0, $total - $remaining);
  533.         $date = $devis->getDate() ?? $devis->getDatecreation();
  534.         $this->addDashboardAmounts($stats[$type], $total, $paid, $remaining);
  535.         $documents[] = [
  536.             'id' => $devis->getId(),
  537.             'type' => $type,
  538.             'typeLabel' => $stats[$type]['shortLabel'],
  539.             'typeColor' => $stats[$type]['color'],
  540.             'number' => $devis->getNum() ?: '#'.$devis->getId(),
  541.             'date' => $date,
  542.             'dateTimestamp' => $date instanceof DateTimeInterface ? $date->getTimestamp() : 0,
  543.             'client' => $this->getDevisClientLabel($devis),
  544.             'total' => $total,
  545.             'paid' => $paid,
  546.             'remaining' => $remaining,
  547.             'url' => $type === 'BLP'
  548.                 ? $this->generateUrl('app_devis_particulier_show', ['id' => $devis->getId()])
  549.                 : $this->generateUrl('app_devis_show', ['id' => $devis->getId()]),
  550.         ];
  551.     }
  552.     private function addBondesortieDashboardDocument(Bondesortie $bondesortie, array &$stats, array &$documents, bool $unpaidOnly): void
  553.     {
  554.         if ($this->isExcludedFromDashboard($this->getBondesortieClientLabel($bondesortie))) {
  555.             return;
  556.         }
  557.         $type = 'BS';
  558.         $total = $this->firstPositiveAmount([$bondesortie->getTotalttc(), $bondesortie->getTotaleht()]);
  559.         $remaining = $total;
  560.         if (!$this->shouldKeepDashboardDocument($remaining, $unpaidOnly)) {
  561.             return;
  562.         }
  563.         $paid = 0.0;
  564.         $date = $bondesortie->getDate() ?? $bondesortie->getDatecreation();
  565.         $this->addDashboardAmounts($stats[$type], $total, $paid, $remaining);
  566.         $documents[] = [
  567.             'id' => $bondesortie->getId(),
  568.             'type' => $type,
  569.             'typeLabel' => $stats[$type]['shortLabel'],
  570.             'typeColor' => $stats[$type]['color'],
  571.             'number' => $bondesortie->getNumbs() ?: '#'.$bondesortie->getId(),
  572.             'date' => $date,
  573.             'dateTimestamp' => $date instanceof DateTimeInterface ? $date->getTimestamp() : 0,
  574.             'client' => $this->getBondesortieClientLabel($bondesortie),
  575.             'total' => $total,
  576.             'paid' => $paid,
  577.             'remaining' => $remaining,
  578.             'url' => $this->generateUrl('app_bondesortie_show', ['id' => $bondesortie->getId()]),
  579.         ];
  580.     }
  581.     private function shouldKeepDashboardDocument(float $remaining, bool $unpaidOnly): bool
  582.     {
  583.         return !$unpaidOnly || round($remaining, 3) > 0.001;
  584.     }
  585.     private function isExcludedFromDashboard(string $clientLabel): bool
  586.     {
  587.         // Apply before aggregation, including documents with a manually entered client name.
  588.         return mb_strtolower(trim($clientLabel)) === 'textiles forever';
  589.     }
  590.     private function addDashboardAmounts(array &$stat, float $total, float $paid, float $remaining): void
  591.     {
  592.         $stat['count']++;
  593.         $stat['total'] += $total;
  594.         $stat['paid'] += $paid;
  595.         $stat['remaining'] += $remaining;
  596.     }
  597.     private function firstPositiveAmount(array $amounts): float
  598.     {
  599.         foreach ($amounts as $amount) {
  600.             $parsedAmount = $this->parseAmount($amount);
  601.             if ($parsedAmount > 0) {
  602.                 return $parsedAmount;
  603.             }
  604.         }
  605.         return 0.0;
  606.     }
  607.     private function firstAvailableAmount(array $amounts, float $fallback): float
  608.     {
  609.         foreach ($amounts as $amount) {
  610.             if ($amount !== null && trim((string) $amount) !== '') {
  611.                 return $this->parseAmount($amount);
  612.             }
  613.         }
  614.         return $fallback;
  615.     }
  616.     private function parseAmount($value): float
  617.     {
  618.         $normalized = trim((string) $value);
  619.         if ($normalized === '') {
  620.             return 0.0;
  621.         }
  622.         $normalized = preg_replace('/[^0-9,\.\-]/', '', $normalized) ?? '';
  623.         if (str_contains($normalized, ',') && !str_contains($normalized, '.')) {
  624.             $normalized = str_replace(',', '.', $normalized);
  625.         } else {
  626.             $normalized = str_replace(',', '', $normalized);
  627.         }
  628.         return is_numeric($normalized) ? (float) $normalized : 0.0;
  629.     }
  630.     private function getDevisClientLabel(Devis $devis): string
  631.     {
  632.         if ($devis->getClient() instanceof Client) {
  633.             return $this->formatClientLabel($devis->getClient());
  634.         }
  635.         $nomClient = trim((string) $devis->getNomclient());
  636.         return $nomClient !== '' ? $nomClient : '-';
  637.     }
  638.     private function getBondesortieClientLabel(Bondesortie $bondesortie): string
  639.     {
  640.         if ($bondesortie->getClient() instanceof Client) {
  641.             return $this->formatClientLabel($bondesortie->getClient());
  642.         }
  643.         $nomPrenom = trim((string) $bondesortie->getNomprenom());
  644.         return $nomPrenom !== '' ? $nomPrenom : '-';
  645.     }
  646.     private function formatClientLabel(Client $client): string
  647.     {
  648.         $raisonSociale = trim((string) $client->getRaisonsocial());
  649.         if ($raisonSociale !== '') {
  650.             return $raisonSociale;
  651.         }
  652.         $nomComplet = trim(sprintf('%s %s', (string) $client->getNom(), (string) $client->getPrenom()));
  653.         return $nomComplet !== '' ? $nomComplet : '-';
  654.     }
  655.     #[Route('admin/Listes_techniciens', name: 'app_listeassitant')]
  656.     public function listeassitant(Request $request, UserRepository $userRepository): Response
  657.     {
  658.         $assistant= $userRepository->findByRoleTechnicien();
  659.         return $this->render('registration/listeassistant.html.twig', [
  660.                 'assistants' =>$assistant,
  661.         ]);
  662.     }
  663.     #[Route('admin/Listes_clients', name: 'app_clients_liste')]
  664.     public function listeclient(Request $request, ClientRepository $clientRepository): Response
  665.     {
  666.         $assistant= $clientRepository->findAll();
  667.         return $this->render('registration/listeclient.html.twig', [
  668.             'clients' =>$assistant,
  669.         ]);
  670.     }
  671.     #[Route('admin/technicien', name: 'app_assistant')]
  672.     public function addadmin(Request $request, UserPasswordHasherInterface $userPasswordHasher, MailerInterface $mailer, EntityManagerInterface $entityManager): Response
  673.     {
  674.         $user = new User();
  675.         $form = $this->createForm(TechnicienType::class, $user);
  676.         $form->handleRequest($request);
  677.         if ($form->isSubmitted() && $form->isValid()) {
  678.             // Generate a password
  679.             $password = $this->generatePassword(12);
  680.             // Encode the plain password
  681.             $user->setPassword(
  682.                 $userPasswordHasher->hashPassword(
  683.                     $user,
  684.                     $password
  685.                 )
  686.             );
  687.             $user->setRoles(['ROLE_TECHNICIEN']);
  688.             $user->setPhoto("user.png");
  689.             $entityManager->persist($user);
  690.             $entityManager->flush();
  691.             // Send email
  692.             $email = (new TemplatedEmail())
  693.                 ->from(new Address('support@smart-it-partner.com', 'Tricotage '))
  694.                 ->to($user->getEmail())
  695.                 ->subject('Démarrez Votre Parcours avec Tricotage en tant que Technicien')
  696.                 ->htmlTemplate('registration/send_email.html.twig')
  697.                 ->context([
  698.                     'emails' => $user->getEmail(),
  699.                     'password' => $password,'user'=>$user,
  700.                 ]);
  701.             $mailer->send($email);
  702.             return $this->redirectToRoute('app_listeassitant');
  703.         }
  704.         return $this->render('registration/ajouterassistant.html.twig', [
  705.             'registrationForm' => $form->createView(),
  706.         ]);
  707.     }
  708.     #[Route('/techniciens/{id}/edit', name: 'app_techniciens_edit', methods: ['GET', 'POST'])]
  709.     public function edittechniciens(Request $request, User $user,UserRepository $userRepository): Response
  710.     {
  711.         $form = $this->createForm(TechnicienType::class, $user);
  712.         $form->handleRequest($request);
  713.         if ($form->isSubmitted() && $form->isValid()) {
  714.             $userRepository->save($user, true);
  715.             return $this->redirectToRoute('app_listeassitant', [], Response::HTTP_SEE_OTHER);
  716.         }
  717.         return $this->renderForm('registration/edittechniciens.html.twig', [
  718.             'user' => $user,
  719.             'registrationForm' => $form,
  720.         ]);
  721.     }
  722.     #[Route('admin/clients', name: 'app_clients')]
  723.     public function addclients(Request $request, UserPasswordHasherInterface $userPasswordHasher, MailerInterface $mailer, EntityManagerInterface $entityManager): Response
  724.     {
  725.         $user = new Client();
  726.         $form = $this->createForm(ClientType::class, $user);
  727.         $form->handleRequest($request);
  728.         if ($form->isSubmitted() && $form->isValid()) {
  729.             // Generate a password
  730.             $user->setPhoto("user.png");
  731.             $entityManager->persist($user);
  732.             $entityManager->flush();
  733.             return $this->redirectToRoute('app_clients_liste');
  734.         }
  735.         return $this->render('registration/ajouterclient.html.twig', [
  736.             'registrationForm' => $form->createView(),
  737.         ]);
  738.     }
  739.     #[Route('/verifier-client', name: 'verifier_client', methods: ['POST'])]
  740.     public function verifierClient(Request $request, ClientRepository $clientRepository): JsonResponse
  741.     {
  742.         // Récupérer la Raison Sociale envoyée par la requête AJAX
  743.         $raisonSociale = $request->request->get('raisonSociale');
  744. //        dump($raisonSociale);
  745.         // Vérifier si un client avec cette Raison Sociale existe
  746.         if ($raisonSociale) {
  747.             $client = $clientRepository->findOneBy(['raisonsocial' => $raisonSociale]);
  748.             // Si le client existe, retourner une réponse JSON
  749.             if ($client) {
  750.                 return new JsonResponse([
  751.                     'exists' => true,
  752.                     'message' => 'Le client existe déjà avec cette Raison Sociale.'
  753.                 ]);
  754.             }
  755.             // Si le client n'existe pas, retourner une réponse JSON indiquant cela
  756.             return new JsonResponse([
  757.                 'exists' => false,
  758.                 'message' => 'Aucun client trouvé avec cette Raison Sociale.'
  759.             ]);
  760.         }
  761.         // En cas de requête invalide ou sans Raison Sociale
  762.         return new JsonResponse([
  763.             'exists' => false,
  764.             'message' => 'Raison Sociale non fournie.'
  765.         ], 400);  // Code HTTP 400 pour indiquer une requête invalide
  766.     }
  767.     private function generatePassword($size)
  768.     {
  769.         $characters = array(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z");
  770.         $password = '';
  771.         for ($i = 0; $i < $size; $i++) {
  772.             $password .= ($i % 2) ? strtoupper($characters[array_rand($characters)]) : $characters[array_rand($characters)];
  773.         }
  774.         return $password;
  775.     }
  776.     #[Route('admin/edit_profil', name: 'app_editprofile',methods: ['GET', 'POST'])]
  777.     public function editprofile(Request $request, UserRepository $userRepository): Response
  778.     {
  779.         $user= $userRepository->findOneBy(['id'=>$this->getUser()->getid()]) ;
  780.         $name = $user->getPhoto();
  781.         $form = $this->createForm(AdminEditType::class, $user);
  782.         $form->handleRequest($request);
  783.         if ($form->isSubmitted() && $form->isValid()) {
  784.             $logo = $form->get('photo')->getData();
  785.             if ($logo) {
  786.                 $originalLogoname = pathinfo( $logo->getClientOriginalName(), PATHINFO_FILENAME);
  787.                 $newLogoname =   $originalLogoname.'-'.uniqid().'.'.$logo->guessExtension();
  788.                 // Move the file to the directory where brochures are stored
  789.                 try {
  790.                     $logo->move(
  791.                         $this->getParameter('admin_directory'),
  792.                         $newLogoname
  793.                     );
  794.                 } catch (FileException $e) {
  795.                     // ... handle exception if something happens during file upload
  796.                 }
  797.                 $user->setPhoto($newLogoname);
  798.             }
  799.             else
  800.             {
  801.                 $user->setPhoto($name);
  802.             }
  803.             $userRepository->save($user,true);
  804.             $this->addFlash('success', 'Votre compte a été modifié avec succès');
  805.             return $this->redirectToRoute('app_editprofile');
  806.         }
  807.         return $this->render('editprofile/editprofile.html.twig', [
  808.             'users' => $user,
  809.             'form' => $form->createView(),
  810.         ]);
  811.     }
  812.     #[Route('admin/edit_password', name: 'modifier-password')]
  813.     public function editpassword(Request $request, UserRepository $userRepository ,UserPasswordHasherInterface $userPasswordHasher): Response
  814.     {
  815.         $user = $this->getUser();
  816.         // Vérifier si l'utilisateur est connecté
  817.         if (!$user) {
  818.             throw $this->createAccessDeniedException('Vous devez être connecté pour modifier le mot de passe.');
  819.         }
  820.         $user= $userRepository->findOneBy(['id'=>$this->getUser()->getid()]) ;
  821. // Récupérer les données du formulaire
  822.         $currentPassword = $request->request->get('current-password');
  823.         $newPassword = $request->request->get('new-password');
  824. // Vérifier si le mot de passe actuel est correct
  825.         if (!$userPasswordHasher->isPasswordValid($user, $currentPassword)) {
  826.             $this->addFlash('error', 'Mot de passe actuel incorrect.');
  827.             return $this->redirectToRoute('app_editprofile');
  828.         }
  829. // Encoder et définir le nouveau mot de passe
  830.         $encodedPassword = $userPasswordHasher->hashPassword($user, $newPassword);
  831.         $user->setPassword($encodedPassword);
  832. // Enregistrer les modifications dans la base de données
  833.         $userRepository->save($user,true);
  834.         $this->addFlash('success', 'Le mot de passe a été modifié avec succès.');
  835.         return $this->redirectToRoute('app_editprofile');
  836.     }
  837.     #[Route('admin/intervention', name: 'app_intervention')]
  838.     public function intervention(Request $request, UserRepository $userRepository): Response
  839.     {
  840.         $assistant= $userRepository->findByRoleclient();
  841.         return $this->render('intervention/intervention.html.twig', [
  842.             'assistants' =>$assistant,
  843.         ]);
  844.     }
  845.     #[Route('/deletetechnicien/{id}', name: 'app_tecnicien_delete')]
  846.     public function deletetechniciens(Request $request, User $user , UserRepository $userRepository): Response
  847.     {
  848. //        if ($this->isCsrfTokenValid('delete'.$devi->getId(), $request->request->get('_token'))) {
  849.         $userRepository->remove($user, true);
  850. //        }
  851.         return $this->redirectToRoute('app_listeassitant', [], Response::HTTP_SEE_OTHER);
  852.     }
  853.     //Fonction mettre a jous les deux table intervention et devis a jours: si on click sur les liste dans espace admin le champs lu sera true avec la fonction ajax dans base.html.twig à l'aide de data-value
  854.     #[Route('/update_elements', name: 'update_elements', methods: ['POST','GET'])]
  855.     public function updateElements(DevisRepository $devisRepository,InterventionRepository $interventionRepository,Request $request,EntityManagerInterface $entityManager): JsonResponse
  856.     {
  857.         $url= $request->request->get('url');
  858.         // Mettre à jour les demande devis
  859.         if ($url === 'app_devis_demande') {
  860. //            dump($url);
  861.             $devis = $devisRepository->findBy(['lu' => false, 'statut'=>'Demande devis']);
  862.             foreach ($devis as $devi) {
  863.                 $devi->setLu(true);
  864.                 // Peut-être d'autres opérations ici, selon vos besoins
  865.             }
  866.         }
  867.         if ($url === 'app_devis_index') {
  868.             $devis = $devisRepository->findBy(['lu' => false, 'statut'=>'Demande devis']);
  869.             foreach ($devis as $devi) {
  870.                 $devi->setLu(true);
  871.                 // Peut-être d'autres opérations ici, selon vos besoins
  872.             }
  873.         }
  874.         // Mettre à jour les contacts
  875.         if ($url === 'app_intervention_index') {
  876.             $intervention = $interventionRepository->findBy(['lu' => false]);
  877.             foreach ($intervention as $interventions) {
  878.                 $interventions->setLu(true);
  879.                 // Peut-être d'autres opérations ici, selon vos besoins
  880.             }
  881.         }
  882.         // Enregistrer les modifications dans la base de données
  883.         $entityManager->flush();
  884.         // Répondre avec un JSON pour indiquer le succès de l'opération
  885.         return new JsonResponse(['success' => true]);
  886.     }
  887.     //La fonction compte le nombre total de notifications dans l'espace administrateur, demandes de devis et interventions signalées par le technicien.
  888.     #[Route('/totalnotif', name: 'app_total_notif', methods: ['GET'])]
  889.     public function totalnotif(DevisRepository $devisRepository,InterventionRepository $interventionRepository,Request $request): Response
  890.     {
  891.         $totaldevis = $devisRepository->getDevisByTechnicianRoleNonLu();
  892.         $totalintervention=$interventionRepository->findBy(['lu'=>false]);
  893.         $output[]=array(count($totaldevis));
  894.         $output[]=array(count($totalintervention) );
  895.         $output[]=array(count($totaldevis) + count($totalintervention) );
  896. //        dump($output);
  897.         return  new JsonResponse($output);
  898.     }
  899.     #[Route('/notifsintervention', name: 'app_notifsintervention')]
  900.     public function notifsmessage(UrlGeneratorInterface $router,InterventionRepository $interventionRepository)
  901.     {
  902.         $intervention = $interventionRepository->getInterventionsByTechnicianRoleNonLu();
  903.         $interventions = array();
  904.         foreach ($intervention as $r) {
  905.             $datee = $r->getDatecreation() ? $r->getDatecreation()->format('d-m-Y H:i') : null;
  906.             $data = [
  907.                 'id' => $r->getId(),
  908.                 'nomtech' => $r->getUser()->getNom() ,
  909.                 'prenomtech' => $r->getUser()->getPrenom() ,
  910.                 'datee' => $datee,
  911. //                'url' => $router->generate('app_message_repondre', ['id' => $r->getId()])
  912.                 // Ajoutez d'autres champs si nécessaire
  913.             ];
  914.             $interventions[] = $data;
  915.         }
  916.         return new JsonResponse($interventions);
  917.     }
  918.     #[Route('/notifsdemandedevis', name: 'app_notifsdemande_devis')]
  919.     public function notifsdemandedevis(UrlGeneratorInterface $router,DevisRepository $devisRepository)
  920.     {
  921.         $devis = $devisRepository->getDevisByTechnicianRoleNonLu();
  922.         $deviss = array();
  923.         foreach ($devis as $r) {
  924.             $datee = $r->getDatecreation() ? $r->getDatecreation()->format('d-m-Y H:i') : null;
  925.             $data = [
  926.                 'id' => $r->getId(),
  927.                 'nomtech' => $r->getUser()->getNom() ,
  928.                 'prenomtech' => $r->getUser()->getPrenom() ,
  929.                 'nomclient' => $r->getClient()->getNom() ,
  930.                 'prenomclient' => $r->getClient()->getPrenom() ,
  931.                 'datee' => $datee,
  932.                 'url' => $router->generate('app_confirmation_demande_devis', ['id' => $r->getId()])
  933.                 // Ajoutez d'autres champs si nécessaire
  934.             ];
  935.             $deviss[] = $data;
  936.         }
  937.         return new JsonResponse($deviss);
  938.     }
  939. }