src/Controller/CourseController.php line 1033

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