add Nairobi project fields to budget investments
Add objectives, actual cost, start and end dates, source of funds and sector/sub-sector to investments. Sectors are a catalog managed from the admin panel, and the sub-sector select only shows the sub-sectors of the selected sector. The investment page shows these details along with the current milestone status and implementation percentage. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -11,6 +11,7 @@ var initialize_modules = function() {
|
||||
// Add calls to your custom code here; this will be called when
|
||||
// loading a page.
|
||||
App.BudgetSets.initialize();
|
||||
App.Sectors.initialize();
|
||||
};
|
||||
|
||||
var destroy_non_idempotent_modules = function() {
|
||||
|
||||
31
app/assets/javascripts/custom/sectors.js
Normal file
31
app/assets/javascripts/custom/sectors.js
Normal file
@@ -0,0 +1,31 @@
|
||||
(function() {
|
||||
"use strict";
|
||||
App.Sectors = {
|
||||
// Keeps only the sub-sectors of the selected sector in the sub-sector select
|
||||
filterSubSectors: function(sectorSelect) {
|
||||
var subSectorSelect = $(sectorSelect.data("sub-sector-select"));
|
||||
var allOptions = subSectorSelect.data("all-options");
|
||||
var sectorId = String(sectorSelect.val());
|
||||
var selected = subSectorSelect.val();
|
||||
|
||||
if (!allOptions) {
|
||||
allOptions = subSectorSelect.find("option[data-sector-id]").clone();
|
||||
subSectorSelect.data("all-options", allOptions);
|
||||
}
|
||||
|
||||
subSectorSelect.find("optgroup, option[data-sector-id]").remove();
|
||||
allOptions.filter(function() {
|
||||
return String($(this).data("sector-id")) === sectorId;
|
||||
}).clone().appendTo(subSectorSelect);
|
||||
|
||||
subSectorSelect.val(subSectorSelect.find("option[value='" + selected + "']").length ? selected : "");
|
||||
},
|
||||
initialize: function() {
|
||||
$(".js-sector-select").each(function() {
|
||||
App.Sectors.filterSubSectors($(this));
|
||||
}).off("change.sectors").on("change.sectors", function() {
|
||||
App.Sectors.filterSubSectors($(this));
|
||||
});
|
||||
}
|
||||
};
|
||||
}).call(this);
|
||||
46
app/assets/stylesheets/custom/sectors.scss
Normal file
46
app/assets/stylesheets/custom/sectors.scss
Normal file
@@ -0,0 +1,46 @@
|
||||
.admin-sidebar > ul > li > .sectors-link {
|
||||
@include has-fa-icon(tags, solid);
|
||||
|
||||
&::before {
|
||||
@extend %admin-menu-icon;
|
||||
}
|
||||
}
|
||||
|
||||
.admin .sectors-table .sub-sector td:first-child {
|
||||
color: $text-medium;
|
||||
}
|
||||
|
||||
.admin .project-details-fields {
|
||||
margin-top: $line-height;
|
||||
}
|
||||
|
||||
.investment-project-details {
|
||||
margin: $line-height 0;
|
||||
|
||||
dl {
|
||||
display: grid;
|
||||
gap: calc($line-height / 2) $line-height;
|
||||
grid-template-columns: 1fr;
|
||||
|
||||
@include breakpoint(medium) {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
.project-detail {
|
||||
border-#{$global-left}: 3px solid $border;
|
||||
padding-#{$global-left}: calc($line-height / 2);
|
||||
}
|
||||
|
||||
dt {
|
||||
color: $text-medium;
|
||||
font-size: $small-font-size;
|
||||
font-weight: normal;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
dd {
|
||||
font-weight: bold;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
1
app/components/custom/admin/menu_component.html.erb
Normal file
1
app/components/custom/admin/menu_component.html.erb
Normal file
@@ -0,0 +1 @@
|
||||
<%= link_list(*links, id: "admin_menu") %>
|
||||
25
app/components/custom/admin/menu_component.rb
Normal file
25
app/components/custom/admin/menu_component.rb
Normal file
@@ -0,0 +1,25 @@
|
||||
class Admin::MenuComponent < ApplicationComponent; end
|
||||
|
||||
load Rails.root.join("app", "components", "admin", "menu_component.rb")
|
||||
|
||||
class Admin::MenuComponent
|
||||
private
|
||||
|
||||
alias_method :consul_default_links, :default_links
|
||||
|
||||
def default_links
|
||||
links = consul_default_links
|
||||
budgets_index = links.index { |link| link.is_a?(Array) && link.last.try(:[], :class) == "budgets-link" }
|
||||
|
||||
budgets_index ? links.insert(budgets_index + 1, sectors_link) : links
|
||||
end
|
||||
|
||||
def sectors_link
|
||||
[
|
||||
t("admin.menu.sectors"),
|
||||
admin_sectors_path,
|
||||
controller_name == "sectors",
|
||||
class: "sectors-link"
|
||||
]
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,12 @@
|
||||
<section class="investment-project-details" aria-labelledby="project_details_header">
|
||||
<h2 id="project_details_header"><%= t("budgets.investments.project_details.title") %></h2>
|
||||
|
||||
<dl>
|
||||
<% details.each do |field, value| %>
|
||||
<div class="project-detail project-detail-<%= field.to_s.dasherize %>">
|
||||
<dt><%= t("budgets.investments.project_details.#{field}") %></dt>
|
||||
<dd><%= value %></dd>
|
||||
</div>
|
||||
<% end %>
|
||||
</dl>
|
||||
</section>
|
||||
@@ -0,0 +1,52 @@
|
||||
class Budgets::Investments::ProjectDetailsComponent < ApplicationComponent
|
||||
attr_reader :investment
|
||||
|
||||
def initialize(investment)
|
||||
@investment = investment
|
||||
end
|
||||
|
||||
def render?
|
||||
details.any?
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def details
|
||||
@details ||= {
|
||||
sector: sector,
|
||||
status: status&.name,
|
||||
implementation: implementation,
|
||||
price: (money(investment.price) if show_costs?),
|
||||
actual_cost: (money(investment.actual_cost) if show_costs? && investment.actual_cost.present?),
|
||||
start_date: month(investment.start_date),
|
||||
end_date: month(investment.end_date),
|
||||
funding_source: investment.funding_source.presence
|
||||
}.compact
|
||||
end
|
||||
|
||||
def sector
|
||||
[investment.sector&.name, investment.sub_sector&.name].compact.join(" › ").presence
|
||||
end
|
||||
|
||||
def status
|
||||
investment.milestones.published.with_status.order_by_publication_date.last&.status
|
||||
end
|
||||
|
||||
def implementation
|
||||
percentage = investment.primary_progress_bar&.percentage
|
||||
"#{percentage}%" if percentage
|
||||
end
|
||||
|
||||
# Same rule Consul uses to show the price of an investment
|
||||
def show_costs?
|
||||
investment.should_show_price?
|
||||
end
|
||||
|
||||
def money(amount)
|
||||
investment.budget.formatted_amount(amount)
|
||||
end
|
||||
|
||||
def month(date)
|
||||
l(date, format: :month_year) if date
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,13 @@
|
||||
load Rails.root.join("app", "controllers", "admin", "budget_investments_controller.rb")
|
||||
|
||||
class Admin::BudgetInvestmentsController
|
||||
private
|
||||
|
||||
alias_method :consul_allowed_params, :allowed_params
|
||||
|
||||
def allowed_params
|
||||
project_attributes = [:sector_id, :sub_sector_id, :funding_source, :actual_cost, :start_date, :end_date]
|
||||
|
||||
[*project_attributes, *consul_allowed_params]
|
||||
end
|
||||
end
|
||||
54
app/controllers/custom/admin/sectors_controller.rb
Normal file
54
app/controllers/custom/admin/sectors_controller.rb
Normal file
@@ -0,0 +1,54 @@
|
||||
class Admin::SectorsController < Admin::BaseController
|
||||
include FeatureFlags
|
||||
|
||||
feature_flag :budgets
|
||||
|
||||
before_action :load_sector, only: [:edit, :update, :destroy]
|
||||
|
||||
def index
|
||||
@sectors = Sector.main.includes(:sub_sectors)
|
||||
end
|
||||
|
||||
def new
|
||||
@sector = Sector.new(parent_id: params[:parent_id])
|
||||
end
|
||||
|
||||
def create
|
||||
@sector = Sector.new(sector_params)
|
||||
|
||||
if @sector.save
|
||||
redirect_to admin_sectors_path, notice: t("admin.sectors.create.notice")
|
||||
else
|
||||
render :new
|
||||
end
|
||||
end
|
||||
|
||||
def edit
|
||||
end
|
||||
|
||||
def update
|
||||
if @sector.update(sector_params)
|
||||
redirect_to admin_sectors_path, notice: t("admin.sectors.update.notice")
|
||||
else
|
||||
render :edit
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
if @sector.destroy
|
||||
redirect_to admin_sectors_path, notice: t("admin.sectors.destroy.notice")
|
||||
else
|
||||
redirect_to admin_sectors_path, alert: t("admin.sectors.destroy.unable_notice")
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def load_sector
|
||||
@sector = Sector.find(params[:id])
|
||||
end
|
||||
|
||||
def sector_params
|
||||
params.require(:sector).permit(:name, :parent_id)
|
||||
end
|
||||
end
|
||||
19
app/helpers/sectors_helper.rb
Normal file
19
app/helpers/sectors_helper.rb
Normal file
@@ -0,0 +1,19 @@
|
||||
module SectorsHelper
|
||||
def sector_select_options
|
||||
Sector.main.map { |sector| [sector.name, sector.id] }
|
||||
end
|
||||
|
||||
# Sub-sectors grouped by sector; each option knows its sector so the list
|
||||
# can be filtered when a sector is selected
|
||||
def sub_sector_select_options(selected = nil)
|
||||
grouped_options = Sector.main.includes(:sub_sectors).map do |sector|
|
||||
options = sector.sub_sectors.map do |sub_sector|
|
||||
[sub_sector.name, sub_sector.id, { data: { sector_id: sector.id }}]
|
||||
end
|
||||
|
||||
[sector.name, options]
|
||||
end
|
||||
|
||||
grouped_options_for_select(grouped_options, selected)
|
||||
end
|
||||
end
|
||||
@@ -2,6 +2,16 @@ load Rails.root.join("app", "models", "budget", "investment.rb")
|
||||
|
||||
class Budget
|
||||
class Investment
|
||||
translates :objectives, touch: true
|
||||
|
||||
belongs_to :sector, optional: true
|
||||
belongs_to :sub_sector, class_name: "Sector", optional: true
|
||||
|
||||
validates :actual_cost, numericality: { greater_than_or_equal_to: 0, allow_nil: true }
|
||||
validate :sector_is_a_main_sector
|
||||
validate :sub_sector_belongs_to_sector
|
||||
validate :end_date_after_start_date
|
||||
|
||||
def self.apply_filters_and_search(_budget, params, current_filter = nil)
|
||||
investments = all
|
||||
investments = investments.send(current_filter) if current_filter.present?
|
||||
@@ -11,5 +21,21 @@ class Budget
|
||||
investments = investments.filter_by(params[:advanced_search])
|
||||
investments
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def sector_is_a_main_sector
|
||||
errors.add(:sector_id, :invalid) if sector && !sector.main?
|
||||
end
|
||||
|
||||
def sub_sector_belongs_to_sector
|
||||
errors.add(:sub_sector_id, :invalid) if sub_sector && sub_sector.parent_id != sector_id
|
||||
end
|
||||
|
||||
def end_date_after_start_date
|
||||
if start_date && end_date && end_date < start_date
|
||||
errors.add(:end_date, :invalid)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
6
app/models/custom/milestone.rb
Normal file
6
app/models/custom/milestone.rb
Normal file
@@ -0,0 +1,6 @@
|
||||
load Rails.root.join("app", "models", "milestone.rb")
|
||||
|
||||
class Milestone
|
||||
# The milestone status is shown in cached views of the milestoneable
|
||||
after_commit -> { milestoneable&.touch unless milestoneable&.destroyed? }
|
||||
end
|
||||
6
app/models/custom/progress_bar.rb
Normal file
6
app/models/custom/progress_bar.rb
Normal file
@@ -0,0 +1,6 @@
|
||||
load Rails.root.join("app", "models", "progress_bar.rb")
|
||||
|
||||
class ProgressBar
|
||||
# The primary progress bar is shown in cached views of the progressable
|
||||
after_commit -> { progressable&.touch unless progressable&.destroyed? }
|
||||
end
|
||||
27
app/models/custom/sector.rb
Normal file
27
app/models/custom/sector.rb
Normal file
@@ -0,0 +1,27 @@
|
||||
class Sector < ApplicationRecord
|
||||
belongs_to :parent, class_name: "Sector", optional: true, inverse_of: :sub_sectors
|
||||
has_many :sub_sectors, -> { order(:name) }, class_name: "Sector", foreign_key: :parent_id,
|
||||
inverse_of: :parent, dependent: :restrict_with_error
|
||||
has_many :investments, class_name: "Budget::Investment", dependent: :restrict_with_error
|
||||
has_many :sub_sector_investments, class_name: "Budget::Investment", foreign_key: :sub_sector_id,
|
||||
inverse_of: :sub_sector, dependent: :restrict_with_error
|
||||
|
||||
validates :name, presence: true, uniqueness: { scope: :parent_id, case_sensitive: false }
|
||||
validate :parent_is_a_main_sector
|
||||
|
||||
scope :main, -> { where(parent_id: nil).order(:name) }
|
||||
|
||||
def main?
|
||||
parent_id.nil?
|
||||
end
|
||||
|
||||
def self.find_or_create_by_name!(name, parent: nil)
|
||||
where(parent: parent).where("LOWER(name) = ?", name.downcase).first || create!(name: name, parent: parent)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def parent_is_a_main_sector
|
||||
errors.add(:parent_id, :invalid) if parent && !parent.main?
|
||||
end
|
||||
end
|
||||
161
app/views/custom/admin/budget_investments/edit.html.erb
Normal file
161
app/views/custom/admin/budget_investments/edit.html.erb
Normal file
@@ -0,0 +1,161 @@
|
||||
<%= link_to admin_budget_budget_investment_path(@budget, @investment, Budget::Investment.filter_params(params).to_h), class: "back" do %>
|
||||
<span class="icon-angle-left"></span> <%= t("shared.back") %>
|
||||
<% end %>
|
||||
|
||||
<%= render "shared/globalize_locales", resource: @investment %>
|
||||
|
||||
<div class="row">
|
||||
<%= translatable_form_for @investment,
|
||||
url: admin_budget_budget_investment_path(@budget, @investment) do |f| %>
|
||||
|
||||
<%= render "shared/errors", resource: @investment %>
|
||||
|
||||
<% Budget::Investment.filter_params(params).to_h.each do |filter_name, filter_value| %>
|
||||
<%= hidden_field_tag filter_name, filter_value %>
|
||||
<% end %>
|
||||
|
||||
<div class="row expanded">
|
||||
<%= f.translatable_fields do |translations_form| %>
|
||||
<div class="small-12 column">
|
||||
<%= translations_form.text_field :title,
|
||||
maxlength: Budget::Investment.title_max_length %>
|
||||
</div>
|
||||
|
||||
<div class="small-12 column">
|
||||
<%= translations_form.text_area :description,
|
||||
maxlength: Budget::Investment.description_max_length,
|
||||
class: "html-area" %>
|
||||
</div>
|
||||
|
||||
<div class="small-12 column">
|
||||
<%= translations_form.text_area :objectives, rows: 3 %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<div class="small-12 column">
|
||||
<%= f.text_field :tag_list,
|
||||
value: @investment.tag_list.sort.join(","),
|
||||
label: t("admin.budget_investments.edit.user_tags") %>
|
||||
</div>
|
||||
|
||||
<div class="small-12 column">
|
||||
<div class="small-12 medium-6">
|
||||
<%= f.text_field :external_url %>
|
||||
</div>
|
||||
|
||||
<div class="small-12 medium-6">
|
||||
<%= f.select :heading_id, budget_heading_select_options(@budget), include_blank: t("admin.budget_investments.edit.select_heading") %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row expanded project-details-fields">
|
||||
<div class="small-12 column">
|
||||
<h2 id="project_details"><%= t("admin.budget_investments.edit.project_details") %></h2>
|
||||
<p class="help-text">
|
||||
<%= link_to t("admin.budget_investments.edit.manage_sectors"), admin_sectors_path %>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="small-12 medium-6 column">
|
||||
<%= f.select :sector_id,
|
||||
sector_select_options,
|
||||
{ include_blank: t("admin.budget_investments.edit.select_sector") },
|
||||
class: "js-sector-select", data: { sub_sector_select: "#budget_investment_sub_sector_id" } %>
|
||||
</div>
|
||||
|
||||
<div class="small-12 medium-6 column">
|
||||
<%= f.select :sub_sector_id,
|
||||
sub_sector_select_options(@investment.sub_sector_id),
|
||||
{ include_blank: t("admin.budget_investments.edit.select_sub_sector") },
|
||||
class: "js-sub-sector-select" %>
|
||||
</div>
|
||||
|
||||
<div class="small-12 medium-6 column">
|
||||
<%= f.text_field :funding_source %>
|
||||
</div>
|
||||
|
||||
<div class="small-12 medium-6 column">
|
||||
<%= f.number_field :actual_cost, min: 0, step: 1 %>
|
||||
</div>
|
||||
|
||||
<div class="small-12 medium-6 column">
|
||||
<%= f.date_field :start_date %>
|
||||
</div>
|
||||
|
||||
<div class="small-12 medium-6 column">
|
||||
<%= f.date_field :end_date %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row expanded margin-top">
|
||||
|
||||
<div class="small-12 column">
|
||||
<h2 id="classification"><%= t("admin.budget_investments.edit.classification") %></h2>
|
||||
|
||||
<div class="small-12 medium-6">
|
||||
<%= f.select(:administrator_id,
|
||||
@admins.map { |a| [a.description_or_name_and_email, a.id] },
|
||||
{ include_blank: t("admin.budget_investments.edit.undefined") }) %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="small-12 column">
|
||||
<%= f.label :valuation_tag_list %>
|
||||
<div class="tags">
|
||||
<% @tags.each do |tag| %>
|
||||
<a class="js-add-tag-link"><%= tag.name %></a>
|
||||
<% end %>
|
||||
</div>
|
||||
<%= f.text_field :valuation_tag_list,
|
||||
value: @investment.valuation_tag_list.sort.join(","),
|
||||
label: false,
|
||||
placeholder: t("admin.budget_investments.edit.tags_placeholder"),
|
||||
class: "js-tag-list" %>
|
||||
</div>
|
||||
|
||||
<div class="small-12 column margin-top">
|
||||
<%= f.label :valuator_ids %>
|
||||
<ul>
|
||||
<%= f.collection_check_boxes :valuator_group_ids, @valuator_groups, :id, :name do |group| %>
|
||||
<li><%= group.label(title: group.object.name) { group.check_box + truncate(group.object.name, length: 60) } %></li>
|
||||
<% end %>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="small-12 column">
|
||||
<hr>
|
||||
<%= f.label :valuator_ids, t("admin.budget_investments.edit.assigned_valuators") %>
|
||||
<ul>
|
||||
<%= f.collection_check_boxes :valuator_ids, @valuators, :id, :email do |b| %>
|
||||
<li><%= b.label(title: valuator_label(b.object)) { b.check_box + truncate(b.object.description_or_email, length: 60) } %></li>
|
||||
<% end %>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row expanded margin-top">
|
||||
<% if @investment.incompatible? || @investment.winner? %>
|
||||
<div class="small-12 medium-3 column">
|
||||
<h2 id="incompatible"><%= t("admin.budget_investments.edit.compatibility") %></h2>
|
||||
<%= f.check_box :incompatible,
|
||||
title: t("admin.budget_investments.edit.compatibility") %>
|
||||
</div>
|
||||
<% end %>
|
||||
<div class="small-12 medium-3 column float-left">
|
||||
<h2 id="selected"><%= t("admin.budget_investments.edit.selection") %></h2>
|
||||
<%= f.check_box :selected,
|
||||
title: t("admin.budget_investments.edit.selection") %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="small-12 column">
|
||||
<%= f.text_field :milestone_tag_list,
|
||||
value: @investment.milestone_tag_list.sort.join(", ") %>
|
||||
</div>
|
||||
|
||||
<div class="small-12 column margin-top">
|
||||
<%= f.submit(class: "button", value: t("admin.budget_investments.edit.submit_button")) %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
19
app/views/custom/admin/sectors/_form.html.erb
Normal file
19
app/views/custom/admin/sectors/_form.html.erb
Normal file
@@ -0,0 +1,19 @@
|
||||
<%= form_for [:admin, @sector] do |f| %>
|
||||
<%= render "shared/errors", resource: @sector %>
|
||||
|
||||
<div class="small-12 medium-6">
|
||||
<%= f.text_field :name %>
|
||||
</div>
|
||||
|
||||
<div class="small-12 medium-6">
|
||||
<%= f.select :parent_id,
|
||||
Sector.main.where.not(id: @sector.id).map { |sector| [sector.name, sector.id] },
|
||||
{ include_blank: t("admin.sectors.form.no_parent"),
|
||||
hint: t("admin.sectors.form.parent_hint") },
|
||||
disabled: @sector.sub_sectors.any? %>
|
||||
</div>
|
||||
|
||||
<div class="margin-top">
|
||||
<%= f.submit t("admin.sectors.form.submit"), class: "button success" %>
|
||||
</div>
|
||||
<% end %>
|
||||
5
app/views/custom/admin/sectors/edit.html.erb
Normal file
5
app/views/custom/admin/sectors/edit.html.erb
Normal file
@@ -0,0 +1,5 @@
|
||||
<%= back_link_to admin_sectors_path %>
|
||||
|
||||
<h2><%= t("admin.sectors.edit.title") %></h2>
|
||||
|
||||
<%= render "form" %>
|
||||
43
app/views/custom/admin/sectors/index.html.erb
Normal file
43
app/views/custom/admin/sectors/index.html.erb
Normal file
@@ -0,0 +1,43 @@
|
||||
<h2 class="inline-block"><%= t("admin.sectors.index.title") %></h2>
|
||||
|
||||
<%= link_to t("admin.sectors.index.new_sector"), new_admin_sector_path, class: "button float-right" %>
|
||||
|
||||
<p class="help-text clear"><%= t("admin.sectors.index.help") %></p>
|
||||
|
||||
<% if @sectors.any? %>
|
||||
<table class="sectors-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><%= t("admin.sectors.index.table_name") %></th>
|
||||
<th><%= t("admin.sectors.index.table_sub_sectors") %></th>
|
||||
<th><%= t("admin.actions.actions") %></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% @sectors.each do |sector| %>
|
||||
<tr id="<%= dom_id(sector) %>">
|
||||
<td><strong><%= sector.name %></strong></td>
|
||||
<td><%= sector.sub_sectors.size %></td>
|
||||
<td>
|
||||
<%= render Admin::TableActionsComponent.new(sector) do |actions| %>
|
||||
<%= actions.action(:new,
|
||||
text: t("admin.sectors.index.new_sub_sector"),
|
||||
path: new_admin_sector_path(parent_id: sector.id)) %>
|
||||
<% end %>
|
||||
</td>
|
||||
</tr>
|
||||
<% sector.sub_sectors.each do |sub_sector| %>
|
||||
<tr id="<%= dom_id(sub_sector) %>" class="sub-sector">
|
||||
<td> ↳ <%= sub_sector.name %></td>
|
||||
<td></td>
|
||||
<td><%= render Admin::TableActionsComponent.new(sub_sector) %></td>
|
||||
</tr>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
<% else %>
|
||||
<div class="callout primary margin-top clear">
|
||||
<%= t("admin.sectors.index.empty") %>
|
||||
</div>
|
||||
<% end %>
|
||||
5
app/views/custom/admin/sectors/new.html.erb
Normal file
5
app/views/custom/admin/sectors/new.html.erb
Normal file
@@ -0,0 +1,5 @@
|
||||
<%= back_link_to admin_sectors_path %>
|
||||
|
||||
<h2><%= t("admin.sectors.new.title") %></h2>
|
||||
|
||||
<%= render "form" %>
|
||||
@@ -0,0 +1,84 @@
|
||||
<h1><%= investment.title %></h1>
|
||||
<div class="budget-investment-info">
|
||||
<%= render "/shared/author_info", resource: investment %>
|
||||
|
||||
<span class="bullet"> • </span>
|
||||
<%= l investment.created_at.to_date %>
|
||||
<span class="bullet"> • </span>
|
||||
<%= render Shared::CommentsCountComponent.new(investment.comments_count, url: "#comments") %>
|
||||
<span class="bullet"> • </span>
|
||||
<%= investment.heading.name %>
|
||||
<span class="bullet"> • </span>
|
||||
<% if local_assigns[:preview].nil? %>
|
||||
<span class="js-flag-actions">
|
||||
<%= render "shared/flag_actions", flaggable: investment %>
|
||||
</span>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<br>
|
||||
|
||||
<%= render_image(investment.image, :large, true) if investment.image.present? %>
|
||||
|
||||
<p id="investment_code">
|
||||
<%= sanitize(t("budgets.investments.show.code", code: investment.id)) %>
|
||||
</p>
|
||||
|
||||
<%= auto_link_already_sanitized_html wysiwyg(investment.description) %>
|
||||
|
||||
<% if investment.objectives.present? %>
|
||||
<h2 id="objectives"><%= t("budgets.investments.project_details.objectives") %></h2>
|
||||
<%= simple_format(investment.objectives) %>
|
||||
<% end %>
|
||||
|
||||
<%= render Budgets::Investments::ProjectDetailsComponent.new(investment) %>
|
||||
|
||||
<% if feature?(:map) && map_location_available?(@investment.map_location) %>
|
||||
<div class="margin">
|
||||
<%= render_map(investment.map_location) %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<% if investment.location.present? %>
|
||||
<p>
|
||||
<%= sanitize(t("budgets.investments.show.location", location: investment.location)) %>
|
||||
</p>
|
||||
<% end %>
|
||||
|
||||
<% if investment.organization_name.present? %>
|
||||
<p>
|
||||
<%= sanitize(t("budgets.investments.show.organization_name", name: investment.organization_name)) %>
|
||||
</p>
|
||||
<% end %>
|
||||
|
||||
<% if feature?(:allow_attached_documents) %>
|
||||
<%= render Documents::DocumentsComponent.new(investment.documents) %>
|
||||
<% end %>
|
||||
|
||||
<%= render "shared/tags", taggable: investment %>
|
||||
|
||||
<% if investment.external_url.present? %>
|
||||
<div class="investment-external-link">
|
||||
<%= sanitize_and_auto_link investment.external_url %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<% if investment.should_show_unfeasibility_explanation? %>
|
||||
<h2><%= t("budgets.investments.show.unfeasibility_explanation") %></h2>
|
||||
<p><%= investment.unfeasibility_explanation %></p>
|
||||
<% end %>
|
||||
|
||||
<% if investment.should_show_price_explanation? %>
|
||||
<h2 id="price_explanation" data-magellan-target="price_explanation">
|
||||
<%= t("budgets.investments.show.price_explanation") %>
|
||||
</h2>
|
||||
<p><%= investment.price_explanation %></p>
|
||||
<% end %>
|
||||
|
||||
<% if local_assigns[:preview].nil? %>
|
||||
<%= render "relationable/related_content", relationable: investment %>
|
||||
|
||||
<div class="js-moderator-investment-actions margin">
|
||||
<%= render "budgets/investments/actions", investment: investment %>
|
||||
</div>
|
||||
<% end %>
|
||||
@@ -140,6 +140,7 @@ ignore_unused:
|
||||
- "activemodel.*"
|
||||
- "attributes.*"
|
||||
- "date.order"
|
||||
- "date.formats.month_year"
|
||||
- "unauthorized.*"
|
||||
- "admin.officials.level_*"
|
||||
- "admin.hidden_comments.index.filter*"
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
en:
|
||||
date:
|
||||
formats:
|
||||
month_year: "%B %Y"
|
||||
activerecord:
|
||||
attributes:
|
||||
budget/investment:
|
||||
|
||||
65
config/locales/custom/en/projects.yml
Normal file
65
config/locales/custom/en/projects.yml
Normal file
@@ -0,0 +1,65 @@
|
||||
en:
|
||||
activerecord:
|
||||
models:
|
||||
sector:
|
||||
one: "Sector"
|
||||
other: "Sectors"
|
||||
attributes:
|
||||
budget/investment:
|
||||
sector_id: "Sector"
|
||||
sub_sector_id: "Sub-sector"
|
||||
funding_source: "Source of funds"
|
||||
actual_cost: "Actual cost"
|
||||
start_date: "Start date"
|
||||
end_date: "End date"
|
||||
budget/investment/translation:
|
||||
objectives: "Objectives"
|
||||
sector:
|
||||
name: "Name"
|
||||
parent_id: "Parent sector"
|
||||
admin:
|
||||
menu:
|
||||
sectors: "Sectors"
|
||||
budget_investments:
|
||||
edit:
|
||||
project_details: "Project details"
|
||||
manage_sectors: "Manage sectors and sub-sectors"
|
||||
select_sector: "Select sector"
|
||||
select_sub_sector: "Select sub-sector"
|
||||
sectors:
|
||||
index:
|
||||
title: "Sectors"
|
||||
help: "Sectors and sub-sectors are used to classify investment projects. Each sub-sector belongs to a sector."
|
||||
new_sector: "Create sector"
|
||||
new_sub_sector: "Add sub-sector"
|
||||
table_name: "Name"
|
||||
table_sub_sectors: "Sub-sectors"
|
||||
empty: "There are no sectors yet."
|
||||
new:
|
||||
title: "New sector or sub-sector"
|
||||
edit:
|
||||
title: "Edit sector"
|
||||
form:
|
||||
no_parent: "None (this is a main sector)"
|
||||
parent_hint: "Choose a parent sector to create a sub-sector."
|
||||
submit: "Save sector"
|
||||
create:
|
||||
notice: "Sector created successfully"
|
||||
update:
|
||||
notice: "Sector updated successfully"
|
||||
destroy:
|
||||
notice: "Sector deleted successfully"
|
||||
unable_notice: "This sector cannot be deleted because it has sub-sectors or investment projects"
|
||||
budgets:
|
||||
investments:
|
||||
project_details:
|
||||
title: "Project details"
|
||||
objectives: "Objectives"
|
||||
sector: "Sector"
|
||||
status: "Status"
|
||||
implementation: "Implementation"
|
||||
price: "Project cost"
|
||||
actual_cost: "Actual cost"
|
||||
start_date: "Start date"
|
||||
end_date: "End date"
|
||||
funding_source: "Source of funds"
|
||||
@@ -1,4 +1,7 @@
|
||||
es:
|
||||
date:
|
||||
formats:
|
||||
month_year: "%B de %Y"
|
||||
activerecord:
|
||||
attributes:
|
||||
budget/investment:
|
||||
|
||||
65
config/locales/custom/es/projects.yml
Normal file
65
config/locales/custom/es/projects.yml
Normal file
@@ -0,0 +1,65 @@
|
||||
es:
|
||||
activerecord:
|
||||
models:
|
||||
sector:
|
||||
one: "Sector"
|
||||
other: "Sectores"
|
||||
attributes:
|
||||
budget/investment:
|
||||
sector_id: "Sector"
|
||||
sub_sector_id: "Subsector"
|
||||
funding_source: "Fuente de financiación"
|
||||
actual_cost: "Coste real"
|
||||
start_date: "Fecha de inicio"
|
||||
end_date: "Fecha de fin"
|
||||
budget/investment/translation:
|
||||
objectives: "Objetivos"
|
||||
sector:
|
||||
name: "Nombre"
|
||||
parent_id: "Sector padre"
|
||||
admin:
|
||||
menu:
|
||||
sectors: "Sectores"
|
||||
budget_investments:
|
||||
edit:
|
||||
project_details: "Datos del proyecto"
|
||||
manage_sectors: "Gestionar sectores y subsectores"
|
||||
select_sector: "Seleccionar sector"
|
||||
select_sub_sector: "Seleccionar subsector"
|
||||
sectors:
|
||||
index:
|
||||
title: "Sectores"
|
||||
help: "Los sectores y subsectores sirven para clasificar los proyectos de gasto. Cada subsector pertenece a un sector."
|
||||
new_sector: "Crear sector"
|
||||
new_sub_sector: "Añadir subsector"
|
||||
table_name: "Nombre"
|
||||
table_sub_sectors: "Subsectores"
|
||||
empty: "Todavía no hay sectores."
|
||||
new:
|
||||
title: "Nuevo sector o subsector"
|
||||
edit:
|
||||
title: "Editar sector"
|
||||
form:
|
||||
no_parent: "Ninguno (es un sector principal)"
|
||||
parent_hint: "Elige un sector padre para crear un subsector."
|
||||
submit: "Guardar sector"
|
||||
create:
|
||||
notice: "Sector creado correctamente"
|
||||
update:
|
||||
notice: "Sector actualizado correctamente"
|
||||
destroy:
|
||||
notice: "Sector eliminado correctamente"
|
||||
unable_notice: "No se puede eliminar este sector porque tiene subsectores o proyectos de gasto"
|
||||
budgets:
|
||||
investments:
|
||||
project_details:
|
||||
title: "Datos del proyecto"
|
||||
objectives: "Objetivos"
|
||||
sector: "Sector"
|
||||
status: "Estado"
|
||||
implementation: "Ejecución"
|
||||
price: "Coste del proyecto"
|
||||
actual_cost: "Coste real"
|
||||
start_date: "Fecha de inicio"
|
||||
end_date: "Fecha de fin"
|
||||
funding_source: "Fuente de financiación"
|
||||
@@ -1,4 +1,7 @@
|
||||
sw-KE:
|
||||
date:
|
||||
formats:
|
||||
month_year: "%B %Y"
|
||||
activerecord:
|
||||
attributes:
|
||||
budget/investment:
|
||||
|
||||
29
config/locales/custom/sw-KE/projects.yml
Normal file
29
config/locales/custom/sw-KE/projects.yml
Normal file
@@ -0,0 +1,29 @@
|
||||
sw-KE:
|
||||
activerecord:
|
||||
models:
|
||||
sector:
|
||||
one: "Sekta"
|
||||
other: "Sekta"
|
||||
attributes:
|
||||
budget/investment:
|
||||
sector_id: "Sekta"
|
||||
sub_sector_id: "Sekta ndogo"
|
||||
funding_source: "Chanzo cha fedha"
|
||||
actual_cost: "Gharama halisi"
|
||||
start_date: "Tarehe ya kuanza"
|
||||
end_date: "Tarehe ya kumaliza"
|
||||
budget/investment/translation:
|
||||
objectives: "Malengo"
|
||||
budgets:
|
||||
investments:
|
||||
project_details:
|
||||
title: "Maelezo ya mradi"
|
||||
objectives: "Malengo"
|
||||
sector: "Sekta"
|
||||
status: "Hali"
|
||||
implementation: "Utekelezaji"
|
||||
price: "Gharama ya mradi"
|
||||
actual_cost: "Gharama halisi"
|
||||
start_date: "Tarehe ya kuanza"
|
||||
end_date: "Tarehe ya kumaliza"
|
||||
funding_source: "Chanzo cha fedha"
|
||||
@@ -39,6 +39,8 @@ constraints lambda { |request| !Rails.application.multitenancy_management_mode?
|
||||
resources :budgets, only: [] do
|
||||
resources :sets, except: [:index, :show], controller: "budget_sets"
|
||||
end
|
||||
|
||||
resources :sectors, except: :show
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
class AddNairobiProjectFieldsToBudgetInvestments < ActiveRecord::Migration[7.1]
|
||||
def change
|
||||
create_table :sectors do |t|
|
||||
t.string :name, null: false
|
||||
t.references :parent, foreign_key: { to_table: :sectors }, index: true
|
||||
|
||||
t.timestamps
|
||||
end
|
||||
add_index :sectors, [:parent_id, :name], unique: true
|
||||
|
||||
change_table :budget_investments, bulk: true do |t|
|
||||
t.bigint :actual_cost
|
||||
t.date :start_date
|
||||
t.date :end_date
|
||||
t.string :funding_source
|
||||
t.references :sector, foreign_key: true, index: true
|
||||
t.references :sub_sector, foreign_key: { to_table: :sectors }, index: true
|
||||
end
|
||||
|
||||
add_column :budget_investment_translations, :objectives, :text
|
||||
end
|
||||
end
|
||||
23
db/schema.rb
23
db/schema.rb
@@ -10,7 +10,7 @@
|
||||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema[7.1].define(version: 2026_10_05_120000) do
|
||||
ActiveRecord::Schema[7.1].define(version: 2026_10_08_100000) do
|
||||
# These are extensions that must be enabled in order to support this database
|
||||
enable_extension "pg_trgm"
|
||||
enable_extension "plpgsql"
|
||||
@@ -249,6 +249,7 @@ ActiveRecord::Schema[7.1].define(version: 2026_10_05_120000) do
|
||||
t.string "title"
|
||||
t.text "description"
|
||||
t.datetime "hidden_at", precision: nil
|
||||
t.text "objectives"
|
||||
t.index ["budget_investment_id"], name: "index_budget_investment_translations_on_budget_investment_id"
|
||||
t.index ["hidden_at"], name: "index_budget_investment_translations_on_hidden_at"
|
||||
t.index ["locale"], name: "index_budget_investment_translations_on_locale"
|
||||
@@ -293,6 +294,12 @@ ActiveRecord::Schema[7.1].define(version: 2026_10_05_120000) do
|
||||
t.datetime "ignored_flag_at", precision: nil
|
||||
t.integer "flags_count", default: 0
|
||||
t.integer "original_heading_id"
|
||||
t.bigint "actual_cost"
|
||||
t.date "start_date"
|
||||
t.date "end_date"
|
||||
t.string "funding_source"
|
||||
t.bigint "sector_id"
|
||||
t.bigint "sub_sector_id"
|
||||
t.index ["administrator_id"], name: "index_budget_investments_on_administrator_id"
|
||||
t.index ["author_id"], name: "index_budget_investments_on_author_id"
|
||||
t.index ["budget_id"], name: "index_budget_investments_on_budget_id"
|
||||
@@ -300,7 +307,9 @@ ActiveRecord::Schema[7.1].define(version: 2026_10_05_120000) do
|
||||
t.index ["group_id"], name: "index_budget_investments_on_group_id"
|
||||
t.index ["heading_id"], name: "index_budget_investments_on_heading_id"
|
||||
t.index ["incompatible"], name: "index_budget_investments_on_incompatible"
|
||||
t.index ["sector_id"], name: "index_budget_investments_on_sector_id"
|
||||
t.index ["selected"], name: "index_budget_investments_on_selected"
|
||||
t.index ["sub_sector_id"], name: "index_budget_investments_on_sub_sector_id"
|
||||
t.index ["tsv"], name: "index_budget_investments_on_tsv", using: :gin
|
||||
end
|
||||
|
||||
@@ -1460,6 +1469,15 @@ ActiveRecord::Schema[7.1].define(version: 2026_10_05_120000) do
|
||||
t.index ["goal_id"], name: "index_sdg_targets_on_goal_id"
|
||||
end
|
||||
|
||||
create_table "sectors", force: :cascade do |t|
|
||||
t.string "name", null: false
|
||||
t.bigint "parent_id"
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.index ["parent_id", "name"], name: "index_sectors_on_parent_id_and_name", unique: true
|
||||
t.index ["parent_id"], name: "index_sectors_on_parent_id"
|
||||
end
|
||||
|
||||
create_table "settings", id: :serial, force: :cascade do |t|
|
||||
t.string "key"
|
||||
t.string "value"
|
||||
@@ -1795,6 +1813,8 @@ ActiveRecord::Schema[7.1].define(version: 2026_10_05_120000) do
|
||||
add_foreign_key "budget_groups", "budget_sets", column: "set_id"
|
||||
add_foreign_key "budget_headings", "geozones"
|
||||
add_foreign_key "budget_investments", "communities"
|
||||
add_foreign_key "budget_investments", "sectors"
|
||||
add_foreign_key "budget_investments", "sectors", column: "sub_sector_id"
|
||||
add_foreign_key "budget_set_translations", "budget_sets"
|
||||
add_foreign_key "budget_sets", "budgets"
|
||||
add_foreign_key "budget_valuators", "budgets"
|
||||
@@ -1841,6 +1861,7 @@ ActiveRecord::Schema[7.1].define(version: 2026_10_05_120000) do
|
||||
add_foreign_key "related_content_scores", "related_contents"
|
||||
add_foreign_key "related_content_scores", "users"
|
||||
add_foreign_key "sdg_managers", "users"
|
||||
add_foreign_key "sectors", "sectors", column: "parent_id"
|
||||
add_foreign_key "users", "geozones"
|
||||
add_foreign_key "valuators", "users"
|
||||
end
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
require "rails_helper"
|
||||
|
||||
describe Budgets::Investments::ProjectDetailsComponent do
|
||||
let(:sector) { create(:sector, name: "Business and Hustler Opportunities") }
|
||||
let(:investment) do
|
||||
create(:budget_investment, :winner,
|
||||
price: 242_592_913,
|
||||
actual_cost: 194_000_000,
|
||||
start_date: Date.new(2024, 2, 1),
|
||||
end_date: Date.new(2025, 2, 1),
|
||||
funding_source: "NCCG",
|
||||
sector: sector,
|
||||
sub_sector: create(:sector, parent: sector, name: "Markets and Trading Services"))
|
||||
end
|
||||
|
||||
it "shows the project details" do
|
||||
investment.budget.update!(currency_symbol: "KSh", phase: "finished")
|
||||
create(:progress_bar, progressable: investment, percentage: 85)
|
||||
create(:milestone, milestoneable: investment, status: create(:milestone_status, name: "Delayed"))
|
||||
|
||||
render_inline Budgets::Investments::ProjectDetailsComponent.new(investment)
|
||||
|
||||
expect(page).to have_css "h2", exact_text: "Project details"
|
||||
expect(page).to have_css "dt", exact_text: "Sector"
|
||||
expect(page).to have_css "dd",
|
||||
exact_text: "Business and Hustler Opportunities › Markets and Trading Services"
|
||||
expect(page).to have_css "dd", exact_text: "Delayed"
|
||||
expect(page).to have_css "dd", exact_text: "85%"
|
||||
expect(page).to have_css "dd", text: "242,592,913"
|
||||
expect(page).to have_css "dd", text: "194,000,000"
|
||||
expect(page).to have_css "dd", exact_text: "February 2024"
|
||||
expect(page).to have_css "dd", exact_text: "February 2025"
|
||||
expect(page).to have_css "dd", exact_text: "NCCG"
|
||||
end
|
||||
|
||||
it "does not show the costs before prices are published" do
|
||||
investment.budget.update!(phase: "accepting")
|
||||
|
||||
render_inline Budgets::Investments::ProjectDetailsComponent.new(investment)
|
||||
|
||||
expect(page).not_to have_content "Project cost"
|
||||
expect(page).not_to have_content "Actual cost"
|
||||
expect(page).to have_content "NCCG"
|
||||
end
|
||||
|
||||
it "does not show the costs when the budget hides money" do
|
||||
investment.budget.update!(voting_style: "approval", hide_money: true)
|
||||
|
||||
render_inline Budgets::Investments::ProjectDetailsComponent.new(investment)
|
||||
|
||||
expect(page).not_to have_content "Project cost"
|
||||
expect(page).not_to have_content "Actual cost"
|
||||
expect(page).to have_content "NCCG"
|
||||
end
|
||||
|
||||
it "is not rendered without project details" do
|
||||
render_inline Budgets::Investments::ProjectDetailsComponent.new(create(:budget_investment, price: nil))
|
||||
|
||||
expect(page).not_to be_rendered
|
||||
end
|
||||
end
|
||||
9
spec/factories/custom/sectors.rb
Normal file
9
spec/factories/custom/sectors.rb
Normal file
@@ -0,0 +1,9 @@
|
||||
FactoryBot.define do
|
||||
factory :sector do
|
||||
sequence(:name) { |n| "Sector #{n}" }
|
||||
|
||||
trait :sub_sector do
|
||||
parent factory: :sector
|
||||
end
|
||||
end
|
||||
end
|
||||
69
spec/models/custom/sector_spec.rb
Normal file
69
spec/models/custom/sector_spec.rb
Normal file
@@ -0,0 +1,69 @@
|
||||
require "rails_helper"
|
||||
|
||||
describe Sector do
|
||||
it "is valid" do
|
||||
expect(build(:sector)).to be_valid
|
||||
expect(build(:sector, :sub_sector)).to be_valid
|
||||
end
|
||||
|
||||
it "requires a unique name within the same parent" do
|
||||
markets = create(:sector, name: "Markets")
|
||||
create(:sector, name: "Trading", parent: markets)
|
||||
|
||||
expect(build(:sector, name: "markets")).not_to be_valid
|
||||
expect(build(:sector, name: "trading", parent: markets)).not_to be_valid
|
||||
expect(build(:sector, name: "Trading", parent: create(:sector))).to be_valid
|
||||
end
|
||||
|
||||
it "does not allow sub-sectors of sub-sectors" do
|
||||
expect(build(:sector, parent: create(:sector, :sub_sector))).not_to be_valid
|
||||
end
|
||||
|
||||
it "cannot be deleted when it has sub-sectors or investments" do
|
||||
sector = create(:sector)
|
||||
create(:sector, parent: sector)
|
||||
|
||||
expect(sector.destroy).to be false
|
||||
|
||||
used = create(:sector)
|
||||
create(:budget_investment, sector: used)
|
||||
|
||||
expect(used.destroy).to be false
|
||||
end
|
||||
|
||||
describe ".find_or_create_by_name!" do
|
||||
it "finds sectors ignoring the case and creates them otherwise" do
|
||||
sector = create(:sector, name: "Health")
|
||||
|
||||
expect(Sector.find_or_create_by_name!("HEALTH")).to eq sector
|
||||
expect { Sector.find_or_create_by_name!("Clinics", parent: sector) }.to change {
|
||||
sector.sub_sectors.count
|
||||
}.by(1)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe Budget::Investment do
|
||||
describe "project fields" do
|
||||
let(:sector) { create(:sector) }
|
||||
|
||||
it "requires the sub-sector to belong to the sector" do
|
||||
sub_sector = create(:sector, parent: sector)
|
||||
|
||||
expect(build(:budget_investment, sector: sector, sub_sector: sub_sector)).to be_valid
|
||||
expect(build(:budget_investment, sector: create(:sector), sub_sector: sub_sector)).not_to be_valid
|
||||
expect(build(:budget_investment, sector: sub_sector)).not_to be_valid
|
||||
end
|
||||
|
||||
it "requires the end date to be after the start date" do
|
||||
expect(build(:budget_investment, start_date: "2024-02-01", end_date: "2025-02-01")).to be_valid
|
||||
expect(build(:budget_investment, start_date: "2025-02-01", end_date: "2024-02-01")).not_to be_valid
|
||||
end
|
||||
|
||||
it "has translatable objectives" do
|
||||
investment = create(:budget_investment, objectives: "Decent trading spaces")
|
||||
|
||||
expect(investment.reload.objectives).to eq "Decent trading spaces"
|
||||
end
|
||||
end
|
||||
end
|
||||
74
spec/system/custom/admin/sectors_spec.rb
Normal file
74
spec/system/custom/admin/sectors_spec.rb
Normal file
@@ -0,0 +1,74 @@
|
||||
require "rails_helper"
|
||||
|
||||
describe "Admin sectors", :admin do
|
||||
scenario "Create sectors and sub-sectors" do
|
||||
visit admin_root_path
|
||||
within("#admin_menu") { click_link "Sectors" }
|
||||
|
||||
expect(page).to have_content "There are no sectors yet."
|
||||
|
||||
click_link "Create sector"
|
||||
fill_in "Name", with: "Health"
|
||||
click_button "Save sector"
|
||||
|
||||
expect(page).to have_content "Sector created successfully"
|
||||
|
||||
within("tr", text: "Health") { click_link "Add sub-sector" }
|
||||
|
||||
expect(page).to have_select "Parent sector", selected: "Health"
|
||||
|
||||
fill_in "Name", with: "Clinics"
|
||||
click_button "Save sector"
|
||||
|
||||
expect(page).to have_css "tr.sub-sector", text: "Clinics"
|
||||
end
|
||||
|
||||
scenario "Sectors with investments cannot be deleted" do
|
||||
sector = create(:sector, name: "Water")
|
||||
create(:budget_investment, sector: sector)
|
||||
|
||||
visit admin_sectors_path
|
||||
|
||||
accept_confirm { click_button "Delete Water" }
|
||||
|
||||
expect(page).to have_content "This sector cannot be deleted"
|
||||
expect(page).to have_css "tr", text: "Water"
|
||||
end
|
||||
|
||||
scenario "Edit the project details of an investment" do
|
||||
health = create(:sector, name: "Health")
|
||||
create(:sector, parent: health, name: "Clinics")
|
||||
markets = create(:sector, name: "Markets")
|
||||
create(:sector, parent: markets, name: "Trading Services")
|
||||
investment = create(:budget_investment)
|
||||
|
||||
visit edit_admin_budget_budget_investment_path(investment.budget, investment)
|
||||
|
||||
select "Markets", from: "Sector"
|
||||
|
||||
expect(page).to have_select "Sub-sector", options: ["Select sub-sector", "Trading Services"]
|
||||
|
||||
select "Health", from: "Sector"
|
||||
|
||||
expect(page).to have_select "Sub-sector", options: ["Select sub-sector", "Clinics"]
|
||||
|
||||
select "Clinics", from: "Sub-sector"
|
||||
fill_in "Objectives", with: "Better health services"
|
||||
fill_in "Source of funds", with: "NCCG"
|
||||
fill_in "Actual cost", with: "1500"
|
||||
fill_in "Start date", with: Date.new(2024, 2, 1)
|
||||
fill_in "End date", with: Date.new(2025, 2, 1)
|
||||
click_button "Update"
|
||||
|
||||
expect(page).to have_content "Investment project updated successfully"
|
||||
|
||||
investment.reload
|
||||
expect(investment.sector).to eq health
|
||||
expect(investment.sub_sector.name).to eq "Clinics"
|
||||
expect(investment.objectives).to eq "Better health services"
|
||||
expect(investment.funding_source).to eq "NCCG"
|
||||
expect(investment.actual_cost).to eq 1500
|
||||
expect(investment.start_date).to eq Date.new(2024, 2, 1)
|
||||
expect(investment.end_date).to eq Date.new(2025, 2, 1)
|
||||
end
|
||||
end
|
||||
35
spec/system/custom/investment_project_details_spec.rb
Normal file
35
spec/system/custom/investment_project_details_spec.rb
Normal file
@@ -0,0 +1,35 @@
|
||||
require "rails_helper"
|
||||
|
||||
describe "Investment project details" do
|
||||
scenario "are shown on the investment page and updated with its milestones" do
|
||||
budget = create(:budget, :finished)
|
||||
investment = create(:budget_investment, :winner,
|
||||
budget: budget,
|
||||
objectives: "To provide additional decent trading spaces",
|
||||
funding_source: "NCCG")
|
||||
create(:progress_bar, progressable: investment, percentage: 40)
|
||||
create(:milestone, milestoneable: investment, status: create(:milestone_status, name: "Ongoing"))
|
||||
|
||||
visit budget_investment_path(budget, investment)
|
||||
|
||||
within "#objectives + p" do
|
||||
expect(page).to have_content "To provide additional decent trading spaces"
|
||||
end
|
||||
|
||||
within ".investment-project-details" do
|
||||
expect(page).to have_content "Status\nOngoing"
|
||||
expect(page).to have_content "Implementation\n40%"
|
||||
expect(page).to have_content "Source of funds\nNCCG"
|
||||
end
|
||||
|
||||
investment.primary_progress_bar.update!(percentage: 85)
|
||||
create(:milestone, milestoneable: investment, status: create(:milestone_status, name: "Delayed"))
|
||||
|
||||
refresh
|
||||
|
||||
within ".investment-project-details" do
|
||||
expect(page).to have_content "Status\nDelayed"
|
||||
expect(page).to have_content "Implementation\n85%"
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user