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 = "