customer-riverside/web/modules/custom/riverside_pt/riverside_pt.install
Philip Peterson 1b7577fa17 Calendar polish: selection persistence, no-slots overlay, various fixes
- Persist selected date across month navigation using module-level vars;
  datesSet re-applies is-selected and restores slots when returning
- Show "No availability this month" overlay after a fetch returns empty;
  gated on initializedRef+fetchedRef so auto-advance phase is silent
- Fix Dec 31 overflow: showNonCurrentDates:false hides adjacent-month days
- Fix fc-day-disabled background tint in calendar.css
- Gate auto-advance on loading() callback so removeAllEventSources()
  spurious eventsSet() fires don't trigger premature month jumping
- Inline overlay styles to avoid Tailwind cascade uncertainty; document
  the module-level CX constant pattern as the general fix
- firstName added to booking form; storeSlot sends email directly when
  full contact info present, skipping tempstore redirect
- Remove BookingForm.php and /schedule/book route (replaced by inline form)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-03 20:51:43 -07:00

239 lines
8 KiB
Text

<?php
use Drupal\node\Entity\Node;
use Drupal\node\Entity\NodeType;
use Drupal\menu_link_content\Entity\MenuLinkContent;
use Drupal\path_alias\Entity\PathAlias;
use Drupal\user\Entity\Role;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\field\Entity\FieldConfig;
/**
* Implements hook_install().
*
* Delegates to the rebuild function so that normal module installs and
* container rebuilds both produce the same result.
*/
function riverside_pt_install() {
_riverside_pt_rebuild();
}
/**
* Rebuilds the Riverside PT site structure from code.
*
* This function is idempotent and can be safely called multiple times.
* It is the single source of truth for content types, fields, roles,
* and navigation used by the Docker entrypoint.
*/
function _riverside_pt_rebuild(): void {
$entity_type_manager = \Drupal::entityTypeManager();
$storage_node_type = $entity_type_manager->getStorage('node_type');
$storage_role = $entity_type_manager->getStorage('user_role');
$storage_field_storage = $entity_type_manager->getStorage('field_storage_config');
$storage_field_config = $entity_type_manager->getStorage('field_config');
// --- Content types (idempotent) ---
if (!$storage_node_type->load('appointment')) {
NodeType::create([
'type' => 'appointment',
'name' => 'Appointment',
'description' => 'A booking between a Patient and a Provider at a particular time.',
'new_revision' => FALSE,
'display_submitted' => FALSE,
])->save();
}
if (!$storage_node_type->load('provider_availability')) {
NodeType::create([
'type' => 'provider_availability',
'name' => 'Provider Availability',
'description' => 'A window of time during which a Provider is available for appointments.',
'new_revision' => FALSE,
'display_submitted' => FALSE,
])->save();
}
// --- Role (idempotent) ---
if (!$storage_role->load('provider')) {
Role::create(['id' => 'provider', 'label' => 'Provider'])->save();
}
// --- Field storages (idempotent) ---
$field_storages = [
'field_appointment_date' => [
'entity_type' => 'node',
'type' => 'datetime',
'settings' => ['datetime_type' => 'datetime'],
],
'field_duration_minutes' => [
'entity_type' => 'node',
'type' => 'integer',
],
'field_service_type' => [
'entity_type' => 'node',
'type' => 'list_string',
'settings' => [
'allowed_values' => [
'diagnostic' => 'Diagnostic',
'sports_rehab' => 'Sports Rehab',
'pre_post_surgical_rehab' => 'Pre/Post-Surgical Rehab',
'neurological_pt' => 'Neurological PT',
],
],
],
'field_provider' => [
'entity_type' => 'node',
'type' => 'entity_reference',
'settings' => ['target_type' => 'user'],
],
'field_start_datetime' => [
'entity_type' => 'node',
'type' => 'datetime',
'settings' => ['datetime_type' => 'datetime'],
],
'field_end_datetime' => [
'entity_type' => 'node',
'type' => 'datetime',
'settings' => ['datetime_type' => 'datetime'],
],
];
foreach ($field_storages as $field_name => $definition) {
if (!$storage_field_storage->load("node.$field_name")) {
FieldStorageConfig::create([
'field_name' => $field_name,
] + $definition)->save();
}
}
// Clear field definition cache so FieldConfig creation can see the storages.
\Drupal::service('entity_field.manager')->clearCachedFieldDefinitions();
// --- Field configs (idempotent) ---
$field_configs = [
// Appointment bundle
'node.appointment.field_appointment_date' => [
'field_name' => 'field_appointment_date',
'entity_type' => 'node',
'bundle' => 'appointment',
'label' => 'Appointment Date',
'required' => TRUE,
],
'node.appointment.field_duration_minutes' => [
'field_name' => 'field_duration_minutes',
'entity_type' => 'node',
'bundle' => 'appointment',
'label' => 'Duration (Minutes)',
'required' => TRUE,
],
'node.appointment.field_service_type' => [
'field_name' => 'field_service_type',
'entity_type' => 'node',
'bundle' => 'appointment',
'label' => 'Service Type',
'required' => TRUE,
],
'node.appointment.field_provider' => [
'field_name' => 'field_provider',
'entity_type' => 'node',
'bundle' => 'appointment',
'label' => 'Provider',
'required' => TRUE,
'settings' => [
'handler' => 'default:user',
'handler_settings' => [
'filter' => ['type' => '_role', 'role' => ['provider' => 'provider']],
],
],
],
// Provider availability bundle
'node.provider_availability.field_provider' => [
'field_name' => 'field_provider',
'entity_type' => 'node',
'bundle' => 'provider_availability',
'label' => 'Provider',
'required' => TRUE,
'settings' => [
'handler' => 'default:user',
'handler_settings' => [
'filter' => ['type' => '_role', 'role' => ['provider' => 'provider']],
],
],
],
'node.provider_availability.field_start_datetime' => [
'field_name' => 'field_start_datetime',
'entity_type' => 'node',
'bundle' => 'provider_availability',
'label' => 'Start',
'required' => TRUE,
],
'node.provider_availability.field_end_datetime' => [
'field_name' => 'field_end_datetime',
'entity_type' => 'node',
'bundle' => 'provider_availability',
'label' => 'End',
'required' => TRUE,
],
];
foreach ($field_configs as $field_id => $definition) {
if (!$storage_field_config->load($field_id)) {
FieldConfig::create($definition)->save();
}
}
// Navigation and basic site settings (these are intentionally destructive on menu).
try {
_riverside_pt_build_navigation();
}
catch (\Exception $e) {
\Drupal::logger('riverside_pt')->error('Navigation setup failed: @msg', ['@msg' => $e->getMessage()]);
}
\Drupal::configFactory()->getEditable('system.site')
->set('name', 'Riverside Therapeutics')
->set('page.front', '/home')
->save();
}
function _riverside_pt_build_navigation(): void {
$em = \Drupal::entityTypeManager();
foreach ($em->getStorage('menu_link_content')->loadByProperties(['menu_name' => 'main']) as $link) {
$link->delete();
}
foreach (['Services', 'About', 'FAQ', 'Contact'] as $title) {
if ($em->getStorage('node')->loadByProperties(['title' => $title, 'type' => 'page'])) {
continue;
}
$node = Node::create(['type' => 'page', 'title' => $title, 'status' => 1]);
$node->save();
PathAlias::create([
'path' => '/node/' . $node->id(),
'alias' => '/' . strtolower($title),
'langcode' => 'en',
])->save();
}
$defs = [
['title' => 'Home', 'uri' => 'route:<front>', 'weight' => 0, 'class' => NULL],
['title' => 'Services', 'uri' => 'internal:/services', 'weight' => 1, 'class' => NULL],
['title' => 'About', 'uri' => 'internal:/about', 'weight' => 2, 'class' => NULL],
['title' => 'FAQ', 'uri' => 'internal:/faq', 'weight' => 3, 'class' => NULL],
['title' => 'Contact', 'uri' => 'internal:/contact', 'weight' => 4, 'class' => 'nav-cta nav-cta--primary'],
['title' => 'Book An Appointment', 'uri' => 'internal:/home', 'weight' => 5, 'class' => 'nav-cta nav-cta--primary'],
];
foreach ($defs as $def) {
$options = $def['class'] ? ['attributes' => ['class' => explode(' ', $def['class'])]] : [];
MenuLinkContent::create([
'title' => $def['title'],
'link' => ['uri' => $def['uri'], 'options' => $options],
'menu_name' => 'main',
'weight' => $def['weight'],
'enabled' => TRUE,
])->save();
}
}