Use modal
This commit is contained in:
parent
1a5ece3f7d
commit
269813c12d
6 changed files with 288 additions and 25 deletions
|
|
@ -0,0 +1,39 @@
|
||||||
|
holidays:
|
||||||
|
- date: '2026-01-01'
|
||||||
|
name: "New Year's Day"
|
||||||
|
- date: '2026-01-19'
|
||||||
|
name: 'Martin Luther King Jr. Day'
|
||||||
|
- date: '2026-02-14'
|
||||||
|
name: "Valentine's Day"
|
||||||
|
- date: '2026-02-16'
|
||||||
|
name: "Presidents' Day"
|
||||||
|
- date: '2026-03-17'
|
||||||
|
name: "St. Patrick's Day"
|
||||||
|
- date: '2026-04-05'
|
||||||
|
name: 'Easter Sunday'
|
||||||
|
- date: '2026-05-10'
|
||||||
|
name: "Mother's Day"
|
||||||
|
- date: '2026-05-25'
|
||||||
|
name: 'Memorial Day'
|
||||||
|
- date: '2026-06-19'
|
||||||
|
name: 'Juneteenth'
|
||||||
|
- date: '2026-06-21'
|
||||||
|
name: "Father's Day"
|
||||||
|
- date: '2026-07-04'
|
||||||
|
name: 'Independence Day'
|
||||||
|
- date: '2026-08-03'
|
||||||
|
name: 'Summer Bank Holiday'
|
||||||
|
- date: '2026-09-07'
|
||||||
|
name: 'Labor Day'
|
||||||
|
- date: '2026-10-12'
|
||||||
|
name: 'Columbus Day'
|
||||||
|
- date: '2026-10-31'
|
||||||
|
name: 'Halloween'
|
||||||
|
- date: '2026-11-11'
|
||||||
|
name: "Veterans Day"
|
||||||
|
- date: '2026-11-26'
|
||||||
|
name: 'Thanksgiving'
|
||||||
|
- date: '2026-12-25'
|
||||||
|
name: 'Christmas'
|
||||||
|
- date: '2026-12-31'
|
||||||
|
name: "New Year's Eve"
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
riverside_pt.settings:
|
||||||
|
type: config_object
|
||||||
|
label: 'Riverside PT Settings'
|
||||||
|
mapping:
|
||||||
|
holidays:
|
||||||
|
type: sequence
|
||||||
|
label: 'Holidays'
|
||||||
|
sequence:
|
||||||
|
type: mapping
|
||||||
|
label: 'Holiday'
|
||||||
|
mapping:
|
||||||
|
date:
|
||||||
|
type: string
|
||||||
|
label: 'Date (YYYY-MM-DD)'
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
label: 'Holiday name'
|
||||||
89
web/modules/custom/riverside_pt/css/calendar.css
Normal file
89
web/modules/custom/riverside_pt/css/calendar.css
Normal file
|
|
@ -0,0 +1,89 @@
|
||||||
|
#riverside-calendar {
|
||||||
|
max-width: 480px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#riverside-booking-backdrop {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.45);
|
||||||
|
z-index: 999;
|
||||||
|
}
|
||||||
|
|
||||||
|
#riverside-booking-panel {
|
||||||
|
position: fixed;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
width: 90%;
|
||||||
|
max-width: 340px;
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 1rem;
|
||||||
|
z-index: 1000;
|
||||||
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.riverside-booking-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
#riverside-booking-close {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 1rem;
|
||||||
|
line-height: 1;
|
||||||
|
padding: 0;
|
||||||
|
color: #6b7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
#riverside-booking-slots {
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#riverside-booking-slots li {
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
border: 1px solid #3b82f6;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: #1d4ed8;
|
||||||
|
}
|
||||||
|
|
||||||
|
#riverside-booking-slots li:hover {
|
||||||
|
background: #eff6ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
#riverside-calendar .fc-more-popover {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#riverside-calendar .is-holiday .fc-more-link {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#riverside-calendar .fc-day-other .riverside-holiday-label,
|
||||||
|
#riverside-calendar .fc-day-other .fc-more-link {
|
||||||
|
opacity: 0.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
#riverside-calendar .riverside-holiday-label {
|
||||||
|
font-size: 0.65rem;
|
||||||
|
color: #b45309;
|
||||||
|
text-align: center;
|
||||||
|
padding-bottom: 2px;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
@ -4,8 +4,73 @@
|
||||||
if (!el) return;
|
if (!el) return;
|
||||||
|
|
||||||
const calendar = new FullCalendar.Calendar(el, {
|
const calendar = new FullCalendar.Calendar(el, {
|
||||||
initialView: 'timeGridWeek',
|
initialView: 'dayGridMonth',
|
||||||
|
headerToolbar: { left: 'prev', center: 'title', right: 'next' },
|
||||||
|
validRange: function (now) {
|
||||||
|
return {
|
||||||
|
start: new Date(now.getFullYear(), now.getMonth(), 1),
|
||||||
|
end: new Date(now.getFullYear(), now.getMonth() + 7, 1),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
fixedWeekCount: false,
|
||||||
|
height: 'auto',
|
||||||
events: drupalSettings.riversidePt.eventsUrl,
|
events: drupalSettings.riversidePt.eventsUrl,
|
||||||
|
eventBackgroundColor: '#3b82f6',
|
||||||
|
eventBorderColor: '#3b82f6',
|
||||||
|
dayMaxEvents: 0,
|
||||||
|
moreLinkContent: function (arg) {
|
||||||
|
return arg.num + ' slot' + (arg.num !== 1 ? 's' : '');
|
||||||
|
},
|
||||||
|
dayCellClassNames: function (arg) {
|
||||||
|
const date = arg.date.toISOString().substring(0, 10);
|
||||||
|
if (drupalSettings.riversidePt.holidays[date]) return ['is-holiday'];
|
||||||
|
},
|
||||||
|
dayCellDidMount: function (arg) {
|
||||||
|
const date = arg.date.toISOString().substring(0, 10);
|
||||||
|
const holiday = drupalSettings.riversidePt.holidays[date];
|
||||||
|
if (!holiday) return;
|
||||||
|
const label = document.createElement('div');
|
||||||
|
label.className = 'riverside-holiday-label';
|
||||||
|
label.textContent = holiday;
|
||||||
|
arg.el.appendChild(label);
|
||||||
|
},
|
||||||
|
moreLinkClick: function (arg) {
|
||||||
|
arg.jsEvent.preventDefault();
|
||||||
|
arg.jsEvent.stopPropagation();
|
||||||
|
const date = arg.date.toLocaleDateString(undefined, { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' });
|
||||||
|
panelDate.textContent = date;
|
||||||
|
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' });
|
||||||
|
li.textContent = start + ' – ' + end;
|
||||||
|
panelSlots.appendChild(li);
|
||||||
|
});
|
||||||
|
openPanel();
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const panel = document.getElementById('riverside-booking-panel');
|
||||||
|
const backdrop = document.getElementById('riverside-booking-backdrop');
|
||||||
|
const panelDate = document.getElementById('riverside-booking-date');
|
||||||
|
const panelSlots = document.getElementById('riverside-booking-slots');
|
||||||
|
|
||||||
|
function closePanel() {
|
||||||
|
panel.hidden = true;
|
||||||
|
backdrop.hidden = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function openPanel() {
|
||||||
|
backdrop.hidden = false;
|
||||||
|
panel.hidden = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('riverside-booking-close').addEventListener('click', closePanel);
|
||||||
|
backdrop.addEventListener('click', closePanel);
|
||||||
|
document.addEventListener('keydown', function (e) {
|
||||||
|
if (e.key === 'Escape') closePanel();
|
||||||
});
|
});
|
||||||
|
|
||||||
calendar.render();
|
calendar.render();
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,7 @@
|
||||||
schedule:
|
schedule:
|
||||||
|
css:
|
||||||
|
theme:
|
||||||
|
css/calendar.css: {}
|
||||||
js:
|
js:
|
||||||
js/fullcalendar.min.js: { minified: true }
|
js/fullcalendar.min.js: { minified: true }
|
||||||
js/calendar.js: {}
|
js/calendar.js: {}
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@ namespace Drupal\riverside_pt\Controller;
|
||||||
|
|
||||||
use Drupal\Core\Controller\ControllerBase;
|
use Drupal\Core\Controller\ControllerBase;
|
||||||
use Drupal\Core\Url;
|
use Drupal\Core\Url;
|
||||||
use Drupal\node\Entity\Node;
|
|
||||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
|
||||||
|
|
@ -12,47 +11,98 @@ class ScheduleController extends ControllerBase {
|
||||||
|
|
||||||
public function page(): array {
|
public function page(): array {
|
||||||
return [
|
return [
|
||||||
|
'#type' => 'container',
|
||||||
|
'intro' => [
|
||||||
|
'#type' => 'html_tag',
|
||||||
|
'#tag' => 'p',
|
||||||
|
'#value' => $this->t('View provider availability below. Use the calendar to browse open appointment slots by week.'),
|
||||||
|
],
|
||||||
|
'calendar' => [
|
||||||
'#type' => 'html_tag',
|
'#type' => 'html_tag',
|
||||||
'#tag' => 'div',
|
'#tag' => 'div',
|
||||||
'#attributes' => ['id' => 'riverside-calendar'],
|
'#attributes' => ['id' => 'riverside-calendar'],
|
||||||
|
],
|
||||||
|
'booking_backdrop' => [
|
||||||
|
'#type' => 'html_tag',
|
||||||
|
'#tag' => 'div',
|
||||||
|
'#attributes' => ['id' => 'riverside-booking-backdrop', 'hidden' => TRUE],
|
||||||
|
'#value' => '',
|
||||||
|
],
|
||||||
|
'booking_panel' => [
|
||||||
|
'#type' => 'html_tag',
|
||||||
|
'#tag' => 'div',
|
||||||
|
'#attributes' => ['id' => 'riverside-booking-panel', 'hidden' => TRUE],
|
||||||
|
'header' => [
|
||||||
|
'#type' => 'html_tag',
|
||||||
|
'#tag' => 'div',
|
||||||
|
'#attributes' => ['class' => ['riverside-booking-header']],
|
||||||
|
'title' => [
|
||||||
|
'#type' => 'html_tag',
|
||||||
|
'#tag' => 'span',
|
||||||
|
'#attributes' => ['id' => 'riverside-booking-date'],
|
||||||
|
'#value' => '',
|
||||||
|
],
|
||||||
|
'close' => [
|
||||||
|
'#type' => 'html_tag',
|
||||||
|
'#tag' => 'button',
|
||||||
|
'#attributes' => ['id' => 'riverside-booking-close', 'type' => 'button'],
|
||||||
|
'#value' => $this->t('✕'),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'slots' => [
|
||||||
|
'#type' => 'html_tag',
|
||||||
|
'#tag' => 'ul',
|
||||||
|
'#attributes' => ['id' => 'riverside-booking-slots'],
|
||||||
|
'#value' => '',
|
||||||
|
],
|
||||||
|
],
|
||||||
'#attached' => [
|
'#attached' => [
|
||||||
'library' => ['riverside_pt/schedule'],
|
'library' => ['riverside_pt/schedule'],
|
||||||
'drupalSettings' => [
|
'drupalSettings' => [
|
||||||
'riversidePt' => [
|
'riversidePt' => [
|
||||||
'eventsUrl' => Url::fromRoute('riverside_pt.schedule_events')->toString(),
|
'eventsUrl' => Url::fromRoute('riverside_pt.schedule_events')->toString(),
|
||||||
|
'holidays' => $this->buildHolidaysMap(),
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function buildHolidaysMap(): array {
|
||||||
|
$holidays = $this->config('riverside_pt.settings')->get('holidays') ?? [];
|
||||||
|
$map = [];
|
||||||
|
foreach ($holidays as $holiday) {
|
||||||
|
$map[$holiday['date']] = $holiday['name'];
|
||||||
|
}
|
||||||
|
return $map;
|
||||||
|
}
|
||||||
|
|
||||||
public function events(Request $request): JsonResponse {
|
public function events(Request $request): JsonResponse {
|
||||||
$start = $request->query->get('start');
|
$start = $request->query->get('start');
|
||||||
$end = $request->query->get('end');
|
$end = $request->query->get('end');
|
||||||
|
|
||||||
$query = \Drupal::entityQuery('node')
|
$current = new \DateTime($start ?? 'now');
|
||||||
->condition('type', 'provider_availability')
|
$until = new \DateTime($end ?? 'now');
|
||||||
->condition('status', 1)
|
$events = [];
|
||||||
->accessCheck(TRUE);
|
$id = 1;
|
||||||
|
|
||||||
if ($start) {
|
while ($current < $until) {
|
||||||
$query->condition('field_end_datetime', $start, '>=');
|
$i = (int) floor($current->getTimestamp() / 86400);
|
||||||
|
$count = ($i % 5 + $i % 7 + $i % 11) % 6;
|
||||||
|
for ($n = 0; $n < $count; $n++) {
|
||||||
|
$slot = clone $current;
|
||||||
|
$slot->setTime(9 + $n, 0);
|
||||||
|
$events[] = [
|
||||||
|
'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'),
|
||||||
|
];
|
||||||
}
|
}
|
||||||
if ($end) {
|
$current->modify('+1 day');
|
||||||
$query->condition('field_start_datetime', $end, '<=');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$nids = $query->execute();
|
return new JsonResponse($events);
|
||||||
$nodes = Node::loadMultiple($nids);
|
|
||||||
|
|
||||||
$events = array_map(fn(Node $node) => [
|
|
||||||
'id' => $node->id(),
|
|
||||||
'title' => $node->field_provider->entity?->getDisplayName() ?? 'Provider',
|
|
||||||
'start' => $node->field_start_datetime->value,
|
|
||||||
'end' => $node->field_end_datetime->value,
|
|
||||||
], $nodes);
|
|
||||||
|
|
||||||
return new JsonResponse(array_values($events));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue