Structure

This commit is contained in:
Mork Swork 2026-05-12 17:28:47 -07:00
parent bc44ac087c
commit 81c82c3da8
11 changed files with 274 additions and 24 deletions

View file

@ -1,8 +1,3 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="FileTypeManager" version="17">
<extensionMap>
<mapping ext="install" type="PHP" />
</extensionMap>
</component>
</project>

View file

@ -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/

8
Makefile Normal file
View file

@ -0,0 +1,8 @@
shell:
docker compose exec app bash
drush:
docker compose exec app drush $(filter-out $@,$(MAKECMDGOALS))
%:
@:

View file

@ -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 <cmd> # run any drush command, e.g. make drush cr
```
## Modules
- **FullCalendar View** — interactive appointment calendar
- **Webform** — patient booking forms
- **Symfony Mailer** — transactional email

View file

@ -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:

View file

@ -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

View file

@ -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);

View file

@ -1,16 +1,143 @@
<?php
use Drupal\node\Entity\NodeType;
use Drupal\user\Entity\Role;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\field\Entity\FieldConfig;
function riverside_pt_install() {
$type = NodeType::create([
'type' => '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());
}

View file

@ -0,0 +1,6 @@
schedule:
js:
js/fullcalendar.min.js: { minified: true }
js/calendar.js: {}
dependencies:
- core/drupalSettings

View file

@ -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

View file

@ -0,0 +1,58 @@
<?php
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;
class ScheduleController extends ControllerBase {
public function page(): array {
return [
'#type' => '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));
}
}