diff --git a/Gemfile b/Gemfile index 303014731..e274a06c1 100644 --- a/Gemfile +++ b/Gemfile @@ -35,6 +35,7 @@ gem "json-schema" gem "devise" gem "turbo-rails", "~> 0.8" gem "uk_postcode" +gem "view_component" group :development, :test do # Call 'byebug' anywhere in the code to stop execution and get a debugger console diff --git a/Gemfile.lock b/Gemfile.lock index b64e1380e..b986e007b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -438,6 +438,7 @@ DEPENDENCIES turbo-rails (~> 0.8) tzinfo-data uk_postcode + view_component web-console (>= 4.1.0) webpacker (~> 5.0) diff --git a/app/components/tab_navigation_component.html.erb b/app/components/tab_navigation_component.html.erb new file mode 100644 index 000000000..874caaf39 --- /dev/null +++ b/app/components/tab_navigation_component.html.erb @@ -0,0 +1,15 @@ + diff --git a/app/components/tab_navigation_component.rb b/app/components/tab_navigation_component.rb new file mode 100644 index 000000000..06c879389 --- /dev/null +++ b/app/components/tab_navigation_component.rb @@ -0,0 +1,14 @@ +class TabNavigationComponent < ViewComponent::Base + attr_reader :items + + def initialize(items:) + @items = items + super + end + + def strip_query(url) + url = Addressable::URI.parse(url) + url.query_values = nil + url.to_s + end +end diff --git a/app/controllers/organisations_controller.rb b/app/controllers/organisations_controller.rb new file mode 100644 index 000000000..87b075e15 --- /dev/null +++ b/app/controllers/organisations_controller.rb @@ -0,0 +1,14 @@ +class OrganisationsController < ApplicationController + before_action :authenticate_user! + before_action :find_organisation + + def users + render "users" + end + +private + + def find_organisation + @organisation = Organisation.find(params[:id]) + end +end diff --git a/app/helpers/user_table_helper.rb b/app/helpers/user_table_helper.rb new file mode 100644 index 000000000..867108556 --- /dev/null +++ b/app/helpers/user_table_helper.rb @@ -0,0 +1,12 @@ +module UserTableHelper + include GovukLinkHelper + + def user_cell(user) + [govuk_link_to(user.name, user), user.email].join("\n") + end + + def org_cell(user) + role = "#{user.role}" + [user.organisation.name, role].join("\n") + end +end diff --git a/app/javascript/styles/_tab-navigation.scss b/app/javascript/styles/_tab-navigation.scss new file mode 100644 index 000000000..0b5ebb6db --- /dev/null +++ b/app/javascript/styles/_tab-navigation.scss @@ -0,0 +1,76 @@ +.app-tab-navigation { + @include govuk-font(19, $weight: bold); + @include govuk-responsive-margin(6, "bottom"); +} + +.app-tab-navigation__list { + @include govuk-clearfix; + left: govuk-spacing(-3); + list-style: none; + margin: 0; + padding: 0; + position: relative; + right: govuk-spacing(-3); + width: calc(100% + #{govuk-spacing(6)}); + + @include govuk-media-query($from: tablet) { + box-shadow: inset 0 -1px 0 $govuk-border-colour; + } +} + +.app-tab-navigation__item { + box-sizing: border-box; + display: block; + line-height: 40px; + height: 40px; + padding: 0 govuk-spacing(3); + + @include govuk-media-query($from: tablet) { + box-shadow: none; + display: block; + float: left; + line-height: 50px; + height: 50px; + padding: 0 govuk-spacing(3); + position: relative; + } +} + +.app-tab-navigation__item--current { + @include govuk-media-query($until: tablet) { + border-left: 4px solid $govuk-link-colour; + padding-left: 11px; + } + + @include govuk-media-query($from: tablet) { + border-bottom: 4px solid $govuk-link-colour; + padding-left: govuk-spacing(3); + } +} + +.app-tab-navigation__link { + @include govuk-link-common; + @include govuk-link-style-no-visited-state; + @include govuk-link-style-no-underline; + @include govuk-typography-weight-bold; + + &:not(:focus):hover { + color: $govuk-link-colour; + } + + // Extend the touch area of the link to the list + &:after { + bottom: 0; + content: ""; + left: 0; + position: absolute; + right: 0; + top: 0; + } +} + +.app-tab-navigation__item--current .app-tab-navigation__link { + &:hover { + text-decoration: none; + } +} diff --git a/app/javascript/styles/application.scss b/app/javascript/styles/application.scss index 7e1158bd7..28f46740f 100644 --- a/app/javascript/styles/application.scss +++ b/app/javascript/styles/application.scss @@ -12,6 +12,7 @@ $govuk-image-url-function: frontend-image-url; @import "~govuk-frontend/govuk/all"; @import '_task-list'; +@import '_tab-navigation'; $govuk-global-styles: true; @@ -19,3 +20,8 @@ $govuk-global-styles: true; // display: block; // border: 1px solid blue // } + +//overrides +.app-\!-colour-muted { + color: $govuk-secondary-text-colour !important; +} diff --git a/app/models/organisation.rb b/app/models/organisation.rb index fb6dab181..392f6366f 100644 --- a/app/models/organisation.rb +++ b/app/models/organisation.rb @@ -14,4 +14,21 @@ class Organisation < ApplicationRecord def not_completed_case_logs case_logs.not_completed end + + def address_string + %i[address_line1 address_line2 postcode].map { |field| public_send(field) }.join("\n") + end + + def display_attributes + { + name: name, + address: address_string, + telephone_number: phone, + type: org_type, + local_authorities_operated_in: local_authorities, + holds_own_stock: holds_own_stock, + other_stock_owners: other_stock_owners, + managing_agents: managing_agents, + } + end end diff --git a/app/models/user.rb b/app/models/user.rb index 3640e1829..81d3d2d9c 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -1,7 +1,8 @@ class User < ApplicationRecord # Include default devise modules. Others available are: - # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable - devise :database_authenticatable, :recoverable, :rememberable, :validatable + # :confirmable, :lockable, :timeoutable and :omniauthable + devise :database_authenticatable, :recoverable, :rememberable, :validatable, + :trackable belongs_to :organisation has_many :owned_case_logs, through: :organisation diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index 7cfc25059..8eda1a8bb 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -39,6 +39,7 @@ if current_user.nil? component.navigation_item(text: 'Case logs', href: '/case_logs') elsif + component.navigation_item(text: 'Your organisation', href: "/organisations/#{current_user.organisation.id}") component.navigation_item(text: 'Your account', href: '/users/account') component.navigation_item(text: 'Sign out', href: destroy_user_session_path, options: {:method => :delete}) end @@ -65,7 +66,7 @@ end %> <% end %> - <%= yield %> + <%= content_for?(:content) ? yield(:content) : yield %> diff --git a/app/views/layouts/organisations.html.erb b/app/views/layouts/organisations.html.erb new file mode 100644 index 000000000..ab71e2549 --- /dev/null +++ b/app/views/layouts/organisations.html.erb @@ -0,0 +1,23 @@ +<% content_for :before_content do %> + <%= govuk_back_link( + text: 'Back', + href: :back, + ) %> +<% end %> + +<% content_for :content do %> +

+ Your organisation +

+ + <%= render TabNavigationComponent.new(items: [ + { name: t('Details'), url: details_organisation_path(@organisation) }, + { name: t('Users'), url: users_organisation_path(@organisation) }, + ]) %> + +

<%= content_for(:tab_title) %>

+ + <%= content_for?(:organisations_content) ? yield(:organisations_content) : yield %> +<% end %> + +<%= render template: "layouts/application" %> diff --git a/app/views/organisations/show.html.erb b/app/views/organisations/show.html.erb new file mode 100644 index 000000000..49aa63e67 --- /dev/null +++ b/app/views/organisations/show.html.erb @@ -0,0 +1,16 @@ +<% content_for :tab_title do %> + <%= "Details" %> +<% end %> + +
+
+ <%= govuk_summary_list do |summary_list| %> + <% @organisation.display_attributes.each do |attr, val| %> + <%= summary_list.row do |row| + row.key { attr.to_s.humanize } + row.value { simple_format(val.to_s, {}, wrapper_tag: "div") } + end %> + <% end %> + <% end %> +
+
diff --git a/app/views/organisations/users.html.erb b/app/views/organisations/users.html.erb new file mode 100644 index 000000000..82266b2af --- /dev/null +++ b/app/views/organisations/users.html.erb @@ -0,0 +1,23 @@ +<% content_for :tab_title do %> + <%= "Users" %> +<% end %> + +<%= govuk_button_link_to "Invite user", new_user_path, method: :post %> +<%= govuk_table do |table| %> + <%= table.head do |head| %> + <%= head.row do |row| + row.cell(header: true, text: "Name and email adress") + row.cell(header: true, text: "Organisation and role") + row.cell(header: true, text: "Last logged in") + end %> + <% end %> + <% @organisation.users.each do |user| %> + <%= table.body do |body| %> + <%= body.row do |row| + row.cell(text: simple_format(user_cell(user), {}, wrapper_tag: "div")) + row.cell(text: simple_format(org_cell(user), {}, wrapper_tag: "div")) + row.cell(text: user.last_sign_in_at&.strftime("%d %b %Y") ) + end %> + <% end %> + <% end %> +<% end %> diff --git a/config/routes.rb b/config/routes.rb index 6c4fd7946..f30e8868c 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -13,11 +13,23 @@ Rails.application.routes.draw do root to: "test#index" get "about", to: "about#index" get "/users/account", to: "users/account#index" - get "/users/account/personal_details", to: "users/account#personal_details" form_handler = FormHandler.instance form = form_handler.get_form("2021_2022") + resources :users do + collection do + get "account/personal_details", to: "users/account#personal_details" + end + end + + resources :organisations do + member do + get "details", to: "organisations#show" + get "users", to: "organisations#users" + end + end + resources :case_logs do collection do post "/bulk_upload", to: "bulk_upload#bulk_upload" diff --git a/db/migrate/20211130144840_add_user_last_logged_in.rb b/db/migrate/20211130144840_add_user_last_logged_in.rb new file mode 100644 index 000000000..01cebcef9 --- /dev/null +++ b/db/migrate/20211130144840_add_user_last_logged_in.rb @@ -0,0 +1,12 @@ +class AddUserLastLoggedIn < ActiveRecord::Migration[6.1] + def change + change_table :users, bulk: true do |t| + ## Trackable + t.integer :sign_in_count, default: 0, null: false + t.datetime :current_sign_in_at + t.datetime :last_sign_in_at + t.string :current_sign_in_ip + t.string :last_sign_in_ip + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 393589bb2..d31322f7c 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2021_11_30_090246) do +ActiveRecord::Schema.define(version: 2021_11_30_144840) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -200,6 +200,11 @@ ActiveRecord::Schema.define(version: 2021_11_30_090246) do t.string "name" t.string "role" t.bigint "organisation_id" + t.integer "sign_in_count", default: 0, null: false + t.datetime "current_sign_in_at" + t.datetime "last_sign_in_at" + t.string "current_sign_in_ip" + t.string "last_sign_in_ip" t.index ["email"], name: "index_users_on_email", unique: true t.index ["organisation_id"], name: "index_users_on_organisation_id" t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true diff --git a/db/seeds.rb b/db/seeds.rb index 99d537266..6ff200682 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -6,6 +6,15 @@ # movies = Movie.create([{ name: 'Star Wars' }, { name: 'Lord of the Rings' }]) # Character.create(name: 'Luke', movie: movies.first) -org = Organisation.create!(name: "DLUHC", address_line1: "2 Marsham Street", address_line2: "London", postcode: "SW1P 4DF") +org = Organisation.create!( + name: "DLUHC", + address_line1: "2 Marsham Street", + address_line2: "London", + postcode: "SW1P 4DF", + local_authorities: "None", + holds_own_stock: false, + other_stock_owners: "None", + managing_agents: "None", +) User.create!(email: "test@example.com", password: "password", organisation: org) AdminUser.create!(email: "admin@example.com", password: "password") diff --git a/package.json b/package.json index c3ede97ff..e20af9cbb 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "@rails/webpacker": "5.4.0", "chart.js": "^3.6.0", "chartkick": "^4.1.0", - "govuk-frontend": "^3.13.0", + "govuk-frontend": "^3.14.0", "stimulus": "^3.0.0", "webpack": "^4.46.0", "webpack-cli": "^3.3.12" diff --git a/spec/components/tab_navigation_component_spec.rb b/spec/components/tab_navigation_component_spec.rb new file mode 100644 index 000000000..f2436650f --- /dev/null +++ b/spec/components/tab_navigation_component_spec.rb @@ -0,0 +1,27 @@ +require "rails_helper" + +RSpec.describe TabNavigationComponent, type: :component do + let(:items) do + [{ name: "Application", url: "#", current: true }, + { name: "Notes", url: "#" }, + { name: "Timeline", url: "#" }] + end + + context "nav tabs appearing as selected" do + it "when the item is 'current' then that tab is selected" do + result = render_inline(described_class.new(items: items)) + + expect(result.css('.app-tab-navigation__link[aria-current="page"]').text).to include("Application") + end + end + + context "rendering tabs" do + it "renders all of the nav tabs specified in the items hash passed to it" do + result = render_inline(described_class.new(items: items)) + + expect(result.text).to include("Application") + expect(result.text).to include("Notes") + expect(result.text).to include("Timeline") + end + end +end diff --git a/spec/factories/user.rb b/spec/factories/user.rb index 6509c2457..36789715a 100644 --- a/spec/factories/user.rb +++ b/spec/factories/user.rb @@ -1,8 +1,10 @@ FactoryBot.define do factory :user do sequence(:email) { |i| "test#{i}@example.com" } + name { "Danny Rojas" } password { "pAssword1" } organisation + role { "Data Provider" } created_at { Time.zone.now } updated_at { Time.zone.now } end diff --git a/spec/features/organisation_spec.rb b/spec/features/organisation_spec.rb new file mode 100644 index 000000000..2ad4358d5 --- /dev/null +++ b/spec/features/organisation_spec.rb @@ -0,0 +1,29 @@ +require "rails_helper" +require_relative "form/helpers" + +RSpec.describe "User Features" do + include Helpers + let!(:user) { FactoryBot.create(:user) } + let(:organisation) { user.organisation } + let(:org_id) { organisation.id } + + before do + sign_in user + end + + context "Organisation page" do + it "default to organisation details" do + visit("/case_logs") + click_link("Your organisation") + expect(page).to have_content(user.organisation.name) + end + + it "can switch tabs" do + visit("/organisations/#{org_id}") + click_link("Users") + expect(page).to have_current_path("/organisations/#{org_id}/users") + click_link("Details") + expect(page).to have_current_path("/organisations/#{org_id}/details") + end + end +end diff --git a/spec/helpers/user_table_helper_spec.rb b/spec/helpers/user_table_helper_spec.rb new file mode 100644 index 000000000..6d8921a0f --- /dev/null +++ b/spec/helpers/user_table_helper_spec.rb @@ -0,0 +1,19 @@ +require "rails_helper" + +RSpec.describe UserTableHelper do + let(:user) { FactoryBot.build(:user) } + + describe "#user_cell" do + it "returns user link and email separated by a newline character" do + expected_html = "Danny Rojas\n#{user.email}" + expect(user_cell(user)).to match(expected_html) + end + end + + describe "#org_cell" do + it "returns the users org name and role separated by a newline character" do + expected_html = "DLUHC\nData Provider" + expect(org_cell(user)).to match(expected_html) + end + end +end diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index c9f3b729e..5787ffe66 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -7,6 +7,7 @@ abort("The Rails environment is running in production mode!") if Rails.env.produ require "rspec/rails" require "capybara/rspec" require "selenium-webdriver" +require "view_component/test_helpers" Capybara.register_driver :headless do |app| options = Selenium::WebDriver::Firefox::Options.new @@ -80,4 +81,6 @@ RSpec.configure do |config| config.include Devise::Test::ControllerHelpers, type: :controller config.include Devise::Test::IntegrationHelpers, type: :request + config.include ViewComponent::TestHelpers, type: :component + config.include Capybara::RSpecMatchers, type: :component end diff --git a/spec/requests/organisations_controller_spec.rb b/spec/requests/organisations_controller_spec.rb new file mode 100644 index 000000000..3239dafbf --- /dev/null +++ b/spec/requests/organisations_controller_spec.rb @@ -0,0 +1,59 @@ +require "rails_helper" + +RSpec.describe OrganisationsController, type: :request do + let(:user) { FactoryBot.create(:user) } + let(:organisation) { user.organisation } + let(:headers) { { "Accept" => "text/html" } } + + context "details tab" do + before do + sign_in user + get "/organisations/#{organisation.id}", headers: headers, params: {} + end + + it "shows the tab navigation" do + expected_html = "