2026-05-01 04:24:10 -08:00
|
|
|
<?php
|
|
|
|
|
|
2026-05-14 19:05:46 -08:00
|
|
|
use Drupal\node\Entity\Node;
|
2026-05-01 04:24:10 -08:00
|
|
|
use Drupal\node\Entity\NodeType;
|
2026-05-14 19:05:46 -08:00
|
|
|
use Drupal\menu_link_content\Entity\MenuLinkContent;
|
|
|
|
|
use Drupal\path_alias\Entity\PathAlias;
|
2026-05-12 16:28:47 -08:00
|
|
|
use Drupal\user\Entity\Role;
|
|
|
|
|
use Drupal\field\Entity\FieldStorageConfig;
|
|
|
|
|
use Drupal\field\Entity\FieldConfig;
|
2026-05-01 04:24:10 -08:00
|
|
|
|
2026-05-27 20:58:19 -08:00
|
|
|
/**
|
|
|
|
|
* Implements hook_install().
|
|
|
|
|
*
|
|
|
|
|
* Delegates to the rebuild function so that normal module installs and
|
|
|
|
|
* container rebuilds both produce the same result.
|
|
|
|
|
*/
|
2026-05-01 04:24:10 -08:00
|
|
|
function riverside_pt_install() {
|
2026-05-27 20:58:19 -08:00
|
|
|
_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',
|
|
|
|
|
],
|
2026-05-14 19:39:30 -08:00
|
|
|
],
|
|
|
|
|
],
|
2026-05-27 20:58:19 -08:00
|
|
|
'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.
|
2026-05-14 19:39:30 -08:00
|
|
|
\Drupal::service('entity_field.manager')->clearCachedFieldDefinitions();
|
|
|
|
|
|
2026-05-27 20:58:19 -08:00
|
|
|
// --- 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']],
|
|
|
|
|
],
|
2026-05-14 19:39:30 -08:00
|
|
|
],
|
|
|
|
|
],
|
2026-05-27 20:58:19 -08:00
|
|
|
// 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']],
|
|
|
|
|
],
|
2026-05-14 19:39:30 -08:00
|
|
|
],
|
|
|
|
|
],
|
2026-05-27 20:58:19 -08:00
|
|
|
'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();
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-05-14 19:39:30 -08:00
|
|
|
|
2026-05-27 20:58:19 -08:00
|
|
|
// Navigation and basic site settings (these are intentionally destructive on menu).
|
2026-05-14 19:39:30 -08:00
|
|
|
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')
|
2026-06-01 00:47:26 -08:00
|
|
|
->set('name', 'Riverside Therapeutics')
|
2026-05-14 19:39:30 -08:00
|
|
|
->set('page.front', '/home')
|
|
|
|
|
->save();
|
2026-05-14 19:05:46 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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) {
|
2026-05-14 19:39:30 -08:00
|
|
|
if ($em->getStorage('node')->loadByProperties(['title' => $title, 'type' => 'page'])) {
|
2026-05-14 19:05:46 -08:00
|
|
|
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],
|
2026-05-16 08:52:21 -08:00
|
|
|
|
|
|
|
|
['title' => 'Contact', 'uri' => 'internal:/contact', 'weight' => 4, 'class' => 'nav-cta nav-cta--primary'],
|
2026-06-03 19:51:43 -08:00
|
|
|
['title' => 'Book An Appointment', 'uri' => 'internal:/home', 'weight' => 5, 'class' => 'nav-cta nav-cta--primary'],
|
2026-05-14 19:05:46 -08:00
|
|
|
];
|
|
|
|
|
|
|
|
|
|
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();
|
|
|
|
|
}
|
2026-05-12 16:28:47 -08:00
|
|
|
}
|