itself, outside its panel)
+// also closes it. No custom focus-trap or key handling to maintain.
+export default class extends Controller {
+ static targets = ['dialog']
+ static values = { openOnConnect: Boolean }
+
+ connect () {
+ if (this.openOnConnectValue) this.open()
+ }
+
+ open () {
+ this.dialogTarget.showModal()
+ }
+
+ close () {
+ this.dialogTarget.close()
+ }
+
+ backdropClose (event) {
+ if (event.target === this.dialogTarget) {
+ this.close()
+ }
+ }
+}
diff --git a/app/javascript/controllers/multiple_select_controller.js b/app/javascript/controllers/multiple_select_controller.js
index 739c4dcaa6..dea148609e 100644
--- a/app/javascript/controllers/multiple_select_controller.js
+++ b/app/javascript/controllers/multiple_select_controller.js
@@ -1,6 +1,18 @@
import { Controller } from '@hotwired/stimulus'
import TomSelect from 'tom-select'
+// Open the dropdown above the control when there isn't room below, so a field
+// near the bottom of the page keeps its menu on-screen. `this` is the TomSelect
+// instance when these run.
+function onDropdownOpen (dropdown) {
+ const rect = this.control.getBoundingClientRect()
+ const needed = dropdown.offsetHeight || 240
+ this.wrapper.classList.toggle('ts-flip-up', window.innerHeight - rect.bottom < needed && rect.top > needed)
+}
+function onDropdownClose () {
+ this.wrapper.classList.remove('ts-flip-up')
+}
+
export default class extends Controller {
static targets = ['select', 'option', 'item', 'hiddenItem', 'selectAllOption']
static values = {
@@ -29,7 +41,9 @@ export default class extends Controller {
remove_button: {
title: 'Remove this item'
}
- }
+ },
+ onDropdownOpen,
+ onDropdownClose
})
}
@@ -55,7 +69,9 @@ export default class extends Controller {
let initItems = this.selectedItemsValue
if (showAllOptionCheck) {
const emptyItem = [' ']
- initItems = hasInitialItems ? emptyItem.concat(this.selectedItemsValue) : orderedOptionVals
+ // Load blank (placeholder) when nothing is pre-selected; the dropdown's
+ // "Select/Unselect all" still selects everything on demand.
+ initItems = hasInitialItems ? emptyItem.concat(this.selectedItemsValue) : []
}
const dropdownOptions = showAllOptionCheck
@@ -64,6 +80,8 @@ export default class extends Controller {
/* eslint-disable no-new */
new TomSelect(this.selectTarget, {
+ onDropdownOpen,
+ onDropdownClose,
onItemRemove: function (value) {
if (value === ' ') {
this.clear()
diff --git a/app/javascript/metrics.js b/app/javascript/metrics.js
new file mode 100644
index 0000000000..16a1e1986c
--- /dev/null
+++ b/app/javascript/metrics.js
@@ -0,0 +1,4 @@
+import { application } from './controllers/application'
+import ChartHoverController from './controllers/chart_hover_controller'
+
+application.register('chart-hover', ChartHoverController)
diff --git a/app/javascript/src/display_app_metric.js b/app/javascript/src/display_app_metric.js
deleted file mode 100644
index 4df412b94c..0000000000
--- a/app/javascript/src/display_app_metric.js
+++ /dev/null
@@ -1,242 +0,0 @@
-import { Chart, registerables } from 'chart.js'
-import 'chartjs-adapter-luxon'
-
-const { Notifier } = require('./notifier')
-
-Chart.register(...registerables)
-
-const days = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']
-
-$(() => { // JQuery's callback for the DOM loading
- const caseContactCreationTimesBubbleChart = document.getElementById('caseContactCreationTimeBubbleChart')
- const monthLineChart = document.getElementById('monthLineChart')
- const uniqueUsersMonthLineChart = document.getElementById('uniqueUsersMonthLineChart')
-
- const notificationsElement = $('#notifications')
- const pageNotifier = notificationsElement.length ? new Notifier(notificationsElement) : null
-
- if (caseContactCreationTimesBubbleChart) {
- fetchDataAndCreateChart('/health/case_contacts_creation_times_in_last_week', caseContactCreationTimesBubbleChart, function (data) {
- const timestamps = data.timestamps
- const graphData = formatTimestampsAsBubbleChartData(timestamps)
- createChart(caseContactCreationTimesBubbleChart, graphData)
- })
- }
-
- if (monthLineChart) {
- fetchDataAndCreateChart('/health/monthly_line_graph_data', monthLineChart, function (data) {
- console.log(data)
- createLineChartForCaseContacts(monthLineChart, data)
- })
- }
-
- if (uniqueUsersMonthLineChart) {
- fetchDataAndCreateChart('/health/monthly_unique_users_graph_data', uniqueUsersMonthLineChart, function (data) {
- console.log(data)
- createLineChartForUniqueUsersMonthly(uniqueUsersMonthLineChart, data)
- })
- }
-
- function fetchDataAndCreateChart (url, chartElement, successCallback) {
- $.ajax({
- type: 'GET',
- url,
- success: successCallback,
- error: handleAjaxError
- })
- }
-
- function handleAjaxError (xhr, status, error) {
- console.error('Failed to fetch data for case contact entry times chart display')
- console.error(error)
- pageNotifier?.notify('Failed to display metric chart. Check the console for error details.', 'error')
- }
-})
-
-function formatTimestampsAsBubbleChartData (timestamps) {
- const bubbleDataAsObject = {}
-
- for (const timestamp of timestamps) {
- const contactCreationTime = new Date(timestamp * 1000)
- const day = contactCreationTime.getDay()
- const hour = contactCreationTime.getHours()
-
- // Group case contacts with the same hour and day creation time into the same data point
-
- let dayData
-
- if (!(day in bubbleDataAsObject)) {
- dayData = {}
- bubbleDataAsObject[day] = dayData
- } else {
- dayData = bubbleDataAsObject[day]
- }
-
- if (!(hour in dayData)) {
- dayData[hour] = 1
- } else {
- dayData[hour]++
- }
- }
-
- // Flatten data points
-
- const bubbleDataAsArray = []
-
- for (const day in bubbleDataAsObject) {
- const hours = bubbleDataAsObject[day]
-
- for (const hour in hours) {
- bubbleDataAsArray.push({
- x: hour,
- y: day,
- r: Math.sqrt(hours[hour]) * 4
- })
- }
- }
-
- return bubbleDataAsArray
-}
-
-function createChart (chartElement, dataset) {
- const ctx = chartElement.getContext('2d')
-
- return new Chart(ctx, {
- type: 'bubble',
- data: {
- datasets: [
- {
- label: 'Case Contact Creation Times',
- data: dataset,
- backgroundColor: 'rgba(255, 99, 132, 0.2)',
- borderColor: 'rgba(255, 99, 132, 1)'
- }
- ]
- },
- options: {
- scales: {
- x: {
- min: 0,
- max: 23,
- ticks: {
- beginAtZero: true,
- stepSize: 1
- }
- },
- y: {
- min: 0,
- max: 6,
- ticks: {
- beginAtZero: true,
- callback: getYTickCallback,
- stepSize: 1
- }
- }
- },
- plugins: {
- legend: {
- display: false
- },
- title: {
- display: true,
- font: {
- size: 18
- },
- text: 'Case Contact Creation Times in the Past Week'
- },
- tooltip: {
- callbacks: {
- label: getTooltipLabelCallback
- }
- }
- }
- }
- })
-}
-
-function getYTickCallback (value) {
- return days[value]
-}
-
-function getTooltipLabelCallback (context) {
- const bubbleData = context.dataset.data[context.dataIndex]
- const caseContactCountSqrt = bubbleData.r / 4
- return `${Math.round(caseContactCountSqrt * caseContactCountSqrt)} case contacts created on ${days[bubbleData.y]} at ${bubbleData.x}:00`
-}
-
-function createLineChartForCaseContacts (chartElement, dataset) {
- const datasetLabels = ['Total Case Contacts', 'Total Case Contacts with Notes', 'Total Case Contact Users']
- return createLineChart(chartElement, dataset, 'Case Contact Creation', datasetLabels)
-}
-
-function createLineChartForUniqueUsersMonthly (chartElement, dataset) {
- const datasetLabels = ['Total Volunteers', 'Total Supervisors', 'Total Admins', 'Total logged Volunteers']
- return createLineChart(chartElement, dataset, 'Monthly Active Users', datasetLabels)
-}
-
-function createLineChart (chartElement, dataset, chartTitle, datasetLabels) {
- const ctx = chartElement.getContext('2d')
- const allMonths = extractChartData(dataset, 0)
- const datasets = []
- const colors = ['#308af3', '#48ba16', '#FF0000', '#FFA500']
-
- for (let i = 1; i < dataset[0].length; i++) {
- const data = extractChartData(dataset, i)
- const label = datasetLabels[i - 1]
- const color = colors[i - 1]
- datasets.push(createLineChartDataset(label, data, color, color))
- }
-
- return new Chart(ctx, {
- type: 'line',
- data: {
- labels: allMonths,
- datasets
- },
- options: createChartOptions(chartTitle)
- })
-}
-
-function extractChartData (dataset, index) {
- return dataset.map(data => data[index])
-}
-
-function createLineChartDataset (label, data, borderColor, pointBackgroundColor) {
- return {
- label,
- data,
- fill: false,
- borderColor,
- pointBackgroundColor,
- pointBorderWidth: 2,
- pointHoverBackgroundColor: '#fff',
- pointHoverBorderWidth: 2,
- lineTension: 0.05
- }
-}
-
-function createChartOptions (label) {
- return {
- legend: { display: true },
- plugins: {
- legend: { display: true, position: 'bottom' },
- title: {
- display: true,
- font: { size: 18 },
- text: label
- },
- tooltips: {
- callbacks: {
- label: function (tooltipItem, data) {
- let label = data.datasets[tooltipItem.datasetIndex].label || ''
- if (label) {
- label += ': '
- }
- label += Math.round(tooltipItem.yLabel * 100) / 100
- return label
- }
- }
- }
- }
- }
-}
diff --git a/app/models/case_contact.rb b/app/models/case_contact.rb
index 3e73ea490e..32a02dc3ba 100644
--- a/app/models/case_contact.rb
+++ b/app/models/case_contact.rb
@@ -16,8 +16,10 @@ class CaseContact < ApplicationRecord
allow_nil: true
}
# NOTE: 'extra' day is a temporary fix for user selecting current date, but this validation failing
+ # NOTE: a lambda so the cutoff is evaluated per-validation, not frozen at class-load time (which
+ # made specs flaky when the class first loaded under travel_to a past date).
validates :occurred_at, comparison: {
- less_than: Time.zone.tomorrow + 1.day,
+ less_than: -> { Time.zone.tomorrow + 1.day },
message: :cant_be_future,
allow_nil: true
}
diff --git a/app/models/concerns/name_presentation.rb b/app/models/concerns/name_presentation.rb
new file mode 100644
index 0000000000..0f7ffc93db
--- /dev/null
+++ b/app/models/concerns/name_presentation.rb
@@ -0,0 +1,17 @@
+# Shared name-formatting helpers. User display names should never show an
+# honorific prefix (Mr./Mrs./Ms./...) anywhere in the app — see User#display_name.
+module NamePresentation
+ HONORIFICS = %w[mr mrs ms miss mx dr prof rev sir madam].freeze
+
+ module_function
+
+ # "Mrs. Hung Bergstrom" -> "Hung Bergstrom"; names without a leading
+ # honorific are returned unchanged.
+ def strip_honorific(name)
+ parts = name.to_s.split(" ")
+ return name.to_s if parts.size <= 1
+
+ leading = parts.first.sub(/\.\z/, "").downcase
+ HONORIFICS.include?(leading) ? parts.drop(1).join(" ") : name.to_s
+ end
+end
diff --git a/app/models/contact_type_group.rb b/app/models/contact_type_group.rb
index a2c6f79a5b..1a84dcb9cd 100644
--- a/app/models/contact_type_group.rb
+++ b/app/models/contact_type_group.rb
@@ -11,14 +11,16 @@ class ContactTypeGroup < ApplicationRecord
scope :alphabetically, -> { order(:name) }
+ # App-shipped defaults use sentence case (design system), keeping proper nouns/acronyms
+ # (CASA, IEP). Org admins can still rename them; only these seeded defaults are cased here.
DEFAULT_CONTACT_TYPE_GROUPS = {
CASA: ["Youth", "Supervisor"],
- Family: ["Parent", "Other Family", "Sibling", "Grandparent", "Aunt Uncle or Cousin", "Fictive Kin"],
- Placement: ["Foster Parent", "Caregiver Family", "Therapeutic Agency Worker"],
- "Social Services": ["Social Worker"],
+ Family: ["Parent", "Other family", "Sibling", "Grandparent", "Aunt, uncle, or cousin", "Fictive kin"],
+ Placement: ["Foster parent", "Caregiver family", "Therapeutic agency worker"],
+ "Social services": ["Social worker"],
Legal: ["Court", "Attorney"],
- Health: ["Medical Professional", "Mental Health Therapist", "Other Therapist", "Psychiatric Practitioner"],
- Education: ["School", "Guidance Counselor", "Teacher", "IEP Team"]
+ Health: ["Medical professional", "Mental health therapist", "Other therapist", "Psychiatric practitioner"],
+ Education: ["School", "Guidance counselor", "Teacher", "IEP team"]
}.freeze
class << self
diff --git a/app/services/admin_dashboard.rb b/app/services/admin_dashboard.rb
new file mode 100644
index 0000000000..6b9bdcfc53
--- /dev/null
+++ b/app/services/admin_dashboard.rb
@@ -0,0 +1,65 @@
+# Builds an admin's org-wide landing dashboard: chapter-level stats plus the cases that
+# need action (unassigned, or not contacted recently). Queries are aggregate/batched
+# (active cases + one grouped contact lookup + count queries) so this stays cheap at org
+# scale — do not reintroduce per-case/per-volunteer queries here.
+class AdminDashboard
+ FOLLOWUP_DAYS = Volunteer::CONTACT_MADE_IN_DAYS_NUM
+ ATTENTION_LIMIT = 6
+
+ def initialize(casa_org)
+ @casa_org = casa_org
+ end
+
+ def active_cases
+ @active_cases ||= @casa_org.casa_cases.active.order(:case_number).to_a
+ end
+
+ def unassigned_cases
+ @unassigned_cases ||= active_cases.reject { |casa_case| assigned_case_ids.include?(casa_case.id) }
+ end
+
+ def cases_needing_contact
+ @cases_needing_contact ||= active_cases.select do |casa_case|
+ last = last_contacts[casa_case.id]
+ last.nil? || (Date.current - last.to_date).to_i > FOLLOWUP_DAYS
+ end
+ end
+
+ def needs_attention
+ unassigned_cases.first(ATTENTION_LIMIT)
+ end
+
+ def empty?
+ active_cases.empty? && active_volunteers.zero?
+ end
+
+ def stats
+ {
+ volunteers: active_volunteers,
+ cases: active_cases.size,
+ unassigned: unassigned_cases.size,
+ needs_contact: cases_needing_contact.size
+ }
+ end
+
+ private
+
+ def active_volunteers
+ @active_volunteers ||= Volunteer.in_organization(@casa_org).active.count
+ end
+
+ def assigned_case_ids
+ @assigned_case_ids ||= CaseAssignment.active
+ .where(casa_case_id: active_cases.map(&:id))
+ .distinct
+ .pluck(:casa_case_id)
+ .to_set
+ end
+
+ def last_contacts
+ @last_contacts ||= CaseContact
+ .where(contact_made: true, casa_case_id: active_cases.map(&:id))
+ .group(:casa_case_id)
+ .maximum(:occurred_at)
+ end
+end
diff --git a/app/services/supervisor_dashboard.rb b/app/services/supervisor_dashboard.rb
new file mode 100644
index 0000000000..fff99a3b90
--- /dev/null
+++ b/app/services/supervisor_dashboard.rb
@@ -0,0 +1,96 @@
+# Builds the data for a supervisor's landing dashboard: the volunteers assigned to
+# them, each volunteer's contact-follow-up status, and the summary stats up top.
+#
+# NOTE: runs a few queries per volunteer. Fine for a supervisor's assigned volunteers
+# (typically a handful); revisit with a batched query (see VolunteerDatatable) if a
+# supervisor ever carries a very large roster.
+class SupervisorDashboard
+ AVATAR_COLORS = %w[sky violet emerald amber rose teal indigo].freeze
+ RECENT_CONTACT_DAYS = 60
+ RECENT_HOURS_DAYS = 30
+
+ Row = Struct.new(:volunteer, :cases_count, :status, :last_contact_on, :contacts_recent, :minutes_recent, keyword_init: true) do
+ def needs_followup?
+ status == :follow_up
+ end
+
+ def no_cases?
+ status == :no_cases
+ end
+
+ def avatar_color
+ AVATAR_COLORS[volunteer.id % AVATAR_COLORS.size]
+ end
+
+ def last_contact_label
+ return "No contact logged" unless last_contact_on
+
+ days = (Date.current - last_contact_on.to_date).to_i
+ case days
+ when 0 then "Today"
+ when 1 then "Yesterday"
+ else "#{days} days ago"
+ end
+ end
+
+ def hours_label
+ return "—" if minutes_recent.zero?
+
+ hours, minutes = minutes_recent.divmod(60)
+ [("#{hours}h" if hours.positive?), ("#{minutes}m" if minutes.positive?)].compact.join(" ")
+ end
+ end
+
+ def initialize(supervisor)
+ @supervisor = supervisor
+ end
+
+ def volunteers
+ @volunteers ||= @supervisor.volunteers.to_a
+ end
+
+ def rows
+ @rows ||= volunteers.map { |volunteer| build_row(volunteer) }
+ end
+
+ def needs_attention
+ @needs_attention ||= rows.select(&:needs_followup?)
+ end
+
+ def stats
+ {
+ active: rows.size,
+ needs_followup: needs_attention.size,
+ no_cases: rows.count(&:no_cases?),
+ hours_label: format_hours(rows.sum(&:minutes_recent))
+ }
+ end
+
+ private
+
+ def build_row(volunteer)
+ cases_count = volunteer.actively_assigned_and_active_cases.size
+ made = CaseContact.where(creator_id: volunteer.id, contact_made: true)
+
+ Row.new(
+ volunteer: volunteer,
+ cases_count: cases_count,
+ status: status_for(volunteer, cases_count),
+ last_contact_on: made.maximum(:occurred_at),
+ contacts_recent: made.where(occurred_at: RECENT_CONTACT_DAYS.days.ago.to_date..).count,
+ minutes_recent: made.where(occurred_at: RECENT_HOURS_DAYS.days.ago.to_date..).sum(:duration_minutes).to_i
+ )
+ end
+
+ def status_for(volunteer, cases_count)
+ return :no_cases if cases_count.zero?
+
+ volunteer.made_contact_with_all_cases_in_days? ? :on_track : :follow_up
+ end
+
+ def format_hours(minutes)
+ return "0h" if minutes.zero?
+
+ "#{(minutes / 60.0).round(1)}h"
+ end
+end
diff --git a/app/services/volunteer_dashboard.rb b/app/services/volunteer_dashboard.rb
new file mode 100644
index 0000000000..71cdf1fbdf
--- /dev/null
+++ b/app/services/volunteer_dashboard.rb
@@ -0,0 +1,62 @@
+# Builds a volunteer's landing dashboard: their actively-assigned active cases, each
+# case's contact-follow-up status, and the summary stats up top. Per-case last-contact
+# lookups are batched into one query, so this is safe for a volunteer's roster of cases.
+class VolunteerDashboard
+ RECENT_HOURS_DAYS = 30
+
+ Row = Struct.new(:casa_case, :last_contact_on, keyword_init: true) do
+ def needs_contact?
+ last_contact_on.nil? || (Date.current - last_contact_on.to_date).to_i > Volunteer::CONTACT_MADE_IN_DAYS_NUM
+ end
+
+ def status
+ needs_contact? ? :needs_contact : :on_track
+ end
+
+ def last_contact_label
+ return "No contact logged" unless last_contact_on
+
+ days = (Date.current - last_contact_on.to_date).to_i
+ case days
+ when 0 then "Today"
+ when 1 then "Yesterday"
+ else "#{days} days ago"
+ end
+ end
+ end
+
+ def initialize(volunteer)
+ @volunteer = volunteer
+ end
+
+ def cases
+ @cases ||= @volunteer.actively_assigned_and_active_cases.to_a
+ end
+
+ def rows
+ @rows ||= cases
+ .map { |casa_case| Row.new(casa_case: casa_case, last_contact_on: last_contacts[casa_case.id]) }
+ .sort_by { |row| [row.needs_contact? ? 0 : 1, row.last_contact_on || Date.new(0)] }
+ end
+
+ def needs_attention
+ @needs_attention ||= rows.select(&:needs_contact?)
+ end
+
+ def stats
+ {
+ cases: rows.size,
+ needs_contact: needs_attention.size,
+ hours_label: @volunteer.hours_spent_in_days(RECENT_HOURS_DAYS).presence || "0h"
+ }
+ end
+
+ private
+
+ def last_contacts
+ @last_contacts ||= CaseContact
+ .where(creator_id: @volunteer.id, contact_made: true, casa_case_id: cases.map(&:id))
+ .group(:casa_case_id)
+ .maximum(:occurred_at)
+ end
+end
diff --git a/app/views/banners/index.html.erb b/app/views/banners/index.html.erb
index fdfa86d7f8..c8a92d5141 100644
--- a/app/views/banners/index.html.erb
+++ b/app/views/banners/index.html.erb
@@ -48,7 +48,7 @@
<%= banner_expiration_time_in_words(banner) %>
- <%= banner.user.display_name %>
+ <%= formatted_name(banner.user.display_name) %>
<%= time_ago_in_words(banner.updated_at) %> ago
diff --git a/app/views/casa_cases/_case_contact_card.html.erb b/app/views/casa_cases/_case_contact_card.html.erb
new file mode 100644
index 0000000000..ae5604be15
--- /dev/null
+++ b/app/views/casa_cases/_case_contact_card.html.erb
@@ -0,0 +1,71 @@
+<%# Tailwind case-contact card for the case-show page. Show-specific (the shared
+ case_contacts/_case_contact partial is still Bootstrap and used by un-migrated
+ pages); consolidate when the contacts index migrates. `data-case-contact` is the
+ count hook the show spec targets. %>
+
+
+
+
+
+
+
+
+ <%= "[DELETE] " if policy(contact).restore? && contact.deleted? %><%= contact.decorate.contact_groups %>
+
+ <% unless contact.active? %>
+ Draft
+ <% end %>
+ <% if policy(contact).restore? && contact.deleted? %>
+ <%= link_to "Undelete", restore_case_contact_path(contact.id), method: :post, data: {turbo: false},
+ class: "text-xs font-medium text-brand-600 hover:text-brand-700 focus-visible:underline focus-visible:outline-none" %>
+ <% end %>
+
+
<%= contact.decorate.contact_types %>
+
<%= contact.decorate.subheading %>
+
+ <% answers = contact.contact_topic_answers.reject { |answer| answer.value.blank? } %>
+ <% if answers.any? || contact.notes.present? %>
+
+ <% answers.each do |answer| %>
+
+
<%= answer.contact_topic.question %>
+ <%= answer.value %>
+
+ <% end %>
+ <% if contact.notes.present? %>
+
+
Additional Notes
+ <%= contact.notes %>
+
+ <% end %>
+
+ <% end %>
+
+
+
+
+
+ Created by
+ <% creator = contact.creator %>
+ <% if policy(contact).edit? && !current_user.volunteer? && creator %>
+ <% creator_path = creator.supervisor? ? edit_supervisor_path(creator) : (creator.casa_admin? ? edit_users_path : edit_volunteer_path(creator)) %>
+ <%= link_to formatted_name(creator.display_name), creator_path, data: {turbo: false}, class: "font-medium text-brand-600 hover:text-brand-700" %>
+ <% else %>
+ <%= formatted_name(creator&.display_name) %>
+ <% end %>
+
+
+ <% if policy(contact).update? %>
+ <%= render "casa_cases/case_contact_followup", contact: contact, followup: contact.requested_followup %>
+ <%= link_to edit_case_contact_path(contact), data: {turbo: false}, class: button_classes(:secondary) do %>
+ Edit
+ <% end %>
+ <% end %>
+ <% if policy(contact).destroy? && !contact.deleted? %>
+ <%= link_to case_contact_path(contact.id), method: :delete, data: {turbo: false}, class: button_classes(:danger_outline) do %>
+ Delete
+ <% end %>
+ <% end %>
+
+
+
diff --git a/app/views/casa_cases/_case_contact_followup.html.erb b/app/views/casa_cases/_case_contact_followup.html.erb
new file mode 100644
index 0000000000..8b28efb642
--- /dev/null
+++ b/app/views/casa_cases/_case_contact_followup.html.erb
@@ -0,0 +1,13 @@
+<%# Follow-up control for a case contact on the Tailwind case-show card. Keeps the
+ .followup-button class + followup-button- id so the existing jQuery handler
+ (src/case_contact.js) binds the "Make Reminder" prompt. Labels stay Title Case
+ because system specs click them by exact text. %>
+<% if followup %>
+ <%= button_to resolve_followup_path(followup), method: :patch, id: "resolve", data: {turbo: false}, form_class: "inline", class: button_classes(:success) do %>
+ Resolve Reminder
+ <% end %>
+<% else %>
+
+ Make Reminder
+
+<% end %>
diff --git a/app/views/casa_cases/_court_dates.html.erb b/app/views/casa_cases/_court_dates.html.erb
index 759cc25fda..34995a9fbf 100644
--- a/app/views/casa_cases/_court_dates.html.erb
+++ b/app/views/casa_cases/_court_dates.html.erb
@@ -1,24 +1,25 @@
+<%# Court dates list for the CASA case edit page (casa_app / Tailwind). Edit-only partial
+ (the court-date FORM fields live in court_dates/_fields, which stays Bootstrap). %>
<% court_dates = casa_case.court_dates.includes(:hearing_type).ordered_ascending.load %>
-Court dates:
+
+
Court dates
+ <%= link_to new_casa_case_court_date_path(casa_case), class: button_classes(:secondary) do %>
+ Add a court date
+ <% end %>
+
<% if court_dates.empty? %>
- No Court Dates
+ No court dates yet.
<% else %>
-
+
<% court_dates.each do |pcd| %>
-
-
- <%= link_to(pcd.decorate.court_date_info, casa_case_court_date_path(casa_case, pcd), class: "court-date-link") %>
- <%= render 'calendar_button',
- id: "court-date-#{pcd.id}",
- title: "Court Date #{pcd.casa_case.case_number}",
- date: {
- start: pcd.date.strftime("%Y-%m-%d"),
- end: pcd.date.strftime("%Y-%m-%d")
- },
+
+ <%= link_to pcd.decorate.court_date_info, casa_case_court_date_path(casa_case, pcd),
+ class: "text-sm font-medium text-brand-600 hover:text-brand-700" %>
+ <%= render "calendar_button", id: "court-date-#{pcd.id}", title: "Court Date #{pcd.casa_case.case_number}",
+ date: {start: pcd.date.strftime("%Y-%m-%d"), end: pcd.date.strftime("%Y-%m-%d")},
tooltip: "Add court date to calendar" %>
-
<% if report = pcd.latest_associated_report %>
- <%= link_to(rails_blob_path(report, disposition: 'attachment')) do %>
+ <%= link_to rails_blob_path(report, disposition: "attachment"), class: "text-xs font-medium text-slate-500 hover:text-slate-700" do %>
(Attached Report)
<% end %>
<% end %>
@@ -26,11 +27,3 @@
<% end %>
<% end %>
-
-
- <%= link_to new_casa_case_court_date_path(casa_case), class: "main-btn btn-sm primary-btn btn-hover ml-3" do %>
-
- Add a court date
- <% end %>
-
-
diff --git a/app/views/casa_cases/_court_order_fields.html.erb b/app/views/casa_cases/_court_order_fields.html.erb
new file mode 100644
index 0000000000..2f6b920613
--- /dev/null
+++ b/app/views/casa_cases/_court_order_fields.html.erb
@@ -0,0 +1,21 @@
+<%# One court order entry (casa_app / Tailwind). Preserves the nested-form + court-order-form
+ Stimulus hooks (.nested-form-wrapper, .court-order-text-entry, .implementation-status,
+ data-type, data-new-record, the remove data-action, the hidden _destroy) so the shared
+ court-order-form controller keeps working. Mirrors shared/_court_order_form (Bootstrap,
+ still used by the legacy court-date pages). %>
+
+
+ <%= f.text_area :text, rows: 2, "aria-label": "Court order text", placeholder: "Describe the court order",
+ class: "court-order-text-entry block w-full rounded-lg border border-slate-300 px-3.5 py-2.5 text-sm text-slate-900 shadow-sm placeholder:text-slate-500 focus:border-brand-500 focus:ring-2 focus:ring-brand-500/30 focus:outline-none" %>
+
+
+ <%= f.select :implementation_status, court_order_select_options, {include_blank: "Set Implementation Status"},
+ {"aria-label": "Implementation status", class: "implementation-status block w-full appearance-none rounded-lg border border-slate-300 bg-white py-2.5 pl-3.5 pr-9 text-sm text-slate-900 shadow-sm focus:border-brand-500 focus:ring-2 focus:ring-brand-500/30 focus:outline-none"} %>
+
+
+
+ Delete
+
+ <%= f.hidden_field :_destroy %>
+
diff --git a/app/views/casa_cases/_court_orders.html.erb b/app/views/casa_cases/_court_orders.html.erb
new file mode 100644
index 0000000000..b3554f1481
--- /dev/null
+++ b/app/views/casa_cases/_court_orders.html.erb
@@ -0,0 +1,91 @@
+<%# Court orders for the CASA case edit page (casa_app / Tailwind). Self-contained so the
+ shared Bootstrap shared/_court_order_list (still used by the legacy court-date pages) is
+ left intact. Preserves the court-order-form Stimulus targets (template / target /
+ selectedCourtOrder) and the copy-from-sibling JS hooks: `copy-court-button` plus the
+ hidden `casa_case` field, which casa_case.js reads via the button's next sibling. %>
+Court orders
+Please check that you didn't enter any youth names.
+
+
+ <%= form.fields_for :case_court_orders, CaseCourtOrder.new, child_index: "NEW_RECORD" do |ff| %>
+ <%= render "casa_cases/court_order_fields", f: ff %>
+ <% end %>
+
+
+
+ <% if siblings_casa_cases && siblings_casa_cases.count >= 1 %>
+
+
+
+
Copy all orders from case
+
+
+ Select a case
+ <% siblings_casa_cases.each do |scc| %>
+ <%= scc.case_number %>
+ <% end %>
+
+
+
+
+ <%= button_tag "Copy", type: "button", id: "copy-court-button", disabled: true, data: {copy_court_orders_target: "button", action: "copy-court-orders#open"}, class: button_classes(:secondary) %>
+
+
+
+
+ <%= render Dialog::HeaderComponent.new(title: "Copy court orders?", icon: "bi bi-files", variant: :info) %>
+ <%= render Dialog::BodyComponent.new do %>
+ Copy all orders from case # ? They are added to this case and existing orders are kept.
+ <% end %>
+ <%= render Dialog::FooterComponent.new do %>
+ Cancel
+ Yes, copy
+ <% end %>
+
+
+
+ <% end %>
+
+
+ <%= form.fields_for :case_court_orders do |ff| %>
+ <%= render "casa_cases/court_order_fields", f: ff %>
+ <% end %>
+
+
+
+
+
+ <%= label_tag :selectedCourtOrder, "Court Order Type", class: "mb-1.5 block text-sm font-medium text-slate-700" %>
+
+ <%= select_tag :selectedCourtOrder, options_for_select(CaseCourtOrder.court_order_options),
+ include_blank: "Create custom court order", data: {court_order_form_target: "selectedCourtOrder"},
+ class: "block w-full appearance-none rounded-lg border border-slate-300 bg-white py-2.5 pl-3.5 pr-9 text-sm text-slate-900 shadow-sm focus:border-brand-500 focus:ring-2 focus:ring-brand-500/30 focus:outline-none" %>
+
+
+
+ <%= button_tag type: "button", id: "add-standard-court-order-button", class: button_classes(:secondary), data: {action: "court-order-form#add"} do %>
+
Add a court order
+ <% end %>
+
+
+
+<%# Confirm removing a persisted court order. Design-system Dialog: the modal controller
+ centers it and closes it (backdrop / Cancel / X); court-order-form opens it from #remove
+ and does the removal on #confirmRemove. %>
+
+
+ <%= render Dialog::HeaderComponent.new(title: "Remove this court order?", icon: "bi bi-exclamation-triangle", variant: :danger) %>
+ <%= render Dialog::BodyComponent.new do %>
+ Are you sure you want to remove this court order? Doing so will delete all records of it unless it was included in a previous court report.
+ <% end %>
+ <%= render Dialog::FooterComponent.new do %>
+ Cancel
+ Yes, remove
+ <% end %>
+
+
diff --git a/app/views/casa_cases/_court_report_modal.html.erb b/app/views/casa_cases/_court_report_modal.html.erb
new file mode 100644
index 0000000000..35d0de1198
--- /dev/null
+++ b/app/views/casa_cases/_court_report_modal.html.erb
@@ -0,0 +1,40 @@
+<%# Court report generator (design-system dialog + court-report Stimulus controller).
+ Locals:
+ casa_case the case (required)
+ trigger_class classes for the trigger button (required)
+ wrapper_class wrapper layout (default "inline-flex"; use "contents" in a dropdown menu) %>
+<%= render Dialog::GroupComponent.new(id: "generate-court-report", label: "Download court report", controllers: "court-report", wrapper_class: local_assigns.fetch(:wrapper_class, "inline-flex")) do |dialog| %>
+ <% dialog.with_trigger do %>
+
+ Generate Court Report
+
+ <% end %>
+ <%= form_with url: generate_case_court_reports_path, local: false, data: {court_report_target: "form"} do |form| %>
+ <%= render Dialog::HeaderComponent.new(title: "Download court report") %>
+ <%= render Dialog::BodyComponent.new(classes: "space-y-4") do %>
+ Choose a date range for <%= casa_case.case_number %> .
+
+ <%= form.hidden_field :case_number, value: casa_case.case_number %>
+ <%= form.hidden_field :time_zone, data: {court_report_target: "timeZone"} %>
+
+
+ <%= form.label :start_date, "Starting from", class: "mb-1.5 block text-sm font-medium text-slate-700" %>
+ <%= form.date_field :start_date, value: (casa_case.most_recent_past_court_date&.date || Date.current), required: true,
+ class: "block w-full rounded-lg border border-slate-300 px-3.5 py-2.5 text-sm text-slate-900 shadow-sm focus:border-brand-500 focus:ring-2 focus:ring-brand-500/30 focus:outline-none" %>
+
+
+ <%= form.label :end_date, "Ending at", class: "mb-1.5 block text-sm font-medium text-slate-700" %>
+ <%= form.date_field :end_date, value: Date.current, required: true,
+ class: "block w-full rounded-lg border border-slate-300 px-3.5 py-2.5 text-sm text-slate-900 shadow-sm focus:border-brand-500 focus:ring-2 focus:ring-brand-500/30 focus:outline-none" %>
+
+
+ <% end %>
+ <%= render Dialog::FooterComponent.new do %>
+ Cancel
+
+
+ Generate Report
+
+ <% end %>
+ <% end %>
+<% end %>
diff --git a/app/views/casa_cases/_filter.html.erb b/app/views/casa_cases/_filter.html.erb
index aac24684da..15b896d197 100644
--- a/app/views/casa_cases/_filter.html.erb
+++ b/app/views/casa_cases/_filter.html.erb
@@ -1,143 +1,50 @@
-
-
-
-
Filter by:
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- <% unless current_user.volunteer? %>
-
- <% end %>
-
-
+<%# Casa case filter bar (casadesign, bespoke). Clear server-side selects that submit on
+ change (auto-submit controller; Turbo Drive keeps it smooth). Volunteers never reach
+ this — the view gates it on can_see_filters?. %>
+<% select_class = "block w-full appearance-none rounded-lg border border-slate-300 bg-white py-2 pl-3 pr-9 text-sm text-slate-900 shadow-sm focus:border-brand-500 focus:ring-2 focus:ring-brand-500/30 focus:outline-none" %>
+<% label_class = "mb-1 block text-xs font-medium text-slate-500" %>
+<% chevron_class = "bi bi-chevron-down pointer-events-none absolute right-3 top-1/2 -translate-y-1/2 text-xs text-slate-500" %>
+
+<%= form_with url: casa_cases_path, method: :get, data: {controller: "auto-submit", action: "change->auto-submit#submit"}, class: "mb-4 grid grid-cols-2 items-end gap-3 lg:flex lg:flex-wrap" do %>
+
+ <%= label_tag :search, "Search", class: label_class %>
+
+
+ <%= search_field_tag :search, params[:search], placeholder: "Search case number or volunteer", autocomplete: "off", class: "block w-full rounded-lg border border-slate-300 bg-white py-2 pl-9 #{params[:search].present? ? 'pr-9' : 'pr-3'} text-sm text-slate-900 shadow-sm placeholder:text-slate-500 focus:border-brand-500 focus:ring-2 focus:ring-brand-500/30 focus:outline-none [&::-webkit-search-cancel-button]:hidden" %>
+ <% if params[:search].present? %>
+ <%= link_to casa_cases_path(request.query_parameters.except("search", "page")), "aria-label": "Clear search", class: "absolute right-2.5 top-1/2 -translate-y-1/2 grid h-5 w-5 place-items-center rounded text-slate-500 hover:text-slate-600" do %> <% end %>
+ <% end %>
-
-
-
-
-
-
-
-
-
-
- Select columns:
-
- <% CasaCase::TABLE_COLUMNS.each_with_index do |column, index| %>
-
- <%= check_box_tag "pick-#{column}",
- "1",
- false,
- class: "form-check-input toggle-visibility",
- data: { column: index } %>
- <%= label_tag "pick-#{column}", column.titleize, class: "form-check-label" %>
-
- <% end %>
-
-
-
- Close
-
-
-
-
+
+ <%= label_tag :status, "Status", class: label_class %>
+
+ <%= select_tag :status, options_for_select([["Active", "active"], ["Inactive", "inactive"], ["All", "all"]], params[:status].presence || "active"), class: select_class %>
+
+
+
+
+ <%= label_tag :assigned, "Assignment", class: label_class %>
+
+ <%= select_tag :assigned, options_for_select([["All volunteers", ""], ["Assigned", "assigned"], ["Unassigned", "unassigned"]], params[:assigned]), class: select_class %>
+
+
+
+
+ <%= label_tag :transition, "Transition-aged youth", class: label_class %>
+
+ <%= select_tag :transition, options_for_select([["All", ""], ["Yes", "yes"], ["No", "no"]], params[:transition]), class: select_class %>
+
+
+
+
+ <%= label_tag :prefix, "Case number prefix", class: label_class %>
+
+ <%= select_tag :prefix, options_for_select([["All", ""], ["CINA", "CINA"], ["TPR", "TPR"], ["None", "None"]], params[:prefix]), class: select_class %>
+
-
+
+ <%= submit_tag "Filter", class: button_classes(:primary) %>
+
+<% end %>
diff --git a/app/views/casa_cases/_generate_report_modal.html.erb b/app/views/casa_cases/_generate_report_modal.html.erb
deleted file mode 100644
index f6429f0815..0000000000
--- a/app/views/casa_cases/_generate_report_modal.html.erb
+++ /dev/null
@@ -1,56 +0,0 @@
-<%= form_with url: generate_case_court_reports_path, local: false do |form| %>
- <% id = "generate-docx-report-modal" %>
- <%= render(Modal::OpenButtonComponent.new(
- target: id,
- klass: "d-inline main-btn btn-hover btn-sm success-btn pull-right")) do %>
- Generate Report
- <% end %>
- <%= render(Modal::GroupComponent.new(id: id)) do |component| %>
- <% component.with_header(text: "Download Court Report as a .docx", id: id, klass: "content-1") %>
- <% component.with_body do %>
-
-
- To download a court report specify the date range.
-
-
- <%= form.hidden_field :case_number, value: @casa_case.case_number %>
- <%= form.hidden_field :time_zone, id: "user-time-zone" %>
-
Casa Case:
-
<%= @casa_case.decorate.court_report_select_option.first %>
-
-
-
-
Starting From
- <%= form.text_field :start_date,
- value: @casa_case.formatted_latest_court_date,
- data: { provide: "datepicker",
- date_format: ::DateHelper::JQUERY_MONTH_DAY_YEAR_FORMAT },
- class: "form-control" %>
-
-
-
-
Ending At
- <%= form.text_field :end_date,
- value: Time.zone.now.strftime(::DateHelper::RUBY_MONTH_DAY_YEAR_FORMAT),
- data: { provide: "datepicker",
- date_format: ::DateHelper::JQUERY_MONTH_DAY_YEAR_FORMAT },
- class: "form-control" %>
-
-
-
- <% end %>
- <% component.with_footer do %>
- <%= button_tag type: :submit,
- data: {
- button_name: "Generate Report"
- },
- id: "btnGenerateReport",
- class: "main-btn primary-btn btn-hover btn-sm",
- onclick: "setTimeZone()" do %>
-
-
⏳
- Generate Report
- <% end %>
- <% end %>
- <% end %>
-<% end %>
diff --git a/app/views/casa_cases/_inactive_case.html.erb b/app/views/casa_cases/_inactive_case.html.erb
index 344467789b..ccdc5b0c84 100644
--- a/app/views/casa_cases/_inactive_case.html.erb
+++ b/app/views/casa_cases/_inactive_case.html.erb
@@ -1,10 +1,9 @@
-
-
Case was deactivated on: <%= I18n.l(@casa_case.updated_at, format: :standard, default: nil) %>
- <%= link_to(
- "Reactivate CASA Case",
- reactivate_casa_case_path(@casa_case),
- method: :patch,
- class: "main-btn primary-btn btn-hover pull-right"
- ) if Pundit.policy(current_user, @casa_case).update_case_status? %>
+
+
+
+ Case was deactivated on: <%= I18n.l(@casa_case.updated_at, format: :standard, default: nil) %>
+ <% if Pundit.policy(current_user, @casa_case).update_case_status? %>
+ <%= link_to "Reactivate CASA Case", reactivate_casa_case_path(@casa_case), method: :patch, class: button_classes(:primary) %>
+ <% end %>
diff --git a/app/views/casa_cases/_month_year_select.html.erb b/app/views/casa_cases/_month_year_select.html.erb
new file mode 100644
index 0000000000..4f8c236f29
--- /dev/null
+++ b/app/views/casa_cases/_month_year_select.html.erb
@@ -0,0 +1,24 @@
+<%# Month + year picker styled with the design-system select (appearance-none + chevron).
+ Keeps Rails' date-part field names/ids (_2i month, _1i year, hidden _3i day = 1) so the
+ behaviour matches `date_select` exactly. Locals: form, method, label, disabled (optional). %>
+<% my_select = "block w-full appearance-none rounded-lg border border-slate-300 bg-white py-2.5 pl-3.5 pr-9 text-sm text-slate-900 shadow-sm focus:border-brand-500 focus:ring-2 focus:ring-brand-500/30 focus:outline-none disabled:bg-slate-50 disabled:text-slate-500" %>
+<% my_chevron = "bi bi-chevron-down pointer-events-none absolute right-3 top-1/2 -translate-y-1/2 text-xs text-slate-500" %>
+<% current = form.object.public_send(method) %>
+
+
<%= label %>
+
+
+ <%= select_tag "#{form.object_name}[#{method}(2i)]",
+ options_for_select((1..12).map { |m| [Date::MONTHNAMES[m], m] }, current&.month),
+ prompt: "Month", id: "#{form.object_name}_#{method}_2i", disabled: local_assigns[:disabled], class: my_select %>
+
+
+
+ <%= select_tag "#{form.object_name}[#{method}(1i)]",
+ options_for_select((1989..Date.current.year).to_a.reverse.map { |y| [y, y] }, current&.year),
+ prompt: "Year", id: "#{form.object_name}_#{method}_1i", disabled: local_assigns[:disabled], class: my_select %>
+
+
+
+ <%= hidden_field_tag "#{form.object_name}[#{method}(3i)]", 1 %>
+
diff --git a/app/views/casa_cases/_placements.html.erb b/app/views/casa_cases/_placements.html.erb
index be1545446c..fe3d8a4b9d 100644
--- a/app/views/casa_cases/_placements.html.erb
+++ b/app/views/casa_cases/_placements.html.erb
@@ -1,18 +1,13 @@
<% current_placement = casa_case.placements.order(placement_started_at: :desc).first %>
-
-<% if current_placement %>
-
- Current Placement:
- <%= current_placement.placement_type.name %>
-
-
-
Placed since: <%= current_placement.decorate.formatted_date %>
-
<%= link_to "See All Placements", casa_case_placements_path(casa_case), class: 'text-primary hover-underline' %>
-<% elsif casa_case.casa_org.placement_types.present? %>
-
- Current Placement:
- Unknown
-
-
-
<%= link_to "See All Placements", casa_case_placements_path(casa_case), class: 'text-primary hover-underline' %>
+<% if current_placement || casa_case.casa_org.placement_types.present? %>
+
+
Current placement
+ <% if current_placement %>
+
<%= current_placement.placement_type.name %>
+
Placed since: <%= current_placement.decorate.formatted_date %>
+ <% else %>
+
Unknown
+ <% end %>
+
<%= link_to "See All Placements", casa_case_placements_path(casa_case), class: "text-sm font-medium text-brand-600 hover:text-brand-700" %>
+
<% end %>
diff --git a/app/views/casa_cases/_volunteer_assignment.html.erb b/app/views/casa_cases/_volunteer_assignment.html.erb
index 0a26121283..e5756a315c 100644
--- a/app/views/casa_cases/_volunteer_assignment.html.erb
+++ b/app/views/casa_cases/_volunteer_assignment.html.erb
@@ -1,86 +1,108 @@
-<% content_for :table_title do %>
-
-
-
-
-
Assigned Volunteers
-
-
-
+<%# Volunteer assignments for a CASA case (casa_app / Tailwind). Self-contained: a
+ stacked-card list of current assignments + an "assign a volunteer" form. Does not
+ use the shared Bootstrap `manage_volunteers` partial (still used by supervisors/edit).
+ A card list (not a wide table) because the edit column is narrow and each row has many
+ fields, so a `
` of labeled fields reads better than a cramped 7-column table. %>
+<% assignments = @casa_case.case_assignments %>
+<% available_volunteers = @casa_case.unassigned_volunteers %>
+<% pill_base = "inline-flex items-center gap-1.5 rounded-full px-2.5 py-1 text-xs font-medium" %>
+
+
+
Volunteers
-<% end %>
-<% content_for :table_body do %>
-
- <% @casa_case.case_assignments.each do |assignment| %>
-
-
- <%= link_to assignment.volunteer.display_name, edit_volunteer_path(assignment.volunteer) %>
-
- <%= assignment&.volunteer&.email %>
-
- <% if assignment.active? %>
- <%= render BadgeComponent.new(text: "Assigned", type: :success, margin: false) %>
- <% else %>
- <%= render BadgeComponent.new(text: assignment.volunteer.active? ? "Unassigned" : "Deactivated volunteer",
- type: :danger,
- margin: false) %>
- <% end %>
-
- <%= I18n.l(assignment.created_at, format: :full, default: nil) %>
-
- <% unless assignment.active? %>
- <%= I18n.l(assignment.updated_at, format: :full, default: nil) %>
- <% end %>
-
-
-
-
- <% if policy(assignment).unassign? %>
- <%= button_to("Unassign Volunteer",
- unassign_case_assignment_path(assignment),
- method: :patch,
- class: "btn btn-outline-danger text-wrap") %>
- <% else %>
- <% if !assignment.volunteer.active? && policy(assignment.volunteer).activate? %>
- <%= link_to "Activate Volunteer",
- activate_volunteer_path(assignment.volunteer,
- redirect_to_path: 'casa_case',
- casa_case_id: @casa_case.id
- ),
- method: :patch,
- class: "btn btn-outline-success" %>
- <% end %>
- <% if assignment.hide_old_contacts? %>
- <%= button_to("Show Old Contacts",
- show_hide_contacts_case_assignment_path(assignment),
- method: :patch,
- class: "btn btn-outline-warning text-wrap") %>
- <% else %>
- <%= button_to("Hide Old Contacts",
- show_hide_contacts_case_assignment_path(assignment),
- method: :patch,
- class: "btn btn-outline-secondary text-wrap") %>
+
+
+
Assigned:
+ <%= I18n.l(assignment.created_at, format: :full, default: nil) %>
+
+
+
Unassigned:
+ <% unless assignment.active? %><%= I18n.l(assignment.updated_at, format: :full, default: nil) %><% end %>
+
+
+
+
+ <%= form_with model: assignment, url: reimbursement_case_assignment_path(assignment), method: :patch do |rf| %>
+
+ <%= rf.check_box :allow_reimbursement, class: "h-4 w-4 rounded border-slate-300 text-brand-600 focus:ring-brand-500", onchange: "this.form.submit();" %>
+ Enable reimbursement
+
<% end %>
- <% end %>
-
-
+
+
+ <% if policy(assignment).unassign? %>
+ <%= button_to "Unassign Volunteer", unassign_case_assignment_path(assignment), method: :patch, form_class: "contents", class: button_classes(:danger_outline) %>
+ <% else %>
+ <% if !volunteer.active? && policy(volunteer).activate? %>
+ <%= button_to "Activate Volunteer", activate_volunteer_path(volunteer, redirect_to_path: "casa_case", casa_case_id: @casa_case.id), method: :patch, form_class: "contents", class: button_classes(:secondary) %>
+ <% end %>
+ <% if assignment.hide_old_contacts? %>
+ <%= button_to "Show Old Contacts", show_hide_contacts_case_assignment_path(assignment), method: :patch, form_class: "contents", class: button_classes(:secondary) %>
+ <% else %>
+ <%= button_to "Hide Old Contacts", show_hide_contacts_case_assignment_path(assignment), method: :patch, form_class: "contents", class: button_classes(:secondary) %>
+ <% end %>
+ <% end %>
+
+
+
+ <% end %>
+
+ <% else %>
+
+
No volunteers are assigned to this case yet.
+
+ <% end %>
+
+
+
Assign a volunteer to this case
+ <%= form_with model: CaseAssignment.new, url: case_assignments_path(casa_case_id: @casa_case.id) do |form| %>
+
>
+
+
+
Select a volunteer
+
+
+ Please select a volunteer
+ <% available_volunteers.each do |v| %>
+ <%= formatted_name(v.display_name) %>
+ <% end %>
+
+
+
+
+ <%= button_tag "Assign Volunteer", type: "submit", class: button_classes(:primary) %>
+
+
+ <% end %>
+ <% unless available_volunteers.any? %>
+
There are no active, unassigned volunteers available.
<% end %>
-
-<% end %>
-<%=
- render(
- "shared/manage_volunteers",
- assignable_obj: CaseAssignment,
- assign_action: case_assignments_path(casa_case_id: @casa_case.id),
- available_volunteers: @casa_case.unassigned_volunteers,
- select_id: 'case_assignment_casa_case_id',
- select_name: 'case_assignment[volunteer_id]',
- show_assigned_volunteers: @casa_case.volunteers.present?
- )
-%>
+
+
diff --git a/app/views/casa_cases/_volunteer_reminder_form.html.erb b/app/views/casa_cases/_volunteer_reminder_form.html.erb
new file mode 100644
index 0000000000..153c298720
--- /dev/null
+++ b/app/views/casa_cases/_volunteer_reminder_form.html.erb
@@ -0,0 +1,25 @@
+<%# Show-page reminder control: a "Send Reminder" trigger opens a design-system Dialog to
+ confirm and optionally CC the supervisor/admin, then PATCHes volunteers#reminder (which
+ redirects back to the case). The shared volunteers/_volunteer_reminder_form is still
+ Bootstrap and used by the un-migrated volunteers/edit page. %>
+<%= render Dialog::GroupComponent.new(label: "Send a reminder to #{formatted_name(volunteer.display_name)}", wrapper_class: "contents") do |dialog| %>
+ <% dialog.with_trigger do %>
+ <%= button_tag type: "button", data: {action: "modal#open"}, title: "Remind volunteer to input case contacts", class: button_classes(:secondary) do %>
+
Send Reminder
+ <% end %>
+ <% end %>
+ <%= form_with url: reminder_volunteer_path(volunteer), method: :patch do %>
+ <%= render Dialog::HeaderComponent.new(title: "Send a reminder?", icon: "bi bi-bell", variant: :info) %>
+ <%= render Dialog::BodyComponent.new do %>
+
Email <%= formatted_name(volunteer.display_name) %> a reminder to log their case contacts.
+
+ <%= check_box_tag "with_cc", 1, false, id: "with_cc_#{volunteer.id}", class: "h-4 w-4 rounded border-slate-300 text-brand-600 focus:ring-brand-500" %>
+ Send CC to Supervisor and Admin
+
+ <% end %>
+ <%= render Dialog::FooterComponent.new do %>
+
Cancel
+ <%= button_tag "Yes, send reminder", type: "submit", class: button_classes(:primary) %>
+ <% end %>
+ <% end %>
+<% end %>
diff --git a/app/views/casa_cases/edit.html.erb b/app/views/casa_cases/edit.html.erb
index 64f98e8561..818baa2e6a 100644
--- a/app/views/casa_cases/edit.html.erb
+++ b/app/views/casa_cases/edit.html.erb
@@ -1,14 +1,91 @@
-
-
-
-
-
Editing <%= link_to(@casa_case.case_number, @casa_case) %>
-
+<% content_for :page_title, "Edit case #{@casa_case.case_number}" %>
+<% btn_primary = button_classes(:primary) %>
+<% input_class = "block w-full rounded-lg border border-slate-300 px-3.5 py-2.5 text-sm text-slate-900 shadow-sm placeholder:text-slate-500 focus:border-brand-500 focus:ring-2 focus:ring-brand-500/30 focus:outline-none" %>
+<% select_class = "block w-full appearance-none rounded-lg border border-slate-300 bg-white py-2.5 pl-3.5 pr-9 text-sm text-slate-900 shadow-sm focus:border-brand-500 focus:ring-2 focus:ring-brand-500/30 focus:outline-none" %>
+<% chevron_class = "bi bi-chevron-down pointer-events-none absolute right-3 top-1/2 -translate-y-1/2 text-xs text-slate-500" %>
+<% label_class = "mb-1.5 block text-sm font-medium text-slate-700" %>
+<% card = "rounded-2xl border border-slate-200 bg-white p-5 shadow-sm sm:p-6" %>
+
+
+
+
+
<%= link_to "Cases", casa_cases_path, class: "hover:text-brand-700" %> / <%= link_to @casa_case.case_number, @casa_case, class: "hover:text-brand-700" %>
+
Edit case
+
+ <% if @casa_case.active? %>
+ <%= form_with model: @casa_case, local: true, data: {controller: "court-order-form", nested_form_wrapper_selector_value: ".nested-form-wrapper"} do |form| %>
+ <%= render "shared/form_errors", resource: @casa_case %>
+
+
+ <% if @casa_case.new_record? || policy(@casa_case).update_case_number? %>
+
+ <%= form.label :case_number, "Case number", class: label_class %>
+ <%= form.text_field :case_number, required: true, class: input_class %>
+
+ <% end %>
+
Court details
+
+ <%= render "casa_cases/month_year_select", form: form, method: :birth_month_year_youth, label: "Youth's birth month and year", disabled: !policy(@casa_case).update_birth_month_year_youth? %>
+ <% if policy(@casa_case).update_date_in_care_youth? %>
+ <%= render "casa_cases/month_year_select", form: form, method: :date_in_care, label: "Youth's date in care" %>
+ <% end %>
+
+ <%= form.label :court_report_status, "Court report status", class: label_class %>
+
+ <%= form.select :court_report_status, CasaCase.court_report_statuses&.map { |s| [s.first.humanize, s.first] }, {}, class: select_class %>
+
+
+
+ <% if Pundit.policy(current_user, @casa_case).update_contact_types? %>
+
+ Contact types
+ <%= render "case_contacts/form/contact_types", form: form, options: @contact_types, selected_items: @selected_contact_type_ids, casa_cases: [@casa_case] %>
+
+ <% end %>
+
+
+
+ <% if policy(@casa_case).update_court_orders? %>
+
+ <%= render partial: "casa_cases/court_orders", locals: {casa_case: @casa_case, siblings_casa_cases: @siblings_casa_cases, form: form, resource: "casa_case"} %>
+
+ <% end %>
+
+
+ <%= render "court_dates", casa_case: @casa_case %>
+
+
+
+ <%= button_tag "Save changes", type: "submit", class: btn_primary %>
+ <%= link_to "Cancel", @casa_case, class: "text-sm font-medium text-slate-600 hover:text-slate-900" %>
+
+
+ <% end %>
+
+ <%# Deactivate is a separate PATCH action, so it lives outside the edit form (a
+ button_to nested in the form would submit the outer form instead). %>
+ <% if policy(@casa_case).update_case_status? %>
+
+
+
Deactivate this case
+
Deactivating unassigns all volunteers. You can reactivate it later.
+
+ <%= render "shared/confirm_button",
+ url: deactivate_casa_case_path(@casa_case), method: :patch,
+ title: "Deactivate this case?",
+ message: "Deactivating a CASA case unassigns any volunteers currently assigned to it.",
+ confirm_label: "Yes, deactivate",
+ trigger_label: "Deactivate CASA Case",
+ trigger_class: button_classes(:danger_outline) %>
+
+ <% end %>
+
+ <% if Pundit.policy(current_user, @casa_case).assign_volunteers? %>
+ <%= render "volunteer_assignment" %>
+ <% end %>
+ <% else %>
+ <%= render "inactive_case", casa_case: @casa_case %>
+ <% end %>
-<% if @casa_case.active %>
- <%= render 'form', casa_case: @casa_case %>
-<% else %>
- <%= render 'inactive_case', casa_case: @casa_case %>
-<% end %>
diff --git a/app/views/casa_cases/index.html.erb b/app/views/casa_cases/index.html.erb
index e75fb79924..4a421bbee4 100644
--- a/app/views/casa_cases/index.html.erb
+++ b/app/views/casa_cases/index.html.erb
@@ -1,108 +1,160 @@
-<% if Rails.env.development? %>
-
-
-<% end %>
+<% content_for :page_title, current_user.volunteer? ? "My cases" : "Cases" %>
+<% btn_primary = button_classes(:primary) %>
+<% btn_secondary = button_classes(:secondary) %>
+<% th = "px-4 py-3 text-left text-xs font-semibold text-slate-600" %>
+<% td = "px-4 py-3 text-slate-700" %>
+<% col_count = current_user.volunteer? ? 5 : 6 %>
-
-
-
-
- <% if current_user.volunteer? %>
-
My <%= "Case".pluralize(@casa_cases.count) %>
- <% else %>
- <%= "Case".pluralize(@casa_cases.count) %>
- <% end %>
-
-
-
-
-
-
- <% if policy(:dashboard).see_volunteers_section? %>
- <%= link_to new_casa_case_path, class: "main-btn btn-sm primary-btn btn-hover ml-3" do %>
-
- New Case
- <% end %>
- <% end %>
-
- <% if policy(:application).admin_or_supervisor? %>
-
- <%= link_to case_groups_path, class: "main-btn btn-sm primary-btn btn-hover ml-3" do %>
- Case Groups
- <% end %>
-
-
- <%= link_to new_bulk_court_date_path, class: "main-btn btn-sm primary-btn btn-hover ml-3" do %>
-
- New Bulk Court Date
- <% end %>
-
- <% end %>
-
- <% if policy(CasaCase).can_see_filters? %>
-
- Pick displayed columns
-
- <% end %>
-
-
+
+
+
+ <% if current_user.volunteer? %>My <%= "Case".pluralize(@pagy.count) %><% else %><%= "Case".pluralize(@pagy.count) %><% end %>
+
+
+ <% if policy(:dashboard).see_volunteers_section? %>
+ <%= link_to new_casa_case_path, class: btn_primary do %>
New Case<% end %>
+ <% end %>
+ <% if policy(:application).admin_or_supervisor? %>
+
+
+ More
+
+
+ <%= link_to case_groups_path, class: "flex items-center gap-2 px-4 py-2 text-sm text-slate-600 hover:bg-slate-50 focus-visible:bg-slate-50 focus-visible:outline-none" do %> Case Groups<% end %>
+ <%= link_to new_bulk_court_date_path, class: "flex items-center gap-2 px-4 py-2 text-sm text-slate-600 hover:bg-slate-50 focus-visible:bg-slate-50 focus-visible:outline-none" do %> New Bulk Court Date<% end %>
+
+
+ <% end %>
-
-<% if policy(CasaCase).can_see_filters? %>
- <%= render 'filter' %>
-<% end %>
-
-
-
-
-
-
-
- Case Number
- Hearing Type
- Judge
- Status
- Transition Aged Youth
- <% unless current_user.volunteer? %>
- Assigned To
- <% end %>
-
-
-
-
+
+ <% if policy(CasaCase).can_see_filters? %>
+ <%= render "filter" %>
+ <% end %>
+
+ <% if params[:search].present? %>
+ Showing <%= @pagy.count %> <%= "case".pluralize(@pagy.count) %> matching “<%= params[:search] %>”
+ <% end %>
+
+ <%# Desktop: full table (md and up) %>
+
+
+
+
+
+ <%= sortable_header("Case number", "case_number", sort: @sort, direction: @direction) %>
+ <%= sortable_header("Next court date", "next_court_date", sort: @sort, direction: @direction) %>
+ <%= sortable_header("Status", "status", sort: @sort, direction: @direction) %>
+ <%= sortable_header("Transition aged youth", "transition", sort: @sort, direction: @direction) %>
+ <% unless current_user.volunteer? %>
+ <%= sortable_header("Assigned to", "assigned", sort: @sort, direction: @direction) %>
+ <% end %>
+ Actions
+
+
+
+ <% if @casa_cases.any? %>
<% @casa_cases.each do |casa_case| %>
-
- <%= link_to(casa_case.case_number, casa_case) %>
+
+ <%= link_to casa_case.case_number, casa_case, class: "font-medium text-brand-600 hover:text-brand-700" %>
+ <%= casa_case.decorate.formatted_next_court_date || tag.span("—", class: "text-slate-500") %>
+ <%= casa_case.decorate.status %>
+
+ <% if casa_case.in_transition_age? %>
+ Yes
+ <% else %>
+ No
+ <% end %>
- <%= casa_case.hearing_type_name %>
- <%= casa_case.judge_name %>
- <%= casa_case.decorate.status %>
- <%= casa_case.decorate.transition_aged_youth %>
<% unless current_user.volunteer? %>
-
+
<% if casa_case.active? %>
<%= safe_join(casa_case.assigned_volunteers.map { |vol|
- link_to(vol.display_name, edit_volunteer_path(vol)) },
- ", ") %>
+ link_to(formatted_name(vol.display_name), edit_volunteer_path(vol), class: "text-brand-600 hover:text-brand-700") }, ", ") %>
<% else %>
Case was deactivated on: <%= I18n.l(casa_case.updated_at, format: :standard, default: nil) %>
<% end %>
<% end %>
-
-
- <%= link_to "Edit", edit_casa_case_path(casa_case), class: 'text-danger' %>
+
+ <%= link_to edit_casa_case_path(casa_case), "aria-label": "Edit #{casa_case.case_number}", class: "inline-flex items-center gap-1.5 rounded-lg px-2 py-1 text-sm font-medium text-slate-600 hover:bg-slate-100 hover:text-slate-900 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-500" do %>
+ Edit
+ <% end %>
<% end %>
-
-
-
+ <% else %>
+
+
+
+ No cases found
+ <%= policy(CasaCase).can_see_filters? ? "Try adjusting your filters." : "Cases assigned to you will appear here." %>
+
+
+ <% end %>
+
+
+
+ <%# Mobile: stacked cards (below md) %>
+
+ <% if @casa_cases.any? %>
+
+ <% @casa_cases.each do |casa_case| %>
+
+
+ <%= link_to casa_case.case_number, casa_case, class: "text-base font-semibold text-brand-600 hover:text-brand-700" %>
+ <%= link_to edit_casa_case_path(casa_case), "aria-label": "Edit #{casa_case.case_number}", class: "-mr-1 -mt-1 inline-flex flex-none items-center gap-1.5 rounded-lg px-2 py-1 text-sm font-medium text-slate-600 hover:bg-slate-100 hover:text-slate-900 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-500" do %> Edit<% end %>
+
+
+
+
Next court date
+ <%= casa_case.decorate.formatted_next_court_date || tag.span("—", class: "text-slate-500") %>
+
+
+
Status
+ <%= casa_case.decorate.status %>
+
+
+
Transition aged youth
+
+ <% if casa_case.in_transition_age? %>
+ Yes
+ <% else %>
+ No
+ <% end %>
+
+
+ <% unless current_user.volunteer? %>
+
+
Assigned to
+
+ <% if casa_case.active? %>
+ <%= safe_join(casa_case.assigned_volunteers.map { |vol|
+ link_to(formatted_name(vol.display_name), edit_volunteer_path(vol), class: "text-brand-600 hover:text-brand-700") }, ", ").presence || tag.span("—", class: "text-slate-500") %>
+ <% else %>
+ Deactivated <%= I18n.l(casa_case.updated_at, format: :standard, default: nil) %>
+ <% end %>
+
+
+ <% end %>
+
+
+ <% end %>
+
+ <% else %>
+
+
+
No cases found
+
<%= policy(CasaCase).can_see_filters? ? "Try adjusting your filters." : "Cases assigned to you will appear here." %>
+
+ <% end %>
+
+
+ <% if @casa_cases.any? %>
+
+ <%= render "shared/pagination", pagy: @pagy %>
+
+ <% end %>
diff --git a/app/views/casa_cases/new.html.erb b/app/views/casa_cases/new.html.erb
index c7d105b56a..74629bbc7c 100644
--- a/app/views/casa_cases/new.html.erb
+++ b/app/views/casa_cases/new.html.erb
@@ -1,10 +1,86 @@
-
-
-
-
-
New CASA Case
+<% content_for :page_title, "New case" %>
+<% btn_primary = button_classes(:primary) %>
+<% input_class = "block w-full rounded-lg border border-slate-300 px-3.5 py-2.5 text-sm text-slate-900 shadow-sm placeholder:text-slate-500 focus:border-brand-500 focus:ring-2 focus:ring-brand-500/30 focus:outline-none" %>
+<% select_class = "block w-full appearance-none rounded-lg border border-slate-300 bg-white py-2.5 pl-3.5 pr-9 text-sm text-slate-900 shadow-sm focus:border-brand-500 focus:ring-2 focus:ring-brand-500/30 focus:outline-none" %>
+<% chevron_class = "bi bi-chevron-down pointer-events-none absolute right-3 top-1/2 -translate-y-1/2 text-xs text-slate-500" %>
+<% label_class = "mb-1.5 block text-sm font-medium text-slate-700" %>
+
+
+
+ <%= form_with model: @casa_case, local: true do |form| %>
+
+
<%= link_to "Cases", casa_cases_path, class: "hover:text-brand-700" %>
+
New case
-
+
+ <%= render "shared/form_errors", resource: @casa_case %>
+
+
+
+
+ <%= form.label :case_number, "Case number", class: label_class %>
+ <%= form.text_field :case_number, required: true, class: input_class %>
+
+
+
Court details
+
+
+ <%= render "casa_cases/month_year_select", form: form, method: :birth_month_year_youth,
+ label: "Youth's birth month and year", disabled: !policy(@casa_case).update_birth_month_year_youth? %>
+
+ <% if policy(@casa_case).update_date_in_care_youth? %>
+ <%= render "casa_cases/month_year_select", form: form, method: :date_in_care, label: "Youth's date in care" %>
+ <% end %>
+
+ <% if policy(@casa_case).update_court_date? %>
+
+ <%= form.fields_for :court_dates, @casa_case.court_dates.new do |cdf| %>
+ <%= cdf.label :date, "Next Court Date", class: label_class %>
+ <%= cdf.date_field :date, class: input_class, hidden: @empty_court_date %>
+ <% end %>
+
+ <%= form.check_box :empty_court_date, {checked: @empty_court_date, class: "toggle-court-date-input h-4 w-4 rounded border-slate-300 text-brand-600 focus:ring-brand-500"} %>
+ I don't know the court date yet
+
+
+ <% end %>
+
+
+ <%= form.label :court_report_status, "Court report status", class: label_class %>
+
+ <%= form.select :court_report_status,
+ CasaCase.court_report_statuses&.map { |status| [status.first.humanize, status.first] },
+ {}, class: select_class %>
+
+
+
+
+ <% if Pundit.policy(current_user, @casa_case).update_contact_types? %>
+
+ Contact types
+ <%= render "case_contacts/form/contact_types", form: form, options: @contact_types, selected_items: @selected_contact_type_ids, casa_cases: [@casa_case], render_option_subtext: false %>
+
+ <% end %>
+
+
+ <%= form.fields_for :case_assignments, @casa_case.case_assignments.new do |caf| %>
+ <%= caf.label :volunteer_id, "Assign a volunteer", class: label_class %>
+
+ <%= caf.select :volunteer_id,
+ @casa_case.unassigned_volunteers.map { |v| [formatted_name(v.display_name), v.id] },
+ {prompt: "Please select a volunteer"}, {class: select_class} %>
+
+
+ <% end %>
+
+
+
+
+
+ <%= button_tag "Create case", type: "submit", class: btn_primary %>
+ <%= link_to "Cancel", casa_cases_path, class: "text-sm font-medium text-slate-600 hover:text-slate-900" %>
+
+
+ <% end %>
-<%= render 'form', casa_case: @casa_case %>
diff --git a/app/views/casa_cases/show.html.erb b/app/views/casa_cases/show.html.erb
index e69cdb3864..c74bed724d 100644
--- a/app/views/casa_cases/show.html.erb
+++ b/app/views/casa_cases/show.html.erb
@@ -1,201 +1,224 @@
-
- <%= render "thank_you_modal" %>
-
-
-
-
- <% if !@casa_case.active %>
- <%= render "inactive_case", casa_case: @casa_case %>
- <% end %>
-
-
Case number: <%= @casa_case.case_number %>
- <%= volunteer_badge(@casa_case, current_user) %>
-
-
- <% if @casa_case.hearing_type %>
+<% content_for :page_title, "Case #{@casa_case.case_number}" %>
+<% btn_primary = button_classes(:primary) %>
+<% btn_secondary = button_classes(:secondary) %>
+<% card = "rounded-2xl border border-slate-200 bg-white p-5 shadow-sm sm:p-6" %>
+<% section_title = "text-base font-semibold text-slate-900" %>
+<% fact_label = "text-sm font-medium text-slate-500" %>
+<% fact_value = "text-sm text-slate-800" %>
+<% menu_item = "flex items-start gap-2 px-4 py-2 text-sm text-slate-600 hover:bg-slate-50 focus-visible:bg-slate-50 focus-visible:outline-none" %>
+
+
+
+ <%# Header %>
+
-
Hearing Type: <%= @casa_case.hearing_type_name %>
+
<%= link_to "Cases", casa_cases_path, class: "hover:text-brand-700" %>
+
Case <%= @casa_case.case_number %>
- <% end %>
- <% if @casa_case.has_judge_name? %>
-
-
Judge: <%= @casa_case.judge_name %>
+
+ <% if @casa_case.active? %>
+ <%= link_to new_case_contact_path(case_contact: {casa_case_id: @casa_case.id}), class: btn_primary do %>
+
New Case Contact
+ <% end %>
+ <%# Edit is a visible button on sm+ and collapses into the More menu on mobile %>
+
+ <%= link_to edit_casa_case_path(@casa_case), class: btn_secondary do %>
+ Edit Case Details
+ <% end %>
+
+ <% end %>
+ <%# Overflow menu (matches the cases index). On mobile it also holds Edit. %>
+
+
+ More
+
+
+ <% if @casa_case.active? %>
+ <%= link_to edit_casa_case_path(@casa_case), class: "#{menu_item} w-full sm:hidden" do %>
+ Edit Case Details
+ <% end %>
+ <% end %>
+ <%= render "casa_cases/court_report_modal", casa_case: @casa_case, trigger_class: "#{menu_item} w-full text-left", wrapper_class: "contents" %>
+ <% if @casa_case.in_transition_age? %>
+ <%= link_to casa_case_emancipation_path(@casa_case), class: "#{menu_item} justify-between" do %>
+ Emancipation
+ <%= @casa_case.decorate.emancipation_checklist_count %>
+ <% end %>
+ <% end %>
+ <% if @casa_case.casa_org.show_fund_request %>
+ <%= link_to new_casa_case_fund_request_path(@casa_case), class: menu_item do %>
+ New Fund Request
+ <% end %>
+ <% end %>
+
+
- <% end %>
-
-
Transition Aged Youth: <%= @casa_case.decorate.transition_aged_youth %>
-
-
-
-
- Youth's Date in Care:
-
- <%= @casa_case.decorate.date_in_care %>
- <%= @casa_case.decorate.duration_in_care %>
-
-
-
-
- Next Court Date:
- <%= I18n.l(@casa_case.next_court_date&.date, format: :day_and_date, default: '') %>
- <%= render "calendar_button", date: @casa_case.decorate.calendar_next_court_date,
- id: "btnNextCourtDate",
- tooltip: "Add Next Court Date to Calendar",
- title: "Next Court Date for [#{@casa_case.case_number}]" %>
-
-
-
-
- Court Report Status:
- <%= @casa_case.decorate.court_report_submission %>
- <% unless @casa_case.court_report_not_submitted? %>
- <% if @casa_case.court_report_submitted? && @casa_case.court_reports.attached? %>
- <%= link_to('Click to download',
- rails_blob_path(@casa_case.latest_court_report, disposition: 'attachment')) %>
- <% end %>
-
-
- Court Report Submitted Date:
- <%= @casa_case.decorate.court_report_submitted_date %>
+
+ <%# Inactive banner %>
+ <% unless @casa_case.active? %>
+
+
Case was deactivated on <%= I18n.l(@casa_case.updated_at, format: :standard, default: nil) %>.
+ <% if Pundit.policy(current_user, @casa_case).update_case_status? %>
+ <%= link_to "Reactivate CASA Case", reactivate_casa_case_path(@casa_case), method: :patch, class: button_classes(:danger) %>
+ <% end %>
<% end %>
- <% if @casa_case.case_court_orders.exists? %>
-
Court Orders:
- <% @casa_case.case_court_orders.each do |court_order| %>
-
<%= court_order.implementation_status_symbol %> <%= court_order.text %>
- <% end %>
- <% end %>
- <%= render "court_dates", casa_case: @casa_case %>
- <%= render "placements", casa_case: @casa_case %>
-
-
Assigned Volunteers:
-
- <% Pundit.policy_scope(current_user, @casa_case.assigned_volunteers).each do |volunteer| %>
-
- <% if current_user.volunteer? %>
-
<%= volunteer.display_name %>
- <% else %>
-
- <%= link_to "#{volunteer.display_name} ", edit_volunteer_path(volunteer) %>
-
-
- <%= render "volunteers/volunteer_reminder_form", volunteer: volunteer %>
-
+
+ <%# Case details %>
+
+ Case details
+
+
+
Transition Aged Youth:
+ <%= @casa_case.in_transition_age? ? "Yes" : "No" %>
+
+
+
Youth's Date in Care:
+
<%= @casa_case.decorate.date_in_care || tag.span("Not set", class: "text-slate-500") %>
+ <% if @casa_case.decorate.duration_in_care %>
+
<%= @casa_case.decorate.duration_in_care %>
+ <% end %>
+
+
+
Next Court Date:
+
+ <%= I18n.l(@casa_case.next_court_date&.date, format: :day_and_date, default: "None set") %>
+ <% if (cal = @casa_case.decorate.calendar_next_court_date) %>
+
<% end %>
-
+
+
+
+
Court Report Status:
+
<%= @casa_case.decorate.court_report_submission %>
+ <% unless @casa_case.court_report_not_submitted? %>
+
+ Submitted <%= @casa_case.decorate.court_report_submitted_date %><% if @casa_case.court_report_submitted? && @casa_case.court_reports.attached? %> · <%= link_to "Click to download", rails_blob_path(@casa_case.latest_court_report, disposition: "attachment"), class: "font-medium text-brand-600 hover:text-brand-700" %><% end %>
+
+ <% end %>
+
+
+
+ <% if @casa_case.case_court_orders.exists? %>
+
+
Court orders
+
+ <% @casa_case.case_court_orders.each do |court_order| %>
+
+ <%= court_order.implementation_status_symbol %>
+ <%= court_order.text %>
+
+ <% end %>
+
+
+ <% end %>
+
+ <%= render "placements", casa_case: @casa_case %>
+
+
+ <%# Court dates %>
+ <% court_dates = @casa_case.court_dates.includes(:hearing_type).ordered_ascending.load %>
+
+
+
Court dates
+ <%= link_to new_casa_case_court_date_path(@casa_case), class: btn_secondary do %>
+ Add a court date
<% end %>
-
- <% if @casa_case.case_contacts.present? %>
-
- <%= link_to casa_case_path(params[:id], format: :xlsx), class: "main-btn primary-btn btn-sm btn-hover" do %>
-
- Download All
+ <% if court_dates.empty? %>
+
No Court Dates
+ <% else %>
+
+ <% court_dates.each do |pcd| %>
+
+
+ <%= link_to pcd.decorate.court_date_info, casa_case_court_date_path(@casa_case, pcd), class: "text-sm font-medium text-brand-600 hover:text-brand-700" %>
+ <% if (report = pcd.latest_associated_report) %>
+ <%= link_to "Attached report", rails_blob_path(report, disposition: "attachment"), class: "text-xs text-slate-500 hover:text-slate-700" %>
+ <% end %>
+
+ "
+ data-add-to-calendar-end-value="<%= pcd.date.strftime("%Y-%m-%d") %>"
+ data-add-to-calendar-tooltip-value="Add court date to calendar">
+
<% end %>
-
+
<% end %>
-
+
-<%# clear local storage when successfully created case_contacts %>
-
+<%# Thank-you dialog (auto-opens on the success redirect; also clears the saved draft) %>
+<% if params[:success].present? %>
+ <%= render Dialog::GroupComponent.new(label: "Case contact created", open_on_connect: true, controllers: "local-storage-reset", data: {local_storage_reset_key_value: "casa-contact-form"}) do %>
+ <%= render Dialog::BodyComponent.new(centered: true) do %>
+
+
Case contact created
+
<%= thank_you_message %>
+ <% case when_do_we_have_court_dates(@casa_case)
+ when "future" %>
+
Please enter the previous court date if you know it so court reports can be generated with the correct case contacts.
+ <% when "past" %>
+
Please enter the next court date if you know it so court reports can be generated with the correct case contacts.
+ <% when "none" %>
+
Please enter any past or future court dates if you know them so court reports can be generated with the correct case contacts.
+ <% end %>
+ <% end %>
+ <%= render Dialog::FooterComponent.new(align: :center) do %>
+
Close
+ <% end %>
+ <% end %>
+<% end %>
diff --git a/app/views/case_assignments/index.html.erb b/app/views/case_assignments/index.html.erb
index ddcef964a5..4af9616366 100644
--- a/app/views/case_assignments/index.html.erb
+++ b/app/views/case_assignments/index.html.erb
@@ -1,4 +1,4 @@
-
<%= "Editing #{@volunteer.display_name}" %>
+
<%= "Editing #{formatted_name(@volunteer.display_name)}" %>
diff --git a/app/views/case_contacts/_case_contact.html.erb b/app/views/case_contacts/_case_contact.html.erb
index fbabde64af..a81a3c564d 100644
--- a/app/views/case_contacts/_case_contact.html.erb
+++ b/app/views/case_contacts/_case_contact.html.erb
@@ -56,18 +56,18 @@
Created by:
<% if policy(contact).edit? %>
<% if current_user.volunteer? %>
- <%= contact.creator&.display_name %>
+ <%= formatted_name(contact.creator&.display_name) %>
<% else %>
<% if contact.creator&.supervisor? %>
- <%= link_to contact.creator&.display_name, edit_supervisor_path(contact.creator), data: { turbo: false } %>
+ <%= link_to formatted_name(contact.creator&.display_name), edit_supervisor_path(contact.creator), data: { turbo: false } %>
<% elsif contact.creator&.casa_admin? %>
- <%= link_to contact.creator&.display_name, edit_users_path, data: { turbo: false } %>
+ <%= link_to formatted_name(contact.creator&.display_name), edit_users_path, data: { turbo: false } %>
<% else %>
- <%= link_to contact.creator&.display_name, edit_volunteer_path(contact.creator), data: { turbo: false } %>
+ <%= link_to formatted_name(contact.creator&.display_name), edit_volunteer_path(contact.creator), data: { turbo: false } %>
<% end %>
<% end %>
<% else %>
- <%= contact.creator&.display_name %>
+ <%= formatted_name(contact.creator&.display_name) %>
<% end %>
diff --git a/app/views/case_contacts/case_contacts_new_design/index.html.erb b/app/views/case_contacts/case_contacts_new_design/index.html.erb
index f9da61b526..c934015896 100644
--- a/app/views/case_contacts/case_contacts_new_design/index.html.erb
+++ b/app/views/case_contacts/case_contacts_new_design/index.html.erb
@@ -182,18 +182,18 @@
<% if policy(case_contact).edit? %>
<% if current_user.volunteer? %>
- <%= case_contact.creator&.display_name %>
+ <%= formatted_name(case_contact.creator&.display_name) %>
<% else %>
<% if case_contact.creator&.supervisor? %>
- <%= link_to case_contact.creator&.display_name, edit_supervisor_path(case_contact.creator), data: { turbo: false } %>
+ <%= link_to formatted_name(case_contact.creator&.display_name), edit_supervisor_path(case_contact.creator), data: { turbo: false } %>
<% elsif case_contact.creator&.casa_admin? %>
- <%= link_to case_contact.creator&.display_name, edit_users_path, data: { turbo: false } %>
+ <%= link_to formatted_name(case_contact.creator&.display_name), edit_users_path, data: { turbo: false } %>
<% else %>
- <%= link_to case_contact.creator&.display_name, edit_volunteer_path(case_contact.creator), data: { turbo: false } %>
+ <%= link_to formatted_name(case_contact.creator&.display_name), edit_volunteer_path(case_contact.creator), data: { turbo: false } %>
<% end %>
<% end %>
<% else %>
- <%= case_contact.creator&.display_name %>
+ <%= formatted_name(case_contact.creator&.display_name) %>
<% end %>
diff --git a/app/views/case_contacts/form/_contact_types.html.erb b/app/views/case_contacts/form/_contact_types.html.erb
index 517d71b65b..0b39eed8ea 100644
--- a/app/views/case_contacts/form/_contact_types.html.erb
+++ b/app/views/case_contacts/form/_contact_types.html.erb
@@ -1,11 +1,12 @@
-Choose from the available options below by searching or selecting from the dropdown menu.
+Choose from the available options below by searching or selecting from the dropdown menu.
<%= render(Form::MultipleSelectComponent.new(
form: form,
name: :contact_type_ids,
options: options.decorate.map { |ct| ct.hash_for_multi_select_with_cases(casa_cases&.pluck(:id)) },
selected_items: selected_items,
- render_option_subtext: true,
+ render_option_subtext: local_assigns.fetch(:render_option_subtext, true),
+ placeholder_term: "contact types",
show_all_option: true,
)) %>
diff --git a/app/views/case_groups/_form.html.erb b/app/views/case_groups/_form.html.erb
index 125957f11f..9c114c8c45 100644
--- a/app/views/case_groups/_form.html.erb
+++ b/app/views/case_groups/_form.html.erb
@@ -1,43 +1,41 @@
-
-
-
-
-
- <% if case_group.persisted? %>
- Edit Case Group
- <% else %>
- New Case Group
- <% end %>
-
-
-
-
-
-
- <%= form_with model: case_group do |form| %>
- <%= render "shared/error_messages", resource: case_group %>
+<% content_for :page_title, case_group.persisted? ? "Edit case group" : "New case group" %>
+<% input_class = "block w-full rounded-lg border border-slate-300 px-3.5 py-2.5 text-sm text-slate-900 shadow-sm placeholder:text-slate-500 focus:border-brand-500 focus:ring-2 focus:ring-brand-500/30 focus:outline-none" %>
+<% btn_primary = button_classes(:primary) %>
-