Compare commits

...

10 Commits

Author SHA1 Message Date
Trevor Vallender 213cc31275 Stop using issue class on jobs
Refs #2185
2023-12-19 09:44:26 +00:00
Trevor Vallender a74d1ad2ea Allow sorting/grouping by jobs
This takes affect in the time entry view.
2023-12-07 13:44:10 +00:00
Trevor Vallender bcb669745e Remove unique constraint on Job name
Refs #2186
2023-12-05 14:28:42 +00:00
Trevor Vallender a71ffbcacc Auto-assign time to jobs
If a job is not specified, a reasonable choice will be made.

Refs #2111
2023-12-05 14:25:24 +00:00
Trevor Vallender 40eb43e0c5 Make Jobs a module
Should be disable-able on projects.

Closes #2180
2023-12-04 13:44:59 +00:00
Trevor Vallender d81982cd20 Fix CSS on jobs show page
Stop using the issue class inappropriately

Closes #2185
2023-12-04 13:31:19 +00:00
Trevor Vallender 543dc1572a Allow inputting blank job on time entry
Closes #2189
2023-12-04 13:10:10 +00:00
Trevor Vallender 14c8f6633d Only show active jobs when logging time
Closes #2110
2023-12-04 13:07:08 +00:00
Trevor Vallender 244c8f1a07 Various small fixes
Particularly prevents blowing up on validation error

Completes #2187
2023-12-04 13:04:51 +00:00
Trevor Vallender 3e04193d16 Auto-assign logged time to budgets
Now that users have a "budget category" assigned to them, we can show
time logged against the correct budget.

Refs #2165
2023-11-30 19:51:21 +00:00
15 changed files with 106 additions and 22 deletions

View File

@ -26,10 +26,12 @@ class JobsController < ApplicationController
end end
def create def create
if @job = Job.create(job_params) @job = Job.new(remove_empty_time_budgets(job_params))
if @job.save
redirect_to project_job_path(@job.project, @job) redirect_to project_job_path(@job.project, @job)
else else
render :edit @project = @job.project
render :new
end end
end end

View File

@ -7,6 +7,9 @@ module JobsHelper
end end
def progress_bar_for(budget) def progress_bar_for(budget)
l_hours_short(budget.hours) progress_bar(budget.done_ratio,
legend: "#{budget.done_ratio}%
(#{l_hours_short(budget.total_time_logged)}/#{l_hours_short(budget.hours)})",
class: "progress")
end end
end end

View File

@ -35,12 +35,6 @@ class Job < ActiveRecord::Base
time_budgets.sum(&:hours) time_budgets.sum(&:hours)
end end
def time_budget_for(category)
return 0 if category.nil? || time_budgets.find_by(category_id: category.id).nil?
time_budgets.find_by(category_id: category.id).hours
end
def total_time_logged def total_time_logged
TimeEntry.where(job_id: id) TimeEntry.where(job_id: id)
.sum(:hours) .sum(:hours)
@ -56,4 +50,27 @@ class Job < ActiveRecord::Base
ActionView::Base.send(:include, Rails.application.routes.url_helpers) ActionView::Base.send(:include, Rails.application.routes.url_helpers)
ActionController::Base.helpers.link_to name, ActionController::Base.helpers.project_job_path(project, self) ActionController::Base.helpers.link_to name, ActionController::Base.helpers.project_job_path(project, self)
end end
def self.fields_for_order_statement
"jobs.name"
end
def self.default_for(time_entry)
projects = [time_entry.project, time_entry.project.parent]
jobs = Job.where(project: projects).active
support = jobs.where(category: JobCategory.support).first
retainer = jobs.where(category: JobCategory.retainer).first
sprints = jobs.where(category: JobCategory.sprints).first
priority_list = [sprints, retainer, support].compact
return jobs.first if priority_list.empty?
return support if time_entry.activity.name == "Support"
return priority_list.first if time_entry.issue.blank?
return support if time_entry.issue.tracker.name == "Support"
priority_list.first
end
end end

View File

@ -5,6 +5,10 @@ class JobCategory < Enumeration
OptionName = :enumeration_job_category OptionName = :enumeration_job_category
scope :support, -> { where(name: 'Support').first }
scope :retainer, -> { where(name: 'Retainer').first }
scope :sprints, -> { where(name: 'Sprints').first }
def option_name def option_name
OptionName OptionName
end end

View File

@ -10,4 +10,18 @@ class TimeBudget < ActiveRecord::Base
belongs_to :job belongs_to :job
belongs_to :category, class_name: "TimeBudgetCategory" belongs_to :category, class_name: "TimeBudgetCategory"
def done_ratio
return 0 if hours.zero?
(total_time_logged / hours * 100).to_i
end
def total_time_logged
TimeEntry.joins(:user)
.where(
job_id: job_id,
users: { time_budget_category_id: category_id }
).sum(:hours)
end
end end

View File

@ -24,7 +24,7 @@
<%= f.hidden_field :project_id, value: @job.project.id %> <%= f.hidden_field :project_id, value: @job.project.id %>
<fieldset> <fieldset>
<legend>Budget</legend> <legend>Budget (hours)</legend>
<%= f.fields_for :time_budgets do |ff| %> <%= f.fields_for :time_budgets do |ff| %>
<p> <p>
<%= ff.label :hours, ff.object.category&.name || "Unassigned" %> <%= ff.label :hours, ff.object.category&.name || "Unassigned" %>

View File

@ -3,10 +3,6 @@
<td><%= job.category&.name || "Unassigned" %></td> <td><%= job.category&.name || "Unassigned" %></td>
<td><%= format_date(job.starts_on) %></td> <td><%= format_date(job.starts_on) %></td>
<td><%= format_date(job.ends_on) %></td> <td><%= format_date(job.ends_on) %></td>
<td><%= job.project_id %></td>
<td>
<%= l_hours_short(job.total_time_budget) %>
</td>
<td> <td>
<%= total_progress_bar(job) %> <%= total_progress_bar(job) %>
</td> </td>

View File

@ -15,8 +15,6 @@
<th>Category</th> <th>Category</th>
<th>Starts on</th> <th>Starts on</th>
<th>Ends on</th> <th>Ends on</th>
<th>Project</th>
<th>External project</th>
<th>Progress</th> <th>Progress</th>
</tr> </tr>
</thead> </thead>

View File

@ -1,3 +1,7 @@
<% content_for :header_tags do %>
<%= stylesheet_link_tag "jobs", plugin: "jobs" %>
<% end %>
<% html_title @job.name %> <% html_title @job.name %>
<div class="contextual"> <div class="contextual">
<%= link_to 'Edit', edit_project_job_path(@project, @job), class: "icon icon-edit edit-job" %> <%= link_to 'Edit', edit_project_job_path(@project, @job), class: "icon icon-edit edit-job" %>
@ -7,7 +11,7 @@
<% end %> <% end %>
</div> </div>
<h2>Job #<%= @job.id %></h2> <h2>Job #<%= @job.id %></h2>
<div class="issue"> <div class="job">
<div class="subject"><h3><%= @job.name %></h3></div> <div class="subject"><h3><%= @job.name %></h3></div>
<p><%= @job.description %></p> <p><%= @job.description %></p>
<div class="attributes"> <div class="attributes">
@ -19,7 +23,7 @@
<div class="label">Starts on:</div> <div class="label">Starts on:</div>
<div class="value"><%= format_date(@job.starts_on) %></div> <div class="value"><%= format_date(@job.starts_on) %></div>
<div class="label">Ends on:</div> <div class="label">Ends on:</div>
<div class="value"><%= format_date(@job.starts_on) %></div> <div class="value"><%= format_date(@job.ends_on) %></div>
</div> </div>
</div> </div>
<div class="splitcontentright"> <div class="splitcontentright">

View File

@ -1,3 +1,3 @@
<p> <p>
<%= form.label :job_id %> <%= form.label :job_id %>
<%= form.collection_select :job_id, Job.project_or_parent(@project), :id, :name %> <%= form.collection_select :job_id, Job.active.project_or_parent(@project), :id, :name, include_blank: true %>

View File

@ -0,0 +1,16 @@
/* These are a copy of the default CSS for issues */
div.job {background:#ffffdd; padding:6px; margin-bottom:6px; border: 1px solid #d7d7d7; border-radius:3px;}
div.job div.subject div div { padding-left: 16px; word-break: break-word; }
div.job div.subject p {margin: 0; margin-bottom: 0.1em; font-size: 90%; color: #999;}
div.job div.subject>div>p { margin-top: 0.5em; }
div.job div.subject h3 {margin: 0; margin-bottom: 0.1em;}
div.job p.author {margin-top:0.5em;}
div.job span.private, div.journal span.private {font-size: 60%;}
div.job .next-prev-links {color:#999;}
div.job .attributes {margin-top: 2em;}
div.job .attributes .attribute {padding-left:180px; clear:left; min-height: 1.8em;}
div.job .attributes .attribute .label {width: 170px; margin-left:-180px; font-weight:bold; float:left; overflow: clip visible; text-overflow: ellipsis;}
div.job .attribute .value {overflow:auto; text-overflow: ellipsis;}
div.job .attribute.string_cf .value .wiki p {margin-top: 0; margin-bottom: 0;}
div.job .attribute.text_cf .value .wiki p:first-of-type {margin-top: 0;}
div.job.overdue .due-date .value { color: #c22; }

View File

@ -0,0 +1,5 @@
class RemoveUniqueNameConstraintFromJobs < ActiveRecord::Migration[6.1]
def change
remove_index :jobs, :name
end
end

View File

@ -6,7 +6,6 @@ Redmine::Plugin.register :jobs do
url 'http://tsvallender.co.uk' url 'http://tsvallender.co.uk'
author_url 'http://tsvallender.co.uk' author_url 'http://tsvallender.co.uk'
permission :jobs, { jobs: [:index, :show, :new, :create, :edit, :update, :destroy] }, public: false
menu :project_menu, :jobs, { controller: 'jobs', action: 'index' }, caption: 'Jobs', after: :issues, param: :project_id menu :project_menu, :jobs, { controller: 'jobs', action: 'index' }, caption: 'Jobs', after: :issues, param: :project_id
TimeEntry.safe_attributes 'job_id' TimeEntry.safe_attributes 'job_id'
@ -18,4 +17,8 @@ Redmine::Plugin.register :jobs do
TimeEntry.send(:include, TimeEntryPatch) TimeEntry.send(:include, TimeEntryPatch)
Project.send(:include, ProjectPatch) Project.send(:include, ProjectPatch)
end end
project_module :jobs do
permission :jobs, { jobs: [:index, :show, :new, :create, :edit, :update, :destroy] }, public: false
end
end end

View File

@ -7,5 +7,13 @@ module TimeEntryPatch
included do included do
belongs_to :job belongs_to :job
before_save :set_job, unless: :job
def set_job
return if job.present?
self.job = Job.default_for(self)
end
end end
end end

View File

@ -14,6 +14,9 @@ module TimeEntryQueryPatch
alias_method :available_columns_without_jobs, :available_columns alias_method :available_columns_without_jobs, :available_columns
alias_method :available_columns, :available_columns_with_jobs alias_method :available_columns, :available_columns_with_jobs
alias_method :joins_for_order_statement_without_jobs, :joins_for_order_statement
alias_method :joins_for_order_statement, :joins_for_order_statement_with_jobs
end end
module InstanceMethods module InstanceMethods
@ -25,7 +28,7 @@ module TimeEntryQueryPatch
def available_columns_with_jobs def available_columns_with_jobs
if @available_columns.nil? if @available_columns.nil?
@available_columns = available_columns_without_jobs @available_columns = available_columns_without_jobs
@available_columns << QueryColumn.new(:job) @available_columns << QueryColumn.new(:job, groupable: true, sortable: -> { Job.fields_for_order_statement })
else else
available_columns_without_jobs available_columns_without_jobs
end end
@ -48,5 +51,16 @@ module TimeEntryQueryPatch
[job.name, job.id.to_s] [job.name, job.id.to_s]
end end
end end
def joins_for_order_statement_with_jobs(order_options)
joins = joins_for_order_statement_without_jobs(order_options) || ""
if order_options
if order_options.include?('jobs')
joins += " LEFT OUTER JOIN #{Job.table_name} ON #{Job.table_name}.id = #{TimeEntry.table_name}.job_id"
end
end
joins
end
end end
end end