diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8a3dc86..897f759 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -6,6 +6,22 @@ jobs: backend: name: 'Rails API Tests' runs-on: ubuntu-latest + + services: + postgres: + image: postgres:latest + env: + POSTGRES_USER: postgres_user + POSTGRES_PASSWORD: testpassword + POSTGRES_DB: monitoring_sys_test + ports: + - 5432:5432 + options: >- + --health-cmd "pg_isready -U postgres" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + steps: - name: Checkout code uses: actions/checkout@v4 @@ -18,12 +34,26 @@ jobs: - name: Install dependencies run: | cd backend + gem install bundler bundle install - - name: Run Rails tests + - name: Set up environment variables + run: | + echo "DATABASE_HOST=localhost" >> $GITHUB_ENV + echo "DATABASE_USER=postgres_user" >> $GITHUB_ENV + echo "DATABASE_PASSWORD=testpassword" >> $GITHUB_ENV + + - name: Set up database + run: | + cd backend + RAILS_ENV=test rails db:create + RAILS_ENV=test rails db:migrate + RAILS_ENV=test rails db:seed + + - name: Run RSpec tests run: | cd backend - bin/rails test + RAILS_ENV=test bundle exec rspec frontend: name: 'Vue Frontend Tests' diff --git a/2-availability_management.png b/2-availability_management.png new file mode 100644 index 0000000..25b6b19 Binary files /dev/null and b/2-availability_management.png differ diff --git a/README.md b/README.md index df56d70..41a894f 100644 --- a/README.md +++ b/README.md @@ -26,17 +26,22 @@ ### Componentes #### Endpoints - Gestion de Turnos (Shifts) - 1st Dropdown (Services) - - GET /api/company_services + - GET /api/company_services + - [example response](frontend/src/mock/company_services.json) - 2nd Dropdown (Weeks) - GET /api/company_services/:id/weeks + - [example response](frontend/src/mock/weeks_service_a.json) - Engineers Table - GET /api/company_services/:id/engineers?week=YYYY-WW + - [example response](frontend/src/mock/engineers_a_w1.json) - Shifts Table - GET /api/company_services/:id/shifts?week=YYYY-WW + - [example response](frontend/src/mock/shifts_a_w1.json) #### Endpoints - Gestion de Disponibilidad (Availability) - Dropdowns anteriores (gestion de turnos) para el filtrado y llenado de semana - Boton Editar Disponibilidad: Consultar Disponibilidad de ingenieros - GET /api/company_services/:id/engineers/availability?week=YYYY-WW + - [example response](frontend/src/mock/eng_availability_a_w1.json.json) - Updates Engineer Availability - POST /api/company_services/:id/engineers/availability - week @@ -51,12 +56,66 @@ #### Modelos 1. Servicios monitoreados - - Bloques de 1h - - Horario establecido (grupo de bloques) -2. Semana -3. Engineer -4. Turno (bloques de 1 hora) -5. Asignacion (Relacion Ingeniero - Hora) + - Contrato: Fechas Establecidas +2. Engineer +3. CompanyServiceEngineer + - Asignar 3 ingenieros encargados del servicio durante el contrato. +4. Shift (Turno) - bloques de 1 hora + - Contrato: Horas por dia de semana establecidas (grupo de bloques) +5. EngineerShift + - Bloque asignado a ingeniero +6. Availability (Disponibilidad) - Must: engineer + +#### Modelos - Instancias Ejemplo +1. CompanyService + id: 1 + name: "Service A" + contract_start_date: "2024-08-01" + contract_end_date: "2024-08-31" + +2. Engineer + id: 1 + name:"Alice Smith" + color:"Bob Johnson" + +3. Shift + company_service:1 + engineer:(sin asignar) + week:"2024-32" + day:"Monday" + start_time:"2024-08-07 09:00:00" + end_time:"2024-08-07 10:00:00" + +4. Availability + engineer:1 + week:"2024-32" + day:"Monday" + start_time:"2024-08-07 09:00:00" + end_time:"2024-08-07 10:00:00" + available:true + +#### Modelos - Instancias Factory Bot +```ruby +# 1. CompanyService +FactoryBot.attributes_for :company_service +=> {:name=>"Farrell, Mohr and Haley", :contract_start_date=>Thu, 18 Jul 2024, :contract_end_date=>Tue, 20 Aug 2024} + +# 2. Engineer +FactoryBot.attributes_for :engineer +=> {:name=>"Russell Hermann", :color=>"#0c0d0d"} + +# 3. CompanyServiceEngineer +#FactoryBot.attributes_for :company_service_engineer + +# 4. Shift +FactoryBot.attributes_for :shift +=> {:week=>"2024-32", :day=>"Tuesday", :start_time=>"13:00", :end_time=>"18:00"} +# 5. EngineerShift + +# 6. Availability +FactoryBot.attributes_for :availability + +``` #### Arquitectura Frontend (Grafica Figma) - View @@ -65,6 +124,7 @@ - CompanyServiceApi.ts #### Arquitectura Backend (Grafica Figma) + ### Ejecución #### Ambiente de desarrollo - Se ha usado Devcontainer y docker-compose para facilitar el desarrollo usando contenedores y vscode @@ -77,12 +137,17 @@ - abrir command palette: ctrl + shift + p - Seleccionar: Reopen in container - Seleccionar: "Rails API Container" - - Dentro ejecutar: `rails s` + - Dentro ejecutar: + ```bash + rails db:setup + rails s -b 0.0.0.0 + ``` - Ejecutar el contenedor Vue Container: - abrir command palette: ctrl + shift + p - Seleccionar: Reopen in container - Seleccionar: "Vue Container" - - Dentro ejecutar: `yarn dev` + - Dentro ejecutar mocked: `yarn dev` + - (alternativa) Dentro ejecutar api: `yarn serve:api` - navegar a 0.0.0.0:8080 para empezar a usar la app - Ejecutar tests e2e: ```bash @@ -106,6 +171,8 @@ docker-compose -f .devcontainer/docker-compose.yml up ### Screenshots #### Ejecución ![figma-1](./1-shift_management.png) +![figma-2](./2-availability_management.png) + #### Figma ![figma-1](./shift-availability-management-figma.jpg) diff --git a/backend/app/controllers/application_controller.rb b/backend/app/controllers/application_controller.rb index 4ac8823..36885eb 100644 --- a/backend/app/controllers/application_controller.rb +++ b/backend/app/controllers/application_controller.rb @@ -1,2 +1,3 @@ class ApplicationController < ActionController::API + include JsonResponse end diff --git a/backend/app/controllers/company_services/engineers_controller.rb b/backend/app/controllers/company_services/engineers_controller.rb new file mode 100644 index 0000000..fcdf998 --- /dev/null +++ b/backend/app/controllers/company_services/engineers_controller.rb @@ -0,0 +1,9 @@ +module CompanyServices + class EngineersController < ApplicationController + def index + company_service = CompanyService.find(params[:company_service_id]) + week = params[:week] + @engineers = company_service.engineers + end + end +end \ No newline at end of file diff --git a/backend/app/controllers/company_services/shifts_controller.rb b/backend/app/controllers/company_services/shifts_controller.rb new file mode 100644 index 0000000..64ce919 --- /dev/null +++ b/backend/app/controllers/company_services/shifts_controller.rb @@ -0,0 +1,7 @@ +module CompanyServices + class ShiftsController < ApplicationController + def index + @shifts = FetchShiftsService.new(params[:company_service_id],params[:week]).call + end + end +end diff --git a/backend/app/controllers/concerns/.keep b/backend/app/controllers/concerns/.keep deleted file mode 100644 index e69de29..0000000 diff --git a/backend/app/controllers/concerns/json_response.rb b/backend/app/controllers/concerns/json_response.rb new file mode 100644 index 0000000..093368a --- /dev/null +++ b/backend/app/controllers/concerns/json_response.rb @@ -0,0 +1,26 @@ +module JsonResponse + extend ActiveSupport::Concern + + included do + rescue_from ActiveRecord::RecordNotFound, with: :render_not_found + rescue_from ActionController::RoutingError, with: :render_not_found + + before_action :ensure_json_request + before_action :set_default_format + + def render_not_found + render json: { error: 'Not Found' }, status: :not_found + end + + end + + private + def set_default_format + request.format = :json + end + + def ensure_json_request + return if request.format.json? + render json: { error: 'Not Acceptable' }, status: :not_acceptable + end +end diff --git a/backend/app/controllers/weeks_controller.rb b/backend/app/controllers/weeks_controller.rb new file mode 100644 index 0000000..e473e6b --- /dev/null +++ b/backend/app/controllers/weeks_controller.rb @@ -0,0 +1,5 @@ +class WeeksController < ApplicationController + def index + @weeks = WeekService.new(params[:company_service_id]).call + end +end diff --git a/backend/app/models/company_service.rb b/backend/app/models/company_service.rb index 3d16c72..5e1bb44 100644 --- a/backend/app/models/company_service.rb +++ b/backend/app/models/company_service.rb @@ -10,4 +10,7 @@ # updated_at :datetime not null # class CompanyService < ApplicationRecord + has_many :company_service_engineers, dependent: :destroy + has_many :engineers, through: :company_service_engineers + has_many :shifts, dependent: :destroy end diff --git a/backend/app/models/company_service_engineer.rb b/backend/app/models/company_service_engineer.rb new file mode 100644 index 0000000..1004d18 --- /dev/null +++ b/backend/app/models/company_service_engineer.rb @@ -0,0 +1,24 @@ +# == Schema Information +# +# Table name: company_service_engineers +# +# id :bigint not null, primary key +# company_service_id :bigint not null +# engineer_id :bigint not null +# created_at :datetime not null +# updated_at :datetime not null +# +class CompanyServiceEngineer < ApplicationRecord + belongs_to :company_service + belongs_to :engineer + + validate :limited_engineers + + private + + def limited_engineers + if company_service.present? && company_service.company_service_engineers.count >= 3 + errors.add(:base, "Cannot assign more than 3 engineers to a company service") + end + end +end diff --git a/backend/app/models/engineer.rb b/backend/app/models/engineer.rb new file mode 100644 index 0000000..95a3a56 --- /dev/null +++ b/backend/app/models/engineer.rb @@ -0,0 +1,14 @@ +# == Schema Information +# +# Table name: engineers +# +# id :bigint not null, primary key +# name :string +# color :string +# created_at :datetime not null +# updated_at :datetime not null +# +class Engineer < ApplicationRecord + has_many :company_service_engineers, dependent: :destroy + has_many :company_services, through: :company_service_engineers +end diff --git a/backend/app/models/shift.rb b/backend/app/models/shift.rb new file mode 100644 index 0000000..98ae105 --- /dev/null +++ b/backend/app/models/shift.rb @@ -0,0 +1,16 @@ +# == Schema Information +# +# Table name: shifts +# +# id :bigint not null, primary key +# company_service_id :bigint not null +# week :string +# day :string +# start_time :time +# end_time :time +# created_at :datetime not null +# updated_at :datetime not null +# +class Shift < ApplicationRecord + belongs_to :company_service +end diff --git a/backend/app/services/fetch_shifts_service.rb b/backend/app/services/fetch_shifts_service.rb new file mode 100644 index 0000000..a6237d8 --- /dev/null +++ b/backend/app/services/fetch_shifts_service.rb @@ -0,0 +1,60 @@ +class FetchShiftsService + def initialize(company_service_id, week) + @company_service = CompanyService.find(company_service_id) + @week = week + end + + def call + shifts_by_day.map do |day, shifts| + { + day: day, + dayLabel: formatted_day_label(day), + time_blocks: format_time_blocks(shifts) + } + end + end + + private + + def shifts_by_day + # "Monday" => [shifts] + @company_service.shifts + .where(week: @company_service.contract_start_week) + .group_by(&:day) + end + + def formatted_day_label(day) + current_year = Date.today.year + week_number = @week.split('-').last.to_i + day_index = Date::DAYNAMES.index(day.capitalize) + day_index = day_index == 0 ? 7 : day_index + date = Date.commercial( + current_year, + week_number, + day_index + ) + # I18n.l(date, format: "%A %d de %B", locale: I18n.locale) + I18n.l(date, format: :long, locale: :es) + end + + def format_time_blocks(shifts) + shifts.map do |shift| + { + start_time: shift.start_time.strftime("%H:%W"), + end_time: shift.end_time.strftime("%H:%W"), + amount_of_hours: ((shift.end_time - shift.start_time) / 1.hour).to_i, + engineer: nil #format_engineer(shift.engineer) + } + end + end + + def format_engineer(engineer) + return nil unless engineer.present? + + { + id: engineer.id, + name: engineer.name, + color: engineer.color + } + end +end diff --git a/backend/app/services/week_service.rb b/backend/app/services/week_service.rb new file mode 100644 index 0000000..a450d5d --- /dev/null +++ b/backend/app/services/week_service.rb @@ -0,0 +1,76 @@ +class WeekService + def initialize(company_service_id) + company_service = CompanyService.find(company_service_id) + @start_date = company_service.contract_start_date.to_date + @end_date = company_service.contract_end_date.to_date + end + + def call + current_date = Date.today + past_weeks = [] + future_weeks = [] + + # binding.pry + if date_in_contract?(@start_date, @end_date, current_date) + past_weeks = fetch_past(@start_date, current_date) + future_weeks = fetch_future(current_date, @end_date) + elsif current_date < @start_date + future_weeks = fetch_future(@start_date, @end_date, offset: true) + elsif current_date > @end_date + past_weeks = fetch_past(@start_date, @end_date, offset: true ) + end + { past: past_weeks, future: future_weeks } + end + + private + def date_in_contract?(start_date, end_date, date) + # service.contract_start_date <= date && date <= service.contract_end_date + (start_date..end_date).cover?(date) + end + + def week_identifier(date) + "#{date.year}-#{date.cweek}" + end + + def one_week(week_start, week_end) + { + id: week_identifier(week_start), + label: "Semana #{week_start.cweek} del #{week_start.year}", + start_date: week_start.strftime('%d/%m/%Y'), + end_date: week_end.strftime('%d/%m/%Y') + } + end + + def fetch_past(start_date_limit, end_date, offset: false) + first_monday = start_date_limit.beginning_of_week + weeks = [] + end_date = end_date.next_week if offset # include current if offset + mondays = [end_date.beginning_of_week] + while mondays.last > first_monday + week_start = mondays.last.last_week + week_end = week_start.end_of_week + weeks << one_week(week_start, week_end) + mondays << week_start + end + weeks + end + + def fetch_future(start_date, end_date_limit, offset: false) + weeks = [] + week_start = offset ? Date.today.beginning_of_week : start_date.beginning_of_week + weeks_limit = 5 + while weeks.count < weeks_limit && week_start <= end_date_limit + if offset && week_start < start_date.beginning_of_week + week_start = week_start.next_week + weeks_limit -= 1 + next + end + week_end = week_start.end_of_week + weeks << one_week(week_start, week_end) + week_start = week_start.next_week + end + + weeks + end + +end \ No newline at end of file diff --git a/backend/app/views/company_services/_company_service.json.jbuilder b/backend/app/views/company_services/_company_service.json.jbuilder index 2bf158a..8c8cd64 100644 --- a/backend/app/views/company_services/_company_service.json.jbuilder +++ b/backend/app/views/company_services/_company_service.json.jbuilder @@ -1,2 +1 @@ -json.extract! company_service, :id, :name, :contract_start_date, :contract_end_date, :created_at, :updated_at -json.url company_service_url(company_service, format: :json) +json.extract! company_service, :id, :name diff --git a/backend/app/views/company_services/engineers/_engineer.json.jbuilder b/backend/app/views/company_services/engineers/_engineer.json.jbuilder new file mode 100644 index 0000000..1a545fb --- /dev/null +++ b/backend/app/views/company_services/engineers/_engineer.json.jbuilder @@ -0,0 +1 @@ +json.extract! engineer, :id, :name, :color diff --git a/backend/app/views/company_services/engineers/index.json.jbuilder b/backend/app/views/company_services/engineers/index.json.jbuilder new file mode 100644 index 0000000..98e1159 --- /dev/null +++ b/backend/app/views/company_services/engineers/index.json.jbuilder @@ -0,0 +1,6 @@ +json.data do + json.array! @engineers, partial: "company_services/engineers/engineer", as: :engineer +end + +json.status 200 +json.statusText "OK" \ No newline at end of file diff --git a/backend/app/views/company_services/engineers/show.json.jbuilder b/backend/app/views/company_services/engineers/show.json.jbuilder new file mode 100644 index 0000000..e902769 --- /dev/null +++ b/backend/app/views/company_services/engineers/show.json.jbuilder @@ -0,0 +1 @@ +json.partial! "engineers/engineer", engineer: @engineer diff --git a/backend/app/views/company_services/shifts/_shift.json.jbuilder b/backend/app/views/company_services/shifts/_shift.json.jbuilder new file mode 100644 index 0000000..805e1bd --- /dev/null +++ b/backend/app/views/company_services/shifts/_shift.json.jbuilder @@ -0,0 +1 @@ +json.extract! shift, :day, :dayLabel, :time_blocks \ No newline at end of file diff --git a/backend/app/views/company_services/shifts/index.json.jbuilder b/backend/app/views/company_services/shifts/index.json.jbuilder new file mode 100644 index 0000000..b41ffa8 --- /dev/null +++ b/backend/app/views/company_services/shifts/index.json.jbuilder @@ -0,0 +1,6 @@ +json.data do + json.array! @shifts, partial: "company_services/shifts/shift", as: :shift +end + +json.status 200 +json.statusText "OK" \ No newline at end of file diff --git a/backend/app/views/company_services/shifts/show.json.jbuilder b/backend/app/views/company_services/shifts/show.json.jbuilder new file mode 100644 index 0000000..74930f6 --- /dev/null +++ b/backend/app/views/company_services/shifts/show.json.jbuilder @@ -0,0 +1 @@ +json.partial! "shifts/shift", shift: @shift diff --git a/backend/app/views/weeks/_week.json.jbuilder b/backend/app/views/weeks/_week.json.jbuilder new file mode 100644 index 0000000..ef864ac --- /dev/null +++ b/backend/app/views/weeks/_week.json.jbuilder @@ -0,0 +1 @@ +json.extract! week, :id, :label, :start_date, :end_date \ No newline at end of file diff --git a/backend/app/views/weeks/index.json.jbuilder b/backend/app/views/weeks/index.json.jbuilder new file mode 100644 index 0000000..3b4ed56 --- /dev/null +++ b/backend/app/views/weeks/index.json.jbuilder @@ -0,0 +1,11 @@ +json.data do + json.past do + json.array! @weeks[:past], partial: "weeks/week", as: :week + end + json.future do + json.array! @weeks[:future], partial: "weeks/week", as: :week + end +end + +json.status 200 +json.statusText "OK" \ No newline at end of file diff --git a/backend/config/locales/es.yml b/backend/config/locales/es.yml new file mode 100644 index 0000000..bce98ee --- /dev/null +++ b/backend/config/locales/es.yml @@ -0,0 +1,30 @@ +es: + date: + day_names: + - Domingo + - Lunes + - Martes + - Miércoles + - Jueves + - Viernes + - Sábado + month_names: + - Enero + - Enero + - Febrero + - Marzo + - Abril + - Mayo + - Junio + - Julio + - Agosto + - Septiembre + - Octubre + - Noviembre + - Diciembre + formats: + default: "%d/%m/%Y" + long: "%A %d de %B" + time: + am: "AM" + pm: "PM" diff --git a/backend/config/routes.rb b/backend/config/routes.rb index 0d0947a..f41770e 100644 --- a/backend/config/routes.rb +++ b/backend/config/routes.rb @@ -1,11 +1,11 @@ Rails.application.routes.draw do - resources :company_services - # Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html + resources :company_services, only: [:index], constraints: { format: 'json' } do + resources :weeks, only: [:index] + resources :engineers, only: [:index], module: :company_services + resources :shifts, only: [:index], module: :company_services + end - # Reveal health status on /up that returns 200 if the app boots with no exceptions, otherwise 500. - # Can be used by load balancers and uptime monitors to verify that the app is live. get "up" => "rails/health#show", as: :rails_health_check - # Defines the root path route ("/") - # root "posts#index" + match '*unmatched', to: 'application#render_not_found', via: :all end diff --git a/backend/db/migrate/20240809024958_create_engineers.rb b/backend/db/migrate/20240809024958_create_engineers.rb new file mode 100644 index 0000000..dc147bb --- /dev/null +++ b/backend/db/migrate/20240809024958_create_engineers.rb @@ -0,0 +1,10 @@ +class CreateEngineers < ActiveRecord::Migration[7.1] + def change + create_table :engineers do |t| + t.string :name + t.string :color + + t.timestamps + end + end +end diff --git a/backend/db/migrate/20240809191347_create_company_service_engineers.rb b/backend/db/migrate/20240809191347_create_company_service_engineers.rb new file mode 100644 index 0000000..c752796 --- /dev/null +++ b/backend/db/migrate/20240809191347_create_company_service_engineers.rb @@ -0,0 +1,10 @@ +class CreateCompanyServiceEngineers < ActiveRecord::Migration[7.1] + def change + create_table :company_service_engineers do |t| + t.references :company_service, null: false, foreign_key: true + t.references :engineer, null: false, foreign_key: true + + t.timestamps + end + end +end diff --git a/backend/db/migrate/20240809212458_create_shifts.rb b/backend/db/migrate/20240809212458_create_shifts.rb new file mode 100644 index 0000000..33706e7 --- /dev/null +++ b/backend/db/migrate/20240809212458_create_shifts.rb @@ -0,0 +1,13 @@ +class CreateShifts < ActiveRecord::Migration[7.1] + def change + create_table :shifts do |t| + t.references :company_service, null: false, foreign_key: true + t.string :week + t.string :day + t.time :start_time + t.time :end_time + + t.timestamps + end + end +end diff --git a/backend/db/migrate/20240810215412_add_contract_weeks_to_company_services.rb b/backend/db/migrate/20240810215412_add_contract_weeks_to_company_services.rb new file mode 100644 index 0000000..f2b0c3a --- /dev/null +++ b/backend/db/migrate/20240810215412_add_contract_weeks_to_company_services.rb @@ -0,0 +1,6 @@ +class AddContractWeeksToCompanyServices < ActiveRecord::Migration[7.1] + def change + add_column :company_services, :contract_start_week, :string + add_column :company_services, :contract_end_week, :string + end +end diff --git a/backend/db/schema.rb b/backend/db/schema.rb index 8ea75aa..4ea7bae 100644 --- a/backend/db/schema.rb +++ b/backend/db/schema.rb @@ -10,16 +10,48 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.1].define(version: 2024_08_02_031906) do +ActiveRecord::Schema[7.1].define(version: 2024_08_10_215412) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" + create_table "company_service_engineers", force: :cascade do |t| + t.bigint "company_service_id", null: false + t.bigint "engineer_id", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["company_service_id"], name: "index_company_service_engineers_on_company_service_id" + t.index ["engineer_id"], name: "index_company_service_engineers_on_engineer_id" + end + create_table "company_services", force: :cascade do |t| t.string "name" t.datetime "contract_start_date" t.datetime "contract_end_date" t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.string "contract_start_week" + t.string "contract_end_week" + end + + create_table "engineers", force: :cascade do |t| + t.string "name" + t.string "color" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + + create_table "shifts", force: :cascade do |t| + t.bigint "company_service_id", null: false + t.string "week" + t.string "day" + t.time "start_time" + t.time "end_time" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["company_service_id"], name: "index_shifts_on_company_service_id" end + add_foreign_key "company_service_engineers", "company_services" + add_foreign_key "company_service_engineers", "engineers" + add_foreign_key "shifts", "company_services" end diff --git a/backend/db/seeds.rb b/backend/db/seeds.rb index a32107f..248d7c7 100644 --- a/backend/db/seeds.rb +++ b/backend/db/seeds.rb @@ -1,9 +1,35 @@ require 'faker' -10.times do - company_service = CompanyService.create( - name: Faker::Company.unique.name, - contract_start_date: Faker::Date.backward(days: 30), - contract_end_date: Faker::Date.forward(days: 30) - ) -end \ No newline at end of file +10.times { FactoryBot.create(:company_service) } +puts "10 Company Services Created" + +10.times { FactoryBot.create(:engineer) } +puts "10 Engineers Created" + +# Assign 3 random engineers to each CompanyService and create shifts +CompanyService.all.each do |company_service| + # Select 3 random engineers + selected_engineers = Engineer.all.sample(3) + + # Assign selected engineers to a CompanyService + selected_engineers.each do |engineer| + CompanyServiceEngineer.create!(company_service: company_service, engineer: engineer) + end + puts "Assigned 3 Engineers to #{company_service.name}" + + puts "Get 1st week of #{company_service.name}" + # Get the start and end weeks from the company_service + start_week = company_service.contract_start_week + # end_week = company_service.contract_end_week + puts "Create Shift Week for #{company_service.name}" + week_days = %w[Monday Tuesday Wednesday Thursday Friday Saturday Sunday] + start_time = Time.parse("09:00") + + week_days.each do |day| + FactoryBot.create(:shift, + company_service: company_service, + week: start_week, + day: day) + start_time += 1.day + end +end diff --git a/backend/spec/factories/company_service_engineers.rb b/backend/spec/factories/company_service_engineers.rb new file mode 100644 index 0000000..b212fbc --- /dev/null +++ b/backend/spec/factories/company_service_engineers.rb @@ -0,0 +1,16 @@ +# == Schema Information +# +# Table name: company_service_engineers +# +# id :bigint not null, primary key +# company_service_id :bigint not null +# engineer_id :bigint not null +# created_at :datetime not null +# updated_at :datetime not null +# +FactoryBot.define do + factory :company_service_engineer do + company_service + engineer + end +end diff --git a/backend/spec/factories/company_services.rb b/backend/spec/factories/company_services.rb index 235c287..82c293d 100644 --- a/backend/spec/factories/company_services.rb +++ b/backend/spec/factories/company_services.rb @@ -11,8 +11,14 @@ # FactoryBot.define do factory :company_service do - name { "MyString" } - contract_start_date { "2024-08-02 03:19:06" } - contract_end_date { "2024-08-02 03:19:06" } + name { Faker::Company.unique.name } + contract_start_date { Faker::Date.between(from: '2024-07-01', to: '2024-08-31').beginning_of_week } + contract_end_date { Faker::Date.between( + from: contract_start_date, + to: '2024-10-31').end_of_week + } + contract_start_week { "#{contract_start_date.year}-#{contract_start_date.cweek}" } + contract_end_week { "#{contract_end_date.year}-#{contract_end_date.cweek}" } end end +# improve: company_service defien contract_start_week and end_week to avoid doing calculations on date level. diff --git a/backend/spec/factories/engineers.rb b/backend/spec/factories/engineers.rb new file mode 100644 index 0000000..2168ad6 --- /dev/null +++ b/backend/spec/factories/engineers.rb @@ -0,0 +1,23 @@ +# == Schema Information +# +# Table name: engineers +# +# id :bigint not null, primary key +# name :string +# color :string +# created_at :datetime not null +# updated_at :datetime not null +# +FactoryBot.define do + factory :engineer do + name { Faker::Name.name } + # color { Faker::Color.hex_color } + color do + # Generate a pastel color by keeping RGB values high and saturation low + r = rand(200..255) + g = rand(200..255) + b = rand(200..255) + "#%02x%02x%02x" % [r, g, b] + end + end +end diff --git a/backend/spec/factories/shifts.rb b/backend/spec/factories/shifts.rb new file mode 100644 index 0000000..c92fdd5 --- /dev/null +++ b/backend/spec/factories/shifts.rb @@ -0,0 +1,41 @@ +# == Schema Information +# +# Table name: shifts +# +# id :bigint not null, primary key +# company_service_id :bigint not null +# week :string +# day :string +# start_time :time +# end_time :time +# created_at :datetime not null +# updated_at :datetime not null + +FactoryBot.define do + factory :shift do + company_service + + week { "#{Date.today.year}-#{Date.today.cweek}" } + day { %w[Monday Tuesday Wednesday Thursday Friday Saturday Sunday].sample } + + start_time { + Faker::Time.between( + from: DateTime.now.change(hour: 04, min: 00), + to: DateTime.now.change(hour: 20, min: 00) + ).change(min: 0) + .strftime("%H:%M") + } + end_time do + start_time_time = Time.parse(start_time) + plus_one_hour = start_time_time + 1.hour + midnight = start_time_time.end_of_day + end_time_range = (plus_one_hour)..(midnight) + + Faker::Time.between( + from: end_time_range.first, + to: end_time_range.last + ).change(min: 0) + .strftime("%H:%M") + end + end +end \ No newline at end of file diff --git a/backend/spec/models/company_service_engineer_spec.rb b/backend/spec/models/company_service_engineer_spec.rb new file mode 100644 index 0000000..1de3e61 --- /dev/null +++ b/backend/spec/models/company_service_engineer_spec.rb @@ -0,0 +1,25 @@ +require 'rails_helper' + +RSpec.describe CompanyServiceEngineer, type: :model do + describe 'associations' do + it { should belong_to(:company_service) } + it { should belong_to(:engineer) } + end + + describe 'validations' do + it 'validates that no more than 3 engineers can be assigned to a company service' do + company_service = create(:company_service) + engineers = create_list(:engineer, 3) + + engineers.each do |engineer| + CompanyServiceEngineer.create!(company_service: company_service, engineer: engineer) + end + + fourth_engineer = create(:engineer) + company_service_engineer = CompanyServiceEngineer.new(company_service: company_service, engineer: fourth_engineer) + + expect(company_service_engineer.valid?).to be_falsey + expect(company_service_engineer.errors.full_messages).to include("Cannot assign more than 3 engineers to a company service") + end + end +end diff --git a/backend/spec/models/company_service_spec.rb b/backend/spec/models/company_service_spec.rb index 469fcca..42d990d 100644 --- a/backend/spec/models/company_service_spec.rb +++ b/backend/spec/models/company_service_spec.rb @@ -12,5 +12,8 @@ require 'rails_helper' RSpec.describe CompanyService, type: :model do - pending "add some examples to (or delete) #{__FILE__}" + describe 'associations' do + it { should have_many(:company_service_engineers).dependent(:destroy) } + it { should have_many(:engineers).through(:company_service_engineers) } + end end diff --git a/backend/spec/models/engineer_spec.rb b/backend/spec/models/engineer_spec.rb new file mode 100644 index 0000000..3d77aad --- /dev/null +++ b/backend/spec/models/engineer_spec.rb @@ -0,0 +1,8 @@ +require 'rails_helper' + +RSpec.describe Engineer, type: :model do + describe 'associations' do + it { should have_many(:company_service_engineers).dependent(:destroy) } + it { should have_many(:company_services).through(:company_service_engineers) } + end +end diff --git a/backend/spec/requests/company_services_spec.rb b/backend/spec/requests/company_services_spec.rb index e443de0..48bdd03 100644 --- a/backend/spec/requests/company_services_spec.rb +++ b/backend/spec/requests/company_services_spec.rb @@ -1,127 +1,55 @@ require 'rails_helper' -# This spec was generated by rspec-rails when you ran the scaffold generator. -# It demonstrates how one might use RSpec to test the controller code that -# was generated by Rails when you ran the scaffold generator. -# -# It assumes that the implementation code is generated by the rails scaffold -# generator. If you are using any extension libraries to generate different -# controller code, this generated spec may or may not pass. -# -# It only uses APIs available in rails and/or rspec-rails. There are a number -# of tools you can use to make these specs even more expressive, but we're -# sticking to rails and rspec-rails APIs to keep things simple and stable. - RSpec.describe "/company_services", type: :request do - # This should return the minimal set of attributes required to create a valid - # CompanyService. As you add validations to CompanyService, be sure to - # adjust the attributes here as well. - let(:valid_attributes) { - skip("Add a hash of attributes valid for your model") - } - - let(:invalid_attributes) { - skip("Add a hash of attributes invalid for your model") - } + let!(:service_a) { create(:company_service)} + let!(:service_b) { create(:company_service)} + let!(:service_c) { create(:company_service)} - # This should return the minimal set of values that should be in the headers - # in order to pass any filters (e.g. authentication) defined in - # CompanyServicesController, or in your router and rack - # middleware. Be sure to keep this updated too. let(:valid_headers) { - {} + {"Content-Type" => "application/json"} } - describe "GET /index" do - it "renders a successful response" do - CompanyService.create! valid_attributes - get company_services_url, headers: valid_headers, as: :json - expect(response).to be_successful - end - end - - describe "GET /show" do - it "renders a successful response" do - company_service = CompanyService.create! valid_attributes - get company_service_url(company_service), as: :json - expect(response).to be_successful - end - end - - describe "POST /create" do - context "with valid parameters" do - it "creates a new CompanyService" do - expect { - post company_services_url, - params: { company_service: valid_attributes }, headers: valid_headers, as: :json - }.to change(CompanyService, :count).by(1) - end - - it "renders a JSON response with the new company_service" do - post company_services_url, - params: { company_service: valid_attributes }, headers: valid_headers, as: :json - expect(response).to have_http_status(:created) - expect(response.content_type).to match(a_string_including("application/json")) - end - end - - context "with invalid parameters" do - it "does not create a new CompanyService" do - expect { - post company_services_url, - params: { company_service: invalid_attributes }, as: :json - }.to change(CompanyService, :count).by(0) + describe "GET /company_services" do + describe 'Response format' do + context "when format is json" do + it "returns a successful response" do + get company_services_url, headers: valid_headers, as: :json + expect(response).to be_successful + expect(response.content_type).to eq("application/json; charset=utf-8") + end end - it "renders a JSON response with errors for the new company_service" do - post company_services_url, - params: { company_service: invalid_attributes }, headers: valid_headers, as: :json - expect(response).to have_http_status(:unprocessable_entity) - expect(response.content_type).to match(a_string_including("application/json")) + context "when format is not json" do + it "returns a not acceptable response" do + get company_services_url, headers: valid_headers, as: :html + expect(response).to have_http_status(:not_acceptable) + end end end - end - - describe "PATCH /update" do - context "with valid parameters" do - let(:new_attributes) { - skip("Add a hash of attributes valid for your model") - } - it "updates the requested company_service" do - company_service = CompanyService.create! valid_attributes - patch company_service_url(company_service), - params: { company_service: new_attributes }, headers: valid_headers, as: :json - company_service.reload - skip("Add assertions for updated state") + describe 'Response structure' do + it 'returns the correct JSON structure' do + get company_services_url, headers: valid_headers, as: :json + json_response = JSON.parse(response.body) + + expect(json_response).to have_key('data') + expect(json_response['data'].length).to eq(3) + expect(json_response['data']).to match_array([ + { 'id' => service_a.id, 'name' => service_a.name }, + { 'id' => service_b.id, 'name' => service_b.name }, + { 'id' => service_c.id, 'name' => service_c.name } + ]) + expect(json_response).to have_key('status') + expect(json_response).to have_key('statusText') end - it "renders a JSON response with the company_service" do - company_service = CompanyService.create! valid_attributes - patch company_service_url(company_service), - params: { company_service: new_attributes }, headers: valid_headers, as: :json - expect(response).to have_http_status(:ok) - expect(response.content_type).to match(a_string_including("application/json")) - end - end + it 'returns status 200' do + get company_services_url, headers: valid_headers, as: :json + json_response = JSON.parse(response.body) - context "with invalid parameters" do - it "renders a JSON response with errors for the company_service" do - company_service = CompanyService.create! valid_attributes - patch company_service_url(company_service), - params: { company_service: invalid_attributes }, headers: valid_headers, as: :json - expect(response).to have_http_status(:unprocessable_entity) - expect(response.content_type).to match(a_string_including("application/json")) + expect(json_response['status']).to eq(200) + expect(json_response['statusText']).to eq('OK') end end end - - describe "DELETE /destroy" do - it "destroys the requested company_service" do - company_service = CompanyService.create! valid_attributes - expect { - delete company_service_url(company_service), headers: valid_headers, as: :json - }.to change(CompanyService, :count).by(-1) - end - end end diff --git a/backend/spec/requests/engineers_spec.rb b/backend/spec/requests/engineers_spec.rb new file mode 100644 index 0000000..553ee64 --- /dev/null +++ b/backend/spec/requests/engineers_spec.rb @@ -0,0 +1,47 @@ +require 'rails_helper' + +RSpec.describe "CompanyServices::Engineers", type: :request do + let!(:company_service) { create(:company_service) } + let(:week) { "2024-32" } + + let(:valid_headers) { + {"Content-Type" => "application/json"} + } + + before do + @engineer1 = create(:engineer, name: "Engineer 1", color: "#a5b4fc") + @engineer2 = create(:engineer, name: "Engineer 2", color: "#5eead4") + @engineer3 = create(:engineer, name: "Engineer 3", color: "#bef264") + + CompanyServiceEngineer.create!(company_service: company_service, engineer: @engineer1) + CompanyServiceEngineer.create!(company_service: company_service, engineer: @engineer2) + CompanyServiceEngineer.create!(company_service: company_service, engineer: @engineer3) + + end + + describe "GET /company_services/:company_service_id/engineers?week=YYYY-WW" do + it "renders a successful response" do + get company_service_engineers_url(company_service_id: company_service.id, week: week), + headers: valid_headers, + as: :json + expect(response).to be_successful + end + it 'returns the correct JSON structure' do + get company_service_engineers_url(company_service_id: company_service.id, week: week), + headers: valid_headers, + as: :json + json_response = JSON.parse(response.body) + + expect(json_response).to have_key('data') + expect(json_response['data'].length).to eq(3) + expect(json_response['data']).to match_array([ + { 'id' => @engineer1.id, 'name' => @engineer1.name, 'color' => @engineer1.color }, + { 'id' => @engineer2.id, 'name' => @engineer2.name, 'color' => @engineer2.color }, + { 'id' => @engineer3.id, 'name' => @engineer3.name, 'color' => @engineer3.color } + ]) + expect(json_response).to have_key('status') + expect(json_response).to have_key('statusText') + end + end + +end diff --git a/backend/spec/requests/errors_spec.rb b/backend/spec/requests/errors_spec.rb new file mode 100644 index 0000000..fef786d --- /dev/null +++ b/backend/spec/requests/errors_spec.rb @@ -0,0 +1,20 @@ +require 'rails_helper' + +RSpec.describe "Error Handling", type: :request do + describe 'GET /unknown' do + context "when format is json" do + it 'returns a 404 Not Found' do + get '/unknown', as: :json + expect(response).to have_http_status(:not_found) + expect(response.body).to include('Not Found') + end + end + + context "when format is not json" do + it 'returns a not acceptable response' do + get '/unknown', as: :html + expect(response).to have_http_status(:not_acceptable) + end + end + end +end diff --git a/backend/spec/requests/shifts_spec.rb b/backend/spec/requests/shifts_spec.rb new file mode 100644 index 0000000..b12d269 --- /dev/null +++ b/backend/spec/requests/shifts_spec.rb @@ -0,0 +1,55 @@ +require 'rails_helper' + +RSpec.describe "CompanyServices::Shifts", type: :request do + let!(:company_service) { create(:company_service, + contract_start_date: Date.parse('2024-08-05'), + contract_end_date: Date.parse('2024-08-11')) } + let(:week) { company_service.contract_start_week } + + let(:valid_headers) { + {"Content-Type" => "application/json"} + } + before do + create(:shift, company_service: company_service, week: week, day: "Monday", start_time: "09:00", end_time: "10:00") + create(:shift, company_service: company_service, week: week, day: "Monday", start_time: "10:00", end_time: "11:00") + + create(:shift, company_service: company_service, week: week, day: "Tuesday", start_time: "18:00", end_time: "19:00") + create(:shift, company_service: company_service, week: week, day: "Tuesday", start_time: "20:00", end_time: "21:00") + + I18n.locale = :es + get company_service_shifts_url(company_service_id: company_service.id, week: week), + headers: valid_headers, + as: :json + end + + describe "GET /index" do + it "renders a successful response" do + expect(response).to be_successful + end + end + + describe 'JSON response' do + it 'returns the correct JSON structure' do + + json_response = JSON.parse(response.body) + + expect(json_response['status']).to eq(200) + expect(json_response['statusText']).to eq("OK") + expect(json_response['data']).to be_an(Array) + + # Verify structure for Monday + monday_shifts = json_response['data'].find { |d| d['day'] == "Monday" } + expect(monday_shifts['dayLabel']).to eq("Lunes 05 de Agosto") + expect(monday_shifts['time_blocks'].count).to eq(2) + expect(monday_shifts['time_blocks'][0]['engineer']).to be_nil + expect(monday_shifts['time_blocks'][1]['engineer']).to be_nil + + # Verify that an unassigned shift has a nil engineer + tuesday_shifts = json_response['data'].find { |d| d['day'] == "Tuesday" } + expect(tuesday_shifts['time_blocks'][0]['engineer']).to be_nil + expect(tuesday_shifts['time_blocks'][1]['engineer']).to be_nil + end + + end + +end diff --git a/backend/spec/requests/weeks_spec.rb b/backend/spec/requests/weeks_spec.rb new file mode 100644 index 0000000..72dc3eb --- /dev/null +++ b/backend/spec/requests/weeks_spec.rb @@ -0,0 +1,48 @@ +require 'rails_helper' + +RSpec.describe "Weeks", type: :request do + let!(:company_service) { create(:company_service) } + + let(:valid_headers) { + {"Content-Type" => "application/json"} + } + + describe "GET /company_services/:id/weeks" do + before { + get company_service_weeks_path(company_service.id), + headers: valid_headers, + as: :json + } + + it 'returns a success response' do + expect(response).to have_http_status(:success) + end + + it 'returns the correct JSON structure' do + json_response = JSON.parse(response.body) + expect(json_response).to have_key('data') + expect(json_response).to have_key('status') + expect(json_response).to have_key('statusText') + + expect(json_response['data']).to include('past', 'future') + expect(json_response['status']).to eq(200) + expect(json_response['statusText']).to eq("OK") + end + + it 'returns the correct weeks data' do + json_response = JSON.parse(response.body) + + expect(json_response['data']['future']).to be_an(Array) + + # Check the format of the weeks + expect(json_response['data']['future']).to all(include('id', 'label', 'start_date', 'end_date')) + + week_identifier_format = /^\d{4}-\d{2}$/ #YYYY-WWW + expect(json_response['data']['future']).to all(have_key('id')) + expect(json_response['data']['future']).to all( + include('id' => match(/^\d{4}-\d{2}$/)) + ) + end + end + +end diff --git a/backend/spec/routing/company_services_routing_spec.rb b/backend/spec/routing/company_services_routing_spec.rb index 1dd58ff..2638077 100644 --- a/backend/spec/routing/company_services_routing_spec.rb +++ b/backend/spec/routing/company_services_routing_spec.rb @@ -5,26 +5,5 @@ it "routes to #index" do expect(get: "/company_services").to route_to("company_services#index") end - - it "routes to #show" do - expect(get: "/company_services/1").to route_to("company_services#show", id: "1") - end - - - it "routes to #create" do - expect(post: "/company_services").to route_to("company_services#create") - end - - it "routes to #update via PUT" do - expect(put: "/company_services/1").to route_to("company_services#update", id: "1") - end - - it "routes to #update via PATCH" do - expect(patch: "/company_services/1").to route_to("company_services#update", id: "1") - end - - it "routes to #destroy" do - expect(delete: "/company_services/1").to route_to("company_services#destroy", id: "1") - end end end diff --git a/backend/spec/routing/engineers_routing_spec.rb b/backend/spec/routing/engineers_routing_spec.rb new file mode 100644 index 0000000..d16d1c0 --- /dev/null +++ b/backend/spec/routing/engineers_routing_spec.rb @@ -0,0 +1,13 @@ +require 'rails_helper' + +RSpec.describe CompanyServices::EngineersController, type: :routing do + it 'routes to the engineers assigned to an specific company service' do + expect(get: '/company_services/1/engineers?week=2024-36').to route_to( + controller: 'company_services/engineers', + action: 'index', + company_service_id: '1', + week: '2024-36' + ) + end +end + diff --git a/backend/spec/routing/errors_routing_spec.rb b/backend/spec/routing/errors_routing_spec.rb new file mode 100644 index 0000000..6d8c65c --- /dev/null +++ b/backend/spec/routing/errors_routing_spec.rb @@ -0,0 +1,11 @@ +require 'rails_helper' + +RSpec.describe ApplicationController, type: :routing do + it "routes unmatched paths to the render_not_found action" do + expect(get: "/unknown_path").to route_to( + controller: "application", + action: "render_not_found", + unmatched: "unknown_path" + ) + end +end \ No newline at end of file diff --git a/backend/spec/routing/shifts_routing_spec.rb b/backend/spec/routing/shifts_routing_spec.rb new file mode 100644 index 0000000..f270912 --- /dev/null +++ b/backend/spec/routing/shifts_routing_spec.rb @@ -0,0 +1,13 @@ +require 'rails_helper' + +RSpec.describe CompanyServices::ShiftsController, type: :routing do + it 'routes to the shifts related to an specific company service in an specific week' do + expect(get: '/company_services/1/shifts?week=2024-36').to route_to( + controller: 'company_services/shifts', + action: 'index', + company_service_id: '1', + week: '2024-36' + ) + end +end + diff --git a/backend/spec/routing/weeks_routing_spec.rb b/backend/spec/routing/weeks_routing_spec.rb new file mode 100644 index 0000000..5f22769 --- /dev/null +++ b/backend/spec/routing/weeks_routing_spec.rb @@ -0,0 +1,13 @@ +require 'rails_helper' + +RSpec.describe WeeksController, type: :routing do + describe 'routing' do + it 'routes to the weeks index for a specific company service' do + expect(get: '/company_services/1/weeks').to route_to( + controller: 'weeks', + action: 'index', + company_service_id: '1' + ) + end + end +end diff --git a/backend/spec/services/fetch_shifts_service_spec.rb b/backend/spec/services/fetch_shifts_service_spec.rb new file mode 100644 index 0000000..a09de6a --- /dev/null +++ b/backend/spec/services/fetch_shifts_service_spec.rb @@ -0,0 +1,57 @@ +require 'rails_helper' + +RSpec.describe FetchShiftsService, type: :service do + let(:company_service) { create(:company_service, + contract_start_date: Date.parse('2024-08-05'), + contract_end_date: Date.parse('2024-08-11')) } + let(:week) { company_service.contract_start_week } + let(:service) { FetchShiftsService.new(company_service.id, week) } + + describe '#call' do + context 'when there are shifts for the week' do + let!(:shift1) { create(:shift, company_service: company_service, week: week, day: 'Monday', start_time: '09:00', end_time: '12:00') } + let!(:shift2) { create(:shift, company_service: company_service, week: week, day: 'Monday', start_time: '14:00', end_time: '18:00') } + let!(:shift3) { create(:shift, company_service: company_service, week: week, day: 'Tuesday', start_time: '09:00', end_time: '11:00') } + + it 'returns formatted shift data grouped by day' do + result = service.call + + expect(result).to be_an(Array) + expect(result.size).to eq(2) # Monday and Tuesday + + monday = result.find { |day_data| day_data[:day] == 'Monday' } + tuesday = result.find { |day_data| day_data[:day] == 'Tuesday' } + + expect(monday[:dayLabel]).to eq('Lunes 05 de Agosto') # Adjust this label depending on the exact date of the week 32 + expect(monday[:time_blocks].size).to eq(2) + expect(monday[:time_blocks].first[:start_time]).to eq('09:00') + expect(monday[:time_blocks].first[:end_time]).to eq('12:00') + expect(monday[:time_blocks].first[:amount_of_hours]).to eq(3) + + expect(tuesday[:dayLabel]).to eq('Martes 06 de Agosto') + expect(tuesday[:time_blocks].size).to eq(1) + expect(tuesday[:time_blocks].first[:start_time]).to eq('09:00') + expect(tuesday[:time_blocks].first[:end_time]).to eq('11:00') + expect(tuesday[:time_blocks].first[:amount_of_hours]).to eq(2) + end + end + + context 'when there are no shifts for the week' do + it 'returns an empty array' do + result = service.call + expect(result).to eq([]) + end + end + + context 'when formatting the day label' do + let!(:shift) { create(:shift, company_service: company_service, week: week, day: 'Wednesday', start_time: '09:00', end_time: '17:00') } + + it 'returns the correct day label in Spanish' do + result = service.call + wednesday = result.find { |day_data| day_data[:day] == 'Wednesday' } + + expect(wednesday[:dayLabel]).to eq('Miércoles 07 de Agosto') # Adjust based on actual week date + end + end + end +end diff --git a/backend/spec/services/week_service_spec.rb b/backend/spec/services/week_service_spec.rb new file mode 100644 index 0000000..28cd54d --- /dev/null +++ b/backend/spec/services/week_service_spec.rb @@ -0,0 +1,187 @@ +require 'rails_helper' + +RSpec.describe WeekService, type: :service do + let(:company_service) { create(:company_service, + contract_start_date: Date.parse('2024-07-15'), + contract_end_date: Date.parse('2024-09-15')) + } + let(:service) { WeekService.new(company_service.id) } + + describe '#call generates weeks data based on contract' do + before do + # Monday, 5 of August, 2024 - Week: 32 + allow(Date).to receive(:today).and_return(Date.new(2024, 8, 5)) + end + describe 'week identifier' do + it 'returns the current week 32 identifier within the future weeks' do + # .week_identifier + result = service.call + future_weeks = result[:future] + + expect(future_weeks).to include( + a_hash_including(id: "2024-32") + ) + end + end + + describe 'Single Week' do + it 'returns the correct week structure for the past and future' do + result = service.call + past_weeks = result[:past] + future_weeks = result[:future] + + expect(past_weeks).to include( + a_hash_including( + id: "2024-31", + label: "Semana 31 del 2024", + start_date: "29/07/2024", + end_date: "04/08/2024" + ) + ) + + expect(future_weeks).to include( + a_hash_including( + id: "2024-32", + label: "Semana 32 del 2024", + start_date: "05/08/2024", + end_date: "11/08/2024" + ) + ) + end + end + + context 'Contract Range: 15Jul -> 15 Set 2024 - W29-W37 = 9 weeks' do + describe 'Past Weeks' do + it 'returns all past weeks until the contract start date' do + result = service.call + past_weeks = result[:past] + + expect(past_weeks).to eq([ + { + id: "2024-31", + label: "Semana 31 del 2024", + start_date: "29/07/2024", + end_date: "04/08/2024" + }, + { + id: "2024-30", + label: "Semana 30 del 2024", + start_date: "22/07/2024", + end_date: "28/07/2024" + }, + { + id: "2024-29", + label: "Semana 29 del 2024", + start_date: "15/07/2024", + end_date: "21/07/2024" + } + ]) + end + it 'returns all contract weeks when the current date surpass contract dates' do + # go to the future - suprass contract dates + allow(Date).to receive(:today).and_return(Date.new(2024, 12, 1)) + result = service.call + past_weeks = result[:past] + future_weeks = result[:future] + expect(past_weeks.count).to eq(9) + expect(future_weeks.count).to eq(0) + end + end + + describe 'Future Weeks' do + it 'returns the correct 5 future weeks based on end_date_limit' do + result = service.call + future_weeks = result[:future] + + expect(future_weeks).to eq([ + { + id: "2024-32", + label: "Semana 32 del 2024", + start_date: "05/08/2024", + end_date: "11/08/2024" + }, + { + id: "2024-33", + label: "Semana 33 del 2024", + start_date: "12/08/2024", + end_date: "18/08/2024" + }, + { + id: "2024-34", + label: "Semana 34 del 2024", + start_date: "19/08/2024", + end_date: "25/08/2024" + }, + { + id: "2024-35", + label: "Semana 35 del 2024", + start_date: "26/08/2024", + end_date: "01/09/2024" + }, + { + id: "2024-36", + label: "Semana 36 del 2024", + start_date: "02/09/2024", + end_date: "08/09/2024" + } + ]) + end + + it 'returns fewer future weeks when the end_date_limit is close' do + # go 1 week in the future - get closer to end date + allow(Date).to receive(:today).and_return(Date.new(2024, 8, 26)) + + result = service.call + future_weeks = result[:future] + + expect(future_weeks).to eq([ + { + id: "2024-35", + label: "Semana 35 del 2024", + start_date: "26/08/2024", + end_date: "01/09/2024" + }, + { + id: "2024-36", + label: "Semana 36 del 2024", + start_date: "02/09/2024", + end_date: "08/09/2024" + }, + { + id: "2024-37", + label: "Semana 37 del 2024", + start_date: "09/09/2024", + end_date: "15/09/2024" + } + ]) + end + + it 'returns only one week when its the last week of contract' do + # go to the future - last week of contract + allow(Date).to receive(:today).and_return(Date.new(2024, 9, 9)) + + result = service.call + future_weeks = result[:future] + + expect(future_weeks).to eq([ + { + id: "2024-37", + label: "Semana 37 del 2024", + start_date: "09/09/2024", + end_date: "15/09/2024" + } + ]) + end + it 'returns all weeks that enter on 5 weeks range when the current date a bit far in future' do + # go to the past - contract dates still in future + allow(Date).to receive(:today).and_return(Date.new(2024, 07, 1)) + result = service.call + past_weeks = result[:past] + future_weeks = result[:future] + expect(past_weeks.count).to eq(0) + expect(future_weeks.count).to eq(3) + end + end + end + end +end diff --git a/frontend/requests/01_company_services.http b/frontend/requests/01_company_services.http new file mode 100644 index 0000000..0876fc1 --- /dev/null +++ b/frontend/requests/01_company_services.http @@ -0,0 +1,30 @@ +## Endpoints - Gestion de Turnos (Shifts) +### 1st Dropdown - Get Company Services +GET http://rails:3000/company_services HTTP/1.1 +Accept: application/json + +### 2nd Dropdown - Get Weeks +GET http://rails:3000/company_services/1/weeks HTTP/1.1 +Accept: application/json + + +### Engineers Table +GET http://rails:3000/company_services/:id/engineers?week=YYYY-WW HTTP/1.1 +Accept: application/json +### Shifts Table +GET http://rails:3000/company_services/:id/shifts?week=YYYY-WW HTTP/1.1 +Accept: application/json + + +## Endpoints - Gestion de Disponibilidad (Availability) +### Boton Editar Disponibilidad: Consultar Disponibilidad de ingenieros +GET http://rails:3000/company_services/:id/engineers/availability?week=YYYY-WW HTTP/1.1 +Accept: application/json +### Updates Engineer Availability +POST http://rails:3000/company_services/:id/engineers/availability HTTP/1.1 +Accept: application/json + + +### error handling +GET http://rails:3000/abc HTTP/1.1 +Accept: application/json \ No newline at end of file diff --git a/frontend/src/api/AvailabilityApi.ts b/frontend/src/api/AvailabilityApi.ts index 6fbf548..aaa6e0b 100644 --- a/frontend/src/api/AvailabilityApi.ts +++ b/frontend/src/api/AvailabilityApi.ts @@ -20,7 +20,12 @@ export const requestAvailabilities = async ( } else { try { const response = await fetch( - `/api/company_services/${serviceId}//engineers/availability?week=${weekId}` + `/api/company_services/${serviceId}//engineers/availability?week=${weekId}`, + { + headers: { + Accept: "application/json", + }, + } ); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); @@ -49,6 +54,7 @@ export const storeAvailabilities = async ( method: "POST", headers: { "Content-Type": "application/json", + Accept: "application/json", }, body: JSON.stringify(availabilityPayload), } diff --git a/frontend/src/api/CompanyServiceApi.ts b/frontend/src/api/CompanyServiceApi.ts index d5aaafd..b9c7298 100644 --- a/frontend/src/api/CompanyServiceApi.ts +++ b/frontend/src/api/CompanyServiceApi.ts @@ -18,7 +18,12 @@ export const fetchCompanyServices = async (): Promise => { }); } else { try { - const response = await fetch("/api/company_services"); + // const response = await fetch("/api/company_services"); + const response = await fetch("/api/company_services", { + headers: { + Accept: "application/json", + }, + }); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } @@ -45,7 +50,11 @@ export const requestWeeks = async (serviceId: number): Promise => { }); } else { try { - const response = await fetch(`/api/company_services/${serviceId}/weeks`); + const response = await fetch(`/api/company_services/${serviceId}/weeks`, { + headers: { + Accept: "application/json", + }, + }); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } @@ -76,7 +85,12 @@ export const requestShifts = async ( } else { try { const response = await fetch( - `/api/company_services/${serviceId}/shifts?week=${weekId}` + `/api/company_services/${serviceId}/shifts?week=${weekId}`, + { + headers: { + Accept: "application/json", + }, + } ); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); @@ -108,7 +122,12 @@ export const requestEngineers = async ( } else { try { const response = await fetch( - `/api/company_services/${serviceId}/engineers?week=${weekId}` + `/api/company_services/${serviceId}/engineers?week=${weekId}`, + { + headers: { + Accept: "application/json", + }, + } ); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`);