Skip to content

GQuantrill/ct-scan

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

91 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

CT Booking - Scanner Management System

A full-stack web application for booking X-ray CT scanners, managing projects, customers, and generating reports on machine utilisation and project performance.

Documentation map

If you want to… Read
Get the stack running locally for development This file, Quick Start (Development)
Deploy to a server fronted by Traefik for the first time deploy.md — full walk-through (server prerequisites, Let's Encrypt, day-to-day ops, troubleshooting)
Import historic calendar / spreadsheet data into a fresh DB This file, Production Deployment (Traefik) §3
Promote data from a test deployment into a fresh production one goLive.mdpg_dump test → restore on prod
Look up the architecture / API / pages / design decisions The reference sections lower in this file

Architecture

ct-booking/
├── ct-booking-api/          # NestJS 11 + Prisma 6 backend (TypeScript)
│   ├── prisma/              # Schema + seed + migrations
│   ├── src/
│   │   ├── common/          # Guards, decorators, interceptors
│   │   └── modules/         # 11 feature modules
│   └── Dockerfile
├── ct-booking-fe/           # Angular 21 + PrimeNG 19 + Tailwind 4.2 frontend
│   ├── src/app/
│   │   ├── core/            # Services, models, guards, interceptors
│   │   └── features/        # 21 standalone page components
│   ├── nginx.conf
│   └── Dockerfile
├── database/                # Raw SQL schema (reference)
├── docker-compose.yml       # Production: Postgres + API + Frontend
└── docker-compose.local.yml # Dev: Postgres only

Tech Stack

Layer Technology
Frontend Angular 21, Tailwind CSS 4.2, PrimeNG 19, Chart.js
Backend NestJS 11, Prisma 6, passport-jwt, Swagger
Database PostgreSQL 16
Auth JWT Bearer tokens (8h expiry)
Containers Docker, Docker Compose, nginx reverse proxy

Quick Start (Development)

1. Start the database

# Fresh install: wipe any existing dev volume first so migrations and seeds
# run against an empty DB. Omit `-v` if you want to keep existing data.
docker compose -f docker-compose.local.yml down -v 2>/dev/null
docker compose -f docker-compose.local.yml up -d

2. Set up the backend

cd ct-booking-api
cp .env.example .env
yarn install
yarn prisma:generate
yarn prisma:migrate          # applies all migrations, including Unknown status

3. Seed data

The recommended path is one command that does baseline + every historic CSV in a directory:

# from ct-booking-api/
yarn seed:all /path/to/data-dir

seed:all runs prisma:seed (baseline users, systems, customers, demo projects) then seed:historic for every *.CSV it finds in the directory, sorted by filename. It's idempotent — safe to re-run any time.

If you want to run things piecemeal, the individual scripts still work:

yarn prisma:seed                          # baseline only
yarn seed:historic /path/to/mt.CSV        # one scanner at a time

Outlook CSV exports per scanner (mt.CSV, tu.CSV, w3.CSV, z6.CSV, n4.CSV) are imported by seed.historic.ts. The system code is taken from the filename (mt.CSVMT). Subjects matching an existing project are reused; everything else is created with status Unknown and anchored to its earliest booking date.

Generated outputs (rejections + a high-level import_summary.csv) land in a sibling summary/ folder of the data directory — e.g. data/summary/. This keeps the data folder free of generated files so a re-run only iterates the fresh inputs you've placed there.

  • summary/rejections_<SYSTEM>.csv — one file per scanner, full row + reason.
  • summary/import_summary.csv — one row per scanner (counts + flattened rejection subjects), upserted on re-run.

Project metadata imports from the NXCT spreadsheets stay manual:

yarn seed:projects "/path/to/NXCT Database P2 Prototype.xlsx" \
                   "/path/to/NXCT Database Updated LOCAL.xlsx"

4. Start the API and frontend

# API (from ct-booking-api/)
yarn start:dev
# Frontend (from ct-booking-fe/)
yarn install
yarn start
  • API: http://localhost:3000 — Swagger docs: http://localhost:3000/api/docs
  • UI: http://localhost:4200 (dev server proxies /api to backend automatically)

Default login: admin@ctbooking.local / changeme123

Resetting to a clean install

To start over with a fresh database (drops all data, including imports):

# from ct-scan/
docker compose -f docker-compose.local.yml down -v     # wipe DB volume
docker compose -f docker-compose.local.yml up -d       # bring DB back up
cd ct-booking-api
yarn prisma:migrate                                    # apply schema migrations

# seed:all = baseline seed (prisma/seed.ts) + seed:historic for every *.CSV
# in the directory, in alphabetical order. Rejections + import_summary.csv
# land in a sibling `summary/` folder next to <data-dir>.
yarn seed:all /path/to/data-dir

# optional — project metadata import from the NXCT spreadsheets
yarn seed:projects "/path/to/P2.xlsx" "/path/to/Updated_LOCAL.xlsx"

Production Deployment (Traefik)

The repo ships two compose files for production:

File Purpose
docker-compose.yml Base stack: db, api, frontend
docker-compose.traefik.yml Override that adds the traefik-public external network and routing labels on frontend so Traefik picks it up

Both are applied together with -f flags. The base file alone is fine for direct bare-metal hosting (frontend on :80); the Traefik override is what you want on a server already running Traefik with TLS termination.

For a full, walk-through deployment guide (server prerequisites, Let's Encrypt, rollback, etc.) see deploy.md. What's below is the short version, focused on the new historic-data import.

1. Environment

Create .env at the repo root. docker-compose.yml reads it directly. Minimum required vars are the first four; the rest are optional and have sensible defaults.

cat > .env <<'EOF'
# Routing + TLS
DOMAIN=ct-booking.example.com
PUBLIC_URL=https://ct-booking.example.com

# Secrets — avoid @ : / ? in DB_PASSWORD (it goes into DATABASE_URL).
# Generate strong values with `openssl rand -base64 48`.
DB_PASSWORD=<strong-random>
JWT_SECRET=<strong-random>

# Optional
# JWT_EXPIRY=8h

# Optional SMTP — if SMTP_HOST is blank, reset links are logged to API stdout.
# SMTP_HOST=smtp.example.com
# SMTP_PORT=587
# SMTP_USER=noreply@ct-booking.example.com
# SMTP_PASS=...
# SMTP_FROM=noreply@ct-booking.example.com
EOF
chmod 600 .env
Variable Required Used for
DOMAIN yes Traefik host rule on the frontend service
PUBLIC_URL yes API CORS_ORIGIN and base URL for reset-password links (APP_URL)
DB_PASSWORD yes Postgres role password — appears in DATABASE_URL (avoid @ : / ?)
JWT_SECRET yes Session token signing — app refuses to start without it
JWT_EXPIRY no Token lifetime, default 8h
SMTP_HOSTSMTP_FROM no Outbound mail for password resets; blank SMTP_HOST falls back to stdout logging

The traefik-public Docker network must exist (docker network ls | grep traefik-public). It's created by the Traefik install — not by this stack.

For local API development (running the API directly with yarn start:dev, no Docker for the API), ct-booking-api/.env.example is the template — copy it to ct-booking-api/.env and adjust. The variables there are the same set, just with DATABASE_URL written out longhand instead of derived from DB_PASSWORD.

2. Fresh install — wipe the volume and bring everything up

# from ct-scan/  (the -v drops the pgdata volume; skip it to keep existing data)
docker compose -f docker-compose.yml -f docker-compose.traefik.yml down -v

docker compose -f docker-compose.yml -f docker-compose.traefik.yml build
docker compose -f docker-compose.yml -f docker-compose.traefik.yml up -d

On startup the API container runs prisma migrate deploy then dist/prisma/seed.js (no-op if users already exist), so once it's healthy you have the baseline schema, demo users, systems, customers, and example projects.

3. Import historic data with seed:all

The seed scripts (seed.all.ts, seed.historic.ts, optionally seed.projects.ts) aren't baked into the production image — they live at the repo root and are run on-demand. The yarn scripts in ct-booking-api/package.json reference them as ../seed.*.ts, so once we drop them in at / inside the container (one level up from /app), the same yarn seed:all flow that works in dev works in prod too.

# from ct-scan/ on the server (or your workstation, if you have docker context set)

# 1. Copy the scripts into / inside the api container
docker compose -f docker-compose.yml -f docker-compose.traefik.yml cp \
  seed.all.ts api:/seed.all.ts
docker compose -f docker-compose.yml -f docker-compose.traefik.yml cp \
  seed.historic.ts api:/seed.historic.ts

# 2. Copy your scanner CSVs into the container. No chown needed — the seed
#    writes its outputs to a sibling /tmp/summary/ folder (created on demand
#    as the `nestjs` user), not back into /tmp/data/.
docker compose -f docker-compose.yml -f docker-compose.traefik.yml cp \
  /path/to/data api:/tmp/data

# 3. Run the orchestrator (baseline seed is a no-op since the API container
#    already ran it on startup; the historic seed runs once per *.CSV)
docker compose -f docker-compose.yml -f docker-compose.traefik.yml exec api \
  yarn seed:all /tmp/data

# 4. Pull the generated summary + rejections back to your workstation
docker compose -f docker-compose.yml -f docker-compose.traefik.yml cp \
  api:/tmp/summary/. ./summary/

The historic importer is idempotent at booking level — re-running won't create duplicates. All outputs (per-scanner rejections_<SYSTEM>.csv plus the rolled-up import_summary.csv) live in /tmp/summary/ inside the container, kept separate from /tmp/data/ so re-runs only iterate the original CSV inputs you copied in.

For the project metadata (xlsx) import, copy seed.projects.ts and drop the spreadsheets into the same /tmp/data/ folder (seed:all only picks up *.CSV so the .xlsx files there won't be processed by the historic importer). rejections_projects.csv lands in the same /tmp/summary/ folder as the historic rejections:

docker compose -f docker-compose.yml -f docker-compose.traefik.yml cp \
  seed.projects.ts api:/seed.projects.ts
docker compose -f docker-compose.yml -f docker-compose.traefik.yml cp \
  "/path/to/NXCT Database P2 Prototype.xlsx" api:/tmp/data/P2.xlsx
docker compose -f docker-compose.yml -f docker-compose.traefik.yml cp \
  "/path/to/NXCT Database Updated LOCAL.xlsx" api:/tmp/data/LOCAL.xlsx

docker compose -f docker-compose.yml -f docker-compose.traefik.yml exec api \
  yarn seed:projects /tmp/data/P2.xlsx /tmp/data/LOCAL.xlsx

4. Verify

docker compose -f docker-compose.yml -f docker-compose.traefik.yml ps
curl -fsSL https://${DOMAIN}/api/docs -o /dev/null && echo "API proxy OK"

Default seed login (change immediately on a public deployment): admin@ctbooking.local / changeme123.

Database Schema

Core Entities

Table Description
users Engineers / operators who manage scanners
systems 5 X-ray CT scanners (Tescan UniTOM, Zeiss Metrotom, Waygate, Zeiss Versa 620, Nikon XT H 450)
customers External contacts — academic or company, with autocomplete for institutions/companies/groups
projects Primary entity: type (FATPOA/Quoted/TRAC/PhD/etc.), status lifecycle, quoted days, cost centres, document links
bookings Time-slot reservations on a system for a project, with overlap detection
nxct_cost_centres FATPOA budget tracking: scan/analysis day targets with date ranges; is_archived flag hides from dropdowns
outcomes Papers, conferences, funding linked to projects (M:M)
disciplines Lookup: Chemistry, Engineering, Physics, Life Sciences, etc.
project_status_history Every status transition with timestamp and user
project_notes Timestamped notes per project, newest first
day_quotas Annual FATPOA/Paid day targets per Nov–Oct year

API Endpoints

All endpoints prefixed with /api/, JWT-authenticated except /api/auth/login.

Resource Key Endpoints
Auth POST /auth/login
Users GET, POST /users · GET, PATCH, DELETE /users/:id
Systems GET, POST /systems · PATCH /systems/:id (isActive to archive)
Disciplines GET, POST /disciplines · PATCH, DELETE /disciplines/:id
Customers GET, POST /customers · PATCH, DELETE /:id · autocomplete (institutions, companies, groups)
Projects GET, POST /projects · GET, PATCH /:id · POST /:id/notes · GET /export/csv
Bookings GET, POST /bookings · PATCH, DELETE /:id (overlap detection on create/update)
NXCT Cost Centres GET, POST /nxct-cost-centres · PATCH /:id (isArchived to archive; ?includeArchived=true to list all)
Outcomes GET, POST /outcomes · PATCH /:id · POST /:id/projects/:projectId
Reports recently-closed · open-jobs · potential-work · jobs-to-invoice · summary · system-usage

Frontend Pages (21 components)

Section Route Description
Calendar /calendar Week and month view of all systems; double-click to book; quick-slot buttons (9–1, 1–5, all day); click a booking to edit or delete it
Projects /projects Searchable paginated list with type/status filters and CSV export
/projects/new Create form with customer autocomplete, conditional TRAC/FATPOA fields, NXCT cost centre dropdowns
/projects/:id/edit Edit form (reuses create form with pre-populated data)
/projects/:id Detail view: info panel, status update with confirmation dialog, notes, status history timeline, NXCT cost centres
Customers /customers Searchable list
/customers/new Create form with academic/company toggle, institution/company/group autocomplete
/customers/:id/edit Edit form with delete button (inline confirmation)
Search /search Tabbed search: projects (with "empty field" filter + inline edit dialog), customers, bookings
Outcomes /outcomes Paper/conference/funding tracking with create dialog
Reviews /reviews/recently-closed Projects completed/invoiced in last 7 days
/reviews/open-jobs Active FATPOA/Quoted/TRAC with inline note-add
/reviews/potential-work Discussion/Quoted/NDA pipeline with quick new-project link
/reviews/jobs-to-invoice Completed Quoted/TRAC awaiting invoice
/reviews/summary Multi-year scan/analysis day comparison by type with NXCT cost centre and day quota tables
Reports /reports/system-usage Per-system utilisation with Chart.js bar + doughnut charts and usage-type breakdown
/reports/nxct-fatpoa FATPOA projects by cost centre with progress cards (target/used/remaining)
/reports/wmg-projects WMG (TRAC) projects with scan/analysis totals
/reports/uow-projects University of Warwick projects filtered by customer institution
Admin /admin Tabbed management: Users (create/edit/delete), Systems (edit/archive), NXCT Cost Centres (edit/archive), Disciplines (create/edit/delete)

Cross-Cutting Concerns

Concern Implementation
Authentication JWT via passport-jwt; authInterceptor attaches Bearer token; authGuard protects all routes
Error handling Global errorInterceptor: 401 → auto-logout, 403 → toast, network errors → toast
Notifications NotificationService wraps PrimeNG MessageService — success/error/warn/info toasts on all mutations
Confirmations ConfirmService wraps PrimeNG ConfirmationService — promise-based, used on status changes
Validation Backend: NestJS ValidationPipe with class-validator DTOs. Frontend: required-field checks before submit
CSV Export Server-side export via /projects/export/csv with current filters applied
Booking conflicts API checks for overlapping bookings on the same system before insert/update; navigates to conflict week on error
Archiving Systems and NXCT cost centres are archived (not deleted) — archived systems are hidden from the calendar and booking dialogs
Unit tests Jest + jest-preset-angular; tests cover booking CRUD, month/week view logic, customer delete, admin manage (users, systems, cost centres, disciplines)

Key Design Decisions

  1. Prisma ORM over raw SQL — type-safe queries, automatic migrations, seed scripts. Raw schema.sql included for reference.
  2. Status history as a separate table — enables "date status changed to X" queries from the requirements.
  3. Project notes as first-class entities — multiple timestamped entries per project, not a single text field.
  4. Angular signals throughout — reactive state without RxJS boilerplate in components.
  5. Standalone components only — no NgModules in the frontend; every component self-declares its imports.
  6. Lazy-loaded routes — each page loads on demand via loadComponent().
  7. Tailwind 4 + PrimeNG — Tailwind for layout/spacing, PrimeNG for data-heavy widgets (tables, dialogs, autocomplete).

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages