src/Controller/CourseController.php line 1147

Open in your IDE?
  1. <?php
  2. namespace App\Controller;
  3. use App\Entity\Course;
  4. use App\Entity\CourseData;
  5. use App\Entity\CourseOccurrence;
  6. use App\Entity\Invoice;
  7. use App\Entity\InvoicePayment;
  8. use App\Entity\Order;
  9. use App\Entity\OrderItem;
  10. use App\Entity\OrderItemPerson;
  11. use App\Entity\Presence;
  12. use App\Entity\WaitItem;
  13. use App\Entity\CartItem;
  14. use App\Entity\InvoiceItem;
  15. use App\Entity\CourseSubscriptionBooking;
  16. use App\Form\CourseImagesType;
  17. use App\Form\CourseType;
  18. use App\Repository\CartItemRepository;
  19. use App\Repository\CourseDataRepository;
  20. use App\Repository\CourseFieldRepository;
  21. use App\Repository\CourseOccurrenceRepository;
  22. use App\Repository\CourseOccurrenceTimeRepository;
  23. use App\Repository\CourseRepository;
  24. use App\Repository\InvoiceItemRepository;
  25. use App\Repository\OrderItemPersonRepository;
  26. use App\Repository\OrderItemRepository;
  27. use App\Repository\OrderRepository;
  28. use App\Repository\PersonRepository;
  29. use App\Repository\PresenceReasonRepository;
  30. use App\Repository\PresenceRepository;
  31. use App\Repository\SpeakerRepository;
  32. use App\Repository\TagsPersonRepository;
  33. use App\Repository\TextblocksRepository;
  34. use App\Repository\WaitItemRepository;
  35. use App\Service\CertificatePdfBundleService;
  36. use App\Service\CertificateService;
  37. use App\Service\ConfigurationService;
  38. use App\Service\EmailHistoryService;
  39. use App\Service\Exception\ServiceException;
  40. use App\Service\InvoiceService;
  41. use App\Service\MailerService;
  42. use App\Service\OrderService;
  43. use App\Service\PdfService;
  44. use App\Service\SepaXmlService;
  45. use App\Service\ZoomService;
  46. use Doctrine\Common\Collections\ArrayCollection;
  47. use Doctrine\ORM\EntityManagerInterface;
  48. use Doctrine\Persistence\ManagerRegistry;
  49. use App\User\Controller\AbstractClientableController;
  50. use App\User\Repository\UserRepository;
  51. use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted;
  52. use Symfony\Component\Filesystem\Filesystem;
  53. use Symfony\Component\HttpFoundation\JsonResponse;
  54. use Symfony\Component\HttpFoundation\Request;
  55. use Symfony\Component\HttpFoundation\Response;
  56. use Symfony\Component\Routing\Annotation\Route;
  57. /**
  58.  * @Route("/course")
  59.  *
  60.  * @IsGranted("ROLE_SPEAKER")
  61.  */
  62. class CourseController extends AbstractClientableController
  63. {
  64.     private $managerRegistry;
  65.     private $certificateService;
  66.     public function __construct(ManagerRegistry $managerRegistryCertificateService $certificateService)
  67.     {
  68.         $this->managerRegistry $managerRegistry->getManager();
  69.         $this->certificateService $certificateService;
  70.     }
  71.     public const LISTING_LIMIT 25;
  72.     private function getListingLimit(): int
  73.     {
  74.         return !empty($_ENV['COURSES_LISTINGLIMIT']) ? (int) $_ENV['COURSES_LISTINGLIMIT'] : 4;
  75.     }
  76.     /**
  77.      * @Route("/", name="course_index", methods="GET")
  78.      */
  79.     public function index(
  80.         Request $request,
  81.         \App\Service\UiService $uiService,
  82.         CourseRepository $courseRepository,
  83.         PersonRepository $personRepository,
  84.         EntityManagerInterface $manager,
  85.         SpeakerRepository $speakerRepository,
  86.         UserRepository $userRepository,
  87.     ): Response {
  88.         //  $this->denyAccessUnlessGranted('ROLE_MANAGER');
  89.         $order $uiService->getSortOrder('course-index-listing');
  90.         $archive = !empty($request->get('archive'));
  91.         // /////////////////////////////////////////////////////////
  92.         $user $this->getCurrentUser();
  93.         $person $personRepository->getByUser($user);
  94.         $speaker $speakerRepository->getByUser($user);
  95.         /*
  96. if ($speaker == null) {
  97.     $this->addFlash('error', 'Sie sind kein Speaker');
  98.     return $this->redirectToRoute('dashboard');
  99. }else{
  100. $this->addFlash('notice', 'Speaker ID: ' . $speaker->getId().' Person ID'.$user->getId());
  101. }*/
  102.         $courses $courseRepository->getCoursesByClientPaged(
  103.             $this->getCurrentClient(),
  104.             $this->getListingLimit(),
  105.             $order['orderDirection'] ?? 'ASC',
  106.             $order['orderBy'] ?? 'title',
  107.             1,
  108.             $archive,
  109.             $speaker,
  110.         );
  111.         // die Anzahl der Kurse mit den gebuchten überprüfen und die bookedSlots bei den CourseOccurreces aktualisieren
  112.         $this->manager $manager;
  113.         foreach ($courses as $course) {
  114.             foreach ($course->getOccurrences() as $occurrence) {
  115.                 $occurrence->setBookedSlots($occurrence->getBookedSlots());
  116.                 $this->manager->persist($occurrence);
  117.             }
  118.         }
  119.         $this->manager->flush();
  120.         if (null == $speaker) {
  121.             $render 'course/index.html.twig';
  122.         } else {
  123.             $render 'course/index_speaker.html.twig';
  124.         }
  125.         // /////////////////////////////////////////
  126.         return $this->render($render, [
  127.             'uiService' => $uiService,
  128.             'courses' => $courses,
  129.             'total' => $courses->count(),
  130.             'pages' => ceil($courses->count() / $this->getListingLimit()),
  131.             'page' => 1,
  132.             'archive' => $archive,
  133.             'env' => $_ENV,
  134.             'user' => $user,
  135.             'person' => $person,
  136.         ]);
  137.     }
  138.     /**
  139.      * @Route("/{page}/{orderby}/{order}", name="course_index_listing", methods="GET", requirements={"page"="\d+","order"="asc|desc"})
  140.      */
  141.     public function indexListing(
  142.         Request $request,
  143.         CourseRepository $courseRepository,
  144.         \App\Service\UiService $uiService,
  145.         $page,
  146.         $orderby,
  147.         $order): Response
  148.     {
  149.         $uiService->storeSortOrder('course-index-listing'$orderby$order);
  150.         $archive = !empty($request->get('archive'));
  151.         $courses $courseRepository->getCoursesByClientPaged($this->getCurrentClient(), $this->getListingLimit(), $order$orderby$page$archive);
  152.         return $this->render('course/_index_listing.html.twig', [
  153.             'courses' => $courses,
  154.             'total' => $courses->count(),
  155.             'pages' => ceil($courses->count() / $this->getListingLimit()),
  156.             'page' => $page,
  157.             'archive' => $archive,
  158.             'env' => $_ENV,
  159.         ]);
  160.     }
  161.     /**
  162.      * @Route("/new", name="course_new", methods="GET|POST")
  163.      */
  164.     public function new(
  165.         Request $request,
  166.         ConfigurationService $configService,
  167.         PersonRepository $personRepository): Response
  168.     {
  169.         $course = new Course();
  170.         if (!empty($courseNature $request->get('courseNature'))) {
  171.             $course->setCourseNature($courseNature);
  172.         }
  173.         $user $this->getCurrentUser();
  174.         $person $personRepository->getByUser($user);
  175.         $form $this->createForm(CourseType::class, $course, [
  176.             'client' => $this->getCurrentClient(),
  177.             'taxes' => $configService->getTaxConfigbyClient($this->getCurrentClient()),
  178.         ]);
  179.         $form->handleRequest($request);
  180.         if ($form->isSubmitted() && $form->isValid()) {
  181.             $course->setClient($this->getCurrentClient());
  182.             $course->setNumber($configService->getNewCourseNumberByClient($this->getCurrentClient()));
  183.             foreach ($course->getTexts() as $key => $text) {
  184.                 $text->setCreated(new \DateTime());
  185.                 if (empty($text->getOrderId())) {
  186.                     $text->setOrderId($key 1000);
  187.                 }
  188.             }
  189.             $em $this->getDoctrine()->getManager();
  190.             $course->setCreated(new \DateTime());
  191.             $em->persist($course);
  192.             $em->flush();
  193.             $this->addFlash('success''Kurs angelegt');
  194.             return $this->redirectToRoute('course-occurrence_new', ['courseId' => $course->getId()]);
  195.         }
  196.         return $this->render('course/new.html.twig', [
  197.             'course' => $course,
  198.             'fields' => null,
  199.             'form' => $form->createView(),
  200.             'user' => $user,
  201.         ]);
  202.     }
  203.     /**
  204.      * @Route("/{id}/edit", name="course_edit", methods="GET|POST", requirements={"id"="\d+"})
  205.      */
  206.     public function edit(
  207.         Request $request,
  208.         Course $course,
  209.         ConfigurationService $configService,
  210.         CourseFieldRepository $courseFieldRepository,
  211.         CourseDataRepository $courseDataRepository,
  212.         PersonRepository $personRepository): Response
  213.     {
  214.         // Security: Check client ownership via CourseSecurityVoter
  215.         $this->denyAccessUnlessGranted('view'$course);
  216.         $user $this->getCurrentUser();
  217.         $person $personRepository->getByUser($user);
  218.         $courseTexts = new ArrayCollection();
  219.         foreach ($course->getTexts() as $text) {
  220.             $courseTexts->add($text);
  221.         }
  222.         $form $this->createForm(CourseType::class, $course, [
  223.             'client' => $this->getCurrentClient(),
  224.             'taxes' => $configService->getTaxConfigbyClient($this->getCurrentClient()),
  225.         ]);
  226.         $form->handleRequest($request);
  227.         if ($form->isSubmitted() && $form->isValid()) {
  228.             $manager $this->getDoctrine()->getManager();
  229.             foreach ($courseTexts as $text) {
  230.                 if (false === $course->getTexts()->contains($text)) {
  231.                     $text->setCourse(null);
  232.                     $manager->remove($text);
  233.                 }
  234.             }
  235.             foreach ($course->getTexts() as $key => $text) {
  236.                 if (empty($text->getOrderId())) {
  237.                     $text->setCreated(new \DateTime());
  238.                     $text->setOrderId($key 1000);
  239.                 }
  240.                 $text->setModified(new \DateTime());
  241.             }
  242.             $fields $request->request->get('fields');
  243.             if (!is_null($fields)) {
  244.                 foreach ($fields as $fieldId => $value) {
  245.                     $field $courseFieldRepository->find($fieldId);
  246.                     $data $courseDataRepository->findBy([
  247.                         'course' => $course,
  248.                         'field' => $field,
  249.                     ]);
  250.                     if (== count($data)) {
  251.                         $data = new CourseData();
  252.                         $data->setClient($this->getCurrentClient());
  253.                         $data->setCourse($course);
  254.                         $data->setField($field);
  255.                         $data->setCreated(new \DateTime());
  256.                         $manager->persist($data);
  257.                     } else {
  258.                         $data $data[0];
  259.                     }
  260.                     $data->setValueText($value);
  261.                     $data->setModified(new \DateTime());
  262.                 }
  263.             } else {
  264.                 $fields = [];
  265.             }
  266.             $course->setModified(new \DateTime());
  267.             $manager->flush();
  268.             $this->addFlash('notice''Kurs gespeichert');
  269.             return $this->redirectToRoute('course_edit', ['id' => $course->getId()]);
  270.         }
  271.         // Fetch course fields
  272.         $sql 'SELECT
  273.             f.*,
  274.             d.value_text,
  275.             d.value_integer,
  276.             d.value_date
  277.         FROM
  278.             course_field f
  279.         LEFT JOIN
  280.             course_data d
  281.         ON 
  282.             d.field_id = f.id AND
  283.             d.course_id = '.$course->getId();
  284.         $em $this->getDoctrine()->getManager();
  285.         $stmt $em->getConnection()->prepare($sql);
  286.         $stmt->execute();
  287.         $result $stmt->fetchAll();
  288.         $fields = [];
  289.         $isset false;
  290.         foreach ($result as $field) {
  291.             $isset false;
  292.             if (!empty($field['category'])) {
  293.                 if (!$course->getCategory()) {
  294.                     continue;
  295.                 }
  296.                 if (!in_array($course->getCategory()->getId(), json_decode($field['category'], true))) {
  297.                     continue;
  298.                 } else {
  299.                     $field $this->createDescription($field'course');
  300.                     $isset true;
  301.                 }
  302.             }
  303.             if (!empty($field['course_type'])) {
  304.                 if (!$course->getType()) {
  305.                     continue;
  306.                 }
  307.                 if (!in_array($course->getType()->getId(), json_decode($field['course_type'], true))) {
  308.                     continue;
  309.                 } else {
  310.                     if (!$isset) {
  311.                         $field $this->createDescription($field'course');
  312.                         $isset true;
  313.                     }
  314.                 }
  315.             }
  316.             if (empty($field['category']) && empty($field['course_type']) && !empty($field['certificate'])) {
  317.                 if (!$isset) {
  318.                     $field $this->createDescription($field'certificate');
  319.                 }
  320.             }
  321.             if (
  322.                 !empty($field['category'])
  323.                 || !empty($field['course_type'])
  324.                 || $field['certificate']
  325.             ) {
  326.                 $fields[] = $field;
  327.             }
  328.         }
  329.         return $this->render('course/edit.html.twig', [
  330.             'course' => $course,
  331.             'form' => $form->createView(),
  332.             'fields' => $fields,
  333.             'env' => $_ENV,
  334.             'user' => $user,
  335.         ]);
  336.     }
  337.     /**
  338.      * @Route("/{id}", name="course_delete", methods="DELETE", requirements={"id"="\d+"})
  339.      */
  340.     public function delete(Request $requestCourse $course): Response
  341.     {
  342.         // Security: Check client ownership via CourseSecurityVoter
  343.         $this->denyAccessUnlessGranted('view'$course);
  344.         if ($this->isCsrfTokenValid('delete'.$course->getId(), $request->request->get('_token'))) {
  345.             $em $this->getDoctrine()->getManager();
  346.             
  347.             // Prüfe Abhängigkeiten vor dem Löschen
  348.             $dependencies $this->checkCourseDependencies($course);
  349.             
  350.             if (!empty($dependencies)) {
  351.                 $this->addFlash('error''Der Kurs kann nicht gelöscht werden, weil folgende Abhängigkeiten existieren:');
  352.                 foreach ($dependencies as $dependency) {
  353.                     $this->addFlash('error'$dependency);
  354.                 }
  355.                 return $this->redirectToRoute('course_index');
  356.             }
  357.             
  358.             $em->remove($course);
  359.             try {
  360.                 $em->flush();
  361.             } catch (\Exception $e) {
  362.                 $errorMessage $e->getMessage();
  363.                 if (str_contains($errorMessage'Integrity constraint violation')) {
  364.                     $this->addFlash('error''Der Kurs kann nicht gelöscht werden, weil er an anderer Stelle gebraucht wird.');
  365.                 } else {
  366.                     $this->addFlash('error''Beim Löschen ist ein Fehler aufgetreten.');
  367.                 }
  368.                 return $this->redirectToRoute('course_index');
  369.             }
  370.             $this->addFlash('notice''Kurs gelöscht');
  371.         }
  372.         return $this->redirectToRoute('course_index');
  373.     }
  374.     
  375.     /**
  376.      * Prüft alle Abhängigkeiten eines Kurses
  377.      */
  378.     private function checkCourseDependencies(Course $course): array
  379.     {
  380.         $dependencies = [];
  381.         $em $this->getDoctrine()->getManager();
  382.         
  383.         // 1. Prüfe Kurstermine mit Buchungen
  384.         $occurrences $course->getOccurrences();
  385.         $occurrencesWithBookings 0;
  386.         $totalBookings 0;
  387.         
  388.         foreach ($occurrences as $occurrence) {
  389.             $orderItems $em->getRepository(OrderItem::class)->findBy(['courseOccurrence' => $occurrence]);
  390.             if (count($orderItems) > 0) {
  391.                 $occurrencesWithBookings++;
  392.                 $totalBookings += count($orderItems);
  393.             }
  394.         }
  395.         
  396.         if ($totalBookings 0) {
  397.             $dependencies[] = sprintf(
  398.                 '• %d Buchung(en) in %d Kurstermin(en)',
  399.                 $totalBookings,
  400.                 $occurrencesWithBookings
  401.             );
  402.         }
  403.         
  404.         // 2. Prüfe Warteliste
  405.         $totalWaitItems 0;
  406.         foreach ($occurrences as $occurrence) {
  407.             $waitItems $em->getRepository(WaitItem::class)->findBy(['courseOccurrence' => $occurrence]);
  408.             $totalWaitItems += count($waitItems);
  409.         }
  410.         
  411.         if ($totalWaitItems 0) {
  412.             $dependencies[] = sprintf('• %d Reservierung(en) auf der Warteliste'$totalWaitItems);
  413.         }
  414.         
  415.         // 3. Prüfe Rechnungen
  416.         $totalInvoiceItems 0;
  417.         foreach ($occurrences as $occurrence) {
  418.             $orderItems $em->getRepository(OrderItem::class)->findBy(['courseOccurrence' => $occurrence]);
  419.             foreach ($orderItems as $orderItem) {
  420.                 $invoiceItems $em->getRepository(InvoiceItem::class)->findBy(['orderItem' => $orderItem]);
  421.                 $totalInvoiceItems += count($invoiceItems);
  422.             }
  423.         }
  424.         
  425.         if ($totalInvoiceItems 0) {
  426.             $dependencies[] = sprintf('• %d Rechnungsposition(en)'$totalInvoiceItems);
  427.         }
  428.         
  429.         // 4. Prüfe Warenkorbeinträge
  430.         $totalCartItems 0;
  431.         foreach ($occurrences as $occurrence) {
  432.             $cartItems $em->getRepository(CartItem::class)->findBy(['courseOccurrence' => $occurrence]);
  433.             $totalCartItems += count($cartItems);
  434.         }
  435.         
  436.         if ($totalCartItems 0) {
  437.             $dependencies[] = sprintf('• %d Warenkorb-Eintrag/Einträge'$totalCartItems);
  438.         }
  439.         
  440.         // 5. Prüfe Kursabo-Buchungen
  441.         $subscriptionBookings $em->getRepository(CourseSubscriptionBooking::class)->findBy(['course' => $course]);
  442.         if (count($subscriptionBookings) > 0) {
  443.             $dependencies[] = sprintf('• %d Kursabo-Buchung(en)'count($subscriptionBookings));
  444.         }
  445.         
  446.         return $dependencies;
  447.     }
  448.     /**
  449.      * @Route("/{id}/copy", name="course_copy", methods={"POST","GET"}, requirements={"id"="\d+"})
  450.      */
  451.     public function copy(
  452.         Request $request,
  453.         Course $course,
  454.         ConfigurationService $configService,
  455.         CourseDataRepository $courseDataRepository,
  456.     ): Response {
  457.         $em $this->getDoctrine()->getManager();
  458.         $client $this->getCurrentClient();
  459.         $withOccurrences = (bool) $request->query->get('withOccurrences'false);
  460.         // --- NEUEN KURS ANLEGEN & Basisfelder kopieren ---
  461.         $new = new Course();
  462.         $new->setClient($client);
  463.         $new->setNumber($configService->getNewCourseNumberByClient($client));
  464.         $new->setCourseNature($course->getCourseNature());
  465.         // Primitive/Relationen 1:1 übernehmen
  466.         $new->setTitle($course->getTitle().' (Kopie)');
  467.         $new->setSubtitle($course->getSubtitle());
  468.         $new->setDescription($course->getDescription());
  469.         $new->setPrice($course->getPrice() ?? 0.0);
  470.         $new->setTaxRate($course->getTaxRate() ?? 0.0);
  471.         $new->setCategory($course->getCategory());
  472.         $new->setSeries($course->getSeries());
  473.         $new->setType($course->getType());
  474.         $new->setSubscription($course->getSubscription());
  475.         $new->setMaterialCost($course->getMaterialCost());
  476.         $new->setTargetAgeMin($course->getTargetAgeMin());
  477.         $new->setTargetAgeMax($course->getTargetAgeMax());
  478.         $new->setInvoiceUpperComment($course->getInvoiceUpperComment());
  479.         $new->setInvoiceLowerComment($course->getInvoiceLowerComment());
  480.         $new->setInvoiceLowerCommentDebit($course->getInvoiceLowerCommentDebit());
  481.         // --- TEXTE kopieren ---
  482.         foreach ($course->getTexts() as $idx => $oldText) {
  483.             $text = new \App\Entity\CourseText();
  484.             // Felder vorsichtig kopieren – passe an deine CourseText-Entity an:
  485.             $text->setCourse($new);
  486.             $text->setCreated(new \DateTime());
  487.             // Häufige Felder (falls vorhanden):
  488.             if (method_exists($oldText'getTitle') && method_exists($text'setTitle')) {
  489.                 $text->setTitle($oldText->getTitle());
  490.             }
  491.             if (method_exists($oldText'getContent') && method_exists($text'setContent')) {
  492.                 $text->setContent($oldText->getContent());
  493.             }
  494.             // Reihenfolge stabil halten
  495.             $order method_exists($oldText'getOrderId') ? $oldText->getOrderId() : ($idx 1000);
  496.             if (method_exists($text'setOrderId')) {
  497.                 $text->setOrderId($order);
  498.             }
  499.             $text->setCreated(new \DateTime());
  500.             $text->setModified(new \DateTime());
  501.             $new->addText($text);
  502.         }
  503.         // --- BILDER kopieren ---
  504.         $fs = new Filesystem();
  505.         $publicDir rtrim($this->getParameter('kernel.project_dir'), '/').'/public';
  506.         $dirRel 'images/kurse';                         // fixer Web-Pfad relativ zu /public
  507.         $dirAbs $publicDir.'/'.$dirRel;             // absoluter Ziel/Quell-Pfad
  508.         $fs->mkdir($dirAbs);                                  // sicherstellen, dass es existiert
  509.         foreach ($course->getImages() as $idx => $oldImage) {
  510.             $img = new \App\Entity\CourseImage();
  511.             $img->setCourse($new);
  512.             // --- Meta übernehmen ---
  513.             if (method_exists($oldImage'getTitle') && method_exists($img'setTitle')) {
  514.                 $img->setTitle($oldImage->getTitle());
  515.             }
  516.             if (method_exists($oldImage'getAuthor') && method_exists($img'setAuthor')) {
  517.                 $img->setAuthor($oldImage->getAuthor());
  518.             }
  519.             if (method_exists($oldImage'getDescription') && method_exists($img'setDescription')) {
  520.                 $img->setDescription($oldImage->getDescription());
  521.             }
  522.             if (method_exists($oldImage'getOrderId') && method_exists($img'setOrderId')) {
  523.                 $img->setOrderId($oldImage->getOrderId() ?? ($idx 1000));
  524.             } elseif (method_exists($img'setOrderId')) {
  525.                 $img->setOrderId($idx 1000);
  526.             }
  527.             if (method_exists($img'setCreated')) {
  528.                 $img->setCreated(new \DateTime());
  529.             }
  530.             if (method_exists($img'setModified')) {
  531.                 $img->setModified(new \DateTime());
  532.             }
  533.             // --- Quelldateiname ermitteln (in deiner DB steht nur der Name) ---
  534.             $srcName null;
  535.             if (method_exists($oldImage'getImage')) {
  536.                 $srcName $oldImage->getImage();   // z. B. "bild.jpg"
  537.             } elseif (method_exists($oldImage'getPath')) {
  538.                 // Falls früher mal ein Pfad gespeichert wurde, auf Dateinamen reduzieren
  539.                 $srcName basename((string) $oldImage->getPath());
  540.             }
  541.             if ($srcName) {
  542.                 // Normalize (Backslashes etc. entfernen, nur Name behalten)
  543.                 $srcName basename(str_replace('\\''/'trim($srcName)));
  544.                 // Primäre Quelle: /public/Images/Kurse/<Datei>
  545.                 $srcAbs $dirAbs.'/'.$srcName;
  546.                 // Optionaler Fallback: falls Altbestand unter kleinem Pfad lag
  547.                 if (!$fs->exists($srcAbs)) {
  548.                     $lowerAbs $publicDir.'/images/kurse/'.$srcName;
  549.                     if ($fs->exists($lowerAbs)) {
  550.                         $srcAbs $lowerAbs;
  551.                     }
  552.                 }
  553.                 if ($fs->exists($srcAbs)) {
  554.                     $pi pathinfo($srcAbs);
  555.                     $ext = isset($pi['extension']) && '' !== $pi['extension'] ? '.'.$pi['extension'] : '';
  556.                     $newFilename = ($pi['filename'] ?? 'image').'-copy-'.bin2hex(random_bytes(4)).$ext;
  557.                     $dstAbs $dirAbs.'/'.$newFilename;
  558.                     // Datei physisch duplizieren
  559.                     $fs->copy($srcAbs$dstAbstrue);
  560.                     // In die Entität NUR den Dateinamen schreiben
  561.                     if (method_exists($img'setImage')) {
  562.                         $img->setImage($newFilename);
  563.                     } elseif (method_exists($img'setPath')) {
  564.                         $img->setPath($newFilename);
  565.                     }
  566.                 } else {
  567.                     // Quelle nicht gefunden → Originalnamen übernehmen (kein physisches Duplikat möglich)
  568.                     if (method_exists($img'setImage')) {
  569.                         $img->setImage($srcName);
  570.                     } elseif (method_exists($img'setPath')) {
  571.                         $img->setPath($srcName);
  572.                     }
  573.                     // Optional: $this->addFlash('warning', "Bild nicht gefunden: {$srcName}");
  574.                 }
  575.             }
  576.             $new->addImage($img);
  577.         }
  578.         // --- (OPTIONAL) OCCURRENCES kopieren ---
  579.         if ($withOccurrences) {
  580.             foreach ($course->getAllOccurrences(falsefalse) as $oldOcc) {
  581.                 $occ = new CourseOccurrence();
  582.                 $occ->setCourse($new);
  583.                 // Typische Felder – bitte an deine Entity anpassen:
  584.                 if (method_exists($occ'setStart') && method_exists($oldOcc'getStart')) {
  585.                     $occ->setStart($oldOcc->getStart());
  586.                 }
  587.                 if (method_exists($occ'setEnd') && method_exists($oldOcc'getEnd')) {
  588.                     $occ->setEnd($oldOcc->getEnd());
  589.                 }
  590.                 if (method_exists($occ'setVenue') && method_exists($oldOcc'getVenue')) {
  591.                     $occ->setVenue($oldOcc->getVenue());
  592.                 }
  593.                 if (method_exists($occ'setPublished') && method_exists($oldOcc'getPublished')) {
  594.                     $occ->setPublished(false); // Sicherer Default: nicht sofort veröffentlichen
  595.                 }
  596.                 if (method_exists($occ'setSlots') && method_exists($oldOcc'getSlots')) {
  597.                     $occ->setSlots($oldOcc->getSlots());
  598.                 }
  599.                 // Übernehme weitere Felder
  600.                 if (method_exists($occ'setSlots') && method_exists($oldOcc'getSlots')) {
  601.                     $occ->setSlots($oldOcc->getSlots());
  602.                 }
  603.                 if (method_exists($occ'setCode') && method_exists($oldOcc'getCode')) {
  604.                     $occ->setCode($oldOcc->getCode());
  605.                 }
  606.                 if (method_exists($occ'setReservationAllowed') && method_exists($oldOcc'getReservationAllowed')) {
  607.                     $occ->setReservationAllowed($oldOcc->getReservationAllowed());
  608.                 }
  609.                 if (method_exists($occ'setPrice') && method_exists($oldOcc'getPrice')) {
  610.                     $occ->setPrice($oldOcc->getPrice());
  611.                 }
  612.                 if (method_exists($occ'setTaxRate') && method_exists($oldOcc'getTaxRate')) {
  613.                     $occ->setTaxRate($oldOcc->getTaxRate());
  614.                 }
  615.                 if (method_exists($occ'setMaterialCost') && method_exists($oldOcc'getMaterialCost')) {
  616.                     $occ->setMaterialCost($oldOcc->getMaterialCost());
  617.                 }
  618.                 if (method_exists($occ'setNumber') && method_exists($oldOcc'getNumber')) {
  619.                     $occ->setNumber($oldOcc->getNumber());
  620.                 }
  621.                 if (method_exists($occ'setCreated')) {
  622.                     $occ->setCreated(new \DateTime());
  623.                 }
  624.                 if (method_exists($occ'setVenueRoom') && method_exists($oldOcc'getVenueRoom')) {
  625.                     $occ->setVenueRoom($oldOcc->getVenueRoom());
  626.                 }
  627.                 $new->addOccurrence($occ);
  628.             }
  629.         }
  630.         // --- COURSE_DATA (dynamische Felder) kopieren ---
  631.         $oldDataList $courseDataRepository->findBy(['course' => $course]);
  632.         foreach ($oldDataList as $old) {
  633.             $data = new CourseData();
  634.             $data->setClient($client);
  635.             $data->setCourse($new);
  636.             $data->setField($old->getField()); // funktioniert jetzt
  637.             $data->setCreated(new \DateTime());
  638.             $data->setModified(new \DateTime());
  639.             if (method_exists($old'getValueText')) {
  640.                 $data->setValueText($old->getValueText());
  641.             }
  642.             if (method_exists($old'getValueInteger')) {
  643.                 $data->setValueInteger($old->getValueInteger());
  644.             }
  645.             if (method_exists($old'getValueDate')) {
  646.                 $data->setValueDate($old->getValueDate());
  647.             }
  648.             $em->persist($data);
  649.         }
  650.         $new->setCreated(new \DateTime());
  651.         $em->persist($new);
  652.         $em->flush();
  653.         $this->addFlash('success''Kurs kopiert');
  654.         return $this->redirectToRoute('course_edit', ['id' => $new->getId()]);
  655.     }
  656.     /**
  657.      * @Route("/multiple", name="course_delete-multiple", methods="DELETE")
  658.      */
  659.     public function deleteMultiple(
  660.         Request $request,
  661.         CourseRepository $courseRepo,
  662.         CartItemRepository $cartItemRepo,
  663.         WaitItemRepository $waitItemRepo,
  664.         OrderItemRepository $orderItemRepo): Response
  665.     {
  666.         if ($this->isCsrfTokenValid('delete_courses'$request->request->get('_token'))) {
  667.             $em $this->getDoctrine()->getManager();
  668.             $deleteIds $request->request->get('delete');
  669.             
  670.             // Prüfe zuerst alle Kurse auf Abhängigkeiten
  671.             $coursesWithDependencies = [];
  672.             foreach ($deleteIds as $id => $value) {
  673.                 if ($value) {
  674.                     $course $courseRepo->find($id);
  675.                     // Security: Check client ownership via CourseSecurityVoter
  676.                     $this->denyAccessUnlessGranted('view'$course);
  677.                     
  678.                     $dependencies $this->checkCourseDependencies($course);
  679.                     if (!empty($dependencies)) {
  680.                         $coursesWithDependencies[$course->getTitle()] = $dependencies;
  681.                     }
  682.                 }
  683.             }
  684.             
  685.             // Wenn Abhängigkeiten existieren, zeige sie an
  686.             if (!empty($coursesWithDependencies)) {
  687.                 $this->addFlash('error''Folgende Kurse können nicht gelöscht werden:');
  688.                 foreach ($coursesWithDependencies as $courseTitle => $dependencies) {
  689.                     $this->addFlash('error'sprintf('"%s":'$courseTitle));
  690.                     foreach ($dependencies as $dependency) {
  691.                         $this->addFlash('error''  ' $dependency);
  692.                     }
  693.                 }
  694.                 return $this->redirectToRoute('course_index');
  695.             }
  696.             
  697.             // Lösche Kurse ohne Abhängigkeiten
  698.             foreach ($deleteIds as $id => $value) {
  699.                 if ($value) {
  700.                     $course $courseRepo->find($id);
  701.                     $waitItems $waitItemRepo->findBy(['course' => $course]);
  702.                     foreach ($waitItems as $waitItem) {
  703.                         $em->remove($waitItem);
  704.                     }
  705.                     $cartItems $cartItemRepo->findBy(['course' => $course]);
  706.                     foreach ($cartItems as $cartItem) {
  707.                         $em->remove($cartItem);
  708.                     }
  709.                     $orderItems $orderItemRepo->findBy(['course' => $course]);
  710.                     foreach ($orderItems as $orderItem) {
  711.                         $orderItem->setCourseOccurrence(null);
  712.                     }
  713.                     $em->remove($course);
  714.                 }
  715.             }
  716.             try {
  717.                 $em->flush();
  718.             } catch (\Exception $e) {
  719.                 $errorMessage $e->getMessage();
  720.                 if (str_contains($errorMessage'Integrity constraint violation')) {
  721.                     $this->addFlash('error''Mindestens ein Kurs kann nicht gelöscht werden, weil er an anderer Stelle gebraucht wird.');
  722.                 } else {
  723.                     $this->addFlash('error''Beim Löschen ist ein Fehler aufgetreten.');
  724.                 }
  725.                 return $this->redirectToRoute('course_index');
  726.             }
  727.             $this->addFlash('notice'count($deleteIds) > 'Kurse gelöscht' 'Kurs gelöscht');
  728.         }
  729.         return $this->redirectToRoute('course_index');
  730.     }
  731.     /**
  732.      * @Route("/{id}/occurrences", name="course_occurrences", methods="GET", requirements={"id"="\d+"})
  733.      */
  734.     public function courseOccurrences(
  735.         Request $request,
  736.         Course $course,
  737.         CourseOccurrenceRepository $repo,
  738.         \App\Service\UiService $uiService,
  739.         PersonRepository $personRepository): Response
  740.     {
  741.         //   $this->denyAccessUnlessGranted('ROLE_MANAGER', $course);
  742.         $user $this->getCurrentUser();
  743.         $person $personRepository->getByUser($user);
  744.         $order $uiService->getSortOrder('course-occurrences-listing');
  745.         $archive = !empty($request->get('archive'));
  746.         $occurrences $repo->findByCoursePaged(
  747.             $course,
  748.             self::LISTING_LIMIT,
  749.             $order['orderDirection'] ?? 'ASC',
  750.             $order['orderBy'] ?? 'title'
  751.         );
  752.         return $this->render('course/occurrences.html.twig', [
  753.             'uiService' => $uiService,
  754.             'course' => $course,
  755.             'user' => $user,
  756.             'occurrences' => $occurrences->getIterator(),
  757.             'total' => $occurrences->count(),
  758.             'pages' => self::LISTING_LIMIT 0
  759.                 ceil($occurrences->count() / self::LISTING_LIMIT)
  760.                 : 1// Fallback, wenn LISTING_LIMIT 0 ist
  761.             'page' => 1,
  762.             'env' => $_ENV,
  763.             'archive' => $archive,
  764.         ]);
  765.     }
  766.     /**
  767.      * @Route("/{id}/occurrences/{page}/{orderby}/{order}/{search}", name="course_occurrences_listing", methods="GET", defaults={"search"="", "order"="desc", "orderby"="start"}, requirements={"id"="\d+"})
  768.      */
  769.     public function courseOccurrencesListing(
  770.         Request $request,
  771.         Course $course,
  772.         $page,
  773.         $orderby,
  774.         $order,
  775.         $search,
  776.         CourseOccurrenceRepository $repo,
  777.         \App\Service\UiService $uiService): Response
  778.     {
  779.         //    $this->denyAccessUnlessGranted('ROLE_MANAGER', $course);
  780.         $uiService->storeSortOrder('course-occurrences-listing'$orderby$order);
  781.         $occurrences $repo->findByCoursePaged($courseself::LISTING_LIMIT$order$orderby$page$search);
  782.         return $this->render('course/tabs/_occurrences_listing.html.twig', [
  783.             'course' => $course,
  784.             'occurrences' => $occurrences->getIterator(),
  785.             'total' => $occurrences->count(),
  786.             'pages' => ceil($occurrences->count() / self::LISTING_LIMIT),
  787.             'page' => $page,
  788.             'env' => $_ENV,
  789.         ]);
  790.     }
  791.     /**
  792.      * @Route("/{id}/images", name="course_images", methods="GET|POST", requirements={"id"="\d+"})
  793.      */
  794.     public function courseImages(
  795.         Request $request,
  796.         Course $course,
  797.         PersonRepository $personRepository,
  798.     ) {
  799.         //    $this->denyAccessUnlessGranted('ROLE_MANAGER', $course);
  800.         $courseImages = new ArrayCollection();
  801.         foreach ($course->getImages() as $image) {
  802.             $courseImages->add($image);
  803.         }
  804.         $form $this->createForm(CourseImagesType::class, $course);
  805.         $form->handleRequest($request);
  806.         $user $this->getCurrentUser();
  807.         $person $personRepository->getByUser($user);
  808.         if ($form->isSubmitted() && $form->isValid()) {
  809.             $manager $this->getDoctrine()->getManager();
  810.             foreach ($courseImages as $image) {
  811.                 $image->setCreated(new \DateTime());
  812.                 if (false === $course->getImages()->contains($image)) {
  813.                     $image->setCourse(null);
  814.                     $manager->remove($image);
  815.                 }
  816.             }
  817.             foreach ($course->getImages() as $key => $image) {
  818.                 // Setze das `created`-Datum, falls es nicht gesetzt wurde
  819.                 if (null === $image->getCreated()) {
  820.                     $image->setCreated(new \DateTime());
  821.                 }
  822.                 // Setze die Reihenfolge, falls `orderId` leer ist
  823.                 if (empty($image->getOrderId())) {
  824.                     $image->setOrderId($key 1000);
  825.                 }
  826.             }
  827.             $manager->flush();
  828.             $this->addFlash('notice''Kursbilder gespeichert');
  829.             return $this->redirectToRoute('course_images', ['id' => $course->getId()]);
  830.         }
  831.         return $this->render('course/images.html.twig', [
  832.             'course' => $course,
  833.             'form' => $form->createView(),
  834.             'env' => $_ENV,
  835.             'user' => $user,
  836.         ]);
  837.     }
  838.     /**
  839.      * @Route("/{id}/invoices", name="course_invoices", methods="GET", requirements={"id"="\d+"})
  840.      */
  841.     public function courseInvoices(
  842.         Request $request,
  843.         Course $course,
  844.         OrderItemRepository $repo,
  845.         OrderService $orderService,
  846.         TagsPersonRepository $tagsPersonRepository,
  847.         PersonRepository $personRepository,
  848.     ) {
  849.         // Security: Check client ownership via CourseSecurityVoter
  850.         $this->denyAccessUnlessGranted('view'$course);
  851.         $user $this->getCurrentUser();
  852.         $person $personRepository->getByUser($user);
  853.         $orderItems $repo->findByCoursePaged($course);
  854.         /**
  855.          * The display logic of subscription courses is different, as there only one order exists per
  856.          * customer/participant, but they should appear in every following course occurrence until they cancel.
  857.          */
  858.         // if ($course->getCourseNature() === 'CourseSubscription') {
  859.         //     return $this->render('course/invoices-subscription.html.twig', [
  860.         //         'course' => $course,
  861.         //         'orderItems' => $orderItems->getIterator(),
  862.         //     ]);
  863.         // } else {
  864.         $archive = !empty($request->get('archive'));
  865.         if ('CourseSubscription' === $course->getCourseNature()) {
  866.             foreach ($orderItems as $orderItem) {
  867.                 $orderItem->isAfterCancelDate $orderService->isOrderItemOccurrenceAfterCancelDateByParticipant($this->getCurrentClient(), $orderItem);
  868.             }
  869.         }
  870.         return $this->render('course/invoices.html.twig', [
  871.             'tagsPerson' => $tagsPersonRepository->findAll(),
  872.             'course' => $course,
  873.             'orderItems' => $orderItems->getIterator(),
  874.             'archive' => $archive,
  875.             'user' => $user,
  876.         ]);
  877.         // }
  878.     }
  879.     /**
  880.      * @Route("/{id}/invoices/create", name="course_create_invoices", methods="POST", requirements={"id"="\d+"})
  881.      */
  882.     public function courseCreateInvoices(
  883.         Request $request,
  884.         Course $course,
  885.         OrderItemRepository $itemRepo,
  886.         InvoiceService $invoiceService,
  887.     ) {
  888.         // Security: Check client ownership via CourseSecurityVoter
  889.         $this->denyAccessUnlessGranted('view'$course);
  890.         if ($this->isCsrfTokenValid('create_invoices'$request->request->get('_token'))) {
  891.             $em $this->getDoctrine()->getManager();
  892.             $createIds $request->request->get('create');
  893.             $count 0;
  894.             if (!empty($createIds)) {
  895.                 foreach ($createIds as $id => $value) {
  896.                     if ($value) {
  897.                         $orderItem $itemRepo->find($id);
  898.                         $results $invoiceService->createInvoiceFromOrderItem($orderItem);
  899.                         foreach ($results['attendees'] as $attendee) {
  900.                             $em->persist($attendee);
  901.                         }
  902.                         $em->persist($results['invoice']);
  903.                         $em->flush();
  904.                         ++$count;
  905.                     }
  906.                 }
  907.                 $em->flush();
  908.             }
  909.             $this->addFlash('notice'$count.(=== $count ' Rechnung' ' Rechnungen').' erstellt');
  910.         }
  911.         return $this->redirectToRoute('course_invoices', ['id' => $course->getId()]);
  912.     }
  913.     /**
  914.      * @Route("/{id}/invoices/merge-pdf", name="course_invoices_merge-pdf", methods="POST", requirements={"id"="\d+"})
  915.      */
  916.     public function courseMergePdf(
  917.         Request $request,
  918.         Course $course,
  919.         OrderItemRepository $repo,
  920.         PdfService $pdfService,
  921.     ) {
  922.         // Security: Check client ownership via CourseSecurityVoter
  923.         $this->denyAccessUnlessGranted('view'$course);
  924.         if ($this->isCsrfTokenValid('create_invoices'$request->request->get('_token'))) {
  925.             $em $this->getDoctrine()->getManager();
  926.             $mergeIds $request->request->get('close');
  927.             if (!empty($mergeIds)) {
  928.                 $mergeInvoices = new ArrayCollection();
  929.                 foreach ($mergeIds as $id => $value) {
  930.                     if ($value) {
  931.                         $orderItem $repo->find($id);
  932.                         $order $orderItem->getOrder();
  933.                         foreach ($order->getInvoices() as $invoice) {
  934.                             if (!$mergeInvoices->contains($invoice)) {
  935.                                 $mergeInvoices->add($invoice);
  936.                             }
  937.                         }
  938.                     }
  939.                 }
  940.                 $pdf $pdfService->getMergedInvoicePdf($this->getCurrentClient(), $mergeInvoices->toArray());
  941.                 $pdf->Output('D''Rechnungen_'.date('Y-m-d_H-i').'.pdf');
  942.                 exit;
  943.             } else {
  944.                 $this->addFlash('notice''Keine Rechnungen ausgewählt.');
  945.             }
  946.         }
  947.         return $this->redirectToRoute('course_invoices', ['id' => $course->getId()]);
  948.     }
  949.     /**
  950.      * @Route("/{id}/invoices/close", name="course_close_invoices", methods="POST", requirements={"id"="\d+"})
  951.      */
  952.     public function courseCloseInvoices(
  953.         Request $request,
  954.         Course $course,
  955.         InvoiceItemRepository $repo,
  956.         ConfigurationService $configService,
  957.         MailerService $mailer,
  958.         PdfService $pdfService,
  959.         EmailHistoryService $emailHistoryService,
  960.     ) {
  961.         // Security: Check client ownership via CourseSecurityVoter
  962.         $this->denyAccessUnlessGranted('view'$course);
  963.         if ($this->isCsrfTokenValid('create_invoices'$request->request->get('_token'))) {
  964.             $em $this->getDoctrine()->getManager();
  965.             $closeIds $request->request->get('close');
  966.             $count 0;
  967.             if (!empty($closeIds)) {
  968.                 foreach ($closeIds as $id => $value) {
  969.                     if ($value) {
  970.                         $invoiceItem $repo->findOneBy(['orderItem' => $id]);
  971.                         $invoice $invoiceItem->getInvoice();
  972.                         if (Invoice::STATUS_DRAFT == $invoice->getStatus()) {
  973.                             $pdf $pdfService->getInvoicePdf($this->getCurrentClient(), $invoice);
  974.                             $sentMessage $mailer->sendInvoiceEmail(
  975.                                 $invoice,
  976.                                 'Rechnung-'.$invoice->getNumber().'.pdf',
  977.                                 $pdf->Output('S''Rechnung-'.$invoice->getNumber().'.pdf')
  978.                             );
  979.                             $outputfile $this->generateUniqueFileName().'.pdf';
  980.                             $outputpath $this->getParameter('attachment_directory').'/'.$outputfile;
  981.                             $pdf->Output('F'$outputpath);
  982.                             $emailHistoryService->saveProtocolEntryFromInvoiceMessage(
  983.                                 $invoice,
  984.                                 $sentMessage['sender'],
  985.                                 $sentMessage['subject'],
  986.                                 $sentMessage['message'],
  987.                                 $outputfile,
  988.                                 'Rechnung-'.$invoice->getNumber().'.pdf'
  989.                             );
  990.                             if (Invoice::STATUS_CLOSED != $invoice->getStatus()) {
  991.                                 if ($invoice->isPaymentDebit()) {
  992.                                     $invoice->setStatus(Invoice::STATUS_DEBIT_PENDING);
  993.                                 } else {
  994.                                     $invoice->setStatus(Invoice::STATUS_CLOSED);
  995.                                 }
  996.                             }
  997.                             ++$count;
  998.                         } else {
  999.                             // Send invoice again
  1000.                             $pdf $pdfService->getInvoicePdf($this->getCurrentClient(), $invoice);
  1001.                             $sentMessage $mailer->sendInvoiceEmail(
  1002.                                 $invoice,
  1003.                                 'Rechnung-'.$invoice->getNumber().'.pdf',
  1004.                                 $pdf->Output('S''Rechnung-'.$invoice->getNumber().'.pdf')
  1005.                             );
  1006.                             ++$count;
  1007.                         }
  1008.                         // Update the order status
  1009.                         $newOrderState $invoice->getOrder()->setStatus(Order::STATUS_DONE);
  1010.                         $em->persist($newOrderState);
  1011.                         $em->flush();
  1012.                     }
  1013.                 }
  1014.             }
  1015.             $this->addFlash('notice'$count.(=== $count ' Rechnung' ' Rechnungen').' versendet');
  1016.         }
  1017.         return $this->redirectToRoute('course_invoices', ['id' => $course->getId()]);
  1018.     }
  1019.     /**
  1020.      * @Route("/{id}/invoices/close-sepa/{all}", name="course_close_sepa-invoices", defaults={"all"="false"},methods="POST", requirements={"id"="\d+"})
  1021.      */
  1022.     public function courseCloseSepaInvoices(
  1023.         Request $request,
  1024.         Course $course,
  1025.         $all false,
  1026.         OrderRepository $repo,
  1027.         OrderItemRepository $itemRepo,
  1028.         ConfigurationService $configService,
  1029.         SepaXmlService $sepaXmlService,
  1030.     ) {
  1031.         // Security: Check client ownership via CourseSecurityVoter
  1032.         $this->denyAccessUnlessGranted('view'$course);
  1033.         if ($this->isCsrfTokenValid('create_invoices'$request->request->get('_token'))) {
  1034.             $em $this->getDoctrine()->getManager();
  1035.             $closeIds $request->request->get('close');
  1036.             $invoicesToExport = new ArrayCollection();
  1037.             if ($all) {
  1038.                 $orderItems $itemRepo->findByCoursePaged($course);
  1039.                 foreach ($orderItems as $orderItem) {
  1040.                     $order $orderItem->getOrder();
  1041.                     foreach ($order->getInvoices() as $invoice) {
  1042.                         if (
  1043.                             $invoice->containsCourse($course)
  1044.                             && !$invoicesToExport->contains($invoice)
  1045.                             && $invoice->isPaymentDebit()
  1046.                         ) {
  1047.                             $invoicesToExport->add($invoice);
  1048.                             $invoice->setStatus(Invoice::STATUS_CLOSED);
  1049.                             if (!$order->getCustomer()->getDebitActive()) {
  1050.                                 $order->getCustomer()->setDebitActive(true);
  1051.                                 $invoice->setIsNewSepaMandate(true);
  1052.                             }
  1053.                         }
  1054.                         if (!empty($_ENV['SEPAEXPORT_PAYED'])) {
  1055.                             $restsumme $invoice->getMissingSum();
  1056.                             if (!= $restsumme) {
  1057.                                 $invoicePayment = new InvoicePayment();
  1058.                                 $invoicePayment->setInvoice($invoice);
  1059.                                 $invoicePayment->setPayedDate(new \DateTime());
  1060.                                 $invoicePayment->setSum($invoice->getMissingSum());
  1061.                                 $invoice->setPaymentStatus(Invoice::FULLY_PAID);
  1062.                                 $invoice->setExportStatus(Invoice::EXPORTED);
  1063.                                 $em $this->getDoctrine()->getManager();
  1064.                                 $em->persist($invoicePayment);
  1065.                             }
  1066.                             $invoice->setPaymentStatus(Invoice::FULLY_PAID);
  1067.                             $invoice->setExportStatus(Invoice::EXPORTED);
  1068.                             $em->persist($invoice);
  1069.                             $em->flush();
  1070.                         }
  1071.                     }
  1072.                 }
  1073.             } elseif (!empty($closeIds)) {
  1074.                 foreach ($closeIds as $id => $value) {
  1075.                     if ($value) {
  1076.                         $orderItem $itemRepo->find($id);
  1077.                         $order $orderItem->getOrder();
  1078.                         foreach ($order->getInvoices() as $invoice) {
  1079.                             if (
  1080.                                 $invoice->containsCourse($course)
  1081.                                 && !$invoicesToExport->contains($invoice)
  1082.                                 && $invoice->isPaymentDebit()
  1083.                             ) {
  1084.                                 $invoicesToExport->add($invoice);
  1085.                                 $invoice->setStatus(Invoice::STATUS_CLOSED);
  1086.                                 if (!$order->getCustomer()->getDebitActive()) {
  1087.                                     $order->getCustomer()->setDebitActive(true);
  1088.                                     $invoice->setIsNewSepaMandate(true);
  1089.                                 }
  1090.                             }
  1091.                         }
  1092.                         if (!empty($_ENV['SEPAEXPORT_PAYED'])) {
  1093.                             $restsumme $invoice->getMissingSum();
  1094.                             if (!= $restsumme) {
  1095.                                 $invoicePayment = new InvoicePayment();
  1096.                                 $invoicePayment->setInvoice($invoice);
  1097.                                 $invoicePayment->setPayedDate(new \DateTime());
  1098.                                 $invoicePayment->setSum($invoice->getMissingSum());
  1099.                                 $invoice->setExportStatus(Invoice::EXPORTED);
  1100.                                 $em $this->getDoctrine()->getManager();
  1101.                                 $em->persist($invoicePayment);
  1102.                             }
  1103.                             $invoice->setPaymentStatus(Invoice::FULLY_PAID);
  1104.                             $invoice->setExportStatus(Invoice::EXPORTED);
  1105.                             $em->persist($invoice);
  1106.                             $em->flush();
  1107.                         }
  1108.                     }
  1109.                 }
  1110.             } else {
  1111.                 $this->addFlash('warning''Es wurden keine Rechnungen zum Export ausgewählt.');
  1112.                 return $this->redirectToRoute('course_invoices', ['id' => $course->getId()]);
  1113.             }
  1114.             // Check invoices for past due dates
  1115.             foreach ($invoicesToExport as $invoice) {
  1116.                 if (new \DateTime() > $invoice->getDueDate()) {
  1117.                     $this->addFlash('warning''Mindestens eine Rechnung enthält ein Zahlungsziel in der Vergangenheit.');
  1118.                     // return $this->redirectToRoute('course_invoices', ['id' => $course->getId()]);
  1119.                 }
  1120.             }
  1121.             if (count($invoicesToExport) > 0) {
  1122.                 $config $configService->getSepaXmlConfigByClient($this->getCurrentClient());
  1123.                 try {
  1124.                     $xml $sepaXmlService->getSepaXmlMultiple($this->getCurrentClient(), $config$invoicesToExport);
  1125.                 } catch (ServiceException $e) {
  1126.                     $this->addFlash('error'$e->getMessage());
  1127.                     return $this->redirectToRoute('invoice_index');
  1128.                 }
  1129.                 $em->flush();
  1130.                 $response = new Response($xml);
  1131.                 $response->headers->set('Content-Type''text/xml');
  1132.                 $response->headers->set('Content-disposition''attachment; filename="SEPA-'.date('Ymd-His').'.xml"');
  1133.                 return $response;
  1134.             }
  1135.             $this->addFlash('error''Mindestens eine Rechnung enthält Fehler.');
  1136.             return $this->redirectToRoute('course_invoices', ['id' => $course->getId()]);
  1137.         }
  1138.         $this->addFlash('error''Der Sicherheits-Token ist ungültig. Bitte versuchen Sie es noch einmal.');
  1139.         return $this->redirectToRoute('course_invoices', ['id' => $course->getId()]);
  1140.     }
  1141.     /**
  1142.      * @Route("/{id}/invoices/close-sepa/", name="course_close_sepa-invoice_selected", methods="POST", requirements={"id"="\d+"})
  1143.      */
  1144.     public function courseCloseSepaInvoiceSelected(
  1145.         Request $request,
  1146.         Course $course,
  1147.         OrderItemRepository $itemRepo,
  1148.         ConfigurationService $configService,
  1149.         SepaXmlService $sepaXmlService,
  1150.     ) {
  1151.         // Security: Check client ownership via CourseSecurityVoter
  1152.         $this->denyAccessUnlessGranted('view'$course);
  1153.         if ($this->isCsrfTokenValid('create_invoices'$request->request->get('_token'))) {
  1154.             $em $this->getDoctrine()->getManager();
  1155.             $closeIds $request->request->get('close');
  1156.             $invoicesToExport = new ArrayCollection();
  1157.             if (!empty($closeIds)) {
  1158.                 foreach ($closeIds as $id => $value) {
  1159.                     if ($value) {
  1160.                         $orderItem $itemRepo->find($id);
  1161.                         $order $orderItem->getOrder();
  1162.                         foreach ($order->getInvoices() as $invoice) {
  1163.                             if (
  1164.                                 $invoice->containsCourse($course)
  1165.                                 && !$invoicesToExport->contains($invoice)
  1166.                                 && $invoice->isPaymentDebit()
  1167.                             ) {
  1168.                                 $invoicesToExport->add($invoice);
  1169.                                 $invoice->setStatus(Invoice::STATUS_CLOSED);
  1170.                                 if (!$order->getCustomer()->getDebitActive()) {
  1171.                                     $order->getCustomer()->setDebitActive(true);
  1172.                                     $invoice->setIsNewSepaMandate(true);
  1173.                                 }
  1174.                             }
  1175.                         }
  1176.                         if (!empty($_ENV['SEPAEXPORT_PAYED'])) {
  1177.                             $restsumme $invoice->getMissingSum();
  1178.                             if (!= $restsumme) {
  1179.                                 $invoicePayment = new InvoicePayment();
  1180.                                 $invoicePayment->setInvoice($invoice);
  1181.                                 $invoicePayment->setPayedDate(new \DateTime());
  1182.                                 $invoicePayment->setSum($invoice->getMissingSum());
  1183.                                 $invoice->setExportStatus(Invoice::EXPORTED);
  1184.                                 $em $this->getDoctrine()->getManager();
  1185.                                 $em->persist($invoicePayment);
  1186.                             }
  1187.                             $invoice->setPaymentStatus(Invoice::FULLY_PAID);
  1188.                             $invoice->setExportStatus(Invoice::EXPORTED);
  1189.                             $em->persist($invoice);
  1190.                             $em->flush();
  1191.                         }
  1192.                     }
  1193.                 }
  1194.             } else {
  1195.                 $this->addFlash('warning''Es wurden keine Rechnungen zum Export ausgewählt.');
  1196.                 return $this->redirectToRoute('course_invoices', ['id' => $course->getId()]);
  1197.             }
  1198.             // Check invoices for past due dates
  1199.             foreach ($invoicesToExport as $invoice) {
  1200.                 if (new \DateTime() > $invoice->getDueDate()) {
  1201.                     $this->addFlash('warning''Mindestens eine Rechnung enthält ein Zahlungsziel in der Vergangenheit.');
  1202.                     // return $this->redirectToRoute('course_invoices', ['id' => $course->getId()]);
  1203.                 }
  1204.             }
  1205.             if (count($invoicesToExport) > 0) {
  1206.                 $config $configService->getSepaXmlConfigByClient($this->getCurrentClient());
  1207.                 try {
  1208.                     $xml $sepaXmlService->getSepaXmlMultiple($this->getCurrentClient(), $config$invoicesToExport);
  1209.                 } catch (ServiceException $e) {
  1210.                     $this->addFlash('error'$e->getMessage());
  1211.                     return $this->redirectToRoute('invoice_index');
  1212.                 }
  1213.                 $em->flush();
  1214.                 $response = new Response($xml);
  1215.                 $response->headers->set('Content-Type''text/xml');
  1216.                 $response->headers->set('Content-disposition''attachment; filename="SEPA-'.date('Ymd-His').'.xml"');
  1217.                 return $response;
  1218.             }
  1219.             $this->addFlash('error''Mindestens eine Rechnung enthält Fehler.');
  1220.             return $this->redirectToRoute('course_invoices', ['id' => $course->getId()]);
  1221.         }
  1222.         $this->addFlash('error''Der Sicherheits-Token ist ungültig. Bitte versuchen Sie es noch einmal.');
  1223.         return $this->redirectToRoute('course_invoices', ['id' => $course->getId()]);
  1224.     }
  1225.     /**
  1226.      * @Route("/{id}/participants", name="course_participants", methods="GET", requirements={"id"="\d+"})
  1227.      */
  1228.     public function courseParticipants(
  1229.         Request $request,
  1230.         Course $course,
  1231.         OrderItemRepository $repo,
  1232.         OrderService $orderService,
  1233.         TagsPersonRepository $tagsPersonRepository,
  1234.         PersonRepository $personRepository,
  1235.         PresenceRepository $presenceRepository,
  1236.     ) {
  1237.         // Security: Check client ownership via CourseSecurityVoter
  1238.         $this->denyAccessUnlessGranted('view'$course);
  1239.         $orderItems $repo->findByCoursePaged($course);
  1240.         $user $this->getCurrentUser();
  1241.         $person $personRepository->getByUser($user);
  1242.         $archive = !empty($request->get('archive'));
  1243.         if ('CourseSubscription' === $course->getCourseNature()) {
  1244.             foreach ($orderItems as $orderItem) {
  1245.                 foreach ($orderItem->getParticipants() as $participant) {
  1246.                     $participant->isAfterCancelDate $orderService->isOrderItemOccurrenceAfterCancelDateByParticipant($this->getCurrentClient(), $orderItem$participant->getId());
  1247.                     $participant->cancelDate $orderService->getCancelDateForParticipantInCourse($this->getCurrentClient(), $participant);
  1248.                 }
  1249.             }
  1250.         }
  1251.         $occurrenceIds = [];
  1252.         foreach ($orderItems as $oi) {
  1253.             if ($oi->getCourseOccurrence()) {
  1254.                 $occurrenceIds[$oi->getCourseOccurrence()->getId()] = true;
  1255.             }
  1256.         }
  1257.         $occurrenceIds array_keys($occurrenceIds);
  1258.         // 2) Alle Presences zu diesen Occurrences in EINER Query holen (mit Reason/Time/Person)
  1259.         $presences = [];
  1260.         if (!empty($occurrenceIds)) {
  1261.             $presences $presenceRepository->createQueryBuilder('p')
  1262.                 ->addSelect('r''t''o''per')
  1263.                 ->leftJoin('p.presenceReason''r')
  1264.                 ->leftJoin('p.occurrenceTime''t')
  1265.                 ->leftJoin('p.occurrence''o')
  1266.                 ->leftJoin('p.person''per')
  1267.                 ->andWhere('p.occurrence IN (:occIds)')
  1268.                 ->setParameter('occIds'$occurrenceIds)
  1269.                 ->orderBy('per.lastname''ASC')
  1270.                 ->getQuery()->getResult();
  1271.         }
  1272.         // 3) Aggregation je (Occurrence, Person)
  1273.         // presenceSummary[occId][personId] = [
  1274.         //   'present' => int, 'total' => int,
  1275.         //   'lastAbsentReason' => ?string, 'lastAbsentAt' => ?\DateTimeInterface
  1276.         // ]
  1277.         $presenceSummary = [];
  1278.         foreach ($presences as $p) {
  1279.             $occId $p->getOccurrence()->getId();
  1280.             $person $p->getPerson();
  1281.             if (!$person) {
  1282.                 continue;
  1283.             }
  1284.             $perId $person->getId();
  1285.             if (!isset($presenceSummary[$occId][$perId])) {
  1286.                 $presenceSummary[$occId][$perId] = [
  1287.                     'present' => 0,
  1288.                     'total' => 0,
  1289.                     'lastAbsentReason' => null,
  1290.                     'lastAbsentAt' => null,
  1291.                     'details' => [],   // ← NEU
  1292.                 ];
  1293.             }
  1294.             ++$presenceSummary[$occId][$perId]['total'];
  1295.             $present = (bool) $p->getPresence();
  1296.             $reason $p->getPresenceReason() ? $p->getPresenceReason()->getName() : null;
  1297.             // Datum für Anzeige (Startzeit des Einzeltermins, sonst modified/created)
  1298.             $dt $p->getOccurrenceTime() ? $p->getOccurrenceTime()->getStart() : ($p->getModified() ?? $p->getCreated());
  1299.             $dateStr $dt $dt->format('d.m.Y H:i') : '';
  1300.             // Details-Zeile hinzufügen (nur Strings/Bool, kein DateTime in JSON)
  1301.             $presenceSummary[$occId][$perId]['details'][] = [
  1302.                 'date' => $dateStr,
  1303.                 'present' => $present,
  1304.                 'reason' => $present null : ($reason ?? ''),
  1305.             ];
  1306.             if ($present) {
  1307.                 ++$presenceSummary[$occId][$perId]['present'];
  1308.             } else {
  1309.                 // letzte Abwesenheit aktualisieren
  1310.                 $prev $presenceSummary[$occId][$perId]['lastAbsentAt'];
  1311.                 if ($dt && (!$prev || $dt $prev)) {
  1312.                     $presenceSummary[$occId][$perId]['lastAbsentAt'] = $dt;
  1313.                     $presenceSummary[$occId][$perId]['lastAbsentReason'] = $reason;
  1314.                 }
  1315.             }
  1316.         }
  1317.         return $this->render('course/participants.html.twig', [
  1318.             'env' => $_ENV,
  1319.             'course' => $course,
  1320.             'orderItems' => $orderItems->getIterator(),
  1321.             'showCertificatesLink' => !empty($_ENV['CERTIFICATES_ENABLED']),
  1322.             'archive' => $archive,
  1323.             'user' => $user,
  1324.             // ⬇️ neu:
  1325.             // 'presenceSummary' => $presenceSummary,
  1326.         ]);
  1327.     }
  1328.     /**
  1329.      * @Route("/{id}/participants-pdf/{page}/{orderby}/{order}", name="course_participants_pdf", methods="GET", requirements={"id"="\d+"})
  1330.      *
  1331.      * @IsGranted("ROLE_SPEAKER")
  1332.      */
  1333.     public function courseParticipantsPdf(
  1334.         Request $request,
  1335.         CourseOccurrence $courseOccurrence,
  1336.         OrderItemRepository $repo,
  1337.         PdfService $pdfService,
  1338.         OrderService $orderService,
  1339.         $page 1,
  1340.         $orderby 'customerLastname',
  1341.         $order 'asc',
  1342.     ) {
  1343.         //    $this->denyAccessUnlessGranted('client_allowed', $courseOccurrence);
  1344.         // Security: Check client ownership via CourseOccurrenceVoter
  1345.         $this->denyAccessUnlessGranted('view'$courseOccurrence);
  1346.         $orderItems $repo->findByCourseOccurrence($courseOccurrence$orderby$order);
  1347.         if ('CourseSubscription' === $courseOccurrence->getCourse()->getCourseNature()) {
  1348.             foreach ($orderItems as $orderItem) {
  1349.                 foreach ($orderItem->getParticipants() as $participant) {
  1350.                     $participant->isAfterCancelDate $orderService->isOrderItemOccurrenceAfterCancelDateByParticipant($this->getCurrentClient(), $orderItem$participant->getId());
  1351.                 }
  1352.             }
  1353.         }
  1354.         $pdf $pdfService->getParticipantsPdf($this->getCurrentClient(), $courseOccurrence$orderItems);
  1355.         $pdf->Output('D''Teilnehmerliste-'.$courseOccurrence->getStart()->format('Y-m-d').'.pdf');
  1356.         exit;
  1357.     }
  1358.     /**
  1359.      * @Route("/{id}/participants-pdf-esf/{page}/{orderby}/{order}", name="course_participants_pdf_esf", methods="GET", requirements={"id"="\d+"})
  1360.      *
  1361.      * @IsGranted("ROLE_SPEAKER")
  1362.      */
  1363.     public function courseParticipantsPdfEsf(
  1364.         Request $request,
  1365.         CourseOccurrence $courseOccurrence,
  1366.         OrderItemRepository $repo,
  1367.         PdfService $pdfService,
  1368.         OrderService $orderService,
  1369.         $page 1,
  1370.         $orderby 'customerLastname',
  1371.         $order 'asc',
  1372.     ) {
  1373.         //    $this->denyAccessUnlessGranted('client_allowed', $courseOccurrence);
  1374.         // Security: Check client ownership via CourseOccurrenceVoter
  1375.         $this->denyAccessUnlessGranted('view'$courseOccurrence);
  1376.         $orderItems $repo->findByCourseOccurrence($courseOccurrence$orderby$order);
  1377.         if ('CourseSubscription' === $courseOccurrence->getCourse()->getCourseNature()) {
  1378.             foreach ($orderItems as $orderItem) {
  1379.                 foreach ($orderItem->getParticipants() as $participant) {
  1380.                     $participant->isAfterCancelDate $orderService->isOrderItemOccurrenceAfterCancelDateByParticipant($this->getCurrentClient(), $orderItem$participant->getId());
  1381.                 }
  1382.             }
  1383.         }
  1384.         $pdf $pdfService->getParticipantsPdfEsf($this->getCurrentClient(), $courseOccurrence$orderItems'esf');
  1385.         $pdf->Output('D''ESF-Teilnehmerliste-'.$courseOccurrence->getStart()->format('Y-m-d').'.pdf');
  1386.         exit;
  1387.     }
  1388.     /**
  1389.      * @Route("/participant/certificateemails/{id}", name="course_participants_Zertifikat_emails", methods="GET", requirements={"id"="\d+","downlaod"="\d+"})
  1390.      */
  1391.     public function courseParticipantsCertificateEmails(
  1392.         Request $request,
  1393.         $id,
  1394.         CourseOccurrenceRepository $repo,
  1395.         ConfigurationService $configService,
  1396.         TextblocksRepository $textblocksRepository,
  1397.         CourseDataRepository $courseDataRepository,
  1398.         OrderItemRepository $orderItemRepo,
  1399.         $orderby 'customerLastname',
  1400.         $order 'asc'): Response
  1401.     {
  1402.         $courseOccurrence $repo->find($id);
  1403.         // Security: Check client ownership via CourseOccurrenceVoter
  1404.         $this->denyAccessUnlessGranted('view'$courseOccurrence);
  1405.         $orderItems $orderItemRepo->findByCourseOccurrence($courseOccurrence$orderby$order);
  1406.         foreach ($orderItems as $orderItem) {
  1407.             $participants $orderItem->getParticipants();
  1408.             foreach ($participants as $participant) {
  1409.                 if ('cancelled' === $participant->getStatus() && !$participant->isStillSubscribed($courseOccurrence->getStart())) {
  1410.                     continue;
  1411.                 }
  1412.                 if ('cancelled' === $participant->getStatus()) {
  1413.                     continue;
  1414.                 }
  1415.                 if ((null != $participant->getCancelled()) and ($courseOccurrence->getEnd() > $participant->getCancelled())) {
  1416.                     continue;
  1417.                 }
  1418.                 if (!$participant->getOrderItem()->getOrder()->getCustomer()) {
  1419.                     continue;
  1420.                 }
  1421.                 if ($participant->getOrderItem()->isCancelled()) {
  1422.                     continue;
  1423.                 }
  1424.                 //   if (($participant->getOrderItem()->getStatus() === 'partially_cancelled'))  continue;
  1425.                 if ($participant->getOrderItem()->getOrder()->isCancelled()) {
  1426.                     continue;
  1427.                 }
  1428.                 if (isset($participant)) {
  1429.                     $orderItemPerson $participant;
  1430.                     $id $participant->getId();
  1431.                     $this->certificateService->generateAndSendCertificate(
  1432.                         $request,
  1433.                         $id,
  1434.                         $configService,
  1435.                         $orderItemPerson,
  1436.                         $textblocksRepository,
  1437.                         $courseDataRepository,
  1438.                         $orderItemRepo,
  1439.                     );
  1440.                 }
  1441.             }
  1442.         }
  1443.         return $this->redirectToRoute('course_participants', ['id' => $orderItemPerson->getOrderItem()->getCourse()->getId()]);
  1444.     }
  1445.     /**
  1446.      * @Route("/participant/downloadAllCertificates/{id}", name="course_participants_all_Zertifikat_download", methods="GET", requirements={"id"="\d+","downlaod"="\d+"})
  1447.      */
  1448.     public function downloadAllCertificates(
  1449.         Request $request,
  1450.         $id,
  1451.         CourseOccurrenceRepository $repo,
  1452.         ConfigurationService $configService,
  1453.         TextblocksRepository $textblocksRepository,
  1454.         CourseDataRepository $courseDataRepository,
  1455.         CertificatePdfBundleService $certificatePdfBundleService,
  1456.         OrderItemRepository $orderItemRepo,
  1457.         $orderby 'customerLastname',
  1458.         $order 'asc')
  1459.     {
  1460.         $orderby $request->query->get('orderby''customerLastname');
  1461.         $order $request->query->get('order''asc');
  1462.         $courseOccurrence $repo->find($id);
  1463.         // Security: Check client ownership via CourseOccurrenceVoter
  1464.         $this->denyAccessUnlessGranted('view'$courseOccurrence);
  1465.         $orderItems $orderItemRepo->findByCourseOccurrence($courseOccurrence$orderby$order);
  1466.         // Hier werden die Teilnehmer gefiltert, die ein Zertifikat erhalten sollen
  1467.         // und die nicht mehr abgemeldet sind oder deren Abmeldung nicht nach dem Kursende liegt.
  1468.         $filteredParticipants = [];
  1469.         foreach ($orderItems as $orderItem) {
  1470.             $participants $orderItem->getParticipants();
  1471.             // Filter wie du sie schon hast:
  1472.             foreach ($participants as $participant) {
  1473.                 if ('cancelled' === $participant->getStatus() && !$participant->isStillSubscribed($courseOccurrence->getStart())) {
  1474.                     continue;
  1475.                 }
  1476.                 if ('cancelled' === $participant->getStatus()) {
  1477.                     continue;
  1478.                 }
  1479.                 if (null != $participant->getCancelled() && $courseOccurrence->getEnd() > $participant->getCancelled()) {
  1480.                     continue;
  1481.                 }
  1482.                 if (!$participant->getOrderItem()->getOrder()->getCustomer()) {
  1483.                     continue;
  1484.                 }
  1485.                 if ($participant->getOrderItem()->isCancelled()) {
  1486.                     continue;
  1487.                 }
  1488.                 if ($participant->getOrderItem()->getOrder()->isCancelled()) {
  1489.                     continue;
  1490.                 }
  1491.                 $filteredParticipants[] = $participant;
  1492.             }
  1493.         }
  1494.         // Optional: Template wählen, Default reicht meist
  1495.         $viewTemplate $_ENV['ZERTIFIKAT'] ?? 'Default';
  1496.         return $certificatePdfBundleService->createPdfForAllParticipants($filteredParticipants$viewTemplate);
  1497.     }
  1498.     /**
  1499.      * @Route(
  1500.      *   "/participant/certificateemails-selected/{id}",
  1501.      *   name="course_participants_Zertifikat_emails_selected",
  1502.      *   requirements={"id"="\d+"},
  1503.      *   methods={"POST"}
  1504.      * )
  1505.      *
  1506.      * @IsGranted("ROLE_SPEAKER", subject="courseOccurrence")
  1507.      */
  1508.     public function certificateEmailsSelected(
  1509.         Request $request,
  1510.         int $id,
  1511.         CourseOccurrenceRepository $occRepo,
  1512.         OrderItemPersonRepository $participantRepo,
  1513.         ConfigurationService $configService,
  1514.         CertificateService $certificateService,
  1515.         TextblocksRepository $textRepo,
  1516.         CourseDataRepository $courseDataRepo,
  1517.         OrderItemRepository $orderItemRepo): Response
  1518.     {
  1519.         // CSRF-Schutz
  1520.         $this->denyAccessUnlessGranted('ROLE_USER');
  1521.         if (!$this->isCsrfTokenValid(
  1522.             'cert_select_'.$id,
  1523.             $request->request->get('_csrf_token')
  1524.         )) {
  1525.             throw $this->createAccessDeniedException('Ungültiges CSRF-Token');
  1526.         }
  1527.         $courseOccurrence $occRepo->find($id);
  1528.         if (!$courseOccurrence) {
  1529.             throw $this->createNotFoundException();
  1530.         }
  1531.         /** @var int[] $ids */
  1532.         $ids $request->request->get('participants', []);
  1533.         if (!$ids) {
  1534.             $this->addFlash('warning''Es wurde kein Teilnehmer ausgewählt.');
  1535.             return $this->redirectToRoute(
  1536.                 'course_participants',
  1537.                 ['id' => $courseOccurrence->getCourse()->getId()]
  1538.             );
  1539.         }
  1540.         $participants $participantRepo->findBy(['id' => $ids]);
  1541.         foreach ($participants as $participant) {
  1542.             // Sicherheits-Checks: gehört der Teilnehmer zu diesem Termin?
  1543.             if ($participant->getOrderItem()
  1544.                 ->getCourseOccurrence()->getId() !== $id
  1545.             ) {
  1546.                 continue;
  1547.             }
  1548.             if ('cancelled' === $participant->getStatus()) {
  1549.                 continue;
  1550.             }
  1551.             if ($participant->getCancelled()
  1552.                 && ($courseOccurrence->getEnd() > $participant->getCancelled())
  1553.             ) {
  1554.                 continue;
  1555.             }
  1556.             // Zertifikat erzeugen + mailen
  1557.             $certificateService->generateAndSendCertificate(
  1558.                 $request,
  1559.                 $id,
  1560.                 $configService,
  1561.                 $participant,
  1562.                 $textRepo,
  1563.                 $courseDataRepo,
  1564.                 $orderItemRepo
  1565.             );
  1566.         }
  1567.         $this->addFlash(
  1568.             'success',
  1569.             'Zertifikate wurden an die ausgewählten Teilnehmer versendet.'
  1570.         );
  1571.         return $this->redirectToRoute(
  1572.             'course_participants',
  1573.             ['id' => $courseOccurrence->getCourse()->getId()]
  1574.         );
  1575.     }
  1576.     /**
  1577.      * @Route("/participant/{id}/certificateemail", name="course_participants_Zertifikat_email", methods="GET", requirements={"id"="\d+","downlaod"="\d+"})
  1578.      */
  1579.     public function courseParticipantsCertificateEmail(
  1580.         Request $request,
  1581.         $id,
  1582.         ConfigurationService $configService,
  1583.         OrderItemPerson $orderItemPerson,
  1584.         TextblocksRepository $textblocksRepository,
  1585.         CourseDataRepository $courseDataRepository,
  1586.         OrderItemRepository $repo): Response
  1587.     {
  1588.         $orderItem $repo->find($id);
  1589.         $currentUrl $request->getUri();
  1590.         // $orderItemPerson = $orderItemPersonRepository->find($id);
  1591.         // hier werden die reihenfolge, ANzahl und Namen fü die Tickets vorbereitet
  1592.         $participants $orderItemPerson->getOrderItem()->getParticipants();
  1593.         $searchedName $id// Der Name, den du suchst.
  1594.         $position null;
  1595.         $i 0;
  1596.         foreach ($participants as $participant) {
  1597.             ++$i;
  1598.             if ($participant->getId() == $searchedName) {
  1599.                 $position $i;
  1600.                 break;
  1601.             }
  1602.         }
  1603.         if (null === $position) {
  1604.             $position '1';
  1605.         }
  1606.         // /
  1607.         $this->certificateService->generateAndSendCertificate(
  1608.             $request,
  1609.             $id,
  1610.             $configService,
  1611.             $orderItemPerson,
  1612.             $textblocksRepository,
  1613.             $courseDataRepository,
  1614.             $repo
  1615.         );
  1616.         return $this->redirectToRoute('course_participants', ['id' => $orderItemPerson->getOrderItem()->getCourse()->getId()]);
  1617.     }
  1618.     /**
  1619.      * @Route("/{courseId}/participants/certificateemail-multiple", name="course_participants_Zertifikat_emails_multiple", methods={"POST"})
  1620.      */
  1621.     public function sendMultipleCertificates(
  1622.         $courseId,
  1623.         Request $request,
  1624.         ConfigurationService $configService,
  1625.         OrderItemPersonRepository $orderItemPersonRepository,
  1626.         TextblocksRepository $textblocksRepository,
  1627.         CourseDataRepository $courseDataRepository,
  1628.         OrderItemRepository $orderItemRepository): Response
  1629.     {
  1630.         $items $request->request->get('item'); // kommt als [id => id, ...]
  1631.         if (!$items) {
  1632.             $this->addFlash('warning''Keine Teilnehmer ausgewählt.');
  1633.             return $this->redirectToRoute('course_index');
  1634.         }
  1635.         $lastOccurrence null;
  1636.         $successCount 0;
  1637.         foreach (array_keys($items) as $id) {
  1638.             $participant $orderItemPersonRepository->find($id);
  1639.             if (!$participant) {
  1640.                 continue;
  1641.             }
  1642.             $orderItem $participant->getOrderItem();
  1643.             if (!$orderItem || !$orderItem->getCourseOccurrence()) {
  1644.                 continue;
  1645.             }
  1646.             $lastOccurrence $orderItem->getCourseOccurrence();
  1647.             // ggf. Zugriffsrechte prüfen
  1648.             $this->denyAccessUnlessGranted('ROLE_SPEAKER'$lastOccurrence);
  1649.             // Zertifikat erzeugen und senden
  1650.             $this->certificateService->generateAndSendCertificate(
  1651.                 $request,
  1652.                 $participant->getId(),
  1653.                 $configService,
  1654.                 $participant,
  1655.                 $textblocksRepository,
  1656.                 $courseDataRepository,
  1657.                 $orderItemRepository
  1658.             );
  1659.             ++$successCount;
  1660.         }
  1661.         $this->addFlash('success'"$successCount Zertifikate wurden versendet.");
  1662.         return $this->redirectToRoute('course_participants', ['id' => $courseId]);
  1663.     }
  1664.     /**
  1665.      * @Route("/{id}/presences", name="course_presences", methods="GET", requirements={"id"="\d+"})
  1666.      */
  1667.     public function coursePresences(
  1668.         Request $request,
  1669.         Course $course,
  1670.         OrderItemRepository $repo,
  1671.         OrderService $orderService,
  1672.         CourseOccurrenceRepository $occurrencesrepo,
  1673.         TagsPersonRepository $tagsPersonRepository,
  1674.         \App\Service\UiService $uiService,
  1675.         PresenceRepository $presenceRepository,
  1676.         PersonRepository $personRepository,
  1677.         PresenceReasonRepository $reasonRepo,
  1678.         SpeakerRepository $speakerRepository)
  1679.     {
  1680.         // Security: Check client ownership via CourseSecurityVoter
  1681.         $this->denyAccessUnlessGranted('view'$course);
  1682.         // Grunddaten
  1683.         $orderItems $repo->findByCoursePaged($course);
  1684.         $order $uiService->getSortOrder('course-occurrences-listing');
  1685.         $user $this->getCurrentUser();
  1686.         $person $personRepository->getByUser($user);
  1687.         $speaker $speakerRepository->getByUser($user);
  1688.         $archive = !empty($request->get('archive'));
  1689.         // Zusatzinfos für Abo-Kurse
  1690.         if ('CourseSubscription' === $course->getCourseNature()) {
  1691.             foreach ($orderItems as $orderItem) {
  1692.                 foreach ($orderItem->getParticipants() as $participant) {
  1693.                     $participant->isAfterCancelDate $orderService->isOrderItemOccurrenceAfterCancelDateByParticipant(
  1694.                         $this->getCurrentClient(),
  1695.                         $orderItem,
  1696.                         $participant->getId()
  1697.                     );
  1698.                     $participant->cancelDate $orderService->getCancelDateForParticipantInCourse(
  1699.                         $this->getCurrentClient(),
  1700.                         $participant
  1701.                     );
  1702.                 }
  1703.             }
  1704.         }
  1705.         // Occurrences (paginiert) laden
  1706.         $occurrences $occurrencesrepo->findByCoursePaged(
  1707.             $course,
  1708.             self::LISTING_LIMIT,
  1709.             $order['orderDirection'] ?? 'ASC',
  1710.             $order['orderBy'] ?? 'title'
  1711.         );
  1712.         $occArray iterator_to_array($occurrences->getIterator(), false);
  1713.         $occIter = new \ArrayIterator($occArray);
  1714.         // Filtern: nur Occurrences, bei denen der Speaker NICHT zugeordnet ist
  1715.         if ($speaker) {
  1716.             $filteredOccurrences = [];
  1717.             foreach ($occurrences as $occurrence) {
  1718.                 if (in_array($speaker$occurrence->getSpeakers()->toArray())) {
  1719.                     $filteredOccurrences[] = $occurrence;
  1720.                 }
  1721.             }
  1722.             // Erstelle neuen Iterator mit gefilterten Daten
  1723.             $occurrences = new \ArrayIterator($filteredOccurrences);
  1724.         }
  1725.         // Wir brauchen ein *stabiles* Array der Occurrences (für IN()-Query und fürs Template)
  1726.         // Abwesenheitsgründe
  1727.         $reasons $reasonRepo->createQueryBuilder('r')
  1728.             ->andWhere('r.active = :a')->setParameter('a'true)
  1729.             ->orderBy('r.sort''ASC')->addOrderBy('r.name''ASC')
  1730.             ->getQuery()->getResult();
  1731.         // Presences nur für die gelisteten Occurrences laden (inkl. Beziehungen, um N+1 zu vermeiden)
  1732.         $presences = [];
  1733.         if (!empty($occArray)) {
  1734.             $presences $presenceRepository->createQueryBuilder('p')
  1735.                 ->addSelect('r''t''o''per')
  1736.                 ->leftJoin('p.presenceReason''r')
  1737.                 ->leftJoin('p.occurrenceTime''t')
  1738.                 ->leftJoin('p.occurrence''o')
  1739.                 ->leftJoin('p.person''per')
  1740.                 ->andWhere('p.occurrence IN (:occs)')
  1741.                 ->setParameter('occs'$occArray// ENTITÄTEN sind ok als Parameter
  1742.                  ->orderBy('per.lastname''ASC')
  1743.                 ->getQuery()->getResult();
  1744.         }
  1745.         // Index: [occId][timeId][personId] => Presence
  1746.         $presenceIndex = [];
  1747.         foreach ($presences as $p) {
  1748.             // Falls in deiner DB occurrenceTime/person garantiert NOT NULL ist, brauchst du die Checks nicht
  1749.             if (!$p->getOccurrence() || !$p->getOccurrenceTime() || !$p->getPerson()) {
  1750.                 continue;
  1751.             }
  1752.             $oid $p->getOccurrence()->getId();
  1753.             $tid $p->getOccurrenceTime()->getId();
  1754.             $pid $p->getPerson()->getId();
  1755.             $presenceIndex[$oid][$tid][$pid] = $p;
  1756.         }
  1757.         return $this->render('course/presences.html.twig', [
  1758.             // KEIN 'presences' => findAll() mehr!
  1759.             'presenceIndex' => $presenceIndex,
  1760.             'uiService' => $uiService,
  1761.             'tagsPerson' => $tagsPersonRepository->findAll(),
  1762.             'env' => $_ENV,
  1763.             'course' => $course,
  1764.             'occurrences' => $occurrences// stabiles Array
  1765.             'total' => $occurrences->count(),
  1766.             'pages' => ceil($occurrences->count() / self::LISTING_LIMIT),
  1767.             'page' => 1,
  1768.             'orderItems' => $orderItems->getIterator(),
  1769.             'showCertificatesLink' => !empty($_ENV['CERTIFICATES_ENABLED']),
  1770.             'archive' => $archive,
  1771.             'user' => $user,
  1772.             'person' => $person,
  1773.             'reasons' => $reasons,
  1774.         ]);
  1775.     }
  1776.     /**
  1777.      * @Route("/coursepresence/{id}/add/{courseOccurrence}/{participant}", name="course_presence_add", methods="GET|POST")
  1778.      */
  1779.     public function savePresenseNew(
  1780.         $id,
  1781.         $courseOccurrence,
  1782.         $participant,
  1783.         CourseOccurrenceTimeRepository $occurrenceTimeRepository,
  1784.         CourseOccurrenceRepository $occurrenceRepository,
  1785.         PersonRepository $personRepository,
  1786.     ) {
  1787.         $occurrenceTime $occurrenceTimeRepository->find($id);
  1788.         $occurrence $occurrenceRepository->find($courseOccurrence);
  1789.         $user $this->getCurrentUser();
  1790.         // $person = $personRepository->getByUser($user);
  1791.         $person $personRepository->find($participant);
  1792.         $newpresence = new Presence();
  1793.         $newpresence->setOccurrence($occurrence);
  1794.         $newpresence->setOccurrenceTime($occurrenceTime);
  1795.         $newpresence->setUser($user);
  1796.         $newpresence->setPerson($person);
  1797.         $newpresence->setPresence('1');
  1798.         $newpresence->setCreated(new \DateTime());
  1799.         $newpresence->setClient($this->getCurrentClient());
  1800.         $this->managerRegistry->persist($newpresence);
  1801.         $this->managerRegistry->flush();
  1802.         return $this->json([
  1803.             'success' => 'Die Anwesenheit wurde eingetragen.',
  1804.             'presence' => true,
  1805.         ]);
  1806.     }
  1807.     /**
  1808.      * @Route("/coursepresence/{id}/delete", name="course_presence_delete", methods="GET|POST")
  1809.      */
  1810.     public function deletePresense(
  1811.         $id,
  1812.         PresenceRepository $presenceRepository): Response
  1813.     {
  1814.         $presence $presenceRepository->find($id);
  1815.         // $presenceRepository->remove($presenceremove);
  1816.         $presence->setPresence('0');
  1817.         $presence->setModified(new \DateTime());
  1818.         $this->managerRegistry->persist($presence);
  1819.         $this->managerRegistry->flush();
  1820.         return $this->json([
  1821.             'success' => 'Die Anwesenheit wurde ausgetragen.',
  1822.             'presence' => true,
  1823.         ]);
  1824.     }
  1825.     /**
  1826.      * @Route("/coursepresence/{id}/edit", name="course_presence_edit", methods="GET|POST")
  1827.      */
  1828.     public function editPresense(
  1829.         $id,
  1830.         PresenceRepository $presenceRepository): Response
  1831.     {
  1832.         $presence $presenceRepository->find($id);
  1833.         // $presenceRepository->remove($presenceremove);
  1834.         $presence->setReason('x');
  1835.         $presence->setModified(new \DateTime());
  1836.         $this->managerRegistry->persist($presence);
  1837.         $this->managerRegistry->flush();
  1838.         return $this->json([
  1839.             'success' => 'Der Grund wurde eingetragen.',
  1840.         ]);
  1841.     }
  1842.     /**
  1843.      * @Route("/coursepresence/{id}/update", name="course_presence_update", methods="GET|POST")
  1844.      */
  1845.     public function updatePresence(Request $request): JsonResponse
  1846.     {
  1847.         // JSON-Daten aus der Anfrage extrahieren
  1848.         $data json_decode($request->getContent(), true);
  1849.         $id $data['id'] ?? null;
  1850.         $value $data['value'] ?? null;
  1851.         if (null === $id || null === $value) {
  1852.             return new JsonResponse(['success' => false'message' => 'Invalid data'], 400);
  1853.         }
  1854.         // Hier können Sie die Logik für das Aktualisieren der Anwesenheit implementieren
  1855.         // Zum Beispiel: Suchen Sie das entsprechende Entity und aktualisieren Sie den Wert
  1856.         $entityManager $this->getDoctrine()->getManager();
  1857.         $presence $entityManager->getRepository(Presence::class)->find($id);
  1858.         if (!$presence) {
  1859.             return new JsonResponse(['success' => false'message' => 'Presence not found'], 404);
  1860.         }
  1861.         // Setzen Sie den neuen Wert und speichern Sie ihn
  1862.         $presence->setreason($value); // Beispiel: setValue() sollte zu Ihrem Entity passen
  1863.         $entityManager->persist($presence);
  1864.         $entityManager->flush();
  1865.         // Erfolgreiche Antwort zurückgeben
  1866.         return new JsonResponse(['success' => true]);
  1867.     }
  1868.     /**
  1869.      * @Route("/coursepresence/{id}/participantspresenceexport", name="course_participants_presences_export", methods="GET", requirements={"id"="\d+"})
  1870.      *
  1871.      * @IsGranted("ROLE_SPEAKER")
  1872.      */
  1873.     public function courseParticipantsPresencesExport(
  1874.         Request $request,
  1875.         CourseOccurrence $courseOccurrence,
  1876.         OrderItemRepository $repo,
  1877.         PresenceRepository $presenceRepository,
  1878.         CourseOccurrenceTimeRepository $occurrenceTimeRepository,
  1879.         CourseOccurrenceRepository $occurrenceRepository,
  1880.         $orderby 'customerLastname',
  1881.         $order 'asc',
  1882.     ) {
  1883.         // Security: Check client ownership via CourseOccurrenceVoter
  1884.         $this->denyAccessUnlessGranted('view'$courseOccurrence);
  1885.         $orderItems $repo->findByCourseOccurrence($courseOccurrence$orderby$order);
  1886.         $course $courseOccurrence->getCourse()->getId();
  1887.         $orderItems $repo->findByCourseOccurrence($courseOccurrence$orderby$order);
  1888.         $presences $presenceRepository->findByOccurrence($courseOccurrence);
  1889.         // Summen je Person vorbereiten
  1890.         $statsByPerson = []; // [personId => ['present'=>int,'absent'=>int,'reasons'=>[name=>count]]]
  1891.         foreach ($presences as $p) {
  1892.             $person $p->getPerson();
  1893.             if (!$person) {
  1894.                 continue;
  1895.             }
  1896.             $pid $person->getId();
  1897.             if (!isset($statsByPerson[$pid])) {
  1898.                 $statsByPerson[$pid] = ['present' => 0'absent' => 0'reasons' => []];
  1899.             }
  1900.             if ($p->getPresence()) {
  1901.                 ++$statsByPerson[$pid]['present'];
  1902.             } else {
  1903.                 ++$statsByPerson[$pid]['absent'];
  1904.                 $rName $p->getPresenceReason() ? $p->getPresenceReason()->getName() : '—';
  1905.                 $statsByPerson[$pid]['reasons'][$rName] = ($statsByPerson[$pid]['reasons'][$rName] ?? 0) + 1;
  1906.             }
  1907.         }
  1908.         $response $this->render('person/export-participants-presences.csv.twig', [
  1909.             'presences' => $presenceRepository->findByOccurrence($courseOccurrence),
  1910.             'course' => $course,
  1911.             'occurrence' => $courseOccurrence,
  1912.             'orderItems' => $orderItems,
  1913.             'statsByPerson' => $statsByPerson// ← neu
  1914.         ]);
  1915.         $csv $response->getContent();                // <— nur Body
  1916.         $encodedCsvContent mb_convert_encoding($csv'ISO-8859-1''UTF-8');
  1917.         $response = new Response($encodedCsvContent);
  1918.         $response->setStatusCode(200);
  1919.         $response->headers->set('Content-Type''text/csv; charset=ISO-8859-1');
  1920.         //        $response->headers->set('Content-Type', 'text/csv; charset=utf-8');
  1921.         $response->headers->set('Content-Disposition''attachment; filename="Anwesenheit_Kurs.csv"');
  1922.         return $response;
  1923.     }
  1924.     /**
  1925.      * @Route("/coursepresence/{id}/exportcourseparticipants", name="export_course_participants", methods="GET", requirements={"id"="\d+"})
  1926.      *
  1927.      * @IsGranted("ROLE_SPEAKER")
  1928.      */
  1929.     public function courseParticipantsExport(
  1930.         Request $request,
  1931.         CourseOccurrence $courseOccurrence,
  1932.         OrderItemRepository $repo,
  1933.         $orderby 'customerLastname',
  1934.         $order 'asc',
  1935.     ) {
  1936.         // Security: Check client ownership via CourseOccurrenceVoter
  1937.         $this->denyAccessUnlessGranted('view'$courseOccurrence);
  1938.         $header $request->get('header'false); // Holen Sie den Wert von 'header'
  1939.         // Wenn 'header' true ist, setzen Sie ihn auf 1
  1940.         if ($header) {
  1941.             $header '1';
  1942.         } else {
  1943.             $header '0';
  1944.         }
  1945.         $course $courseOccurrence->getCourse()->getId();
  1946.         $orderItems $repo->findByCourseOccurrence($courseOccurrence$orderby$order);
  1947.         // Rendern des CSV-Inhalts als String (UTF-8)
  1948.         $csvContent $this->renderView('person/export-course-participants.csv.twig', [
  1949.             'header' => $header,
  1950.             'course' => $course,
  1951.             'occurrence' => $courseOccurrence,
  1952.             'orderItems' => $orderItems,
  1953.         ]);
  1954.         // Konvertiere den CSV-Inhalt in ISO-8859-1
  1955.         $encodedCsvContent mb_convert_encoding($csvContent'ISO-8859-1''UTF-8');
  1956.         // Erstelle eine Antwort mit dem konvertierten Inhalt
  1957.         $response = new Response($encodedCsvContent);
  1958.         $response->setStatusCode(200);
  1959.         $response->headers->set('Content-Type''text/csv; charset=ISO-8859-1');
  1960.         //        $response->headers->set('Content-Type', 'text/csv; charset=utf-8');
  1961.         $startDate $courseOccurrence->getStart();
  1962.         $formattedDate $startDate->format('d.m.y');
  1963.         // Konstruktion des Dateinamens
  1964.         $courseTitle $courseOccurrence->getCourse()->getTitle();
  1965.         $fileName 'Kurs-Teilnehmer-'.$courseTitle.'-'.$formattedDate.'.csv';
  1966.         // Setzen des Content-Disposition-Headers
  1967.         $response->headers->set('Content-Disposition''attachment; filename="'.$fileName.'"');
  1968.         return $response;
  1969.     }
  1970.     /**
  1971.      * @Route("/{id}/reservations", name="course_reservations", methods="GET", requirements={"id"="\d+"})
  1972.      */
  1973.     public function courseReservations(
  1974.         Request $request,
  1975.         Course $course,
  1976.         WaitItemRepository $repo,
  1977.         TagsPersonRepository $tagsPersonRepository,
  1978.         PersonRepository $personRepository,
  1979.     ) {
  1980.         // Security: Check client ownership via CourseSecurityVoter
  1981.         $this->denyAccessUnlessGranted('view'$course);
  1982.         $waitItems $repo->findByCoursePaged($course);
  1983.         $user $this->getCurrentUser();
  1984.         $person $personRepository->getByUser($user);
  1985.         return $this->render('course/reservations.html.twig', [
  1986.             'course' => $course,
  1987.             'waitItems' => $waitItems->getIterator(),
  1988.             'tagsPerson' => $tagsPersonRepository->findAll(),
  1989.             'user' => $user,
  1990.         ]);
  1991.     }
  1992.     /**
  1993.      * @Route("/waititem/{id}/delete", name="waititem_delete", methods="GET|POST")
  1994.      */
  1995.     public function deleteWaitItem(
  1996.         $id,
  1997.         WaitItemRepository $waitItemRepository,
  1998.         TagsPersonRepository $tagsPersonRepository,
  1999.         EntityManagerInterface $entityManager): Response
  2000.     {
  2001.         $waitItem $waitItemRepository->find($id);
  2002.         $course $waitItem->getCourseOccurrence()->getCourse();
  2003.         // Security: Check client ownership via CourseSecurityVoter
  2004.         $this->denyAccessUnlessGranted('view'$course);
  2005.         $waitItems $waitItemRepository->findByCoursePaged($course);
  2006.         if (!$waitItem) {
  2007.             throw $this->createNotFoundException('WaitItem not found');
  2008.         }
  2009.         $entityManager->remove($waitItem);
  2010.         $entityManager->flush();
  2011.         $this->addFlash('success''WaitItem deleted successfully');
  2012.         return $this->render('course/reservations.html.twig', [
  2013.             'course' => $course,
  2014.             'waitItems' => $waitItems->getIterator(),
  2015.             'tagsPerson' => $tagsPersonRepository->findAll(),
  2016.         ]);
  2017.     }
  2018.     /**
  2019.      * @Route("/{id}/reservations/move", name="course_reservations_move", methods="POST", requirements={"id"="\d+"})
  2020.      */
  2021.     public function moveCourseReservations(
  2022.         Request $request,
  2023.         Course $course,
  2024.         WaitItemRepository $repo): Response
  2025.     {
  2026.         // Security: Check client ownership via CourseSecurityVoter
  2027.         $this->denyAccessUnlessGranted('view'$course);
  2028.         $em $this->getDoctrine()->getManager();
  2029.         $moveIds $request->request->get('item');
  2030.         foreach ($moveIds as $id => $value) {
  2031.             if ($value) {
  2032.                 $waitItem $repo->find($value);
  2033.                 $orderItem OrderItem::createFromWaitItem($waitItem);
  2034.                 $participants $waitItem->getParticipants();
  2035.                 foreach ($participants as $participant) {
  2036.                     if ($participant->getPerson()->getId() === $id) {
  2037.                         $participant->setWaitItem(null);
  2038.                         $participant->setOrderItem($orderItem);
  2039.                         $orderItem->setQuantity($orderItem->getQuantity() + 1);
  2040.                         $waitItem->setQuantity($waitItem->getQuantity() - 1);
  2041.                         break;
  2042.                     }
  2043.                 }
  2044.                 $waitItem->getCourseOccurrence()->bookSlots($orderItem->getQuantity());
  2045.                 $order $waitItem->getOrder();
  2046.                 $order->addOrderItem($orderItem);
  2047.                 if (=== $waitItem->getQuantity()) {
  2048.                     $order->removeWaitItem($waitItem);
  2049.                 }
  2050.                 $em->persist($order);
  2051.             }
  2052.         }
  2053.         $this->addFlash('notice'count($moveIds).(count($moveIds) > ' Wartelistenplätze verschoben' ' Wartelistenplatz verschoben'));
  2054.         $em->flush();
  2055.         return $this->redirectToRoute('course_reservations', ['id' => $course->getId()]);
  2056.     }
  2057.     /**
  2058.      * @Route("/{id}/participants-zoommembers", name="course_participants_zoommembers", methods="GET", requirements={"id"="\d+"})
  2059.      *
  2060.      * @IsGranted("ROLE_SPEAKER")
  2061.      */
  2062.     public function courseParticipantsZommMembers(
  2063.         Request $request,
  2064.         CourseOccurrence $courseOccurrence,
  2065.         OrderItemRepository $repo,
  2066.         ZoomService $zoomService,
  2067.         PersonRepository $personRepo,
  2068.     ) {
  2069.         //    $this->denyAccessUnlessGranted('client_allowed', $courseOccurrence);
  2070.         // Security: Check client ownership via CourseOccurrenceVoter
  2071.         $this->denyAccessUnlessGranted('view'$courseOccurrence);
  2072.         $orderItems $repo->findByCourseOccurrence($courseOccurrence'name''asc');
  2073.         foreach ($orderItems as $orderItem) {
  2074.             foreach ($orderItem->getParticipants() as $participant) {
  2075.                 //  dd($participant->getPerson());
  2076.                 if (null != $participant && null != $participant->getPerson()) {
  2077.                     if (null != $participant->getPerson()->getContactEmail()) {
  2078.                         $email $participant->getPerson()->getContactEmail();
  2079.                     } else {
  2080.                         $email $orderItem->getOrder()->getCustomerContactEmail();
  2081.                     }
  2082.                     $registrant $zoomService->addRegistrantToWebinar(
  2083.                         $this->getCurrentClient(),  // Client-Kontext für mandantenspezifische Zoom-Config
  2084.                         $orderItem->getCourseOccurrence()->getCode(),
  2085.                         $email,
  2086.                         $participant->getPerson()->getFirstname(),
  2087.                         $participant->getPerson()->getLastname()
  2088.                     );
  2089.                 }
  2090.             }
  2091.         }
  2092.         return $this->redirectToRoute('course_participants', ['id' => $courseOccurrence->getCourse()->getId()]);
  2093.     }
  2094.     /**
  2095.      * @Route("/{id}/participant-zoommember/{participant}", name="course_participant_zoommember", methods="GET", requirements={"id"="\d+"})
  2096.      *
  2097.      * @IsGranted("ROLE_SPEAKER")
  2098.      */
  2099.     public function courseParticipantZommMember(
  2100.         Request $request,
  2101.         CourseOccurrence $courseOccurrence,
  2102.         CourseOccurrenceRepository $courseOccurrenceRepo,
  2103.         ZoomService $zoomService,
  2104.         PersonRepository $personRepo,
  2105.     ) {
  2106.         //    $this->denyAccessUnlessGranted('client_allowed', $courseOccurrence);
  2107.         // Security: Check client ownership via CourseOccurrenceVoter
  2108.         $this->denyAccessUnlessGranted('view'$courseOccurrence);
  2109.         $participant $personRepo->find($request->get('participant'));
  2110.         $courseOccurrence $courseOccurrenceRepo->find($courseOccurrence->getId());
  2111.         if (null != $participant->getContactEmail()) {
  2112.             $email $participant->getContactEmail();
  2113.         } else {
  2114.             $email $participant->getFamilyMemberOf()->getContactEmail();
  2115.         }
  2116.         $registrant $zoomService->addRegistrantToWebinar(
  2117.             $courseOccurrence->getCode(),
  2118.             $email,
  2119.             $participant->getFirstname(),
  2120.             $participant->getLastname()
  2121.         );
  2122.         $this->addFlash('success''Teilnehmer wurde erfolgreich zu Zoom hinzugefügt.');
  2123.         return $this->redirectToRoute('course_participants', ['id' => $courseOccurrence->getCourse()->getId()]);
  2124.     }
  2125.     private function generateUniqueFileName()
  2126.     {
  2127.         return md5(uniqid());
  2128.     }
  2129.     private function createDescription($field$option)
  2130.     {
  2131.         switch ($option) {
  2132.             case 'course':
  2133.                 if (!empty($field['certificate'])) {
  2134.                     $field['name'] = $this->generateHTMLForDescription(
  2135.                         $field['name'],
  2136.                         'für den Kurs und das Zertifikat'
  2137.                     );
  2138.                 } else {
  2139.                     $field['name'] = $this->generateHTMLForDescription(
  2140.                         $field['name'],
  2141.                         'für den Kurs'
  2142.                     );
  2143.                 }
  2144.                 break;
  2145.             case 'certificate':
  2146.                 $field['name'] = $this->generateHTMLForDescription(
  2147.                     $field['name'],
  2148.                     'für das Zertifikat'
  2149.                 );
  2150.                 break;
  2151.             default:
  2152.                 break;
  2153.         }
  2154.         return $field;
  2155.     }
  2156.     private function generateHTMLForDescription($name$text)
  2157.     {
  2158.         return '<strong>'.$name.'</strong><span style="font-size: 0.7rem"> ('.$text.')</span>';
  2159.     }
  2160. }