From 8108f6974612d175f32c89b33119e9405d12bb5c Mon Sep 17 00:00:00 2001 From: Kevin Rukundo Date: Fri, 31 Jul 2026 01:20:37 +0200 Subject: [PATCH 1/2] feat(2632): yodate organization statistics section - add data from last entry when it loads - disabled dates without lessons. - Show a message when there is not data --- .../organization_statistics.rb | 23 +++++++----- app/components/datepicker.rb | 7 ++-- app/controllers/organizations_controller.rb | 10 ++++-- .../controllers/datepicker_controller.js | 17 +++++---- app/views/organizations/show.html.erb | 4 +-- config/locales/en.yml | 3 ++ spec/features/organization_features_spec.rb | 35 ++++++++++++++----- 7 files changed, 69 insertions(+), 30 deletions(-) diff --git a/app/components/common_components/organization_statistics.rb b/app/components/common_components/organization_statistics.rb index 7fcf85346..3e14f28d1 100644 --- a/app/components/common_components/organization_statistics.rb +++ b/app/components/common_components/organization_statistics.rb @@ -2,9 +2,11 @@ class CommonComponents::OrganizationStatistics < ViewComponent::Base include ApplicationHelper include CollectionHelper - def initialize(group_lesson_summaries, selected_date = Time.zone.today) + def initialize(group_lesson_summaries, selected_date:, available_lesson_dates: [], used_default_date: false) @lesson_summaries = group_lesson_summaries @selected_date = selected_date + @available_lesson_dates = available_lesson_dates + @used_default_date = used_default_date @number_of_lessons = @lesson_summaries.count @total_data_points = @lesson_summaries.sum(&:grade_count) end @@ -12,13 +14,18 @@ def initialize(group_lesson_summaries, selected_date = Time.zone.today) erb_template <<~ERB
- <%= render Datepicker.new(date: @selected_date, target: 'selected_date') do |picker| %> - <% picker.with_input_field do %> - - <%= render CommonComponents::ButtonComponent.new(label: t(:filter), options: { 'data-datepicker-target' => 'anchor' })%> +
+ <%= render Datepicker.new(date: @selected_date, target: 'selected_date', enabled_dates: @available_lesson_dates.map(&:to_s)) do |picker| %> + <% picker.with_input_field do %> + + <%= render CommonComponents::ButtonComponent.new(label: t(:filter), options: { 'data-datepicker-target' => 'anchor' })%> + <% end %> + <% end %> + <% if @used_default_date && @available_lesson_dates.any? %> +

<%= t(:showing_data_for_last_lessons) %>

<% end %> - <% end %> +
<% if @lesson_summaries.any? %>
@@ -40,7 +47,7 @@ def initialize(group_lesson_summaries, selected_date = Time.zone.today)
<% else %> -

No data for the selected date

+

<%= t(:no_lessons_data_exists) %>

<% end %> ERB diff --git a/app/components/datepicker.rb b/app/components/datepicker.rb index 070c50f30..4ea2c68ba 100644 --- a/app/components/datepicker.rb +++ b/app/components/datepicker.rb @@ -2,7 +2,7 @@ class Datepicker < ViewComponent::Base renders_one :input_field erb_template <<~ERB -
> +
<% if @enabled_dates %> data-datepicker-enabled-dates-value='<%= @enabled_dates.to_json %>'<% end %>> <% if @form %> <%= @form.text_field @target, data: { 'datepicker-target' => 'picker' }, class: @custom_class || 'rounded-md border-purple-500 text-sm focus:border-green-600 focus:outline-hidden focus:ring-green-600', autocomplete: 'disabled' %> @@ -15,11 +15,14 @@ class Datepicker < ViewComponent::Base
ERB - def initialize(date:, target:, form: nil, custom_name: nil, custom_class: nil) + # rubocop:disable Metrics/ParameterLists + def initialize(date:, target:, form: nil, custom_name: nil, custom_class: nil, enabled_dates: nil) @date = date @target = target @form = form @custom_name = custom_name @custom_class = custom_class + @enabled_dates = enabled_dates end + # rubocop:enable Metrics/ParameterLists end diff --git a/app/controllers/organizations_controller.rb b/app/controllers/organizations_controller.rb index f695b6d9b..3e0109de2 100644 --- a/app/controllers/organizations_controller.rb +++ b/app/controllers/organizations_controller.rb @@ -118,8 +118,7 @@ def initialize_organization(id) @new_member = User.new @roles = Role::LOCAL_ROLES.keys - @selected_date = params[:selected_date] || Time.zone.today - @lesson_summaries = GroupLessonSummary.where(chapter_id: @organization.chapters, lesson_date: @selected_date) + populate_lesson_activity end def chapter_order_scope @@ -171,6 +170,13 @@ def undelete private + def populate_lesson_activity + @available_lesson_dates = GroupLessonSummary.where(chapter_id: @organization.chapters).distinct.pluck(:lesson_date).sort + @used_default_date = params[:selected_date].blank? + @selected_date = params[:selected_date].presence || @available_lesson_dates.last || Time.zone.today + @lesson_summaries = GroupLessonSummary.where(chapter_id: @organization.chapters, lesson_date: @selected_date) + end + def file_is_csv?(content_type) %w[text/csv text/x-csv application/vnd.ms-excel application/csv application/x-csv].include? content_type end diff --git a/app/javascript/controllers/datepicker_controller.js b/app/javascript/controllers/datepicker_controller.js index 6c4f25459..1caa9ebe9 100644 --- a/app/javascript/controllers/datepicker_controller.js +++ b/app/javascript/controllers/datepicker_controller.js @@ -3,7 +3,7 @@ import * as pikaday from 'pikaday' // Connects to data-controller="datepicker" export default class extends Controller { - static values = { date: String } + static values = { date: String, enabledDates: Array } static targets = [ "picker", "anchor" ] updateFilter() { @@ -19,16 +19,18 @@ export default class extends Controller { } connect() { + const formatDate = (date) => { + const parts = [date.getFullYear(), ('0'+(date.getMonth()+1)).slice(-2), ('0'+date.getDate()).slice(-2)]; + return parts.join("-"); + } + const enabledDates = this.hasEnabledDatesValue && this.enabledDatesValue.length > 0 ? new Set(this.enabledDatesValue) : null + const picker = new Pikaday({ field: this.pickerTarget, minDate: new Date(Date.parse('1970-01-01')), maxDate: new Date(), format: 'YYYY-MM-DD', - toString(date, format) { - // showing the date here in the format defined above - const parts = [date.getFullYear(), ('0'+(date.getMonth()+1)).slice(-2), ('0'+date.getDate()).slice(-2)]; - return parts.join("-"); - }, + toString: formatDate, parse(dateString, format) { // dateString is the result of the `toString` method const parts = dateString.split('-'); @@ -36,7 +38,8 @@ export default class extends Controller { const month = parseInt(parts[1]) - 1; const day = parseInt(parts[2]); return new Date(year, month, day); - } + }, + disableDayFn: enabledDates ? (date) => !enabledDates.has(formatDate(date)) : undefined }); } } diff --git a/app/views/organizations/show.html.erb b/app/views/organizations/show.html.erb index 9d490c4bb..c5775df3b 100644 --- a/app/views/organizations/show.html.erb +++ b/app/views/organizations/show.html.erb @@ -31,10 +31,10 @@ end %> <% end %> <% end %> <% end %> - <%= render CommonComponents::Card.new(title: "Statistics".capitalize) do |card| %> + <%= render CommonComponents::Card.new(title: t(:lesson_activity)) do |card| %> <% card.with_card_content do %>
- <%= render CommonComponents::OrganizationStatistics.new(@lesson_summaries, @selected_date) %> + <%= render CommonComponents::OrganizationStatistics.new(@lesson_summaries, selected_date: @selected_date, available_lesson_dates: @available_lesson_dates, used_default_date: @used_default_date) %>
<% end %> <% end %> diff --git a/config/locales/en.yml b/config/locales/en.yml index 83b1a7f19..1e112cdbf 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -395,6 +395,9 @@ en: groups_with_lessons: Groups with Lessons performance_change: Performance change + lesson_activity: Lesson Activity + showing_data_for_last_lessons: Showing data for last lessons + no_lessons_data_exists: No lessons data exists nr_of_lessons: Nr. of Lessons nr_of_assessments: Nr. of Assessments nr_of_graded_students: Nr. of Graded students diff --git a/spec/features/organization_features_spec.rb b/spec/features/organization_features_spec.rb index fcae57eb8..195aa09b1 100644 --- a/spec/features/organization_features_spec.rb +++ b/spec/features/organization_features_spec.rb @@ -48,26 +48,43 @@ end end - it 'does not display the organization\'s statistics when there is no data' do - expect(page).to have_content 'No data for the selected date' - expect(page).to have_content 'Statistics' + it 'defaults to the most recent lesson date and shows lesson activity immediately' do + expect(page).to have_content 'Lesson Activity' + expect(page).to have_content 'Showing data for last lessons' + expect(page).to have_content 'Nr. of Lessons' + expect(page).to have_content 'Nr. of Assessments' + expect(page).to have_content 'Groups with Lessons' + + statistic_components = all('dd.tracking-tight') + + expect(statistic_components[0]).to have_content 1 + expect(statistic_components[1]).to have_content 3 + expect(statistic_components[2]).to have_content @groups.first.group_chapter_name end - it 'does display the organization\'s statistics when there is data' do - expect(page).to have_content 'Statistics' + it 'shows a clear message when the selected date has no lesson data' do + empty_date = @lesson.date - 100 + visit organization_path(@organization, selected_date: empty_date) + + expect(page).to have_content 'No lessons data exists' + expect(page).not_to have_content 'Showing data for last lessons' + end + + it 'still shows lesson activity when explicitly filtering to a date with data' do fill_in 'select_date', with: @lesson.date click_link 'Filter' expect(page).to have_content 'Nr. of Lessons' expect(page).to have_content 'Nr. of Assessments' expect(page).to have_content 'Groups with Lessons' + expect(page).not_to have_content 'Showing data for last lessons' + end - statistic_components = all('dd.tracking-tight') + it 'only enables dates with lesson data in the date picker' do + enabled_dates = JSON.parse(find('[data-controller="datepicker"]')['data-datepicker-enabled-dates-value']) - expect(statistic_components[0]).to have_content 1 - expect(statistic_components[1]).to have_content 3 - expect(statistic_components[2]).to have_content @groups.first.group_chapter_name + expect(enabled_dates).to eq [@lesson.date.to_s] end end From 77da4199378a00eaeb5f3755db0c69f02c1eb8b3 Mon Sep 17 00:00:00 2001 From: Kevin Rukundo Date: Thu, 6 Aug 2026 12:48:57 +0200 Subject: [PATCH 2/2] fix(2632): address organization statistics review feedback - pass number_of_lessons/total_data_points from the controller instead of computing them in the component - fix datepicker enabling all dates when an organization has zero lessons - keep available lesson dates and lesson summaries sourced from the same GroupLessonSummary relation, so we don't default to (or enable) a date with no actual data - remove redundant enabled-dates feature spec --- .../common_components/organization_statistics.rb | 10 ++++++---- app/controllers/organizations_controller.rb | 7 +++++-- app/javascript/controllers/datepicker_controller.js | 2 +- app/views/organizations/show.html.erb | 2 +- spec/features/organization_features_spec.rb | 6 ------ 5 files changed, 13 insertions(+), 14 deletions(-) diff --git a/app/components/common_components/organization_statistics.rb b/app/components/common_components/organization_statistics.rb index 3e14f28d1..95ef624b2 100644 --- a/app/components/common_components/organization_statistics.rb +++ b/app/components/common_components/organization_statistics.rb @@ -2,14 +2,16 @@ class CommonComponents::OrganizationStatistics < ViewComponent::Base include ApplicationHelper include CollectionHelper - def initialize(group_lesson_summaries, selected_date:, available_lesson_dates: [], used_default_date: false) - @lesson_summaries = group_lesson_summaries + # rubocop:disable Metrics/ParameterLists + def initialize(lesson_summaries, number_of_lessons:, total_data_points:, selected_date:, available_lesson_dates: [], used_default_date: false) + @lesson_summaries = lesson_summaries + @number_of_lessons = number_of_lessons + @total_data_points = total_data_points @selected_date = selected_date @available_lesson_dates = available_lesson_dates @used_default_date = used_default_date - @number_of_lessons = @lesson_summaries.count - @total_data_points = @lesson_summaries.sum(&:grade_count) end + # rubocop:enable Metrics/ParameterLists erb_template <<~ERB
diff --git a/app/controllers/organizations_controller.rb b/app/controllers/organizations_controller.rb index 3e0109de2..f2b858e89 100644 --- a/app/controllers/organizations_controller.rb +++ b/app/controllers/organizations_controller.rb @@ -171,10 +171,13 @@ def undelete private def populate_lesson_activity - @available_lesson_dates = GroupLessonSummary.where(chapter_id: @organization.chapters).distinct.pluck(:lesson_date).sort + lesson_summaries_for_organization = GroupLessonSummary.where(chapter_id: @organization.chapters) + @available_lesson_dates = lesson_summaries_for_organization.distinct.pluck(:lesson_date).sort @used_default_date = params[:selected_date].blank? @selected_date = params[:selected_date].presence || @available_lesson_dates.last || Time.zone.today - @lesson_summaries = GroupLessonSummary.where(chapter_id: @organization.chapters, lesson_date: @selected_date) + @lesson_summaries = lesson_summaries_for_organization.where(lesson_date: @selected_date).to_a + @number_of_lessons = @lesson_summaries.count + @total_data_points = @lesson_summaries.sum(&:grade_count) end def file_is_csv?(content_type) diff --git a/app/javascript/controllers/datepicker_controller.js b/app/javascript/controllers/datepicker_controller.js index 1caa9ebe9..63b2251cd 100644 --- a/app/javascript/controllers/datepicker_controller.js +++ b/app/javascript/controllers/datepicker_controller.js @@ -23,7 +23,7 @@ export default class extends Controller { const parts = [date.getFullYear(), ('0'+(date.getMonth()+1)).slice(-2), ('0'+date.getDate()).slice(-2)]; return parts.join("-"); } - const enabledDates = this.hasEnabledDatesValue && this.enabledDatesValue.length > 0 ? new Set(this.enabledDatesValue) : null + const enabledDates = this.hasEnabledDatesValue ? new Set(this.enabledDatesValue) : null const picker = new Pikaday({ field: this.pickerTarget, diff --git a/app/views/organizations/show.html.erb b/app/views/organizations/show.html.erb index c5775df3b..a1676987e 100644 --- a/app/views/organizations/show.html.erb +++ b/app/views/organizations/show.html.erb @@ -34,7 +34,7 @@ end %> <%= render CommonComponents::Card.new(title: t(:lesson_activity)) do |card| %> <% card.with_card_content do %>
- <%= render CommonComponents::OrganizationStatistics.new(@lesson_summaries, selected_date: @selected_date, available_lesson_dates: @available_lesson_dates, used_default_date: @used_default_date) %> + <%= render CommonComponents::OrganizationStatistics.new(@lesson_summaries, number_of_lessons: @number_of_lessons, total_data_points: @total_data_points, selected_date: @selected_date, available_lesson_dates: @available_lesson_dates, used_default_date: @used_default_date) %>
<% end %> <% end %> diff --git a/spec/features/organization_features_spec.rb b/spec/features/organization_features_spec.rb index 195aa09b1..f85f27265 100644 --- a/spec/features/organization_features_spec.rb +++ b/spec/features/organization_features_spec.rb @@ -80,12 +80,6 @@ expect(page).to have_content 'Groups with Lessons' expect(page).not_to have_content 'Showing data for last lessons' end - - it 'only enables dates with lesson data in the date picker' do - enabled_dates = JSON.parse(find('[data-controller="datepicker"]')['data-datepicker-enabled-dates-value']) - - expect(enabled_dates).to eq [@lesson.date.to_s] - end end describe 'Adding members to organization' do