Structure
This commit is contained in:
parent
bc44ac087c
commit
81c82c3da8
11 changed files with 274 additions and 24 deletions
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
8
Makefile
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
shell:
|
||||
docker compose exec app bash
|
||||
|
||||
drush:
|
||||
docker compose exec app drush $(filter-out $@,$(MAKECMDGOALS))
|
||||
|
||||
%:
|
||||
@:
|
||||
23
README.md
23
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 <cmd> # run any drush command, e.g. make drush cr
|
||||
```
|
||||
|
||||
## Modules
|
||||
|
||||
- **FullCalendar View** — interactive appointment calendar
|
||||
- **Webform** — patient booking forms
|
||||
- **Symfony Mailer** — transactional email
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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 theme:enable olivero claro_compact
|
||||
$DRUSH config:set system.theme default olivero -y
|
||||
$DRUSH config:set system.theme admin claro -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
|
||||
|
|
|
|||
13
web/modules/custom/riverside_pt/js/calendar.js
Normal file
13
web/modules/custom/riverside_pt/js/calendar.js
Normal 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);
|
||||
|
|
@ -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([
|
||||
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,
|
||||
'preview_mode' => DRUPAL_DISABLED,
|
||||
'display_submitted' => FALSE,
|
||||
]);
|
||||
$type->save();
|
||||
}
|
||||
]),
|
||||
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());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,6 @@
|
|||
schedule:
|
||||
js:
|
||||
js/fullcalendar.min.js: { minified: true }
|
||||
js/calendar.js: {}
|
||||
dependencies:
|
||||
- core/drupalSettings
|
||||
17
web/modules/custom/riverside_pt/riverside_pt.routing.yml
Normal file
17
web/modules/custom/riverside_pt/riverside_pt.routing.yml
Normal 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
|
||||
|
|
@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
Loading…
Reference in a new issue