This commit is contained in:
Mork Swork 2026-05-13 15:26:33 -07:00
parent 0a2e80f7b0
commit 9454dc03b4
5 changed files with 142 additions and 33 deletions

View file

@ -47,15 +47,30 @@
panelSlots.innerHTML = '';
arg.allSegs.forEach(function (seg) {
const li = document.createElement('li');
const start = seg.event.start.toLocaleTimeString(undefined, { hour: 'numeric', minute: '2-digit' });
const end = seg.event.end.toLocaleTimeString(undefined, { hour: 'numeric', minute: '2-digit' });
const params = new URLSearchParams({
const startLabel = seg.event.start.toLocaleTimeString(undefined, { hour: 'numeric', minute: '2-digit' });
const endLabel = seg.event.end.toLocaleTimeString(undefined, { hour: 'numeric', minute: '2-digit' });
const a = document.createElement('a');
a.href = '#';
a.textContent = startLabel + ' ' + endLabel;
a.addEventListener('click', function (e) {
e.preventDefault();
fetch(drupalSettings.riversidePt.storeSlotUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
start: seg.event.startStr,
end: seg.event.endStr,
}),
}).then(function (res) {
if (res.ok) {
window.location.href = drupalSettings.riversidePt.bookingUrl;
} else {
a.textContent += ' (no longer available)';
a.style.pointerEvents = 'none';
a.style.opacity = '0.5';
}
});
});
const a = document.createElement('a');
a.href = drupalSettings.riversidePt.bookingUrl + '?' + params.toString();
a.textContent = start + ' ' + end;
li.appendChild(a);
panelSlots.appendChild(li);
});

View file

@ -1,5 +1,17 @@
<?php
use Drupal\Core\Breadcrumb\Breadcrumb;
use Drupal\Core\Link;
use Drupal\Core\Routing\RouteMatchInterface;
function riverside_pt_system_breadcrumb_alter(Breadcrumb &$breadcrumb, RouteMatchInterface $route_match, array $context): void {
if ($route_match->getRouteName() === 'riverside_pt.booking') {
$breadcrumb = new Breadcrumb();
$breadcrumb->addLink(Link::createFromRoute('← Back', 'riverside_pt.schedule'));
$breadcrumb->addCacheContexts(['route']);
}
}
function riverside_pt_mail(string $key, array &$message, array $params): void {
if ($key !== 'booking_request') {
return;
@ -9,9 +21,15 @@ function riverside_pt_mail(string $key, array &$message, array $params): void {
$end = new \DateTime($params['end']);
$message['subject'] = 'Booking request — ' . $start->format('M j, Y g:i A');
$message['body'][] = implode("\n", [
$lines = [
'Name: ' . $params['first_name'] . ' ' . $params['last_name'],
'Phone: ' . $params['phone'],
'Slot: ' . $start->format('l, F j, Y') . ', ' . $start->format('g:i A') . '' . $end->format('g:i A'),
]);
];
if (!empty($params['comments'])) {
$lines[] = 'Comments: ' . $params['comments'];
}
$message['body'][] = implode("\n", $lines);
}

View file

@ -14,6 +14,14 @@ riverside_pt.booking:
requirements:
_permission: 'access content'
riverside_pt.booking_store_slot:
path: '/schedule/book/slot'
defaults:
_controller: '\Drupal\riverside_pt\Controller\ScheduleController::storeSlot'
requirements:
_permission: 'access content'
methods: [POST]
riverside_pt.schedule_events:
path: '/schedule/events'
defaults:

View file

@ -3,12 +3,25 @@
namespace Drupal\riverside_pt\Controller;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\TempStore\PrivateTempStore;
use Drupal\Core\TempStore\PrivateTempStoreFactory;
use Drupal\Core\Url;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
class ScheduleController extends ControllerBase {
private PrivateTempStore $tempStore;
public function __construct(PrivateTempStoreFactory $tempStoreFactory) {
$this->tempStore = $tempStoreFactory->get('riverside_pt');
}
public static function create(ContainerInterface $container): static {
return new static($container->get('tempstore.private'));
}
public function page(): array {
return [
'#type' => 'container',
@ -62,6 +75,7 @@ class ScheduleController extends ControllerBase {
'riversidePt' => [
'eventsUrl' => Url::fromRoute('riverside_pt.schedule_events')->toString(),
'bookingUrl' => Url::fromRoute('riverside_pt.booking')->toString(),
'storeSlotUrl' => Url::fromRoute('riverside_pt.booking_store_slot')->toString(),
'holidays' => $this->buildHolidaysMap(),
],
],
@ -78,6 +92,23 @@ class ScheduleController extends ControllerBase {
return $map;
}
public function storeSlot(Request $request): JsonResponse {
$data = json_decode($request->getContent(), TRUE) ?? [];
$start = $data['start'] ?? '';
if (!$start || new \DateTime($start) < new \DateTime()) {
return new JsonResponse(['error' => 'past'], 422);
}
$this->tempStore->set('booking_slot', [
'start' => $start,
'end' => $data['end'] ?? '',
'provider_id' => $data['provider_id'] ?? '',
]);
return new JsonResponse(['ok' => TRUE]);
}
public function events(Request $request): JsonResponse {
$start = $request->query->get('start');
$end = $request->query->get('end');

View file

@ -6,26 +6,29 @@ use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Mail\MailManagerInterface;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\TempStore\PrivateTempStore;
use Drupal\Core\TempStore\PrivateTempStoreFactory;
use Drupal\user\Entity\User;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\RequestStack;
class BookingForm extends FormBase {
private PrivateTempStore $tempStore;
public function __construct(
private readonly MailManagerInterface $mailManager,
ConfigFactoryInterface $configFactory,
RequestStack $requestStack,
PrivateTempStoreFactory $tempStoreFactory,
) {
$this->configFactory = $configFactory;
$this->requestStack = $requestStack;
$this->tempStore = $tempStoreFactory->get('riverside_pt');
}
public static function create(ContainerInterface $container): static {
return new static(
$container->get('plugin.manager.mail'),
$container->get('config.factory'),
$container->get('request_stack'),
$container->get('tempstore.private'),
);
}
@ -34,10 +37,10 @@ class BookingForm extends FormBase {
}
public function buildForm(array $form, FormStateInterface $form_state): array {
$query = $this->requestStack->getCurrentRequest()->query;
$start = $query->get('start', '');
$end = $query->get('end', '');
$uid = $query->get('provider', '');
$slot = $this->tempStore->get('booking_slot') ?? [];
$start = $slot['start'] ?? '';
$end = $slot['end'] ?? '';
$uid = $slot['provider_id'] ?? '';
$slot_display = '';
if ($start && $end) {
@ -46,6 +49,8 @@ class BookingForm extends FormBase {
$slot_display = $s->format('l, F j, Y') . ', ' . $s->format('g:i A') . '' . $e->format('g:i A');
}
$form['#cache'] = ['max-age' => 0];
$form['slot_summary'] = [
'#type' => 'item',
'#title' => $this->t('Appointment'),
@ -60,10 +65,6 @@ class BookingForm extends FormBase {
];
}
$form['start'] = ['#type' => 'hidden', '#value' => $start];
$form['end'] = ['#type' => 'hidden', '#value' => $end];
$form['provider_id'] = ['#type' => 'hidden', '#value' => $uid];
$form['first_name'] = [
'#type' => 'textfield',
'#title' => $this->t('First name'),
@ -82,6 +83,12 @@ class BookingForm extends FormBase {
'#required' => TRUE,
];
$form['comments'] = [
'#type' => 'textarea',
'#title' => $this->t('Comments'),
'#rows' => 4,
];
$form['actions'] = ['#type' => 'actions'];
$form['actions']['submit'] = [
'#type' => 'submit',
@ -91,19 +98,49 @@ class BookingForm extends FormBase {
return $form;
}
public function validateForm(array &$form, FormStateInterface $form_state): void {
$slot = $this->tempStore->get('booking_slot') ?? [];
$start = $slot['start'] ?? '';
if (!$start) {
$form_state->setError($form['slot_summary'], $this->t('No slot selected. Please go back and choose a time.'));
return;
}
if (new \DateTime($start) < new \DateTime()) {
$form_state->setError($form['slot_summary'], $this->t('That slot is in the past. Please go back and choose another time.'));
return;
}
$provider_id = $slot['provider_id'] ?? '';
$conflict = \Drupal::entityQuery('node')
->condition('type', 'appointment')
->condition('field_appointment_date', $start)
->condition('field_provider', $provider_id ?: 0)
->accessCheck(FALSE)
->count()
->execute();
if ($conflict > 0) {
$form_state->setError($form['slot_summary'], $this->t('That slot was just booked. Please go back and choose another time.'));
}
}
public function submitForm(array &$form, FormStateInterface $form_state): void {
$slot = $this->tempStore->get('booking_slot') ?? [];
$this->tempStore->delete('booking_slot');
$to = $this->configFactory->get('riverside_pt.settings')->get('notification_email');
$lang = $this->languageManager()->getDefaultLanguage()->getId();
$params = [
$sent = $this->mailManager->mail('riverside_pt', 'booking_request', $to, $lang, [
'first_name' => $form_state->getValue('first_name'),
'last_name' => $form_state->getValue('last_name'),
'phone' => $form_state->getValue('phone'),
'start' => $form_state->getValue('start'),
'end' => $form_state->getValue('end'),
];
$sent = $this->mailManager->mail('riverside_pt', 'booking_request', $to, $lang, $params);
'comments' => $form_state->getValue('comments'),
'start' => $slot['start'] ?? '',
'end' => $slot['end'] ?? '',
]);
if ($sent['result']) {
$this->messenger()->addStatus($this->t('Your request has been submitted. We will contact you to confirm.'));