wip flow
This commit is contained in:
parent
0a2e80f7b0
commit
9454dc03b4
5 changed files with 142 additions and 33 deletions
|
|
@ -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({
|
||||
start: seg.event.startStr,
|
||||
end: seg.event.endStr,
|
||||
});
|
||||
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 = drupalSettings.riversidePt.bookingUrl + '?' + params.toString();
|
||||
a.textContent = start + ' – ' + end;
|
||||
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';
|
||||
}
|
||||
});
|
||||
});
|
||||
li.appendChild(a);
|
||||
panelSlots.appendChild(li);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
@ -60,9 +73,10 @@ class ScheduleController extends ControllerBase {
|
|||
'library' => ['riverside_pt/schedule'],
|
||||
'drupalSettings' => [
|
||||
'riversidePt' => [
|
||||
'eventsUrl' => Url::fromRoute('riverside_pt.schedule_events')->toString(),
|
||||
'bookingUrl' => Url::fromRoute('riverside_pt.booking')->toString(),
|
||||
'holidays' => $this->buildHolidaysMap(),
|
||||
'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');
|
||||
|
|
@ -98,10 +129,10 @@ class ScheduleController extends ControllerBase {
|
|||
$slot = clone $current;
|
||||
$slot->setTime(9 + $n, 0);
|
||||
$events[] = [
|
||||
'id' => $id++,
|
||||
'id' => $id++,
|
||||
'title' => 'Available',
|
||||
'start' => $slot->format('Y-m-d\TH:i:s'),
|
||||
'end' => (clone $slot)->modify('+1 hour')->format('Y-m-d\TH:i:s'),
|
||||
'end' => (clone $slot)->modify('+1 hour')->format('Y-m-d\TH:i:s'),
|
||||
];
|
||||
}
|
||||
$current->modify('+1 day');
|
||||
|
|
|
|||
|
|
@ -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.'));
|
||||
|
|
|
|||
Loading…
Reference in a new issue