src/Controller/CourseController.php line 1021

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