#15 - maintainer email template
This commit was merged in pull request #20.
This commit is contained in:
@@ -6,6 +6,10 @@ The form definition is an array of element it contains. Each element has a *type
|
|||||||
|
|
||||||
Keep the form structure and content separate -> when working with the form, take time to figure, if you are working with structure, or existence of something within.
|
Keep the form structure and content separate -> when working with the form, take time to figure, if you are working with structure, or existence of something within.
|
||||||
|
|
||||||
|
## Rendering
|
||||||
|
|
||||||
|
The rendering currently happens on the backend, but that might be wrong. It renders out structure, but interactive components gets rendered on the frontend anyway.
|
||||||
|
|
||||||
## Submitting
|
## Submitting
|
||||||
|
|
||||||
When user submits the form, the `RsvFormSubmitter.js` is called. It collects the values, which we describe in more detail, and then sends it using `fetch` as POST to `reservations/forms/{form_id}`.
|
When user submits the form, the `RsvFormSubmitter.js` is called. It collects the values, which we describe in more detail, and then sends it using `fetch` as POST to `reservations/forms/{form_id}`.
|
||||||
|
|||||||
@@ -8,10 +8,12 @@ use Reservair\Templating\RsvTemplateEngine;
|
|||||||
*
|
*
|
||||||
* Two responsibilities:
|
* Two responsibilities:
|
||||||
* 1. Maintainer approval request — fires per timetable item that needs
|
* 1. Maintainer approval request — fires per timetable item that needs
|
||||||
* confirmation; template is hardcoded (not admin-configurable).
|
* confirmation, carrying the accept/refuse links the maintainer acts on.
|
||||||
* 2. User notification on form close — fires once per form submission when
|
* 2. User notification on form close — fires once per form submission when
|
||||||
* every reservation item reaches a terminal state. Subject/body come from
|
* every reservation item reaches a terminal state.
|
||||||
* the form definition's reservation element `email_templates` attr.
|
*
|
||||||
|
* Both subjects/bodies come from the reservation element's `email_templates`
|
||||||
|
* attr, falling back to the hardcoded defaults when the admin left them blank.
|
||||||
*
|
*
|
||||||
* Exceptions are swallowed so a mail failure never rolls back a transaction.
|
* Exceptions are swallowed so a mail failure never rolls back a transaction.
|
||||||
*/
|
*/
|
||||||
@@ -52,7 +54,15 @@ class RsvEmailListener {
|
|||||||
$start_dt = (clone $event->reservation->start_utc)->setTimezone($tz);
|
$start_dt = (clone $event->reservation->start_utc)->setTimezone($tz);
|
||||||
$end_dt = (clone $event->reservation->end_utc)->setTimezone($tz);
|
$end_dt = (clone $event->reservation->end_utc)->setTimezone($tz);
|
||||||
|
|
||||||
$body = $engine->render(self::DEFAULT_PENDING_BODY, [
|
[$tpl, $form_values] = self::resolve_maintainer_template($event);
|
||||||
|
|
||||||
|
// Treat blank (admin cleared the field) the same as "use the default".
|
||||||
|
$subject_tpl = trim((string) ($tpl['subject'] ?? '')) !== '' ? $tpl['subject'] : self::DEFAULT_PENDING_SUBJECT;
|
||||||
|
$body_tpl = trim((string) ($tpl['body'] ?? '')) !== '' ? $tpl['body'] : self::DEFAULT_PENDING_BODY;
|
||||||
|
|
||||||
|
// Reservation context wins over form values on key collisions, so the
|
||||||
|
// accept/refuse links can never be shadowed by a submitted field.
|
||||||
|
$symbols = array_merge($form_values, [
|
||||||
'reservation_id' => (string) $event->reservation_id,
|
'reservation_id' => (string) $event->reservation_id,
|
||||||
'date' => $start_dt->format('Y-m-d'),
|
'date' => $start_dt->format('Y-m-d'),
|
||||||
'start' => $start_dt->format('H:i'),
|
'start' => $start_dt->format('H:i'),
|
||||||
@@ -61,12 +71,59 @@ class RsvEmailListener {
|
|||||||
'refuse_url' => get_rest_url(null, 'reservations/v1/timetable-reservation/refuse/' . $event->code),
|
'refuse_url' => get_rest_url(null, 'reservations/v1/timetable-reservation/refuse/' . $event->code),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
(new RsvEmailSender())->send($event->maintainer_email, self::DEFAULT_PENDING_SUBJECT, $body);
|
$subject = sanitize_text_field($engine->render_plain($subject_tpl, $symbols));
|
||||||
|
$body = $engine->render($body_tpl, $symbols);
|
||||||
|
|
||||||
|
(new RsvEmailSender())->send($event->maintainer_email, $subject, $body);
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
Logger::error($e);
|
Logger::error($e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves the admin-authored maintainer template for the timetable this
|
||||||
|
* reservation belongs to, together with the submission's field values so the
|
||||||
|
* template can reference the submitter's answers.
|
||||||
|
*
|
||||||
|
* @return array{0: array<string, mixed>, 1: array<string, mixed>} The
|
||||||
|
* maintainer template (empty when none is configured) and form values.
|
||||||
|
*/
|
||||||
|
private static function resolve_maintainer_template(RsvTimetableReservationPendingEvent $event): array {
|
||||||
|
$form_submit_id = (new RsvReservationRepository())->get_form_submit_id($event->reservation_id);
|
||||||
|
if ($form_submit_id === null) {
|
||||||
|
return [[], []];
|
||||||
|
}
|
||||||
|
|
||||||
|
$form_submit = (new RsvFormSubmitRepository())->get($form_submit_id);
|
||||||
|
if ($form_submit === null) {
|
||||||
|
return [[], []];
|
||||||
|
}
|
||||||
|
|
||||||
|
$form_values = is_array($form_submit['values'] ?? null) ? $form_submit['values'] : [];
|
||||||
|
|
||||||
|
$definition = (new RsvFormDefinitionRepository())->get((int) $form_submit['form_id']);
|
||||||
|
if ($definition === null) {
|
||||||
|
return [[], $form_values];
|
||||||
|
}
|
||||||
|
|
||||||
|
$elements = $definition['definition']['elements'] ?? [];
|
||||||
|
$reservation_element = null;
|
||||||
|
foreach ($elements as $el) {
|
||||||
|
if (!is_array($el) || ($el['type'] ?? '') !== 'reservation') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$reservation_element ??= $el; // fall back to the first reservation element
|
||||||
|
if ((int) ($el['timetable_id'] ?? 0) === $event->reservation->timetable_id) {
|
||||||
|
$reservation_element = $el;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$template = $reservation_element['email_templates']['maintainer'] ?? [];
|
||||||
|
|
||||||
|
return [is_array($template) ? $template : [], $form_values];
|
||||||
|
}
|
||||||
|
|
||||||
public static function on_form_submit_closed(RsvFormSubmitClosedEvent $event): void {
|
public static function on_form_submit_closed(RsvFormSubmitClosedEvent $event): void {
|
||||||
try {
|
try {
|
||||||
$form_submit = (new RsvFormSubmitRepository())->get($event->form_submit_id);
|
$form_submit = (new RsvFormSubmitRepository())->get($event->form_submit_id);
|
||||||
|
|||||||
@@ -167,6 +167,8 @@ class RsvFormsPage extends RsvAdminPage {
|
|||||||
accepted_body: <?= json_encode("<h1>Vaše rezervace byla přijata</h1>\n<p>Vaše rezervace byla schválena. Těšíme se na vás!</p>") ?>,
|
accepted_body: <?= json_encode("<h1>Vaše rezervace byla přijata</h1>\n<p>Vaše rezervace byla schválena. Těšíme se na vás!</p>") ?>,
|
||||||
refused_subject: <?= json_encode('Rezervace zamítnuta') ?>,
|
refused_subject: <?= json_encode('Rezervace zamítnuta') ?>,
|
||||||
refused_body: <?= json_encode("<h1>Vaše rezervace byla zamítnuta</h1>\n<p>Vaše rezervace bohužel nebyla schválena. Zkuste prosím jiný termín.</p>") ?>,
|
refused_body: <?= json_encode("<h1>Vaše rezervace byla zamítnuta</h1>\n<p>Vaše rezervace bohužel nebyla schválena. Zkuste prosím jiný termín.</p>") ?>,
|
||||||
|
maintainer_subject: <?= json_encode('Nová rezervace čeká na schválení') ?>,
|
||||||
|
maintainer_body: <?= json_encode("<h1>Nový požadavek o rezervaci</h1>\n<p>Rezervace č. {{reservation_id}} čeká na vaše schválení.</p>\n<p><strong>Datum:</strong> {{date}}</p>\n<p><strong>Čas:</strong> {{start}} – {{end}}</p>\n<p><reservation-actions></reservation-actions></p>") ?>,
|
||||||
};
|
};
|
||||||
|
|
||||||
const rsv_elements_source = (function(initial_items, next_id_start) {
|
const rsv_elements_source = (function(initial_items, next_id_start) {
|
||||||
@@ -205,6 +207,10 @@ class RsvFormsPage extends RsvAdminPage {
|
|||||||
timetable_id: data.timetable_id ? parseInt(data.timetable_id) : null,
|
timetable_id: data.timetable_id ? parseInt(data.timetable_id) : null,
|
||||||
price_per_block: parseFloat(data.price_per_block ?? '0') || 0,
|
price_per_block: parseFloat(data.price_per_block ?? '0') || 0,
|
||||||
email_templates: {
|
email_templates: {
|
||||||
|
maintainer: {
|
||||||
|
subject: data.email_maintainer_subject ?? RSV_EMAIL_DEFAULTS.maintainer_subject,
|
||||||
|
body: data.email_maintainer_body ?? RSV_EMAIL_DEFAULTS.maintainer_body,
|
||||||
|
},
|
||||||
on_accepted: {
|
on_accepted: {
|
||||||
enabled: !!data.email_accepted_enabled,
|
enabled: !!data.email_accepted_enabled,
|
||||||
subject: data.email_accepted_subject ?? RSV_EMAIL_DEFAULTS.accepted_subject,
|
subject: data.email_accepted_subject ?? RSV_EMAIL_DEFAULTS.accepted_subject,
|
||||||
@@ -298,6 +304,7 @@ class RsvFormsPage extends RsvAdminPage {
|
|||||||
.input_checkbox('required', 'Required', data?.required ?? false);
|
.input_checkbox('required', 'Required', data?.required ?? false);
|
||||||
|
|
||||||
if ((data?.type ?? rsv_element_types[0]) === 'reservation') {
|
if ((data?.type ?? rsv_element_types[0]) === 'reservation') {
|
||||||
|
const maintainer = data?.email_templates?.maintainer ?? {};
|
||||||
const accepted = data?.email_templates?.on_accepted ?? {};
|
const accepted = data?.email_templates?.on_accepted ?? {};
|
||||||
const refused = data?.email_templates?.on_refused ?? {};
|
const refused = data?.email_templates?.on_refused ?? {};
|
||||||
const timetable_options = [
|
const timetable_options = [
|
||||||
@@ -307,6 +314,9 @@ class RsvFormsPage extends RsvAdminPage {
|
|||||||
builder
|
builder
|
||||||
.input_select('timetable_id', 'Timetable', timetable_options, data?.timetable_id ?? '')
|
.input_select('timetable_id', 'Timetable', timetable_options, data?.timetable_id ?? '')
|
||||||
.input_number('price_per_block', 'Price per block', data?.price_per_block ?? 0)
|
.input_number('price_per_block', 'Price per block', data?.price_per_block ?? 0)
|
||||||
|
.fieldset('Email - for maintainer', '100%')
|
||||||
|
.input_text('email_maintainer_subject', 'Subject', maintainer.subject ?? RSV_EMAIL_DEFAULTS.maintainer_subject)
|
||||||
|
.input_textarea('email_maintainer_body', 'Body', maintainer.body ?? RSV_EMAIL_DEFAULTS.maintainer_body)
|
||||||
.fieldset('Email — accepted', '100%')
|
.fieldset('Email — accepted', '100%')
|
||||||
.input_checkbox('email_accepted_enabled', 'Send email when accepted', accepted.enabled ?? true)
|
.input_checkbox('email_accepted_enabled', 'Send email when accepted', accepted.enabled ?? true)
|
||||||
.input_text('email_accepted_subject', 'Subject', accepted.subject ?? RSV_EMAIL_DEFAULTS.accepted_subject)
|
.input_text('email_accepted_subject', 'Subject', accepted.subject ?? RSV_EMAIL_DEFAULTS.accepted_subject)
|
||||||
|
|||||||
Reference in New Issue
Block a user