From 81c82c3da8efec352c1c1c03910eb7571a7a851b Mon Sep 17 00:00:00 2001 From: Mork Swork Date: Tue, 12 May 2026 17:28:47 -0700 Subject: [PATCH] Structure --- .idea/misc.xml | 5 - Dockerfile | 7 +- Makefile | 8 + README.md | 23 ++- docker-compose.yml | 5 +- docker/php/entrypoint.sh | 9 +- .../custom/riverside_pt/js/calendar.js | 13 ++ .../custom/riverside_pt/riverside_pt.install | 147 ++++++++++++++++-- .../riverside_pt/riverside_pt.libraries.yml | 6 + .../riverside_pt/riverside_pt.routing.yml | 17 ++ .../src/Controller/ScheduleController.php | 58 +++++++ 11 files changed, 274 insertions(+), 24 deletions(-) create mode 100644 Makefile create mode 100644 web/modules/custom/riverside_pt/js/calendar.js create mode 100644 web/modules/custom/riverside_pt/riverside_pt.libraries.yml create mode 100644 web/modules/custom/riverside_pt/riverside_pt.routing.yml create mode 100644 web/modules/custom/riverside_pt/src/Controller/ScheduleController.php diff --git a/.idea/misc.xml b/.idea/misc.xml index ed86406..db93c94 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,8 +1,3 @@ - - - - - diff --git a/Dockerfile b/Dockerfile index 96bdef0..0daeb3d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -11,6 +11,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ libzip-dev \ git \ unzip \ + locales \ && rm -rf /var/lib/apt/lists/* RUN docker-php-ext-configure gd --with-freetype --with-jpeg && \ @@ -46,7 +47,7 @@ RUN composer require drupal/core-composer-scaffold:^11 --no-update RUN composer require drush/drush:"^13 || ^14" --no-update # Extra -RUN composer require drupal/webform drupal/symfony_mailer --no-update +RUN composer require drupal/webform drupal/symfony_mailer drupal/claro_compact --no-update ####### @@ -57,6 +58,10 @@ RUN composer install --no-dev --optimize-autoloader --no-interaction COPY web/sites/default/settings.php web/sites/default/settings.php COPY web/sites/default/files/ web/sites/default/files/ COPY web/modules/custom/ web/modules/custom/ + +ARG FULLCALENDAR_VERSION=6.1.15 +RUN curl -fsSL "https://cdn.jsdelivr.net/npm/fullcalendar@${FULLCALENDAR_VERSION}/index.global.min.js" \ + -o web/modules/custom/riverside_pt/js/fullcalendar.min.js COPY config/sync/ config/sync/ # Debian nginx runs as www-data (matches php-fpm), config in conf.d/ diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..1691e5d --- /dev/null +++ b/Makefile @@ -0,0 +1,8 @@ +shell: + docker compose exec app bash + +drush: + docker compose exec app drush $(filter-out $@,$(MAKECMDGOALS)) + +%: + @: diff --git a/README.md b/README.md index b08f89e..41ae4fc 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,24 @@ -How to run: +# Riverside Patient Tracker -``` +A Drupal-based appointment scheduling site for booking sessions between patients and practitioners. + +## Running locally + +```sh docker compose up --build ``` -Username, password: admin / admin +Admin login: `admin` / `admin` at `/user/login` + +## Makefile commands + +```sh +make shell # open a bash shell in the app container +make drush # run any drush command, e.g. make drush cr +``` + +## Modules + +- **FullCalendar View** — interactive appointment calendar +- **Webform** — patient booking forms +- **Symfony Mailer** — transactional email diff --git a/docker-compose.yml b/docker-compose.yml index d70a07a..1787e9f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -28,9 +28,12 @@ services: POSTGRES_PASSWORD: drupal volumes: - ./docker/postgres/init.sql:/docker-entrypoint-initdb.d/init.sql:ro + - postgres_data:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U drupal -d drupal"] interval: 5s timeout: 5s retries: 20 - # No named volume for data = fully ephemeral (recreated from init.sql on every `docker compose up` after `down`) + +volumes: + postgres_data: diff --git a/docker/php/entrypoint.sh b/docker/php/entrypoint.sh index 9b3fc08..6a9aaa4 100644 --- a/docker/php/entrypoint.sh +++ b/docker/php/entrypoint.sh @@ -26,7 +26,7 @@ if [ "$HAS_TABLES" = "1" ]; then echo "[entrypoint] No config to import, continuing." else echo "[entrypoint] Fresh database, installing Drupal..." - $DRUSH site:install minimal \ + $DRUSH site:install standard \ --site-name="${SITE_NAME:-Portfolio}" \ --account-name=admin \ --account-pass="${ADMIN_PASS:-admin}" \ @@ -37,13 +37,14 @@ else $DRUSH en -y views views_ui field_ui text options link datetime $DRUSH en -y webform webform_ui $DRUSH en -y symfony_mailer + $DRUSH en -y riverside_pt echo "[entrypoint] Modules enabled." echo "[entrypoint] Setting themes..." - $DRUSH theme:enable olivero claro - $DRUSH config:set system.theme default olivero -y - $DRUSH config:set system.theme admin claro -y + $DRUSH theme:enable olivero claro_compact + $DRUSH config:set system.theme default olivero -y + $DRUSH config:set system.theme admin claro_compact -y echo "[entrypoint] Themes set." if ls /var/www/html/config/sync/*.yml >/dev/null 2>&1; then diff --git a/web/modules/custom/riverside_pt/js/calendar.js b/web/modules/custom/riverside_pt/js/calendar.js new file mode 100644 index 0000000..daee8e6 --- /dev/null +++ b/web/modules/custom/riverside_pt/js/calendar.js @@ -0,0 +1,13 @@ +(function (drupalSettings) { + document.addEventListener('DOMContentLoaded', function () { + const el = document.getElementById('riverside-calendar'); + if (!el) return; + + const calendar = new FullCalendar.Calendar(el, { + initialView: 'timeGridWeek', + events: drupalSettings.riversidePt.eventsUrl, + }); + + calendar.render(); + }); +})(drupalSettings); diff --git a/web/modules/custom/riverside_pt/riverside_pt.install b/web/modules/custom/riverside_pt/riverside_pt.install index e95f14e..1bdbc14 100644 --- a/web/modules/custom/riverside_pt/riverside_pt.install +++ b/web/modules/custom/riverside_pt/riverside_pt.install @@ -1,16 +1,143 @@ 'appointment', - 'name' => 'Appointment', - 'description' => 'A booking between a Patient and a Provider at a particular time.', - 'new_revision' => FALSE, - 'preview_mode' => DRUPAL_DISABLED, - 'display_submitted' => FALSE, - ]); - $type->save(); -} + array_walk([ + // 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, + ]), + FieldStorageConfig::create([ + 'field_name' => 'field_appointment_date', + 'entity_type' => 'node', + 'type' => 'datetime', + 'settings' => ['datetime_type' => 'datetime'], + ]), + FieldStorageConfig::create([ + 'field_name' => 'field_duration_minutes', + 'entity_type' => 'node', + 'type' => 'integer', + ]), + FieldStorageConfig::create([ + 'field_name' => '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', + ], + ], + ]), + FieldStorageConfig::create([ + 'field_name' => 'field_provider', + 'entity_type' => 'node', + 'type' => 'entity_reference', + 'settings' => ['target_type' => 'user'], + ]), + FieldConfig::create([ + 'field_name' => 'field_appointment_date', + 'entity_type' => 'node', + 'bundle' => 'appointment', + 'label' => 'Appointment Date', + 'required' => TRUE, + ]), + FieldConfig::create([ + 'field_name' => 'field_duration_minutes', + 'entity_type' => 'node', + 'bundle' => 'appointment', + 'label' => 'Duration (Minutes)', + 'required' => TRUE, + ]), + FieldConfig::create([ + 'field_name' => 'field_service_type', + 'entity_type' => 'node', + 'bundle' => 'appointment', + 'label' => 'Service Type', + 'required' => TRUE, + ]), + FieldConfig::create([ + '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 + Role::create([ + 'id' => 'provider', + 'label' => 'Provider', + ]), + + // 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, + ]), + FieldStorageConfig::create([ + 'field_name' => 'field_start_datetime', + 'entity_type' => 'node', + 'type' => 'datetime', + 'settings' => ['datetime_type' => 'datetime'], + ]), + FieldStorageConfig::create([ + 'field_name' => 'field_end_datetime', + 'entity_type' => 'node', + 'type' => 'datetime', + 'settings' => ['datetime_type' => 'datetime'], + ]), + FieldConfig::create([ + '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'], + ], + ], + ], + ]), + FieldConfig::create([ + 'field_name' => 'field_start_datetime', + 'entity_type' => 'node', + 'bundle' => 'provider_availability', + 'label' => 'Start', + 'required' => TRUE, + ]), + FieldConfig::create([ + 'field_name' => 'field_end_datetime', + 'entity_type' => 'node', + 'bundle' => 'provider_availability', + 'label' => 'End', + 'required' => TRUE, + ]), + ], fn($entity) => $entity->save()); +} diff --git a/web/modules/custom/riverside_pt/riverside_pt.libraries.yml b/web/modules/custom/riverside_pt/riverside_pt.libraries.yml new file mode 100644 index 0000000..aa3fee1 --- /dev/null +++ b/web/modules/custom/riverside_pt/riverside_pt.libraries.yml @@ -0,0 +1,6 @@ +schedule: + js: + js/fullcalendar.min.js: { minified: true } + js/calendar.js: {} + dependencies: + - core/drupalSettings diff --git a/web/modules/custom/riverside_pt/riverside_pt.routing.yml b/web/modules/custom/riverside_pt/riverside_pt.routing.yml new file mode 100644 index 0000000..295bf00 --- /dev/null +++ b/web/modules/custom/riverside_pt/riverside_pt.routing.yml @@ -0,0 +1,17 @@ +riverside_pt.schedule: + path: '/schedule' + defaults: + _controller: '\Drupal\riverside_pt\Controller\ScheduleController::page' + _title: 'Schedule' + requirements: + _permission: 'access content' + +riverside_pt.schedule_events: + path: '/schedule/events' + defaults: + _controller: '\Drupal\riverside_pt\Controller\ScheduleController::events' + requirements: + _permission: 'access content' + options: + _auth: + - cookie diff --git a/web/modules/custom/riverside_pt/src/Controller/ScheduleController.php b/web/modules/custom/riverside_pt/src/Controller/ScheduleController.php new file mode 100644 index 0000000..c05b862 --- /dev/null +++ b/web/modules/custom/riverside_pt/src/Controller/ScheduleController.php @@ -0,0 +1,58 @@ + 'html_tag', + '#tag' => 'div', + '#attributes' => ['id' => 'riverside-calendar'], + '#attached' => [ + 'library' => ['riverside_pt/schedule'], + 'drupalSettings' => [ + 'riversidePt' => [ + 'eventsUrl' => Url::fromRoute('riverside_pt.schedule_events')->toString(), + ], + ], + ], + ]; + } + + 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); + + if ($start) { + $query->condition('field_end_datetime', $start, '>='); + } + if ($end) { + $query->condition('field_start_datetime', $end, '<='); + } + + $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)); + } + +}