diff --git a/Gemfile.lock b/Gemfile.lock
index b392990b5b..c553b6b67c 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -446,6 +446,9 @@ GEM
unf_ext (0.0.8.2)
unicode-display_width (2.4.2)
uniform_notifier (1.16.0)
+ unparser (0.6.7)
+ diff-lcs (~> 1.3)
+ parser (>= 3.2.0)
view_component (3.0.0)
activesupport (>= 5.2.0, < 8.0)
concurrent-ruby (~> 1.0)
diff --git a/app/controllers/health_controller.rb b/app/controllers/health_controller.rb
index 1d2be30eba..e7fc2a5e89 100644
--- a/app/controllers/health_controller.rb
+++ b/app/controllers/health_controller.rb
@@ -1,5 +1,6 @@
class HealthController < ApplicationController
skip_before_action :authenticate_user!
+ skip_after_action :verify_authorized
def index
respond_to do |format|
@@ -10,4 +11,15 @@ def index
format.json { render json: {latest_deploy_time: Health.instance.latest_deploy_time} }
end
end
+
+ def case_contacts_creation_times_in_last_week
+ # Get the case contacts created in the last week
+ case_contacts = CaseContact.where("created_at >= ?", 10.week.ago)
+
+ # Extract the created_at timestamps and convert them to Unix time
+ timestamps = case_contacts.pluck(:created_at).map { |t| t.to_i }
+
+ # Return the timestamps as a JSON response
+ render json: {timestamps: timestamps}
+ end
end
diff --git a/app/javascript/application.js b/app/javascript/application.js
index 178513b4c7..db83def93f 100644
--- a/app/javascript/application.js
+++ b/app/javascript/application.js
@@ -33,3 +33,4 @@ require('./src/select')
require('./src/sidebar')
require('./src/tooltip')
require('./src/session_timeout_poller.js')
+require('./src/display_app_metric.js')
diff --git a/app/javascript/src/display_app_metric.js b/app/javascript/src/display_app_metric.js
new file mode 100644
index 0000000000..a944c14de0
--- /dev/null
+++ b/app/javascript/src/display_app_metric.js
@@ -0,0 +1,120 @@
+import { Chart, registerables } from 'chart.js'
+import 'chartjs-adapter-luxon'
+
+Chart.register(...registerables)
+
+const days = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']
+
+$(document).ready(function () {
+ $.ajax({
+ type: 'GET',
+ url: '/health/case_contacts_creation_times_in_last_week',
+ success: function (data) {
+ const timestamps = data.timestamps
+
+ const counts = getCountsByDayAndHour(timestamps)
+ const dataset = getDatasetFromCounts(counts)
+
+ createChart(dataset)
+ }
+ })
+})
+
+function getCountsByDayAndHour (timestamps) {
+ const counts = {}
+
+ for (let i = 0; i < timestamps.length; i++) {
+ const timestamp = new Date(timestamps[i] * 1000)
+ const day = days[timestamp.getUTCDay()]
+ const hour = timestamp.getUTCHours()
+ const key = day + ' ' + hour
+ counts[key] = (counts[key] || 0) + 1
+ }
+
+ return counts
+}
+
+function getDatasetFromCounts (counts) {
+ const dataset = []
+
+ for (const key in counts) {
+ const parts = key.split(' ')
+ const day = parts[0]
+ const hour = parseInt(parts[1])
+ const count = counts[key]
+
+ dataset.push({
+ x: hour,
+ y: days.indexOf(day),
+ r: Math.sqrt(count) * 2,
+ count
+ })
+ }
+
+ return dataset
+}
+
+function createChart (dataset) {
+ const ctx = document.getElementById('myChart').getContext('2d')
+ // eslint-disable-next-line no-unused-vars
+ const myChart = new Chart(ctx, {
+ type: 'bubble',
+ data: {
+ datasets: [{
+ label: 'Case Contacts Creation Times in Last Week',
+ data: dataset,
+ backgroundColor: 'rgba(255, 99, 132, 0.2)',
+ borderColor: 'rgba(255, 99, 132, 1)'
+ }]
+ },
+ options: getChartOptions()
+ })
+}
+
+function getChartOptions () {
+ return {
+ scales: {
+ x: getXScale(),
+ y: getYScale()
+ },
+ plugins: {
+ tooltip: {
+ callbacks: {
+ label: getTooltipLabelCallback()
+ }
+ }
+ }
+ }
+}
+
+function getXScale () {
+ return {
+ ticks: {
+ beginAtZero: true,
+ stepSize: 1
+ }
+ }
+}
+
+function getYScale () {
+ return {
+ ticks: {
+ beginAtZero: true,
+ stepSize: 1,
+ callback: getYTickCallback()
+ }
+ }
+}
+
+function getYTickCallback () {
+ return function (value, index, values) {
+ return days[value]
+ }
+}
+
+function getTooltipLabelCallback () {
+ return function (context) {
+ const datum = context.dataset.data[context.dataIndex]
+ return datum.count + ' case contacts created on ' + days[datum.y] + ' at ' + datum.x + ':00'
+ }
+}
diff --git a/app/views/health/index.html.erb b/app/views/health/index.html.erb
index 8ce787f915..c7f7a54cad 100644
--- a/app/views/health/index.html.erb
+++ b/app/views/health/index.html.erb
@@ -1,2 +1,4 @@
+
+
Chart: Display App Metric
diff --git a/config/routes.rb b/config/routes.rb
index b8ac514430..0a7d68cea8 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -24,7 +24,11 @@
root to: "all_casa_admins/sessions#new", as: :unauthenticated_all_casa_root
end
- resources :health, only: %i[index]
+ resources :health, only: %i[index] do
+ collection do
+ get :case_contacts_creation_times_in_last_week
+ end
+ end
get "/.well-known/assetlinks.json", to: "android_app_associations#index"
resources :casa_cases, except: %i[destroy] do
diff --git a/package.json b/package.json
index 993cdf9c49..bd0722694d 100644
--- a/package.json
+++ b/package.json
@@ -23,11 +23,14 @@
"bootstrap-datepicker": "^1.10.0",
"bootstrap-scss": "^5.2.3",
"bootstrap-select": "^1.13.18",
+ "chart.js": "^4.2.1",
+ "chartjs-adapter-luxon": "^1.3.1",
"datatables.net-dt": "^1.13.4",
"esbuild": "^0.17.19",
"faker": "^5.5.3",
"jquery": "^3.6.4",
"lodash": "^4.17.21",
+ "luxon": "^3.3.0",
"popper.js": "^1.16.1",
"sass": "^1.62.1",
"select2": "^4.0.13",
diff --git a/spec/requests/health_spec.rb b/spec/requests/health_spec.rb
index 1d56050271..9d18d1860f 100644
--- a/spec/requests/health_spec.rb
+++ b/spec/requests/health_spec.rb
@@ -32,4 +32,17 @@
expect(hash_body.keys).to match_array(["latest_deploy_time"])
end
end
+
+ describe "GET #case_contacts_creation_times_in_last_week" do
+ it "returns timestamps of case contacts created in the last week" do
+ case_contact1 = create(:case_contact, created_at: 1.week.ago)
+ case_contact2 = create(:case_contact, created_at: 2.weeks.ago)
+ get case_contacts_creation_times_in_last_week_health_index_path
+ expect(response).to have_http_status(:ok)
+ expect(response.content_type).to include("application/json")
+ timestamps = JSON.parse(response.body)["timestamps"]
+ expect(timestamps).to include(case_contact1.created_at.to_i)
+ expect(timestamps).not_to include(case_contact2.created_at.iso8601(3))
+ end
+ end
end
diff --git a/spec/system/imports/index_spec.rb b/spec/system/imports/index_spec.rb
index b340d219d3..671cd97321 100644
--- a/spec/system/imports/index_spec.rb
+++ b/spec/system/imports/index_spec.rb
@@ -1,13 +1,11 @@
require "rails_helper"
RSpec.describe "imports/index", type: :system do
- let(:volunteer) { create(:volunteer) }
- let(:admin) { create(:casa_admin) }
-
context "as a volunteer" do
- before { sign_in volunteer }
-
it "redirects the user with an error message" do
+ volunteer = create(:volunteer)
+
+ sign_in volunteer
visit imports_path
expect(page).to have_selector(".alert", text: "Sorry, you are not authorized to perform this action.")
@@ -15,9 +13,10 @@
end
context "import volunteer csv with phone numbers", js: true do
- let(:import_file_path) { Rails.root.join("spec", "fixtures", "volunteers.csv") }
-
it "shows sms opt in modal" do
+ import_file_path = Rails.root.join("spec", "fixtures", "volunteers.csv")
+ admin = create(:casa_admin)
+
sign_in admin
visit imports_path(:volunteer)
@@ -38,9 +37,10 @@
end
context "import volunteer csv without phone numbers", js: true do
- let(:import_file_path) { Rails.root.join("spec", "fixtures", "volunteers_without_phone_numbers.csv") }
-
it "shows successful import" do
+ import_file_path = Rails.root.join("spec", "fixtures", "volunteers_without_phone_numbers.csv")
+ admin = create(:casa_admin)
+
sign_in admin
visit imports_path(:volunteer)
@@ -54,9 +54,10 @@
end
context "import supervisors csv with phone numbers", js: true do
- let(:import_file_path) { Rails.root.join("spec", "fixtures", "supervisors.csv") }
-
it "shows sms opt in modal" do
+ import_file_path = Rails.root.join("spec", "fixtures", "supervisors.csv")
+ admin = create(:casa_admin)
+
sign_in admin
visit imports_path
click_on "supervisor-tab"
@@ -78,12 +79,14 @@
end
context "import supervisors csv without phone numbers", js: true do
- let(:import_file_path) { Rails.root.join("spec", "fixtures", "supervisors_without_phone_numbers.csv") }
-
it "shows successful import" do
+ import_file_path = Rails.root.join("spec", "fixtures", "supervisors_without_phone_numbers.csv")
+ admin = create(:casa_admin)
+
sign_in admin
visit imports_path
- click_link "supervisor-tab"
+
+ click_on "Import Supervisors"
expect(page).to have_content("Import Supervisors")
diff --git a/yarn.lock b/yarn.lock
index 8d994e4e73..6c9c606d9c 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1670,6 +1670,11 @@
"@jridgewell/resolve-uri" "3.1.0"
"@jridgewell/sourcemap-codec" "1.4.14"
+"@kurkle/color@^0.3.0":
+ version "0.3.2"
+ resolved "https://registry.yarnpkg.com/@kurkle/color/-/color-0.3.2.tgz#5acd38242e8bde4f9986e7913c8fdf49d3aa199f"
+ integrity sha512-fuscdXJ9G1qb7W8VdHi+IwRqij3lBkosAm4ydQtEmbY58OzHXqQhvlxqEkoz0yssNVn38bcpRWgA9PP+OGoisw==
+
"@nodelib/fs.scandir@2.1.5":
version "2.1.5"
resolved "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz"
@@ -2288,6 +2293,18 @@ char-regex@^1.0.2:
resolved "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz"
integrity sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==
+chart.js@^4.2.1:
+ version "4.2.1"
+ resolved "https://registry.yarnpkg.com/chart.js/-/chart.js-4.2.1.tgz#d2bd5c98e9a0ae35408975b638f40513b067ba1d"
+ integrity sha512-6YbpQ0nt3NovAgOzbkSSeeAQu/3za1319dPUQTXn9WcOpywM8rGKxJHrhS8V8xEkAlk8YhEfjbuAPfUyp6jIsw==
+ dependencies:
+ "@kurkle/color" "^0.3.0"
+
+chartjs-adapter-luxon@^1.3.1:
+ version "1.3.1"
+ resolved "https://registry.yarnpkg.com/chartjs-adapter-luxon/-/chartjs-adapter-luxon-1.3.1.tgz#8ad8be7cc1521bacd6cd2ff4b013669d4dd49364"
+ integrity sha512-yxHov3X8y+reIibl1o+j18xzrcdddCLqsXhriV2+aQ4hCR66IYFchlRXUvrJVoxglJ380pgytU7YWtoqdIgqhg==
+
check-more-types@2.24.0:
version "2.24.0"
resolved "https://registry.npmjs.org/check-more-types/-/check-more-types-2.24.0.tgz"
@@ -4283,6 +4300,11 @@ lru-cache@^6.0.0:
dependencies:
yallist "^4.0.0"
+luxon@^3.3.0:
+ version "3.3.0"
+ resolved "https://registry.yarnpkg.com/luxon/-/luxon-3.3.0.tgz#d73ab5b5d2b49a461c47cedbc7e73309b4805b48"
+ integrity sha512-An0UCfG/rSiqtAIiBPO0Y9/zAnHUZxAMiCpTd5h2smgsj7GGmcenvrvww2cqNA8/4A5ZrD1gJpHN2mIHZQF+Mg==
+
make-dir@^3.0.0:
version "3.1.0"
resolved "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz"