From 8a1f9c546aefb4de3b66f954b2c51bc55a464543 Mon Sep 17 00:00:00 2001 From: Rob Sterner Date: Sat, 21 Sep 2024 20:01:16 -0400 Subject: [PATCH] feat: adds upsertWorkWeeks mutation --- app/graphql/mutations/upsert_work_weeks.rb | 31 +++ app/graphql/schema.graphql | 40 ++++ app/graphql/types/mutation_type.rb | 1 + .../staff_plan/work_weeks_input_object.rb | 8 + app/models/work_week.rb | 24 +++ .../mutations/upsert_work_weeks_spec.rb | 179 ++++++++++++++++++ spec/models/work_week_spec.rb | 8 + 7 files changed, 291 insertions(+) create mode 100644 app/graphql/mutations/upsert_work_weeks.rb create mode 100644 app/graphql/types/staff_plan/work_weeks_input_object.rb create mode 100644 spec/graphql/mutations/upsert_work_weeks_spec.rb diff --git a/app/graphql/mutations/upsert_work_weeks.rb b/app/graphql/mutations/upsert_work_weeks.rb new file mode 100644 index 00000000..895fd831 --- /dev/null +++ b/app/graphql/mutations/upsert_work_weeks.rb @@ -0,0 +1,31 @@ +module Mutations + class UpsertWorkWeeks < BaseMutation + description "Create or update a work week record for a StaffPlan user." + + # arguments passed to the `resolve` method + argument :assignment_id, ID, required: true, description: "The ID of the assignment this work week is being created for." + argument :work_weeks, [Types::StaffPlan::WorkWeeksInputObject], required: true, description: "Attributes for creating or updating a work week record for a StaffPlan user." + + # return type from the mutation + type Types::StaffPlan::AssignmentType + + def resolve(assignment_id:, work_weeks:) + current_company = context[:current_company] + + # try and find the assignment + assignment = current_company.assignments.find(assignment_id) + + unless assignment.user.memberships.active.exists?(company: current_company) + # if the assignment isn't for an active user, raise an error + raise GraphQL::ExecutionError, "User is not an active member of the company" + end + + work_weeks.each do |ww| + work_week = assignment.work_weeks.find_or_initialize_by(cweek: ww.cweek, year: ww.year) + work_week.update!(ww.to_h.slice(:estimated_hours, :actual_hours)) + end + + assignment + end + end +end diff --git a/app/graphql/schema.graphql b/app/graphql/schema.graphql index 67122fdb..6eb168d4 100644 --- a/app/graphql/schema.graphql +++ b/app/graphql/schema.graphql @@ -311,6 +311,21 @@ type Mutation { """ year: Int! ): WorkWeek! + + """ + Create or update a work week record for a StaffPlan user. + """ + upsertWorkWeeks( + """ + The ID of the assignment this work week is being created for. + """ + assignmentId: ID! + + """ + Attributes for creating or updating a work week record for a StaffPlan user. + """ + workWeeks: [WorkWeeksInputObject!]! + ): Assignment! } type Project { @@ -470,3 +485,28 @@ type WorkWeek { user: User! year: Int! } + +""" +Attributes for creating or updating a work week record for a StaffPlan user. +""" +input WorkWeeksInputObject { + """ + The actual hours for this work week. + """ + actualHours: Int + + """ + The calendar week number of the work week. + """ + cweek: Int! + + """ + The estimated hours for this work week. + """ + estimatedHours: Int + + """ + The calendar year of the work week. + """ + year: Int! +} diff --git a/app/graphql/types/mutation_type.rb b/app/graphql/types/mutation_type.rb index 0be1b024..aff423c8 100644 --- a/app/graphql/types/mutation_type.rb +++ b/app/graphql/types/mutation_type.rb @@ -4,6 +4,7 @@ module Types class MutationType < Types::BaseObject field :set_current_company, mutation: Mutations::SetCurrentCompany field :upsert_work_week, mutation: Mutations::UpsertWorkWeek + field :upsert_work_weeks, mutation: Mutations::UpsertWorkWeeks field :upsert_assignment, mutation: Mutations::UpsertAssignment field :upsert_project, mutation: Mutations::UpsertProject field :upsert_client, mutation: Mutations::UpsertClient diff --git a/app/graphql/types/staff_plan/work_weeks_input_object.rb b/app/graphql/types/staff_plan/work_weeks_input_object.rb new file mode 100644 index 00000000..fb7fc855 --- /dev/null +++ b/app/graphql/types/staff_plan/work_weeks_input_object.rb @@ -0,0 +1,8 @@ + +class Types::StaffPlan::WorkWeeksInputObject < Types::BaseInputObject + description "Attributes for creating or updating a work week record for a StaffPlan user." + argument :cweek, Int, required: true, description: "The calendar week number of the work week." + argument :year, Int, required: true, description: "The calendar year of the work week." + argument :estimated_hours, Int, required: false, description: "The estimated hours for this work week." + argument :actual_hours, Int, required: false, description: "The actual hours for this work week." +end diff --git a/app/models/work_week.rb b/app/models/work_week.rb index 281bff97..0d80553a 100644 --- a/app/models/work_week.rb +++ b/app/models/work_week.rb @@ -11,4 +11,28 @@ class WorkWeek < ApplicationRecord validates :year, presence: true, numericality: { only_integer: true, greater_than_or_equal_to: 2000, less_than_or_equal_to: 2200 } validates :estimated_hours, presence: true, numericality: { only_integer: true, greater_than_or_equal_to: 0, less_than_or_equal_to: 168 } validates :actual_hours, presence: true, numericality: { only_integer: true, greater_than_or_equal_to: 0, less_than_or_equal_to: 168 } + validate :no_future_actual_hours + + private + + def no_future_actual_hours + return if actual_hours_allowed? + + assign_attributes(actual_hours: 0) if actual_hours > 0 + end + + def year_zero? + year.blank? || year == 0 + end + + def cweek_zero? + cweek.blank? || cweek == 0 + end + + def actual_hours_allowed? + return false if year_zero? || cweek_zero? + + today = Date.today + today.year < year || (today.year == year && today.cweek >= cweek) + end end diff --git a/spec/graphql/mutations/upsert_work_weeks_spec.rb b/spec/graphql/mutations/upsert_work_weeks_spec.rb new file mode 100644 index 00000000..cd9afd14 --- /dev/null +++ b/spec/graphql/mutations/upsert_work_weeks_spec.rb @@ -0,0 +1,179 @@ +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe Mutations::UpsertWorkWeeks do + + context "when updating work weeks" do + it "updates the work weeks with valid params" do + query_string = <<-GRAPHQL + mutation($assignmentId: ID!, $workWeeks: [WorkWeeksInputObject!]!) { + upsertWorkWeeks(assignmentId: $assignmentId, workWeeks: $workWeeks) { + workWeeks { + cweek + year + actualHours + estimatedHours + } + } + } + GRAPHQL + + user = create(:user) + assignment = assignment_for_user(user:) + work_weeks = 5.times.map do |i| + date = Date.today - i.weeks + create(:work_week, :blank, assignment:, cweek: date.cweek, year: date.year) + end + + updated_work_weeks = work_weeks.map.with_index do |week, i| + { + cweek: week.cweek, + year: week.year, + actualHours: i * 5, + estimatedHours: i * 6 + } + end + + result = StaffplanReduxSchema.execute( + query_string, + context: { + current_user: user, + current_company: assignment.company + }, + variables: { + assignmentId: assignment.id, + workWeeks: updated_work_weeks + } + ) + + post_result = result["data"]["upsertWorkWeeks"]["workWeeks"] + + post_result.each do |result| + work_week = updated_work_weeks.detect do |uww| + uww[:cweek] == result["cweek"] && uww[:year] == result["year"] + end + + expect(work_week).to be_present + expect(work_week[:actualHours]).to eq(result["actualHours"]) + expect(work_week[:estimatedHours]).to eq(result["estimatedHours"]) + end + end + + it "fails if the assignment is not found" do + query_string = <<-GRAPHQL + mutation($assignmentId: ID!, $workWeeks: [WorkWeeksInputObject!]!) { + upsertWorkWeeks(assignmentId: $assignmentId, workWeeks: $workWeeks) { + workWeeks { + cweek + year + actualHours + estimatedHours + } + } + } + GRAPHQL + + user = create(:user) + work_weeks = Array(create(:work_week, :blank)).map do |ww| + { + cweek: ww.cweek, + year: ww.year, + actualHours: 5, + estimatedHours: 6 + } + end + + result = StaffplanReduxSchema.execute( + query_string, + context: { + current_user: user, + current_company: user.current_company + }, + variables: { + assignmentId: -1, + workWeeks: work_weeks + } + ) + + post_result = result["errors"] + expect(post_result.length).to eq(1) + expect(post_result.first["message"]).to eq("Assignment not found") + end + + it "fails if the current_user is not a member of the company that the assignment belongs to" do + query_string = <<-GRAPHQL + mutation($assignmentId: ID!, $workWeeks: [WorkWeeksInputObject!]!) { + upsertWorkWeeks(assignmentId: $assignmentId, workWeeks: $workWeeks) { + workWeeks { + actualHours + estimatedHours + } + } + } + GRAPHQL + + user = create(:user) + assignment = assignment_for_user(user:) + random_user = create(:user) + + result = StaffplanReduxSchema.execute( + query_string, + context: { + current_user: random_user, + current_company: random_user.current_company + }, + variables: { + assignmentId: assignment.id, + workWeeks: [{ + cweek: 15, + year: 2024, + actualHours: 5, + estimatedHours: 5 + }] + } + ) + + post_result = result["errors"] + expect(post_result.length).to eq(1) + expect(post_result.first["message"]).to eq("Assignment not found") + end + + it "fails if the user is not an active member of the company" do + query_string = <<-GRAPHQL + mutation($assignmentId: ID!, $workWeeks: [WorkWeeksInputObject!]!) { + upsertWorkWeeks(assignmentId: $assignmentId, workWeeks: $workWeeks) { + workWeeks { + actualHours + estimatedHours + } + } + } + GRAPHQL + + work_week = create(:work_week, :blank) + work_week.user.memberships.update_all(status: "inactive") + + result = StaffplanReduxSchema.execute( + query_string, + context: { + current_user: work_week.user, + current_company: work_week.company + }, + variables: { + assignmentId: work_week.assignment_id, + workWeeks: [{ + cweek: 14, + year: 2023, + actualHours: 5, + estimatedHours: 12 + }] + } + ) + + post_result = result["errors"] + expect(post_result.length).to eq(1) + expect(post_result.first["message"]).to eq("User is not an active member of the company") + end + end +end diff --git a/spec/models/work_week_spec.rb b/spec/models/work_week_spec.rb index b8b966a7..9434f3dd 100644 --- a/spec/models/work_week_spec.rb +++ b/spec/models/work_week_spec.rb @@ -13,6 +13,14 @@ it { should validate_numericality_of(:estimated_hours).only_integer.is_greater_than_or_equal_to(0).is_less_than_or_equal_to(168) } it { should validate_presence_of(:actual_hours) } it { should validate_numericality_of(:actual_hours).only_integer.is_greater_than_or_equal_to(0).is_less_than_or_equal_to(168) } + + it 'resets actual_hours values greater than 0 for future assignments' do + next_month = Date.today.next_month + work_week = create(:work_week, actual_hours: 1, year: next_month.year, cweek: next_month.cweek, assignment: create(:assignment)) + + expect(work_week).to be_valid + expect(work_week.reload.actual_hours).to eql(0) + end end context "associations" do