comsa / sulu-reservations
Reservation bundle for Sulu
Requires
- php: ^8.0
- dantleech/phpcr-migrations-bundle: ^1.2
- doctrine/orm: >=2.0
- mollie/mollie-api-php: ^2.0
- phpoffice/phpspreadsheet: ^1.21
- sulu/form-bundle: ^2.2.1
- sulu/sulu: ^2.4
- symfony/config: ^4.3 || ^5.0
- symfony/dependency-injection: ^4.3 || ^5.0
- symfony/event-dispatcher: ^4.3 || ^5.0
- symfony/form: ^4.3 || ^5.0
- symfony/http-foundation: ^4.3 || ^5.0
- symfony/http-kernel: ^4.3 || ^5.0
- symfony/mailer: ^4.3 || ^5.0
- twig/twig: >=2.0 || ^3.0
Requires (Dev)
- jackalope/jackalope-doctrine-dbal: ^1.3.4
- jackalope/jackalope-jackrabbit: ^1.3
- phpcr/phpcr-shell: ^1.1
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