<?php
declare(strict_types=1);
/*
* This file is part of contao-weinanlieferung-bundle.
*
* (c) vonRotenberg
*
* @license commercial
*/
namespace vonRotenberg\WeinanlieferungBundle\Controller\Backend;
use Contao\Ajax;
use Contao\Backend;
use Contao\BackendTemplate;
use Contao\CheckBox;
use Contao\Config;
use Contao\Controller;
use Contao\CoreBundle\Controller\AbstractController;
use Contao\CoreBundle\Csrf\ContaoCsrfTokenManager;
use Contao\Date;
use Contao\DC_File;
use Contao\DC_Table;
use Contao\Environment;
use Contao\FileTree;
use Contao\FrontendUser;
use Contao\Image;
use Contao\Input;
use Contao\Message;
use Contao\SelectMenu;
use Contao\StringUtil;
use Contao\System;
use Contao\TextArea;
use Contao\TextField;
use Contao\Upload;
use Contao\Widget;
use Doctrine\DBAL\Connection;
use MenAtWork\MultiColumnWizardBundle\Contao\Widgets\MultiColumnWizard;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Contracts\Translation\TranslatorInterface;
use Twig\Environment as TwigEnvironment;
use vonRotenberg\WeinanlieferungBundle\Model\WeinanlieferungLeseartModel;
use vonRotenberg\WeinanlieferungBundle\Model\WeinanlieferungRebsorteModel;
use vonRotenberg\WeinanlieferungBundle\Model\WeinanlieferungReservationModel;
use vonRotenberg\WeinanlieferungBundle\Model\WeinanlieferungSlotsModel;
use vonRotenberg\WeinanlieferungBundle\Model\WeinanlieferungSlottypesModel;
/**
* @Route("%contao.backend.route_prefix%/weinanlieferung/slotassistant", name=self::class, defaults={"_scope" = "backend"})
*/
class WeinanlieferungSlotAssistantController extends AbstractController
{
private $twig;
private $tokenManager;
private $request;
private $db;
private $translator;
protected $fields;
public function __construct(TwigEnvironment $twig, ContaoCsrfTokenManager $tokenManager, RequestStack $requestStack, Connection $db, TranslatorInterface $translator)
{
$this->twig = $twig;
$this->tokenManager = $tokenManager;
$this->request = $requestStack->getCurrentRequest();
$this->db = $db;
$this->translator = $translator;
$container = System::getContainer();
$objSession = $container->get('session');
$strKey = Input::get('popup') ? 'popupReferer' : 'referer';
$strRefererId = $this->request->attributes->get('_contao_referer_id');
$session = $objSession->get($strKey);
$session[$strRefererId]['current'] = substr(Environment::get('requestUri'), \strlen(Environment::get('path')) + 1);
$objSession->set($strKey, $session);
Controller::loadDataContainer('tl_vr_wa_slot');
System::loadLanguageFile('tl_vr_wa_slot');
}
public function __invoke(): Response
{
System::loadLanguageFile('default');
System::loadLanguageFile('tl_vr_wa_slotassistant');
Controller::loadDataContainer('tl_vr_wa_slot');
Controller::loadDataContainer('tl_vr_wa_slotassistant');
$GLOBALS['TL_CSS']['cirrus'] = 'bundles/vonrotenbergweinanlieferung/css/backend.css|static';
$arrData = [
'request_token' => $this->tokenManager->getDefaultTokenValue(),
'ref' => $this->request->attributes->get('_contao_referer_id')
];
// Handle ajax request pre actions
if ($_POST && Environment::get('isAjaxRequest'))
{
$this->objAjax = new Ajax(Input::post('action'));
$this->objAjax->executePreActions();
}
// Form submitted
if (Input::post('FORM_SUBMIT') == 'wa_slotassistant')
{
$this->generateSlots();
}
$arrData['fields'] = $this->getWidgets();
$arrData['messages'] = Message::generateUnwrapped() . Backend::getSystemMessages();
// Handle ajax request post actions
if ($_POST && Environment::get('isAjaxRequest'))
{
$dc = new DC_File('tl_vr_wa_slotassistant');
$this->objAjax->executePostActions($dc);
}
return new Response(
$this->twig->render(
'@Contao_VonrotenbergWeinanlieferungBundle/be_wa_slotassistant.html.twig',
$arrData
)
);
}
protected function getWidgets()
{
if (!empty($this->fields))
{
return $this->fields;
}
$arrWidgets = [
'base_legend' => [
[
'class' => 'w50',
'help' => 'Legen Sie hier den Zeitslot-Typ/Annahmestelle fest.',
'widget' => new SelectMenu([
'name' => 'slotType',
'mandatory' => true,
'options' => $this->getSlottypeOptions(),
'label' => 'Zeitslot-Typ',
])
],
[
'class' => 'w50',
'help' => 'Die Pufferzeit zwischen den Slots.',
'widget' => new TextField([
'name' => 'buffer',
'mandatory' => true,
'label' => 'Pufferzeit in min.',
'rgxp' => 'natural'
])
]
],
'time_legend' => [
/*[
'class' => 'w50 wizard',
'help' => 'Der Zeitpunkt ab wann der erste Zeitslot angelegt werden soll.',
'widget' => new TextField([
'name' => 'date_start[1]',
'mandatory' => true,
'label' => 'Zeitraum Beginn',
'rgxp' => 'datim',
'datepicker' => true,
])
],
[
'class' => 'w50 wizard',
'help' => 'Der Zeitpunkt bis wann der letzte Zeitslot abgeschlossen sein muss.',
'widget' => new TextField([
'name' => 'date_end[1]',
'mandatory' => true,
'label' => 'Zeitraum Ende',
'rgxp' => 'datim',
'datepicker' => true,
])
],*/
[
'class' => 'clr',
'help' => 'Die Zeiträume, für die neue Slots angelegt werden sollen.',
'widget' => new MultiColumnWizard([
'name' => 'timeframe',
'label' => 'Zeiträume',
'minCount' => 1,
'columnFields' =>
[
'date_start' =>
[
'label' => $GLOBALS['TL_LANG']['tl_vr_wa_slotassistant']['date_start'],
'inputType' => 'text',
'eval' => [
'style' => 'width:150px',
'mandatory' => true,
'rgxp' =>'datim',
'datepicker' => true
]
],
'date_end' =>
[
'label' => $GLOBALS['TL_LANG']['tl_vr_wa_slotassistant']['date_end'],
'inputType' => 'text',
'eval' => [
'style' => 'width:150px',
'mandatory' => true,
'rgxp' =>'datim',
'datepicker' => true
]
]
]
])
]
],
'slot_legend' => [
/*[
'class' => 'clr',
'help' => 'Wählen Sie die Attribute aus, die für diesen Slot gelten sollen.',
'widget' => new CheckBox([
'name' => 'attributes',
'mandatory' => false,
'label' => 'Attribute',
'options_callback' => array('vonRotenberg\WeinanlieferungBundle\EventListener\DataContainer\WeinanlieferungSlotContainerListener', 'getAttributeOptions'),
])
],*/
[
'class' => 'clr',
'help' => 'Wählen Sie die Attribute aus, die für diesen Slot gelten sollen.',
'widget' => new CheckBox(CheckBox::getAttributesFromDca($GLOBALS['TL_DCA']['tl_vr_wa_slot']['fields']['attributes'],'attributes',null,'Attribute','tl_vr_wa_slot',new DC_Table('tl_vr_wa_slot')))
],
[
'class' => 'w50 wizard',
'help' => 'Der Beginn des Zeitraums, ab wann der Slot im Vorfeld gebucht werden kann.',
'widget' => new TextField([
'name' => 'bookableFrom',
'mandatory' => true,
'label' => 'Buchbar ab',
'rgxp' => 'datim',
'datepicker' => true
])
],
[
'class' => 'w50',
'help' => 'Das Ende des Zeitraums, bis wann der Slot im Vorfeld gebucht werden kann.',
'widget' => new TextField([
'name' => 'bookableTill',
'mandatory' => true,
'label' => 'Buchbar bis in h',
'rgxp' => 'natural'
])
],
[
'class' => 'clr',
'help' => 'Anmerkungen zum Zeitslot. Diese werden bei der Buchung angezeigt.',
'widget' => new TextArea([
'name' => 'annotation',
'mandatory' => false,
'label' => 'Anmerkungen',
'rte' => 'tinyMCE'
])
],
[
'class' => 'clr',
'help' => 'Anmerkungen zum Zeitslot. Diese werden bei der Buchung angezeigt.',
'widget' => new FileTree([
'name' => 'enclosure',
'mandatory' => false,
'label' => 'Anmerkungen',
'multiple' => true,
'fieldType' => 'checkbox',
'filesOnly' => true,
'isDownloads' => true
])
]
]
];
foreach ($arrWidgets as $group => $fields)
{
foreach ($fields as $field)
{
if (empty($field['widget']->id))
{
$field['widget']->id = $field['widget']->name;
}
if (empty($field['widget']->table))
{
$field['widget']->strTable = 'tl_vr_wa_slotassistant';
}
$wizard = '';
// Date picker
if ($field['widget']->datepicker ?? null)
{
$rgxp = $field['widget']->rgxp ?? 'date';
$format = Date::formatToJs(Config::get($rgxp . 'Format'));
switch ($rgxp)
{
case 'datim':
$time = ",\n timePicker: true";
break;
case 'time':
$time = ",\n pickOnly: \"time\"";
break;
default:
$time = '';
break;
}
$strOnSelect = '';
// Trigger the auto-submit function (see #8603)
if ($field['widget']->submitOnChange ?? null)
{
$strOnSelect = ",\n onSelect: function() { Backend.autoSubmit(\"wa_slotassistant\"); }";
}
$wizard .= ' ' . Image::getHtml('assets/datepicker/images/icon.svg', '', 'title="' . StringUtil::specialchars($GLOBALS['TL_LANG']['MSC']['datepicker']) . '" id="toggle_' . $field['widget']->id . '" style="cursor:pointer"') . '
<script>
window.addEvent("domready", function() {
new Picker.Date($("ctrl_' . $field['widget']->id . '"), {
draggable: false,
toggle: $("toggle_' . $field['widget']->id . '"),
format: "' . $format . '",
positionOffset: {x:-211,y:-209}' . $time . ',
pickerClass: "datepicker_bootstrap",
useFadeInOut: !Browser.ie' . $strOnSelect . ',
startDay: ' . $GLOBALS['TL_LANG']['MSC']['weekOffset'] . ',
titleFormat: "' . $GLOBALS['TL_LANG']['MSC']['titleFormat'] . '"
});
});
</script>';
$field['widget']->wizard = $wizard;
}
// Replace the textarea with an RTE instance
if (!empty($field['widget']->rte))
{
list($file, $type) = explode('|', $field['widget']->rte, 2) + [null, null];
$fileBrowserTypes = [];
$pickerBuilder = System::getContainer()->get('contao.picker.builder');
foreach (['file' => 'image', 'link' => 'file'] as $context => $fileBrowserType)
{
if ($pickerBuilder->supportsContext($context))
{
$fileBrowserTypes[] = $fileBrowserType;
}
}
$objTemplate = new BackendTemplate('be_' . $file);
$objTemplate->selector = 'ctrl_' . $field['widget']->id;
$objTemplate->type = $type;
$objTemplate->fileBrowserTypes = $fileBrowserTypes;
$objTemplate->source = 'wa_slotassistant.' . $field['widget']->id;
$objTemplate->readonly = (bool)($field['widget']->readonly ?? false);
// Deprecated since Contao 4.0, to be removed in Contao 5.0
$objTemplate->language = Backend::getTinyMceLanguage();
$field['widget']->updateMode = $objTemplate->parse();
unset($file, $type, $pickerBuilder, $fileBrowserTypes, $fileBrowserType);
}
}
}
$this->fields = $arrWidgets;
return $this->fields;
}
protected function getSlottypeOptions()
{
$arrOptions = [['value'=>'','label'=>'-']];
$Slottypes = $this->db->prepare("SELECT t.id, t.title, t.containers, t.duration, t.`default`, s.title as 'standort' FROM tl_vr_wa_slottypes t INNER JOIN tl_vr_wa_standort s ON s.id = t.pid ORDER BY t.`default` DESC, t.title")->executeQuery();
$arrOptGroups = [];
if ($Slottypes->rowCount() > 0)
{
foreach ($Slottypes->fetchAllAssociative() as $option)
{
$arrOptGroups[$option['standort']][] = [
'value' => $option['id'],
'label' => $option['title'].' ['.$this->translator->trans('MSC.wa_container', [], 'contao_tl_vr_wa_units').': '.$option['containers'].'] ['.$this->translator->trans('MSC.wa_duration', [], 'contao_tl_vr_wa_units').': '.$option['duration'].' '.($option['duration'] == 1 ? $this->translator->trans('MSC.wa_duration_minute', [], 'contao_tl_vr_wa_units') : $this->translator->trans('MSC.wa_duration_minutes', [], 'contao_tl_vr_wa_units')).'] ['.$option['standort'].']' . ($option['default'] ? ' (Standard)' : '')
];
}
}
ksort($arrOptGroups);
$arrOptions = array_merge($arrOptions, $arrOptGroups);
return $arrOptions;
}
protected function validateForm()
{
$blnSubmit = false;
if (Input::post('FORM_SUBMIT') == 'wa_slotassistant')
{
$blnSubmit = true;
foreach ($this->getWidgets() as $group => $fields)
{
foreach ($fields as $field)
{
$field['widget']->validate();
if ($field['widget']->hasErrors())
{
$blnSubmit = false;
}
}
}
}
return $blnSubmit;
}
protected function generateSlots()
{
if (!Input::post('FORM_SUBMIT') == 'wa_slotassistant' || !$this->validateForm())
{
return;
}
if (
($SlotType = WeinanlieferungSlottypesModel::findByPk(Input::post('slotType'))) === null
|| ($Standort = $SlotType->getRelated('pid')) === null
|| !\is_array(Input::post('timeframe'))
)
{
return;
}
$now = time();
$intNewSlots = 0;
$intBuffer = intval(Input::post('buffer')) * 60;
$intDuration = (int) $SlotType->duration*60;
// Get timespans
foreach (Input::post('timeframe') as $timeframe)
{
if (
!isset($timeframe['date_start'])
|| !isset($timeframe['date_end'])
)
{
continue;
}
$StartDate = new Date($timeframe['date_start'], Date::getNumericDatimFormat());
$EndDate = new Date($timeframe['date_end'], Date::getNumericDatimFormat());
$intStart = $StartDate->tstamp;
$intEnd = $EndDate->tstamp;
$arrTimes = [];
$arrStartTimes = [];
$arrEndTimes = [];
// Get existing slots in the timeframe
$ExistingSlots = $this->db->prepare("SELECT id, date, time as 'start', (time+duration*60-1) as 'end', duration FROM tl_vr_wa_slot WHERE pid = ? AND (time >= ? OR time+$intDuration+$intBuffer >= ?) AND (time <= ? OR time+$intDuration+$intBuffer <= ?) ORDER BY time")
->executeQuery([$Standort->id, $intStart, $intStart, $intEnd, $intEnd]);
if ($ExistingSlots->rowCount())
{
foreach ($ExistingSlots->fetchAllAssociative() as $slot)
{
$arrTimes[$slot['start']] = [
'start' => $slot['start'],
'end' => $slot['end']
];
$arrStartTimes[] = $slot['start'];
$arrEndTimes[] = $slot['end'];
}
}
// Create all possible timeslots for given timeframe
$intTime = $intStart;
while ($intTime <= $intEnd-$intDuration)
{
$arrConflicts = [];
foreach ($arrStartTimes as $check_start)
{
$checkslot = $arrTimes[$check_start];
if (
(
$intTime >= $checkslot['start']
&& $intTime <= $checkslot['end']
)
|| (
$intTime + $intDuration + $intBuffer > $checkslot['start']
&& $intTime + $intDuration + $intBuffer < $checkslot['end']
)
) {
$arrConflicts[] = $checkslot['end'];
}
}
if (\count($arrConflicts) > 0)
{
$intTime = max($arrConflicts) + (max($intBuffer,1));
continue;
}
$NewDateTime = new Date($intTime);
$NewSlot = new WeinanlieferungSlotsModel();
$bookableFrom = new Date(Input::post('bookableFrom'), Date::getNumericDatimFormat());
$NewSlot->setRow([
'pid' => $SlotType->pid,
'tstamp' => $now,
'date' => $NewDateTime->dayBegin,
'time' => $intTime,
'duration' => $intDuration/60,
'behaelter' => $SlotType->containers,
'overcapacity' => $SlotType->overcapacity,
'anmerkungen' => Input::post('annotation'),
'attributes' => \implode(',',\is_array(Input::post('attributes')) ? Input::post('attributes') : []),
'buchbar_ab' => (int) $bookableFrom->tstamp,
'buchbar_bis' => (int) $intTime-intval(Input::post('bookableTill'))*3600,
'published' => '1'
]);
if (Input::post('enclosure'))
{
$NewSlot->addEnclosure = '1';
$NewSlot->enclosure = explode(',',Input::post('enclosure'));
}
$NewSlot->save();
$intNewSlots++;
$intTime = $intTime + $intDuration + $intBuffer;
}
}
Message::addConfirmation(sprintf('Es wurden %s neue Zeitslots angelegt.',$intNewSlots));
Backend::reload();
}
}