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;
|
||||
|
||||
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,
|
||||
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();
|
||||
|
|
|
|||
|
|
@ -1,4 +1,7 @@
|
|||
schedule:
|
||||
css:
|
||||
theme:
|
||||
css/calendar.css: {}
|
||||
js:
|
||||
js/fullcalendar.min.js: { minified: true }
|
||||
js/calendar.js: {}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ namespace Drupal\riverside_pt\Controller;
|
|||
|
||||
use Drupal\Core\Controller\ControllerBase;
|
||||
use Drupal\Core\Url;
|
||||
use Drupal\node\Entity\Node;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
|
|
@ -12,47 +11,98 @@ class ScheduleController extends ControllerBase {
|
|||
|
||||
public function page(): array {
|
||||
return [
|
||||
'#type' => 'html_tag',
|
||||
'#tag' => 'div',
|
||||
'#attributes' => ['id' => 'riverside-calendar'],
|
||||
'#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',
|
||||
'#tag' => 'div',
|
||||
'#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' => [
|
||||
'library' => ['riverside_pt/schedule'],
|
||||
'drupalSettings' => [
|
||||
'riversidePt' => [
|
||||
'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 {
|
||||
$start = $request->query->get('start');
|
||||
$end = $request->query->get('end');
|
||||
|
||||
$query = \Drupal::entityQuery('node')
|
||||
->condition('type', 'provider_availability')
|
||||
->condition('status', 1)
|
||||
->accessCheck(TRUE);
|
||||
$current = new \DateTime($start ?? 'now');
|
||||
$until = new \DateTime($end ?? 'now');
|
||||
$events = [];
|
||||
$id = 1;
|
||||
|
||||
if ($start) {
|
||||
$query->condition('field_end_datetime', $start, '>=');
|
||||
}
|
||||
if ($end) {
|
||||
$query->condition('field_start_datetime', $end, '<=');
|
||||
while ($current < $until) {
|
||||
$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'),
|
||||
];
|
||||
}
|
||||
$current->modify('+1 day');
|
||||
}
|
||||
|
||||
$nids = $query->execute();
|
||||
$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));
|
||||
return new JsonResponse($events);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue