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
|
end
|
||||||
|
|
||||||
def new
|
def new
|
||||||
@job = Job.new
|
@job = Job.new.with_all_time_budgets
|
||||||
end
|
end
|
||||||
|
|
||||||
def edit
|
def edit
|
||||||
|
@job.with_all_time_budgets
|
||||||
end
|
end
|
||||||
|
|
||||||
def update
|
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)
|
redirect_to job_path(@job, project_id: @job.project.id)
|
||||||
else
|
else
|
||||||
render :edit
|
render :edit
|
||||||
|
@ -51,7 +52,8 @@ class JobsController < ApplicationController
|
||||||
:budget,
|
:budget,
|
||||||
:external_project_id,
|
:external_project_id,
|
||||||
:name,
|
:name,
|
||||||
:description
|
:description,
|
||||||
|
time_budgets_attributes: [:id, :activity_id, :hours, :job_id, :_destroy]
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -62,4 +64,12 @@ class JobsController < ApplicationController
|
||||||
def set_job
|
def set_job
|
||||||
@job = Job.find(params[:id])
|
@job = Job.find(params[:id])
|
||||||
end
|
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
|
end
|
||||||
|
|
|
@ -7,10 +7,34 @@ class Job < ActiveRecord::Base
|
||||||
presence: true
|
presence: true
|
||||||
|
|
||||||
belongs_to :project
|
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, ->(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
|
def total_time_logged
|
||||||
TimeEntry.where(job_id: id)
|
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>
|
<p>
|
||||||
<%= f.label :name %>
|
<%= f.label :name %>
|
||||||
<%= f.text_field :name %>
|
<%= f.text_field :name %>
|
||||||
</p>
|
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
<%= f.label :description %>
|
<%= f.label :description %>
|
||||||
<%= f.text_area :description %>
|
<%= f.text_area :description %>
|
||||||
</p>
|
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
<%= f.label :starts_on %>
|
<%= f.label :starts_on %>
|
||||||
<%= f.date_field :starts_on %>
|
<%= f.date_field :starts_on %>
|
||||||
</p>
|
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
<%= f.label :ends_on %>
|
<%= f.label :ends_on %>
|
||||||
<%= f.date_field :ends_on %>
|
<%= f.date_field :ends_on %>
|
||||||
</p>
|
|
||||||
|
|
||||||
<%= f.hidden_field :project_id, value: @project.id %>
|
<%= f.hidden_field :project_id, value: @project.id %>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
<%= f.label :external_project_id %>
|
<%= f.label :external_project_id %>
|
||||||
<%= f.number_field :external_project_id %>
|
<%= f.number_field :external_project_id %>
|
||||||
</p>
|
|
||||||
|
|
||||||
<p>
|
<fieldset>
|
||||||
<%= f.label :budget %>
|
<legend>Budget</legend>
|
||||||
<%= f.number_field :budget %>
|
<%= f.fields_for :time_budgets do |ff| %>
|
||||||
</p>
|
<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>
|
</div>
|
||||||
<%= f.submit %>
|
<%= f.submit %>
|
||||||
|
|
|
@ -1,9 +1,13 @@
|
||||||
<tr>
|
<tr>
|
||||||
<td><%= link_to job.name, job_path(job, project_id: job.project_id) %></td>
|
<td><%= link_to job.name, job_path(job, project_id: job.project_id) %></td>
|
||||||
<td><%= job.starts_on %></td>
|
<td><%= format_date(job.starts_on) %></td>
|
||||||
<td><%= job.ends_on %></td>
|
<td><%= format_date(job.ends_on) %></td>
|
||||||
<td><%= job.project_id %></td>
|
<td><%= job.project_id %></td>
|
||||||
<td><%= job.external_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>
|
</tr>
|
||||||
|
|
||||||
|
|
|
@ -27,12 +27,11 @@
|
||||||
<div class="status attribute">
|
<div class="status attribute">
|
||||||
<div class="label">Total:</div>
|
<div class="label">Total:</div>
|
||||||
<div class="value">
|
<div class="value">
|
||||||
<%= l_hours_short(@job.total_time_logged) %> / <%= l_hours_short(@job.budget) %>
|
<%= l_hours_short(@job.total_time_logged) %> / <%= l_hours_short(@job.total_time_budget) %>
|
||||||
<progress max="<%= @job.budget %>" value="<%= @job.total_time_logged %>"></progress>
|
<progress max="<%= @job.total_time_budget %>" value="<%= @job.total_time_logged %>"></progress>
|
||||||
</div>
|
</div>
|
||||||
<% TimeEntryActivity.all.each do |activity| %>
|
<% @job.time_budgets.each do |budget| %>
|
||||||
<div class="label"><%= activity.name %>:</div>
|
<%= render partial: "budget", locals: { budget: budget } %>
|
||||||
<div class="value"><%= l_hours_short(@job.total_time_logged_for(activity)) %></div>
|
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -7,7 +7,6 @@ class CreateJobs < ActiveRecord::Migration[6.1]
|
||||||
t.string :name, null: false
|
t.string :name, null: false
|
||||||
t.references :project, foreign_key: true
|
t.references :project, foreign_key: true
|
||||||
t.string :description
|
t.string :description
|
||||||
t.integer :budget
|
|
||||||
|
|
||||||
t.timestamps
|
t.timestamps
|
||||||
end
|
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