diff --git a/.ameba.yml b/.ameba.yml index 77817000..ffcda9ec 100644 --- a/.ameba.yml +++ b/.ameba.yml @@ -40,3 +40,5 @@ Lint/NotNil: Documentation/DocumentationAdmonition: Enabled: false +Metrics/CyclomaticComplexity: + Enabled: false diff --git a/spec/commands/time_worked/week_spec.cr b/spec/commands/time_worked/week_spec.cr index c398a717..288e4874 100644 --- a/spec/commands/time_worked/week_spec.cr +++ b/spec/commands/time_worked/week_spec.cr @@ -252,6 +252,169 @@ describe TandaCLI::Commands::TimeWorked::Week do context.stdout.to_s.should eq(expected) end end + + it "Display assumes expected finish date for clock out if forgotten" do + WebMock + .stub(:get, endpoint(Regex.new("/shifts"))) + .to_return( + status: 200, + body: [ + build_shift( + id: 1, + start: Time.local(2024, 12, 23, 8, 30), + finish: nil, + break_start: Time.local(2024, 12, 23, 12), + break_finish: Time.local(2024, 12, 23, 12, 30) + ), + build_shift( + id: 2, + start: Time.local(2024, 12, 24, 8, 30), + finish: nil, + break_start: Time.local(2024, 12, 24, 12), + break_finish: Time.local(2024, 12, 24, 12, 30) + ), + ].to_json, + ) + + travel_to(Time.local(2024, 12, 24, 14)) do + context = run(["time_worked", "week", "--display"]) + + expected = <<-OUTPUT.gsub("", " ") + ⚠️ Warning: Missing finish time for Monday, assuming regular hours finish time + Time worked: 8 hours and 0 minutes + πŸ“… Monday, 23 Dec 2024 + πŸ•“ 8:30 am - 5:00 pm + 🚧 Pending + β˜•οΈ Breaks: + πŸ•“ 12:00 pm - 12:30 pm + ⏸️ 30 minutes + πŸ’° false + + Worked so far: 5 hours and 0 minutes + πŸ“… Tuesday, 24 Dec 2024 + πŸ•“ 8:30 am - + 🚧 Pending + β˜•οΈ Breaks: + πŸ•“ 12:00 pm - 12:30 pm + ⏸️ 30 minutes + πŸ’° false + + Time left today: 3 hours and 0 minutes + You can clock out at: 5:00 pm + + You've worked 13 hours and 0 minutes this week + + OUTPUT + + context.stdout.to_s.should eq(expected) + end + end + + it "Shows overtime if previous day filled and past expected finish" do + WebMock + .stub(:get, endpoint(Regex.new("/shifts"))) + .to_return( + status: 200, + body: [ + build_shift( + id: 1, + start: Time.local(2024, 12, 23, 8, 30), + finish: nil, + break_start: Time.local(2024, 12, 23, 12), + break_finish: Time.local(2024, 12, 23, 12, 30) + ), + build_shift( + id: 2, + start: Time.local(2024, 12, 24, 8, 30), + finish: nil, + break_start: Time.local(2024, 12, 24, 12), + break_finish: Time.local(2024, 12, 24, 12, 30) + ), + ].to_json, + ) + + travel_to(Time.local(2024, 12, 24, 19)) do + context = run(["time_worked", "week", "--display"]) + + expected = <<-OUTPUT.gsub("", " ") + ⚠️ Warning: Missing finish time for Monday, assuming regular hours finish time + Time worked: 8 hours and 0 minutes + πŸ“… Monday, 23 Dec 2024 + πŸ•“ 8:30 am - 5:00 pm + 🚧 Pending + β˜•οΈ Breaks: + πŸ•“ 12:00 pm - 12:30 pm + ⏸️ 30 minutes + πŸ’° false + + Worked so far: 10 hours and 0 minutes + πŸ“… Tuesday, 24 Dec 2024 + πŸ•“ 8:30 am - + 🚧 Pending + β˜•οΈ Breaks: + πŸ•“ 12:00 pm - 12:30 pm + ⏸️ 30 minutes + πŸ’° false + + Overtime this week: 2 hours and 0 minutes + Overtime since: 5:00 pm + + You've worked 18 hours and 0 minutes this week + + OUTPUT + + context.stdout.to_s.should eq(expected) + end + end + + it "Doesn't show time left or overtime if next day with assumed regular hours without breaks" do + WebMock + .stub(:get, endpoint(Regex.new("/shifts"))) + .to_return( + status: 200, + body: [ + build_shift( + id: 1, + start: Time.local(2024, 12, 23, 8, 30), + finish: nil, + break_start: nil, + break_finish: nil + ), + build_shift( + id: 2, + start: Time.local(2024, 12, 24, 8, 30), + finish: nil, + break_start: nil, + break_finish: nil + ), + ].to_json, + ) + + travel_to(Time.local(2024, 12, 25, 1)) do + context = run(["time_worked", "week", "--display"]) + + expected = <<-OUTPUT.gsub("", " ") + ⚠️ Warning: Missing finish time for Monday, assuming regular hours finish time + Time worked: 8 hours and 0 minutes + πŸ“… Monday, 23 Dec 2024 + πŸ•“ 8:30 am - 5:00 pm + 🚧 Pending + β˜•οΈ 30 minutes + + ⚠️ Warning: Missing finish time for Tuesday, assuming regular hours finish time + Time worked: 8 hours and 0 minutes + πŸ“… Tuesday, 24 Dec 2024 + πŸ•“ 8:30 am - 5:00 pm + 🚧 Pending + β˜•οΈ 30 minutes + + You've worked 16 hours and 0 minutes this week + + OUTPUT + + context.stdout.to_s.should eq(expected) + end + end end private def build_shift(id, start, finish, break_start, break_finish) @@ -265,17 +428,19 @@ private def build_shift(id, start, finish, break_start, break_finish) break_finish: break_finish.try(&.to_unix), break_length: 30, breaks: [ - { - id: 1, - selected_automatic_break_rule_id: nil, - shift_id: id, - start: break_start.try(&.to_unix), - finish: break_finish.try(&.to_unix), - length: 30, - paid: false, - updated_at: 1735259689, - }, - ], + if break_start || break_finish + { + id: 1, + selected_automatic_break_rule_id: nil, + shift_id: id, + start: break_start.try(&.to_unix), + finish: break_finish.try(&.to_unix), + length: 30, + paid: false, + updated_at: 1735259689, + } + end, + ].compact, finish: finish.try(&.to_unix), department_id: 1, sub_cost_centre: nil, diff --git a/src/tanda_cli/executors/time_worked/base.cr b/src/tanda_cli/executors/time_worked/base.cr index 97c36bd1..bf49fc3f 100644 --- a/src/tanda_cli/executors/time_worked/base.cr +++ b/src/tanda_cli/executors/time_worked/base.cr @@ -6,6 +6,7 @@ module TandaCLI module TimeWorked abstract class Base alias RegularHoursScheduleBreak = Configuration::Serialisable::Organisation::RegularHoursSchedule::Break + alias RegularHoursSchedule = Configuration::Serialisable::Organisation::RegularHoursSchedule def initialize(@context : Context, @display : Bool, @offset : Int32?); end @@ -23,7 +24,7 @@ module TandaCLI .select(&.visible?) end - private def calculate_time_worked(shifts : Array(Types::Shift)) : Tuple(Time::Span, Time::Span) + private def calculate_time_worked(shifts : Array(Types::Shift), regular_hours_schedules : Array(RegularHoursSchedule)? = nil) : Tuple(Time::Span, Time::Span) total_time_worked = Time::Span.zero total_leave_hours = Time::Span.zero @@ -42,7 +43,11 @@ module TandaCLI time_worked = shift.time_worked(treat_paid_breaks_as_unpaid) worked_so_far = shift.worked_so_far(treat_paid_breaks_as_unpaid) - print_shift(shift, time_worked, worked_so_far) if display? + if time_worked.nil? && shift.finish_time.nil? && regular_hours_schedules + time_worked = calculate_expected_time_worked(shift, treat_paid_breaks_as_unpaid, regular_hours_schedules) + end + + print_shift(shift, time_worked, worked_so_far, regular_hours_schedules) if display? total_time = time_worked || worked_so_far total_time_worked += total_time if total_time @@ -51,14 +56,24 @@ module TandaCLI {total_time_worked, total_leave_hours} end - private def print_shift(shift : Types::Shift, time_worked : Time::Span?, worked_so_far : Time::Span?) + private def print_shift(shift : Types::Shift, time_worked : Time::Span?, worked_so_far : Time::Span?, regular_hours_schedules : Array(RegularHoursSchedule)? = nil) if time_worked @context.display.puts "#{"Time worked:".colorize.white.bold} #{time_worked.hours} hours and #{time_worked.minutes} minutes" elsif worked_so_far @context.display.puts "#{"Worked so far:".colorize.white.bold} #{worked_so_far.hours} hours and #{worked_so_far.minutes} minutes" end - Representers::Shift.new(shift).display(@context.display) + expected_finish_time = nil + expected_break_length = nil + if shift.finish_time.nil? && regular_hours_schedules + schedule = regular_hours_schedules.find(&.day_of_week.==(shift.day_of_week)) + if schedule && shift.date.date != Utils::Time.now.date + expected_finish_time = Utils::Time.pretty_time(schedule.finish_time) + expected_break_length = schedule.break_length + end + end + + Representers::Shift.new(shift, expected_finish_time, expected_break_length).display(@context.display) @context.display.puts end @@ -73,6 +88,33 @@ module TandaCLI Representers::LeaveRequest::DailyBreakdown.new(breakdown, leave_request).display(@context.display) @context.display.puts end + + private def calculate_expected_time_worked(shift : Types::Shift, treat_paid_breaks_as_unpaid : Bool, regular_hours_schedules : Array(RegularHoursSchedule)) : Time::Span? + start_time = shift.start_time + return unless start_time + + schedule = regular_hours_schedules.find(&.day_of_week.==(shift.day_of_week)) + return unless schedule + return if shift.date.date == Utils::Time.now.date + + if display? + @context.display.puts "#{"⚠️ Warning:".colorize.yellow.bold} Missing finish time for #{shift.date.to_s("%A")}, assuming regular hours finish time" + end + + expected_finish = Time.local( + shift.date.year, + shift.date.month, + shift.date.day, + schedule.finish_time.hour, + schedule.finish_time.minute, + location: Utils::Time.location + ) + + actual_break_time = (treat_paid_breaks_as_unpaid ? shift.valid_breaks : shift.valid_breaks.reject(&.paid?)).sum(&.ongoing_length).minutes + expected_break_time = actual_break_time == 0.minutes ? schedule.break_length : 0.minutes + total_break_time = actual_break_time + expected_break_time + (expected_finish - start_time) - total_break_time + end end end end diff --git a/src/tanda_cli/executors/time_worked/week.cr b/src/tanda_cli/executors/time_worked/week.cr index c2c978b1..6fc5b7bd 100644 --- a/src/tanda_cli/executors/time_worked/week.cr +++ b/src/tanda_cli/executors/time_worked/week.cr @@ -17,7 +17,9 @@ module TandaCLI from ||= to.at_beginning_of_week(start_day) shifts = fetch_visible_shifts(from, to) - total_time_worked, total_leave_hours = calculate_time_worked(shifts) + organisation = @context.config.current_organisation! + regular_hours_schedules = organisation.regular_hours_schedules + total_time_worked, total_leave_hours = calculate_time_worked(shifts, regular_hours_schedules) if total_time_worked.zero? && total_leave_hours.zero? @context.display.puts "You haven't clocked in this week" else diff --git a/src/tanda_cli/representers/shift.cr b/src/tanda_cli/representers/shift.cr index 2da8c142..3f785d4a 100644 --- a/src/tanda_cli/representers/shift.cr +++ b/src/tanda_cli/representers/shift.cr @@ -8,27 +8,36 @@ require "../types/shift_break" module TandaCLI module Representers struct Shift < Base(Types::Shift) + def initialize(@object : Types::Shift, @expected_finish_time : String? = nil, @expected_break_length : Time::Span? = nil) + end + private def build_display(builder : Builder) builder << "πŸ“… #{@object.pretty_date}\n" pretty_start = @object.pretty_start_time - pretty_finish = @object.pretty_finish_time + pretty_finish = @object.pretty_finish_time || @expected_finish_time + pretty_finish = pretty_finish.colorize.yellow if pretty_finish && @expected_finish_time builder << "πŸ•“ #{pretty_start} - #{pretty_finish}\n" if pretty_start || pretty_finish builder << "🚧 #{@object.status}\n" - build_shift_breaks(builder) if @object.valid_breaks.present? + build_shift_breaks(builder) if @object.valid_breaks.present? || @expected_break_length build_notes(builder) if @object.notes.present? end private def build_shift_breaks(builder : Builder) - builder << "β˜•οΈ Breaks:\n".colorize.white.bold - valid_breaks = @object.valid_breaks - last_break_index = valid_breaks.size - 1 - - valid_breaks.sort_by(&.id).each_with_index do |shift_break, index| - ShiftBreak.new(shift_break).build(builder) - builder << '\n' if index != last_break_index + expected_break_length = @expected_break_length + + if (breaks = @object.valid_breaks).present? + builder << "β˜•οΈ Breaks:\n".colorize.white.bold + last_break_index = breaks.size - 1 + + breaks.sort_by(&.id).each_with_index do |shift_break, index| + ShiftBreak.new(shift_break).build(builder) + builder << '\n' if index != last_break_index + end + elsif expected_break_length && !expected_break_length.zero? + builder << "β˜•οΈ #{expected_break_length.total_minutes.to_i} minutes\n".colorize.yellow end end diff --git a/src/tanda_cli/types/shift.cr b/src/tanda_cli/types/shift.cr index eb94f21f..97819d3c 100644 --- a/src/tanda_cli/types/shift.cr +++ b/src/tanda_cli/types/shift.cr @@ -101,8 +101,9 @@ module TandaCLI def ongoing? : Bool return false unless start_time + return false unless finish_time.nil? - finish_time.nil? + date.date == Utils::Time.now.date end def ongoing_break? : Bool