<?php declare(strict_types=1); /* * This file is part of contao-weinanlieferung-bundle. * * (c) vonRotenberg * * @license commercial */ namespace vonRotenberg\WeinanlieferungBundle\Controller\Frontend\Ajax; use Contao\Controller; use Contao\CoreBundle\Controller\AbstractController; use Contao\CoreBundle\Exception\PageNotFoundException; use Contao\CoreBundle\Framework\ContaoFramework; use Contao\CoreBundle\Security\Authentication\Token\TokenChecker; use Contao\Environment; use Contao\File; use Contao\FilesModel; use Contao\FormFileUpload; use Contao\Frontend; use Contao\FrontendUser; use Contao\Input; use Contao\MemberModel; use Contao\StringUtil; use Contao\System; use Doctrine\DBAL\Connection; use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Annotation\Route; use Symfony\Contracts\Translation\TranslatorInterface; use vonRotenberg\WeinanlieferungBundle\Event\CheckInCompletedEvent; use vonRotenberg\WeinanlieferungBundle\Model\WeinanlieferungLageModel; use vonRotenberg\WeinanlieferungBundle\Model\WeinanlieferungLeseartModel; use vonRotenberg\WeinanlieferungBundle\Model\WeinanlieferungRebsorteModel; use vonRotenberg\WeinanlieferungBundle\Model\WeinanlieferungReservationModel; use vonRotenberg\WeinanlieferungBundle\Model\WeinanlieferungSlotsModel; use vonRotenberg\WeinanlieferungBundle\Model\WeinanlieferungUnitModel; /** * @Route("/_ajax/vr_wa/v1/slot", name="vr_wa_slot_ajax", defaults={"_scope" = "frontend", "_token_check" = false}) */ class SlotAjaxController extends AbstractController { private $tokenChecker; private $translator; private $framework; private $eventDispatcher; public function __construct( ContaoFramework $framework, TokenChecker $tokenChecker, TranslatorInterface $translator, EventDispatcherInterface $eventDispatcher ) { $this->framework = $framework; $this->tokenChecker = $tokenChecker; $this->translator = $translator; $this->eventDispatcher = $eventDispatcher; } public function __invoke(Request $request) { System::loadLanguageFile('default'); if (!$this->tokenChecker->hasFrontendUser()) { return $this->renderUnauthorized(); } if (empty($_REQUEST['do'])) { return new Response('Required parameter missing',412); } $blnModal = true; if (!empty($_REQUEST['modal'])) { $blnModal = !(strtolower($_REQUEST['modal']) == 'false'); } switch ($_REQUEST['do']) { case 'details': return $this->renderDetails($blnModal); case 'annotation': return $this->renderAnnotation($blnModal); case 'booking': return $this->renderBooking($blnModal); case 'reservate': return $this->reservate(); case 'updateReservation': return $this->updateReservation(); case 'delete': return $this->deleteReservation(); case 'checkin': return $this->renderCheckin($blnModal); case 'updateCheckin': return $this->updateCheckin(); case 'getAvailableNumbers': return $this->getAvailableNumbers(); case 'validateNumber': return $this->validateNumber(); } return new Response('',500); } protected function renderDetails(bool $blnModal=true,string $error=null) { $insertTagService = Controller::getContainer()->get('contao.insert_tag.parser'); if (empty($_REQUEST['id'])) { return new Response('Required parameter missing',412); } if (($Slot = WeinanlieferungSlotsModel::findPublishedById($_REQUEST['id'])) === null) { return new Response('Could not load slot data',500); } // Get slot reservations from user $arrReservations = []; if (($Reservations = WeinanlieferungReservationModel::findBy(["uid = ?","pid = ?"],[FrontendUser::getInstance()->id,$Slot->id])) !== null) { foreach ($Reservations as $reservation) { $arrSortenBooked = []; $SortenLeseart = explode(';',$reservation->sorten); foreach($SortenLeseart as $sorteLeseart) { list($sorte,$leseart) = explode(',',$sorteLeseart); $objSorte = WeinanlieferungRebsorteModel::findByPk($sorte); $objLeseart = WeinanlieferungLeseartModel::findByPk($leseart); $arrSortenBooked[$objSorte->id.','.$objLeseart->id] = ($objSorte !== null ? $objSorte->title : '') . ' ' . ($objLeseart !== null ? $objLeseart->title : ''); } /*if (($Sorten = $reservation->getRelated('sorten')) !== null) { $arrSortenBooked = $Sorten->fetchEach('title'); }*/ // Compute unit display fields for this reservation $unitTitle = $GLOBALS['TL_LANG']['MSC']['wa_unit_base'] ?? 'Behälter'; $unitAmountDisplay = (int) $reservation->behaelter; if ((int)$reservation->unit_id > 0) { $unitModel = WeinanlieferungUnitModel::findByPk((int)$reservation->unit_id); if (null !== $unitModel) { $unitTitle = (string)$unitModel->title; } $unitAmountDisplay = (int) ($reservation->unit_amount ?: 0); if ($unitAmountDisplay <= 0) { $mult = (int) ($unitModel ? $unitModel->multiplier : 0); $unitAmountDisplay = $mult > 0 ? max(1, (int) ($reservation->behaelter / $mult)) : (int) $reservation->behaelter; } } $arrReservations[] = array_merge($reservation->row(),[ 'sorten' => $arrSortenBooked, 'unit_title' => $unitTitle, 'unit_amount_display' => $unitAmountDisplay, ]); } } // Build data $arrSorten = []; $Sorten = StringUtil::deserialize($Slot->sorten,true); foreach($Sorten as $sorte) { $objSorte = WeinanlieferungRebsorteModel::findByPk($sorte['sorte']); $objLeseart = WeinanlieferungLeseartModel::findByPk($sorte['leseart']); $arrSorten[$objSorte->id.','.$objLeseart->id] = ($objSorte !== null ? $objSorte->title : '') . ' ' . ($objLeseart !== null ? $objLeseart->title : ''); } /*if (($Sorten = $Slot->getRelated('sorte')) !== null) { $arrSorten = array_combine($Sorten->fetchEach('id'),$Sorten->fetchEach('title')); }*/ $arrErnteart = []; if ($Slot->ernteart !== null) { foreach (explode(',', $Slot->ernteart) as $ernteart) { $arrErnteart[$ernteart] = $GLOBALS['TL_LANG']['REF']['wa_ernteart'][$ernteart] ?? $ernteart; } } $arrLage = []; if (($Lage = $Slot->getRelated('lage')) !== null) { $arrLage = array_combine($Lage->fetchEach('id'),$Lage->fetchEach('title')); } $intReservedBehaelter = $Slot->getReservedBehaelter(); $intAvailableBehaelter = max(0,$Slot->getAvailableBehaelter()); $arrData = [ 'modal' => $blnModal, 'id' => $Slot->id, 'slot' => array_merge($Slot->row(),[ 'anmerkungen' => $insertTagService->replace($Slot->anmerkungen ?? ''), 'sorte' => $arrSorten, 'behaelterAvailable' => $intAvailableBehaelter, 'behaelterBooked' => $Slot->getReservedBehaelter(), ]), 'standort' => $Slot->getRelated('pid'), 'lage' => $arrLage, 'ernteart' => $arrErnteart, 'buchen' => [ 'buchbar' => (boolean) ($Slot->behaelter*1.5 > $intReservedBehaelter), 'behaelter' => range(min($intAvailableBehaelter,1),$Slot->behaelter*1.5-$intReservedBehaelter), 'units' => $this->getAvailableUnitsForCapacity($intAvailableBehaelter), 'sorten' => $arrSorten, 'lage' => $arrLage, 'ernteart' => $arrErnteart, ], 'reservations' => $arrReservations ]; if (!empty($error)) { $arrData['toast'] = $error; } return $this->render('@Contao/modal_slot_details.html.twig',$arrData); } protected function renderAnnotation(bool $blnModal=true,string $error=null) { $insertTagService = Controller::getContainer()->get('contao.insert_tag.parser'); if (empty($_REQUEST['id'])) { return new Response('Required parameter missing',412); } if (($Slot = WeinanlieferungSlotsModel::findPublishedById($_REQUEST['id'])) === null) { return new Response('Could not load slot data',500); } // Build data $arrSorten = []; $Sorten = StringUtil::deserialize($Slot->sorten,true); foreach($Sorten as $sorte) { $objSorte = WeinanlieferungRebsorteModel::findByPk($sorte['sorte']); $objLeseart = WeinanlieferungLeseartModel::findByPk($sorte['leseart']); $arrSorten[$objSorte->id.','.$objLeseart->id] = ($objSorte !== null ? $objSorte->title : '') . ' ' . ($objLeseart !== null ? $objLeseart->title : ''); } $arrErnteart = []; if ($Slot->ernteart !== null) { foreach (explode(',', $Slot->ernteart) as $ernteart) { $arrErnteart[] = $GLOBALS['TL_LANG']['REF']['wa_ernteart'][$ernteart] ?? $ernteart; } } $arrLage = []; if (($Lage = $Slot->getRelated('lage')) !== null) { $arrLage = $Lage->fetchEach('title'); } $intAvailableBehaelter = max(0,$Slot->getAvailableBehaelter()); $arrData = [ 'modal' => $blnModal, 'id' => $Slot->id, 'slot' => array_merge($Slot->row(),[ 'anmerkungen' => $insertTagService->replace($Slot->anmerkungen ?? ''), 'sorte' => $arrSorten, 'behaelterAvailable' => $intAvailableBehaelter ]), 'standort' => $Slot->getRelated('pid'), 'lage' => $arrLage, 'ernteart' => $arrErnteart, 'buchen' => [ 'buchbar' => (boolean) $intAvailableBehaelter, 'behaelter' => range(min($intAvailableBehaelter,1),$intAvailableBehaelter), 'units' => $this->getAvailableUnitsForCapacity($intAvailableBehaelter), 'sorten' => $arrSorten ], ]; return $this->render('@Contao/modal_slot_annotation.html.twig',$arrData); } protected function renderBooking(bool $blnModal=true,string $error=null) { $insertTagService = Controller::getContainer()->get('contao.insert_tag.parser'); $arrData = []; if (empty($_REQUEST['id'])) { return new Response('Required parameter missing',412); } /** @var WeinanlieferungSlotsModel $Slot */ if (($Booking = WeinanlieferungReservationModel::findById($_REQUEST['id'])) === null || ($Slot = $Booking->getRelated('pid')) === null) { return new Response('Could not load booking data',500); } if ($Booking->approved === '0') { return $this->render('@Contao/modal_message.html.twig',['type'=>'danger','content'=>'Diese Buchungsanfrage wurde abgelehnt und kann nicht mehr geändert werden.']); } $objFile = FilesModel::findByUuid($Booking->upload); if (!empty($_REQUEST['deleteFile']) && $_REQUEST['deleteFile'] && $objFile !== null) { $File = new File($objFile->path); if ($File->delete()) { $objFile->delete(); $objFile = null; } } if (!empty($Booking->upload) && $objFile !== null) { $File = new File($objFile->path); $strHref = Environment::get('request'); // Remove an existing file parameter (see #5683) if (isset($_GET['file'])) { $strHref = preg_replace('/(&(amp;)?|\?)file=[^&]+/', '', $strHref); } $strHref .= (strpos($strHref, '?') !== false ? '&' : '?') . 'file=' . System::urlEncode($File->value); $arrData['file'] = [ 'link' => $strHref, 'filename' => $File->filename, 'extension' => $File->extension, 'name' => $File->name, 'path' => $File->dirname ]; } // Send the file to the browser (see #4632 and #8375) if ($objFile !== null && ($file = Input::get('file', true))) { if ($file == $objFile->path) { Controller::sendFileToBrowser($file); } } $arrSortenAvailable = []; $Sorten = StringUtil::deserialize($Slot->sorten,true); foreach($Sorten as $sorte) { $objSorte = WeinanlieferungRebsorteModel::findByPk($sorte['sorte']); $objLeseart = WeinanlieferungLeseartModel::findByPk($sorte['leseart']); $arrSortenAvailable[$objSorte->id.','.$objLeseart->id] = ($objSorte !== null ? $objSorte->title : '') . ' ' . ($objLeseart !== null ? $objLeseart->title : ''); } $arrSortenBooked = []; $SortenLeseart = explode(';',$Booking->sorten); foreach($SortenLeseart as $sorteLeseart) { list($sorte,$leseart) = explode(',',$sorteLeseart); $objSorte = WeinanlieferungRebsorteModel::findByPk($sorte); $objLeseart = WeinanlieferungLeseartModel::findByPk($leseart); $arrSortenBooked[$objSorte->id.','.$objLeseart->id] = ($objSorte !== null ? $objSorte->title : '') . ' ' . ($objLeseart !== null ? $objLeseart->title : ''); } $arrErnteartAvailable = []; if ($Slot->ernteart !== null) { foreach (explode(',', $Slot->ernteart) as $ernteart) { $arrErnteartAvailable[$ernteart] = $GLOBALS['TL_LANG']['REF']['wa_ernteart'][$ernteart] ?? $ernteart; } } $arrErnteartBooked = []; if ($Booking->ernteart !== null) { $arrErnteartBooked = explode(',', $Booking->ernteart); } $arrLagenAvailable = []; if (($Lagen = $Slot->getRelated('lage')) !== null) { foreach ($Lagen as $lage) { $arrLagenAvailable[$lage->id] = $lage->title; } } $arrLagenBooked = []; if ($Booking->lage !== null) { $arrLagenBooked = explode(',', $Booking->lage); } $intReservedBehaelter = $Slot->getReservedBehaelter(); // While editing, available base units should include the current booking's base units, // because the edit will override the former amount. $intAvailableBehaelter = max(0, $Slot->getAvailableBehaelter() + (int) $Booking->behaelter); $intOcTreshold = $intAvailableBehaelter - $intReservedBehaelter + $Slot->behaelter; $arrData = array_merge($arrData,[ 'modal' => $blnModal, 'id' => $Booking->id, 'slot' => array_merge($Slot->row(),[ 'anmerkungen' => $insertTagService->replace($Slot->anmerkungen ?? ''), 'sorte' => $arrSortenAvailable, 'behaelterAvailable' => $intAvailableBehaelter, 'behaelterOcThreshold' => $intOcTreshold, 'behaelterBooked' => $Slot->getReservedBehaelter(), ]), 'buchung' => array_merge($Booking->row(),[ 'sorten' => $arrSortenBooked, 'ernteart' => $arrErnteartBooked, 'lage' => $arrLagenBooked, // display fields 'unit_title' => (function() use ($Booking) { if ((int)$Booking->unit_id > 0) { $m = WeinanlieferungUnitModel::findByPk((int)$Booking->unit_id); return $m ? (string)$m->title : ($GLOBALS['TL_LANG']['MSC']['wa_unit_base'] ?? 'Behälter'); } return $GLOBALS['TL_LANG']['MSC']['wa_unit_base'] ?? 'Behälter'; })(), 'unit_amount_display' => (function() use ($Booking) { if ((int)$Booking->unit_id > 0) { $m = WeinanlieferungUnitModel::findByPk((int)$Booking->unit_id); $amount = (int) ($Booking->unit_amount ?: 0); if ($amount > 0) return $amount; $mult = (int) ($m ? $m->multiplier : 0); return $mult > 0 ? max(1, (int) ($Booking->behaelter / $mult)) : (int) $Booking->behaelter; } return (int) $Booking->behaelter; })(), ]), 'standort' => $Slot->getRelated('pid'), 'buchen' => [ 'buchbar' => (boolean) $intAvailableBehaelter, 'behaelter' => range(min($intAvailableBehaelter,1),$Slot->behaelter*1.5-$Slot->getReservedBehaelter()+$Booking->behaelter), 'units' => $this->getAvailableUnitsForCapacity($intAvailableBehaelter), 'sorten' => $arrSortenAvailable, 'ernteart' => $arrErnteartAvailable, 'lage' => $arrLagenAvailable, ] ]); if (!empty($error)) { $arrData['toast'] = $error; } return $this->render('@Contao/modal_booking_details.html.twig',$arrData); } protected function reservate() { Controller::loadDataContainer('tl_vr_wa_reservation'); $arrData = []; if (($rootPage = Frontend::getRootPageFromUrl()) !== null && !empty($rootPage->vr_wa_uploadFolderSRC)) { $File = new FormFileUpload(\Contao\Widget::getAttributesFromDca($GLOBALS['TL_DCA']['tl_vr_wa_reservation']['fields']['upload'], 'upload')); $File->storeFile = true; $File->doNotOverwrite = true; $File->uploadFolder = $rootPage->vr_wa_uploadFolderSRC; $File->validate(); if ($File->hasErrors()) { return $this->renderDetails(false,'<div class="toast toast--danger mx-0">' . $File->getErrorAsHTML() . '</div>'); } if (!empty($_SESSION['FILES'][$File->name])) { $arrData['filename'] = $_SESSION['FILES'][$File->name]['name'] ?? ''; $arrData['upload'] = $_SESSION['FILES'][$File->name]['uuid'] ? StringUtil::uuidToBin($_SESSION['FILES'][$File->name]['uuid']) : null; } } if (empty($_REQUEST['id'])) { return new Response('Missing parameter',412); } // recompute the base units $unitId = (int) Input::post('unit_id'); $unitAmount = (int) Input::post('unit_amount'); $multiplier = 1; if ($unitId > 0) { if (($u = WeinanlieferungUnitModel::findByPk($unitId)) !== null) { $multiplier = max(1, (int) $u->multiplier); } } $intBehaelter = $unitAmount > 0 ? $unitAmount * $multiplier : (int) Input::post('behaelter'); // Form validation and unit capacity if (($Slot = WeinanlieferungSlotsModel::findByPk($_REQUEST['id'])) !== null) { if ($intBehaelter > $Slot->behaelter*1.5 - $Slot->getReservedBehaelter()) { return $this->renderDetails(false,sprintf('<div class="toast toast--danger mx-0">Fehler: Es sind mittlerweile nur noch %s Behälter verfügbar.</div>',$Slot->getAvailableBehaelter())); } } $arrError = []; foreach (['sorten','ernteart','lage'] as $field) { if (empty(Input::post($field))) { $arrError = [$field]; } } // Require either (unit selection (allowing 0) with unit_amount, or behaelter fallback) $postedUnitId = Input::post('unit_id'); $hasUnitId = ($postedUnitId !== null && $postedUnitId !== '' && $postedUnitId !== false); if (!$hasUnitId || empty(Input::post('unit_amount'))) { if (empty(Input::post('behaelter'))) { $arrError[] = 'behaelter'; } } if (count($arrError)) { return $this->renderDetails(false,'<div class="toast toast--danger mx-0">Bitte geben Sie alle Pflichtangaben (mit * markierte Felder) an</div>'); } $arrSorten = []; if (!is_array(Input::post('sorten'))) { $arrSorten[] = Input::post('sorten'); } else { $arrSorten = implode(';', Input::post('sorten')); } $arrErnteart = []; if (!is_array(Input::post('ernteart'))) { $arrErnteart[] = Input::post('ernteart'); } else { $arrErnteart = implode(',', Input::post('ernteart')); } $arrLage = []; if (!is_array(Input::post('lage'))) { $arrLage[] = Input::post('lage'); } else { $arrLage = implode(',', Input::post('lage')); } $Reservation = new WeinanlieferungReservationModel(); $time = time(); if ($intBehaelter > $Slot->behaelter - $Slot->getReservedBehaelter()) { $arrData['approved'] = ''; $arrData['approved_on'] = 0; } else { $arrData['approved'] = '1'; $arrData['approved_on'] = $time; } // Determine fields to store $arrData = array_merge($arrData,[ 'pid' => $_REQUEST['id'], 'tstamp' => $time, 'uid' => FrontendUser::getInstance()->id, 'behaelter' => $intBehaelter, 'unit_id' => $unitId, 'unit_amount' => $unitAmount, 'sorten' => $arrSorten, 'ernteart' => $arrErnteart, 'lage' => $arrLage ]); $Reservation->setRow($arrData); $Reservation->save(); if (empty($Reservation->approved)) { return new Response('<div class="toast toast--warning mx-0"><p>Wir haben Ihre Anfrage erhalten. Bitte warten Sie auf eine Freigabe durch uns.</p></div>',200,['HX-Trigger'=> 'updateWaBooking']); } return new Response('<div class="toast toast--success mx-0"><p>Reservierung erfolgreich</p></div>',200,['HX-Trigger'=> 'updateWaList']); } protected function updateReservation() { Controller::loadDataContainer('tl_vr_wa_reservation'); if (empty($_REQUEST['id'])) { return new Response('Missing parameter',412); } if (($Reservation = WeinanlieferungReservationModel::findById($_REQUEST['id'])) === null) { return new Response('Could not load booking data',500); } if (FrontendUser::getInstance()->id != $Reservation->uid) { return new Response('Member not authorized tu update this reservation',500); } if (($rootPage = Frontend::getRootPageFromUrl()) !== null && !empty($rootPage->vr_wa_uploadFolderSRC)) { $File = new FormFileUpload(\Contao\Widget::getAttributesFromDca($GLOBALS['TL_DCA']['tl_vr_wa_reservation']['fields']['upload'], 'upload')); $File->storeFile = true; $File->doNotOverwrite = true; $File->uploadFolder = $rootPage->vr_wa_uploadFolderSRC; $File->validate(); if ($File->hasErrors()) { return $this->renderBooking(false,'<div class="toast toast--danger mx-0">' . $File->getErrorAsHTML() . '</div>'); } if (!empty($_SESSION['FILES'][$File->name])) { $Reservation->filename = $_SESSION['FILES'][$File->name]['name'] ?? ''; $Reservation->upload = $_SESSION['FILES'][$File->name]['uuid'] ? StringUtil::uuidToBin($_SESSION['FILES'][$File->name]['uuid']) : null; } } // recompute base units $unitId = (int) Input::post('unit_id'); $unitAmount = (int) Input::post('unit_amount'); $multiplier = 1; if ($unitId > 0) { if (($u = WeinanlieferungUnitModel::findByPk($unitId)) !== null) { $multiplier = max(1, (int) $u->multiplier); } } $intBehaelter = $unitAmount > 0 ? $unitAmount * $multiplier : (int) Input::post('behaelter'); // Form validation /** @var WeinanlieferungSlotsModel $Slot */ if (($Slot = $Reservation->getRelated('pid')) !== null) { if ($intBehaelter > $Slot->behaelter*1.5 - $Slot->getReservedBehaelter() + $Reservation->behaelter) { return $this->renderBooking(false,sprintf('<div class="toast toast--danger mx-0">Fehler: Es sind mittlerweile nur noch %s Behälter verfügbar.</div>',$Slot->getAvailableBehaelter()+$Reservation->behaelter)); } } $arrError = []; foreach (['sorten','ernteart','lage'] as $field) { if (empty(Input::post($field))) { $arrError = [$field]; } } $postedUnitId = Input::post('unit_id'); $hasUnitId = ($postedUnitId !== null && $postedUnitId !== '' && $postedUnitId !== false); if (!$hasUnitId || empty(Input::post('unit_amount'))) { if (empty(Input::post('behaelter'))) { $arrError[] = 'behaelter'; } } if (count($arrError)) { return $this->renderBooking(false,'<div class="toast toast--danger mx-0">Bitte geben Sie alle Pflichtangaben (mit * markierte Felder) an</div>'); } $arrSorten = []; if (!is_array(Input::post('sorten'))) { $arrSorten[] = Input::post('sorten'); } else { $arrSorten = implode(';', Input::post('sorten')); } $arrErnteart = []; if (!is_array(Input::post('ernteart'))) { $arrErnteart[] = Input::post('ernteart'); } else { $arrErnteart = implode(',', Input::post('ernteart')); } $arrLage = []; if (!is_array(Input::post('lage'))) { $arrLage[] = Input::post('lage'); } else { $arrLage = implode(',', Input::post('lage')); } $time = time(); if ($intBehaelter > $Slot->behaelter - $Slot->getReservedBehaelter() + $Reservation->behaelter) { $Reservation->approved = ''; $Reservation->approved_on = 0; } else { $Reservation->approved = '1'; $Reservation->approved_on = $time; } $Reservation->tstamp = $time; $Reservation->behaelter = $intBehaelter; $Reservation->unit_id = $unitId; $Reservation->unit_amount = $unitAmount; $Reservation->sorten = $arrSorten; $Reservation->ernteart = $arrErnteart; $Reservation->lage = $arrLage; $Reservation->save(); if (empty($Reservation->approved)) { return new Response('<div class="toast toast--warning mx-0"><p>Wir haben Ihre Anfrage erhalten. Bitte warten Sie auf eine Freigabe durch uns.</p></div>',200,['HX-Trigger'=> 'updateWaBooking']); } return new Response('<div class="toast toast--success mx-0"><p>Reservierung erfolgreich geändert</p></div>',200,['HX-Trigger'=> 'updateWaBooking']); } protected function deleteReservation() { if (empty($_REQUEST['id'])) { return new Response('Missing parameter',412); } /** @var Connection $db */ $db = Controller::getContainer()->get('database_connection'); $arrCriteria = [ 'uid' => FrontendUser::getInstance()->id, 'id' => $_REQUEST['id'] ]; if ($db->delete('tl_vr_wa_reservation',$arrCriteria)) { return new Response(null,203,['HX-Trigger'=> 'updateWaBooking']); } return new Response('Could not delete',500); } protected function renderUnauthorized() { return $this->render('@Contao/modal_unauthorized.html.twig'); } protected function renderCheckin(bool $blnModal=true, string $error=null, array $formData=null) { $insertTagService = Controller::getContainer()->get('contao.insert_tag.parser'); $arrData = []; if (empty($_REQUEST['id'])) { return new Response('Required parameter missing', 412); } /** @var WeinanlieferungReservationModel $Booking */ if (($Booking = WeinanlieferungReservationModel::findById($_REQUEST['id'])) === null || ($Slot = $Booking->getRelated('pid')) === null) { return new Response('Could not load booking data', 500); } if ($Booking->approved === '0') { return $this->render('@Contao/modal_message.html.twig', ['type'=>'danger', 'content'=>'Diese Buchungsanfrage wurde abgelehnt und kann nicht eingecheckt werden.']); } if ($Booking->checked_in === '1') { return $this->render('@Contao/modal_message.html.twig', ['type'=>'info', 'content'=>'Diese Buchung wurde bereits eingecheckt.']); } // Get the standort to access the number_ranges $Standort = $Slot->getRelated('pid'); if ($Standort === null) { return new Response('Could not load standort data', 500); } // Prepare data for the template $arrSortenBooked = []; $SortenLeseart = explode(';', $Booking->sorten); foreach($SortenLeseart as $sorteLeseart) { list($sorte, $leseart) = explode(',', $sorteLeseart); $objSorte = WeinanlieferungRebsorteModel::findByPk($sorte); $objLeseart = WeinanlieferungLeseartModel::findByPk($leseart); $arrSortenBooked[$objSorte->id.','.$objLeseart->id] = ($objSorte !== null ? $objSorte->title : '') . ' ' . ($objLeseart !== null ? $objLeseart->title : ''); } $arrErnteartBooked = []; if ($Booking->ernteart !== null) { foreach (explode(',', $Booking->ernteart) as $ernteart) { $arrErnteartBooked[] = $GLOBALS['TL_LANG']['REF']['wa_ernteart'][$ernteart] ?? $ernteart; } } $arrLagenBooked = []; if ($Booking->lage !== null) { if (($Lagen = $Booking->getRelated('lage')) !== null) { $arrLagenBooked = $Lagen->fetchEach('title'); } } // Load the member model for the booking's user $memberModel = MemberModel::findById($Booking->uid); $currentMemberModel = MemberModel::findById(FrontendUser::getInstance()->id); $arrData = array_merge($arrData, [ 'modal' => $blnModal, 'id' => $Booking->id, 'slot' => array_merge($Slot->row(), [ 'anmerkungen' => $insertTagService->replace($Slot->anmerkungen ?? ''), ]), 'buchung' => array_merge($Booking->row(), [ 'sorten' => $arrSortenBooked, 'ernteart' => $arrErnteartBooked, 'lage' => $arrLagenBooked, ]), 'standort' => $Standort, 'checkin' => [ 'behaelter' => $Booking->behaelter, 'expected' => ($Booking->unit_amount ?? 0) > 0 ? (int)$Booking->unit_amount : (int)$Booking->behaelter, 'unit_title' => (function() use ($Booking) { if ((int)$Booking->unit_id > 0) { $m = WeinanlieferungUnitModel::findByPk((int)$Booking->unit_id); return $m ? (string)$m->title : ($GLOBALS['TL_LANG']['MSC']['wa_unit_base'] ?? 'Behälter'); } return $GLOBALS['TL_LANG']['MSC']['wa_unit_base'] ?? 'Behälter'; })(), 'unit_amount_display' => (function() use ($Booking) { if ((int)$Booking->unit_id > 0) { $m = WeinanlieferungUnitModel::findByPk((int)$Booking->unit_id); $amount = (int) ($Booking->unit_amount ?: 0); if ($amount > 0) return $amount; $mult = (int) ($m ? $m->multiplier : 0); return $mult > 0 ? max(1, (int) ($Booking->behaelter / $mult)) : (int) $Booking->behaelter; } return (int) $Booking->behaelter; })(), ], 'member' => $memberModel ? $memberModel->row() : null, 'current_member' => $currentMemberModel ? $currentMemberModel->row() : null ]); // Add form data if provided (to preserve values after validation errors) if ($formData !== null) { $arrData['form_data'] = $formData; } if (!empty($error)) { $arrData['toast'] = $error; } return $this->render('@Contao/modal_checkin.html.twig', $arrData); } protected function updateCheckin() { if (empty($_REQUEST['id'])) { return new Response('Missing parameter', 412); } /** @var WeinanlieferungReservationModel $Booking */ if (($Booking = WeinanlieferungReservationModel::findById($_REQUEST['id'])) === null) { return new Response('Could not load booking data', 500); } // Validate that we have the correct number of behaelter numbers $behaelterNumbers = Input::post('behaelter_numbers'); $expectedCount = ($Booking->unit_amount ?? 0) > 0 ? (int)$Booking->unit_amount : (int)$Booking->behaelter; if (!is_array($behaelterNumbers) || count($behaelterNumbers) != $expectedCount) { // Prepare form data to preserve input values $formData = [ 'behaelter_numbers' => $behaelterNumbers ?: [], 'member_numbers' => Input::post('member_numbers') ?: [] ]; return $this->renderCheckin(false, '<div class="toast toast--danger mx-0">Bitte wählen Sie für jede Einheit eine Nummer aus.</div>', $formData); } // Get member numbers from the form $memberNumbers = Input::post('member_numbers'); // If member numbers are not provided or not an array, initialize an empty array if (!is_array($memberNumbers) || count($memberNumbers) != $Booking->behaelter) { $memberNumbers = array_fill(0, count($behaelterNumbers), ''); } // Get the current member's number to use as fallback $currentMember = MemberModel::findById(FrontendUser::getInstance()->id); $currentMemberNo = $currentMember ? $currentMember->memberno : ''; // Filter out the special value 9999 ("Nummer nicht bekannt") for duplicate check $numbersForDuplicateCheck = array_filter($behaelterNumbers, function($number) { return $number !== '9999'; }); // Check for duplicate numbers (excluding the special value 9999) if (count(array_unique($numbersForDuplicateCheck)) != count($numbersForDuplicateCheck)) { // Prepare form data to preserve input values $formData = [ 'behaelter_numbers' => $behaelterNumbers, 'member_numbers' => $memberNumbers ]; return $this->renderCheckin(false, '<div class="toast toast--danger mx-0">Jede Nummer kann nur einmal verwendet werden.</div>', $formData); } // Validate all numbers on the server side as a final check $invalidNumbers = []; $currentTime = time(); $Slot = $Booking->getRelated('pid'); $Standort = $Slot->getRelated('pid'); // Get all used numbers from current bookings (excluding past bookings) $usedNumbers = []; // Get the database connection $db = Controller::getContainer()->get('database_connection'); // Query to get used numbers from current bookings $sql = "SELECT r.behaelter_numbers FROM tl_vr_wa_reservation r JOIN tl_vr_wa_slot s ON r.pid = s.id WHERE r.behaelter_numbers != '' AND s.time >= ? AND r.id != ? AND s.pid = ?"; // Only check for the same standort $stmt = $db->prepare($sql); $stmt->bindValue(1, $currentTime); $stmt->bindValue(2, $Booking->id); $stmt->bindValue(3, $Standort->id); $result = $stmt->executeQuery(); while ($row = $result->fetchAssociative()) { $numbers = json_decode($row['behaelter_numbers'], true); if (is_array($numbers)) { foreach ($numbers as $item) { $usedNumbers[] = isset($item['behaelter']) ? $item['behaelter'] : $item; } } } // Check each number foreach ($behaelterNumbers as $index => $number) { // Skip the special value 9999 if ($number === '9999') { continue; } // Check if the number is numeric if (!is_numeric($number)) { $invalidNumbers[] = "Behälter " . ($index + 1) . ": Die eingegebene Nummer ist keine gültige Zahl."; continue; } // Check if the number is already in use if (in_array($number, $usedNumbers)) { $invalidNumbers[] = "Behälter " . ($index + 1) . ": Diese Nummer wird bereits in einer anderen aktiven Buchung verwendet."; continue; } // Check if the number is within the valid ranges for this standort $validRanges = $Standort->extractNumbersFromRanges([], 10000); // Get all possible numbers if (!in_array($number, $validRanges) && !empty($validRanges)) { $invalidNumbers[] = "Behälter " . ($index + 1) . ": Die eingegebene Nummer liegt nicht im gültigen Bereich für diesen Standort."; } } // If there are invalid numbers, return an error if (!empty($invalidNumbers)) { $errorMessage = '<div class="toast toast--danger mx-0"><p>Folgende Fehler wurden gefunden:</p><ul>'; foreach ($invalidNumbers as $error) { $errorMessage .= '<li>' . $error . '</li>'; } $errorMessage .= '</ul></div>'; // Prepare form data to preserve input values $formData = [ 'behaelter_numbers' => $behaelterNumbers, 'member_numbers' => $memberNumbers, 'invalid_fields' => array_map(function($error) { // Extract the behälter number from the error message if (preg_match('/Behälter (\d+):/', $error, $matches)) { return (int)$matches[1] - 1; // Convert to zero-based index } return null; }, $invalidNumbers) ]; return $this->renderCheckin(false, $errorMessage, $formData); } // Create combined array with behaelter numbers and member numbers $combinedData = []; foreach ($behaelterNumbers as $index => $behaelterNumber) { $memberNumber = !empty($memberNumbers[$index]) ? $memberNumbers[$index] : $currentMemberNo; $combinedData[] = [ 'behaelter' => $behaelterNumber, 'member' => $memberNumber ]; } // Save the check-in data $time = time(); $Booking->checked_in = '1'; $Booking->checked_in_on = $time; $Booking->behaelter_numbers = json_encode($combinedData); $Booking->tstamp = $time; $Booking->save(); // Create reservation data array for the event $reservationData = $Booking->row(); // Dispatch the check-in completed event $event = new CheckInCompletedEvent($reservationData, $Booking); $this->eventDispatcher->dispatch($event, CheckInCompletedEvent::NAME); return new Response('<div class="toast toast--success mx-0"><p>Check-in erfolgreich durchgeführt</p></div>', 200, ['HX-Trigger'=> 'updateWaBooking']); } protected function getAvailableNumbers() { if (empty($_REQUEST['id'])) { return new Response('Required parameter missing', 412); } /** @var WeinanlieferungReservationModel $Booking */ if (($Booking = WeinanlieferungReservationModel::findById($_REQUEST['id'])) === null || ($Slot = $Booking->getRelated('pid')) === null) { return new Response('Could not load booking data', 500); } if ($Booking->approved === '0') { return new Response(json_encode(['error' => 'Diese Buchungsanfrage wurde abgelehnt und kann nicht eingecheckt werden.']), 400, ['Content-Type' => 'application/json']); } if ($Booking->checked_in === '1') { return new Response(json_encode(['error' => 'Diese Buchung wurde bereits eingecheckt.']), 400, ['Content-Type' => 'application/json']); } // Get the standort to access the number_ranges $Standort = $Slot->getRelated('pid'); if ($Standort === null) { return new Response(json_encode(['error' => 'Could not load standort data']), 500, ['Content-Type' => 'application/json']); } // Get all used numbers from current bookings (excluding past bookings) $usedNumbers = []; $currentTime = time(); // Get the database connection $db = Controller::getContainer()->get('database_connection'); // More efficient query to get used numbers from current bookings $sql = "SELECT r.behaelter_numbers FROM tl_vr_wa_reservation r JOIN tl_vr_wa_slot s ON r.pid = s.id WHERE r.behaelter_numbers != '' AND s.time >= ? AND r.id != ?"; $stmt = $db->prepare($sql); $stmt->bindValue(1, $currentTime); $stmt->bindValue(2, $Booking->id); $result = $stmt->executeQuery(); while ($row = $result->fetchAssociative()) { $numbers = json_decode($row['behaelter_numbers'], true); if (is_array($numbers)) { foreach ($numbers as $number) { $usedNumbers[] = isset($number['behaelter']) ? $number['behaelter'] : $number; } } } // Get a batch of available numbers // We'll limit to a reasonable number to improve performance $limit = 100; // Adjust this based on your needs if (!empty($_REQUEST['limit']) && is_numeric($_REQUEST['limit'])) { $limit = (int)$_REQUEST['limit']; } // Get available numbers directly, excluding used ones $availableNumbers = $Standort->extractNumbersFromRanges($usedNumbers, $limit); // Add the special option "Nummer nicht bekannt" with value 9999 // This option should always be available and can be used multiple times array_unshift($availableNumbers, '9999'); // Return the numbers as JSON return new Response(json_encode(['numbers' => $availableNumbers]), 200, ['Content-Type' => 'application/json']); } protected function validateNumber() { if (empty($_REQUEST['id']) || !isset($_REQUEST['number'])) { return new Response(json_encode(['valid' => false, 'message' => 'Required parameters missing']), 412, ['Content-Type' => 'application/json']); } $number = $_REQUEST['number']; // Special case: "Nummer nicht bekannt" is always valid if ($number === '9999') { return new Response(json_encode(['valid' => true]), 200, ['Content-Type' => 'application/json']); } // Check if the number is a valid number format if (!is_numeric($number)) { return new Response(json_encode([ 'valid' => false, 'message' => 'Die eingegebene Nummer ist keine gültige Zahl.' ]), 200, ['Content-Type' => 'application/json']); } /** @var WeinanlieferungReservationModel $Booking */ if (($Booking = WeinanlieferungReservationModel::findById($_REQUEST['id'])) === null || ($Slot = $Booking->getRelated('pid')) === null) { return new Response(json_encode(['valid' => false, 'message' => 'Could not load booking data']), 500, ['Content-Type' => 'application/json']); } // Get the standort to access the number_ranges $Standort = $Slot->getRelated('pid'); if ($Standort === null) { return new Response(json_encode(['valid' => false, 'message' => 'Could not load standort data']), 500, ['Content-Type' => 'application/json']); } // Get all used numbers from current bookings (excluding past bookings) $usedNumbers = []; $currentTime = time(); // Get the database connection $db = Controller::getContainer()->get('database_connection'); // Query to get used numbers from current bookings $sql = "SELECT r.behaelter_numbers FROM tl_vr_wa_reservation r JOIN tl_vr_wa_slot s ON r.pid = s.id WHERE r.behaelter_numbers != '' AND s.time >= ? AND r.id != ? AND s.pid = ?"; // Only check for the same standort $stmt = $db->prepare($sql); $stmt->bindValue(1, $currentTime); $stmt->bindValue(2, $Booking->id); $stmt->bindValue(3, $Standort->id); $result = $stmt->executeQuery(); while ($row = $result->fetchAssociative()) { $numbers = json_decode($row['behaelter_numbers'], true); if (is_array($numbers)) { foreach ($numbers as $item) { $usedNumbers[] = isset($item['behaelter']) ? $item['behaelter'] : $item; } } } // Check if the number is already in use if (in_array($number, $usedNumbers)) { return new Response(json_encode([ 'valid' => false, 'message' => 'Diese Nummer wird bereits in einer anderen aktiven Buchung verwendet.' ]), 200, ['Content-Type' => 'application/json']); } // Check if the number is within the valid ranges for this standort $validRanges = $Standort->extractNumbersFromRanges([], 10000); // Get all possible numbers if (!in_array($number, $validRanges) && !empty($validRanges)) { return new Response(json_encode([ 'valid' => false, 'message' => 'Die eingegebene Nummer liegt nicht im gültigen Bereich für diesen Standort.' ]), 200, ['Content-Type' => 'application/json']); } // If we got here, the number is valid return new Response(json_encode(['valid' => true]), 200, ['Content-Type' => 'application/json']); } protected function getAvailableUnitsForCapacity(int $availableBaseUnits): array { // Always include the base unit (Behälter) with multiplier 1 $units = []; $units[] = [ 'id' => 0, 'title' => $GLOBALS['TL_LANG']['MSC']['wa_unit_base'] ?? 'Behälter', 'multiplier' => 1, 'max_amount' => $availableBaseUnits, // one check-in per base unit ]; // Load custom units from DB if (($all = WeinanlieferungUnitModel::findAll()) !== null) { foreach ($all as $unit) { $mult = (int)($unit->multiplier ?? 0); if ($mult < 1) { continue; // skip invalid } // A unit fits if at least one of it can be placed into remaining capacity $maxAmount = intdiv(max(0, $availableBaseUnits), $mult); if ($maxAmount < 1) { continue; // does not fit current capacity } $units[] = [ 'id' => (int)$unit->id, 'title' => (string)$unit->title, 'multiplier' => $mult, 'max_amount' => $maxAmount, ]; } } return $units; } }