src/Controller/BusinessContactsController.php line 48

  1. <?php
  2. namespace App\Controller;
  3. use App\Entity\BusinessContacts;
  4. use App\Form\BusinessContactsType;
  5. use App\Form\ImportType;
  6. use App\Repository\BusinessContactsRepository;
  7. use App\Repository\BusinessTypesRepository;
  8. use App\Services\BusinessContactVCFExport;
  9. use App\Services\ImportBusinessContactsService;
  10. use App\Services\CompanyDetailsService;
  11. use App\Services\CountBusinessContactsService;
  12. use App\Services\PhoneAnalyzer;
  13. use Doctrine\ORM\EntityManagerInterface;
  14. use PhpOffice\PhpSpreadsheet\Spreadsheet;
  15. use PhpOffice\PhpSpreadsheet\Writer\Csv;
  16. use Psr\Log\LoggerInterface;
  17. use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
  18. use Symfony\Bundle\SecurityBundle\Security;
  19. use Symfony\Component\HttpFoundation\BinaryFileResponse;
  20. use Symfony\Component\HttpFoundation\File\Exception\FileException;
  21. use Symfony\Component\HttpFoundation\Request;
  22. use Symfony\Component\HttpFoundation\Response;
  23. use Symfony\Component\HttpFoundation\ResponseHeaderBag;
  24. use Symfony\Component\HttpFoundation\StreamedResponse;
  25. use Symfony\Component\Routing\Annotation\Route;
  26. use Symfony\Component\String\Slugger\SluggerInterface;
  27. /**
  28.  * @Route("/businesscontacts")
  29.  */
  30. class BusinessContactsController extends AbstractController
  31. {
  32.     public function __construct(private readonly LoggerInterface $logger)
  33.     {
  34.     }
  35.     /**
  36.      * @Route("/index", name="business_contacts_index", methods={"GET"})
  37.      */
  38.     public function index(Request $requestBusinessContactsRepository $businessContactsRepositoryBusinessTypesRepository $businessTypesRepositoryCountBusinessContactsService $countBusinessContactsServicePhoneAnalyzer $phoneAnalyzer): Response
  39.     {
  40.         // All types (for selectors, etc.) â€” alpha by label
  41.         $business_types_all $businessTypesRepository->findAll();
  42.         usort($business_types_all, function ($a$b) {
  43.             return strcmp($a->getBusinessType(), $b->getBusinessType());
  44.         });
  45.         $businessTypeName $request->query->get('business_type');
  46.         $adminView $request->query->get('admin_view');
  47.         // Determine which types to render as groups + which contacts to include
  48.         if ($businessTypeName) {
  49.             $businessType $businessTypesRepository->findOneBy(['businessType' => $businessTypeName]);
  50.             if ($businessType) {
  51.                 // Filtered to a single type
  52.                 $business_contacts $businessContactsRepository->findBy(
  53.                     ['businessType' => $businessType],
  54.                     ['id' => 'ASC']
  55.                 );
  56.                 $business_types = [$businessType]; // headings for only this type
  57.             } else {
  58.                 // Invalid filter: fallback to all
  59.                 $business_contacts $businessContactsRepository->findAll();
  60.                 $business_types $businessTypesRepository->findBy([], ['ranking' => 'ASC']);
  61.             }
  62.         } else {
  63.             // No filter: show everything
  64.             $business_contacts $businessContactsRepository->findAll();
  65.             $business_types $businessTypesRepository->findBy([], ['ranking' => 'ASC']);
  66.         }
  67.         // Analyze phones (as before)
  68.         foreach ($business_contacts as $business_contact) {
  69.             $business_contact->phoneAnalysis = [
  70.                 'mobile' => $business_contact->getMobile() ? $phoneAnalyzer->analyze($business_contact->getMobile()) : null,
  71.                 'landline' => $business_contact->getLandline() ? $phoneAnalyzer->analyze($business_contact->getLandline()) : null,
  72.             ];
  73.         }
  74.         // --- Group + sort within each group: company -> firstName -> lastName ---
  75.         $norm = function (?string $s): string {
  76.             return $s === null '' mb_strtolower(trim($s));
  77.         };
  78.         $cmp = function ($a$b) use ($norm): int {
  79.             // Adjust getter names here if your entity differs
  80.             $aCompany $norm(method_exists($a'getCompany') ? $a->getCompany() : null);
  81.             $bCompany $norm(method_exists($b'getCompany') ? $b->getCompany() : null);
  82.             $aFirst $norm(method_exists($a'getFirstName') ? $a->getFirstName() : null);
  83.             $bFirst $norm(method_exists($b'getFirstName') ? $b->getFirstName() : null);
  84.             $aLast $norm(method_exists($a'getLastName') ? $a->getLastName() : null);
  85.             $bLast $norm(method_exists($b'getLastName') ? $b->getLastName() : null);
  86.             // Empty values sort AFTER non-empty
  87.             $emptyOrder = function (string $xstring $y): ?int {
  88.                 $xe = ($x === '');
  89.                 $ye = ($y === '');
  90.                 if ($xe && !$ye) return 1;
  91.                 if (!$xe && $ye) return -1;
  92.                 return null// both empty or both non-empty -> continue
  93.             };
  94.             // Compare company
  95.             if (($eo $emptyOrder($aCompany$bCompany)) !== null) return $eo;
  96.             if ($aCompany !== $bCompany) return $aCompany <=> $bCompany;
  97.             // Compare firstName
  98.             if (($eo $emptyOrder($aFirst$bFirst)) !== null) return $eo;
  99.             if ($aFirst !== $bFirst) return $aFirst <=> $bFirst;
  100.             // Compare lastName
  101.             if (($eo $emptyOrder($aLast$bLast)) !== null) return $eo;
  102.             if ($aLast !== $bLast) return $aLast <=> $bLast;
  103.             // Stable fallback by id
  104.             $aId method_exists($a'getId') ? (int)$a->getId() : 0;
  105.             $bId method_exists($b'getId') ? (int)$b->getId() : 0;
  106.             return $aId <=> $bId;
  107.         };
  108.         // Build grouped array keyed by the types we are actually rendering
  109.         $contacts_by_type = [];
  110.         foreach ($business_types as $bt) {
  111.             $contacts_by_type[$bt->getId()] = [];
  112.         }
  113.         foreach ($business_contacts as $c) {
  114.             $type $c->getBusinessType();
  115.             if ($type && array_key_exists($type->getId(), $contacts_by_type)) {
  116.                 $contacts_by_type[$type->getId()][] = $c;
  117.             }
  118.         }
  119.         // Sort each group
  120.         foreach ($contacts_by_type as &$group) {
  121.             usort($group$cmp);
  122.         }
  123.         unset($group);
  124.         // --- end group+sort ---
  125.         return $this->render('business_contacts/index.html.twig', [
  126.             'business_contacts' => $business_contacts,           // kept for compatibility
  127.             'business_types' => $business_types,
  128.             'business_types_all' => $business_types_all,
  129.             'countBusinessContactsService' => $countBusinessContactsService,
  130.             'list_or_map' => 'list',
  131.             'admin_check' => $adminView === 'Admin' 'Yes' 'No',
  132.             'current_business_type' => $businessTypeName,
  133.             'contacts_by_type' => $contacts_by_type,            // used by Twig single-table template
  134.         ]);
  135.     }
  136.     /**
  137.      * @Route("/map/{subset}", name="business_contacts_map", methods={"GET"})
  138.      */
  139.     public function map(Request $requeststring $subsetBusinessContactsRepository $businessContactsRepositoryBusinessTypesRepository $businessTypesRepositoryCountBusinessContactsService $countBusinessContacts): Response
  140.     {
  141.         // Admins see ALL; others only 'Approved'
  142.         $isAdmin $this->isGranted('ROLE_ADMIN');
  143.         // Build criteria
  144.         $criteria = [];
  145.         if (!$isAdmin) {
  146.             $criteria['status'] = 'Approved';
  147.         }
  148.         if ($subset !== 'All') {
  149.             $business_type $businessTypesRepository->findOneBy(['businessType' => $subset]);
  150.             // If the subset name is invalid, return no results instead of error
  151.             if ($business_type) {
  152.                 $criteria['businessType'] = $business_type;
  153.             } else {
  154.                 $business_contacts = [];
  155.             }
  156.         }
  157.         // Fetch contacts (if not already set due to invalid subset)
  158.         if (!isset($business_contacts)) {
  159.             $business_contacts $businessContactsRepository->findBy($criteria);
  160.         }
  161.         // Compute map stats (only count rows with valid coords)
  162.         $latitude_total 0.0;
  163.         $longitude_total 0.0;
  164.         $count 0;
  165.         $latitude_max = -100.0;
  166.         $latitude_min 100.0;
  167.         $longitude_max = -100.0;
  168.         $longitude_min 100.0;
  169.         foreach ($business_contacts as $business_contact) {
  170.             $lat $business_contact->getLocationLatitude();
  171.             $lng $business_contact->getLocationLongitude();
  172.             // Must have both coords and not be zero
  173.             if ($lat !== null && $lng !== null && (float)$lat !== 0.0 && (float)$lng !== 0.0) {
  174.                 $count++;
  175.                 $latitude_total += (float)$lat;
  176.                 $longitude_total += (float)$lng;
  177.                 if ($lat $latitude_max$latitude_max = (float)$lat;
  178.                 if ($lat $latitude_min$latitude_min = (float)$lat;
  179.                 if ($lng $longitude_max$longitude_max = (float)$lng;
  180.                 if ($lng $longitude_min$longitude_min = (float)$lng;
  181.             }
  182.         }
  183.         if ($count === 0) {
  184.             $latitude_average 'No data';
  185.             $longitude_average 'No data';
  186.             $latitude_range 'TBD';
  187.             $longitude_range 'TBD';
  188.         } else {
  189.             $latitude_average $latitude_total $count;
  190.             $longitude_average $longitude_total $count;
  191.             if ($count === 1) {
  192.                 $latitude_range 'TBD';
  193.                 $longitude_range 'TBD';
  194.             } else {
  195.                 $latitude_range $latitude_max $latitude_min;
  196.                 $longitude_range $longitude_max $longitude_min;
  197.             }
  198.         }
  199.         $business_types $businessTypesRepository->findBy([], ['ranking' => 'ASC']);
  200.         return $this->render('business_contacts/map_of_business_contacts.html.twig', [
  201. //            'google_maps_api_key' => $this->getParameter('google_maps_api_key'), // e.g. set in services.yaml or .env
  202.             'business_contacts' => $business_contacts,
  203.             'business_types' => $business_types,
  204.             'subset' => $subset,
  205.             'latitude_max' => $latitude_max,
  206.             'latitude_min' => $latitude_min,
  207.             'latitude_average' => $latitude_average,
  208.             'latitude_range' => $latitude_range,
  209.             'longitude_max' => $longitude_max,
  210.             'longitude_min' => $longitude_min,
  211.             'longitude_average' => $longitude_average,
  212.             'longitude_range' => $longitude_range,
  213.             'count' => $count,
  214.             'list_or_map' => 'map',
  215.             'admin_check' => $isAdmin 'Yes' 'No',
  216.         ]);
  217.     }
  218.     /**
  219.      * @Route("/new/{business_type}", name="business_contacts_new", methods={"GET", "POST"})
  220.      */
  221.     public function new(Request $requeststring $business_typeBusinessContactsRepository $businessContactsRepositoryBusinessTypesRepository $businessTypesRepositoryCompanyDetailsService $companyDetailsEntityManagerInterface $entityManager): Response
  222.     {
  223.         $business_type $businessTypesRepository->find($business_type);
  224.         $default_country $companyDetails->getCompanyDetails()->getCompanyAddressCountry();
  225.         $businessContact = new BusinessContacts();
  226.         $businessContact->setBusinessType($business_type);
  227.         $businessContact->setAddressCountry($default_country);
  228.         $form $this->createForm(BusinessContactsType::class, $businessContact);
  229.         $form->handleRequest($request);
  230.         if ($form->isSubmitted() && $form->isValid()) {
  231.             $photo $form['photo']->getData();
  232.             if ($photo) {
  233.                 $uniqueId uniqid(); // Generates a unique ID
  234.                 $uniqueId3digits substr($uniqueId103); // Extracts the first 3 digits
  235.                 $files_name = [];
  236.                 $photo_directory $this->getParameter('business_contacts_photos_directory');
  237.                 $fileName pathinfo($photo->getClientOriginalName(), PATHINFO_FILENAME);
  238.                 $file_extension $photo->guessExtension();
  239.                 $newFileName $businessContact->getCompany() . "_" $uniqueId3digits "." $file_extension;
  240.                 $photo->move($photo_directory$newFileName);
  241.                 $businessContact->setPhoto($newFileName);
  242.             }
  243.             $file $form['files']->getData();
  244.             if ($file) {
  245.                 $file_name = [];
  246.                 $file_directory $this->getParameter('business_contacts_attachments_directory');
  247.                 $fileName pathinfo($file->getClientOriginalName(), PATHINFO_FILENAME);
  248.                 $file_extension $file->guessExtension();
  249.                 $newFileName $fileName "." $file_extension;
  250.                 $file->move($file_directory$newFileName);
  251.                 $businessContact->setFiles($newFileName);
  252.             }
  253.             $businessContactsRepository->add($businessContacttrue);
  254.             $firstName $businessContact->getFirstName();
  255.             $lastName $businessContact->getLastName();
  256.             $entityManager->persist($businessContact);
  257.             $entityManager->flush();
  258.             return $this->redirectToRoute('business_contacts_index', [], Response::HTTP_SEE_OTHER);
  259.         }
  260.         return $this->renderForm('business_contacts/new.html.twig', [
  261.             'business_contact' => $businessContact,
  262.             'form' => $form,
  263.         ]);
  264.     }
  265.     /**
  266.      * @Route("/suggestion", name="business_contacts_suggestion", methods={"GET", "POST"})
  267.      */
  268.     public function suggestion(Request $requestBusinessContactsRepository $businessContactsRepository): Response
  269.     {
  270.         $businessContact = new BusinessContacts();
  271.         $form $this->createForm(BusinessContactsType::class, $businessContact);
  272.         $form->handleRequest($request);
  273.         if ($form->isSubmitted() && $form->isValid()) {
  274.             $businessContactsRepository->add($businessContacttrue);
  275.             $businessContact->setStatus('Pending');
  276.             return $this->redirectToRoute('business_contacts_index', [], Response::HTTP_SEE_OTHER);
  277.         }
  278.         return $this->renderForm('business_contacts/new.html.twig', [
  279.             'business_contact' => $businessContact,
  280.             'form' => $form,
  281.         ]);
  282.     }
  283.     /**
  284.      * @Route("/show/{id}", name="business_contacts_show", methods={"GET"})
  285.      */
  286.     public function show(BusinessContacts $businessContact): Response
  287.     {
  288.         $longitude $businessContact->getLocationLongitude();
  289.         $latitude $businessContact->getLocationLatitude();
  290.         return $this->render('business_contacts/show.html.twig', [
  291.             'business_contact' => $businessContact,
  292.             'longitude' => $longitude,
  293.             'latitude' => $latitude,
  294.         ]);
  295.     }
  296.     /**
  297.      * @Route("/edit/{id}", name="business_contacts_edit", methods={"GET", "POST"})
  298.      */
  299.     public function edit(Request $requestBusinessContacts $businessContactBusinessContactsRepository $businessContactsRepository): Response
  300.     {
  301.         $form $this->createForm(BusinessContactsType::class, $businessContact);
  302.         $form->handleRequest($request);
  303.         if ($form->isSubmitted() && $form->isValid()) {
  304.             $photo $form['photo']->getData();
  305.             if ($photo) {
  306.                 $files_name = [];
  307.                 $photo_directory $this->getParameter('business_contacts_photos_directory');
  308.                 $fileName pathinfo($photo->getClientOriginalName(), PATHINFO_FILENAME);
  309.                 $file_extension $photo->guessExtension();
  310.                 if ($businessContact->getFirstName() == '') {
  311.                     $newFileName $businessContact->getCompany() . "." $file_extension;
  312.                 } else {
  313.                     $newFileName $businessContact->getCompany() . "_" $businessContact->getFirstName() . "_" $businessContact->getLastName() . "." $file_extension;
  314.                 }
  315.                 $photo->move($photo_directory$newFileName);
  316.                 $businessContact->setPhoto($newFileName);
  317.             }
  318.             $file $form['files']->getData();
  319.             if ($file) {
  320.                 $file_name = [];
  321.                 $file_directory $this->getParameter('business_contacts_attachments_directory');
  322.                 $fileName pathinfo($file->getClientOriginalName(), PATHINFO_FILENAME);
  323.                 $file_extension $file->guessExtension();
  324.                 $newFileName $fileName "." $file_extension;
  325.                 $file->move($file_directory$newFileName);
  326.                 $businessContact->setFiles($newFileName);
  327.             }
  328.             $businessContactsRepository->add($businessContacttrue);
  329.             return $this->redirectToRoute('business_contacts_index', [], Response::HTTP_SEE_OTHER);
  330.         }
  331.         return $this->renderForm('business_contacts/edit.html.twig', [
  332.             'business_contact' => $businessContact,
  333.             'form' => $form,
  334.         ]);
  335.     }
  336.     /**
  337.      * @Route("/delete/{id}", name="business_contacts_delete", methods={"POST"})
  338.      */
  339.     public function delete(Request $requestBusinessContacts $businessContactBusinessContactsRepository $businessContactsRepositoryEntityManagerInterface $entityManager): Response
  340.     {
  341.         $referer $request->headers->get('referer');
  342.         $file_name $businessContact->getFiles();
  343.         if ($file_name) {
  344.             $file $this->getParameter('business_contacts_attachments_directory') . $file_name;
  345.             if (file_exists($file)) {
  346.                 unlink($file);
  347.             }
  348.             $businessContact->setFiles('');
  349.             $entityManager->flush();
  350.         }
  351.         $photo_file_name $businessContact->getPhoto();
  352.         if ($photo_file_name) {
  353.             $photo_file_name $this->getParameter('business_contacts_photos_directory') . $photo_file_name;
  354.             if (file_exists($photo_file_name)) {
  355.                 unlink($photo_file_name);
  356.             }
  357.             $businessContact->setPhoto('');
  358.             $entityManager->flush();
  359.         }
  360.         if ($this->isCsrfTokenValid('delete' $businessContact->getId(), $request->request->get('_token'))) {
  361.             $id $businessContact->getId();
  362.             $businessContactsRepository->remove($businessContacttrue);
  363.             // Check if referer is a show or edit page for this deleted entity, if so redirect to index
  364.             if ($referer) {
  365.                 $refererPath parse_url($refererPHP_URL_PATH);
  366.                 if (strpos($refererPath'/business_contacts/show/' $id) !== false ||
  367.                     strpos($refererPath'/business_contacts/edit/' $id) !== false) {
  368.                     return $this->redirectToRoute('business_contacts_index');
  369.                 }
  370.             }
  371.         }
  372.         return $this->redirect($referer ?: $this->generateUrl('business_contacts_index'));
  373.     }
  374.     /**
  375.      * @Route("/delete_GPS_location/{id}", name="business_contacts_delete_GPS_location", methods={"POST"})
  376.      */
  377.     public function deleteGPSLocation(Request $requestint $idBusinessContactsRepository $businessContactsRepositoryEntityManagerInterface $entityManager): Response
  378.     {
  379.         $referer $request->headers->get('referer');
  380.         $businessContact $businessContactsRepository->find($id);
  381.         if ($this->isCsrfTokenValid('delete_gps' $businessContact->getId(), $request->request->get('_token'))) {
  382.             $businessContact->setLocationLatitude(null);
  383.             $businessContact->setLocationLongitude(null);
  384.             $entityManager->flush();
  385.         }
  386.         return $this->redirect($referer ?: $this->generateUrl('business_contacts_index'));
  387.     }
  388.     /**
  389.      * @Route("/delete_all_GPS_locations", name="business_contacts_delete_all_GPS_locations", methods={"POST"})
  390.      */
  391.     public function deleteAllBusinessContactsGPSLocations(Request $requestBusinessContactsRepository $businessContactsRepositoryEntityManagerInterface $entityManager): Response
  392.     {
  393.         $referer $request->headers->get('referer');
  394.         if ($this->isCsrfTokenValid('delete_all_gps'$request->request->get('_token'))) {
  395.             $business_contacts $businessContactsRepository->findAll();
  396.             foreach ($business_contacts as $business_contact) {
  397.                 $business_contact->setLocationLongitude(null);
  398.                 $business_contact->setLocationLatitude(null);
  399.             }
  400.             $entityManager->flush();
  401.         }
  402.         return $this->redirect($referer ?: $this->generateUrl('business_contacts_index'));
  403.     }
  404.     /**
  405.      * @Route("/delete_all", name="business_contacts_delete_all", methods={"POST"})
  406.      */
  407.     public function deleteAllBusinessContacts(Request $requestBusinessContactsRepository $businessContactsRepositoryEntityManagerInterface $entityManager): Response
  408.     {
  409.         if ($this->isCsrfTokenValid('delete_all_business_contacts'$request->request->get('_token'))) {
  410.             $business_contacts $businessContactsRepository->findAll();
  411.             foreach ($business_contacts as $business_contact) {
  412.                 $entityManager->remove($business_contact);
  413.             }
  414.             $entityManager->flush();
  415.         }
  416.         return $this->redirectToRoute('business_contacts_index', [], Response::HTTP_SEE_OTHER);
  417.     }
  418.     /**
  419.      * @Route("/delete_photo_file/{id}", name="business_contact_delete_photo_file", methods={"POST"})
  420.      */
  421.     public function deleteBusinessContactPhotoFile(int $idRequest $requestBusinessContacts $businessContactEntityManagerInterface $entityManager)
  422.     {
  423.         $referer $request->headers->get('referer');
  424.         if ($this->isCsrfTokenValid('delete_photo' $businessContact->getId(), $request->request->get('_token'))) {
  425.             $photo_file_name $businessContact->getPhoto();
  426.             if ($photo_file_name) {
  427.                 $photo_file_name $this->getParameter('business_contacts_photos_directory') . "/" $photo_file_name;
  428.                 if (file_exists($photo_file_name)) {
  429.                     unlink($photo_file_name);
  430.                 }
  431.                 $businessContact->setPhoto('');
  432.                 $entityManager->flush();
  433.             }
  434.         }
  435.         return $this->redirect($referer);
  436.     }
  437.     /**
  438.      * @Route("/delete_attachment_file/{id}", name="business_contact_delete_attachment_file", methods={"POST"})
  439.      */
  440.     public function deleteBusinessContactAttachmentFile(int $idRequest $requestBusinessContacts $businessContactEntityManagerInterface $entityManager)
  441.     {
  442.         $referer $request->headers->get('referer');
  443.         if ($this->isCsrfTokenValid('delete_attachment' $businessContact->getId(), $request->request->get('_token'))) {
  444.             $file_name $businessContact->getFiles();
  445.             if ($file_name) {
  446.                 $file $this->getParameter('business_contacts_attachments_directory') . "/" $file_name;
  447.                 if (file_exists($file)) {
  448.                     unlink($file);
  449.                 }
  450.                 $businessContact->setFiles('');
  451.                 $entityManager->flush();
  452.             }
  453.         }
  454.         return $this->redirect($referer);
  455.     }
  456.     /**
  457.      * @Route ("/view/photo/{id}", name="view_business_contact_photo")
  458.      */
  459.     public function viewBusinessContactPhoto(Request $request$idBusinessContactsRepository $businessContactsRepository)
  460.     {
  461.         $business_contact $businessContactsRepository->find($id);
  462.         return $this->render('business_contacts/view_photo.html.twig', ['business_contact' => $business_contact]);
  463.     }
  464.     /**
  465.      * @Route ("/business_contacts_export", name="business_contacts_export" )
  466.      */
  467.     public function businessContactsExport(BusinessContactsRepository $businessContactsRepository)
  468.     {
  469.         $data = [];
  470.         $exported_date = new \DateTime('now');
  471.         $exported_date_formatted $exported_date->format('d-M-Y');
  472.         $fileName 'business_contacts_export_' $exported_date_formatted '.csv';
  473.         $count 0;
  474.         $business_contact_list $businessContactsRepository->findAll();
  475.         foreach ($business_contact_list as $business_contact) {
  476.             $data[] = [
  477.                 "BusinessContacts",
  478.                 $business_contact->getStatus(),
  479.                 $business_contact->getBusinessOrPerson(),
  480.                 $business_contact->getBusinessType()->getBusinessType(),
  481.                 $business_contact->getCompany(),
  482.                 $business_contact->getFirstName(),
  483.                 $business_contact->getLastName(),
  484.                 $business_contact->getWebsite(),
  485.                 $business_contact->getEmail(),
  486.                 $business_contact->getLandline(),
  487.                 $business_contact->getMobile(),
  488.                 $business_contact->getAddressStreet(),
  489.                 $business_contact->getAddressCity(),
  490.                 $business_contact->getAddressCounty(),
  491.                 $business_contact->getAddressPostCode(),
  492.                 $business_contact->getAddressCountry(),
  493.                 $business_contact->getLocationLongitude(),
  494.                 $business_contact->getLocationLatitude(),
  495.                 $business_contact->getNotes()
  496.             ];
  497.         }
  498.         $spreadsheet = new Spreadsheet();
  499.         $sheet $spreadsheet->getActiveSheet();
  500.         $sheet->setTitle('Business Contacts');
  501.         $sheet->getCell('A1')->setValue('Entity');
  502.         $sheet->getCell('B1')->setValue('Status');
  503.         $sheet->getCell('C1')->setValue('Business Or Person');
  504.         $sheet->getCell('D1')->setValue('Business Type');
  505.         $sheet->getCell('E1')->setValue('Company');
  506.         $sheet->getCell('F1')->setValue('First Name');
  507.         $sheet->getCell('G1')->setValue('Last Name');
  508.         $sheet->getCell('H1')->setValue('Web Page');
  509.         $sheet->getCell('I1')->setValue('E-mail');
  510.         $sheet->getCell('J1')->setValue('Business Phone');
  511.         $sheet->getCell('K1')->setValue('Mobile Phone');
  512.         $sheet->getCell('L1')->setValue('Business Street');
  513.         $sheet->getCell('M1')->setValue('Business City');
  514.         $sheet->getCell('N1')->setValue('Business County');
  515.         $sheet->getCell('O1')->setValue('Business Postal Code');
  516.         $sheet->getCell('P1')->setValue('Business Country/Region');
  517.         $sheet->getCell('Q1')->setValue('Location Longitude');
  518.         $sheet->getCell('R1')->setValue('Location Latitude');
  519.         $sheet->getCell('S1')->setValue('Notes');
  520.         $sheet->fromArray($datanull'A2'true);
  521.         $total_rows $sheet->getHighestRow();
  522.         for ($i 2$i <= $total_rows$i++) {
  523.             $cell "L" $i;
  524.             $sheet->getCell($cell)->getHyperlink()->setUrl("https://google.com");
  525.         }
  526.         $writer = new Csv($spreadsheet);
  527.         $response = new StreamedResponse(function () use ($writer) {
  528.             $writer->save('php://output');
  529.         });
  530.         $response->headers->set('Content-Type''application/vnd.ms-excel');
  531.         $response->headers->set('Content-Disposition'sprintf('attachment;filename="%s"'$fileName));
  532.         $response->headers->set('Cache-Control''max-age=0');
  533.         return $response;
  534.     }
  535.     /**
  536.      * Export all BusinessContacts as a ZIP of individual .vcf files.
  537.      *
  538.      * @Route("/business_contacts_export_vcf_zip", name="business_contacts_export_vcf_zip", methods={"GET"})
  539.      */
  540.     public function exportBusinessContactsAsZip(BusinessContactsRepository $businessContactsRepositoryBusinessContactVCFExport $businessContactVCFExport): Response
  541.     {
  542.         $contacts $businessContactsRepository->findAll();
  543.         if (!$contacts) {
  544.             return new Response('No contacts to export.'Response::HTTP_NO_CONTENT);
  545.         }
  546.         // Final file name: business_contacts_YYYYMMDD.zip
  547.         $datePart = (new \DateTimeImmutable('now'))->format('Ymd');
  548.         $baseName "business_contacts_{$datePart}";
  549.         $exportDir rtrim($businessContactVCFExport->getExportDirectory(), "/\\");
  550.         $zipPath $exportDir DIRECTORY_SEPARATOR $baseName '.zip';
  551.         // Ensure export dir is OK
  552.         if (!is_dir($exportDir) || !is_writable($exportDir)) {
  553.             $msg "Export directory is not writable: {$exportDir}";
  554.             $this->logger->error($msg);
  555.             return new Response($msgResponse::HTTP_INTERNAL_SERVER_ERROR);
  556.         }
  557.         // Make name unique if needed
  558.         if (file_exists($zipPath)) {
  559.             $i 1;
  560.             do {
  561.                 $zipPath $exportDir DIRECTORY_SEPARATOR $baseName '-' $i '.zip';
  562.                 $i++;
  563.             } while (file_exists($zipPath));
  564.         }
  565.         $downloadName basename($zipPath); // what the user downloads
  566.         // Create temp dir for individual VCFs
  567.         $tempDir sys_get_temp_dir() . DIRECTORY_SEPARATOR 'bc_vcards_' bin2hex(random_bytes(6));
  568.         if (!@mkdir($tempDir0777true) && !is_dir($tempDir)) {
  569.             $msg "Failed to create temporary directory: {$tempDir}";
  570.             $this->logger->error($msg);
  571.             return new Response($msgResponse::HTTP_INTERNAL_SERVER_ERROR);
  572.         }
  573.         try {
  574.             // Write each contact as its own .vcf using the service's BusinessContacts generator
  575.             foreach ($contacts as $c) {
  576.                 $first trim((string)($c->getFirstName() ?? ''));
  577.                 $last trim((string)($c->getLastName() ?? ''));
  578.                 $comp trim((string)($c->getCompany() ?? ''));
  579.                 $label $comp !== '' $comp trim($first ' ' $last);
  580.                 if ($label === '') {
  581.                     $label 'contact';
  582.                 }
  583.                 $safe preg_replace('/[^a-zA-Z0-9._-]/''_'$label);
  584.                 $vcfPath $tempDir DIRECTORY_SEPARATOR $safe '.vcf';
  585.                 // This writes the file to $vcfPath (and returns its contents)
  586.                 $businessContactVCFExport->generateVCard($c$vcfPath);
  587.             }
  588.             // Zip them up
  589.             $zip = new \ZipArchive();
  590.             if ($zip->open($zipPath\ZipArchive::CREATE \ZipArchive::OVERWRITE) !== true) {
  591.                 throw new \RuntimeException("Cannot create ZIP archive at: {$zipPath}");
  592.             }
  593.             foreach (glob($tempDir DIRECTORY_SEPARATOR '*.vcf') as $file) {
  594.                 $zip->addFile($filebasename($file));
  595.             }
  596.             $zip->close();
  597.         } catch (\Throwable $e) {
  598.             $this->logger->error('Failed to generate ZIP of vCards: ' $e->getMessage(), ['exception' => $e]);
  599.             // Cleanup temp dir on failure
  600.             foreach (glob($tempDir DIRECTORY_SEPARATOR '*') ?: [] as $f) {
  601.                 @unlink($f);
  602.             }
  603.             @rmdir($tempDir);
  604.             return new Response('Failed to generate ZIP: ' $e->getMessage(), Response::HTTP_INTERNAL_SERVER_ERROR);
  605.         }
  606.         // Cleanup temp dir (ZIP stays until sent)
  607.         foreach (glob($tempDir DIRECTORY_SEPARATOR '*') ?: [] as $f) {
  608.             @unlink($f);
  609.         }
  610.         @rmdir($tempDir);
  611.         /** @var BinaryFileResponse $response */
  612.         $response $this->file($zipPath$downloadNameResponseHeaderBag::DISPOSITION_ATTACHMENT);
  613.         // Delete the ZIP after it's been sent to the client
  614.         $response->deleteFileAfterSend(true);
  615.         return $response;
  616.     }
  617.     /**
  618.      * @Route ("/business_contacts_import", name="business_contacts_import" )
  619.      */
  620.     public function businessContactsImport(Request $requestSluggerInterface $sluggerBusinessContactsRepository $businessContactsRepositoryImportBusinessContactsService $businessContactsImportService): Response
  621.     {
  622.         $form $this->createForm(ImportType::class);
  623.         $form->handleRequest($request);
  624.         if ($form->isSubmitted() && $form->isValid()) {
  625.             $importFile $form->get('File')->getData();
  626.             if ($importFile) {
  627.                 $originalFilename pathinfo($importFile->getClientOriginalName(), PATHINFO_FILENAME);
  628.                 $safeFilename $slugger->slug($originalFilename);
  629.                 $newFilename $safeFilename '.' 'csv';
  630.                 try {
  631.                     $importFile->move(
  632.                         $this->getParameter('business_contacts_import_directory'),
  633.                         $newFilename
  634.                     );
  635.                 } catch (FileException $e) {
  636.                     die('Import failed');
  637.                 }
  638.                 $businessContactsImportService->importBusinessContacts($newFilename);
  639.                 return $this->redirectToRoute('business_contacts_index');
  640.             }
  641.         }
  642.         return $this->render('home/import.html.twig', [
  643.             'form' => $form->createView(),
  644.             'heading' => 'Business Contacts Import',
  645.         ]);
  646.     }
  647.     /**
  648.      * @Route("/update/location", name="update_business_contact_location", methods={"POST"})
  649.      */
  650.     public function updateLocation(BusinessContactsRepository $businessContactsRepositoryEntityManagerInterface $manager): Response
  651.     {
  652.         $id $_POST['id'];
  653.         $latitude $_POST['latitude'];
  654.         $longitude $_POST['longitude'];
  655.         $gps $latitude ',' $longitude;
  656.         $business_contact $businessContactsRepository->find($id);
  657.         $business_contact->setLocationLongitude($longitude)
  658.             ->setLocationLatitude($latitude);
  659.         $manager->flush();
  660.         return new Response(null);
  661.     }
  662.     /**
  663.      * @Route("/gps_location_clear/{id}", name="business_contact_clear_gps_location")
  664.      */
  665.     public
  666.     function clearGPSLocation(Request $requestBusinessContacts $businessContactsEntityManagerInterface $entityManager)
  667.     {
  668.         $referer $request->headers->get('referer');
  669.         $businessContacts->setLocationLongitude(null);
  670.         $businessContacts->setLocationLatitude(null);
  671.         $entityManager->flush();
  672.         return $this->redirect($referer);
  673.     }
  674.     /**
  675.      * @Route("/show_attachment/{id}", name="business_contact_show_attachment")
  676.      */
  677.     public function showAttachmentBusinessContact(int $idBusinessContactsRepository $businessContactsRepository)
  678.     {
  679.         $business_contact $businessContactsRepository->find($id);
  680.         $filename $business_contact->getFiles();
  681.         $filepath $this->getParameter('business_contacts_attachments_directory') . "/" $filename;
  682.         if (file_exists($filepath)) {
  683.             $response = new BinaryFileResponse($filepath);
  684.             $response->setContentDisposition(
  685.                 ResponseHeaderBag::DISPOSITION_INLINE//use ResponseHeaderBag::DISPOSITION_ATTACHMENT to save as an attachment
  686.                 $filename
  687.             );
  688.             return $response;
  689.         } else {
  690.             return new Response("file does not exist");
  691.         }
  692.     }
  693. //    /**
  694. //     * @Route("/create/Vcard/{id}", name="create_vcard")
  695. //     */
  696. //    public function createVcard(int $id, BusinessContactsRepository $businessContactsRepository)
  697. //    {
  698. //        $business_contact = $businessContactsRepository->find($id);
  699. //        $vcard = new VCard();
  700. //        $businessOrPerson = $business_contact->getBusinessOrPerson();
  701. //        $business_type = $business_contact->getBusinessType()->getBusinessType();
  702. //        $firstName = $business_contact->getFirstName();
  703. //        $lastName = $business_contact->getLastName();
  704. //        $mobile = $business_contact->getMobile();
  705. //        $landline = $business_contact->getLandline();
  706. //        $company = $business_contact->getCompany();
  707. //        $website = $business_contact->getWebsite();
  708. //        $addressStreet = $business_contact->getAddressStreet();
  709. //        $addressCity = $business_contact->getAddressCity();
  710. //        $addressPostCode = $business_contact->getAddressPostcode();
  711. //        $addressCountry = $business_contact->getAddressCountry();
  712. //        $longitude = $business_contact->getLocationLongitude();
  713. //        $latitude = $business_contact->getLocationLatitude();
  714. //        $notes = $business_contact->getNotes();
  715. //
  716. //        if ($businessOrPerson = "Business") {
  717. //            $firstNameCard = $company;
  718. //            $lastNameCard = $business_type;
  719. //            $companyCard = [];
  720. //        }
  721. //        if ($businessOrPerson = "Person") {
  722. //            $firstNameCard = $firstName;
  723. //            $lastNameCard = $lastName;
  724. //            $companyCard = $company;
  725. //        }
  726. //        $vcard->addName($lastNameCard, $firstNameCard);
  727. //        $vcard->addEmail($business_contact->getEmail())
  728. //            ->addPhoneNumber($landline, 'work')
  729. //            ->addPhoneNumber($mobile, 'mobile')
  730. //            ->addCompany($companyCard)
  731. //            ->addURL($website)
  732. //            ->addNote($notes)
  733. //            ->addAddress($name = '', $extended = '', $street = $addressStreet, $city = $addressCity, $region = '', $zip = $addressPostCode, $country = $addressCountry, $type = 'WORK;POSTAL');
  734. //        $vcard->download();
  735. //        return new Response(null);
  736. //    }
  737.     /**
  738.      * Export a single BusinessContact as vCard with embedded photo.
  739.      * @Route("/create_vcard_business_contact/{id}", name="create_vcard_business_contact", methods={"GET"})
  740.      */
  741.     public function createVCFBusinessContact(Request $requestSecurity $securityBusinessContacts $businessContactsBusinessContactVCFExport $exporter): Response
  742.     {
  743.         // Decide filename based on Business vs Person
  744.         $businessOrPerson = (string)($businessContacts->getBusinessOrPerson() ?? 'Person');
  745.         $businessType method_exists($businessContacts'getBusinessType') && $businessContacts->getBusinessType()
  746.             ? (string)$businessContacts->getBusinessType()->getBusinessType()
  747.             : '';
  748.         if (strcasecmp($businessOrPerson'Business') === 0) {
  749.             $baseA = (string)($businessContacts->getCompany() ?? 'Company');
  750.             $baseB $businessType !== '' $businessType 'Business';
  751.         } else {
  752.             $baseA = (string)($businessContacts->getFirstName() ?? 'Unknown');
  753.             $baseB = (string)($businessContacts->getLastName() ?? 'Contact');
  754.         }
  755.         // Sanitize but keep the comma between parts
  756.         $safe = fn(string $s) => preg_replace('/[^\w\-\.\s]/u'''$s); // allow letters, numbers, _, -, ., space
  757.         $fileName sprintf('%s,%s.vcf'$safe($baseA), $safe($baseB));
  758.         $exportDir $exporter->getExportDirectory();
  759.         if (!is_dir($exportDir) || !is_writable($exportDir)) {
  760.             return new Response("Export directory is not writable: $exportDir"Response::HTTP_INTERNAL_SERVER_ERROR);
  761.         }
  762.         $filePath rtrim($exportDir'/\\') . DIRECTORY_SEPARATOR $fileName;
  763.         try {
  764.             $exporter->generateVCard($businessContacts$filePath);
  765.         } catch (\Throwable $e) {
  766.             return new Response('Failed to generate vCard: ' $e->getMessage(), Response::HTTP_INTERNAL_SERVER_ERROR);
  767.         }
  768.         if (!is_file($filePath) || filesize($filePath) === 0) {
  769.             return new Response('vCard file was not created or is empty.'Response::HTTP_INTERNAL_SERVER_ERROR);
  770.         }
  771.         // Force download with your exact requested filename
  772.         return $this->file($filePath$fileNameResponseHeaderBag::DISPOSITION_ATTACHMENT);
  773.     }
  774. }