Skip to content
Closed
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
230 changes: 226 additions & 4 deletions app/assets/stylesheets/application.css
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,119 @@ code, pre { font-family: var(--font-mono); font-size: 0.9375em; }

.site-main { padding-block: var(--space-5) var(--space-6); }

/* ---------------------------------------------------------------- admin --- */
/*
* The administrative area - the lists of medications, types and forms that
* are set up once rather than used daily. It is not a separate app: same
* header, same palette. The one signal that you have stepped sideways into
* setup is this second nav band, on the muted grey the sheet already uses for
* table headings and field captions.
*
* These sections still live at /medications and friends, so the band is what
* ties them together. ApplicationHelper#admin_area? decides when it shows.
*/

.admin-nav {
border-bottom: 1px solid hsl(var(--border));
background: hsl(var(--muted));
font-size: 0.9375rem;
}

.admin-nav__inner {
max-width: var(--content-width);
margin-inline: auto;
padding-inline: var(--space-3);
padding-block: var(--space-2);
display: flex;
flex-wrap: wrap;
align-items: baseline;
gap: var(--space-1) var(--space-3);
}

/* Names the band rather than labelling any one link, so the row still reads
as "Admin: Overview, Medications, ..." when the header is scrolled away.
The nav's aria-label says the same thing for a screen reader. */
.admin-nav__label {
color: hsl(var(--muted-foreground));
font-size: 0.8125rem;
font-weight: 600;
letter-spacing: 0.02em;
text-transform: uppercase;
}

.admin-nav a {
color: hsl(var(--muted-foreground));
text-decoration: none;
padding-block: var(--space-1);
}

.admin-nav a:hover { color: hsl(var(--foreground)); }

.admin-nav a[aria-current="page"] {
color: hsl(var(--primary));
font-weight: 600;
}

/* The opening sentence on /admin. Wider than body text would allow it to be
emphatic, so it stays the same size and only lightens. */
.lede {
max-width: 46ch;
color: hsl(var(--muted-foreground));
margin-bottom: var(--space-4);
}

.admin-cards {
list-style: none;
margin: 0;
padding: 0;
display: grid;
gap: var(--space-3);
/* One column on a phone, two once there is room for two readable ones. */
grid-template-columns: repeat(auto-fit, minmax(17rem, 1fr));
}

.admin-card {
position: relative;
padding: var(--space-3) var(--space-4);
border: 1px solid hsl(var(--border));
border-radius: var(--radius);
background: hsl(var(--card));
}

.admin-card:hover { border-color: hsl(var(--primary) / 0.5); }

.admin-card__title {
font-size: 1.0625rem;
margin-bottom: var(--space-1);
}

/* Stretching the one link over the whole card makes the card a click target
without adding a second link to the same place - which a screen reader would
read out twice and a keyboard user would have to tab through twice. */
.admin-card__link { text-decoration: none; }

.admin-card__link::after {
content: "";
position: absolute;
inset: 0;
border-radius: inherit;
}

/* With the pseudo-element covering the card, the focus ring would otherwise
draw around the words alone while the whole card is what activates. */
.admin-card__link:focus-visible { outline: none; }

.admin-card:has(.admin-card__link:focus-visible) {
outline: 2px solid hsl(var(--ring));
outline-offset: 2px;
}

.admin-card__description {
margin: 0;
color: hsl(var(--muted-foreground));
font-size: 0.9375rem;
}

.site-footer {
border-top: 1px solid hsl(var(--border));
color: hsl(var(--muted-foreground));
Expand Down Expand Up @@ -312,6 +425,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 +543,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 +555,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 +566,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
12 changes: 12 additions & 0 deletions app/controllers/admin_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# The landing page for the administrative area. It has no model of its own: the
# sections it lists live in ApplicationHelper#admin_sections, and each is still
# served by its own top-level controller.
#
# When these pages stop being open to everyone, this is the seam - the
# before_action that gates the area goes here and in the three reference-data
# controllers, or they all move under an Admin:: namespace inheriting from a
# base controller that carries it.
class AdminController < ApplicationController
def index
end
end
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
Loading
Loading