comsa/sulu-reservations

Reservation bundle for Sulu

3.1.3 2022-04-27 17:00 UTC

README

How it works

What is this bundle?

  • Reservation system
  • Integerated with Sulu FormBundle

The bundles makes use of reservables that you can create. These can be linked to reservable options that you can create as well. The bundle supports single payment and payments per person. Each reservable can have different price groups that have their own price (e.g. adults => €5, children => €3). Payments are configured inside the settings tab of the module. There are 3 types supported which can have their own extra cost (bank, cash or mollie).

Installation

Add to assets/admin/package.json:

"sulu-reservations-bundle": "file:node_modules/@sulu/vendor/comsa/sulu-reservations/Resources/js"

Add to assets/website/package.json:

"sulu-reservations-bundle": "file:../../vendor/comsa/sulu-reservations/Resources/js",

Run npm install in both

Add it to assets/admin/index.js:

import 'sulu-reservations-bundle/admin'

Add it to assets/website/index.js:

import 'sulu-reservations-bundle/website'

And build both it using npm run build, this might take a while :)

Add routes to both routes_admin.yaml and routes_website.yaml

In: config/routes_admin.yaml

sulu_reservations_admin:
  type: rest
  resource: "@SuluReservationsBundle/Resources/config/routes/admin.yaml"
  prefix: /admin/reservations/api

In: config/routes_website.yaml

sulu_reservations_website:
  resource: "@SuluReservationsBundle/Resources/config/routes/website.yaml"
  prefix: /sulu-reservations

Create the following parameter in config/services.yaml and give the correct value:

    comsa_sulu_reservations_mollie_api_key: '%env(COMSA_RE_MOLLIE_API_KEY)%'

Create a template named comsa_reservable (this key is used to generate pages when adding a reservable) and add the following field:

<property name="reservable" type="single_reservable_selection">
    <meta>
        <title lang="en">Reservable</title>
        <title lang="nl">Reserveerbaar Item</title>
    </meta>
</property>

Add the following code in config/services.yaml.

sulu_form.request_listener:
    class: App\EventListener\RequestListener
    arguments:
        - '@sulu_form.builder'
        - '@sulu_form.handler'
        - '@sulu_form.configuration.form_configuration_factory'
        - '@event_dispatcher'
        - '@router.default'
    tags:
        - { name: 'kernel.event_listener', event: 'kernel.request', method: 'onKernelRequest' }

This code overwrites the standard RequestListener from the sulu/form-bundle. Create the following file:

<?php

declare(strict_types=1);

namespace App\EventListener;

use Sulu\Bundle\FormBundle\Configuration\FormConfigurationFactory;
use Sulu\Bundle\FormBundle\Entity\Dynamic;
use Sulu\Bundle\FormBundle\Event\DynFormSavedEvent;
use Sulu\Bundle\FormBundle\Form\BuilderInterface;
use Sulu\Bundle\FormBundle\Form\HandlerInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\Routing\RouterInterface;

class RequestListener
{
    protected BuilderInterface $formBuilder;
    protected HandlerInterface $formHandler;
    protected FormConfigurationFactory $formConfigurationFactory;
    protected EventDispatcherInterface $eventDispatcher;
    protected RouterInterface $router;

    /**
     * RequestListener constructor.
     */
    public function __construct(
        BuilderInterface $formBuilder,
        HandlerInterface $formHandler,
        FormConfigurationFactory $formConfigurationFactory,
        EventDispatcherInterface $eventDispatcher,
        RouterInterface $router
    ) {
        $this->formBuilder = $formBuilder;
        $this->formHandler = $formHandler;
        $this->formConfigurationFactory = $formConfigurationFactory;
        $this->eventDispatcher = $eventDispatcher;
        $this->router = $router;
    }

    public function onKernelRequest(RequestEvent $event): void
    {
        if (!$event->isMasterRequest()) {
            // do nothing if it's not the master request
            return;
        }

        $request = $event->getRequest();

        if (!$request->isMethod('post')) {
            // do nothing if it's not a post request
            return;
        }

        try {
            $form = $this->formBuilder->buildByRequest($request);

            if (!$form || !$form->isSubmitted() || !$form->isValid()) {
                // do nothing when no form was found or not valid
                return;
            }
        } catch (\Exception $e) {
            // Catch all exception on build form by request
            return;
        }

        /** @var Dynamic $dynamic */
        $dynamic = $form->getData();
        $configuration = $this->formConfigurationFactory->buildByDynamic($dynamic);
        $dynamic->setLocale($request->getLocale()); // Need to be set to request locale for shadow pages, configuraiton will hold the original locale


        if ($this->formHandler->handle($form, $configuration)) {

            if (!$request->request->get("reservable")) {
                $serializedObject = $dynamic->getForm()->serializeForLocale($dynamic->getLocale(), $dynamic);
                $dynFormSavedEvent = new DynFormSavedEvent($serializedObject, $dynamic);
                $this->eventDispatcher->dispatch($dynFormSavedEvent, DynFormSavedEvent::NAME);
                $response = new RedirectResponse('?send=true');
            } else {
                $response = new RedirectResponse($this->router->generate("comsa_sulu_reservations_select_payment_method"));
            }

            $event->setResponse($response);
        }
    }
}

Add the following code to the twig file to render the reservation form written in Vue.js

{% extends "base.html.twig" %}

{% block content %}
  {% include '@SuluReservations/reservable.html.twig' %}
{% endblock %}

The following is example code to create a reservable-overview:

XML:

<?xml version="1.0" ?>
<type name="reservable_overview" xmlns="http://schemas.sulu.io/template/template"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xmlns:xi="http://www.w3.org/2001/XInclude"
      xsi:schemaLocation="http://schemas.sulu.io/template/template http://schemas.sulu.io/template/template-1.0.xsd">
    <meta>
        <title lang="en">Reservable overzicht</title>
        <title lang="nl">Reserveerbare items overzicht</title>
    </meta>
    <properties>
        <property name="title" type="text_line">
            <meta>
                <title lang="en">Title</title>
            </meta>
        </property>

        <property name="reservables" type="smart_content">
            <meta>
                <title lang="en">Reservable overview</title>
                <title lang="en">Reserveerbare items overzicht</title>
            </meta>
            <params>
                <param name="provider" value="pages"/>
                <param name="properties" type="collection">
                    <param name="reservable" value="reservable"/>
                </param>
            </params>
        </property>
        <xi:include href="includes/width.xml"/>
    </properties>
</type>

HTML:

<section class="container reservables">
  <h2>{{ block.title }}</h2>
  <ul class="row">
    <table class="d-none d-md-block table table-hover">
      <tr>
        <th>Evenement</th>
        <th>Beschikbare plaatsen</th>
        <th>Start op</th>
        <th>Eindigt op</th>
        <th></th>
      </tr>
      {% for page in block.reservables %}
        {% set spacesLeft = get_spaces_left(page.reservable.id) %}
        <tr>
          <td>{{ page.title }}</td>
          <td>{{ spacesLeft }}</td>
          <td>{{ page.reservable.start|date("d/m/Y") }}</td>
          <td>{{ page.reservable.end|date("d/m/Y") }}</td>
          <td>
            {% if spacesLeft == 0 %}
              <button type="button" class="btn btn-danger">VOLZET</button>
            {% else %}
              <a href="{{ sulu_content_path(page.url) }}" class="btn btn-primary">RESERVEER NU</a>
            {% endif %}
          </td>
        </tr>
      {% endfor %}
    </table>
    {% for page in block.reservables %}
      <li class="col-12 d-md-none">
        <div class="reservable-wrapper">
          <h3>{{ page.title }}</h3>
          <div class="limit">
            {% set spacesLeft = get_spaces_left(page.reservable.id) %}
            <i class="fas fa-user {% if spacesLeft < 5 %} text-danger {% endif %}"></i><span {% if spacesLeft < 5 %} class="text-danger" {% endif %}>{{ spacesLeft }}</span>
          </div>
          <div class="button-wrapper">
            {% if spacesLeft == 0 %}
              <button type="button" class="btn btn-danger">VOLZET</button>
            {% else %}
              <a href="{{ sulu_content_path(page.url) }}" class="btn btn-primary">RESERVEER NU</a>
            {% endif %}
          </div>
        </div>
      </li>
    {% endfor %}
  </ul>
</section>

SCSS:

.reservables {
  margin-left: 0;
  margin-top: 2rem;
  padding: 0;
  width: 100%;

  h3 {
    color: $black;
  }

  ul {
    list-style-type: none;
    padding: 0;
  }

  .reservable-wrapper {
    align-items: center;
    border: solid 1px $gray-500;
    display: flex;
    justify-content: center;
    margin-bottom: 2rem;
    min-height: 14rem;
    padding: 1.5rem;
    width: 100%;

    .button-wrapper {
      bottom: 0;
      position: absolute;
      transform: translateY(-50%);
    }

    .limit {
      border: 1px solid $gray-500;
      border-right: 0;
      border-top: 0;
      min-height: 2rem;
      min-width: 3rem;
      position: absolute;
      right: 0;
      text-align: center;
      top: 0;
      transform: translateX(-35%);
    }

    svg {
      margin-right: 0.5rem;
    }
  }
}

In config/packages/doctrine.yaml:

doctrine:
  orm:
    mappings:
      SuluReservationsBundle:
        is_bundle: true
        type: attribute
        dir: 'Entity'
        prefix: 'Comsa\SuluReservations\Entity'
        alias: SuluReservations

Update your database to add the needed tables using php bin/console doctrine:schema:update -f

Load default settings:

<?php

namespace App\DataFixtures;

use Comsa\SuluReservations\DataFixtures\AppSeed;
use Doctrine\Bundle\FixturesBundle\Fixture;
use Doctrine\Common\DataFixtures\DependentFixtureInterface;
use Doctrine\Common\Persistence\ObjectManager;

class AppFixtures extends Fixture implements DependentFixtureInterface
{
    public function load(ObjectManager $manager)
    {

    }

    public function getDependencies()
    {
        return[
          AppSeed::class
        ];
    }

}

Copy and paste this into the DataFixtures of your app. Run Symfony console doctrine:fixtures:load --append