Time budgets per activity
Add a TimeBudget model which allows a job to have a time budget for each activity type. We’re allowing nil as a holder for a generic budget, though there’s currently no way to use that. We automatically delete any time budgets which are set to zero.
This commit is contained in:
parent
c6cb6fa64d
commit
4b83753e71
|
@ -10,14 +10,15 @@ class JobsController < ApplicationController
|
|||
end
|
||||
|
||||
def new
|
||||
@job = Job.new
|
||||
@job = Job.new.with_all_time_budgets
|
||||
end
|
||||
|
||||
def edit
|
||||
@job.with_all_time_budgets
|
||||
end
|
||||
|
||||
def update
|
||||
if @job.update(job_params)
|
||||
if @job.update(remove_empty_time_budgets(job_params))
|
||||
redirect_to job_path(@job, project_id: @job.project.id)
|
||||
else
|
||||
render :edit
|
||||
|
@ -51,7 +52,8 @@ class JobsController < ApplicationController
|
|||
:budget,
|
||||
:external_project_id,
|
||||
:name,
|
||||
:description
|
||||
:description,
|
||||
time_budgets_attributes: [:id, :activity_id, :hours, :job_id, :_destroy]
|
||||
)
|
||||
end
|
||||
|
||||
|
@ -62,4 +64,12 @@ class JobsController < ApplicationController
|
|||
def set_job
|
||||
@job = Job.find(params[:id])
|
||||
end
|
||||
|
||||
# If a time budget is set to 0, remove it
|
||||
def remove_empty_time_budgets(params)
|
||||
params[:time_budgets_attributes].each do |key, value|
|
||||
params[:time_budgets_attributes][key]["_destroy"] = true if params[:time_budgets_attributes][key]["hours"] == "0"
|
||||
end
|
||||
params
|
||||
end
|
||||
end
|
||||
|
|
|
@ -7,10 +7,34 @@ class Job < ActiveRecord::Base
|
|||
presence: true
|
||||
|
||||
belongs_to :project
|
||||
has_many :time_entries
|
||||
has_many :time_entries, dependent: :restrict_with_error
|
||||
has_many :time_budgets, dependent: :restrict_with_error
|
||||
accepts_nested_attributes_for :time_budgets, allow_destroy: true
|
||||
|
||||
scope :project, ->(project) { where(project_id: project.id) }
|
||||
scope :project_or_parent, ->(project) { where(project_id: [project.id, project.parent.id]) }
|
||||
scope :project_or_parent, ->(project) { where(project_id: [project.id, project.parent&.id]) }
|
||||
|
||||
def with_all_time_budgets
|
||||
TimeEntryActivity.where.not(id: time_budgets.pluck(:activity_id)).each do |activity|
|
||||
time_budgets << TimeBudget.new(job_id: id, activity_id: activity.id)
|
||||
end
|
||||
self
|
||||
end
|
||||
|
||||
def missing_time_budgets
|
||||
budgets = []
|
||||
new_activities.collect { |activity| budgets << TimeBudget.new(job_id: id, activity_id: activity.id) }
|
||||
end
|
||||
|
||||
def total_time_budget
|
||||
return 0 if time_budgets.empty?
|
||||
|
||||
time_budgets.sum(&:hours)
|
||||
end
|
||||
|
||||
def time_budget_for(activity)
|
||||
time_budgets.where(activity_id: activity.id).hours || 0
|
||||
end
|
||||
|
||||
def total_time_logged
|
||||
TimeEntry.where(job_id: id)
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
class TimeBudget < ActiveRecord::Base
|
||||
validates :hours,
|
||||
presence: true
|
||||
|
||||
validates :activity_id,
|
||||
inclusion: { in: TimeEntryActivity.pluck(:id) }
|
||||
|
||||
belongs_to :job
|
||||
belongs_to :activity, class_name: "TimeEntryActivity"
|
||||
|
||||
end
|
|
@ -0,0 +1,11 @@
|
|||
<div class="label"><%= budget.activity.name %></div>
|
||||
<div class="value">
|
||||
<%= l_hours_short(
|
||||
budget.job.total_time_logged_for(budget.activity)
|
||||
) %> /
|
||||
<%= l_hours_short(
|
||||
budget.hours
|
||||
) %>
|
||||
<progress max="<%= budget %>"
|
||||
value="<%= budget.job.total_time_logged_for(budget.activity) %>"></progress>
|
||||
</div>
|
|
@ -3,34 +3,36 @@
|
|||
<p>
|
||||
<%= f.label :name %>
|
||||
<%= f.text_field :name %>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<%= f.label :description %>
|
||||
<%= f.text_area :description %>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<%= f.label :starts_on %>
|
||||
<%= f.date_field :starts_on %>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<%= f.label :ends_on %>
|
||||
<%= f.date_field :ends_on %>
|
||||
</p>
|
||||
|
||||
<%= f.hidden_field :project_id, value: @project.id %>
|
||||
|
||||
<p>
|
||||
<%= f.label :external_project_id %>
|
||||
<%= f.number_field :external_project_id %>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<%= f.label :budget %>
|
||||
<%= f.number_field :budget %>
|
||||
</p>
|
||||
<fieldset>
|
||||
<legend>Budget</legend>
|
||||
<%= f.fields_for :time_budgets do |ff| %>
|
||||
<p>
|
||||
<%= ff.label :hours, ff.object.activity.name %>
|
||||
<%= ff.number_field :hours %>
|
||||
<%= ff.hidden_field :activity_id %>
|
||||
<%= ff.hidden_field :job_id %>
|
||||
<%= ff.hidden_field :_destroy, value: false %>
|
||||
<% end %>
|
||||
</fieldset>
|
||||
|
||||
</div>
|
||||
<%= f.submit %>
|
||||
|
|
|
@ -1,9 +1,13 @@
|
|||
<tr>
|
||||
<td><%= link_to job.name, job_path(job, project_id: job.project_id) %></td>
|
||||
<td><%= job.starts_on %></td>
|
||||
<td><%= job.ends_on %></td>
|
||||
<td><%= format_date(job.starts_on) %></td>
|
||||
<td><%= format_date(job.ends_on) %></td>
|
||||
<td><%= job.project_id %></td>
|
||||
<td><%= job.external_project_id %></td>
|
||||
<td><%= job.budget %></td>
|
||||
<td>
|
||||
<%= l_hours_short(job.total_time_logged) %> /
|
||||
<%= l_hours_short(job.total_time_budget) %>
|
||||
<progress max="<%= job.total_time_budget %>" value="<%= job.total_time_logged %>"></progress>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
|
|
|
@ -27,12 +27,11 @@
|
|||
<div class="status attribute">
|
||||
<div class="label">Total:</div>
|
||||
<div class="value">
|
||||
<%= l_hours_short(@job.total_time_logged) %> / <%= l_hours_short(@job.budget) %>
|
||||
<progress max="<%= @job.budget %>" value="<%= @job.total_time_logged %>"></progress>
|
||||
<%= l_hours_short(@job.total_time_logged) %> / <%= l_hours_short(@job.total_time_budget) %>
|
||||
<progress max="<%= @job.total_time_budget %>" value="<%= @job.total_time_logged %>"></progress>
|
||||
</div>
|
||||
<% TimeEntryActivity.all.each do |activity| %>
|
||||
<div class="label"><%= activity.name %>:</div>
|
||||
<div class="value"><%= l_hours_short(@job.total_time_logged_for(activity)) %></div>
|
||||
<% @job.time_budgets.each do |budget| %>
|
||||
<%= render partial: "budget", locals: { budget: budget } %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -7,7 +7,6 @@ class CreateJobs < ActiveRecord::Migration[6.1]
|
|||
t.string :name, null: false
|
||||
t.references :project, foreign_key: true
|
||||
t.string :description
|
||||
t.integer :budget
|
||||
|
||||
t.timestamps
|
||||
end
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
class CreateTimeBudgets < ActiveRecord::Migration[6.1]
|
||||
def change
|
||||
create_table :time_budgets do |t|
|
||||
t.integer :hours, null: false, default: 0
|
||||
t.references :job, foreign_key: true, null: false
|
||||
t.integer :activity_id, null: true
|
||||
|
||||
t.timestamps
|
||||
end
|
||||
|
||||
add_index :time_budgets, [:job_id, :activity_id], unique: true
|
||||
# TODO: We should use NULLS NOT DISTINCT to only allow one instance of nil activity per-Job also
|
||||
# Requires Postgres 15
|
||||
end
|
||||
end
|
Loading…
Reference in New Issue