diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb index 232ac85..37df884 100644 --- a/app/controllers/groups_controller.rb +++ b/app/controllers/groups_controller.rb @@ -2,7 +2,6 @@ class GroupsController < ApplicationController def index - roots = Group.where(parent_id: nil) - render jsonapi: roots, include: [children: [children: [:children]]] + render json: Groups::SummaryQuery.new.call.as_json end end diff --git a/app/models/group.rb b/app/models/group.rb index 105d08c..b06c1ba 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -30,6 +30,8 @@ class Group < ApplicationRecord private def set_color + return if color.present? + new_color = "##{SecureRandom.hex(3)}".paint new_color = new_color.lighten(30) if new_color.dark? self.color = new_color diff --git a/app/queries/groups/summary_query.rb b/app/queries/groups/summary_query.rb new file mode 100644 index 0000000..637a508 --- /dev/null +++ b/app/queries/groups/summary_query.rb @@ -0,0 +1,31 @@ +# Copyright (C) 2024 Manuel Bustillo + +module Groups + class SummaryQuery + def call + ActiveRecord::Base.connection.execute(query).to_a + end + + private + + def query + <<~SQL.squish + SELECT#{' '} + groups.id, + groups.name, + groups.icon, + groups.parent_id, + groups.color, + count(*) filter (where status IS NOT NULL) as total, + count(*) filter (where status = 0) as considered, + count(*) filter (where status = 10) as invited, + count(*) filter (where status = 20) as confirmed, + count(*) filter (where status = 30) as declined, + count(*) filter (where status = 40) as tentative + FROM groups + LEFT JOIN guests on groups.id = guests.group_id + GROUP BY groups.id + SQL + end + end +end diff --git a/spec/queries/groups/summary_query_spec.rb b/spec/queries/groups/summary_query_spec.rb new file mode 100644 index 0000000..6e2c3d2 --- /dev/null +++ b/spec/queries/groups/summary_query_spec.rb @@ -0,0 +1,98 @@ +# Copyright (C) 2024 Manuel Bustillo + +require 'rails_helper' + +module Groups + RSpec.describe SummaryQuery do + describe '#call' do + subject { described_class.new.call } + + context 'when there are no groups' do + it { is_expected.to eq([]) } + end + + context 'when groups are defined' do + let!(:parent) { create(:group, name: 'Friends', icon: 'icon-1', color: '#FF0000') } + let!(:child) { create(:group, name: 'Family', icon: 'icon-2', color: '#00FF00', parent:) } + + context 'when there are no guests' do + it 'returns the summary of groups' do + is_expected.to contain_exactly( + { 'id' => parent.id, + 'name' => 'Friends', + 'icon' => 'icon-1', + 'parent_id' => nil, + 'color' => '#FF0000', + 'total' => 0, + 'considered' => 0, + 'invited' => 0, + 'confirmed' => 0, + 'declined' => 0, + 'tentative' => 0 }, + { 'id' => child.id, + 'name' => 'Family', + 'icon' => 'icon-2', + 'parent_id' => parent.id, + 'color' => '#00FF00', + 'total' => 0, + 'considered' => 0, + 'invited' => 0, + 'confirmed' => 0, + 'declined' => 0, + 'tentative' => 0 } + ) + end + end + + context 'when there are guests' do + before do + # Parent group + create_list(:guest, 2, group: parent, status: :considered) + create_list(:guest, 3, group: parent, status: :invited) + create_list(:guest, 4, group: parent, status: :confirmed) + create_list(:guest, 5, group: parent, status: :declined) + create_list(:guest, 6, group: parent, status: :tentative) + + # Child group + create_list(:guest, 7, group: child, status: :considered) + create_list(:guest, 8, group: child, status: :invited) + create_list(:guest, 9, group: child, status: :confirmed) + create_list(:guest, 10, group: child, status: :declined) + create_list(:guest, 11, group: child, status: :tentative) + end + + it 'returns the summary of groups' do + is_expected.to contain_exactly( + { + 'id' => parent.id, + 'name' => 'Friends', + 'icon' => 'icon-1', + 'parent_id' => nil, + 'color' => '#FF0000', + 'total' => 20, + 'considered' => 2, + 'invited' => 3, + 'confirmed' => 4, + 'declined' => 5, + 'tentative' => 6 + }, + { + 'id' => child.id, + 'name' => 'Family', + 'icon' => 'icon-2', + 'parent_id' => parent.id, + 'color' => '#00FF00', + 'total' => 45, + 'considered' => 7, + 'invited' => 8, + 'confirmed' => 9, + 'declined' => 10, + 'tentative' => 11 + } + ) + end + end + end + end + end +end