Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .ameba.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,5 @@ Lint/NotNil:
Documentation/DocumentationAdmonition:
Enabled: false

Metrics/CyclomaticComplexity:
Enabled: false
187 changes: 176 additions & 11 deletions spec/commands/time_worked/week_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,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("<space>", " ")
⚠️ 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 -<space>
🚧 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("<space>", " ")
⚠️ 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 -<space>
🚧 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("<space>", " ")
⚠️ 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)
Expand All @@ -98,17 +261,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,
Expand Down
50 changes: 46 additions & 4 deletions src/tanda_cli/executors/time_worked/base.cr
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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

Expand All @@ -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
Expand All @@ -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)
end

private def print_leave(leave_request : Types::LeaveRequest, breakdown : Types::LeaveRequest::DailyBreakdown)
Expand All @@ -71,6 +86,33 @@ module TandaCLI

Representers::LeaveRequest::DailyBreakdown.new(breakdown, leave_request).display(@context.display)
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
Expand Down
4 changes: 3 additions & 1 deletion src/tanda_cli/executors/time_worked/week.cr
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
20 changes: 15 additions & 5 deletions src/tanda_cli/representers/shift.cr
Original file line number Diff line number Diff line change
Expand Up @@ -8,23 +8,33 @@ require "../types/shift_break"
module TandaCLI
module Representers
class 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 : String::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 : String::Builder)
builder << "☕️ Breaks:\n".colorize.white.bold
@object.valid_breaks.sort_by(&.id).each do |shift_break|
builder << ShiftBreak.new(shift_break).build
expected_break_length = @expected_break_length

if (breaks = @object.valid_breaks).present?
builder << "☕️ Breaks:\n".colorize.white.bold
breaks.sort_by(&.id).each do |shift_break|
builder << ShiftBreak.new(shift_break).build
end
elsif expected_break_length && !expected_break_length.zero?
builder << "☕️ #{expected_break_length.total_minutes.to_i} minutes".colorize.yellow
end
end

Expand Down
3 changes: 2 additions & 1 deletion src/tanda_cli/types/shift.cr
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down