mirror of
https://github.com/opf/openproject.git
synced 2026-06-14 03:30:14 +00:00
When the allocation spans over multiple schedules, also show this in the warning
This commit is contained in:
+22
-9
@@ -38,14 +38,14 @@ module ResourceAllocations
|
||||
include OpPrimer::ComponentHelpers
|
||||
|
||||
def initialize(allocation:, project:, allocation_kind:, form_values:, overbooked_ranges: [],
|
||||
working_schedule: nil, filters: nil)
|
||||
working_schedules: [], filters: nil)
|
||||
super
|
||||
@allocation = allocation
|
||||
@project = project
|
||||
@allocation_kind = allocation_kind
|
||||
@form_values = form_values
|
||||
@overbooked_ranges = overbooked_ranges
|
||||
@working_schedule = working_schedule
|
||||
@working_schedules = working_schedules
|
||||
@filters = filters
|
||||
end
|
||||
|
||||
@@ -68,18 +68,31 @@ module ResourceAllocations
|
||||
end
|
||||
|
||||
def working_schedule?
|
||||
@working_schedule.present?
|
||||
@working_schedules.any?
|
||||
end
|
||||
|
||||
# One sentence chaining each schedule effective during the overbooked
|
||||
# span, e.g. "This user works Mon-Fri 8h until 03/31/2026, then Mon-Thu
|
||||
# 6h (80% available for project work)."
|
||||
def schedule_note
|
||||
schedule = @working_schedule.working_days_summary
|
||||
first, *rest = @working_schedules
|
||||
|
||||
if @working_schedule.availability_factor < 100
|
||||
t("resource_management.allocate_resource_dialog.overbooking.schedule_note_with_availability",
|
||||
schedule:, factor: @working_schedule.availability_factor)
|
||||
else
|
||||
t("resource_management.allocate_resource_dialog.overbooking.schedule_note", schedule:)
|
||||
segments = [schedule_summary(first)]
|
||||
rest.each do |schedule|
|
||||
segments << t("resource_management.allocate_resource_dialog.overbooking.schedule_change",
|
||||
date: helpers.format_date(schedule.valid_from - 1),
|
||||
schedule: schedule_summary(schedule))
|
||||
end
|
||||
|
||||
t("resource_management.allocate_resource_dialog.overbooking.schedule_note", schedule: segments.join(" "))
|
||||
end
|
||||
|
||||
def schedule_summary(schedule)
|
||||
summary = schedule.working_days_summary
|
||||
return summary if schedule.availability_factor >= 100
|
||||
|
||||
t("resource_management.allocate_resource_dialog.overbooking.schedule_availability",
|
||||
schedule: summary, factor: schedule.availability_factor)
|
||||
end
|
||||
|
||||
def capacity_summary(range)
|
||||
|
||||
+7
-4
@@ -107,7 +107,7 @@ module ::ResourceManagement
|
||||
form_values: submitted_allocation_params,
|
||||
filters: params[:filters],
|
||||
overbooked_ranges: ranges,
|
||||
working_schedule: working_schedule(allocation, ranges)
|
||||
working_schedules: working_schedules(allocation, ranges)
|
||||
)
|
||||
)
|
||||
replace_via_turbo_stream(
|
||||
@@ -166,10 +166,13 @@ module ::ResourceManagement
|
||||
UserWorkingHours.for_user(allocation.principal).exists?
|
||||
end
|
||||
|
||||
def working_schedule(allocation, ranges)
|
||||
return if ranges.empty?
|
||||
# The schedule note covers the span of the displayed overbooked ranges so
|
||||
# it explains the capacity figures the user actually sees.
|
||||
def working_schedules(allocation, ranges)
|
||||
return [] if ranges.empty?
|
||||
|
||||
availability(allocation).working_schedule(date: allocation.start_date)
|
||||
span = ranges.map(&:start_date).min..ranges.map(&:end_date).max
|
||||
availability(allocation).working_schedules(span)
|
||||
end
|
||||
|
||||
def availability(allocation)
|
||||
|
||||
@@ -86,13 +86,21 @@ module ResourceAllocations
|
||||
OverbookingAnalysis.new(calendar: calendar_for(work_items), items: work_items).call
|
||||
end
|
||||
|
||||
# The user's working schedule active on the given date, carrying the
|
||||
# per-day hours and the availability factor. Nil when no schedule is in
|
||||
# effect on that date.
|
||||
# The user's working schedules effective within the given date range, in
|
||||
# chronological order: the schedule active at the range start (if any)
|
||||
# followed by any schedules taking effect within the range. Empty when no
|
||||
# schedule is in effect at any point of the range.
|
||||
#
|
||||
# @return [UserWorkingHours, nil]
|
||||
def working_schedule(date:)
|
||||
UserWorkingHours.for_user(@user).valid_for_date(date)
|
||||
# @return [Array<UserWorkingHours>]
|
||||
def working_schedules(range)
|
||||
records = UserWorkingHours
|
||||
.for_user(@user)
|
||||
.where(valid_from: ..range.end)
|
||||
.order(:valid_from)
|
||||
.to_a
|
||||
active_index = records.rindex { |record| record.valid_from <= range.begin }
|
||||
|
||||
active_index.nil? ? records : records[active_index..]
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
@@ -80,8 +80,9 @@ en:
|
||||
description: >-
|
||||
The selected user, %{user}, would be allocated beyond their working hours during this period.
|
||||
hidden_work: Other allocations
|
||||
schedule_availability: "%{schedule} (%{factor}% available for project work)"
|
||||
schedule_change: until %{date}, then %{schedule}
|
||||
schedule_note: This user works %{schedule}.
|
||||
schedule_note_with_availability: This user works %{schedule} (%{factor}% available for project work).
|
||||
submit: Allocate overtime
|
||||
title: Do you want to allocate overtime to the user?
|
||||
submit: Allocate
|
||||
|
||||
@@ -377,6 +377,20 @@ RSpec.describe "ResourceAllocations requests",
|
||||
expect(response.body).to include("Mon-Fri 8h (80% available for project work)")
|
||||
end
|
||||
|
||||
it "lists each schedule with its effective dates when the schedule changes during the period" do
|
||||
# Switches to Mon-Fri 6h at 80% on the second day of the allocation.
|
||||
create(:user_working_hours, user: working_assignee, valid_from: Date.new(2026, 3, 3),
|
||||
monday: 360, tuesday: 360, wednesday: 360, thursday: 360, friday: 360,
|
||||
availability_factor: 80)
|
||||
|
||||
post project_resource_allocations_path(project), params: base_params, as: :turbo_stream
|
||||
|
||||
expect(response.body).to include(
|
||||
"Mon-Fri 8h until #{I18n.l(Date.new(2026, 3, 2))}, " \
|
||||
"then Mon-Fri 6h (80% available for project work)"
|
||||
)
|
||||
end
|
||||
|
||||
it "creates the allocation once confirmed" do
|
||||
expect do
|
||||
post project_resource_allocations_path(project),
|
||||
|
||||
+34
-13
@@ -160,23 +160,44 @@ RSpec.describe ResourceAllocations::Availability do
|
||||
end
|
||||
end
|
||||
|
||||
describe "#working_schedule" do
|
||||
it "returns the working hours effective on the given date" do
|
||||
expect(availability.working_schedule(date: monday))
|
||||
.to have_attributes(working_days_summary: "Mon-Fri 8h", availability_factor: 100)
|
||||
describe "#working_schedules" do
|
||||
it "returns the single schedule covering the whole range" do
|
||||
expect(availability.working_schedules(monday..friday))
|
||||
.to contain_exactly(have_attributes(working_days_summary: "Mon-Fri 8h"))
|
||||
end
|
||||
|
||||
it "is nil when the user has no working time configured" do
|
||||
it "includes schedules taking effect within the range, in order" do
|
||||
switched = create(:user_working_hours, user:, valid_from: tuesday)
|
||||
|
||||
expect(availability.working_schedules(monday..friday).last).to eq(switched)
|
||||
expect(availability.working_schedules(monday..friday).size).to eq(2)
|
||||
end
|
||||
|
||||
it "drops schedules superseded before the range starts" do
|
||||
create(:user_working_hours, user:, valid_from: Date.new(2024, 1, 1))
|
||||
|
||||
expect(availability.working_schedules(monday..friday))
|
||||
.to contain_exactly(have_attributes(valid_from: Date.new(2025, 1, 1)))
|
||||
end
|
||||
|
||||
it "excludes schedules taking effect after the range ends" do
|
||||
create(:user_working_hours, user:, valid_from: Date.new(2027, 1, 1))
|
||||
|
||||
expect(availability.working_schedules(monday..friday))
|
||||
.to contain_exactly(have_attributes(valid_from: Date.new(2025, 1, 1)))
|
||||
end
|
||||
|
||||
it "starts with a mid-range schedule when none is in effect at the range start" do
|
||||
newcomer = create(:user)
|
||||
starting = create(:user_working_hours, user: newcomer, valid_from: tuesday)
|
||||
|
||||
expect(described_class.new(user: newcomer).working_schedules(monday..friday)).to eq([starting])
|
||||
end
|
||||
|
||||
it "is empty when the user has no working time configured" do
|
||||
other = described_class.new(user: create(:user))
|
||||
|
||||
expect(other.working_schedule(date: monday)).to be_nil
|
||||
end
|
||||
|
||||
it "is nil when no schedule is in effect yet on that date" do
|
||||
future_user = create(:user)
|
||||
create(:user_working_hours, user: future_user, valid_from: Date.new(2027, 1, 1))
|
||||
|
||||
expect(described_class.new(user: future_user).working_schedule(date: monday)).to be_nil
|
||||
expect(other.working_schedules(monday..friday)).to be_empty
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user