Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
117 changes: 113 additions & 4 deletions app/assets/stylesheets/application.css
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,111 @@ form:has(input[name="_method"][value="delete"]) button:hover {
background: hsl(var(--destructive) / 0.9);
}

/* --------------------------------------------------------------- tables --- */
/*
* The prescriptions index is a log, so it is a real table. Seven columns will
* not fit a phone, and neither hiding columns nor collapsing each row into a
* card keeps a log scannable - so the table keeps its shape and the wrapper
* around it scrolls sideways instead.
*/

.table-scroll {
overflow-x: auto;
margin-bottom: var(--space-4);
border: 1px solid hsl(var(--border));
border-radius: var(--radius);
background: hsl(var(--card));
/* The rounded corners would otherwise be sliced off by the header band. */
overflow-y: hidden;
}

/* --content-width is a comfortable measure for reading sentences, and a log is
not sentences: seven columns do not fit in it, and scrolling sideways on a
wide monitor to reach the last two is silly. So once the window is wide
enough to spare it, the table steps out of the reading column and centres
itself on the page. Percentage margins resolve against the containing block,
so `50% - half the width` is what centres a box wider than its parent. */
@media (min-width: 64rem) {
.table-scroll {
--table-width: min(1120px, 100vw - var(--space-6));

width: var(--table-width);
margin-inline: calc(50% - var(--table-width) / 2);
}
}

table {
width: 100%;
border-collapse: collapse;
font-size: 0.9375rem;
}

th,
td {
padding: var(--space-2) var(--space-3);
text-align: left;
vertical-align: top;
}

/* Same treatment as the field captions inside the scaffold cards below, so a
column heading and a field label read as the same kind of thing. */
thead th {
border-bottom: 1px solid hsl(var(--border));
background: hsl(var(--muted));
color: hsl(var(--muted-foreground));
font-size: 0.8125rem;
font-weight: 600;
letter-spacing: 0.02em;
text-transform: uppercase;
white-space: nowrap;
}

tbody tr + tr th,
tbody tr + tr td { border-top: 1px solid hsl(var(--border)); }

tbody tr:hover { background: hsl(var(--accent) / 0.5); }

/* The row header - which medication this row is about. Everything else in the
row is a detail of it, so only this column is allowed to wrap. */
tbody th[scope="row"] {
font-weight: 550;
min-width: 11rem;
}

tbody td { white-space: nowrap; }

/* A second line under the row header: the form the medication comes in. */
.table-note {
display: block;
color: hsl(var(--muted-foreground));
font-size: 0.8125rem;
font-weight: 400;
}

/* Whether a prescription is current. The word says it; the colour only agrees
with the word, so this still reads with colour vision differences or in
print. */
.status {
display: inline-block;
padding: 1px var(--space-2);
border: 1px solid;
border-radius: 999px;
font-size: 0.8125rem;
font-weight: 600;
}

.status--active {
border-color: hsl(var(--success) / 0.4);
background: hsl(var(--success) / 0.14);
color: hsl(var(--foreground));
}

.status--stopped {
border-color: hsl(var(--border));
background: hsl(var(--muted));
color: hsl(var(--muted-foreground));
}

/* ------------------------------------------------- scaffold accommodation --- */
/*
* Everything below exists so the generator's output looks deliberate without
Expand All @@ -325,7 +430,8 @@ form:has(input[name="_method"][value="delete"]) button:hover {
medication_name and so on, so an unanchored [id^="person_"] would card every
input on every form as well. */
div[id^="person_"],
div[id^="medication_"] {
div[id^="medication_"],
div[id^="prescription_"] {
padding: var(--space-3) var(--space-4);
margin-bottom: var(--space-3);
border: 1px solid hsl(var(--border));
Expand All @@ -336,7 +442,8 @@ div[id^="medication_"] {
/* Inside those cards the pattern is <strong>Field name:</strong> value.
Demoting the label and leading the value reads as a definition list. */
div[id^="person_"] strong,
div[id^="medication_"] strong {
div[id^="medication_"] strong,
div[id^="prescription_"] strong {
display: block;
color: hsl(var(--muted-foreground));
font-size: 0.8125rem;
Expand All @@ -346,10 +453,12 @@ div[id^="medication_"] strong {
}

div[id^="person_"] > div,
div[id^="medication_"] > div { margin-bottom: var(--space-2); }
div[id^="medication_"] > div,
div[id^="prescription_"] > div { margin-bottom: var(--space-2); }

div[id^="person_"] > div:last-child,
div[id^="medication_"] > div:last-child { margin-bottom: 0; }
div[id^="medication_"] > div:last-child,
div[id^="prescription_"] > div:last-child { margin-bottom: 0; }

/* The scaffolds emit `<p style="color: green">` for the flash and
`<div style="color: red">` for the error summary. The inline colour wins on
Expand Down
70 changes: 70 additions & 0 deletions app/controllers/medication_forms_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
class MedicationFormsController < ApplicationController
before_action :set_medication_form, only: %i[ show edit update destroy ]

# GET /medication_forms or /medication_forms.json
def index
@medication_forms = MedicationForm.all
end

# GET /medication_forms/1 or /medication_forms/1.json
def show
end

# GET /medication_forms/new
def new
@medication_form = MedicationForm.new
end

# GET /medication_forms/1/edit
def edit
end

# POST /medication_forms or /medication_forms.json
def create
@medication_form = MedicationForm.new(medication_form_params)

respond_to do |format|
if @medication_form.save
format.html { redirect_to @medication_form, notice: "Medication form was successfully created." }
format.json { render :show, status: :created, location: @medication_form }
else
format.html { render :new, status: :unprocessable_content }
format.json { render json: @medication_form.errors, status: :unprocessable_content }
end
end
end

# PATCH/PUT /medication_forms/1 or /medication_forms/1.json
def update
respond_to do |format|
if @medication_form.update(medication_form_params)
format.html { redirect_to @medication_form, notice: "Medication form was successfully updated.", status: :see_other }
format.json { render :show, status: :ok, location: @medication_form }
else
format.html { render :edit, status: :unprocessable_content }
format.json { render json: @medication_form.errors, status: :unprocessable_content }
end
end
end

# DELETE /medication_forms/1 or /medication_forms/1.json
def destroy
@medication_form.destroy!

respond_to do |format|
format.html { redirect_to medication_forms_path, notice: "Medication form was successfully destroyed.", status: :see_other }
format.json { head :no_content }
end
end

private
# Use callbacks to share common setup or constraints between actions.
def set_medication_form
@medication_form = MedicationForm.find(params.expect(:id))
end

# Only allow a list of trusted parameters through.
def medication_form_params
params.expect(medication_form: [ :name ])
end
end
15 changes: 10 additions & 5 deletions app/controllers/medications_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,16 @@ def update

# DELETE /medications/1 or /medications/1.json
def destroy
@medication.destroy!

# A medication in use by a prescription refuses to be destroyed, so this
# reports the refusal rather than raising.
respond_to do |format|
format.html { redirect_to medications_path, notice: "Medication was successfully destroyed.", status: :see_other }
format.json { head :no_content }
if @medication.destroy
format.html { redirect_to medications_path, notice: "Medication was successfully destroyed.", status: :see_other }
format.json { head :no_content }
else
format.html { redirect_to @medication, alert: @medication.errors.full_messages.to_sentence, status: :see_other }
format.json { render json: @medication.errors, status: :unprocessable_content }
end
end
end

Expand All @@ -65,6 +70,6 @@ def set_medication

# Only allow a list of trusted parameters through.
def medication_params
params.expect(medication: [ :name, :medication_type_id, :current, :dosage, :frequency, :time_of_day, :form, :purpose, :start_date, :stop_date, :refill, :notes ])
params.expect(medication: [ :name, :medication_type_id, :side_effects ])
end
end
74 changes: 74 additions & 0 deletions app/controllers/prescriptions_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
class PrescriptionsController < ApplicationController
before_action :set_prescription, only: %i[ show edit update destroy ]

# GET /prescriptions or /prescriptions.json
def index
# The index reads as a log: what is being taken now, newest first, with the
# stopped ones below it as history. includes avoids a medication and a form
# query per row.
@prescriptions = Prescription.includes(:medication, :medication_form)
.order(active: :desc, start_date: :desc, created_at: :desc)
end

# GET /prescriptions/1 or /prescriptions/1.json
def show
end

# GET /prescriptions/new
def new
@prescription = Prescription.new
end

# GET /prescriptions/1/edit
def edit
end

# POST /prescriptions or /prescriptions.json
def create
@prescription = Prescription.new(prescription_params)

respond_to do |format|
if @prescription.save
format.html { redirect_to @prescription, notice: "Prescription was successfully created." }
format.json { render :show, status: :created, location: @prescription }
else
format.html { render :new, status: :unprocessable_content }
format.json { render json: @prescription.errors, status: :unprocessable_content }
end
end
end

# PATCH/PUT /prescriptions/1 or /prescriptions/1.json
def update
respond_to do |format|
if @prescription.update(prescription_params)
format.html { redirect_to @prescription, notice: "Prescription was successfully updated.", status: :see_other }
format.json { render :show, status: :ok, location: @prescription }
else
format.html { render :edit, status: :unprocessable_content }
format.json { render json: @prescription.errors, status: :unprocessable_content }
end
end
end

# DELETE /prescriptions/1 or /prescriptions/1.json
def destroy
@prescription.destroy!

respond_to do |format|
format.html { redirect_to prescriptions_path, notice: "Prescription was successfully destroyed.", status: :see_other }
format.json { head :no_content }
end
end

private
# Use callbacks to share common setup or constraints between actions.
def set_prescription
@prescription = Prescription.find(params.expect(:id))
end

# Only allow a list of trusted parameters through.
def prescription_params
params.expect(prescription: [ :medication_id, :medication_form_id, :dosage, :frequency, :time_of_day, :prescribing_doctor, :purpose, :active, :start_date, :stop_date, :notes ])
end
end
6 changes: 6 additions & 0 deletions app/helpers/application_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ def section_current?(path)
request.path == path || request.path.start_with?("#{path}/")
end

# An empty table cell reads as a rendering fault. An em dash reads as "nobody
# recorded this", which is what a blank optional field actually means.
def or_dash(value)
value.presence || "—"
end

# The value aria-current wants, or nil to leave the attribute off entirely.
# link_to drops an aria value of nil rather than rendering aria-current="".
def aria_current_section(path)
Expand Down
38 changes: 38 additions & 0 deletions app/helpers/prescriptions_helper.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
module PrescriptionsHelper
# Two lines in one column rather than two columns. Most prescriptions have a
# start and no stop, so a Stopped column would be a mostly empty stripe down
# the log.
def prescription_started(prescription)
prescription_date(prescription.start_date) || "—"
end

# What has happened since it started. Every combination of active and
# stop_date says something, and all four say it out loud: a blank line under
# a Stopped badge is indistinguishable from a column that failed to render.
def prescription_stopped(prescription)
stopped = prescription_date(prescription.stop_date)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From Claude "summary": "prescription_stopped returns nil for an inactive prescription with no stop_date, leaving a blank sub-note alongside a 'Stopped' badge — indistinguishable from a data gap.",

I was wondering if this could be a problem since stop_date is optional?


if prescription.active?
stopped ? "until #{stopped}" : "ongoing"
else
stopped ? "stopped #{stopped}" : "no stop date recorded"
end
end

# The word carries the meaning; the colour only agrees with it. A boolean
# printed raw ("true") is the one thing a caregiver scanning this cannot read
# at a glance.
def prescription_status(prescription)
if prescription.active?
tag.span "Active", class: "status status--active"
else
tag.span "Stopped", class: "status status--stopped"
end
end

# "Aug 1, 2026". Short enough to keep a log column narrow, spelled enough to
# sidestep the 8/1 vs 1/8 question.
def prescription_date(date)
date&.strftime("%b %-d, %Y")
end
end
6 changes: 5 additions & 1 deletion app/models/medication.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
class Medication < ApplicationRecord
belongs_to :medication_type
belongs_to :medication_type, optional: true

has_many :prescriptions, dependent: :restrict_with_error

validates :name, presence: true
end
5 changes: 5 additions & 0 deletions app/models/medication_form.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
class MedicationForm < ApplicationRecord
has_many :prescriptions, dependent: :nullify

validates :name, presence: true
end
3 changes: 3 additions & 0 deletions app/models/medication_type.rb
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
class MedicationType < ApplicationRecord
has_many :medications, dependent: :nullify

validates :name, presence: true
end
4 changes: 4 additions & 0 deletions app/models/prescription.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
class Prescription < ApplicationRecord
belongs_to :medication
belongs_to :medication_form, optional: true
end
Loading
Loading