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
1 change: 1 addition & 0 deletions app/assets/stylesheets/components/_index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,5 @@
@forward "summary-list";
@forward "tables";
@forward "tag";
@forward "timeline";
@forward "vaccine-method";
126 changes: 126 additions & 0 deletions app/assets/stylesheets/components/_timeline.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
@use "../vendor/nhsuk-frontend" as *;

$_badge-size-mobile: 16px;
$_badge-small-size-mobile: 12px;
$_timeline-border-width: 2px;

@function _dot-size($size) {
@if $size == "default" {
@return $_badge-size-mobile;
} @else if $size == "small" {
@return $_badge-small-size-mobile;
}
}

@function _dot-ml($size) {
@return - calc(($size / 2) + ($_timeline-border-width / 2));
}

@function _dot-mt-tablet($margin) {
@return $margin - 1px;
}

@mixin _badge($size) {
$mt: 4px;
$mt-small: 6px;

height: _dot-size($size);
margin-left: _dot-ml(_dot-size($size));
margin-right: if(
$size == "default",
nhsuk-spacing(4),
nhsuk-spacing(4) + 2px
);
margin-top: if($size == "default", $mt, $mt-small);
width: _dot-size($size);

@include nhsuk-media-query($from: tablet) {
$tablet: _dot-size($size) + 4px;

height: $tablet;
margin-left: _dot-ml($tablet);
margin-top: if(
$size == "default",
_dot-mt-tablet($mt),
_dot-mt-tablet($mt-small)
);
width: $tablet;
}
}

.app-timeline {
list-style: none;
padding: 0;

@include nhsuk-responsive-margin(5, "bottom");
@include nhsuk-responsive-padding(2, "top");

&__item {
display: flex;
margin-bottom: 0;
margin-left: 12px;
margin-top: -6px;
position: relative;

@include nhsuk-responsive-padding(5, "bottom");

&:last-child {
padding: 0;

&::before {
border: none;
}
}

&::before {
border-left: $_timeline-border-width solid $color_nhsuk-grey-3;
bottom: 0;
content: "";
display: block;
left: -$_timeline-border-width;
position: absolute;
top: nhsuk-spacing(2);
width: $_timeline-border-width;
}

&--past::before {
border-color: $color_nhsuk-blue;
}
}

&__badge {
flex-shrink: 0;
z-index: 1;

@include _badge("default");

&--small {
@include _badge("small");
}
}

&__header {
font-weight: normal;
margin-bottom: 0;

@include nhsuk-font-size(19);

&.nhsuk-u-font-weight-bold .app-u-monospace {
font-weight: inherit;
}
}

&__content {
:last-child {
margin-bottom: 0;
}
}

&__description {
color: $nhsuk-secondary-text-color;
padding-top: 0;

@include nhsuk-responsive-margin(2, "bottom");
@include nhsuk-font-size(16);
}
}
12 changes: 11 additions & 1 deletion app/components/app_child_summary_component.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ def initialize(

def call
govuk_summary_list(
actions: @change_links.present? || @remove_links.present?
actions:
@change_links.present? || @remove_links.present? ||
pds_search_history_link.present?
) do |summary_list|
summary_list.with_row do |row|
row.with_key { "NHS number" }
Expand All @@ -30,6 +32,8 @@ def call
href:,
visually_hidden_text: "NHS number"
)
elsif (href = pds_search_history_link)
row.with_action(text: "PDS history", href:)
end
end

Expand Down Expand Up @@ -209,4 +213,10 @@ def format_year_group
def highlight_if(value, condition)
condition ? tag.span(value, class: "app-highlight") : value
end

def pds_search_history_link
return unless @child.is_a?(Patient) && @child.pds_lookup_match?

pds_search_history_patient_path(@child)
end
end
41 changes: 41 additions & 0 deletions app/components/app_timeline_component.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# frozen_string_literal: true

class AppTimelineComponent < ViewComponent::Base
erb_template <<-ERB
<ul class="app-timeline">
<% @items.each do |item| %>
<% next if item.blank? %>

<li class="app-timeline__item <%= 'app-timeline__item--past' if item[:is_past_item] %>">
<% if item[:active] || item[:is_past_item] %>
<svg class="app-timeline__badge" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="28" height="28" viewBox="0 0 28 28">
<circle cx="14" cy="14" r="13" fill="#005EB8"/>
</svg>
<% else %>
<svg class="app-timeline__badge app-timeline__badge--small" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 14 14">
<circle cx="7" cy="7" r="6" fill="white" stroke="#AEB7BD" stroke-width="2"/>
</svg>
<% end %>

<div class="app-timeline__content">
<h3 class="app-timeline__header <%= 'nhsuk-u-font-weight-bold' if item[:active] %>">
<%= item[:heading_text].html_safe %>
</h3>

<% if item[:description].present? %>
<p class="app-timeline__description"><%= item[:description].html_safe %></p>
<% end %>
</div>
</li>
<% end %>
</ul>
ERB

def initialize(items)
@items = items
end

def render?
@items.present?
end
end
23 changes: 23 additions & 0 deletions app/controllers/patients_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,29 @@ def log
def edit
end

def pds_search_history
latest_results = @patient.pds_search_results.includes(:import).latest_set

@timeline_items =
if latest_results.present?
latest_results
.map(&:timeline_item)
.sort_by { |item| item["created_at"] }
else
[]
end

time = latest_results&.last&.import&.processed_at

if @patient.nhs_number.present?
@timeline_items << {
active: true,
heading_text: "NHS number is #{@patient.nhs_number}",
description: time&.to_date&.to_fs(:long)
}
end
end

def invite_to_clinic
PatientLocation.find_or_create_by!(
patient: @patient,
Expand Down
8 changes: 8 additions & 0 deletions app/models/patient.rb
Original file line number Diff line number Diff line change
Expand Up @@ -656,6 +656,14 @@ class NHSNumberMismatch < StandardError
class UnknownGPPractice < StandardError
end

def latest_pds_search_result
pds_search_results.latest_set&.first&.changeset&.pds_nhs_number
end

def pds_lookup_match?
nhs_number.present? && nhs_number == latest_pds_search_result
end

private

def patient_status(association, programme:, academic_year:)
Expand Down
43 changes: 43 additions & 0 deletions app/models/pds_search_result.rb
Original file line number Diff line number Diff line change
Expand Up @@ -49,4 +49,47 @@ class PDSSearchResult < ApplicationRecord
no_postcode: 5
},
validate: true

def self.grouped_sets
records = all.to_a
grouped_records =
records.group_by do |record|
if record.import_id.present?
[:import, record.import_type, record.import_id]
else
[:date, record.created_at.to_date]
end
end
grouped_records.values
end

def self.latest_set
grouped_sets.max_by { |set| set.map(&:created_at).max }
end

def pds_nhs_number
changeset&.pds_nhs_number
end

def changeset
return unless import_id

PatientChangeset.find_by(
import_type: import_type,
import_id: import_id,
patient_id: patient_id
)
end

def timeline_item
{
is_past_item: true,
heading_text: human_enum_name(:step),
description:
I18n.t(
"activerecord.attributes.#{self.class.model_name.i18n_key}.results.#{result}",
nhs_number: nhs_number
)
}
end
end
29 changes: 29 additions & 0 deletions app/views/patients/pds_search_history.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<% content_for :before_main do %>
<%= render AppBreadcrumbComponent.new(items: [
{ text: t("dashboard.index.title"), href: dashboard_path },
{ text: t("patients.index.title"), href: patients_path },
{ text: @patient.full_name, href: patient_path(@patient) },
]) %>
<% end %>

<% page_title = "NHS number lookup history" %>
<%= h1 page_title: do %>
<span class="nhsuk-caption-l"><%= @patient.full_name %></span>
<%= page_title %>
<% end %>

<p class="nhsuk-u-reading-width">The following timeline shows how this child's NHS number was found by searching the NHS Patient Demographics Service (PDS)</p>

<%= render AppTimelineComponent.new(@timeline_items) %>

<h2 class="nhsuk-heading-m">Terms used in this lookup</h2>

<p class="nhsuk-u-reading-width">A fuzzy search finds text that matches a term closely as well as exactly.
For example, a fuzzy search can identify Jon Smith even if the term entered was John Smith.</p>

<p class="nhsuk-u-reading-width">A wildcard searches for unknown parts of text. For example, if you only
have part of a postcode: CV1, you can use a wildcard to search all records with a postcode
that includes CV1.</p>

<p class="nhsuk-u-reading-width">History refers to the child’s history, for example if they have changed
their name or address.</p>
13 changes: 13 additions & 0 deletions config/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,19 @@ en:
types:
father: dad
mother: mum
pds_search_result:
steps:
no_fuzzy_with_history: "Non-fuzzy search (with history)"
no_fuzzy_with_history_daily: "Non-fuzzy search (with history)"
no_fuzzy_without_history: "Non-fuzzy search (without history)"
no_fuzzy_with_wildcard_postcode: "Non-fuzzy search (with history and wildcard postcode)"
no_fuzzy_with_wildcard_given_name: "Non-fuzzy search (with history and wildcard first name)"
no_fuzzy_with_wildcard_family_name: "Non-fuzzy search (with history and wildcard surname)"
fuzzy: "Fuzzy search"
results:
no_matches: "No matches found"
one_match: "Found %{nhs_number}"
too_many_matches: "Too many matches found"
programme:
types:
flu: Flu
Expand Down
3 changes: 2 additions & 1 deletion config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -175,8 +175,9 @@
end

member do
get "log"
post "invite-to-clinic"
get "log"
get "pds-search-history"

get "edit/nhs-number",
controller: "patients/edit",
Expand Down
Loading