diff --git a/dpc-portal/app/components/page/credential_delegate/list_component.html.erb b/dpc-portal/app/components/page/credential_delegate/list_component.html.erb index 7fdd4cd821..511da77fad 100644 --- a/dpc-portal/app/components/page/credential_delegate/list_component.html.erb +++ b/dpc-portal/app/components/page/credential_delegate/list_component.html.erb @@ -27,5 +27,18 @@

There are no pending credential delegates.

<% end %> +
+

Expired invitations

+

These invites expired. You can resend the invite to give them more time to accept.

+ <% if @expired_invitations.present? %> + <%= render(Core::Table::TableComponent.new(id: 'expired-invitation-table', additional_classes: ['width-full'], sortable: false)) do %> + <%= render(Core::Table::HeaderComponent.new(caption: 'Expired Invitation Table', + columns: ['Name', 'Email', 'Expired on'])) %> + <%= render(Core::Table::RowComponent.with_collection(@expired_invitations, keys: ['full_name', 'email', 'expired_at'], obj_name: 'expired invitation')) %> + <% end %> + <% else %> +

You have no expired invitations.

+ <% end %> +
<% end %> diff --git a/dpc-portal/app/components/page/credential_delegate/list_component.rb b/dpc-portal/app/components/page/credential_delegate/list_component.rb index 2e45714085..ec09181d57 100644 --- a/dpc-portal/app/components/page/credential_delegate/list_component.rb +++ b/dpc-portal/app/components/page/credential_delegate/list_component.rb @@ -6,11 +6,12 @@ module CredentialDelegate class ListComponent < ViewComponent::Base attr_reader :organization, :active_credential_delegates, :pending_credential_delegates - def initialize(organization, cd_invitations, credential_delegates) + def initialize(organization, pending_invitations, expired_invitations, credential_delegates) super @organization = organization @active_credential_delegates = credential_delegates.map(&:show_attributes) - @pending_credential_delegates = cd_invitations.map(&:show_attributes) + @pending_credential_delegates = pending_invitations.map(&:show_attributes) + @expired_invitations = expired_invitations.map(&:show_attributes) end end end diff --git a/dpc-portal/app/components/page/credential_delegate/list_component_preview.rb b/dpc-portal/app/components/page/credential_delegate/list_component_preview.rb index a0c763ac3b..f848da271c 100644 --- a/dpc-portal/app/components/page/credential_delegate/list_component_preview.rb +++ b/dpc-portal/app/components/page/credential_delegate/list_component_preview.rb @@ -11,24 +11,34 @@ module CredentialDelegate # class ListComponentPreview < ViewComponent::Preview def empty - render(Page::CredentialDelegate::ListComponent.new(org, [], [])) + render(Page::CredentialDelegate::ListComponent.new(org, [], [], [])) end def active user1 = User.new(given_name: 'Bob', family_name: 'Hodges', email: 'bob@example.com') user2 = User.new(given_name: 'Lisa', family_name: 'Franklin', email: 'lisa@example.com') cds = [CdOrgLink.new(user: user1, created_at: 1.week.ago), CdOrgLink.new(user: user2, created_at: 2.weeks.ago)] - render(Page::CredentialDelegate::ListComponent.new(org, [], cds)) + render(Page::CredentialDelegate::ListComponent.new(org, [], [], cds)) end def pending cds = [ Invitation.new(invited_given_name: 'Bob', invited_family_name: 'Hodges', invited_email: 'bob@example.com', - id: 2), + id: 2, created_at: 1.day.ago), Invitation.new(invited_given_name: 'Lisa', invited_family_name: 'Franklin', - invited_email: 'lisa@example.com', id: 3) + invited_email: 'lisa@example.com', id: 3, created_at: 1.day.ago) ] - render(Page::CredentialDelegate::ListComponent.new(org, cds, [])) + render(Page::CredentialDelegate::ListComponent.new(org, cds, [], [])) + end + + def expired + expired_invites = [ + Invitation.new(invited_given_name: 'Bob', invited_family_name: 'Hodges', invited_email: 'bob@example.com', + id: 2, created_at: 3.days.ago), + Invitation.new(invited_given_name: 'Lisa', invited_family_name: 'Franklin', + invited_email: 'lisa@example.com', id: 3, created_at: 3.days.ago) + ] + render(Page::CredentialDelegate::ListComponent.new(org, [], expired_invites, [])) end private diff --git a/dpc-portal/app/components/page/organization/compound_show_component.html.erb b/dpc-portal/app/components/page/organization/compound_show_component.html.erb index 733a80cc2d..f8622261d0 100644 --- a/dpc-portal/app/components/page/organization/compound_show_component.html.erb +++ b/dpc-portal/app/components/page/organization/compound_show_component.html.erb @@ -2,6 +2,6 @@

<%= @organization.name %>

NPI: <%= @organization.npi %>
<%= render(Core::Navigation::TabbedComponent.new('organization_nav', @links)) if @show_cds %> - <%= render(Page::CredentialDelegate::ListComponent.new(@organization, @active_credential_delegates, @pending_credential_delegates)) if @show_cds %> + <%= render(Page::CredentialDelegate::ListComponent.new(@organization, @pending_credential_delegates, @expired_cd_invitations, @active_credential_delegates,)) if @show_cds %> <%= render(Page::Organization::CredentialsComponent.new(@organization)) %> diff --git a/dpc-portal/app/components/page/organization/compound_show_component.rb b/dpc-portal/app/components/page/organization/compound_show_component.rb index 5b1b2ae5c0..9e44bb1a4d 100644 --- a/dpc-portal/app/components/page/organization/compound_show_component.rb +++ b/dpc-portal/app/components/page/organization/compound_show_component.rb @@ -4,13 +4,14 @@ module Page module Organization # Shows tabbed credential delegates and credentials class CompoundShowComponent < ViewComponent::Base - def initialize(organization, cd_invitations, credential_delegates, show_cds) + def initialize(organization, cd_invitations, expired_cd_invitations, credential_delegates, show_cds) super @links = [['User Access', '#credential_delegates', true], ['Credentials', '#credentials', false]] @organization = organization @active_credential_delegates = credential_delegates @pending_credential_delegates = cd_invitations + @expired_cd_invitations = expired_cd_invitations @show_cds = show_cds end end diff --git a/dpc-portal/app/components/page/organization/compound_show_component_preview.rb b/dpc-portal/app/components/page/organization/compound_show_component_preview.rb index 9949960a8e..81b37a7dc1 100644 --- a/dpc-portal/app/components/page/organization/compound_show_component_preview.rb +++ b/dpc-portal/app/components/page/organization/compound_show_component_preview.rb @@ -6,12 +6,12 @@ module Organization class CompoundShowComponentPreview < ViewComponent::Preview def authorized_official org = ProviderOrganization.new(name: 'Health Hut', npi: '1111111111', id: 2) - render(Page::Organization::CompoundShowComponent.new(org, [], [], true)) + render(Page::Organization::CompoundShowComponent.new(org, [], [], [], true)) end def credential_delegate org = ProviderOrganization.new(name: 'Health Hut', npi: '1111111111', id: 2) - render(Page::Organization::CompoundShowComponent.new(org, [], [], false)) + render(Page::Organization::CompoundShowComponent.new(org, [], [], [], false)) end end end diff --git a/dpc-portal/app/controllers/organizations_controller.rb b/dpc-portal/app/controllers/organizations_controller.rb index 232bac9db0..c42a0cb265 100644 --- a/dpc-portal/app/controllers/organizations_controller.rb +++ b/dpc-portal/app/controllers/organizations_controller.rb @@ -19,12 +19,17 @@ def index def show show_cds = current_user.ao?(@organization) if show_cds - @invitations = Invitation.where(provider_organization: @organization, - invited_by: current_user, - status: :pending) + # Invitation expiration is determined in relation to the `created_at` field; the `status` field will + # never be `'expired'`. Therefore, we need to further filter out expired invitations from this query. + @pending_invitations = Invitation.where(provider_organization: @organization, + invited_by: current_user, + status: :pending).reject(&:expired?) + @expired_invitations = Invitation.where(provider_organization: @organization, + invited_by: current_user).select(&:expired?) @cds = CdOrgLink.where(provider_organization: @organization, disabled_at: nil) end - render(Page::Organization::CompoundShowComponent.new(@organization, @cds, @invitations, show_cds)) + render(Page::Organization::CompoundShowComponent.new(@organization, @pending_invitations, @expired_invitations, + @cds, show_cds)) end def new diff --git a/dpc-portal/app/models/invitation.rb b/dpc-portal/app/models/invitation.rb index 8d7c4b5709..6bf0dfae5f 100644 --- a/dpc-portal/app/models/invitation.rb +++ b/dpc-portal/app/models/invitation.rb @@ -22,6 +22,7 @@ class Invitation < ApplicationRecord def show_attributes { full_name: "#{invited_given_name} #{invited_family_name}", email: invited_email, + expired_at: expired_at.to_s, id: }.with_indifferent_access end @@ -30,7 +31,17 @@ def invited_by_full_name end def expired? - created_at < 2.days.ago + Time.now > expiration_date + end + + def expired_at + return unless expired? + + expiration_date + end + + def expiration_date + created_at + 2.days end def accept! diff --git a/dpc-portal/spec/components/page/credential_delegate/list_component_spec.rb b/dpc-portal/spec/components/page/credential_delegate/list_component_spec.rb index f2fc069eaa..527191dca4 100644 --- a/dpc-portal/spec/components/page/credential_delegate/list_component_spec.rb +++ b/dpc-portal/spec/components/page/credential_delegate/list_component_spec.rb @@ -12,14 +12,15 @@ let(:org) { ComponentSupport::MockOrg.new } - let(:component) { described_class.new(org, invitations, credential_delegates) } + let(:component) { described_class.new(org, pending_invitations, expired_invitations, credential_delegates) } before do render_inline(component) end context 'No credential delegates' do - let(:invitations) { [] } + let(:pending_invitations) { [] } + let(:expired_invitations) { [] } let(:credential_delegates) { [] } let(:expected_html) do @@ -45,6 +46,11 @@

Pending invitations

There are no pending credential delegates.

+
+

Expired invitations

+

These invites expired. You can resend the invite to give them more time to accept.

+

You have no expired invitations.

+
@@ -59,7 +65,8 @@ let(:invitation) do Invitation.new(invited_given_name: 'Bob', invited_family_name: 'Hodges', invited_email: 'bob@example.com') end - let(:invitations) { [] } + let(:pending_invitations) { [] } + let(:expired_invitations) { [] } let(:credential_delegates) { [CdOrgLink.new(user:, invitation:)] } it 'has a table' do @@ -102,10 +109,11 @@ end context 'Pending credential delegate' do - let(:invitations) do + let(:pending_invitations) do [Invitation.new(invited_given_name: 'Bob', invited_family_name: 'Hodges', invited_email: 'bob@example.com', - id: 3)] + id: 3, created_at: 1.day.ago)] end + let(:expired_invitations) { [] } let(:credential_delegates) { [] } it 'has a table' do @@ -148,6 +156,61 @@ expected_html = '

There are no active credential delegates.

' is_expected.to include(normalize_space(expected_html)) end + + it 'has no expired invitations' do + expected_html = '

You have no expired invitations.

' + is_expected.to include(normalize_space(expected_html)) + end + end + + context 'Expired invitations' do + let(:pending_invitations) { [] } + let(:expired_invitations) do + [Invitation.new(invited_given_name: 'Bob', invited_family_name: 'Hodges', invited_email: 'bob@example.com', + id: 3, created_at: 3.days.ago)] + end + let(:credential_delegates) { [] } + + it 'has a table' do + expired_at = expired_invitations.first.expired_at + expected_html = <<~HTML + + + + + + + + + + + + + + + + +
NameEmailExpired on
Bob Hodgesbob@example.com#{expired_at}
+ HTML + is_expected.to include(normalize_space(expected_html)) + end + + it 'has a row' do + expired_at = expired_invitations.first.expired_at + expected_html = <<~HTML + + Bob Hodges + bob@example.com + #{expired_at} + + HTML + is_expected.to include(normalize_space(expected_html)) + end + + it 'has no pending credential delegates' do + expected_html = '

There are no pending credential delegates.

' + is_expected.to include(normalize_space(expected_html)) + end end end end diff --git a/dpc-portal/spec/components/page/organization/compound_show_component_spec.rb b/dpc-portal/spec/components/page/organization/compound_show_component_spec.rb index 7c4b0ff32d..a52f20ca67 100644 --- a/dpc-portal/spec/components/page/organization/compound_show_component_spec.rb +++ b/dpc-portal/spec/components/page/organization/compound_show_component_spec.rb @@ -10,7 +10,7 @@ normalize_space(rendered_content) end let(:org) { build(:provider_organization, name: 'Health Hut', npi: '11111111', id: 2) } - let(:component) { described_class.new(org, [], [], show_cds) } + let(:component) { described_class.new(org, [], [], [], show_cds) } before do render_inline(component) diff --git a/dpc-portal/spec/requests/organizations_spec.rb b/dpc-portal/spec/requests/organizations_spec.rb index 65c7cd22c9..e487341ffd 100644 --- a/dpc-portal/spec/requests/organizations_spec.rb +++ b/dpc-portal/spec/requests/organizations_spec.rb @@ -220,7 +220,7 @@ it 'does not assign invitations even if exist' do create(:invitation, :cd, provider_organization: org, invited_by: user) get "/organizations/#{org.id}" - expect(assigns(:invitations)).to be_nil + expect(assigns(:pending_invitations)).to be_nil end end end @@ -260,24 +260,50 @@ expect(response.body).to include('

Credential delegates

') expect(response.body).to include('

Pending invitations

') expect(response.body).to include('

Active

') + expect(response.body).to include('

Expired invitations

') end - context :invitations do + context :pending_invitations do it 'assigns if exist' do create(:invitation, :cd, provider_organization: org, invited_by: user) get "/organizations/#{org.id}" - expect(assigns(:invitations).size).to eq 1 + expect(assigns(:pending_invitations).size).to eq 1 end it 'does not assign if not exist' do get "/organizations/#{org.id}" - expect(assigns(:invitations).size).to eq 0 + expect(assigns(:pending_invitations).size).to eq 0 end it 'does not assign if only accepted exists' do create(:invitation, :cd, provider_organization: org, invited_by: user, status: :accepted) get "/organizations/#{org.id}" - expect(assigns(:invitations).size).to eq 0 + expect(assigns(:pending_invitations).size).to eq 0 + end + + it 'does not assign if expired' do + create(:invitation, :cd, provider_organization: org, invited_by: user, created_at: 3.days.ago) + get "/organizations/#{org.id}" + expect(assigns(:pending_invitations).size).to eq 0 + end + end + + context :expired_invitations do + it 'assigns if exist' do + create(:invitation, :cd, provider_organization: org, invited_by: user, created_at: 3.days.ago) + get "/organizations/#{org.id}" + expect(assigns(:expired_invitations).size).to eq 1 + end + + it 'does not assign if not exist' do + get "/organizations/#{org.id}" + expect(assigns(:pending_invitations).size).to eq 0 + end + + it 'does not assign if invitation is not expired' do + create(:invitation, :cd, provider_organization: org, invited_by: user) + get "/organizations/#{org.id}" + expect(assigns(:expired_invitations).size).to eq 0 end end