diff --git a/.devcontainer/.env b/.devcontainer/.env new file mode 100644 index 0000000..6808357 --- /dev/null +++ b/.devcontainer/.env @@ -0,0 +1,9 @@ +POSTGRES_USER=user +POSTGRES_PASSWORD=password +POSTGRES_DB=monitoring_sys + + +DATABASE_HOST=postgres +DATABASE_USER=user +DATABASE_PASSWORD=password +DATABASE_NAME=monitoring_sys_development \ No newline at end of file diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml index 91ef2dc..9b9bc64 100644 --- a/.devcontainer/docker-compose.yml +++ b/.devcontainer/docker-compose.yml @@ -1,5 +1,8 @@ +name: monitoring-schedule + services: postgres: + container_name: monit-pg image: postgres:latest restart: unless-stopped environment: @@ -10,8 +13,11 @@ services: - "5432:5432" volumes: - postgres-data:/var/lib/postgresql/data + networks: + - backend-net rails: + container_name: monit-rails build: context: . dockerfile: ./rails-container/Dockerfile @@ -22,6 +28,9 @@ services: DATABASE_NAME: monitoring_sys_development volumes: - ..:/workspace:cached + networks: + - frontend-net + - backend-net command: sleep infinity ports: - "3030:3030" @@ -29,11 +38,14 @@ services: - postgres vue: + container_name: monit-vue build: context: . dockerfile: ./vue-container/Dockerfile volumes: - ..:/workspace:cached + networks: + - frontend-net # command: sh -c "yarn install && yarn serve" command: sleep infinity ports: @@ -42,14 +54,21 @@ services: - rails playwright: + container_name: monit-playwright image: mcr.microsoft.com/playwright:v1.39.0-jammy # Use the latest version working_dir: /workspace/frontend user: pwuser volumes: - ..:/workspace:cached + networks: + - frontend-net depends_on: - vue command: sh -c "yarn install && npx playwright install && tail -f /dev/null" volumes: postgres-data: + +networks: + backend-net: {} + frontend-net: {} \ No newline at end of file diff --git a/.devcontainer/rails-container/devcontainer.json b/.devcontainer/rails-container/devcontainer.json index cb6c817..3bee973 100644 --- a/.devcontainer/rails-container/devcontainer.json +++ b/.devcontainer/rails-container/devcontainer.json @@ -35,6 +35,44 @@ "strings": "on" } }, + "peacock": { + "affectTabActiveBorder": true, + "remoteColor": "#dd0531", + "favoriteColors": [ + { + "name": "Angular Red", + "value": "#dd0531" + }, + { + "name": "Something Different", + "value": "#832561" + }, + { + "name": "Svelte Orange", + "value": "#ff3d00" + } + ] + }, + "workbench.colorCustomizations": { + "sash.hoverBorder": "#fa1b49", + "statusBar.background": "#dd0531", + "statusBar.foreground": "#e7e7e7", + "statusBarItem.hoverBackground": "#fa1b49", + "statusBarItem.remoteBackground": "#dd0531", + "statusBarItem.remoteForeground": "#e7e7e7", + "activityBar.activeBackground": "#fa1b49", + "activityBar.background": "#fa1b49", + "activityBar.foreground": "#e7e7e7", + "activityBar.inactiveForeground": "#e7e7e799", + "activityBarBadge.background": "#155e02", + "activityBarBadge.foreground": "#e7e7e7", + "commandCenter.border": "#e7e7e799", + "titleBar.activeBackground": "#dd0531", + "titleBar.activeForeground": "#e7e7e7", + "titleBar.inactiveBackground": "#dd053199", + "titleBar.inactiveForeground": "#e7e7e799", + "tab.activeBorder": "#fa1b49" + }, "[ruby]": { "editor": { "defaultFormatter": "Shopify.ruby-lsp", @@ -100,7 +138,9 @@ "esbenp.prettier-vscode", "streetsidesoftware.code-spell-checker", "marcoroth.stimulus-lsp", - "VisualStudioExptTeam.vscodeintellicode" + "johnpapa.vscode-peacock", + "VisualStudioExptTeam.vscodeintellicode", + "fabiospampinato.vscode-terminals" ] } } diff --git a/.devcontainer/vue-container/devcontainer.json b/.devcontainer/vue-container/devcontainer.json index fe6cfad..648a436 100644 --- a/.devcontainer/vue-container/devcontainer.json +++ b/.devcontainer/vue-container/devcontainer.json @@ -25,6 +25,48 @@ }, "defaultFormatter": "esbenp.prettier-vscode" }, + "peacock": { + "affectTabActiveBorder": true, + "remoteColor": "#42b883", + "favoriteColors": [ + { + "name": "JavaScript Yellow", + "value": "#f9e64f" + }, + { + "name": "Node Green", + "value": "#215732" + }, + { + "name": "React Blue", + "value": "#61dafb" + }, + { + "name": "Vue Green", + "value": "#42b883" + } + ] + }, + "workbench.colorCustomizations": { + "activityBar.activeBackground": "#65c89b", + "activityBar.background": "#65c89b", + "activityBar.foreground": "#15202b", + "activityBar.inactiveForeground": "#15202b99", + "activityBarBadge.background": "#945bc4", + "activityBarBadge.foreground": "#e7e7e7", + "commandCenter.border": "#15202b99", + "sash.hoverBorder": "#65c89b", + "statusBar.background": "#42b883", + "statusBar.foreground": "#15202b", + "statusBarItem.hoverBackground": "#359268", + "statusBarItem.remoteBackground": "#42b883", + "statusBarItem.remoteForeground": "#15202b", + "tab.activeBorder": "#65c89b", + "titleBar.activeBackground": "#42b883", + "titleBar.activeForeground": "#15202b", + "titleBar.inactiveBackground": "#42b88399", + "titleBar.inactiveForeground": "#15202b99" + }, "tailwindCSS": { "includeLanguages": { "plaintext": "html" @@ -59,7 +101,9 @@ "marcoroth.stimulus-lsp", "usernamehw.errorlens", "aaron-bond.better-comments", - "VisualStudioExptTeam.vscodeintellicode" + "VisualStudioExptTeam.vscodeintellicode", + "johnpapa.vscode-peacock", + "mariusalchimavicius.json-to-ts" ] } } diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 18497a2..8a3dc86 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -46,7 +46,20 @@ jobs: - name: Run Jest tests run: | cd frontend - yarn test:unit + yarn test:unit --ci --reporters=default --reporters=jest-junit + + - name: Upload Jest Test Report + uses: actions/upload-artifact@v3 + with: + name: jest-test-report + path: frontend/junit.xml + # - name: Display Jest Test Results + # uses: dorny/test-reporter@v1 + # if: always() # Run this step even if previous steps fail + # with: + # name: Jest Tests + # path: frontend/junit.xml + # reporter: jest-junit # - name: Start Rails server # run: | @@ -88,3 +101,23 @@ jobs: cd frontend yarn test:e2e + - name: Upload Playwright HTML Test Report + uses: actions/upload-artifact@v3 + with: + name: playwright-report + path: frontend/playwright-report + + - name: Upload Playwright Test Report + uses: actions/upload-artifact@v3 + with: + name: playwright-report + path: frontend/playwright-tests.xml + + # - name: Display Playwright Test Results + # uses: dorny/test-reporter@v1 + # if: always() # Run this step even if previous steps fail + # with: + # name: Playwright Tests + # path: frontend/playwright-tests.xml + # reporter: java-junit + diff --git a/1-shift_management.png b/1-shift_management.png new file mode 100644 index 0000000..c42e16a Binary files /dev/null and b/1-shift_management.png differ diff --git a/README.md b/README.md index 072a6e6..df56d70 100644 --- a/README.md +++ b/README.md @@ -26,17 +26,19 @@ ### Componentes #### Endpoints - Gestion de Turnos (Shifts) - 1st Dropdown (Services) - - /api/company_services + - GET /api/company_services - 2nd Dropdown (Weeks) - - /api/company_services/:id/weeks + - GET /api/company_services/:id/weeks - Engineers Table - - /api/company_services/:id/engineers?week=YYYY-WW + - GET /api/company_services/:id/engineers?week=YYYY-WW - Shifts Table - - /api/company_services/:id/shifts?week=YYYY-WW + - GET /api/company_services/:id/shifts?week=YYYY-WW #### Endpoints - Gestion de Disponibilidad (Availability) -- Los de gestion de turnos para el filtrado y llenado de semana +- 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 - Updates Engineer Availability - - /api/company_services/:id/engineers/availability + - POST /api/company_services/:id/engineers/availability - week - availability (array) - engineer_id @@ -99,3 +101,12 @@ docker-compose -f .devcontainer/docker-compose.yml up ``` - navegar a 0.0.0.0:8080 para empezar a usar la app + + +### Screenshots +#### Ejecución +![figma-1](./1-shift_management.png) + +#### Figma +![figma-1](./shift-availability-management-figma.jpg) +![figma-2](./shift-availability-management-figma-p2.jpg) \ No newline at end of file diff --git a/frontend/e2e/checkCompanyServiceShifts.spec.js b/frontend/e2e/checkCompanyServiceShifts.spec.js index d3c5c9f..d5a3f04 100644 --- a/frontend/e2e/checkCompanyServiceShifts.spec.js +++ b/frontend/e2e/checkCompanyServiceShifts.spec.js @@ -61,7 +61,7 @@ test.describe("Check Company Service Shifts", () => { ).toHaveText(`${timeBlock.start} - ${timeBlock.end}`); await expect( specificTimeBlock.locator( - `[aria-label="Engineer Assigned ${timeBlock.engineer}"]` + `[aria-label="Assigned Engineer ${timeBlock.engineer}"]` ) ).toHaveText(timeBlock.engineer); } @@ -75,7 +75,7 @@ test.describe("Check Company Service Shifts", () => { unassignedTimeBlock.locator('[aria-label="Hour 09:00"]') ).toHaveText("09:00 - 10:00"); await expect( - unassignedTimeBlock.locator('[aria-label="Engineer Assigned ⚠"]') + unassignedTimeBlock.locator('[aria-label="Assigned Engineer ⚠"]') ).toHaveText("⚠"); }); }); diff --git a/frontend/e2e/editAvailability.spec.js b/frontend/e2e/editAvailability.spec.js new file mode 100644 index 0000000..3fc973f --- /dev/null +++ b/frontend/e2e/editAvailability.spec.js @@ -0,0 +1,38 @@ +import { test, expect } from "@playwright/test"; + +test.describe("Shift and Availability Table", () => { + test("should render the correct availability and save it", async ({ + page, + }) => { + await page.goto("/"); + + // Select a service + await page.selectOption('select[aria-label="Selecciona un Servicio"]', { + label: "Service A", + }); + + // Select a week + await page.selectOption('select[aria-label="Selecciona una Semana"]', { + label: "Semana 32 del 2024", + }); + + await page.click('button:has-text("Editar Disponibilidad")'); + + // Check the rendering of the shift table + await expect( + page.locator('[aria-label="Day Lunes 05 de Agosto"]') + ).toBeVisible(); + + // Check the rendering of checkboxes + await expect( + page.locator('[aria-label="Availability Monday 09:00 Engineer 1"]') + ).toBeVisible(); + + // Check some checkboxes + await page.check('[aria-label="Availability Monday 09:00 Engineer 1"]'); + await page.check('[aria-label="Availability Monday 10:00 Engineer 2"]'); + + // Save availability + await page.click('button:has-text("Ver Turnos")'); + }); +}); diff --git a/frontend/jest.config.js b/frontend/jest.config.js index d8cf967..c2faa8f 100644 --- a/frontend/jest.config.js +++ b/frontend/jest.config.js @@ -1,4 +1,14 @@ module.exports = { preset: "@vue/cli-plugin-unit-jest/presets/typescript-and-babel", testPathIgnorePatterns: [".*\\.page\\.ts$"], + reporters: [ + "default", + [ + "jest-junit", + { + outputDirectory: "./test-reports", + outputName: "junit.xml", + }, + ], + ], }; diff --git a/frontend/package.json b/frontend/package.json index 00db767..6b7b02d 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -37,6 +37,7 @@ "eslint-plugin-prettier": "^4.0.0", "eslint-plugin-vue": "^8.0.3", "jest": "^27.0.5", + "jest-junit": "^16.0.0", "postcss": "^8.4.40", "prettier": "^2.4.1", "start-server-and-test": "^2.0.5", diff --git a/frontend/playwright.config.js b/frontend/playwright.config.js index f83f65f..9bb8b69 100644 --- a/frontend/playwright.config.js +++ b/frontend/playwright.config.js @@ -10,8 +10,10 @@ export default defineConfig({ expect: { timeout: 5000, }, - // reporter: [ - // ['list'], - // ['html', { outputFile: 'e2e/test-results/report.html' }], - // ], + reporter: [ + ["list"], + ["junit", { outputFile: "playwright-tests.xml" }], + ["html", { outputFolder: "playwright-report", open: "never" }], + // ['html', { outputFile: 'e2e/test-results/report.html' }], + ], }); diff --git a/frontend/src/App.vue b/frontend/src/App.vue index dbf2535..2481f89 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -1,5 +1,5 @@ diff --git a/frontend/src/api/AvailabilityApi.ts b/frontend/src/api/AvailabilityApi.ts new file mode 100644 index 0000000..6fbf548 --- /dev/null +++ b/frontend/src/api/AvailabilityApi.ts @@ -0,0 +1,70 @@ +import EngineersAvailabilityServiceAWeek1 from "@/mock/eng_availability_a_w1.json"; +import { AvailabilityPayload, DayAvailability } from "./types"; + +const isMock = process.env.VUE_APP_USE_MOCK === "true"; + +export const requestAvailabilities = async ( + serviceId: number, + weekId: string +): Promise => { + if (isMock) { + return new Promise((resolve) => { + setTimeout(() => { + if (serviceId === 1) { + resolve(EngineersAvailabilityServiceAWeek1.data); + } else { + resolve(EngineersAvailabilityServiceAWeek1.data); + } + }, 500); + }); + } else { + try { + const response = await fetch( + `/api/company_services/${serviceId}//engineers/availability?week=${weekId}` + ); + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + const { data } = await response.json(); + return data; + } catch (error) { + console.error(error); + console.error("Failed to fetch shifts"); + throw new Error("Failed to fetch shifts"); + } + } +}; + +export const storeAvailabilities = async ( + serviceId: number, + availabilityPayload: AvailabilityPayload +): Promise => { + if (isMock) { + return "Disponibilidades guardadas con éxito"; + } else { + try { + const response = await fetch( + `/api/company_services/${serviceId}/engineers/availability`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(availabilityPayload), + } + ); + + if (response.ok) { + return "Disponibilidades guardadas con éxito"; + } else { + const errorData = await response.json(); + console.error(`Error: ${errorData.message}`); + return "Error al guardar disponibilidades"; + } + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } catch (error: any) { + console.error(`Error: ${error.message}`); + return "Error al guardar disponibilidades"; + } + } +}; diff --git a/frontend/src/api/types.ts b/frontend/src/api/types.ts index 6939cc8..1875129 100644 --- a/frontend/src/api/types.ts +++ b/frontend/src/api/types.ts @@ -47,6 +47,7 @@ export interface ShiftsResponse { export interface Shift { day: string; + dayLabel: string; time_blocks: Timeblock[]; } @@ -57,15 +58,42 @@ export interface Timeblock { engineer: Engineer | null; } +export interface Engineer { + id: number; + name: string; + color: string; + hours_assigned?: number; +} + export interface EngineersResponse { data: Engineer[]; status: number; statusText: string; } -export interface Engineer { +export interface EngineerAvailability { id: number; - name: string; - color: string; - hours_assigned?: number; + available: boolean; +} + +export interface TimeBlockAv { + time: string; + engineers: EngineerAvailability[]; +} + +export interface DayAvailability { + day: string; + dayLabel?: string; + times: TimeBlockAv[]; +} + +export interface AvailabilityResponse { + availability: DayAvailability[]; + status: number; + statusText: string; +} + +export interface AvailabilityPayload { + week: string; + availability: DayAvailability[]; } diff --git a/frontend/src/components/AvailabilityManagement/AvailabilityTable.vue b/frontend/src/components/AvailabilityManagement/AvailabilityTable.vue new file mode 100644 index 0000000..3ab1f49 --- /dev/null +++ b/frontend/src/components/AvailabilityManagement/AvailabilityTable.vue @@ -0,0 +1,66 @@ + + + diff --git a/frontend/src/components/AvailabilityManagement/__tests__/AvailabilityTable.page.ts b/frontend/src/components/AvailabilityManagement/__tests__/AvailabilityTable.page.ts new file mode 100644 index 0000000..4418db5 --- /dev/null +++ b/frontend/src/components/AvailabilityManagement/__tests__/AvailabilityTable.page.ts @@ -0,0 +1,47 @@ +import { mount, VueWrapper } from "@vue/test-utils"; +import { nextTick, Ref, ref } from "vue"; +import AvailabilityTable from "../AvailabilityTable.vue"; +import { DayAvailability, Engineer } from "@/api/types"; + +export class AvailabilityTablePage { + private wrapper: VueWrapper; + public shiftManagementEngineers: Ref; + public availabilities: Ref; + + constructor(engineers: Engineer[], availabilities: DayAvailability[]) { + this.shiftManagementEngineers = ref(engineers); + this.availabilities = ref(availabilities); + + this.wrapper = mount(AvailabilityTable, { + props: { + day: "Monday", + timeBlock: "09:00", + engineers, + }, + global: { + provide: { + shiftManagement: { + engineers: this.shiftManagementEngineers, + }, + availabilityManagement: { + availabilities: this.availabilities, + }, + }, + }, + }); + } + + get checkboxes() { + return this.wrapper.findAll("input[type='checkbox']"); + } + + isCheckboxChecked(index: number): boolean { + const element = this.checkboxes[index].element as HTMLInputElement; + return element.checked; + } + + async toggleCheckbox(index: number): Promise { + await this.checkboxes[index].trigger("change"); + await nextTick(); + } +} diff --git a/frontend/src/components/AvailabilityManagement/__tests__/AvailabilityTable.spec.ts b/frontend/src/components/AvailabilityManagement/__tests__/AvailabilityTable.spec.ts new file mode 100644 index 0000000..109a4f0 --- /dev/null +++ b/frontend/src/components/AvailabilityManagement/__tests__/AvailabilityTable.spec.ts @@ -0,0 +1,65 @@ +import { AvailabilityTablePage } from "./AvailabilityTable.page"; +import { Engineer, DayAvailability } from "@/api/types"; + +describe("AvailabilityTable.vue", () => { + let page: AvailabilityTablePage; + let providerEngineersMock: Engineer[]; + let providerAvailabilitiesMock: DayAvailability[]; + + beforeEach(() => { + providerEngineersMock = [ + { id: 1, name: "Engineer 1", hours_assigned: 10, color: "#a5b4fc" }, + { id: 2, name: "Engineer 2", hours_assigned: 5, color: "#5eead4" }, + { id: 3, name: "Engineer 3", hours_assigned: 8, color: "#bef264" }, + ]; + + providerAvailabilitiesMock = [ + { + day: "Monday", + times: [ + { + time: "09:00", + engineers: [ + { id: 1, available: true }, + { id: 2, available: false }, + { id: 3, available: true }, + ], + }, + ], + }, + ]; + + page = new AvailabilityTablePage( + providerEngineersMock, + providerAvailabilitiesMock + ); + }); + it("renders the correct number of engineers", () => { + expect(page.checkboxes.length).toBe(providerEngineersMock.length); + }); + + it("checks the availability of engineers correctly", () => { + expect(page.isCheckboxChecked(0)).toBe(true); + expect(page.isCheckboxChecked(1)).toBe(false); + expect(page.isCheckboxChecked(2)).toBe(true); + }); + + it("toggles the availability of an engineer when checkbox is changed", async () => { + // Initially, the first engineer is available + expect(page.isCheckboxChecked(0)).toBe(true); + + // Toggle availability + await page.toggleCheckbox(0); + expect(page.isCheckboxChecked(0)).toBe(false); + expect(page.availabilities.value[0].times[0].engineers[0].available).toBe( + false + ); + + // Toggle back + await page.toggleCheckbox(0); + expect(page.isCheckboxChecked(0)).toBe(true); + expect(page.availabilities.value[0].times[0].engineers[0].available).toBe( + true + ); + }); +}); diff --git a/frontend/src/components/ShiftManagement/ShiftTable.vue b/frontend/src/components/ShiftManagement/ShiftTable.vue index d55aada..5adef85 100644 --- a/frontend/src/components/ShiftManagement/ShiftTable.vue +++ b/frontend/src/components/ShiftManagement/ShiftTable.vue @@ -1,46 +1,68 @@