diff --git a/3 b/3
new file mode 100644
index 0000000000..e45619a65e
--- /dev/null
+++ b/3
@@ -0,0 +1,503 @@
+/* global alert */
+/* global $ */
+const AsyncNotifier = require('../src/async_notifier')
+let pageNotifier
+
+const defineCaseContactsTable = function () {
+ $('table#case_contacts').DataTable(
+ {
+ scrollX: true,
+ searching: false,
+ order: [[0, 'desc']]
+ }
+ )
+}
+
+$(() => { // JQuery's callback for the DOM loading
+ const asyncNotificationsElement = $('#async-notifications')
+
+ if (asyncNotificationsElement.length) {
+ pageNotifier = new AsyncNotifier(asyncNotificationsElement)
+ }
+
+ $.fn.dataTable.ext.search.push(
+ function (settings, data, dataIndex) {
+ if (settings.nTable.id !== 'casa-cases') {
+ return true
+ }
+
+ const statusArray = []
+ const assignedToVolunteerArray = []
+ const assignedToMoreThanOneVolunteerArray = []
+ const assignedToTransitionYouthArray = []
+ const caseNumberPrefixArray = []
+
+ $('.status-options').find('input[type="checkbox"]').each(function () {
+ if ($(this).is(':checked')) {
+ statusArray.push($(this).data('value'))
+ }
+ })
+
+ $('.assigned-to-volunteer-options').find('input[type="checkbox"]').each(function () {
+ if ($(this).is(':checked')) {
+ assignedToVolunteerArray.push($(this).data('value'))
+ }
+ })
+
+ $('.more-than-one-volunteer-options').find('input[type="checkbox"]').each(function () {
+ if ($(this).is(':checked')) {
+ assignedToMoreThanOneVolunteerArray.push($(this).data('value'))
+ }
+ })
+
+ $('.transition-youth-options').find('input[type="checkbox"]').each(function () {
+ if ($(this).is(':checked')) {
+ assignedToTransitionYouthArray.push($(this).data('value'))
+ }
+ })
+
+ $('.case-number-prefix-options').find('input[type="checkbox"]').each(function () {
+ if ($(this).is(':checked')) {
+ caseNumberPrefixArray.push($(this).data('value'))
+ }
+ })
+
+ const possibleCaseNumberPrefixes = ['CINA', 'TPR']
+ const status = data[3]
+ const assignedToVolunteer = (data[5] !== '' && data[5].split(',').length >= 1) ? 'Yes' : 'No'
+ const assignedToMoreThanOneVolunteer = (data[5] !== '' && data[5].split(',').length > 1) ? 'Yes' : 'No'
+ const assignedToTransitionYouth = data[4]
+ const caseNumberPrefix = possibleCaseNumberPrefixes.includes(data[0].split('-')[0]) ? data[0].split('-')[0] : 'None'
+
+ return statusArray.includes(status) &&
+ assignedToVolunteerArray.includes(assignedToVolunteer) &&
+ assignedToMoreThanOneVolunteerArray.includes(assignedToMoreThanOneVolunteer) &&
+ assignedToTransitionYouthArray.includes(assignedToTransitionYouth) &&
+ caseNumberPrefixArray.includes(caseNumberPrefix)
+ }
+ )
+
+ const handleAjaxError = e => {
+ console.error(e)
+ if (e.responseJSON && e.responseJSON.error) {
+ alert(e.responseJSON.error)
+ } else {
+ const responseErrorMessage = e.response.statusText
+ ? `\n${e.response.statusText}\n`
+ : ''
+
+ alert(`Sorry, try that again?\n${responseErrorMessage}\nIf you're seeing a problem, please fill out the Report A Site Issue
+ link to the bottom left near your email address.`)
+ }
+ }
+
+ // Enable all data tables on dashboard but only filter on volunteers table
+ const editSupervisorPath = id => `/supervisors/${id}/edit`
+ const editVolunteerPath = id => `/volunteers/${id}/edit`
+ const impersonateVolunteerPath = id => `/volunteers/${id}/impersonate`
+ const casaCasePath = id => `/casa_cases/${id}`
+ const volunteersTable = $('table#volunteers').DataTable({
+ autoWidth: false,
+ stateSave: true,
+ initComplete: function (settings, json) {
+ this.api().columns().every(function (index) {
+ const columnVisible = this.visible()
+ return $('#visibleColumns input[data-column="' + index + '"]').prop('checked', columnVisible)
+ })
+ },
+ stateSaveCallback: function (settings, data) {
+ $.ajax({
+ url: '/preference_sets/table_state_update/' + settings.nTable.id + '_table',
+
+ data: {
+ table_state: JSON.stringify(data)
+ },
+ dataType: 'json',
+ type: 'POST',
+ error: function (jqXHR, textStatus, errorMessage) {
+ console.error(errorMessage)
+ pageNotifier.notify('Error while saving preferences', 'error')
+ }
+ })
+ },
+ stateSaveParams: function (settings, data) {
+ data.search.search = ''
+ return data
+ },
+ stateLoadCallback: function (settings, callback) {
+ $.ajax({
+ url: '/preference_sets/table_state/' + settings.nTable.id + '_table',
+ dataType: 'json',
+ type: 'GET',
+ success: function (json) {
+ callback(json)
+ }
+ })
+ },
+ order: [[7, 'desc']],
+ select: {
+ style: 'multi'
+ },
+ columns: [
+ {
+ data: 'id',
+ targets: 0,
+ searchable: false,
+ orderable: false,
+ checkboxes: {
+ selectRow: true,
+ stateSave: false
+ }
+ },
+ {
+ name: 'display_name',
+ render: (data, type, row, meta) => {
+ return `
+ Name
+
+ ${row.display_name || row.email}
+
+ `
+ }
+ },
+ {
+ name: 'email',
+ render: (data, type, row, meta) => row.email
+ },
+ {
+ className: 'supervisor-column',
+ name: 'supervisor_name',
+ render: (data, type, row, meta) => {
+ return row.supervisor.id
+ ? `
+ Supervisor
+
+ ${row.supervisor.name}
+
+ `
+ : ''
+ }
+ },
+ {
+ name: 'active',
+ render: (data, type, row, meta) => {
+ return `
+ Status
+ ${row.active === 'true' ? 'Active' : 'Inactive'}
+ `
+ },
+ searchable: false
+ },
+ {
+ name: 'has_transition_aged_youth_cases',
+ render: (data, type, row, meta) => {
+ return `
+ Assigned to Transitioned Aged Youth
+ ${row.has_transition_aged_youth_cases === 'true' ? 'Yes 🦋' : 'No 🐛'}`
+ },
+ searchable: false
+ },
+ {
+ name: 'casa_cases',
+ render: (data, type, row, meta) => {
+ const links = row.casa_cases.map(casaCase => {
+ return `
+ ${casaCase.case_number}
+ `
+ })
+ const caseNumbers = `
+ Case Number(s)
+ ${links.join(', ')}
+ `
+ return caseNumbers
+ },
+ orderable: false
+ },
+ {
+ name: 'most_recent_attempt_occurred_at',
+ render: (data, type, row, meta) => {
+ return row.most_recent_attempt.case_id
+ ? `
+ Last Attempted Contact
+
+ ${row.most_recent_attempt.occurred_at}
+
+ `
+ : 'None ❌'
+ },
+ searchable: false
+ },
+ {
+ name: 'contacts_made_in_past_days',
+ render: (data, type, row, meta) => {
+ return `
+ Contacts
+ ${row.contacts_made_in_past_days}
+ `
+ },
+ searchable: false
+ },
+ {
+ name: 'hours_spent_in_days',
+ render: (data, type, row, meta) => {
+ return `
+ Hours spent in last 30 days
+ ${row.hours_spent_in_days}
+ `
+ },
+ searchable: false
+ },
+ {
+ name: 'has_any_extra_languages ',
+ render: (data, type, row, meta) => {
+ const languages = row.extra_languages.map(x => x.name).join(', ')
+ return row.extra_languages.length > 0 ? `🌎` : ''
+ },
+ searchable: false
+ },
+ {
+ name: 'actions',
+ orderable: false,
+ render: (data, type, row, meta) => {
+ return `
+ Actions
+
+ Edit
+
+
+ Impersonate
+
+ `
+ },
+ searchable: false
+ }
+ ],
+ processing: true,
+ serverSide: true,
+ ajax: {
+ url: $('table#volunteers').data('source'),
+ type: 'POST',
+ data: function (d) {
+ const supervisorOptions = $('.supervisor-options input:checked')
+ const supervisorFilter = Array.from(supervisorOptions).map(option => option.dataset.value)
+
+ const statusOptions = $('.status-options input:checked')
+ const statusFilter = Array.from(statusOptions).map(option => JSON.parse(option.dataset.value))
+
+ const transitionYouthOptions = $('.transition-youth-options input:checked')
+ const transitionYouthFilter = Array.from(transitionYouthOptions).map(option => JSON.parse(option.dataset.value))
+
+ const extraLanguageOptions = $('.extra-language-options input:checked')
+ const extraLanguageFilter = Array.from(extraLanguageOptions).map(option => JSON.parse(option.dataset.value))
+ return $.extend({}, d, {
+ additional_filters: {
+ supervisor: supervisorFilter,
+ active: statusFilter,
+ transition_aged_youth: transitionYouthFilter,
+ extra_languages: extraLanguageFilter
+ }
+ })
+ },
+ error: handleAjaxError,
+ dataType: 'json'
+ },
+ drawCallback: function (settings) {
+ $('[data-toggle=tooltip]').tooltip()
+ }
+ })
+
+ $('#form-bulk-assignment').on('submit', function (e) {
+ const form = this
+ const rowsSelected = volunteersTable.column(0).checkboxes.selected()
+
+ $.each(rowsSelected, function (index, rowId) {
+ $(form).append(
+ $('')
+ .attr('type', 'hidden')
+ .attr('name', 'supervisor_volunteer[volunteer_ids][]')
+ .val(rowId)
+ )
+ })
+ })
+
+ volunteersTable.column(0).on('change', function () {
+ const rowsSelected = volunteersTable.column(0).checkboxes.selected()
+ if (rowsSelected.count() === 0) {
+ $('#volunteers-selected').html('')
+ } else {
+ $('#volunteers-selected').html('s (' + rowsSelected.count() + ')')
+ }
+ })
+
+ // Because the table saves state, we have to check/uncheck modal inputs based on what
+ // columns are visible
+ volunteersTable.columns().every(function (index) {
+ const columnVisible = this.visible()
+ $('#visibleColumns input[data-column="' + index + '"]').prop('checked', columnVisible)
+ return true
+ })
+
+ // Add Supervisors Table
+ const supervisorsTable = $('table#supervisors').DataTable({
+ autoWidth: false,
+ stateSave: false,
+ order: [[1, 'asc']], // order by cast contacts
+ columns: [
+ {
+ name: 'display_name',
+ className: 'min-width',
+ render: (data, type, row, meta) => {
+ return `
+
+ ${row.display_name || row.email}
+
+ `
+ }
+ },
+ {
+ name: '',
+ className: 'min-width',
+ render: (data, type, row, meta) => {
+ const noContactVolunteers = Number(row.no_attempt_for_two_weeks)
+ const transitionAgedCaseVolunteers = Number(row.transitions_volunteers)
+ const activeContactVolunteers = Number(row.volunteer_assignments) - noContactVolunteers
+ const activeContactElement = activeContactVolunteers
+ ? (
+ `
+
+ ${activeContactVolunteers}
+
+ `
+ )
+ : ''
+
+ const noContactElement = noContactVolunteers > 0
+ ? (
+ `
+
+ ${noContactVolunteers}
+
+ `
+ )
+ : ''
+
+ let volunteersCounterElement = ''
+ if (activeContactVolunteers <= 0 && noContactVolunteers <= 0) {
+ volunteersCounterElement = 'No assigned volunteers'
+ } else {
+ volunteersCounterElement = `${transitionAgedCaseVolunteers}`
+ }
+
+ return `
+
+ ${activeContactElement + noContactElement + volunteersCounterElement}
+
+ `
+ }
+ },
+ {
+ name: 'actions',
+ orderable: false,
+ render: (data, type, row, meta) => {
+ return `
+
+
+
+
+
+ `
+ },
+ searchable: false
+ }
+ ],
+ processing: true,
+ serverSide: true,
+ ajax: {
+ url: $('table#supervisors').data('source'),
+ type: 'POST',
+ data: function (d) {
+ const statusOptions = $('.status-options input:checked')
+ const statusFilter = Array.from(statusOptions).map(option => JSON.parse(option.dataset.value))
+
+ return $.extend({}, d, {
+ additional_filters: {
+ active: statusFilter
+ }
+ })
+ },
+ error: handleAjaxError,
+ dataType: 'json'
+ },
+ drawCallback: function (settings) {
+ $('[data-toggle=tooltip]').tooltip()
+ },
+ createdRow: function (row, data, dataIndex, cells) {
+ row.setAttribute('id', `supervisor-${data.id}-information`)
+ }
+ })
+
+ const casaCasesTable = $('table#casa-cases').DataTable({
+ autoWidth: false,
+ stateSave: false,
+ columnDefs: [],
+ language: {
+ emptyTable: 'No active cases'
+ }
+ })
+
+ casaCasesTable.columns().every(function (index) {
+ const columnVisible = this.visible()
+
+ if (columnVisible) {
+ $('#visibleColumns input[data-column="' + index + '"]').prop('checked', true)
+ } else {
+ $('#visibleColumns input[data-column="' + index + '"]').prop('checked', false)
+ }
+
+ return true
+ })
+
+ defineCaseContactsTable()
+
+ function filterOutUnassignedVolunteers (checked) {
+ $('.supervisor-options').find('input[type="checkbox"]').not('#unassigned-vol-filter').each(function () {
+ this.checked = checked
+ })
+ }
+
+ $('#unassigned-vol-filter').on('click', function () {
+ if ($('#unassigned-vol-filter').is(':checked')) {
+ filterOutUnassignedVolunteers(false)
+ } else {
+ filterOutUnassignedVolunteers(true)
+ }
+ volunteersTable.draw()
+ })
+
+ $('.volunteer-filters input[type="checkbox"]').not('#unassigned-vol-filter').on('click', function () {
+ volunteersTable.draw()
+ })
+
+ $('.supervisor-filters input[type="checkbox"]').on('click', function () {
+ supervisorsTable.draw()
+ })
+
+ $('.casa-case-filters input[type="checkbox"]').on('click', function () {
+ casaCasesTable.draw()
+ })
+
+ $('input.toggle-visibility').on('click', function (e) {
+ // Get the column API object and toggle the visibility
+ const column = volunteersTable.column($(this).attr('data-column'))
+ column.visible(!column.visible())
+ volunteersTable.columns.adjust().draw()
+
+ const caseColumn = casaCasesTable.column($(this).attr('data-column'))
+ caseColumn.visible(!caseColumn.visible())
+ casaCasesTable.columns.adjust().draw()
+ })
+})
+
+export { defineCaseContactsTable }
diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss
index 2b1c62859f..2525e6b57c 100644
--- a/app/assets/stylesheets/application.scss
+++ b/app/assets/stylesheets/application.scss
@@ -5,6 +5,7 @@
);
@use "bootstrap-datepicker/dist/css/bootstrap-datepicker";
@use "datatables.net-dt/css/jquery.dataTables";
+@use "jquery-datatables-checkboxes/css/dataTables.checkboxes";
@use "@fortawesome/fontawesome-free/scss/fontawesome";
@use "@fortawesome/fontawesome-free/scss/solid";
@use "select2/dist/css/select2";
diff --git a/app/assets/stylesheets/pages/volunteers.scss b/app/assets/stylesheets/pages/volunteers.scss
index 683a4b7f95..2d044fbd6a 100644
--- a/app/assets/stylesheets/pages/volunteers.scss
+++ b/app/assets/stylesheets/pages/volunteers.scss
@@ -18,3 +18,16 @@ body.volunteers {
}
}
}
+
+
+table#volunteers.dataTable.hover tbody tr.selected > * {
+ background-color: rgba(54, 92, 245, 0.1);
+ box-shadow: none;
+ color: inherit;
+}
+
+table#volunteers.dataTable.hover > tbody > tr.selected:hover > * {
+ background-color: rgba(54, 92, 245, 0.15);
+ box-shadow: none !important;
+ color: inherit;
+}
diff --git a/app/assets/stylesheets/shared/header.scss b/app/assets/stylesheets/shared/header.scss
new file mode 100644
index 0000000000..b4902edd78
--- /dev/null
+++ b/app/assets/stylesheets/shared/header.scss
@@ -0,0 +1,23 @@
+@use "../base/breakpoints.scss" as screen-sizes;
+
+.header {
+ .header-left {
+ .menu-toggle-btn {
+ display: none;
+
+ .main-btn {
+ border-radius: 4px !important;
+ }
+ }
+ }
+}
+
+@media only screen and (max-width: screen-sizes.$mobile) {
+ .header {
+ .header-left {
+ .menu-toggle-btn {
+ display: block;
+ }
+ }
+ }
+}
diff --git a/app/controllers/supervisor_volunteers_controller.rb b/app/controllers/supervisor_volunteers_controller.rb
index a3f9616d6b..691a3e2961 100644
--- a/app/controllers/supervisor_volunteers_controller.rb
+++ b/app/controllers/supervisor_volunteers_controller.rb
@@ -25,13 +25,67 @@ def unassign
redirect_to request.referer, notice: flash_message
end
+ def bulk_assignment
+ authorize :supervisor_volunteer
+ if mass_assign_volunteers?
+ volunteer_ids = supervisor_volunteer_params[:volunteer_ids]
+ supervisor = supervisor_volunteer_params[:supervisor_id]
+ vol = "Volunteer".pluralize(volunteer_ids.length)
+
+ if supervisor == "unassign"
+ name_array = bulk_unassign!(volunteer_ids)
+ flash_message = "#{vol} #{name_array.to_sentence} successfully unassigned"
+ else
+ supervisor = supervisor_volunteer_parent
+ name_array = bulk_assign!(supervisor, volunteer_ids)
+ flash_message = "#{vol} #{name_array.to_sentence} successfully reassigned to #{supervisor.display_name}"
+ end
+
+ redirect_to volunteers_path, notice: flash_message
+ else
+ redirect_to volunteers_path, notice: "Please select at least one volunteer and one supervisor."
+ end
+ end
+
private
def supervisor_volunteer_params
- params.require(:supervisor_volunteer).permit(:supervisor_id, :volunteer_id)
+ params.require(:supervisor_volunteer).permit(:supervisor_id, :volunteer_id, volunteer_ids: [])
end
def supervisor_volunteer_parent
Supervisor.find(params[:supervisor_id] || supervisor_volunteer_params[:supervisor_id])
end
+
+ def mass_assign_volunteers?
+ supervisor_volunteer_params[:volunteer_ids] && supervisor_volunteer_params[:supervisor_id] ? true : false
+ end
+
+ def bulk_assign!(supervisor, volunteer_ids)
+ created_volunteers = []
+ volunteer_ids.each do |vol_id|
+ if (supervisor_volunteer = SupervisorVolunteer.find_by(volunteer_id: vol_id.to_i))
+ supervisor_volunteer.update!(supervisor_id: supervisor.id)
+ else
+ supervisor_volunteer = supervisor.supervisor_volunteers.create(volunteer_id: vol_id.to_i)
+ end
+ supervisor_volunteer.is_active = true
+ volunteer = supervisor_volunteer.volunteer
+ supervisor_volunteer.save
+ created_volunteers << volunteer.display_name.to_s
+ end
+ created_volunteers
+ end
+
+ def bulk_unassign!(volunteer_ids)
+ unassigned_volunteers = []
+ volunteer_ids.each do |vol_id|
+ supervisor_volunteer = SupervisorVolunteer.find_by(volunteer_id: vol_id.to_i)
+ supervisor_volunteer.update(is_active: false)
+ volunteer = supervisor_volunteer.volunteer
+ supervisor_volunteer.save
+ unassigned_volunteers << volunteer.display_name.to_s # take into account single assignments and give multiple assignments proper format
+ end
+ unassigned_volunteers
+ end
end
diff --git a/app/controllers/volunteers_controller.rb b/app/controllers/volunteers_controller.rb
index 8790dae554..0a3f9d6fa7 100644
--- a/app/controllers/volunteers_controller.rb
+++ b/app/controllers/volunteers_controller.rb
@@ -6,6 +6,7 @@ class VolunteersController < ApplicationController
def index
authorize Volunteer
+ @supervisors = policy_scope(current_organization.supervisors)
end
def show
diff --git a/app/javascript/application.js b/app/javascript/application.js
index c661eaf523..4cbefd7c25 100644
--- a/app/javascript/application.js
+++ b/app/javascript/application.js
@@ -8,6 +8,8 @@ import 'trix'
import '@rails/actiontext'
require('datatables.net-dt')(null, window.jQuery) // First parameter is the global object. Defaults to window if null
+require('datatables.net-select')(null, window.jQuery)
+require('jquery-datatables-checkboxes')(null, window.jQuery)
require('select2')(window.jQuery)
require('@rails/ujs').start()
require('@rails/activestorage').start()
diff --git a/app/javascript/src/dashboard.js b/app/javascript/src/dashboard.js
index 0edc294589..d2d121e5ba 100644
--- a/app/javascript/src/dashboard.js
+++ b/app/javascript/src/dashboard.js
@@ -134,8 +134,21 @@ $(() => { // JQuery's callback for the DOM loading
}
})
},
- order: [[6, 'desc']],
+ order: [[7, 'desc']],
+ select: {
+ style: 'multi'
+ },
columns: [
+ {
+ data: 'id',
+ targets: 0,
+ searchable: false,
+ orderable: false,
+ checkboxes: {
+ selectRow: true,
+ stateSave: false
+ }
+ },
{
name: 'display_name',
render: (data, type, row, meta) => {
@@ -293,6 +306,29 @@ $(() => { // JQuery's callback for the DOM loading
}
})
+ $('#form-bulk-assignment').on('submit', function (e) {
+ const form = this
+ const rowsSelected = volunteersTable.column(0).checkboxes.selected()
+
+ $.each(rowsSelected, function (index, rowId) {
+ $(form).append(
+ $('')
+ .attr('type', 'hidden')
+ .attr('name', 'supervisor_volunteer[volunteer_ids][]')
+ .val(rowId)
+ )
+ })
+ })
+
+ volunteersTable.column(0).on('change', function () {
+ const rowsSelected = volunteersTable.column(0).checkboxes.selected()
+ if (rowsSelected.count() === 0) {
+ $('#volunteers-selected').html('')
+ } else {
+ $('#volunteers-selected').html('s (' + rowsSelected.count() + ')')
+ }
+ })
+
// Because the table saves state, we have to check/uncheck modal inputs based on what
// columns are visible
volunteersTable.columns().every(function (index) {
diff --git a/app/models/volunteer.rb b/app/models/volunteer.rb
index 1304988afa..52440d3a5d 100644
--- a/app/models/volunteer.rb
+++ b/app/models/volunteer.rb
@@ -3,6 +3,7 @@
class Volunteer < User
devise :invitable, invite_for: 1.year
+ BULK_COLUMN = "bulk"
NAME_COLUMN = "name"
EMAIL_COLUMN = "email"
SUPERVISOR_COLUMN = "supervisor"
@@ -16,6 +17,7 @@ class Volunteer < User
EXTRA_LANGUAGES_COLUMN = "has_any_extra_languages"
ACTIONS_COLUMN = "actions"
TABLE_COLUMNS = [
+ BULK_COLUMN,
NAME_COLUMN,
EMAIL_COLUMN,
SUPERVISOR_COLUMN,
diff --git a/app/policies/supervisor_volunteer_policy.rb b/app/policies/supervisor_volunteer_policy.rb
index 3f065a6e8e..3391673264 100644
--- a/app/policies/supervisor_volunteer_policy.rb
+++ b/app/policies/supervisor_volunteer_policy.rb
@@ -6,4 +6,8 @@ def create?
def unassign?
user.casa_admin? || user.supervisor?
end
+
+ def bulk_assignment?
+ user.casa_admin? || user.supervisor?
+ end
end
diff --git a/app/views/supervisors/index.html.erb b/app/views/supervisors/index.html.erb
index a3dc412b42..e8bcbd6855 100644
--- a/app/views/supervisors/index.html.erb
+++ b/app/views/supervisors/index.html.erb
@@ -129,18 +129,14 @@
- |
Active volunteers not assigned to supervisors |
Assigned to Case(s) |
- <% @available_volunteers.each_with_index do |volunteer, index| %>
+ <% @available_volunteers.each do |volunteer| %>
-
- <%= index + 1 %>
- |
<%= link_to volunteer.display_name, edit_volunteer_path(volunteer) %>
|
diff --git a/app/views/volunteers/index.html.erb b/app/views/volunteers/index.html.erb
index 6f527d4b5f..7408458f5e 100644
--- a/app/views/volunteers/index.html.erb
+++ b/app/views/volunteers/index.html.erb
@@ -161,32 +161,88 @@
+
Responsive Data Table
+
+
+
-
-
-
- Name |
- Email |
- Supervisor |
- Status |
- Assigned To Transition Aged Youth |
- Case Number(s) |
- Last Attempted Contact |
- Contacts Made in Past 60 Days |
- Hours spent in last 30 days |
- Extra Languages |
- Actions |
-
-
-
-
-
+ <%= form_with(url: bulk_assignment_supervisor_volunteers_path, id: "form-bulk-assignment") do |form| %>
+
+
+
+ |
+ Name |
+ Email |
+ Supervisor |
+ Status |
+ Assigned To Transition Aged Youth |
+ Case Number(s) |
+ Last Attempted Contact |
+ Contacts Made in Past 60 Days |
+ Hours spent in last 30 days |
+ Extra Languages |
+ Actions |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <%= form.button type: :submit, class: "submit-bulk-assignment main-btn dark-btn btn-hover m-1" do %>
+ Confirm
+ <% end %>
+
+
+
+
+
+
+ <% end %>
-
+
+
diff --git a/config/routes.rb b/config/routes.rb
index 4bf77eb278..2426b06d65 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -128,6 +128,9 @@
end
end
resources :supervisor_volunteers, only: %i[create] do
+ collection do
+ post :bulk_assignment
+ end
member do
patch :unassign
end
diff --git a/db/schema.rb b/db/schema.rb
index ed5ef0a6d9..34d4d85192 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -363,6 +363,12 @@
t.index ["casa_org_id"], name: "index_judges_on_casa_org_id"
end
+ create_table "jwt_denylist", force: :cascade do |t|
+ t.string "jti", null: false
+ t.datetime "exp", null: false
+ t.index ["jti"], name: "index_jwt_denylist_on_jti"
+ end
+
create_table "languages", force: :cascade do |t|
t.string "name"
t.bigint "casa_org_id", null: false
@@ -392,7 +398,6 @@
create_table "learning_hours", force: :cascade do |t|
t.bigint "user_id", null: false
- t.integer "learning_type", default: 5
t.string "name", null: false
t.integer "duration_minutes", null: false
t.integer "duration_hours", null: false
diff --git a/package.json b/package.json
index 5b27bc1b00..9cbb722304 100644
--- a/package.json
+++ b/package.json
@@ -29,9 +29,11 @@
"chart.js": "^4.4.0",
"chartjs-adapter-luxon": "^1.3.1",
"datatables.net-dt": "^1.13.6",
+ "datatables.net-select": "^1.7.0",
"esbuild": "^0.19.4",
"faker": "^5.5.3",
"jquery": "^3.7.1",
+ "jquery-datatables-checkboxes": "^1.2.14",
"js-cookie": "^3.0.5",
"jstz": "^2.1.1",
"lodash": "^4.17.21",
diff --git a/spec/requests/supervisor_volunteers_spec.rb b/spec/requests/supervisor_volunteers_spec.rb
index 88f7897f7a..3ea79675ff 100644
--- a/spec/requests/supervisor_volunteers_spec.rb
+++ b/spec/requests/supervisor_volunteers_spec.rb
@@ -157,4 +157,119 @@
end
end
end
+ # using describe just for clarity
+ describe "POST /bulk_assign" do
+ let!(:volunteer_1) { create(:volunteer, casa_org: casa_org) }
+ let!(:volunteer_2) { create(:volunteer, casa_org: casa_org) }
+ let!(:volunteer_3) { create(:volunteer, casa_org: casa_org) }
+
+ context "when no pre-existing association between supervisor and volunteer exists" do
+ it "creates a new supervisor_volunteers association for each selected volunteer in bulk assign" do
+ valid_parameters = {
+ supervisor_volunteer: {supervisor_id: supervisor.id, volunteer_ids: [volunteer_1.id, volunteer_2.id, volunteer_3.id]}
+ }
+ sign_in(admin)
+
+ expect {
+ post bulk_assignment_supervisor_volunteers_url, params: valid_parameters, headers: {HTTP_REFERER: supervisors_path.to_s}
+ }.to change(SupervisorVolunteer, :count).by(3)
+ expect(response).to redirect_to volunteers_path
+ end
+ end
+
+ context "when an inactive association between supervisor and volunteer exists" do
+ let!(:association) do
+ create(
+ :supervisor_volunteer,
+ supervisor: supervisor,
+ volunteer: volunteer_1,
+ is_active: false
+ )
+ end
+
+ it "sets that association to active" do
+ valid_parameters = {
+ supervisor_volunteer: {supervisor_id: supervisor.id, volunteer_ids: [volunteer_1.id, volunteer_2.id, volunteer_3.id]}
+ }
+ sign_in(admin)
+
+ expect {
+ post bulk_assignment_supervisor_volunteers_url, params: valid_parameters, headers: {HTTP_REFERER: supervisors_path.to_s}
+ }.to change(SupervisorVolunteer, :count).by(2) # bc 1 already exists, we are creating 2 new ones
+ expect(response).to redirect_to volunteers_path
+
+ association.reload
+ expect(association.is_active?).to be(true)
+ end
+ end
+
+ context "when passing the volunteer_ids as the supervisor_volunteer_params" do
+ let!(:association) do
+ create(
+ :supervisor_volunteer,
+ supervisor: supervisor,
+ volunteer: volunteer_1,
+ is_active: false
+ )
+ end
+
+ it "will still set the association as active while creaing two new supervisor_volunteer records" do
+ valid_parameters = {
+ supervisor_volunteer: {supervisor_id: supervisor.id, volunteer_ids: [volunteer_1.id, volunteer_2.id, volunteer_3.id]}
+ }
+
+ sign_in(admin)
+
+ expect {
+ post bulk_assignment_supervisor_volunteers_url, params: valid_parameters, headers: {HTTP_REFERER: supervisors_path.to_s}
+ }.to change(SupervisorVolunteer, :count).by(2) # bc 1 already exists, we are creating 2 new ones
+ expect(response).to redirect_to volunteers_path
+
+ association.reload
+ expect(association.is_active?).to be(true)
+ end
+ end
+ end
+
+ describe "POST /bulk_unassign" do
+ let!(:volunteer_1) { create(:volunteer, casa_org: casa_org) }
+ let!(:volunteer_2) { create(:volunteer, casa_org: casa_org) }
+ let!(:volunteer_3) { create(:volunteer, casa_org: casa_org) }
+
+ context "when passing in several volunteer with selected --No supervior--" do
+ let!(:assigned_supervisor) { build(:supervisor, casa_org: casa_org) }
+ let!(:association_1) do
+ create(
+ :supervisor_volunteer,
+ supervisor: assigned_supervisor,
+ volunteer: volunteer_1,
+ is_active: true
+ )
+ end
+ let!(:association_2) do
+ create(
+ :supervisor_volunteer,
+ supervisor: assigned_supervisor,
+ volunteer: volunteer_2,
+ is_active: true
+ )
+ end
+ it "will set the association to inactive" do
+ valid_parameters = {
+ supervisor_volunteer: {supervisor_id: "unassign", volunteer_ids: [volunteer_1.id, volunteer_2.id]}
+ }
+ sign_in(admin)
+
+ expect {
+ post bulk_assignment_supervisor_volunteers_url, params: valid_parameters, headers: {HTTP_REFERER: supervisors_path.to_s}
+ }.not_to change(SupervisorVolunteer, :count)
+ expect(response).to redirect_to volunteers_path
+
+ association_1.reload
+ association_2.reload
+ expect(association_1.is_active?).to be(false)
+ expect(association_2.is_active?).to be(false)
+ end
+ end
+ end
end
diff --git a/spec/system/volunteers/index_spec.rb b/spec/system/volunteers/index_spec.rb
index b37c72464b..e6253e1d14 100644
--- a/spec/system/volunteers/index_spec.rb
+++ b/spec/system/volunteers/index_spec.rb
@@ -175,6 +175,54 @@
expect(supervisor_cell.text).to eq ""
end
+
+ context "when bulk assignments" do
+ it "displays supervisor's name when multiple volunteers assigned supervisor", js: true do
+ name = "Superduper Visor"
+ supervisor = create(:supervisor, display_name: name, casa_org: organization)
+ create(:volunteer, casa_org: organization)
+ create(:volunteer, casa_org: organization)
+ sign_in admin
+
+ visit volunteers_path
+
+ find("tr.odd").click
+ find("tr.even").click
+ click_on "Manage Volunteer"
+ select supervisor.display_name, from: "supervisor_volunteer[supervisor_id]"
+
+ click_on "Confirm"
+
+ supervisor_cell1 = page.find("tbody .odd .supervisor-column")
+ supervisor_cell2 = page.find("tbody .even .supervisor-column")
+
+ expect(supervisor_cell1.text).to eq supervisor.display_name
+ expect(supervisor_cell2.text).to eq supervisor.display_name
+ end
+
+ it "displays no supervisor name when multiple volunteers unassigned supervisor", js: true do
+ name = "Superduper Visor"
+ supervisor = create(:supervisor, display_name: name, casa_org: organization)
+ create(:volunteer, supervisor: supervisor, casa_org: organization)
+ create(:volunteer, supervisor: supervisor, casa_org: organization)
+ sign_in admin
+
+ visit volunteers_path
+
+ find("tr.odd").click
+ find("tr.even").click
+ click_on "Manage Volunteer"
+ select "-- No Supervisor --", from: "supervisor_volunteer[supervisor_id]"
+
+ click_on "Confirm"
+
+ supervisor_cell1 = page.find("tbody .odd .supervisor-column")
+ supervisor_cell2 = page.find("tbody .even .supervisor-column")
+
+ expect(supervisor_cell1.text).to eq ""
+ expect(supervisor_cell2.text).to eq ""
+ end
+ end
end
context "when timed out" do
diff --git a/yarn.lock b/yarn.lock
index a72b4d062d..28121c1572 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2910,6 +2910,21 @@ datatables.net-dt@^1.13.6:
datatables.net ">=1.13.4"
jquery ">=1.7"
+datatables.net-select@^1.7.0:
+ version "1.7.0"
+ resolved "https://registry.yarnpkg.com/datatables.net-select/-/datatables.net-select-1.7.0.tgz#a108752ee6109a49392d19fec7406c953be11665"
+ integrity sha512-ps8eL8S2gUV7EdzMraw8mfQlHXpfuc8TC2onBxdk0snP8eizPe85VhpI3r4ULvPRTTI7vcViz8E7JV8aayA2lw==
+ dependencies:
+ datatables.net ">=1.13.4"
+ jquery ">=1.7"
+
+datatables.net@>=1.10.8:
+ version "1.13.6"
+ resolved "https://registry.yarnpkg.com/datatables.net/-/datatables.net-1.13.6.tgz#6e282adbbb2732e8df495611b8bb54e19f7a943e"
+ integrity sha512-rHNcnW+yEP9me82/KmRcid5eKrqPqW3+I/p1TwqCW3c/7GRYYkDyF6aJQOQ9DNS/pw+nyr4BVpjyJ3yoZXiFPg==
+ dependencies:
+ jquery ">=1.7"
+
datatables.net@>=1.13.4:
version "1.13.5"
resolved "https://registry.yarnpkg.com/datatables.net/-/datatables.net-1.13.5.tgz#790a3d70d5e103f5465ed8c52a50eb242e1e2dc4"
@@ -4980,6 +4995,14 @@ joi@^17.7.0:
"@sideway/formula" "^3.0.0"
"@sideway/pinpoint" "^2.0.0"
+jquery-datatables-checkboxes@^1.2.14:
+ version "1.2.14"
+ resolved "https://registry.yarnpkg.com/jquery-datatables-checkboxes/-/jquery-datatables-checkboxes-1.2.14.tgz#5bada981140b9ba645e80d6c2caa8bbbb4123936"
+ integrity sha512-99B6PmS3ZdinDyqF5vBiykd8B+NohHRBBs/a5/hrYRlsBxbWqQ2KtigSvAc2qFdxPOyrcCV75nWBYnlrtdCGgg==
+ dependencies:
+ datatables.net ">=1.10.8"
+ jquery ">=1.7"
+
jquery@>=1.7, "jquery@>=3.4.0 <4.0.0", jquery@^3.7.1:
version "3.7.1"
resolved "https://registry.yarnpkg.com/jquery/-/jquery-3.7.1.tgz#083ef98927c9a6a74d05a6af02806566d16274de"