diff --git a/.rubocop.yml b/.rubocop.yml index a50e8b830..0b5a83bba 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -9,6 +9,10 @@ AllCops: - bin/**/* - vendor/bundle/**/* +Lint/Syntax: + Exclude: + - app/views/**/*.haml + plugins: - rubocop-capybara - rubocop-performance diff --git a/app/controllers/admin/groups_controller.rb b/app/controllers/admin/groups_controller.rb index de2539552..3abf00058 100644 --- a/app/controllers/admin/groups_controller.rb +++ b/app/controllers/admin/groups_controller.rb @@ -22,11 +22,15 @@ def create def show @group = Group.find(params[:id]) authorize @group + + @eligible_count = @group.eligible_members.count + @total_count = @group.members.count + @pagy, @members = pagy(Group.members_by_recent_rsvp(@group), items: 20) end private def group_params - params.expect(group: [:name, :description, :chapter_id]) + params.expect(group: %i[name description chapter_id]) end end diff --git a/app/models/group.rb b/app/models/group.rb index 56cb4506e..c4e0c0bef 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -11,6 +11,15 @@ class Group < ApplicationRecord scope :students, -> { where(name: 'Students') } scope :coaches, -> { where(name: 'Coaches') } + def self.members_by_recent_rsvp(group) + group.members + .joins('LEFT JOIN workshop_invitations ON workshop_invitations.member_id = members.id') + .joins('LEFT JOIN workshops ON workshops.id = workshop_invitations.workshop_id') + .select('members.*, MAX(workshops.date_and_time) as last_rsvp_at') + .group('members.id') + .order('MAX(workshops.date_and_time) DESC NULLS LAST') + end + validates :name, presence: true, inclusion: { in: NAMES, message: 'Invalid name for Group' } alias city chapter diff --git a/app/views/admin/groups/show.html.haml b/app/views/admin/groups/show.html.haml index e7fc1ce68..2155eef43 100644 --- a/app/views/admin/groups/show.html.haml +++ b/app/views/admin/groups/show.html.haml @@ -1,18 +1,84 @@ +- content_for :head do + %link{ href: 'https://cdn.jsdelivr.net/npm/tom-select@2.4.3/dist/css/tom-select.bootstrap5.min.css', rel: 'stylesheet', type: 'text/css' } + %script{ src: 'https://cdn.jsdelivr.net/npm/tom-select@2.4.3/dist/js/tom-select.complete.min.js' } + .container.py-4.py-lg-5 .row.mb-4 .col %h1 = @group.name - %small.text-muted #{@group.chapter.name} + %small.text-muted= @group.chapter.name + + .row + .col + %h3.mb-3 Members (#{@eligible_count} eligible, #{@total_count} total) + + .row.mb-4 + .col-12.col-md-6 + = select_tag 'member_lookup_id', nil, class: 'form-control', placeholder: 'Search members by name or email...' + .col-auto + = link_to 'View Profile', '#', { class: 'btn btn-primary', id: 'view_profile' } .row .col - %h3.mb-3 Members (#{@group.eligible_members.count} eligible, #{@group.members.count} total) %table.table.table-striped.table-hover + %thead + %tr + %th Avatar + %th Name + %th Email + %th Mobile + %th T&C %tbody - - @group.members.each do |member| + - @members.each do |member| %tr %td= image_tag(member.avatar(32), class: 'rounded-circle', title: member.full_name, alt: member.full_name) %td= link_to member.full_name, admin_member_path(member) %td= mail_to member.email, member.email %td= member.mobile + %td + - if member.accepted_toc_at + %i.fa.fa-check.text-success{ title: "Accepted on #{member.accepted_toc_at.to_date}" } + - else + %i.fa.fa-times.text-danger{ title: 'Not accepted' } + + .row + .col + = render partial: 'shared/pagination', locals: { pagy: @pagy, model: 'member' } + +-# TomSelect initialization +:javascript + document.addEventListener('DOMContentLoaded', function() { + var control = document.getElementById('member_lookup_id'); + var viewLink = document.getElementById('view_profile'); + + var ts = new TomSelect(control, { + create: false, + maxItems: 1, + placeholder: 'Search members by name or email...', + valueField: 'id', + labelField: 'full_name', + searchField: ['full_name', 'email'], + load: function(query, callback) { + if (query.length < 3) { + callback(); + return; + } + + var url = '/admin/members/search?q=' + encodeURIComponent(query); + fetch(url) + .then(function(response) { return response.json(); }) + .then(function(data) { callback(data); }) + .catch(function() { callback(); }); + }, + onChange: function(value) { + if (value) { + viewLink.href = '/admin/members/' + value; + viewLink.classList.remove('disabled'); + } else { + viewLink.href = '#'; + viewLink.classList.add('disabled'); + } + } + }); + }); \ No newline at end of file diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb index 78dbe31c8..9165ce696 100644 --- a/spec/models/group_spec.rb +++ b/spec/models/group_spec.rb @@ -27,4 +27,26 @@ expect(group.eligible_members).to be_empty end end + + describe '.members_by_recent_rsvp' do + let(:group) { Fabricate(:group, name: 'Students') } + let(:chapter) { group.chapter } + + it 'orders members by most recent workshop RSVP' do + old_workshop = Fabricate(:workshop, chapter: chapter, date_and_time: 1.month.ago) + new_workshop = Fabricate(:workshop, chapter: chapter, date_and_time: 1.week.ago) + + member_old = Fabricate(:member, groups: [group]) + member_new = Fabricate(:member, groups: [group]) + _member_no_rsvp = Fabricate(:member, groups: [group]) + + Fabricate(:workshop_invitation, workshop: old_workshop, member: member_old, attending: true) + Fabricate(:workshop_invitation, workshop: new_workshop, member: member_new, attending: true) + + results = Group.members_by_recent_rsvp(group).to_a + + expect(results.first).to eq(member_new) + expect(results.last).to eq(_member_no_rsvp) + end + end end