diff --git a/spec/commands/clock_in/manual_spec.cr b/spec/commands/clock_in/manual_spec.cr new file mode 100644 index 00000000..d43f0518 --- /dev/null +++ b/spec/commands/clock_in/manual_spec.cr @@ -0,0 +1,56 @@ +require "../../spec_helper" + +describe TandaCLI::Commands::ClockIn::Manual do + it "Handles clocked out state asking for a time to clock in with" do + stub_shifts + + travel_to(Time.local(2024, 12, 28, 9)) + + stdin = build_stdin( + "9:00am", + "y" + ) + context = run(stdin) + + output = context.stdout.to_s + output.should contain("You are currently clocked out, what do you want to set as the clock in time?\n") + output.should contain("Is this correct?\n") + output.should contain("Success: Set clock in time to 2024-12-28 09:00:00 +00:00\n") + end + + it "Handles blank time input" do + stub_shifts + + stdin = build_stdin(" ") + context = run(stdin) + + output = context.stdout.to_s + output.should contain("You are currently clocked out, what do you want to set as the clock in time?\n") + output.should contain("Error: Input can't be blank!") + end + + it "Handles invalid time input" do + stub_shifts + + time_string = "invalid time" + stdin = build_stdin(time_string) + context = run(stdin) + + output = context.stdout.to_s + output.should contain("You are currently clocked out, what do you want to set as the clock in time?\n") + output.should contain("Error: \"#{time_string}\" is not a valid time!\n") + end +end + +private def run(stdin) + Command.run(["clockin", "manual"], stdin: stdin) +end + +private def stub_shifts(shifts = Array(NamedTuple()).new) + WebMock + .stub(:get, endpoint(Regex.new("/shifts"))) + .to_return( + status: 200, + body: shifts.to_json, + ) +end diff --git a/spec/spec_helper.cr b/spec/spec_helper.cr index 897b2f41..320653da 100644 --- a/spec/spec_helper.cr +++ b/spec/spec_helper.cr @@ -30,3 +30,13 @@ end def endpoint(regex : Regex) Regex.new("#{DEFAULT_BASE_URI}#{regex}") end + +def build_stdin(*lines : String) : IO + IO::Memory.new.tap do |stdin| + lines.each do |line| + stdin.puts line + end + + stdin.rewind + end +end diff --git a/src/tanda_cli/commands/clock_in.cr b/src/tanda_cli/commands/clock_in.cr index 4a563eed..96cc2a80 100644 --- a/src/tanda_cli/commands/clock_in.cr +++ b/src/tanda_cli/commands/clock_in.cr @@ -38,6 +38,7 @@ module TandaCLI add_commands( ClockIn::Start, ClockIn::Finish, + ClockIn::Manual, ClockIn::Break, ClockIn::Photo, ClockIn::Status, diff --git a/src/tanda_cli/commands/clock_in/manual.cr b/src/tanda_cli/commands/clock_in/manual.cr new file mode 100644 index 00000000..85f7789b --- /dev/null +++ b/src/tanda_cli/commands/clock_in/manual.cr @@ -0,0 +1,66 @@ +require "../../models/clock_in_status" + +module TandaCLI + module Commands + class ClockIn + class Manual < Commands::Base + required_scopes :timesheet + + def setup_ + @name = "manual" + @summary = @description = "Manually set shift time for clockin" + end + + def run_(arguments : Cling::Arguments, options : Cling::Options) : Nil + shifts = client.shifts(current.user.id, Utils::Time.now).or { |error| display.error!(error) } + status = Models::ClockInStatus.new(shifts).determine_status + + case status + in .clocked_out? + handle_clocked_out! + in .clocked_in? + puts "Clocked in" + in .break_started? + puts "Break started" + end + end + + private def handle_clocked_out! + message = "You are currently clocked out, what do you want to set as the clock in time?" + time = ask_for_and_parse_time!(message) + + # TODO: Actually create shift and set time + display.success("Set clock in time to #{time}") + end + + private def ask_for_and_parse_time!(message) : Time + time_string = input.request(message) + display.error!("Input can't be blank!") if time_string.nil? + + time = Utils::Time.parse?(time_string).try(&->time_to_day_time(Time)) + display.error!("\"#{time_string}\" is not a valid time!") if time.nil? + + input.request_and(message: "Is this correct?") do |user_input| + display.error!("Command aborted. Please try again.") if user_input != "y" + end + + time + end + + private def time_to_day_time(time : Time) : Time + now = Utils::Time.now + + Time.local( + now.year, + now.month, + now.day, + time.hour, + time.minute, + time.second, + location: now.location + ) + end + end + end + end +end diff --git a/src/tanda_cli/executors/clock_in.cr b/src/tanda_cli/executors/clock_in.cr index a99190dd..f4504d79 100644 --- a/src/tanda_cli/executors/clock_in.cr +++ b/src/tanda_cli/executors/clock_in.cr @@ -1,3 +1,4 @@ +require "../models/clock_in_status" require "../models/photo_path_parser" module TandaCLI @@ -106,22 +107,8 @@ module TandaCLI BreakStarted end - private def determine_status : ClockInStatus - if break_started? - ClockInStatus::BreakStarted - elsif clocked_in? - ClockInStatus::ClockedIn - else - ClockInStatus::ClockedOut - end - end - - private def break_started? : Bool - @shifts.any?(&.ongoing_break?) - end - - private def clocked_in? : Bool - @shifts.any? { |shift| shift.start_time && shift.finish_time.nil? } + private def determine_status : Models::ClockInStatus::Status + Models::ClockInStatus.new(@shifts).determine_status end private def validate_clockin_start! diff --git a/src/tanda_cli/input.cr b/src/tanda_cli/input.cr index 7fbdc700..de7ba3ca 100644 --- a/src/tanda_cli/input.cr +++ b/src/tanda_cli/input.cr @@ -21,7 +21,7 @@ module TandaCLI end retrieve_input(sensitive) do - gets.try(&.chomp).presence + @stdin.gets(chomp: true).presence end end @@ -36,7 +36,10 @@ module TandaCLI private def retrieve_input(sensitive : Bool, & : -> String?) : String? return yield unless sensitive - STDIN.noecho do + stdin = @stdin + return yield unless stdin.is_a?(IO::FileDescriptor) + + stdin.noecho do yield end end diff --git a/src/tanda_cli/models/clock_in_status.cr b/src/tanda_cli/models/clock_in_status.cr new file mode 100644 index 00000000..1a84fa55 --- /dev/null +++ b/src/tanda_cli/models/clock_in_status.cr @@ -0,0 +1,31 @@ +module TandaCLI + module Models + struct ClockInStatus + enum Status + ClockedIn + ClockedOut + BreakStarted + end + + def initialize(@shifts : Array(Types::Shift)); end + + def determine_status : Status + if break_started? + Status::BreakStarted + elsif clocked_in? + Status::ClockedIn + else + Status::ClockedOut + end + end + + private def break_started? : Bool + @shifts.any?(&.ongoing_break?) + end + + private def clocked_in? : Bool + @shifts.any? { |shift| shift.start_time && shift.finish_time.nil? } + end + end + end +end diff --git a/src/tanda_cli/utils/time.cr b/src/tanda_cli/utils/time.cr index 1bbd41ce..3c88ea37 100644 --- a/src/tanda_cli/utils/time.cr +++ b/src/tanda_cli/utils/time.cr @@ -35,6 +35,21 @@ module TandaCLI def location : ::Time::Location ::Time::Location.local end + + def parse?(time_string : String) : ::Time? + formats = { + "%l%P", # "9am", "12pm" + "%l:%M%P", # "1:30pm" + } + + formats + .each + .map do |format| + ::Time.parse(time_string, format, location) + rescue ::Time::Format::Error + end + .find(&.itself) + end end end end