From 57d63ec3fe1fe0df2ad4720ea73007b675f80ada Mon Sep 17 00:00:00 2001 From: nicoleamber Date: Fri, 8 Sep 2023 13:19:56 +0800 Subject: [PATCH 1/7] [M1_TR-214] Backend for M1_TR-211 --- server/.gitignore | 1 + server/package.json | 6 +- server/src/api/jobs/dto/job-list.dto.ts | 10 ++ server/src/api/jobs/dto/job-query.dto.ts | 16 +++ server/src/api/jobs/jobs.controller.spec.ts | 69 +++++++++++ server/src/api/jobs/jobs.controller.ts | 19 ++++ server/src/api/jobs/jobs.module.ts | 10 ++ server/src/api/jobs/jobs.service.spec.ts | 70 ++++++++++++ server/src/api/jobs/jobs.service.ts | 56 +++++++++ .../src/api/sample/sample.controller.spec.ts | 18 --- server/src/api/sample/sample.controller.ts | 9 -- server/src/api/sample/sample.dto.ts | 11 -- server/src/api/sample/sample.entities.ts | 7 -- server/src/app.module.ts | 6 +- server/src/main.ts | 11 ++ .../20230907065110_init/migration.sql | 107 ++++++++++++++++++ .../src/models/migrations/migration_lock.toml | 3 + server/src/models/schema.prisma | 100 +++++++++++++++- server/src/models/seed.ts | 28 +++++ server/src/models/seeders/customer.seed.ts | 24 ++++ server/src/models/seeders/estimation.ts | 25 ++++ server/src/models/seeders/job.seed.ts | 27 +++++ server/src/models/seeders/schedule.seed.ts | 24 ++++ server/src/models/seeders/user.seed.ts | 22 ++++ server/yarn.lock | 54 +++++++-- 25 files changed, 671 insertions(+), 62 deletions(-) create mode 100644 server/src/api/jobs/dto/job-list.dto.ts create mode 100644 server/src/api/jobs/dto/job-query.dto.ts create mode 100644 server/src/api/jobs/jobs.controller.spec.ts create mode 100644 server/src/api/jobs/jobs.controller.ts create mode 100644 server/src/api/jobs/jobs.module.ts create mode 100644 server/src/api/jobs/jobs.service.spec.ts create mode 100644 server/src/api/jobs/jobs.service.ts delete mode 100644 server/src/api/sample/sample.controller.spec.ts delete mode 100644 server/src/api/sample/sample.controller.ts delete mode 100644 server/src/api/sample/sample.dto.ts delete mode 100644 server/src/api/sample/sample.entities.ts create mode 100644 server/src/models/migrations/20230907065110_init/migration.sql create mode 100644 server/src/models/migrations/migration_lock.toml create mode 100644 server/src/models/seed.ts create mode 100644 server/src/models/seeders/customer.seed.ts create mode 100644 server/src/models/seeders/estimation.ts create mode 100644 server/src/models/seeders/job.seed.ts create mode 100644 server/src/models/seeders/schedule.seed.ts create mode 100644 server/src/models/seeders/user.seed.ts diff --git a/server/.gitignore b/server/.gitignore index af40b5b..0938d32 100644 --- a/server/.gitignore +++ b/server/.gitignore @@ -36,3 +36,4 @@ lerna-debug.log* # Keep environment variables out of version control .env +/test diff --git a/server/package.json b/server/package.json index 0661ea4..d190922 100644 --- a/server/package.json +++ b/server/package.json @@ -17,7 +17,8 @@ "test:watch": "jest --watch", "test:cov": "jest --coverage", "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", - "test:e2e": "jest --config ./test/jest-e2e.json" + "test:e2e": "jest --config ./test/jest-e2e.json", + "seed": "ts-node src/models/seed.ts" }, "dependencies": { "@nestjs/common": "^10.0.0", @@ -31,10 +32,13 @@ "rxjs": "^7.8.1" }, "devDependencies": { + "@faker-js/faker": "^8.0.2", "@nestjs/cli": "^10.0.0", "@nestjs/schematics": "^10.0.0", + "@nestjs/swagger": "^7.1.10", "@nestjs/testing": "^10.0.0", "@types/express": "^4.17.17", + "@types/faker": "^6.6.9", "@types/jest": "^29.5.2", "@types/node": "^20.3.1", "@types/supertest": "^2.0.12", diff --git a/server/src/api/jobs/dto/job-list.dto.ts b/server/src/api/jobs/dto/job-list.dto.ts new file mode 100644 index 0000000..717769e --- /dev/null +++ b/server/src/api/jobs/dto/job-list.dto.ts @@ -0,0 +1,10 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { Job } from '@prisma/client'; + +export class JobListDto { + @ApiProperty() + jobs: Job[]; + + @ApiProperty() + count: number; +} diff --git a/server/src/api/jobs/dto/job-query.dto.ts b/server/src/api/jobs/dto/job-query.dto.ts new file mode 100644 index 0000000..a8f26d5 --- /dev/null +++ b/server/src/api/jobs/dto/job-query.dto.ts @@ -0,0 +1,16 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { Transform } from 'class-transformer'; +import { IsNumber, IsOptional } from 'class-validator'; + +export class JobQueryDto { + @ApiProperty() + @IsNumber() + @Transform(({ value }) => parseInt(value)) + page: number; + + @ApiProperty({ required: false}) + @IsOptional() + @IsNumber() + @Transform(({ value }) => parseInt(value)) + perPage?: number; +} diff --git a/server/src/api/jobs/jobs.controller.spec.ts b/server/src/api/jobs/jobs.controller.spec.ts new file mode 100644 index 0000000..57d3453 --- /dev/null +++ b/server/src/api/jobs/jobs.controller.spec.ts @@ -0,0 +1,69 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { JobsController } from './jobs.controller'; +import { JobsService } from './jobs.service'; + +describe('JobsController', () => { + let jobController: JobsController; + + const mockJobService = { + findAll: jest.fn() + }; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [JobsController], + providers: [ + { + provide: JobsService, + useValue: mockJobService, + }, + ], + }).compile(); + + jobController = module.get(JobsController); + }); + + describe('findAll', () => { + it('should return a list of jobs with the total number of jobs', async () => { + const jobQuery = { page: 1, perPage: 2 }; + const jobList = { + jobs: [ + { + id: 1, + title: "Job A", + type: "Type A", + tags: [], + remarks: null, + customerId: 1, + paymentMethod: null, + userId: 1, + pipelinePhase: null, + createdAt: new Date("2023-09-07T09:38:42.296Z"), + updatedAt: new Date("2023-09-07T09:38:42.296Z"), + }, + { + id: 2, + title: "Job B", + type: "Type B", + tags: [], + remarks: null, + customerId: 2, + paymentMethod: null, + userId: 2, + pipelinePhase: null, + createdAt: new Date("2023-09-07T09:38:42.296Z"), + updatedAt: new Date("2023-09-07T09:38:42.296Z"), + } + ], + count: 2 + }; + + jest.spyOn(mockJobService, 'findAll').mockResolvedValue(jobList); + + const jobs = await jobController.findAll(jobQuery); + + expect(mockJobService.findAll).toHaveBeenCalledWith(jobQuery); + expect(jobs).toEqual(jobList); + }); + }); +}); diff --git a/server/src/api/jobs/jobs.controller.ts b/server/src/api/jobs/jobs.controller.ts new file mode 100644 index 0000000..4106a9c --- /dev/null +++ b/server/src/api/jobs/jobs.controller.ts @@ -0,0 +1,19 @@ +import { Controller, Get, Query, UsePipes, ValidationPipe } from '@nestjs/common'; +import { ApiTags } from '@nestjs/swagger'; +import { JobListDto } from './dto/job-list.dto'; +import { JobQueryDto } from './dto/job-query.dto'; +import { JobsService } from './jobs.service'; + +@ApiTags('jobs') +@Controller('jobs') +export class JobsController { + constructor(private readonly jobsService: JobsService) {} + + @Get() + @UsePipes(new ValidationPipe({ transform: true })) + async findAll(@Query() jobQuery: JobQueryDto): Promise { + const jobs = await this.jobsService.findAll(jobQuery); + + return jobs; + } +} diff --git a/server/src/api/jobs/jobs.module.ts b/server/src/api/jobs/jobs.module.ts new file mode 100644 index 0000000..7e16890 --- /dev/null +++ b/server/src/api/jobs/jobs.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; +import { PrismaService } from 'src/database/connection.service'; +import { JobsController } from './jobs.controller'; +import { JobsService } from './jobs.service'; + +@Module({ + controllers: [JobsController], + providers: [JobsService, PrismaService], +}) +export class JobsModule {} diff --git a/server/src/api/jobs/jobs.service.spec.ts b/server/src/api/jobs/jobs.service.spec.ts new file mode 100644 index 0000000..dbd1f80 --- /dev/null +++ b/server/src/api/jobs/jobs.service.spec.ts @@ -0,0 +1,70 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { PrismaService } from '../../database/connection.service'; +import { JobsService } from './jobs.service'; + +describe('JobsService', () => { + let jobService: JobsService; + + const mockPrismaService = { + job: { + findMany: jest.fn(), + count: jest.fn(), + }, + $transaction: jest.fn() + }; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + JobsService, + { + provide: PrismaService, + useValue: mockPrismaService + } + ], + }).compile(); + + jobService = module.get(JobsService); + }); + + describe('findAll', () => { + it('should be return a list of jobs with the total number of jobs', async () => { + const jobQuery = { page: 1, perPage: 2 }; + const mockJobList = [ + { + id: 1, + title: "Job A", + type: "Type A", + tags: [], + remarks: null, + customerId: 1, + paymentMethod: null, + userId: 1, + pipelinePhase: null, + createdAt: new Date("2023-09-07T09:38:42.296Z"), + updatedAt: new Date("2023-09-07T09:38:42.296Z"), + }, + { + id: 2, + title: "Job B", + type: "Type B", + tags: [], + remarks: null, + customerId: 2, + paymentMethod: null, + userId: 2, + pipelinePhase: null, + createdAt: new Date("2023-09-07T09:38:42.296Z"), + updatedAt: new Date("2023-09-07T09:38:42.296Z"), + } + ]; + const mockJobCount = 2; + + mockPrismaService.$transaction.mockResolvedValue([ mockJobList, mockJobCount ]); + + const jobs = await jobService.findAll(jobQuery); + + expect(jobs).toEqual({ jobs: mockJobList, count: mockJobCount }); + }); + }); +}); diff --git a/server/src/api/jobs/jobs.service.ts b/server/src/api/jobs/jobs.service.ts new file mode 100644 index 0000000..0dc8e2d --- /dev/null +++ b/server/src/api/jobs/jobs.service.ts @@ -0,0 +1,56 @@ +import { BadRequestException, Injectable } from '@nestjs/common'; +import { PrismaService } from '../../database/connection.service'; +import { JobListDto } from './dto/job-list.dto'; +import { JobQueryDto } from './dto/job-query.dto'; + +@Injectable() +export class JobsService { + constructor(private prisma: PrismaService){} + + async findAll(query: JobQueryDto): Promise { + try { + const { page, perPage = 12 } = query; + const skip = (page - 1) * perPage; + + const [ jobs, count ] = await this.prisma.$transaction([ + this.prisma.job.findMany({ + take: perPage, + skip, + include: { + customer: { + select: { + firstName: true, + lastName: true, + } + }, + schedules: { + select: { + startDate: true, + startTime: true, + endDate: true, + endTime: true + } + }, + estimation: { + select: { + status: true, + totalCost: true, + } + }, + personInCharge: { + select: { + firstName: true, + lastName: true + } + } + } + }), + this.prisma.job.count() + ]); + + return { jobs, count }; + } catch (err) { + throw new BadRequestException('Something went wrong.'); + } + } +} diff --git a/server/src/api/sample/sample.controller.spec.ts b/server/src/api/sample/sample.controller.spec.ts deleted file mode 100644 index 3164a8c..0000000 --- a/server/src/api/sample/sample.controller.spec.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { SampleController } from './sample.controller'; - -describe('SampleController', () => { - let controller: SampleController; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - controllers: [SampleController], - }).compile(); - - controller = module.get(SampleController); - }); - - it('should be defined', () => { - expect(controller).toBeDefined(); - }); -}); diff --git a/server/src/api/sample/sample.controller.ts b/server/src/api/sample/sample.controller.ts deleted file mode 100644 index 3308a83..0000000 --- a/server/src/api/sample/sample.controller.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Controller, Get } from '@nestjs/common'; - -@Controller() -export class SampleController { - @Get() - getHello(): string { - return '(SERVER) Hello World!'; - } -} diff --git a/server/src/api/sample/sample.dto.ts b/server/src/api/sample/sample.dto.ts deleted file mode 100644 index 42ff6bb..0000000 --- a/server/src/api/sample/sample.dto.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { IsString } from 'class-validator'; - -export class SampleUserDTO { - id: number; - - @IsString() - name: string; - - @IsString() - email: string; -} diff --git a/server/src/api/sample/sample.entities.ts b/server/src/api/sample/sample.entities.ts deleted file mode 100644 index 43baf1a..0000000 --- a/server/src/api/sample/sample.entities.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { SampleUser } from '@prisma/client'; - -export class SampleUserEntity implements SampleUser { - id: number; - name: string; - email: string; -} diff --git a/server/src/app.module.ts b/server/src/app.module.ts index 001246b..b830525 100644 --- a/server/src/app.module.ts +++ b/server/src/app.module.ts @@ -1,10 +1,10 @@ import { Module } from '@nestjs/common'; +import { JobsModule } from './api/jobs/jobs.module'; import { DatabaseModule } from './database/database.module'; -import { SampleController } from './api/sample/sample.controller'; @Module({ - imports: [DatabaseModule], - controllers: [SampleController], + imports: [DatabaseModule, JobsModule], + controllers: [], providers: [], }) export class AppModule {} diff --git a/server/src/main.ts b/server/src/main.ts index 8e348dc..8047b12 100644 --- a/server/src/main.ts +++ b/server/src/main.ts @@ -1,8 +1,19 @@ import { NestFactory } from '@nestjs/core'; +import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; import { AppModule } from './app.module'; async function bootstrap() { const app = await NestFactory.create(AppModule); + + const config = new DocumentBuilder() + .setTitle('Sim-JMS') + .setDescription('This is the API documentation for sim-jms') + .setVersion('1.0') + .build(); + + const document = SwaggerModule.createDocument(app, config); + SwaggerModule.setup('api', app, document); + await app.listen(4000); } bootstrap(); diff --git a/server/src/models/migrations/20230907065110_init/migration.sql b/server/src/models/migrations/20230907065110_init/migration.sql new file mode 100644 index 0000000..4bc22b3 --- /dev/null +++ b/server/src/models/migrations/20230907065110_init/migration.sql @@ -0,0 +1,107 @@ +-- CreateEnum +CREATE TYPE "Role" AS ENUM ('USER', 'ADMIN'); + +-- CreateEnum +CREATE TYPE "Tag" AS ENUM ('TAG_A', 'TAG_B', 'TAG_C'); + +-- CreateEnum +CREATE TYPE "PaymentMethod" AS ENUM ('CASH', 'CARD', 'BANK_TRANSFER'); + +-- CreateEnum +CREATE TYPE "Status" AS ENUM ('NOT_YET_CREATED', 'MAKING', 'APPROVED', 'SENT_TO_CUSTOMER', 'CLOSED'); + +-- CreateEnum +CREATE TYPE "PipelinePhase" AS ENUM ('NEGOTIATION', 'DELIVERY'); + +-- CreateTable +CREATE TABLE "User" ( + "id" SERIAL NOT NULL, + "firstName" TEXT NOT NULL, + "lastName" TEXT NOT NULL, + "email" TEXT NOT NULL, + "role" "Role" NOT NULL DEFAULT 'USER', + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "User_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Job" ( + "id" SERIAL NOT NULL, + "title" TEXT NOT NULL, + "type" TEXT NOT NULL, + "tags" "Tag"[], + "remarks" TEXT, + "customerId" INTEGER NOT NULL, + "paymentMethod" "PaymentMethod" NOT NULL DEFAULT 'CASH', + "userId" INTEGER NOT NULL, + "pipelinePhase" "PipelinePhase" NOT NULL DEFAULT 'NEGOTIATION', + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "Job_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Customer" ( + "id" SERIAL NOT NULL, + "firstName" TEXT NOT NULL, + "lastName" TEXT NOT NULL, + "email" TEXT NOT NULL, + "contact" TEXT NOT NULL, + "address" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "Customer_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Schedule" ( + "id" SERIAL NOT NULL, + "startDate" TIMESTAMP(3) NOT NULL, + "endDate" TIMESTAMP(3) NOT NULL, + "startTime" TIMESTAMP(3) NOT NULL, + "endTime" TIMESTAMP(3) NOT NULL, + "jobId" INTEGER NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "Schedule_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Estimation" ( + "id" SERIAL NOT NULL, + "title" TEXT NOT NULL, + "document" TEXT NOT NULL, + "totalCost" DECIMAL(65,30) NOT NULL, + "status" "Status" NOT NULL, + "jobId" INTEGER NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "Estimation_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "Job_title_customerId_key" ON "Job"("title", "customerId"); + +-- CreateIndex +CREATE UNIQUE INDEX "Customer_email_key" ON "Customer"("email"); + +-- CreateIndex +CREATE UNIQUE INDEX "Estimation_jobId_key" ON "Estimation"("jobId"); + +-- AddForeignKey +ALTER TABLE "Job" ADD CONSTRAINT "Job_customerId_fkey" FOREIGN KEY ("customerId") REFERENCES "Customer"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Job" ADD CONSTRAINT "Job_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Schedule" ADD CONSTRAINT "Schedule_jobId_fkey" FOREIGN KEY ("jobId") REFERENCES "Job"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Estimation" ADD CONSTRAINT "Estimation_jobId_fkey" FOREIGN KEY ("jobId") REFERENCES "Job"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/server/src/models/migrations/migration_lock.toml b/server/src/models/migrations/migration_lock.toml new file mode 100644 index 0000000..fbffa92 --- /dev/null +++ b/server/src/models/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (i.e. Git) +provider = "postgresql" \ No newline at end of file diff --git a/server/src/models/schema.prisma b/server/src/models/schema.prisma index 108018e..fa427aa 100644 --- a/server/src/models/schema.prisma +++ b/server/src/models/schema.prisma @@ -10,9 +10,99 @@ datasource db { url = env("DATABASE_URL") } -// Temporary model so that Prisma Client can initialize -model SampleUser { - id Int @id @default(autoincrement()) - name String - email String @unique +model User { + id Int @id @default(autoincrement()) + firstName String + lastName String + email String + role Role @default(USER) + jobs Job[] + createdAt DateTime @default(now()) + updatedAt DateTime @default(now()) +} + +model Job { + id Int @id @default(autoincrement()) + title String + type String + tags Tag[] + remarks String? + customer Customer @relation(fields: [customerId], references: [id]) + customerId Int + paymentMethod PaymentMethod @default(CASH) + personInCharge User @relation(fields: [userId], references: [id]) + userId Int + schedules Schedule[] + estimation Estimation? + pipelinePhase PipelinePhase @default(NEGOTIATION) + createdAt DateTime @default(now()) + updatedAt DateTime @default(now()) + + @@unique([title, customerId]) +} + +model Customer { + id Int @id @default(autoincrement()) + firstName String + lastName String + email String @unique + contact String + address String + jobs Job[] + createdAt DateTime @default(now()) + updatedAt DateTime @default(now()) +} + +model Schedule { + id Int @id @default(autoincrement()) + startDate DateTime + endDate DateTime + startTime DateTime + endTime DateTime + job Job @relation(fields: [jobId], references: [id]) + jobId Int + createdAt DateTime @default(now()) + updatedAt DateTime @default(now()) +} + +model Estimation { + id Int @id @default(autoincrement()) + title String + document String + totalCost Decimal + status Status + job Job @relation(fields: [jobId], references: [id]) + jobId Int @unique + createdAt DateTime @default(now()) + updatedAt DateTime @default(now()) +} + +enum Role { + USER + ADMIN +} + +enum Tag { + TAG_A + TAG_B + TAG_C +} + +enum PaymentMethod { + CASH + CARD + BANK_TRANSFER +} + +enum Status { + NOT_YET_CREATED + MAKING + APPROVED + SENT_TO_CUSTOMER + CLOSED +} + +enum PipelinePhase { + NEGOTIATION + DELIVERY } diff --git a/server/src/models/seed.ts b/server/src/models/seed.ts new file mode 100644 index 0000000..72958c7 --- /dev/null +++ b/server/src/models/seed.ts @@ -0,0 +1,28 @@ +import { PrismaClient } from "@prisma/client"; + +import seedCustomers from './seeders/customer.seed'; +import seedEstimations from './seeders/estimation'; +import seedJobs from './seeders/job.seed'; +import seedSchedules from './seeders/schedule.seed'; +import seedUsers from "./seeders/user.seed"; + +async function seed() { + const prisma = new PrismaClient(); + + try { + await prisma.$transaction(async () => { + await seedUsers(); + await seedCustomers(); + await seedJobs(); + await seedEstimations(); + await seedSchedules(); + }); + } catch (error) { + console.log('Error seeding database'); + throw error; + } finally { + await prisma.$disconnect(); + } +} + +seed(); diff --git a/server/src/models/seeders/customer.seed.ts b/server/src/models/seeders/customer.seed.ts new file mode 100644 index 0000000..feaebf5 --- /dev/null +++ b/server/src/models/seeders/customer.seed.ts @@ -0,0 +1,24 @@ +import { faker } from '@faker-js/faker'; +import { PrismaClient } from '@prisma/client'; + +export default async function seedCustomers() { + const prisma = new PrismaClient(); + + const seedDataCount = 20; + let customerData = []; + + for (let i = 0; i < seedDataCount; i ++) { + const newCustomer ={ + firstName: faker.person.firstName(), + lastName: faker.person.lastName(), + email: faker.internet.email(), + contact: faker.phone.number('+63 9# ### ## ##'), + address: faker.location.streetAddress(true) + } + customerData = [...customerData, newCustomer] + } + + await prisma.customer.createMany({ + data: customerData + }) +} diff --git a/server/src/models/seeders/estimation.ts b/server/src/models/seeders/estimation.ts new file mode 100644 index 0000000..0474b18 --- /dev/null +++ b/server/src/models/seeders/estimation.ts @@ -0,0 +1,25 @@ +import { faker } from '@faker-js/faker'; +import { PrismaClient, Status } from '@prisma/client'; + +export default async function seedEstimations() { + const prisma = new PrismaClient(); + + const status = Object.values(Status); + const seedDataCount = 20 + let estimationData = []; + + for (let i = 0; i < seedDataCount; i ++) { + const newEstimation = { + title: faker.lorem.word(), + document: faker.system.commonFileName('pdf'), + totalCost: faker.finance.amount({min: 1000, max: 10000, dec: 2}), + status: faker.helpers.arrayElement(status), + jobId: i + 1, + } + estimationData = [...estimationData, newEstimation] + } + + await prisma.estimation.createMany({ + data: estimationData + }) +} diff --git a/server/src/models/seeders/job.seed.ts b/server/src/models/seeders/job.seed.ts new file mode 100644 index 0000000..6156395 --- /dev/null +++ b/server/src/models/seeders/job.seed.ts @@ -0,0 +1,27 @@ +import { faker } from '@faker-js/faker'; +import { PaymentMethod, PrismaClient, Tag } from '@prisma/client'; + +export default async function seedJobs() { + const prisma = new PrismaClient(); + + const paymentMethod = Object.values(PaymentMethod); + const tags = Object.values(Tag); + const seedDataCount = 20 + let jobData = []; + + for (let i = 0; i < seedDataCount; i ++) { + const newJob = { + title: faker.lorem.words(), + type: faker.lorem.word(), + userId: faker.number.int({ min: 1, max: 5 }), + customerId: i + 1, + paymentMethod: faker.helpers.arrayElement(paymentMethod), + tags: faker.helpers.arrayElements(tags, { min: 1, max: 3 } ) + } + jobData = [...jobData, newJob] + } + + await prisma.job.createMany({ + data: jobData + }) +} diff --git a/server/src/models/seeders/schedule.seed.ts b/server/src/models/seeders/schedule.seed.ts new file mode 100644 index 0000000..c797bbf --- /dev/null +++ b/server/src/models/seeders/schedule.seed.ts @@ -0,0 +1,24 @@ +import { faker } from '@faker-js/faker'; +import { PrismaClient } from '@prisma/client'; + +export default async function seedSchedules() { + const prisma = new PrismaClient(); + + const seedDataCount = 20; + let scheduleData = []; + + for (let i = 0; i < seedDataCount; i++) { + const newSchedule = { + startDate: faker.date.soon(), + endDate: faker.date.soon(), + startTime: faker.date.soon(), + endTime: faker.date.soon(), + jobId: faker.number.int({ min: 1, max: 20 }) + } + scheduleData = [...scheduleData, newSchedule] + } + + await prisma.schedule.createMany({ + data: scheduleData + }); +} diff --git a/server/src/models/seeders/user.seed.ts b/server/src/models/seeders/user.seed.ts new file mode 100644 index 0000000..31db144 --- /dev/null +++ b/server/src/models/seeders/user.seed.ts @@ -0,0 +1,22 @@ +import { faker } from '@faker-js/faker'; +import { PrismaClient } from '@prisma/client'; + +export default async function seedUsers() { + const prisma = new PrismaClient(); + + const seedDataCount = 5; + let userData = []; + + for (let i = 0; i < seedDataCount; i++) { + const newUser = { + firstName: faker.person.firstName(), + lastName: faker.person.lastName(), + email: faker.internet.email(), + } + userData = [...userData, newUser] + } + + await prisma.user.createMany({ + data: userData + }) +} diff --git a/server/yarn.lock b/server/yarn.lock index d91c693..2b04a6b 100644 --- a/server/yarn.lock +++ b/server/yarn.lock @@ -402,6 +402,11 @@ resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.46.0.tgz#3f7802972e8b6fe3f88ed1aabc74ec596c456db6" integrity sha512-a8TLtmPi8xzPkCbp/OGFUo5yhRkHM2Ko9kOWP4znJr0WAhWyThaw3PnwX4vOTWOAMsV2uRt32PPDcEz63esSaA== +"@faker-js/faker@^8.0.2": + version "8.0.2" + resolved "https://registry.yarnpkg.com/@faker-js/faker/-/faker-8.0.2.tgz#bab698c5d3da9c52744e966e0e3eedb6c8b05c37" + integrity sha512-Uo3pGspElQW91PCvKSIAXoEgAUlRnH29sX2/p89kg7sP1m2PzCufHINd0FhTXQf6DYGiUlVncdSPa2F9wxed2A== + "@humanwhocodes/config-array@^0.11.10": version "0.11.10" resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.10.tgz#5a3ffe32cc9306365fb3fd572596cd602d5e12d2" @@ -731,6 +736,11 @@ path-to-regexp "3.2.0" tslib "2.6.1" +"@nestjs/mapped-types@2.0.2": + version "2.0.2" + resolved "https://registry.yarnpkg.com/@nestjs/mapped-types/-/mapped-types-2.0.2.tgz#c8a090a8d22145b85ed977414c158534210f2e4f" + integrity sha512-V0izw6tWs6fTp9+KiiPUbGHWALy563Frn8X6Bm87ANLRuE46iuBMD5acKBDP5lKL/75QFvrzSJT7HkCbB0jTpg== + "@nestjs/platform-express@^10.0.0": version "10.1.3" resolved "https://registry.yarnpkg.com/@nestjs/platform-express/-/platform-express-10.1.3.tgz#d1f644e86f2bc45c7529b9eed7669613f4392e99" @@ -753,6 +763,17 @@ jsonc-parser "3.2.0" pluralize "8.0.0" +"@nestjs/swagger@^7.1.10": + version "7.1.10" + resolved "https://registry.yarnpkg.com/@nestjs/swagger/-/swagger-7.1.10.tgz#955deb9c428fae779d2988a0d24a55977a7be11d" + integrity sha512-qreCcxgHFyFX1mOfK36pxiziy4xoa/XcxC0h4Zr9yH54WuqMqO9aaNFhFyuQ1iyd/3YBVQB21Un4gQnh9iGm0w== + dependencies: + "@nestjs/mapped-types" "2.0.2" + js-yaml "4.1.0" + lodash "4.17.21" + path-to-regexp "3.2.0" + swagger-ui-dist "5.4.2" + "@nestjs/testing@^10.0.0": version "10.1.3" resolved "https://registry.yarnpkg.com/@nestjs/testing/-/testing-10.1.3.tgz#596a1dc580373b3b0b070ac73bf70e45f80197dd" @@ -940,6 +961,13 @@ "@types/qs" "*" "@types/serve-static" "*" +"@types/faker@^6.6.9": + version "6.6.9" + resolved "https://registry.yarnpkg.com/@types/faker/-/faker-6.6.9.tgz#1064e7c46be58388fa326e2f918a4f02ab740a7a" + integrity sha512-Y9YYm5L//8ooiiknO++4Gr539zzdI0j3aXnOBjo1Vk+kTvffY10GuE2wn78AFPECwZ5MYGTjiDVw1naLLdDimw== + dependencies: + faker "*" + "@types/graceful-fs@^4.1.3": version "4.1.6" resolved "https://registry.yarnpkg.com/@types/graceful-fs/-/graceful-fs-4.1.6.tgz#e14b2576a1c25026b7f02ede1de3b84c3a1efeae" @@ -2318,6 +2346,11 @@ external-editor@^3.0.3: iconv-lite "^0.4.24" tmp "^0.0.33" +faker@*: + version "6.6.6" + resolved "https://registry.yarnpkg.com/faker/-/faker-6.6.6.tgz#e9529da0109dca4c7c5dbfeaadbd9234af943033" + integrity sha512-9tCqYEDHI5RYFQigXFwF1hnCwcWCOJl/hmll0lr5D2Ljjb0o4wphb69wikeJDz5qCEzXCoPvG6ss5SDP6IfOdg== + fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: version "3.1.3" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" @@ -3290,6 +3323,13 @@ js-tokens@^4.0.0: resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== +js-yaml@4.1.0, js-yaml@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" + integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== + dependencies: + argparse "^2.0.1" + js-yaml@^3.13.1: version "3.14.1" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.1.tgz#dae812fdb3825fa306609a8717383c50c36a0537" @@ -3298,13 +3338,6 @@ js-yaml@^3.13.1: argparse "^1.0.7" esprima "^4.0.0" -js-yaml@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" - integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== - dependencies: - argparse "^2.0.1" - jsesc@^2.5.1: version "2.5.2" resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" @@ -3406,7 +3439,7 @@ lodash.merge@^4.6.2: resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== -lodash@^4.17.21: +lodash@4.17.21, lodash@^4.17.21: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== @@ -4406,6 +4439,11 @@ supports-preserve-symlinks-flag@^1.0.0: resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== +swagger-ui-dist@5.4.2: + version "5.4.2" + resolved "https://registry.yarnpkg.com/swagger-ui-dist/-/swagger-ui-dist-5.4.2.tgz#ff7b936bdfc84673a1823a0f05f3a933ba7ccd4c" + integrity sha512-vT5QxP/NOr9m4gLZl+SpavWI3M9Fdh30+Sdw9rEtZbkqNmNNEPhjXas2xTD9rsJYYdLzAiMfwXvtooWH3xbLJA== + symbol-observable@4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-4.0.0.tgz#5b425f192279e87f2f9b937ac8540d1984b39205" From dc90b618545f96d0fcd05778e7918b511ae7072d Mon Sep 17 00:00:00 2001 From: nicoleamber Date: Wed, 13 Sep 2023 18:14:44 +0800 Subject: [PATCH 2/7] [M1_TR-207] Backend for M1_TR-5 --- server/src/api/jobs/dto/job-query.dto.ts | 26 +++++++- server/src/api/jobs/jobs.controller.spec.ts | 56 +++++++++++++++- server/src/api/jobs/jobs.service.spec.ts | 50 ++++++++++++++- server/src/api/jobs/jobs.service.ts | 40 +++++++++++- .../utils/validators/dateRange.validator.ts | 64 +++++++++++++++++++ 5 files changed, 230 insertions(+), 6 deletions(-) create mode 100644 server/src/utils/validators/dateRange.validator.ts diff --git a/server/src/api/jobs/dto/job-query.dto.ts b/server/src/api/jobs/dto/job-query.dto.ts index a8f26d5..2ce8e7b 100644 --- a/server/src/api/jobs/dto/job-query.dto.ts +++ b/server/src/api/jobs/dto/job-query.dto.ts @@ -1,6 +1,8 @@ import { ApiProperty } from '@nestjs/swagger'; +import { Status, Tag } from '@prisma/client'; import { Transform } from 'class-transformer'; -import { IsNumber, IsOptional } from 'class-validator'; +import { IsEnum, IsNumber, IsOptional } from 'class-validator'; +import { IsDateRange } from '../../../utils/validators/dateRange.validator'; export class JobQueryDto { @ApiProperty() @@ -8,9 +10,29 @@ export class JobQueryDto { @Transform(({ value }) => parseInt(value)) page: number; - @ApiProperty({ required: false}) + @ApiProperty({ required: false }) @IsOptional() @IsNumber() @Transform(({ value }) => parseInt(value)) perPage?: number; + + @ApiProperty({ required: false }) + @IsOptional() + @IsEnum(Tag) + tag: Tag; + + @ApiProperty({ required: false }) + @IsOptional() + @IsEnum(Status) + status: Status; + + @ApiProperty({ required: false }) + @IsOptional() + @IsDateRange() + startDate: Date; + + @ApiProperty({ required: false }) + @IsOptional() + @IsDateRange() + endDate: Date; } diff --git a/server/src/api/jobs/jobs.controller.spec.ts b/server/src/api/jobs/jobs.controller.spec.ts index 57d3453..db2043d 100644 --- a/server/src/api/jobs/jobs.controller.spec.ts +++ b/server/src/api/jobs/jobs.controller.spec.ts @@ -1,4 +1,5 @@ import { Test, TestingModule } from '@nestjs/testing'; +import { $Enums } from '@prisma/client'; import { JobsController } from './jobs.controller'; import { JobsService } from './jobs.service'; @@ -25,7 +26,7 @@ describe('JobsController', () => { describe('findAll', () => { it('should return a list of jobs with the total number of jobs', async () => { - const jobQuery = { page: 1, perPage: 2 }; + const jobQuery = { page: 1, perPage: 2, tag: null, status: null, startDate: null, endDate: null }; const jobList = { jobs: [ { @@ -66,4 +67,57 @@ describe('JobsController', () => { expect(jobs).toEqual(jobList); }); }); + + describe('filter', () => { + it('should return a filtered list of jobs with total number of filtered jobs', async () => { + const mockTags = [$Enums.Tag.TAG_A]; + const mockStatus = $Enums.Status.APPROVED; + const jobQuery = { page: 1, perPage: 2, tag: mockTags[0], status: mockStatus, startDate: null, endDate: null }; + + const jobList = { + jobs: [ + { + id: 1, + title: "Job A", + type: "Type A", + tags: mockTags, + remarks: null, + customerId: 1, + paymentMethod: null, + userId: 1, + pipelinePhase: null, + createdAt: new Date("2023-09-07T09:38:42.296Z"), + updatedAt: new Date("2023-09-07T09:38:42.296Z"), + estimation: { + status: mockStatus, + totalCost: 1000.00 + } + }, + { + id: 2, + title: "Job B", + type: "Type B", + tags: [], + remarks: null, + customerId: 2, + paymentMethod: null, + userId: 2, + pipelinePhase: null, + createdAt: new Date("2023-09-07T09:38:42.296Z"), + updatedAt: new Date("2023-09-07T09:38:42.296Z"), + } + ], + count: 2 + }; + + const filteredJobList = { jobs: jobList.jobs.slice(0, 1), count: 1} + + jest.spyOn(mockJobService, 'findAll').mockResolvedValue(filteredJobList); + + const jobs = await jobController.findAll(jobQuery); + + expect(mockJobService.findAll).toHaveBeenCalledWith(jobQuery); + expect(jobs).toEqual(filteredJobList); + }); + }); }); diff --git a/server/src/api/jobs/jobs.service.spec.ts b/server/src/api/jobs/jobs.service.spec.ts index dbd1f80..1faebd1 100644 --- a/server/src/api/jobs/jobs.service.spec.ts +++ b/server/src/api/jobs/jobs.service.spec.ts @@ -1,4 +1,5 @@ import { Test, TestingModule } from '@nestjs/testing'; +import { $Enums } from '@prisma/client'; import { PrismaService } from '../../database/connection.service'; import { JobsService } from './jobs.service'; @@ -29,7 +30,7 @@ describe('JobsService', () => { describe('findAll', () => { it('should be return a list of jobs with the total number of jobs', async () => { - const jobQuery = { page: 1, perPage: 2 }; + const jobQuery = { page: 1, perPage: 2, tag: null, status: null, startDate: null, endDate: null }; const mockJobList = [ { id: 1, @@ -67,4 +68,51 @@ describe('JobsService', () => { expect(jobs).toEqual({ jobs: mockJobList, count: mockJobCount }); }); }); + + describe('filter', () => { + it('should be return a filtered list of jobs with the total number of filtered jobs', async () => { + const mockTags = [$Enums.Tag.TAG_A]; + const mockStatus = $Enums.Status.APPROVED; + const jobQuery = { page: 1, perPage: 2, tag: mockTags[0], status: mockStatus, startDate: null, endDate: null } + const mockJobList = [ + { + id: 1, + title: "Job A", + type: "Type A", + tags: mockTags, + remarks: null, + customerId: 1, + paymentMethod: null, + userId: 1, + pipelinePhase: null, + createdAt: new Date("2023-09-07T09:38:42.296Z"), + updatedAt: new Date("2023-09-07T09:38:42.296Z"), + estimation: { + status: mockStatus, + totalCost: 1000.00 + } + }, + { + id: 2, + title: "Job B", + type: "Type B", + tags: [], + remarks: null, + customerId: 2, + paymentMethod: null, + userId: 2, + pipelinePhase: null, + createdAt: new Date("2023-09-07T09:38:42.296Z"), + updatedAt: new Date("2023-09-07T09:38:42.296Z"), + } + ]; + const mockJobCount = 1; + + mockPrismaService.$transaction.mockResolvedValue([ mockJobList.slice(0, 1), mockJobCount ]); + + const jobs = await jobService.findAll(jobQuery); + + expect(jobs).toEqual({ jobs: mockJobList.slice(0, 1), count: mockJobCount }); + }); + }); }); diff --git a/server/src/api/jobs/jobs.service.ts b/server/src/api/jobs/jobs.service.ts index 0dc8e2d..37bfb35 100644 --- a/server/src/api/jobs/jobs.service.ts +++ b/server/src/api/jobs/jobs.service.ts @@ -9,11 +9,47 @@ export class JobsService { async findAll(query: JobQueryDto): Promise { try { - const { page, perPage = 12 } = query; + const { page, tag, status, startDate, endDate, perPage = 12 } = query; const skip = (page - 1) * perPage; + const tagCondition = tag ? { + tags: { + has: tag + } + } : {}; + + const statusCondition = status ? { + estimation: { + status: { + equals: status + } + } + } : {}; + + let dateRangeCondition = {} + if (startDate && endDate) { + const newEndDate = new Date(endDate); + newEndDate.setDate(newEndDate.getDate() + 1); + + dateRangeCondition = { + createdAt: { + lt: newEndDate, + gte: new Date(startDate), + } + }; + } + + const whereCondition = { + AND: [ + tagCondition, + statusCondition, + dateRangeCondition + ] + } + const [ jobs, count ] = await this.prisma.$transaction([ this.prisma.job.findMany({ + where: whereCondition, take: perPage, skip, include: { @@ -45,7 +81,7 @@ export class JobsService { } } }), - this.prisma.job.count() + this.prisma.job.count({ where: whereCondition }) ]); return { jobs, count }; diff --git a/server/src/utils/validators/dateRange.validator.ts b/server/src/utils/validators/dateRange.validator.ts new file mode 100644 index 0000000..0755390 --- /dev/null +++ b/server/src/utils/validators/dateRange.validator.ts @@ -0,0 +1,64 @@ +import { + registerDecorator, + ValidationArguments, + ValidationOptions, + ValidatorConstraint, + ValidatorConstraintInterface, +} from 'class-validator'; + + +@ValidatorConstraint({ async: false }) +export class DateRangeValidator implements ValidatorConstraintInterface { + validate(value: number | Date, args: ValidationArguments) { + const { object } = args; + + const startDateStr = object['startDate']; + const endDateStr = object['endDate']; + + if (startDateStr && !endDateStr) { + args.constraints.push('endDateMissing'); + return false; + } + + if (endDateStr && !startDateStr) { + args.constraints.push('startDateMissing'); + return false; + } + + const startDate = new Date(startDateStr); + const endDate = new Date(endDateStr); + + if (endDate < startDate) { + args.constraints.push('invalidDateRange'); + return false; + } + + return true; + } + + defaultMessage(args: ValidationArguments) { + if (args.constraints.includes('startDateMissing')) { + return 'startDate is missing.'; + } + + if (args.constraints.includes('endDateMissing')) { + return 'endDate is missing.'; + } + + if (args.constraints.includes('invalidDateRange')) { + return 'endDate should be greater than startDate.'; + } + } +} + +export function IsDateRange(validationOptions?: ValidationOptions) { + return function (object: object, propertyName: string) { + registerDecorator({ + target: object.constructor, + propertyName: propertyName, + options: validationOptions, + constraints: [], + validator: DateRangeValidator, + }); + }; +} From f1fd0482fb6db3acda6f62a474aca09ff57cb2d8 Mon Sep 17 00:00:00 2001 From: nicoleamber Date: Thu, 7 Sep 2023 11:04:07 +0800 Subject: [PATCH 3/7] [M1_TR-212] Markup for M1_TR-211 --- web/.eslintrc.js | 9 +- web/src/app/page.tsx | 270 +++++++++++++++++- web/src/assets/icons/FilterCog.tsx | 20 ++ web/src/assets/icons/FilterRemove.tsx | 20 ++ web/src/components/atoms/Pagination.tsx | 32 +++ web/src/components/atoms/StatusChip.tsx | 30 ++ .../molecules/CreatedDateRangeFilter.tsx | 60 ++++ .../molecules/EstimationStatusFilter.tsx | 50 ++++ web/src/components/molecules/NavBarItem.tsx | 6 + web/src/components/molecules/SearchBar.tsx | 41 +++ web/src/components/molecules/TagFilter.tsx | 48 ++++ web/src/components/organisms/JobListTable.tsx | 125 ++++++++ .../organisms/SearchFilterHeader.tsx | 70 +++++ web/src/utils/constants/navbarMenu.ts | 6 +- web/styles/Filter.module.css | 36 +++ 15 files changed, 807 insertions(+), 16 deletions(-) create mode 100644 web/src/assets/icons/FilterCog.tsx create mode 100644 web/src/assets/icons/FilterRemove.tsx create mode 100644 web/src/components/atoms/Pagination.tsx create mode 100644 web/src/components/atoms/StatusChip.tsx create mode 100644 web/src/components/molecules/CreatedDateRangeFilter.tsx create mode 100644 web/src/components/molecules/EstimationStatusFilter.tsx create mode 100644 web/src/components/molecules/SearchBar.tsx create mode 100644 web/src/components/molecules/TagFilter.tsx create mode 100644 web/src/components/organisms/JobListTable.tsx create mode 100644 web/src/components/organisms/SearchFilterHeader.tsx create mode 100644 web/styles/Filter.module.css diff --git a/web/.eslintrc.js b/web/.eslintrc.js index 9372a20..4edee31 100644 --- a/web/.eslintrc.js +++ b/web/.eslintrc.js @@ -1,6 +1,7 @@ module.exports = { - extends: ["eslint:recommended"], - rules: { - "no-undef": "off", - }, + extends: ['eslint:recommended'], + rules: { + 'no-undef': 'off', + 'no-unused-vars': 'off' + } }; diff --git a/web/src/app/page.tsx b/web/src/app/page.tsx index 6c76689..ed4cf51 100644 --- a/web/src/app/page.tsx +++ b/web/src/app/page.tsx @@ -1,11 +1,263 @@ -import { Box, Typography } from "@mui/material"; +'use client'; -export default function Home() { - return ( -
- - Content goes here - -
- ) +import Pagination from '@/components/atoms/Pagination'; +import JobListTable from '@/components/organisms/JobListTable'; +import SearchFilterHeader from '@/components/organisms/SearchFilterHeader'; +import { Grid } from '@mui/material'; +import { useState } from 'react'; + +export interface Column { + key: string; + label: string; + width?: number; +} + +interface Estimation { + status: string; + cost: number; +} + +export interface JobData { + id: number; + title: string; + customer: string; + tags: Array; + schedules: Array; + estimation: Estimation; + personInCharge: string; + pipelinePhase: string; + createdAt: string; + [key: string]: string | number | string[] | Estimation; } + +const columns: Column[] = [ + { key: 'title', label: 'Job Title', width: 200 }, + { key: 'customer', label: 'Customer Name', width: 170 }, + { key: 'tags', label: 'Tags', width: 160 }, + { key: 'schedules', label: 'Work Schedule', width: 200 }, + { key: 'estimationStatus', label: 'Estimation Status', width: 180 }, + { key: 'personInCharge', label: 'Person in Charge', width: 170 }, + { key: 'pipelinePhase', label: 'Pipeline Phase', width: 150 }, + { key: 'cost', label: 'Cost', width: 120 }, + { key: 'createdAt', label: 'Created At', width: 120 } +]; + +export default function JobList() { + const [page, setPage] = useState(1); + + const handlePageChange = (value: number) => { + setPage(value); + }; + return ( +
+ + + + + + + + + + + +
+ ); +} + +const data: JobData[] = [ + { + id: 1, + title: 'New Summit', + customer: 'John Doe', + tags: ['Tag A', 'Tag B'], + schedules: ['5-25-2023 5:00 - 15:00'], + estimation: { + status: 'Approved', + cost: 650.0 + }, + personInCharge: 'Michael Murry', + pipelinePhase: 'Delivery', + createdAt: new Date('5-2-2023').toLocaleDateString('en-US', { + year: 'numeric', + month: '2-digit', + day: '2-digit' + }) + }, + { + id: 2, + title: 'New Summit', + customer: 'John Doe', + tags: ['Tag A', 'Tag B', 'Tag C', 'Tag D', 'Tag E'], + schedules: ['5-25-2023 5:00 - 15:00', '5-26-2023 5:00 - 15:00'], + estimation: { + status: 'Making', + cost: 650.0 + }, + personInCharge: 'Michael Murry', + pipelinePhase: 'Delivery', + createdAt: new Date('5-2-2023').toLocaleDateString('en-US', { + year: 'numeric', + month: '2-digit', + day: '2-digit' + }) + }, + { + id: 3, + title: 'New Summit', + customer: 'John Doe', + tags: ['Tag A'], + schedules: ['5-25-2023 5:00 - 15:00'], + estimation: { + status: 'Sent to Customer', + cost: 650.0 + }, + personInCharge: 'Michael Murry', + pipelinePhase: 'Delivery', + createdAt: new Date('5-2-2023').toLocaleDateString('en-US', { + year: 'numeric', + month: '2-digit', + day: '2-digit' + }) + }, + { + id: 4, + title: 'New Summit', + customer: 'John Doe', + tags: ['Tag A', 'Tag B'], + schedules: ['5-25-2023 5:00 - 15:00'], + estimation: { + status: 'Closed', + cost: 650.0 + }, + personInCharge: 'Michael Murry', + pipelinePhase: 'Delivery', + createdAt: new Date('5-2-2023').toLocaleDateString('en-US', { + year: 'numeric', + month: '2-digit', + day: '2-digit' + }) + }, + { + id: 5, + title: 'New Summit', + customer: 'John Doe', + tags: ['Tag A', 'Tag B'], + schedules: [ + '5-25-2023 5:00 - 15:00', + '5-26-2023 5:00 - 15:00', + '5-26-2023 5:00 - 15:00' + ], + estimation: { + status: 'Not yet Created', + cost: 650.0 + }, + personInCharge: 'Michael Murry', + pipelinePhase: 'Delivery', + createdAt: new Date('5-2-2023').toLocaleDateString('en-US', { + year: 'numeric', + month: '2-digit', + day: '2-digit' + }) + }, + { + id: 6, + title: 'New Summit', + customer: 'John Doe', + tags: ['Tag A'], + schedules: ['5-25-2023 5:00 - 15:00'], + estimation: { + status: 'Not yet Created', + cost: 650.0 + }, + personInCharge: 'Michael Murry', + pipelinePhase: 'Delivery', + createdAt: new Date('5-2-2023').toLocaleDateString('en-US', { + year: 'numeric', + month: '2-digit', + day: '2-digit' + }) + }, + { + id: 7, + title: 'New Summit', + customer: 'John Doe', + tags: ['Tag A'], + schedules: ['5-25-2023 5:00 - 15:00'], + estimation: { + status: 'Not yet Created', + cost: 650.0 + }, + personInCharge: 'Michael Murry', + pipelinePhase: 'Delivery', + createdAt: new Date('5-2-2023').toLocaleDateString('en-US', { + year: 'numeric', + month: '2-digit', + day: '2-digit' + }) + }, + { + id: 8, + title: 'New Summit', + customer: 'John Doe', + tags: ['Tag A'], + schedules: ['5-25-2023 5:00 - 15:00'], + estimation: { + status: 'Not yet Created', + cost: 650.0 + }, + personInCharge: 'Michael Murry', + pipelinePhase: 'Delivery', + createdAt: new Date('5-2-2023').toLocaleDateString('en-US', { + year: 'numeric', + month: '2-digit', + day: '2-digit' + }) + }, + { + id: 9, + title: 'New Summit', + customer: 'John Doe', + tags: ['Tag A'], + schedules: ['5-25-2023 5:00 - 15:00'], + estimation: { + status: 'Not yet Created', + cost: 650.0 + }, + personInCharge: 'Michael Murry', + pipelinePhase: 'Delivery', + createdAt: new Date('5-2-2023').toLocaleDateString('en-US', { + year: 'numeric', + month: '2-digit', + day: '2-digit' + }) + }, + { + id: 10, + title: 'New Summit', + customer: 'John Doe', + tags: ['Tag A'], + schedules: ['5-25-2023 5:00 - 15:00'], + estimation: { + status: 'Not yet Created', + cost: 650.0 + }, + personInCharge: 'Michael Murry', + pipelinePhase: 'Delivery', + createdAt: new Date('5-2-2023').toLocaleDateString('en-US', { + year: 'numeric', + month: '2-digit', + day: '2-digit' + }) + } +]; diff --git a/web/src/assets/icons/FilterCog.tsx b/web/src/assets/icons/FilterCog.tsx new file mode 100644 index 0000000..4d0ae75 --- /dev/null +++ b/web/src/assets/icons/FilterCog.tsx @@ -0,0 +1,20 @@ +import { SvgIcon } from '@mui/material'; + +interface Props { + style?: string; +} + +export const FilterCog = ({ style = '' }: Props) => { + return ( + + + + + + ); +}; diff --git a/web/src/assets/icons/FilterRemove.tsx b/web/src/assets/icons/FilterRemove.tsx new file mode 100644 index 0000000..8b40a87 --- /dev/null +++ b/web/src/assets/icons/FilterRemove.tsx @@ -0,0 +1,20 @@ +import { SvgIcon } from '@mui/material'; + +interface Props { + style?: string; +} + +export const FilterRemove = ({ style = '' }: Props) => { + return ( + + + + + + ); +}; diff --git a/web/src/components/atoms/Pagination.tsx b/web/src/components/atoms/Pagination.tsx new file mode 100644 index 0000000..b2e416e --- /dev/null +++ b/web/src/components/atoms/Pagination.tsx @@ -0,0 +1,32 @@ +import { Pagination as MUIPagination } from '@mui/material'; +import { FC } from 'react'; + +interface Props { + count: number; + page: number; + onChange: (page: number) => void; +} + +const Pagination: FC = ({ count = 12, page = 1, onChange }: Props) => { + const handlePageChange = ( + event: React.ChangeEvent, + newPage: number + ) => { + onChange(newPage); + }; + + return ( + + ); +}; + +export default Pagination; diff --git a/web/src/components/atoms/StatusChip.tsx b/web/src/components/atoms/StatusChip.tsx new file mode 100644 index 0000000..6a9a512 --- /dev/null +++ b/web/src/components/atoms/StatusChip.tsx @@ -0,0 +1,30 @@ +import { Chip } from '@mui/material'; +import { FC } from 'react'; + +interface Props { + label: string; +} + +const colors: Record = { + 'Not yet Created': '#FFB4AF', + Making: '#FDFF8F', + Approved: '#8AFFB2', + 'Sent to Customer': '#84C1FF', + Closed: '#65707b33' +}; + +const StatusChip: FC = ({ label }) => { + const color = colors[label] || '#65707b33'; + + return ( + + ); +}; + +export default StatusChip; diff --git a/web/src/components/molecules/CreatedDateRangeFilter.tsx b/web/src/components/molecules/CreatedDateRangeFilter.tsx new file mode 100644 index 0000000..a75ccab --- /dev/null +++ b/web/src/components/molecules/CreatedDateRangeFilter.tsx @@ -0,0 +1,60 @@ +'use-client'; + +import { Box } from '@mui/material'; +import { DatePicker } from '@mui/x-date-pickers'; +import { AdapterMoment } from '@mui/x-date-pickers/AdapterMoment'; +import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; +import { Moment } from 'moment'; +import { useState } from 'react'; +import styles from 'styles/Filter.module.css'; + +const CreateDateRangeFilter = () => { + const [startDate, setStartDate] = useState(); + const [endDate, setEndDate] = useState(); + + const handleStartDateChange = (date: Moment | null): void => { + setStartDate(date); + }; + + const handleEndDateChange = (date: Moment | null): void => { + setEndDate(date); + }; + + return ( + + + + {' - '} + + + + ); +}; + +export default CreateDateRangeFilter; diff --git a/web/src/components/molecules/EstimationStatusFilter.tsx b/web/src/components/molecules/EstimationStatusFilter.tsx new file mode 100644 index 0000000..210d6fe --- /dev/null +++ b/web/src/components/molecules/EstimationStatusFilter.tsx @@ -0,0 +1,50 @@ +import { + FormControl, + InputLabel, + MenuItem, + Select, + SelectChangeEvent +} from '@mui/material'; +import { useState } from 'react'; +import styles from 'styles/Filter.module.css'; + +const estimationStatus = [ + { id: 1, status: 'Not yet Created' }, + { id: 2, status: 'Making' }, + { id: 3, status: 'Approved' }, + { id: 4, status: 'Sent to Customer' }, + { id: 5, status: 'Closed' } +]; + +const EstimationStatusFilter = () => { + const [status, setStatus] = useState(''); + + const handleStatusChange = (e: SelectChangeEvent) => { + setStatus(e.target.value); + }; + + return ( + + + Estimation Status + + + + ); +}; + +export default EstimationStatusFilter; diff --git a/web/src/components/molecules/NavBarItem.tsx b/web/src/components/molecules/NavBarItem.tsx index 9ad1c54..c0ad149 100644 --- a/web/src/components/molecules/NavBarItem.tsx +++ b/web/src/components/molecules/NavBarItem.tsx @@ -1,5 +1,8 @@ +'use client' + import Link from 'next/link' import React, { FC } from 'react' +import { usePathname } from 'next/navigation' import { Box, Typography } from '@mui/material' import {SvgIconComponent} from '@mui/icons-material' @@ -25,6 +28,9 @@ const NavBarItem: FC = ({ linkTo = "/" }) => { + const pathname = usePathname() + active = pathname === linkTo ? true : false + const backgroundColor = active ? 'primary.100' : 'primary.700' const textColor = active? 'dark': 'white' diff --git a/web/src/components/molecules/SearchBar.tsx b/web/src/components/molecules/SearchBar.tsx new file mode 100644 index 0000000..701fe41 --- /dev/null +++ b/web/src/components/molecules/SearchBar.tsx @@ -0,0 +1,41 @@ +import SearchIcon from '@mui/icons-material/Search'; +import { + FormControl, + InputAdornment, + InputLabel, + OutlinedInput +} from '@mui/material'; +import { useState } from 'react'; +import styles from 'styles/Filter.module.css'; + +const SearchBar = () => { + const [searchKeyword, setSearchKeyword] = useState(''); + + const handleSearch = (e: React.ChangeEvent) => { + setSearchKeyword(e.target.value); + }; + + return ( + + Search Job + + + + } + /> + + ); +}; + +export default SearchBar; diff --git a/web/src/components/molecules/TagFilter.tsx b/web/src/components/molecules/TagFilter.tsx new file mode 100644 index 0000000..ea603bc --- /dev/null +++ b/web/src/components/molecules/TagFilter.tsx @@ -0,0 +1,48 @@ +import { + FormControl, + InputLabel, + MenuItem, + Select, + SelectChangeEvent +} from '@mui/material'; +import { useState } from 'react'; +import styles from 'styles/Filter.module.css'; + +const tags = [ + { id: 1, value: 'TAG_A', name: 'Tag A' }, + { id: 2, value: 'TAG_B', name: 'Tag B' }, + { id: 3, value: 'TAG_C', name: 'Tag C' } +]; + +const TagFilter = () => { + const [tag, setTag] = useState(''); + + const handleTagChange = (e: SelectChangeEvent) => { + setTag(e.target.value); + }; + + return ( + + + Tag + + + + ); +}; + +export default TagFilter; diff --git a/web/src/components/organisms/JobListTable.tsx b/web/src/components/organisms/JobListTable.tsx new file mode 100644 index 0000000..d301622 --- /dev/null +++ b/web/src/components/organisms/JobListTable.tsx @@ -0,0 +1,125 @@ +/* eslint-disable no-mixed-spaces-and-tabs */ +import { Column, JobData } from '@/app/page'; +import StatusChip from '@/components/atoms/StatusChip'; +import { + Box, + Paper, + Stack, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + Typography +} from '@mui/material'; +import { FC } from 'react'; + +interface Props { + columns: Column[]; + data: JobData[]; +} + +const JobListTable: FC = ({ columns, data }: Props) => { + const renderTableCellContent = ( + column: Column, + row: JobData + ): JSX.Element => { + switch (column.key) { + case 'tags': + return ( + + {row.tags.map((tag, key) => ( + + ))} + + ); + case 'schedules': + return ( + + {row.schedules.map((schedule, key) => ( + + {schedule} + + ))} + + ); + case 'pipelinePhase': + return ; + case 'estimationStatus': + return ; + case 'cost': + return ( + + {`₱ ${row.estimation.cost.toFixed(2)}`} + + ); + default: + return ( + + {row[column.key] as string} + + ); + } + }; + + return ( + + + + + + + + {columns.map((column) => ( + + + {column.label} + + + ))} + + + + {data.map((row: JobData) => ( + + {columns.map( + (column: Column, index) => ( + + {renderTableCellContent( + column, + row + )} + + ) + )} + + ))} + +
+
+
+
+
+ ); +}; + +export default JobListTable; diff --git a/web/src/components/organisms/SearchFilterHeader.tsx b/web/src/components/organisms/SearchFilterHeader.tsx new file mode 100644 index 0000000..772cd20 --- /dev/null +++ b/web/src/components/organisms/SearchFilterHeader.tsx @@ -0,0 +1,70 @@ +'use-client'; + +import { FilterCog } from '@/assets/icons/FilterCog'; +import { FilterRemove } from '@/assets/icons/FilterRemove'; +import TableRows from '@mui/icons-material/TableRows'; +import { Box, Button, Collapse } from '@mui/material'; +import { useState } from 'react'; +import styles from 'styles/Filter.module.css'; +import CreateDateRangeFilter from '../molecules/CreatedDateRangeFilter'; +import EstimationStatusFilter from '../molecules/EstimationStatusFilter'; +import SearchBar from '../molecules/SearchBar'; +import TagFilter from '../molecules/TagFilter'; + +const SearchFilterHeader = (): JSX.Element => { + const [isExpanded, setIsExpanded] = useState(false); + + const toggleFilters = () => { + setIsExpanded(!isExpanded); + }; + + return ( + + + + + + + + + + + + + + + ); +}; + +export default SearchFilterHeader; diff --git a/web/src/utils/constants/navbarMenu.ts b/web/src/utils/constants/navbarMenu.ts index 1d2cfc2..305e460 100644 --- a/web/src/utils/constants/navbarMenu.ts +++ b/web/src/utils/constants/navbarMenu.ts @@ -1,6 +1,6 @@ import { - Home, CalendarMonth, + Home, Person, SvgIconComponent, } from "@mui/icons-material"; @@ -20,11 +20,11 @@ export const Menus: IMenu[] = [ { name: "Calendar", Icon: CalendarMonth, - href: "/", + href: "/calendar", }, { name: "Customer", Icon: Person, - href: "/", + href: "/customer", }, ]; diff --git a/web/styles/Filter.module.css b/web/styles/Filter.module.css new file mode 100644 index 0000000..4194212 --- /dev/null +++ b/web/styles/Filter.module.css @@ -0,0 +1,36 @@ +.button { + height: 52px; + width: 52px; + background-color: white; + transition: background-color 0.3s; +} + +.button:hover { + background-color: #2A3F52; +} + +.active { + background-color: #2A3F52; +} + +.icon { + height: 20px; + width: 20px; + transition: fill 0.3s; + fill: #3E5367; +} + +.iconWhite { + height: 20px; + width: 20px; + transition: fill 0.3s; + fill: white; +} + +.button:hover .icon { + fill: white; +} + +.input { + background-color: white; +} From 16502018cb0d4f3a91b60d53a9473d1dde845a74 Mon Sep 17 00:00:00 2001 From: nicoleamber Date: Tue, 12 Sep 2023 14:53:51 +0800 Subject: [PATCH 4/7] [M1_TR-212] Refactored structure --- web/src/app/job/context.ts | 4 ++ web/src/app/job/hooks.ts | 22 ++++++ web/src/app/job/page.tsx | 40 +++++++++++ .../{Pagination.tsx => Pagination/index.tsx} | 13 ++-- web/src/components/atoms/StatusChip.tsx | 30 -------- web/src/components/atoms/StatusChip/index.tsx | 17 +++++ .../molecules/CreatedDateRangeFilter/hooks.ts | 22 ++++++ .../index.tsx} | 17 ++--- .../molecules/EstimationStatusFilter.tsx | 50 -------------- .../molecules/EstimationStatusFilter/hooks.ts | 15 ++++ .../EstimationStatusFilter/index.tsx | 33 +++++++++ .../components/molecules/SearchBar/hooks.ts | 15 ++++ .../{SearchBar.tsx => SearchBar/index.tsx} | 10 +-- web/src/components/molecules/TagFilter.tsx | 48 ------------- .../components/molecules/TagFilter/hooks.ts | 15 ++++ .../components/molecules/TagFilter/index.tsx | 33 +++++++++ .../index.tsx} | 20 +++--- .../organisms/SearchFilterHeader/hooks.ts | 14 ++++ .../index.tsx} | 18 ++--- web/{ => src}/styles/Filter.module.css | 0 web/src/utils/constants/estimationStatus.ts | 7 ++ .../constants/jobTableData.ts} | 69 +------------------ web/src/utils/constants/statusChipColor.ts | 11 +++ web/src/utils/constants/tags.ts | 5 ++ web/src/utils/types/job.ts | 28 ++++++++ 25 files changed, 311 insertions(+), 245 deletions(-) create mode 100644 web/src/app/job/context.ts create mode 100644 web/src/app/job/hooks.ts create mode 100644 web/src/app/job/page.tsx rename web/src/components/atoms/{Pagination.tsx => Pagination/index.tsx} (62%) delete mode 100644 web/src/components/atoms/StatusChip.tsx create mode 100644 web/src/components/atoms/StatusChip/index.tsx create mode 100644 web/src/components/molecules/CreatedDateRangeFilter/hooks.ts rename web/src/components/molecules/{CreatedDateRangeFilter.tsx => CreatedDateRangeFilter/index.tsx} (73%) delete mode 100644 web/src/components/molecules/EstimationStatusFilter.tsx create mode 100644 web/src/components/molecules/EstimationStatusFilter/hooks.ts create mode 100644 web/src/components/molecules/EstimationStatusFilter/index.tsx create mode 100644 web/src/components/molecules/SearchBar/hooks.ts rename web/src/components/molecules/{SearchBar.tsx => SearchBar/index.tsx} (73%) delete mode 100644 web/src/components/molecules/TagFilter.tsx create mode 100644 web/src/components/molecules/TagFilter/hooks.ts create mode 100644 web/src/components/molecules/TagFilter/index.tsx rename web/src/components/organisms/{JobListTable.tsx => JobListTable/index.tsx} (86%) create mode 100644 web/src/components/organisms/SearchFilterHeader/hooks.ts rename web/src/components/organisms/{SearchFilterHeader.tsx => SearchFilterHeader/index.tsx} (74%) rename web/{ => src}/styles/Filter.module.css (100%) create mode 100644 web/src/utils/constants/estimationStatus.ts rename web/src/{app/page.tsx => utils/constants/jobTableData.ts} (77%) create mode 100644 web/src/utils/constants/statusChipColor.ts create mode 100644 web/src/utils/constants/tags.ts create mode 100644 web/src/utils/types/job.ts diff --git a/web/src/app/job/context.ts b/web/src/app/job/context.ts new file mode 100644 index 0000000..cc54b7d --- /dev/null +++ b/web/src/app/job/context.ts @@ -0,0 +1,4 @@ +import { JobTable } from '@/utils/types/job'; +import { createContext } from 'react'; + +export const JobListContext = createContext(undefined); diff --git a/web/src/app/job/hooks.ts b/web/src/app/job/hooks.ts new file mode 100644 index 0000000..3d559dd --- /dev/null +++ b/web/src/app/job/hooks.ts @@ -0,0 +1,22 @@ +import { JobTable } from '@/utils/types/job'; +import { useContext, useState } from 'react'; +import { JobListContext } from './context'; + +export const useJobListContext = (): JobTable => { + const jobs = useContext(JobListContext); + + if (jobs === undefined) { + throw new Error('Missing JobListContext'); + } + + return jobs; +}; + +export const useHooks = () => { + const [page, setPage] = useState(1); + + return { + page, + setPage + }; +}; diff --git a/web/src/app/job/page.tsx b/web/src/app/job/page.tsx new file mode 100644 index 0000000..f6f5116 --- /dev/null +++ b/web/src/app/job/page.tsx @@ -0,0 +1,40 @@ +'use client'; + +import Pagination from '@/components/atoms/Pagination'; +import JobListTable from '@/components/organisms/JobListTable'; +import SearchFilterHeader from '@/components/organisms/SearchFilterHeader'; +import { JobColumns, JobData } from '@/utils/constants/jobTableData'; +import { Grid } from '@mui/material'; +import { JobListContext } from './context'; +import { useHooks } from './hooks'; + +const JobList = (): JSX.Element => { + const { page, setPage } = useHooks(); + + return ( +
+ + + + + + + + + + + + + +
+ ); +}; + +export default JobList; diff --git a/web/src/components/atoms/Pagination.tsx b/web/src/components/atoms/Pagination/index.tsx similarity index 62% rename from web/src/components/atoms/Pagination.tsx rename to web/src/components/atoms/Pagination/index.tsx index b2e416e..ec8c7c8 100644 --- a/web/src/components/atoms/Pagination.tsx +++ b/web/src/components/atoms/Pagination/index.tsx @@ -7,20 +7,15 @@ interface Props { onChange: (page: number) => void; } -const Pagination: FC = ({ count = 12, page = 1, onChange }: Props) => { - const handlePageChange = ( - event: React.ChangeEvent, - newPage: number - ) => { - onChange(newPage); - }; - +const Pagination: FC = ({ count, page, onChange }: Props) => { return ( , newPage: number) => + onChange(newPage) + } color='primary' shape='rounded' showFirstButton diff --git a/web/src/components/atoms/StatusChip.tsx b/web/src/components/atoms/StatusChip.tsx deleted file mode 100644 index 6a9a512..0000000 --- a/web/src/components/atoms/StatusChip.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import { Chip } from '@mui/material'; -import { FC } from 'react'; - -interface Props { - label: string; -} - -const colors: Record = { - 'Not yet Created': '#FFB4AF', - Making: '#FDFF8F', - Approved: '#8AFFB2', - 'Sent to Customer': '#84C1FF', - Closed: '#65707b33' -}; - -const StatusChip: FC = ({ label }) => { - const color = colors[label] || '#65707b33'; - - return ( - - ); -}; - -export default StatusChip; diff --git a/web/src/components/atoms/StatusChip/index.tsx b/web/src/components/atoms/StatusChip/index.tsx new file mode 100644 index 0000000..c031708 --- /dev/null +++ b/web/src/components/atoms/StatusChip/index.tsx @@ -0,0 +1,17 @@ +import { ChipColors } from '@/utils/constants/statusChipColor'; +import { Chip, ChipProps } from '@mui/material'; +import { FC } from 'react'; + +const StatusChip: FC = ({ label }: ChipProps) => { + return ( + + ); +}; + +export default StatusChip; diff --git a/web/src/components/molecules/CreatedDateRangeFilter/hooks.ts b/web/src/components/molecules/CreatedDateRangeFilter/hooks.ts new file mode 100644 index 0000000..fd3c385 --- /dev/null +++ b/web/src/components/molecules/CreatedDateRangeFilter/hooks.ts @@ -0,0 +1,22 @@ +import { Moment } from 'moment'; +import { useState } from 'react'; + +export const useHooks = () => { + const [startDate, setStartDate] = useState(); + const [endDate, setEndDate] = useState(); + + const handleStartDateChange = (date: Moment | null): void => { + setStartDate(date); + }; + + const handleEndDateChange = (date: Moment | null): void => { + setEndDate(date); + }; + + return { + startDate, + handleStartDateChange, + endDate, + handleEndDateChange + }; +}; diff --git a/web/src/components/molecules/CreatedDateRangeFilter.tsx b/web/src/components/molecules/CreatedDateRangeFilter/index.tsx similarity index 73% rename from web/src/components/molecules/CreatedDateRangeFilter.tsx rename to web/src/components/molecules/CreatedDateRangeFilter/index.tsx index a75ccab..abfc4b8 100644 --- a/web/src/components/molecules/CreatedDateRangeFilter.tsx +++ b/web/src/components/molecules/CreatedDateRangeFilter/index.tsx @@ -1,24 +1,15 @@ 'use-client'; +import styles from '@/styles/Filter.module.css'; import { Box } from '@mui/material'; import { DatePicker } from '@mui/x-date-pickers'; import { AdapterMoment } from '@mui/x-date-pickers/AdapterMoment'; import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; -import { Moment } from 'moment'; -import { useState } from 'react'; -import styles from 'styles/Filter.module.css'; +import { useHooks } from './hooks'; const CreateDateRangeFilter = () => { - const [startDate, setStartDate] = useState(); - const [endDate, setEndDate] = useState(); - - const handleStartDateChange = (date: Moment | null): void => { - setStartDate(date); - }; - - const handleEndDateChange = (date: Moment | null): void => { - setEndDate(date); - }; + const { startDate, handleStartDateChange, endDate, handleEndDateChange } = + useHooks(); return ( diff --git a/web/src/components/molecules/EstimationStatusFilter.tsx b/web/src/components/molecules/EstimationStatusFilter.tsx deleted file mode 100644 index 210d6fe..0000000 --- a/web/src/components/molecules/EstimationStatusFilter.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import { - FormControl, - InputLabel, - MenuItem, - Select, - SelectChangeEvent -} from '@mui/material'; -import { useState } from 'react'; -import styles from 'styles/Filter.module.css'; - -const estimationStatus = [ - { id: 1, status: 'Not yet Created' }, - { id: 2, status: 'Making' }, - { id: 3, status: 'Approved' }, - { id: 4, status: 'Sent to Customer' }, - { id: 5, status: 'Closed' } -]; - -const EstimationStatusFilter = () => { - const [status, setStatus] = useState(''); - - const handleStatusChange = (e: SelectChangeEvent) => { - setStatus(e.target.value); - }; - - return ( - - - Estimation Status - - - - ); -}; - -export default EstimationStatusFilter; diff --git a/web/src/components/molecules/EstimationStatusFilter/hooks.ts b/web/src/components/molecules/EstimationStatusFilter/hooks.ts new file mode 100644 index 0000000..9c1499c --- /dev/null +++ b/web/src/components/molecules/EstimationStatusFilter/hooks.ts @@ -0,0 +1,15 @@ +import { SelectChangeEvent } from '@mui/material'; +import { useState } from 'react'; + +export const useHooks = () => { + const [status, setStatus] = useState(''); + + const handleChange = (e: SelectChangeEvent) => { + setStatus(e.target.value); + }; + + return { + status, + handleChange + }; +}; diff --git a/web/src/components/molecules/EstimationStatusFilter/index.tsx b/web/src/components/molecules/EstimationStatusFilter/index.tsx new file mode 100644 index 0000000..99188dc --- /dev/null +++ b/web/src/components/molecules/EstimationStatusFilter/index.tsx @@ -0,0 +1,33 @@ +import styles from '@/styles/Filter.module.css'; +import { EstimationStatus } from '@/utils/constants/estimationStatus'; +import { FormControl, InputLabel, MenuItem, Select } from '@mui/material'; +import { useHooks } from './hooks'; + +const EstimationStatusFilter = () => { + const { status, handleChange } = useHooks(); + + return ( + + + Estimation Status + + + + ); +}; + +export default EstimationStatusFilter; diff --git a/web/src/components/molecules/SearchBar/hooks.ts b/web/src/components/molecules/SearchBar/hooks.ts new file mode 100644 index 0000000..ab13b6d --- /dev/null +++ b/web/src/components/molecules/SearchBar/hooks.ts @@ -0,0 +1,15 @@ +import { ChangeEvent, useState } from 'react'; + +export const useHooks = () => { + const [searchKeyword, setSearchKeyword] = useState(''); + + const handleSearch = (e: ChangeEvent) => { + setSearchKeyword(e.target.value); + }; + + return { + searchKeyword, + handleSearch + }; + +}; diff --git a/web/src/components/molecules/SearchBar.tsx b/web/src/components/molecules/SearchBar/index.tsx similarity index 73% rename from web/src/components/molecules/SearchBar.tsx rename to web/src/components/molecules/SearchBar/index.tsx index 701fe41..83af8f4 100644 --- a/web/src/components/molecules/SearchBar.tsx +++ b/web/src/components/molecules/SearchBar/index.tsx @@ -1,3 +1,4 @@ +import styles from '@/styles/Filter.module.css'; import SearchIcon from '@mui/icons-material/Search'; import { FormControl, @@ -5,15 +6,10 @@ import { InputLabel, OutlinedInput } from '@mui/material'; -import { useState } from 'react'; -import styles from 'styles/Filter.module.css'; +import { useHooks } from './hooks'; const SearchBar = () => { - const [searchKeyword, setSearchKeyword] = useState(''); - - const handleSearch = (e: React.ChangeEvent) => { - setSearchKeyword(e.target.value); - }; + const { searchKeyword, handleSearch } = useHooks(); return ( diff --git a/web/src/components/molecules/TagFilter.tsx b/web/src/components/molecules/TagFilter.tsx deleted file mode 100644 index ea603bc..0000000 --- a/web/src/components/molecules/TagFilter.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import { - FormControl, - InputLabel, - MenuItem, - Select, - SelectChangeEvent -} from '@mui/material'; -import { useState } from 'react'; -import styles from 'styles/Filter.module.css'; - -const tags = [ - { id: 1, value: 'TAG_A', name: 'Tag A' }, - { id: 2, value: 'TAG_B', name: 'Tag B' }, - { id: 3, value: 'TAG_C', name: 'Tag C' } -]; - -const TagFilter = () => { - const [tag, setTag] = useState(''); - - const handleTagChange = (e: SelectChangeEvent) => { - setTag(e.target.value); - }; - - return ( - - - Tag - - - - ); -}; - -export default TagFilter; diff --git a/web/src/components/molecules/TagFilter/hooks.ts b/web/src/components/molecules/TagFilter/hooks.ts new file mode 100644 index 0000000..e835a95 --- /dev/null +++ b/web/src/components/molecules/TagFilter/hooks.ts @@ -0,0 +1,15 @@ +import { SelectChangeEvent } from '@mui/material'; +import { useState } from 'react'; + +export const useHooks = () => { + const [tag, setTag] = useState(''); + + const handleChange = (e: SelectChangeEvent) => { + setTag(e.target.value); + }; + + return { + tag, + handleChange + }; +}; diff --git a/web/src/components/molecules/TagFilter/index.tsx b/web/src/components/molecules/TagFilter/index.tsx new file mode 100644 index 0000000..d12fed7 --- /dev/null +++ b/web/src/components/molecules/TagFilter/index.tsx @@ -0,0 +1,33 @@ +import styles from '@/styles/Filter.module.css'; +import { Tags } from '@/utils/constants/tags'; +import { FormControl, InputLabel, MenuItem, Select } from '@mui/material'; +import { useHooks } from './hooks'; + +const TagFilter = () => { + const { tag, handleChange } = useHooks(); + + return ( + + + Tag + + + + ); +}; + +export default TagFilter; diff --git a/web/src/components/organisms/JobListTable.tsx b/web/src/components/organisms/JobListTable/index.tsx similarity index 86% rename from web/src/components/organisms/JobListTable.tsx rename to web/src/components/organisms/JobListTable/index.tsx index d301622..2af535b 100644 --- a/web/src/components/organisms/JobListTable.tsx +++ b/web/src/components/organisms/JobListTable/index.tsx @@ -1,6 +1,8 @@ /* eslint-disable no-mixed-spaces-and-tabs */ -import { Column, JobData } from '@/app/page'; +import { useJobListContext } from '@/app/job/hooks'; import StatusChip from '@/components/atoms/StatusChip'; +import { TableColumn } from '@/utils/constants/jobTableData'; +import { JobTableRow } from '@/utils/types/job'; import { Box, Paper, @@ -13,17 +15,13 @@ import { TableRow, Typography } from '@mui/material'; -import { FC } from 'react'; -interface Props { - columns: Column[]; - data: JobData[]; -} +const JobListTable = (): JSX.Element => { + const { columns, data } = useJobListContext(); -const JobListTable: FC = ({ columns, data }: Props) => { const renderTableCellContent = ( - column: Column, - row: JobData + column: TableColumn, + row: JobTableRow ): JSX.Element => { switch (column.key) { case 'tags': @@ -99,10 +97,10 @@ const JobListTable: FC = ({ columns, data }: Props) => { - {data.map((row: JobData) => ( + {data.map((row: JobTableRow) => ( {columns.map( - (column: Column, index) => ( + (column: TableColumn, index) => ( {renderTableCellContent( column, diff --git a/web/src/components/organisms/SearchFilterHeader/hooks.ts b/web/src/components/organisms/SearchFilterHeader/hooks.ts new file mode 100644 index 0000000..bfad85f --- /dev/null +++ b/web/src/components/organisms/SearchFilterHeader/hooks.ts @@ -0,0 +1,14 @@ +import { useState } from 'react'; + +export const useHooks = () => { + const [isExpanded, setIsExpanded] = useState(false); + + const toggleFilters = () => { + setIsExpanded(!isExpanded); + }; + + return { + isExpanded, + toggleFilters + }; +}; diff --git a/web/src/components/organisms/SearchFilterHeader.tsx b/web/src/components/organisms/SearchFilterHeader/index.tsx similarity index 74% rename from web/src/components/organisms/SearchFilterHeader.tsx rename to web/src/components/organisms/SearchFilterHeader/index.tsx index 772cd20..0b4b406 100644 --- a/web/src/components/organisms/SearchFilterHeader.tsx +++ b/web/src/components/organisms/SearchFilterHeader/index.tsx @@ -2,21 +2,17 @@ import { FilterCog } from '@/assets/icons/FilterCog'; import { FilterRemove } from '@/assets/icons/FilterRemove'; +import styles from '@/styles/Filter.module.css'; import TableRows from '@mui/icons-material/TableRows'; import { Box, Button, Collapse } from '@mui/material'; -import { useState } from 'react'; -import styles from 'styles/Filter.module.css'; -import CreateDateRangeFilter from '../molecules/CreatedDateRangeFilter'; -import EstimationStatusFilter from '../molecules/EstimationStatusFilter'; -import SearchBar from '../molecules/SearchBar'; -import TagFilter from '../molecules/TagFilter'; +import CreateDateRangeFilter from '../../molecules/CreatedDateRangeFilter'; +import EstimationStatusFilter from '../../molecules/EstimationStatusFilter'; +import SearchBar from '../../molecules/SearchBar'; +import TagFilter from '../../molecules/TagFilter'; +import { useHooks } from './hooks'; const SearchFilterHeader = (): JSX.Element => { - const [isExpanded, setIsExpanded] = useState(false); - - const toggleFilters = () => { - setIsExpanded(!isExpanded); - }; + const { isExpanded, toggleFilters } = useHooks(); return ( ; - schedules: Array; - estimation: Estimation; - personInCharge: string; - pipelinePhase: string; - createdAt: string; - [key: string]: string | number | string[] | Estimation; -} - -const columns: Column[] = [ +export const JobColumns : TableColumn[] = [ { key: 'title', label: 'Job Title', width: 200 }, { key: 'customer', label: 'Customer Name', width: 170 }, { key: 'tags', label: 'Tags', width: 160 }, @@ -42,40 +12,7 @@ const columns: Column[] = [ { key: 'createdAt', label: 'Created At', width: 120 } ]; -export default function JobList() { - const [page, setPage] = useState(1); - - const handlePageChange = (value: number) => { - setPage(value); - }; - return ( -
- - - - - - - - - - - -
- ); -} - -const data: JobData[] = [ +export const JobData: JobTableRow[] = [ { id: 1, title: 'New Summit', diff --git a/web/src/utils/constants/statusChipColor.ts b/web/src/utils/constants/statusChipColor.ts new file mode 100644 index 0000000..0f15dd6 --- /dev/null +++ b/web/src/utils/constants/statusChipColor.ts @@ -0,0 +1,11 @@ +export interface ChipProps { + label: string; +} + +export const ChipColors: Record = { + 'Not yet Created': '#FFB4AF', + Making: '#FDFF8F', + Approved: '#8AFFB2', + 'Sent to Customer': '#84C1FF', + Closed: '#65707b33' +}; diff --git a/web/src/utils/constants/tags.ts b/web/src/utils/constants/tags.ts new file mode 100644 index 0000000..f021459 --- /dev/null +++ b/web/src/utils/constants/tags.ts @@ -0,0 +1,5 @@ +export const Tags = [ + { id: 1, value: 'TAG_A', name: 'Tag A' }, + { id: 2, value: 'TAG_B', name: 'Tag B' }, + { id: 3, value: 'TAG_C', name: 'Tag C' } +]; diff --git a/web/src/utils/types/job.ts b/web/src/utils/types/job.ts new file mode 100644 index 0000000..816ad2d --- /dev/null +++ b/web/src/utils/types/job.ts @@ -0,0 +1,28 @@ +export interface TableColumn { + key: string; + label: string; + width?: number; +} + +export interface EstimationType { + status: string; + cost: number; +} + +export interface JobTableRow { + id: number; + title: string; + customer: string; + tags: Array; + schedules: Array; + estimation: EstimationType; + personInCharge: string; + pipelinePhase: string; + createdAt: string; + [key: string]: string | number | string[] | EstimationType; +} + +export interface JobTable { + columns: TableColumn[]; + data: JobTableRow[]; +} From 926dafdee761af7c53ca01b8f7f5d9b0373e0211 Mon Sep 17 00:00:00 2001 From: nicoleamber Date: Wed, 13 Sep 2023 14:59:14 +0800 Subject: [PATCH 5/7] [M1_TR-213] Frontend for M1_TR-211 --- server/src/main.ts | 6 +- web/package.json | 3 + web/src/app/job/hooks.ts | 91 ++++++++- web/src/app/job/page.tsx | 70 +++++-- web/src/app/layout.tsx | 29 ++- .../molecules/CreatedDateRangeFilter/hooks.ts | 18 +- .../CreatedDateRangeFilter/index.tsx | 42 +++- .../molecules/StatusDisplay/index.tsx | 27 +++ .../organisms/JobListTable/index.tsx | 7 +- web/src/utils/constants/jobTableData.ts | 189 +----------------- web/src/utils/constants/navbarMenu.ts | 2 +- web/src/utils/constants/statusChipColor.ts | 4 +- web/src/utils/services/axios.ts | 8 + web/src/utils/types/customer.ts | 8 + web/src/utils/types/estimation.ts | 7 + web/src/utils/types/job.ts | 30 ++- web/src/utils/types/schedule.ts | 7 + web/src/utils/types/user.ts | 7 + web/yarn.lock | 97 +++++++++ 19 files changed, 404 insertions(+), 248 deletions(-) create mode 100644 web/src/components/molecules/StatusDisplay/index.tsx create mode 100644 web/src/utils/services/axios.ts create mode 100644 web/src/utils/types/customer.ts create mode 100644 web/src/utils/types/estimation.ts create mode 100644 web/src/utils/types/schedule.ts create mode 100644 web/src/utils/types/user.ts diff --git a/server/src/main.ts b/server/src/main.ts index 8047b12..97c7a1a 100644 --- a/server/src/main.ts +++ b/server/src/main.ts @@ -3,7 +3,11 @@ import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; import { AppModule } from './app.module'; async function bootstrap() { - const app = await NestFactory.create(AppModule); + const app = await NestFactory.create(AppModule, { + cors: { + origin: process.env.FRONTEND_URL + } + }); const config = new DocumentBuilder() .setTitle('Sim-JMS') diff --git a/web/package.json b/web/package.json index 9be89ca..7f3992b 100644 --- a/web/package.json +++ b/web/package.json @@ -14,11 +14,14 @@ "@emotion/styled": "^11.11.0", "@mui/icons-material": "^5.14.6", "@mui/material": "^5.14.3", + "@mui/x-date-pickers": "^6.13.0", "@types/node": "20.4.8", "@types/react": "18.2.18", "@types/react-dom": "18.2.7", + "axios": "^1.5.0", "eslint-config-next": "13.4.12", "jest-junit": "^16.0.0", + "moment": "^2.29.4", "next": "13.4.12", "react": "18.2.0", "react-dom": "18.2.0", diff --git a/web/src/app/job/hooks.ts b/web/src/app/job/hooks.ts index 3d559dd..b9ed0cb 100644 --- a/web/src/app/job/hooks.ts +++ b/web/src/app/job/hooks.ts @@ -1,5 +1,7 @@ -import { JobTable } from '@/utils/types/job'; -import { useContext, useState } from 'react'; +import { axiosInstance } from '@/utils/services/axios'; +import { JobSchema, JobTable, JobTableRow } from '@/utils/types/job'; +import { ScheduleSchema } from '@/utils/types/schedule'; +import { useContext, useEffect, useState } from 'react'; import { JobListContext } from './context'; export const useJobListContext = (): JobTable => { @@ -14,9 +16,92 @@ export const useJobListContext = (): JobTable => { export const useHooks = () => { const [page, setPage] = useState(1); + const [perPage] = useState(12); + const [jobs, setJobs] = useState([]); + const [count, setCount] = useState(0); + const [pageCount, setPageCount] = useState(1); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(); + + useEffect(() => { + axiosInstance + .get('/jobs', { + params: { + page, + perPage + } + }) + .then((response) => { + setCount(response?.data.count); + setJobs(convertTableData(response?.data.jobs)); + setPageCount(Math.ceil(response?.data.count / perPage)); + setIsLoading(false); + }) + .catch((e) => { + console.log(e); + setIsLoading(false); + setError('Something went wrong.'); + }); + }, [page, perPage]); return { + jobs, + count, page, - setPage + pageCount, + isLoading, + error, + setPage, }; }; + +const formatDate = (date: string): string => { + return new Date(date).toLocaleDateString('en-US', { + year: 'numeric', + month: '2-digit', + day: '2-digit' + }); +}; + +const formatTime = (date: string): string => { + return new Date(date).toLocaleTimeString('en-US', { + hour: '2-digit', + minute: '2-digit', + hour12: false, + }); +}; + +const formatSchedules = (schedules: ScheduleSchema[]): string[] => { + const scheduleData = schedules.map((schedule: ScheduleSchema) => { + const startDateTime = `${formatDate(schedule.startDate)} ${formatTime(schedule.startTime)}`; + const endDateTime = `${formatTime(schedule.endTime)}`; + return `${startDateTime} - ${endDateTime}`; + }); + + return scheduleData; +}; + +const formatEnum = (value: string): string => { + let words = value.split('_'); + words = words.map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()); + return words.join(' '); +} + +const convertTableData = (data: JobSchema[]): JobTableRow[] => { + const tableData = data.map((job: JobSchema) => ({ + id: job.id, + title: job.title, + customer: `${job.customer.firstName} ${job.customer.lastName}`, + tags: job.tags.map((tag) => formatEnum(tag)).sort(), + schedules: formatSchedules(job.schedules).sort(), + estimation: { + status: job.estimation?.status ? formatEnum(job.estimation?.status) : 'Not Yet Created', + cost: job.estimation?.totalCost ? `₱ ${job.estimation?.totalCost}` : '-' + }, + personInCharge: `${job.personInCharge.firstName} ${job.personInCharge.lastName}`, + pipelinePhase: formatEnum(job.pipelinePhase), + createdAt: formatDate(job.createdAt) + })); + + return tableData; +}; diff --git a/web/src/app/job/page.tsx b/web/src/app/job/page.tsx index f6f5116..98f910d 100644 --- a/web/src/app/job/page.tsx +++ b/web/src/app/job/page.tsx @@ -1,37 +1,67 @@ 'use client'; import Pagination from '@/components/atoms/Pagination'; +import StatusDisplay from '@/components/molecules/StatusDisplay'; import JobListTable from '@/components/organisms/JobListTable'; import SearchFilterHeader from '@/components/organisms/SearchFilterHeader'; -import { JobColumns, JobData } from '@/utils/constants/jobTableData'; -import { Grid } from '@mui/material'; +import { JobColumns } from '@/utils/constants/jobTableData'; +import { Grid, Typography } from '@mui/material'; +import { Fragment } from 'react'; import { JobListContext } from './context'; import { useHooks } from './hooks'; const JobList = (): JSX.Element => { - const { page, setPage } = useHooks(); + const { page, jobs, count, pageCount, setPage, isLoading, error } = + useHooks(); return (
- - - + value={{ columns: JobColumns, data: jobs }}> + {isLoading ? ( + + ) : error ? ( + + ) : ( + + + + + {!count ? ( + + + No jobs found + + + ) : ( + + + + + + + + + )} - - - - - - - + )}
); diff --git a/web/src/app/layout.tsx b/web/src/app/layout.tsx index ddbd120..1f643ee 100644 --- a/web/src/app/layout.tsx +++ b/web/src/app/layout.tsx @@ -1,24 +1,23 @@ -import type { Metadata } from 'next' -import { ThemeProvider } from '@mui/material' +import { ThemeProvider } from '@mui/material'; +import type { Metadata } from 'next'; -import { theme } from '@/assets/theme' +import { theme } from '@/assets/theme'; import Wrapper from './wrapper'; export const metadata: Metadata = { - title: 'Create Next App', - description: 'Generated by create next app', -} + title: 'JMS' +}; export default function RootLayout({ - children, + children }: { - children: React.ReactNode + children: React.ReactNode; }) { - return ( - - - {children} - - - ) + return ( + + + {children} + + + ); } diff --git a/web/src/components/molecules/CreatedDateRangeFilter/hooks.ts b/web/src/components/molecules/CreatedDateRangeFilter/hooks.ts index fd3c385..b0ab4b4 100644 --- a/web/src/components/molecules/CreatedDateRangeFilter/hooks.ts +++ b/web/src/components/molecules/CreatedDateRangeFilter/hooks.ts @@ -1,9 +1,10 @@ -import { Moment } from 'moment'; -import { useState } from 'react'; +import moment, { type Moment } from 'moment'; +import { useEffect, useState } from 'react'; export const useHooks = () => { - const [startDate, setStartDate] = useState(); - const [endDate, setEndDate] = useState(); + const [startDate, setStartDate] = useState(moment().startOf('month')); + const [endDate, setEndDate] = useState(moment()); + const [isInvalidDate, setIsInvalidDate] = useState(false); const handleStartDateChange = (date: Moment | null): void => { setStartDate(date); @@ -13,10 +14,17 @@ export const useHooks = () => { setEndDate(date); }; + useEffect(() => { + if (startDate && endDate) { + setIsInvalidDate(endDate <= startDate); + } + }, [startDate, endDate]); + return { startDate, handleStartDateChange, endDate, - handleEndDateChange + handleEndDateChange, + isInvalidDate }; }; diff --git a/web/src/components/molecules/CreatedDateRangeFilter/index.tsx b/web/src/components/molecules/CreatedDateRangeFilter/index.tsx index abfc4b8..9746682 100644 --- a/web/src/components/molecules/CreatedDateRangeFilter/index.tsx +++ b/web/src/components/molecules/CreatedDateRangeFilter/index.tsx @@ -1,6 +1,5 @@ 'use-client'; -import styles from '@/styles/Filter.module.css'; import { Box } from '@mui/material'; import { DatePicker } from '@mui/x-date-pickers'; import { AdapterMoment } from '@mui/x-date-pickers/AdapterMoment'; @@ -8,38 +7,67 @@ import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; import { useHooks } from './hooks'; const CreateDateRangeFilter = () => { - const { startDate, handleStartDateChange, endDate, handleEndDateChange } = - useHooks(); + const { + startDate, + handleStartDateChange, + endDate, + handleEndDateChange, + isInvalidDate + } = useHooks(); return ( {' - '} diff --git a/web/src/components/molecules/StatusDisplay/index.tsx b/web/src/components/molecules/StatusDisplay/index.tsx new file mode 100644 index 0000000..f45a18d --- /dev/null +++ b/web/src/components/molecules/StatusDisplay/index.tsx @@ -0,0 +1,27 @@ +import { Box, CircularProgress, Typography } from '@mui/material'; + +interface Props { + isLoading?: boolean; + error?: string; +} + +const StatusDisplay = ({ isLoading = false, error }: Props): JSX.Element => { + return ( + + {isLoading ? ( + + ) : ( + error && {error} + )} + + ); +}; + +export default StatusDisplay; diff --git a/web/src/components/organisms/JobListTable/index.tsx b/web/src/components/organisms/JobListTable/index.tsx index 2af535b..4793763 100644 --- a/web/src/components/organisms/JobListTable/index.tsx +++ b/web/src/components/organisms/JobListTable/index.tsx @@ -1,8 +1,7 @@ /* eslint-disable no-mixed-spaces-and-tabs */ import { useJobListContext } from '@/app/job/hooks'; import StatusChip from '@/components/atoms/StatusChip'; -import { TableColumn } from '@/utils/constants/jobTableData'; -import { JobTableRow } from '@/utils/types/job'; +import { JobTableRow, TableColumn } from '@/utils/types/job'; import { Box, Paper, @@ -58,7 +57,7 @@ const JobListTable = (): JSX.Element => { case 'cost': return ( - {`₱ ${row.estimation.cost.toFixed(2)}`} + {row.estimation.cost} ); default: @@ -97,7 +96,7 @@ const JobListTable = (): JSX.Element => {
- {data.map((row: JobTableRow) => ( + {data?.map((row: JobTableRow) => ( {columns.map( (column: TableColumn, index) => ( diff --git a/web/src/utils/constants/jobTableData.ts b/web/src/utils/constants/jobTableData.ts index 9df75ac..56ac2e7 100644 --- a/web/src/utils/constants/jobTableData.ts +++ b/web/src/utils/constants/jobTableData.ts @@ -1,4 +1,4 @@ -import { JobTableRow, TableColumn } from '../types/job'; +import { TableColumn } from '../types/job'; export const JobColumns : TableColumn[] = [ { key: 'title', label: 'Job Title', width: 200 }, @@ -11,190 +11,3 @@ export const JobColumns : TableColumn[] = [ { key: 'cost', label: 'Cost', width: 120 }, { key: 'createdAt', label: 'Created At', width: 120 } ]; - -export const JobData: JobTableRow[] = [ - { - id: 1, - title: 'New Summit', - customer: 'John Doe', - tags: ['Tag A', 'Tag B'], - schedules: ['5-25-2023 5:00 - 15:00'], - estimation: { - status: 'Approved', - cost: 650.0 - }, - personInCharge: 'Michael Murry', - pipelinePhase: 'Delivery', - createdAt: new Date('5-2-2023').toLocaleDateString('en-US', { - year: 'numeric', - month: '2-digit', - day: '2-digit' - }) - }, - { - id: 2, - title: 'New Summit', - customer: 'John Doe', - tags: ['Tag A', 'Tag B', 'Tag C', 'Tag D', 'Tag E'], - schedules: ['5-25-2023 5:00 - 15:00', '5-26-2023 5:00 - 15:00'], - estimation: { - status: 'Making', - cost: 650.0 - }, - personInCharge: 'Michael Murry', - pipelinePhase: 'Delivery', - createdAt: new Date('5-2-2023').toLocaleDateString('en-US', { - year: 'numeric', - month: '2-digit', - day: '2-digit' - }) - }, - { - id: 3, - title: 'New Summit', - customer: 'John Doe', - tags: ['Tag A'], - schedules: ['5-25-2023 5:00 - 15:00'], - estimation: { - status: 'Sent to Customer', - cost: 650.0 - }, - personInCharge: 'Michael Murry', - pipelinePhase: 'Delivery', - createdAt: new Date('5-2-2023').toLocaleDateString('en-US', { - year: 'numeric', - month: '2-digit', - day: '2-digit' - }) - }, - { - id: 4, - title: 'New Summit', - customer: 'John Doe', - tags: ['Tag A', 'Tag B'], - schedules: ['5-25-2023 5:00 - 15:00'], - estimation: { - status: 'Closed', - cost: 650.0 - }, - personInCharge: 'Michael Murry', - pipelinePhase: 'Delivery', - createdAt: new Date('5-2-2023').toLocaleDateString('en-US', { - year: 'numeric', - month: '2-digit', - day: '2-digit' - }) - }, - { - id: 5, - title: 'New Summit', - customer: 'John Doe', - tags: ['Tag A', 'Tag B'], - schedules: [ - '5-25-2023 5:00 - 15:00', - '5-26-2023 5:00 - 15:00', - '5-26-2023 5:00 - 15:00' - ], - estimation: { - status: 'Not yet Created', - cost: 650.0 - }, - personInCharge: 'Michael Murry', - pipelinePhase: 'Delivery', - createdAt: new Date('5-2-2023').toLocaleDateString('en-US', { - year: 'numeric', - month: '2-digit', - day: '2-digit' - }) - }, - { - id: 6, - title: 'New Summit', - customer: 'John Doe', - tags: ['Tag A'], - schedules: ['5-25-2023 5:00 - 15:00'], - estimation: { - status: 'Not yet Created', - cost: 650.0 - }, - personInCharge: 'Michael Murry', - pipelinePhase: 'Delivery', - createdAt: new Date('5-2-2023').toLocaleDateString('en-US', { - year: 'numeric', - month: '2-digit', - day: '2-digit' - }) - }, - { - id: 7, - title: 'New Summit', - customer: 'John Doe', - tags: ['Tag A'], - schedules: ['5-25-2023 5:00 - 15:00'], - estimation: { - status: 'Not yet Created', - cost: 650.0 - }, - personInCharge: 'Michael Murry', - pipelinePhase: 'Delivery', - createdAt: new Date('5-2-2023').toLocaleDateString('en-US', { - year: 'numeric', - month: '2-digit', - day: '2-digit' - }) - }, - { - id: 8, - title: 'New Summit', - customer: 'John Doe', - tags: ['Tag A'], - schedules: ['5-25-2023 5:00 - 15:00'], - estimation: { - status: 'Not yet Created', - cost: 650.0 - }, - personInCharge: 'Michael Murry', - pipelinePhase: 'Delivery', - createdAt: new Date('5-2-2023').toLocaleDateString('en-US', { - year: 'numeric', - month: '2-digit', - day: '2-digit' - }) - }, - { - id: 9, - title: 'New Summit', - customer: 'John Doe', - tags: ['Tag A'], - schedules: ['5-25-2023 5:00 - 15:00'], - estimation: { - status: 'Not yet Created', - cost: 650.0 - }, - personInCharge: 'Michael Murry', - pipelinePhase: 'Delivery', - createdAt: new Date('5-2-2023').toLocaleDateString('en-US', { - year: 'numeric', - month: '2-digit', - day: '2-digit' - }) - }, - { - id: 10, - title: 'New Summit', - customer: 'John Doe', - tags: ['Tag A'], - schedules: ['5-25-2023 5:00 - 15:00'], - estimation: { - status: 'Not yet Created', - cost: 650.0 - }, - personInCharge: 'Michael Murry', - pipelinePhase: 'Delivery', - createdAt: new Date('5-2-2023').toLocaleDateString('en-US', { - year: 'numeric', - month: '2-digit', - day: '2-digit' - }) - } -]; diff --git a/web/src/utils/constants/navbarMenu.ts b/web/src/utils/constants/navbarMenu.ts index 305e460..0613bb7 100644 --- a/web/src/utils/constants/navbarMenu.ts +++ b/web/src/utils/constants/navbarMenu.ts @@ -15,7 +15,7 @@ export const Menus: IMenu[] = [ { name: "Job List", Icon: Home, - href: "/", + href: "/job", }, { name: "Calendar", diff --git a/web/src/utils/constants/statusChipColor.ts b/web/src/utils/constants/statusChipColor.ts index 0f15dd6..a31ff2c 100644 --- a/web/src/utils/constants/statusChipColor.ts +++ b/web/src/utils/constants/statusChipColor.ts @@ -3,9 +3,9 @@ export interface ChipProps { } export const ChipColors: Record = { - 'Not yet Created': '#FFB4AF', + 'Not Yet Created': '#FFB4AF', Making: '#FDFF8F', Approved: '#8AFFB2', - 'Sent to Customer': '#84C1FF', + 'Sent To Customer': '#84C1FF', Closed: '#65707b33' }; diff --git a/web/src/utils/services/axios.ts b/web/src/utils/services/axios.ts new file mode 100644 index 0000000..65be5c8 --- /dev/null +++ b/web/src/utils/services/axios.ts @@ -0,0 +1,8 @@ +import axios from 'axios'; + +export const axiosInstance = axios.create({ + baseURL: process.env.NEXT_PUBLIC_API_BASE_URL, + headers: { + 'Content-Type': 'application/json', + }, +}); diff --git a/web/src/utils/types/customer.ts b/web/src/utils/types/customer.ts new file mode 100644 index 0000000..c7392ff --- /dev/null +++ b/web/src/utils/types/customer.ts @@ -0,0 +1,8 @@ +export interface CustomerSchema { + id: number; + firstName: string; + lastName: string; + email: string; + contact: string; + address: string; +} diff --git a/web/src/utils/types/estimation.ts b/web/src/utils/types/estimation.ts new file mode 100644 index 0000000..6d08484 --- /dev/null +++ b/web/src/utils/types/estimation.ts @@ -0,0 +1,7 @@ +export interface EstimationSchema { + id: number; + title: string; + document: string; + totalCost: number; + status: string; +} diff --git a/web/src/utils/types/job.ts b/web/src/utils/types/job.ts index 816ad2d..33eee65 100644 --- a/web/src/utils/types/job.ts +++ b/web/src/utils/types/job.ts @@ -1,3 +1,8 @@ +import { CustomerSchema } from './customer'; +import { EstimationSchema } from './estimation'; +import { ScheduleSchema } from './schedule'; +import { UserSchema } from './user'; + export interface TableColumn { key: string; label: string; @@ -6,7 +11,7 @@ export interface TableColumn { export interface EstimationType { status: string; - cost: number; + cost: string | number; } export interface JobTableRow { @@ -24,5 +29,26 @@ export interface JobTableRow { export interface JobTable { columns: TableColumn[]; - data: JobTableRow[]; + data?: JobTableRow[]; +} + +export interface JobSchema { + id: number; + title: string; + type: string; + tags: string[]; + remarks?: string; + customer: CustomerSchema; + paymentMethod: string; + personInCharge: UserSchema; + schedules: ScheduleSchema[]; + estimation?: EstimationSchema; + pipelinePhase: string; + createdAt: string; + updatedAt: string; +} + +export interface JobListType { + jobs: JobSchema[]; + count: number } diff --git a/web/src/utils/types/schedule.ts b/web/src/utils/types/schedule.ts new file mode 100644 index 0000000..dd2f53b --- /dev/null +++ b/web/src/utils/types/schedule.ts @@ -0,0 +1,7 @@ +export interface ScheduleSchema { + id: number; + startDate: string; + endDate: string; + startTime: string; + endTime: string; +} diff --git a/web/src/utils/types/user.ts b/web/src/utils/types/user.ts new file mode 100644 index 0000000..2db10dd --- /dev/null +++ b/web/src/utils/types/user.ts @@ -0,0 +1,7 @@ +export interface UserSchema { + id: number; + firstName: string; + lastName: string; + email: string; + role: string; +} diff --git a/web/yarn.lock b/web/yarn.lock index 8662640..3b7148c 100644 --- a/web/yarn.lock +++ b/web/yarn.lock @@ -277,6 +277,13 @@ dependencies: regenerator-runtime "^0.14.0" +"@babel/runtime@^7.22.15": + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.22.15.tgz#38f46494ccf6cf020bd4eed7124b425e83e523b8" + integrity sha512-T0O+aa+4w0u06iNmapipJXMV4HoUir03hpx3/YqXXhu9xim3w+dVphjFWl1OH8NbZHw5Lbm9k45drDkgq2VNNA== + dependencies: + regenerator-runtime "^0.14.0" + "@babel/template@^7.22.5", "@babel/template@^7.3.3": version "7.22.5" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.22.5.tgz#0c8c4d944509875849bd0344ff0050756eefc6ec" @@ -423,6 +430,33 @@ resolved "https://registry.yarnpkg.com/@emotion/weak-memoize/-/weak-memoize-0.3.1.tgz#d0fce5d07b0620caa282b5131c297bb60f9d87e6" integrity sha512-EsBwpc7hBUJWAsNPBmJy4hxWx12v6bshQsldrVmjxJoc3isbxhOrF2IcCpaXxfvq03NwkI7sbsOLXbYuqF/8Ww== +"@floating-ui/core@^1.4.1": + version "1.4.1" + resolved "https://registry.yarnpkg.com/@floating-ui/core/-/core-1.4.1.tgz#0d633f4b76052668afb932492ac452f7ebe97f17" + integrity sha512-jk3WqquEJRlcyu7997NtR5PibI+y5bi+LS3hPmguVClypenMsCY3CBa3LAQnozRCtCrYWSEtAdiskpamuJRFOQ== + dependencies: + "@floating-ui/utils" "^0.1.1" + +"@floating-ui/dom@^1.5.1": + version "1.5.2" + resolved "https://registry.yarnpkg.com/@floating-ui/dom/-/dom-1.5.2.tgz#6812e89d1d4d4ea32f10d15c3b81feb7f9836d89" + integrity sha512-6ArmenS6qJEWmwzczWyhvrXRdI/rI78poBcW0h/456+onlabit+2G+QxHx5xTOX60NBJQXjsCLFbW2CmsXpUog== + dependencies: + "@floating-ui/core" "^1.4.1" + "@floating-ui/utils" "^0.1.1" + +"@floating-ui/react-dom@^2.0.1": + version "2.0.2" + resolved "https://registry.yarnpkg.com/@floating-ui/react-dom/-/react-dom-2.0.2.tgz#fab244d64db08e6bed7be4b5fcce65315ef44d20" + integrity sha512-5qhlDvjaLmAst/rKb3VdlCinwTF4EYMiVxuuc/HVUjs46W0zgtbMmAZ1UTsDrRTxRmUEzl92mOtWbeeXL26lSQ== + dependencies: + "@floating-ui/dom" "^1.5.1" + +"@floating-ui/utils@^0.1.1": + version "0.1.2" + resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.1.2.tgz#b7e9309ccce5a0a40ac482cb894f120dba2b357f" + integrity sha512-ou3elfqG/hZsbmF4bxeJhPHIf3G2pm0ujc39hYEZrfVqt7Vk/Zji6CXc3W0pmYM8BW1g40U+akTl9DKZhFhInQ== + "@istanbuljs/load-nyc-config@^1.0.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz#fd3db1d59ecf7cf121e80650bb86712f9b55eced" @@ -677,6 +711,21 @@ prop-types "^15.8.1" react-is "^18.2.0" +"@mui/base@^5.0.0-alpha.87": + version "5.0.0-beta.14" + resolved "https://registry.yarnpkg.com/@mui/base/-/base-5.0.0-beta.14.tgz#315b67b0fd231cbd47e8d54f8f92be23122e4d66" + integrity sha512-Je/9JzzYObsuLCIClgE8XvXNFb55IEz8n2NtStUfASfNiVrwiR8t6VVFFuhofehkyTIN34tq1qbBaOjCnOovBw== + dependencies: + "@babel/runtime" "^7.22.10" + "@emotion/is-prop-valid" "^1.2.1" + "@floating-ui/react-dom" "^2.0.1" + "@mui/types" "^7.2.4" + "@mui/utils" "^5.14.8" + "@popperjs/core" "^2.11.8" + clsx "^2.0.0" + prop-types "^15.8.1" + react-is "^18.2.0" + "@mui/core-downloads-tracker@^5.14.4": version "5.14.4" resolved "https://registry.yarnpkg.com/@mui/core-downloads-tracker/-/core-downloads-tracker-5.14.4.tgz#a517265647ee9660170107d68905db5e400576c5" @@ -756,6 +805,30 @@ prop-types "^15.8.1" react-is "^18.2.0" +"@mui/utils@^5.14.7", "@mui/utils@^5.14.8": + version "5.14.8" + resolved "https://registry.yarnpkg.com/@mui/utils/-/utils-5.14.8.tgz#e1737d5fcd54aa413d6b1aaea3ea670af2919402" + integrity sha512-1Ls2FfyY2yVSz9NEqedh3J8JAbbZAnUWkOWLE2f4/Hc4T5UWHMfzBLLrCqExfqyfyU+uXYJPGeNIsky6f8Gh5Q== + dependencies: + "@babel/runtime" "^7.22.10" + "@types/prop-types" "^15.7.5" + "@types/react-is" "^18.2.1" + prop-types "^15.8.1" + react-is "^18.2.0" + +"@mui/x-date-pickers@^6.13.0": + version "6.13.0" + resolved "https://registry.yarnpkg.com/@mui/x-date-pickers/-/x-date-pickers-6.13.0.tgz#de9acfbc7697aed2bcbea6996173b45e3f0ee0d5" + integrity sha512-mNfWXrS9PMLI+lgeEw6R08g5j1Yewat+A7MiI2QuwZteX3b83JQnm73RCGrSFTpvOP1/v6yIBPbxPZin5eu6GA== + dependencies: + "@babel/runtime" "^7.22.15" + "@mui/base" "^5.0.0-alpha.87" + "@mui/utils" "^5.14.7" + "@types/react-transition-group" "^4.4.6" + clsx "^2.0.0" + prop-types "^15.8.1" + react-transition-group "^4.4.5" + "@next/env@13.4.12": version "13.4.12" resolved "https://registry.yarnpkg.com/@next/env/-/env-13.4.12.tgz#0b88115ab817f178bf9dc0c5e7b367277595b58d" @@ -1308,6 +1381,15 @@ axe-core@^4.6.2: resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.7.2.tgz#040a7342b20765cb18bb50b628394c21bccc17a0" integrity sha512-zIURGIS1E1Q4pcrMjp+nnEh+16G56eG/MUllJH8yEvw7asDo7Ac9uhC9KIH5jzpITueEZolfYglnCGIuSBz39g== +axios@^1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.5.0.tgz#f02e4af823e2e46a9768cfc74691fdd0517ea267" + integrity sha512-D4DdjDo5CY50Qms0qGQTTw6Q44jl7zRwY7bthds06pUGfChBCTcQs+N743eFWGEd6pRTMd6A+I87aWyFV5wiZQ== + dependencies: + follow-redirects "^1.15.0" + form-data "^4.0.0" + proxy-from-env "^1.1.0" + axobject-query@^3.1.1: version "3.2.1" resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-3.2.1.tgz#39c378a6e3b06ca679f29138151e45b2b32da62a" @@ -2130,6 +2212,11 @@ find-up@^4.0.0, find-up@^4.1.0: locate-path "^5.0.0" path-exists "^4.0.0" +follow-redirects@^1.15.0: + version "1.15.2" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13" + integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA== + for-each@^0.3.3: version "0.3.3" resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.3.tgz#69b447e88a0a5d32c3e7084f3f1710034b21376e" @@ -3270,6 +3357,11 @@ mkdirp@^1.0.4: resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== +moment@^2.29.4: + version "2.29.4" + resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.4.tgz#3dbe052889fe7c1b2ed966fcb3a77328964ef108" + integrity sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w== + ms@2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" @@ -3574,6 +3666,11 @@ prop-types@^15.6.2, prop-types@^15.8.1: object-assign "^4.1.1" react-is "^16.13.1" +proxy-from-env@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" + integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== + psl@^1.1.33: version "1.9.0" resolved "https://registry.yarnpkg.com/psl/-/psl-1.9.0.tgz#d0df2a137f00794565fcaf3b2c00cd09f8d5a5a7" From ceea92fac1307eedb09d7f3b946ba0d4bd11474d Mon Sep 17 00:00:00 2001 From: nicoleamber Date: Mon, 18 Sep 2023 13:56:15 +0800 Subject: [PATCH 6/7] [M1_TR-206] Frontend for M1_TR-5 --- web/src/app/job/context.ts | 3 +- web/src/app/job/hooks.ts | 60 +++++++-- web/src/app/job/page.tsx | 121 +++++++++++------- .../molecules/CreatedDateRangeFilter/hooks.ts | 42 ++++-- .../CreatedDateRangeFilter/index.tsx | 31 ++--- .../molecules/EstimationStatusFilter/hooks.ts | 13 +- .../EstimationStatusFilter/index.tsx | 12 +- .../components/molecules/TagFilter/hooks.ts | 14 +- .../components/molecules/TagFilter/index.tsx | 10 +- .../organisms/SearchFilterHeader/hooks.ts | 3 + web/src/utils/constants/estimationStatus.ts | 7 - .../utils/constants/estimationStatusEnum.ts | 8 ++ web/src/utils/constants/tags.ts | 5 - web/src/utils/constants/tagsEnum.ts | 5 + web/src/utils/types/job.ts | 14 ++ 15 files changed, 242 insertions(+), 106 deletions(-) delete mode 100644 web/src/utils/constants/estimationStatus.ts create mode 100644 web/src/utils/constants/estimationStatusEnum.ts delete mode 100644 web/src/utils/constants/tags.ts create mode 100644 web/src/utils/constants/tagsEnum.ts diff --git a/web/src/app/job/context.ts b/web/src/app/job/context.ts index cc54b7d..92fe1c4 100644 --- a/web/src/app/job/context.ts +++ b/web/src/app/job/context.ts @@ -1,4 +1,5 @@ -import { JobTable } from '@/utils/types/job'; +import { JobQuery, JobTable } from '@/utils/types/job'; import { createContext } from 'react'; export const JobListContext = createContext(undefined); +export const JobQueryContext = createContext(undefined); diff --git a/web/src/app/job/hooks.ts b/web/src/app/job/hooks.ts index b9ed0cb..1c05a07 100644 --- a/web/src/app/job/hooks.ts +++ b/web/src/app/job/hooks.ts @@ -1,8 +1,9 @@ import { axiosInstance } from '@/utils/services/axios'; -import { JobSchema, JobTable, JobTableRow } from '@/utils/types/job'; +import { JobQuery, JobSchema, JobTable, JobTableRow } from '@/utils/types/job'; import { ScheduleSchema } from '@/utils/types/schedule'; +import { Moment } from 'moment'; import { useContext, useEffect, useState } from 'react'; -import { JobListContext } from './context'; +import { JobListContext, JobQueryContext } from './context'; export const useJobListContext = (): JobTable => { const jobs = useContext(JobListContext); @@ -14,6 +15,16 @@ export const useJobListContext = (): JobTable => { return jobs; }; +export const useJobQueryContext = (): JobQuery => { + const query = useContext(JobQueryContext); + + if (query === undefined) { + throw new Error('Missing JobQueryContext'); + } + + return query; +}; + export const useHooks = () => { const [page, setPage] = useState(1); const [perPage] = useState(12); @@ -23,13 +34,36 @@ export const useHooks = () => { const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(); + const [isFilter, setIsFilter] = useState(false); + const [tag, setTag] = useState(''); + const [status, setStatus] = useState(''); + const [startDate, setStartDate] = useState(null); + const [endDate, setEndDate] = useState(null); + + const params: { page: number, perPage: number, tag?: string, status?: string, startDate?: string, + endDate?: string } = { + page, perPage, + }; + + if(isFilter) { + if (tag) { + params.tag = tag; + } + + if (status) { + params.status = status; + } + + if (startDate && endDate) { + params.startDate = startDate.format("MM-DD-YYYY"); + params.endDate = endDate.format("MM-DD-YYYY"); + } + } + useEffect(() => { axiosInstance .get('/jobs', { - params: { - page, - perPage - } + params }) .then((response) => { setCount(response?.data.count); @@ -42,7 +76,7 @@ export const useHooks = () => { setIsLoading(false); setError('Something went wrong.'); }); - }, [page, perPage]); + }, [page, perPage, tag, status, startDate, endDate, isFilter]); return { jobs, @@ -52,6 +86,16 @@ export const useHooks = () => { isLoading, error, setPage, + tag, + setTag, + status, + setStatus, + startDate, + setStartDate, + endDate, + setEndDate, + isFilter, + setIsFilter }; }; @@ -81,7 +125,7 @@ const formatSchedules = (schedules: ScheduleSchema[]): string[] => { return scheduleData; }; -const formatEnum = (value: string): string => { +export const formatEnum = (value: string): string => { let words = value.split('_'); words = words.map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()); return words.join(' '); diff --git a/web/src/app/job/page.tsx b/web/src/app/job/page.tsx index 98f910d..05f1d94 100644 --- a/web/src/app/job/page.tsx +++ b/web/src/app/job/page.tsx @@ -7,61 +7,92 @@ import SearchFilterHeader from '@/components/organisms/SearchFilterHeader'; import { JobColumns } from '@/utils/constants/jobTableData'; import { Grid, Typography } from '@mui/material'; import { Fragment } from 'react'; -import { JobListContext } from './context'; +import { JobListContext, JobQueryContext } from './context'; import { useHooks } from './hooks'; const JobList = (): JSX.Element => { - const { page, jobs, count, pageCount, setPage, isLoading, error } = - useHooks(); + const { + page, + jobs, + count, + pageCount, + setPage, + isLoading, + error, + tag, + setTag, + status, + setStatus, + startDate, + setStartDate, + endDate, + setEndDate, + isFilter, + setIsFilter + } = useHooks(); return (
- {isLoading ? ( - - ) : error ? ( - - ) : ( - - - - - {!count ? ( - - - No jobs found - + + {isLoading ? ( + + ) : error ? ( + + ) : ( + + + - ) : ( - - - - - - + {!count ? ( + + + No jobs found + - - )} - - )} + ) : ( + + + + + + + + + )} + + )} +
); diff --git a/web/src/components/molecules/CreatedDateRangeFilter/hooks.ts b/web/src/components/molecules/CreatedDateRangeFilter/hooks.ts index b0ab4b4..ec5db0f 100644 --- a/web/src/components/molecules/CreatedDateRangeFilter/hooks.ts +++ b/web/src/components/molecules/CreatedDateRangeFilter/hooks.ts @@ -1,30 +1,48 @@ -import moment, { type Moment } from 'moment'; +import { useJobQueryContext } from '@/app/job/hooks'; +import { type Moment } from 'moment'; import { useEffect, useState } from 'react'; export const useHooks = () => { - const [startDate, setStartDate] = useState(moment().startOf('month')); - const [endDate, setEndDate] = useState(moment()); - const [isInvalidDate, setIsInvalidDate] = useState(false); + const { setStartDate, setEndDate } = useJobQueryContext(); + const [initialStartDate, setInitialStartDate] = useState(null); + const [initialEndDate, setInitialEndDate] = useState(null); + const [error, setError] = useState(''); const handleStartDateChange = (date: Moment | null): void => { - setStartDate(date); + setInitialStartDate(date); }; const handleEndDateChange = (date: Moment | null): void => { - setEndDate(date); + setInitialEndDate(date); }; useEffect(() => { - if (startDate && endDate) { - setIsInvalidDate(endDate <= startDate); + if (initialStartDate && !initialEndDate || initialEndDate && !initialStartDate) { + setError('Both start and end dates must be set'); } - }, [startDate, endDate]); + + if(!initialStartDate && !initialEndDate) { + setError(''); + setStartDate(null); + setEndDate(null); + } + + if (initialStartDate && initialEndDate) { + if (initialEndDate < initialStartDate) { + setError('Please set a vaild date'); + } else { + setError(''); + setStartDate(initialStartDate); + setEndDate(initialEndDate); + } + } + }, [initialStartDate, initialEndDate]); return { - startDate, + error, + initialStartDate, handleStartDateChange, - endDate, + initialEndDate, handleEndDateChange, - isInvalidDate }; }; diff --git a/web/src/components/molecules/CreatedDateRangeFilter/index.tsx b/web/src/components/molecules/CreatedDateRangeFilter/index.tsx index 9746682..ebd7697 100644 --- a/web/src/components/molecules/CreatedDateRangeFilter/index.tsx +++ b/web/src/components/molecules/CreatedDateRangeFilter/index.tsx @@ -8,11 +8,11 @@ import { useHooks } from './hooks'; const CreateDateRangeFilter = () => { const { - startDate, + error, + initialStartDate, handleStartDateChange, - endDate, - handleEndDateChange, - isInvalidDate + initialEndDate, + handleEndDateChange } = useHooks(); return ( @@ -21,7 +21,7 @@ const CreateDateRangeFilter = () => { { } }} slotProps={{ + actionBar: { + actions: ['clear'] + }, textField: { size: 'small', color: 'secondary', id: 'created-start', name: 'created-start', - error: isInvalidDate, - helperText: isInvalidDate - ? 'Please set a valid date' - : '' + error: !!error, + helperText: error } }} /> - {' - '} { } }} slotProps={{ + actionBar: { + actions: ['clear'] + }, textField: { size: 'small', color: 'secondary', id: 'created-end', name: 'created-end', - error: isInvalidDate, - helperText: isInvalidDate - ? 'Please set a valid date.' - : '' + error: !!error, + helperText: error } }} /> diff --git a/web/src/components/molecules/EstimationStatusFilter/hooks.ts b/web/src/components/molecules/EstimationStatusFilter/hooks.ts index 9c1499c..8bdcc38 100644 --- a/web/src/components/molecules/EstimationStatusFilter/hooks.ts +++ b/web/src/components/molecules/EstimationStatusFilter/hooks.ts @@ -1,15 +1,24 @@ +import { formatEnum, useJobQueryContext } from '@/app/job/hooks'; +import { EstimationStatusEnum } from '@/utils/constants/estimationStatusEnum'; import { SelectChangeEvent } from '@mui/material'; -import { useState } from 'react'; export const useHooks = () => { - const [status, setStatus] = useState(''); + const { status, setStatus } = useJobQueryContext(); const handleChange = (e: SelectChangeEvent) => { setStatus(e.target.value); }; + const statusOptions = Object.keys(EstimationStatusEnum).map((key) => { + return { + value: EstimationStatusEnum[key as keyof typeof EstimationStatusEnum], + name: formatEnum(key) + } + }); + return { status, + statusOptions, handleChange }; }; diff --git a/web/src/components/molecules/EstimationStatusFilter/index.tsx b/web/src/components/molecules/EstimationStatusFilter/index.tsx index 99188dc..9553d24 100644 --- a/web/src/components/molecules/EstimationStatusFilter/index.tsx +++ b/web/src/components/molecules/EstimationStatusFilter/index.tsx @@ -1,10 +1,9 @@ import styles from '@/styles/Filter.module.css'; -import { EstimationStatus } from '@/utils/constants/estimationStatus'; import { FormControl, InputLabel, MenuItem, Select } from '@mui/material'; import { useHooks } from './hooks'; const EstimationStatusFilter = () => { - const { status, handleChange } = useHooks(); + const { status, statusOptions, handleChange } = useHooks(); return ( @@ -20,9 +19,12 @@ const EstimationStatusFilter = () => { color='secondary' onChange={handleChange} className={styles.input}> - {EstimationStatus.map((status) => ( - - {status.status} + + None + + {statusOptions.map((status, key) => ( + + {status.name} ))} diff --git a/web/src/components/molecules/TagFilter/hooks.ts b/web/src/components/molecules/TagFilter/hooks.ts index e835a95..eb706cd 100644 --- a/web/src/components/molecules/TagFilter/hooks.ts +++ b/web/src/components/molecules/TagFilter/hooks.ts @@ -1,15 +1,25 @@ +import { formatEnum, useJobQueryContext } from '@/app/job/hooks'; +import { TagsEnum } from '@/utils/constants/tagsEnum'; + import { SelectChangeEvent } from '@mui/material'; -import { useState } from 'react'; export const useHooks = () => { - const [tag, setTag] = useState(''); + const { tag, setTag } = useJobQueryContext(); const handleChange = (e: SelectChangeEvent) => { setTag(e.target.value); }; + + const tagOptions = Object.keys(TagsEnum).map((key) => { + return { + value: TagsEnum[key as keyof typeof TagsEnum], + name: formatEnum(key) + } + }); return { tag, + tagOptions, handleChange }; }; diff --git a/web/src/components/molecules/TagFilter/index.tsx b/web/src/components/molecules/TagFilter/index.tsx index d12fed7..6594b36 100644 --- a/web/src/components/molecules/TagFilter/index.tsx +++ b/web/src/components/molecules/TagFilter/index.tsx @@ -1,10 +1,9 @@ import styles from '@/styles/Filter.module.css'; -import { Tags } from '@/utils/constants/tags'; import { FormControl, InputLabel, MenuItem, Select } from '@mui/material'; import { useHooks } from './hooks'; const TagFilter = () => { - const { tag, handleChange } = useHooks(); + const { tag, tagOptions, handleChange } = useHooks(); return ( @@ -20,8 +19,11 @@ const TagFilter = () => { color='secondary' onChange={handleChange} className={styles.input}> - {Tags.map((tag) => ( - + + None + + {tagOptions.map((tag, key) => ( + {tag.name} ))} diff --git a/web/src/components/organisms/SearchFilterHeader/hooks.ts b/web/src/components/organisms/SearchFilterHeader/hooks.ts index bfad85f..d499f3c 100644 --- a/web/src/components/organisms/SearchFilterHeader/hooks.ts +++ b/web/src/components/organisms/SearchFilterHeader/hooks.ts @@ -1,10 +1,13 @@ +import { useJobQueryContext } from '@/app/job/hooks'; import { useState } from 'react'; export const useHooks = () => { + const { isFilter, setIsFilter } = useJobQueryContext(); const [isExpanded, setIsExpanded] = useState(false); const toggleFilters = () => { setIsExpanded(!isExpanded); + setIsFilter(!isFilter); }; return { diff --git a/web/src/utils/constants/estimationStatus.ts b/web/src/utils/constants/estimationStatus.ts deleted file mode 100644 index cc2b981..0000000 --- a/web/src/utils/constants/estimationStatus.ts +++ /dev/null @@ -1,7 +0,0 @@ -export const EstimationStatus = [ - { id: 1, status: 'Not yet Created' }, - { id: 2, status: 'Making' }, - { id: 3, status: 'Approved' }, - { id: 4, status: 'Sent to Customer' }, - { id: 5, status: 'Closed' } -]; diff --git a/web/src/utils/constants/estimationStatusEnum.ts b/web/src/utils/constants/estimationStatusEnum.ts new file mode 100644 index 0000000..5493ce0 --- /dev/null +++ b/web/src/utils/constants/estimationStatusEnum.ts @@ -0,0 +1,8 @@ + +export enum EstimationStatusEnum { + NOT_YET_CREATED = "NOT_YET_CREATED", + MAKING = "MAKING", + APPROVED = "APPROVED", + SENT_TO_CUSTOMER = "SENT_TO_CUSTOMER", + CLOSED = "CLOSED", +} diff --git a/web/src/utils/constants/tags.ts b/web/src/utils/constants/tags.ts deleted file mode 100644 index f021459..0000000 --- a/web/src/utils/constants/tags.ts +++ /dev/null @@ -1,5 +0,0 @@ -export const Tags = [ - { id: 1, value: 'TAG_A', name: 'Tag A' }, - { id: 2, value: 'TAG_B', name: 'Tag B' }, - { id: 3, value: 'TAG_C', name: 'Tag C' } -]; diff --git a/web/src/utils/constants/tagsEnum.ts b/web/src/utils/constants/tagsEnum.ts new file mode 100644 index 0000000..7d380b7 --- /dev/null +++ b/web/src/utils/constants/tagsEnum.ts @@ -0,0 +1,5 @@ +export enum TagsEnum { + TAG_A = "TAG_A", + TAG_B = "TAG_B", + TAG_C = "TAG_C", +} diff --git a/web/src/utils/types/job.ts b/web/src/utils/types/job.ts index 33eee65..8cd0af4 100644 --- a/web/src/utils/types/job.ts +++ b/web/src/utils/types/job.ts @@ -1,3 +1,4 @@ +import { Moment } from 'moment'; import { CustomerSchema } from './customer'; import { EstimationSchema } from './estimation'; import { ScheduleSchema } from './schedule'; @@ -52,3 +53,16 @@ export interface JobListType { jobs: JobSchema[]; count: number } + +export interface JobQuery { + tag: string; + setTag: (value: string) => void; + status: string; + setStatus: (value: string) => void; + startDate: Moment | null; + setStartDate: (date: Moment | null) => void; + endDate: Moment | null; + setEndDate: (date: Moment | null) => void; + isFilter: boolean; + setIsFilter: (value: boolean) => void; +} From 1a1fee9fcbbda8ae943ea0d739096151578ebe77 Mon Sep 17 00:00:00 2001 From: nicoleamber Date: Thu, 21 Sep 2023 14:19:02 +0800 Subject: [PATCH 7/7] [M1_TR-206] Refactored context & reusable components and fixed indentation --- web/.eslintrc.js | 11 +- web/src/app/job/hooks.ts | 218 ++++++---------- web/src/app/job/page.tsx | 167 ++++++------- web/src/app/layout.tsx | 20 +- web/src/app/wrapper.tsx | 77 +++--- web/src/assets/icons/FilterCog.tsx | 26 +- web/src/assets/icons/FilterRemove.tsx | 26 +- web/src/assets/icons/Logo.tsx | 32 ++- web/src/components/atoms/Pagination/index.tsx | 27 -- web/src/components/atoms/StatusChip/index.tsx | 18 +- .../molecules/CreatedDateRangeFilter/hooks.ts | 48 ---- .../CreatedDateRangeFilter/index.tsx | 80 ------ .../molecules/DateRangeField/hooks.ts | 49 ++++ .../molecules/DateRangeField/index.tsx | 89 +++++++ .../molecules/EstimationStatusFilter/hooks.ts | 24 -- .../EstimationStatusFilter/index.tsx | 35 --- web/src/components/molecules/NavBarItem.tsx | 124 ++++----- .../components/molecules/Pagination/index.tsx | 27 ++ .../components/molecules/SearchBar/hooks.ts | 16 +- .../components/molecules/SearchBar/index.tsx | 52 ++-- .../molecules/SelectDropdown/index.tsx | 56 +++++ .../molecules/StatusDisplay/index.tsx | 35 ++- .../components/molecules/TagFilter/hooks.ts | 25 -- .../components/molecules/TagFilter/index.tsx | 35 --- web/src/components/organisms/Header.tsx | 62 ++--- .../components/organisms/HeaderProfile.tsx | 236 ++++++++++-------- .../organisms/JobListTable/hooks.ts | 22 ++ .../organisms/JobListTable/index.tsx | 208 +++++++-------- web/src/components/organisms/Navbar.tsx | 125 +++++----- .../organisms/SearchFilterHeader/hooks.ts | 75 ++++-- .../organisms/SearchFilterHeader/index.tsx | 134 ++++++---- .../utils/constants/estimationStatusEnum.ts | 10 +- web/src/utils/constants/jobTableData.ts | 20 +- web/src/utils/constants/navbarMenu.ts | 44 ++-- web/src/utils/constants/statusChipColor.ts | 12 +- web/src/utils/constants/tagsEnum.ts | 6 +- web/src/utils/helpers/index.tsx | 70 ++++++ web/src/utils/services/axios.ts | 8 +- web/src/utils/types/customer.ts | 12 +- web/src/utils/types/estimation.ts | 10 +- web/src/utils/types/job.ts | 84 +++---- web/src/utils/types/schedule.ts | 10 +- web/src/utils/types/user.ts | 10 +- 43 files changed, 1276 insertions(+), 1199 deletions(-) delete mode 100644 web/src/components/atoms/Pagination/index.tsx delete mode 100644 web/src/components/molecules/CreatedDateRangeFilter/hooks.ts delete mode 100644 web/src/components/molecules/CreatedDateRangeFilter/index.tsx create mode 100644 web/src/components/molecules/DateRangeField/hooks.ts create mode 100644 web/src/components/molecules/DateRangeField/index.tsx delete mode 100644 web/src/components/molecules/EstimationStatusFilter/hooks.ts delete mode 100644 web/src/components/molecules/EstimationStatusFilter/index.tsx create mode 100644 web/src/components/molecules/Pagination/index.tsx create mode 100644 web/src/components/molecules/SelectDropdown/index.tsx delete mode 100644 web/src/components/molecules/TagFilter/hooks.ts delete mode 100644 web/src/components/molecules/TagFilter/index.tsx create mode 100644 web/src/components/organisms/JobListTable/hooks.ts create mode 100644 web/src/utils/helpers/index.tsx diff --git a/web/.eslintrc.js b/web/.eslintrc.js index 4edee31..1953b89 100644 --- a/web/.eslintrc.js +++ b/web/.eslintrc.js @@ -1,7 +1,8 @@ module.exports = { - extends: ['eslint:recommended'], - rules: { - 'no-undef': 'off', - 'no-unused-vars': 'off' - } + extends: ['eslint:recommended'], + rules: { + 'no-undef': 'off', + 'no-unused-vars': 'off', + indent: ['error', 2, { SwitchCase: 1 }] + } }; diff --git a/web/src/app/job/hooks.ts b/web/src/app/job/hooks.ts index 1c05a07..f74486f 100644 --- a/web/src/app/job/hooks.ts +++ b/web/src/app/job/hooks.ts @@ -1,151 +1,85 @@ +import { convertTableData } from '@/utils/helpers'; import { axiosInstance } from '@/utils/services/axios'; -import { JobQuery, JobSchema, JobTable, JobTableRow } from '@/utils/types/job'; -import { ScheduleSchema } from '@/utils/types/schedule'; +import { JobTableRow } from '@/utils/types/job'; import { Moment } from 'moment'; -import { useContext, useEffect, useState } from 'react'; -import { JobListContext, JobQueryContext } from './context'; +import { useEffect, useState } from 'react'; -export const useJobListContext = (): JobTable => { - const jobs = useContext(JobListContext); - - if (jobs === undefined) { - throw new Error('Missing JobListContext'); +export const useHooks = () => { + const [page, setPage] = useState(1); + const [perPage] = useState(12); + const [jobs, setJobs] = useState([]); + const [count, setCount] = useState(0); + const [pageCount, setPageCount] = useState(1); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(); + + const [isFilter, setIsFilter] = useState(false); + const [tag, setTag] = useState(''); + const [status, setStatus] = useState(''); + const [startDate, setStartDate] = useState(null); + const [endDate, setEndDate] = useState(null); + + const params: { + page: number, + perPage: number, + tag?: string, + status?: string, + startDate?: string, + endDate?: string; + } = { + page, perPage, + }; + + if (isFilter) { + if (tag) { + params.tag = tag; } - return jobs; -}; - -export const useJobQueryContext = (): JobQuery => { - const query = useContext(JobQueryContext); - - if (query === undefined) { - throw new Error('Missing JobQueryContext'); + if (status) { + params.status = status; } - return query; -}; - -export const useHooks = () => { - const [page, setPage] = useState(1); - const [perPage] = useState(12); - const [jobs, setJobs] = useState([]); - const [count, setCount] = useState(0); - const [pageCount, setPageCount] = useState(1); - const [isLoading, setIsLoading] = useState(true); - const [error, setError] = useState(); - - const [isFilter, setIsFilter] = useState(false); - const [tag, setTag] = useState(''); - const [status, setStatus] = useState(''); - const [startDate, setStartDate] = useState(null); - const [endDate, setEndDate] = useState(null); - - const params: { page: number, perPage: number, tag?: string, status?: string, startDate?: string, - endDate?: string } = { - page, perPage, - }; - - if(isFilter) { - if (tag) { - params.tag = tag; - } - - if (status) { - params.status = status; - } - - if (startDate && endDate) { - params.startDate = startDate.format("MM-DD-YYYY"); - params.endDate = endDate.format("MM-DD-YYYY"); - } + if (startDate && endDate) { + params.startDate = startDate.format("MM-DD-YYYY"); + params.endDate = endDate.format("MM-DD-YYYY"); } - - useEffect(() => { - axiosInstance - .get('/jobs', { - params - }) - .then((response) => { - setCount(response?.data.count); - setJobs(convertTableData(response?.data.jobs)); - setPageCount(Math.ceil(response?.data.count / perPage)); - setIsLoading(false); - }) - .catch((e) => { - console.log(e); - setIsLoading(false); - setError('Something went wrong.'); - }); - }, [page, perPage, tag, status, startDate, endDate, isFilter]); - - return { - jobs, - count, - page, - pageCount, - isLoading, - error, - setPage, - tag, - setTag, - status, - setStatus, - startDate, - setStartDate, - endDate, - setEndDate, - isFilter, - setIsFilter - }; -}; - -const formatDate = (date: string): string => { - return new Date(date).toLocaleDateString('en-US', { - year: 'numeric', - month: '2-digit', - day: '2-digit' - }); -}; - -const formatTime = (date: string): string => { - return new Date(date).toLocaleTimeString('en-US', { - hour: '2-digit', - minute: '2-digit', - hour12: false, - }); -}; - -const formatSchedules = (schedules: ScheduleSchema[]): string[] => { - const scheduleData = schedules.map((schedule: ScheduleSchema) => { - const startDateTime = `${formatDate(schedule.startDate)} ${formatTime(schedule.startTime)}`; - const endDateTime = `${formatTime(schedule.endTime)}`; - return `${startDateTime} - ${endDateTime}`; - }); - - return scheduleData; -}; - -export const formatEnum = (value: string): string => { - let words = value.split('_'); - words = words.map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()); - return words.join(' '); -} - -const convertTableData = (data: JobSchema[]): JobTableRow[] => { - const tableData = data.map((job: JobSchema) => ({ - id: job.id, - title: job.title, - customer: `${job.customer.firstName} ${job.customer.lastName}`, - tags: job.tags.map((tag) => formatEnum(tag)).sort(), - schedules: formatSchedules(job.schedules).sort(), - estimation: { - status: job.estimation?.status ? formatEnum(job.estimation?.status) : 'Not Yet Created', - cost: job.estimation?.totalCost ? `₱ ${job.estimation?.totalCost}` : '-' - }, - personInCharge: `${job.personInCharge.firstName} ${job.personInCharge.lastName}`, - pipelinePhase: formatEnum(job.pipelinePhase), - createdAt: formatDate(job.createdAt) - })); - - return tableData; + } + + useEffect(() => { + axiosInstance + .get('/jobs', { + params + }) + .then((response) => { + setCount(response?.data.count); + setJobs(convertTableData(response?.data.jobs)); + setPageCount(Math.ceil(response?.data.count / perPage)); + setIsLoading(false); + }) + .catch((e) => { + console.log(e); + setIsLoading(false); + setError('Something went wrong.'); + }); + }, [page, perPage, tag, status, startDate, endDate, isFilter]); + + return { + jobs, + count, + page, + pageCount, + isLoading, + error, + setPage, + tag, + setTag, + status, + setStatus, + startDate, + setStartDate, + endDate, + setEndDate, + isFilter, + setIsFilter + }; }; diff --git a/web/src/app/job/page.tsx b/web/src/app/job/page.tsx index 05f1d94..4d29637 100644 --- a/web/src/app/job/page.tsx +++ b/web/src/app/job/page.tsx @@ -1,6 +1,6 @@ 'use client'; -import Pagination from '@/components/atoms/Pagination'; +import Pagination from '@/components/molecules/Pagination'; import StatusDisplay from '@/components/molecules/StatusDisplay'; import JobListTable from '@/components/organisms/JobListTable'; import SearchFilterHeader from '@/components/organisms/SearchFilterHeader'; @@ -11,91 +11,88 @@ import { JobListContext, JobQueryContext } from './context'; import { useHooks } from './hooks'; const JobList = (): JSX.Element => { - const { - page, - jobs, - count, - pageCount, - setPage, - isLoading, - error, - tag, - setTag, - status, - setStatus, - startDate, - setStartDate, - endDate, - setEndDate, - isFilter, - setIsFilter - } = useHooks(); + const { + page, + jobs, + count, + pageCount, + setPage, + isLoading, + error, + tag, + setTag, + status, + setStatus, + startDate, + setStartDate, + endDate, + setEndDate, + isFilter, + setIsFilter + } = useHooks(); - return ( -
- - - {isLoading ? ( - - ) : error ? ( - - ) : ( - - - - - {!count ? ( - - - No jobs found - - - ) : ( - - - - - - - - - )} - - )} - - -
- ); + return ( +
+ + + {isLoading ? ( + + ) : error ? ( + + ) : ( + + + + + {!count ? ( + + No jobs found + + ) : ( + + + + + + + + + )} + + )} + + +
+ ); }; export default JobList; diff --git a/web/src/app/layout.tsx b/web/src/app/layout.tsx index 1f643ee..0442ce7 100644 --- a/web/src/app/layout.tsx +++ b/web/src/app/layout.tsx @@ -5,19 +5,19 @@ import { theme } from '@/assets/theme'; import Wrapper from './wrapper'; export const metadata: Metadata = { - title: 'JMS' + title: 'JMS' }; export default function RootLayout({ - children + children }: { - children: React.ReactNode; + children: React.ReactNode; }) { - return ( - - - {children} - - - ); + return ( + + + {children} + + + ); } diff --git a/web/src/app/wrapper.tsx b/web/src/app/wrapper.tsx index e0f4dd0..bcfd35f 100644 --- a/web/src/app/wrapper.tsx +++ b/web/src/app/wrapper.tsx @@ -1,50 +1,53 @@ -'use client' +'use client'; +import { Box, Container } from '@mui/material'; +import { Open_Sans } from 'next/font/google'; import { FC, useState } from 'react'; -import { Open_Sans } from 'next/font/google' -import { Box, Container } from '@mui/material' import Header from '@/components/organisms/Header'; import NavBar from '@/components/organisms/Navbar'; const openSansFont = Open_Sans({ - subsets: ["latin"], - weight: ["300", "400", "500", "600", "700"], + subsets: ['latin'], + weight: ['300', '400', '500', '600', '700'] }); interface Props { - children: React.ReactNode + children: React.ReactNode; } -export const Wrapper:FC = ({ - children - }) => { - - const [isExpanded, setIsExpanded] = useState(true) - let isOpen = true; - - const handleToggle = () => { - setIsExpanded(!isExpanded) - isOpen = !isOpen - } - - return ( - = ({ children }) => { + const [isExpanded, setIsExpanded] = useState(true); + let isOpen = true; + + const handleToggle = () => { + setIsExpanded(!isExpanded); + isOpen = !isOpen; + }; + + return ( + +
+ + + -
- - - - {children} - - - - ) -} - -export default Wrapper \ No newline at end of file + {children} + + + + ); +}; + +export default Wrapper; diff --git a/web/src/assets/icons/FilterCog.tsx b/web/src/assets/icons/FilterCog.tsx index 4d0ae75..2efa954 100644 --- a/web/src/assets/icons/FilterCog.tsx +++ b/web/src/assets/icons/FilterCog.tsx @@ -1,20 +1,20 @@ import { SvgIcon } from '@mui/material'; interface Props { - style?: string; + style?: string; } export const FilterCog = ({ style = '' }: Props) => { - return ( - - - - - - ); + return ( + + + + + + ); }; diff --git a/web/src/assets/icons/FilterRemove.tsx b/web/src/assets/icons/FilterRemove.tsx index 8b40a87..4fb39e5 100644 --- a/web/src/assets/icons/FilterRemove.tsx +++ b/web/src/assets/icons/FilterRemove.tsx @@ -1,20 +1,20 @@ import { SvgIcon } from '@mui/material'; interface Props { - style?: string; + style?: string; } export const FilterRemove = ({ style = '' }: Props) => { - return ( - - - - - - ); + return ( + + + + + + ); }; diff --git a/web/src/assets/icons/Logo.tsx b/web/src/assets/icons/Logo.tsx index 088c7a6..e6fecd7 100644 --- a/web/src/assets/icons/Logo.tsx +++ b/web/src/assets/icons/Logo.tsx @@ -1,12 +1,26 @@ export const Logo = () => ( - - - + + - - - - + + + + - -) \ No newline at end of file + +); diff --git a/web/src/components/atoms/Pagination/index.tsx b/web/src/components/atoms/Pagination/index.tsx deleted file mode 100644 index ec8c7c8..0000000 --- a/web/src/components/atoms/Pagination/index.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { Pagination as MUIPagination } from '@mui/material'; -import { FC } from 'react'; - -interface Props { - count: number; - page: number; - onChange: (page: number) => void; -} - -const Pagination: FC = ({ count, page, onChange }: Props) => { - return ( - , newPage: number) => - onChange(newPage) - } - color='primary' - shape='rounded' - showFirstButton - showLastButton - /> - ); -}; - -export default Pagination; diff --git a/web/src/components/atoms/StatusChip/index.tsx b/web/src/components/atoms/StatusChip/index.tsx index c031708..0c09ea0 100644 --- a/web/src/components/atoms/StatusChip/index.tsx +++ b/web/src/components/atoms/StatusChip/index.tsx @@ -3,15 +3,15 @@ import { Chip, ChipProps } from '@mui/material'; import { FC } from 'react'; const StatusChip: FC = ({ label }: ChipProps) => { - return ( - - ); + return ( + + ); }; export default StatusChip; diff --git a/web/src/components/molecules/CreatedDateRangeFilter/hooks.ts b/web/src/components/molecules/CreatedDateRangeFilter/hooks.ts deleted file mode 100644 index ec5db0f..0000000 --- a/web/src/components/molecules/CreatedDateRangeFilter/hooks.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { useJobQueryContext } from '@/app/job/hooks'; -import { type Moment } from 'moment'; -import { useEffect, useState } from 'react'; - -export const useHooks = () => { - const { setStartDate, setEndDate } = useJobQueryContext(); - const [initialStartDate, setInitialStartDate] = useState(null); - const [initialEndDate, setInitialEndDate] = useState(null); - const [error, setError] = useState(''); - - const handleStartDateChange = (date: Moment | null): void => { - setInitialStartDate(date); - }; - - const handleEndDateChange = (date: Moment | null): void => { - setInitialEndDate(date); - }; - - useEffect(() => { - if (initialStartDate && !initialEndDate || initialEndDate && !initialStartDate) { - setError('Both start and end dates must be set'); - } - - if(!initialStartDate && !initialEndDate) { - setError(''); - setStartDate(null); - setEndDate(null); - } - - if (initialStartDate && initialEndDate) { - if (initialEndDate < initialStartDate) { - setError('Please set a vaild date'); - } else { - setError(''); - setStartDate(initialStartDate); - setEndDate(initialEndDate); - } - } - }, [initialStartDate, initialEndDate]); - - return { - error, - initialStartDate, - handleStartDateChange, - initialEndDate, - handleEndDateChange, - }; -}; diff --git a/web/src/components/molecules/CreatedDateRangeFilter/index.tsx b/web/src/components/molecules/CreatedDateRangeFilter/index.tsx deleted file mode 100644 index ebd7697..0000000 --- a/web/src/components/molecules/CreatedDateRangeFilter/index.tsx +++ /dev/null @@ -1,80 +0,0 @@ -'use-client'; - -import { Box } from '@mui/material'; -import { DatePicker } from '@mui/x-date-pickers'; -import { AdapterMoment } from '@mui/x-date-pickers/AdapterMoment'; -import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; -import { useHooks } from './hooks'; - -const CreateDateRangeFilter = () => { - const { - error, - initialStartDate, - handleStartDateChange, - initialEndDate, - handleEndDateChange - } = useHooks(); - - return ( - - - - - - - ); -}; - -export default CreateDateRangeFilter; diff --git a/web/src/components/molecules/DateRangeField/hooks.ts b/web/src/components/molecules/DateRangeField/hooks.ts new file mode 100644 index 0000000..ad7f78f --- /dev/null +++ b/web/src/components/molecules/DateRangeField/hooks.ts @@ -0,0 +1,49 @@ +import { type Moment } from 'moment'; +import { useEffect, useState } from 'react'; + +interface Props { + setStartDate: (date: Moment | null) => void, + setEndDate: (date: Moment | null) => void +} + +export const useHooks = ({ setStartDate, setEndDate }: Props) => { + const [initialStartDate, setInitialStartDate] = useState(null); + const [initialEndDate, setInitialEndDate] = useState(null); + const [error, setError] = useState(''); + + const handleStartDateChange = (date: Moment | null): void => { + setInitialStartDate(date); + }; + + const handleEndDateChange = (date: Moment | null): void => { + setInitialEndDate(date); + }; + + useEffect(() => { + if (initialStartDate && !initialEndDate || initialEndDate && !initialStartDate) { + setError('Both start and end dates must be set'); + } + + if (!initialStartDate && !initialEndDate) { + setError(''); + setStartDate(null); + setEndDate(null); + } + + if (initialStartDate && initialEndDate) { + if (initialEndDate < initialStartDate) { + setError('Please set a vaild date'); + } else { + setError(''); + setStartDate(initialStartDate); + setEndDate(initialEndDate); + } + } + }, [initialStartDate, initialEndDate]); + + return { + error, + handleStartDateChange, + handleEndDateChange, + }; +}; diff --git a/web/src/components/molecules/DateRangeField/index.tsx b/web/src/components/molecules/DateRangeField/index.tsx new file mode 100644 index 0000000..557ddf6 --- /dev/null +++ b/web/src/components/molecules/DateRangeField/index.tsx @@ -0,0 +1,89 @@ +'use-client'; + +import { Box } from '@mui/material'; +import { DatePicker } from '@mui/x-date-pickers'; +import { AdapterMoment } from '@mui/x-date-pickers/AdapterMoment'; +import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; +import { Moment } from 'moment'; +import { useHooks } from './hooks'; + +interface Props { + label: string; + disableFuture?: boolean; + startDate: Moment | null; + endDate: Moment | null; + setStartDate: (date: Moment | null) => void; + setEndDate: (date: Moment | null) => void; +} + +const DateRangeField = ({ + label, + disableFuture = false, + startDate, + endDate, + setStartDate, + setEndDate +}: Props) => { + const { error, handleStartDateChange, handleEndDateChange } = useHooks({ + setStartDate, + setEndDate + }); + + const sxProps = { + '& .MuiInputBase-root': { + backgroundColor: 'white' + }, + '& .MuiFormHelperText-root .Mui-error': { + backgroundColor: 'transparent' + } + }; + + return ( + + + + + + + ); +}; + +export default DateRangeField; diff --git a/web/src/components/molecules/EstimationStatusFilter/hooks.ts b/web/src/components/molecules/EstimationStatusFilter/hooks.ts deleted file mode 100644 index 8bdcc38..0000000 --- a/web/src/components/molecules/EstimationStatusFilter/hooks.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { formatEnum, useJobQueryContext } from '@/app/job/hooks'; -import { EstimationStatusEnum } from '@/utils/constants/estimationStatusEnum'; -import { SelectChangeEvent } from '@mui/material'; - -export const useHooks = () => { - const { status, setStatus } = useJobQueryContext(); - - const handleChange = (e: SelectChangeEvent) => { - setStatus(e.target.value); - }; - - const statusOptions = Object.keys(EstimationStatusEnum).map((key) => { - return { - value: EstimationStatusEnum[key as keyof typeof EstimationStatusEnum], - name: formatEnum(key) - } - }); - - return { - status, - statusOptions, - handleChange - }; -}; diff --git a/web/src/components/molecules/EstimationStatusFilter/index.tsx b/web/src/components/molecules/EstimationStatusFilter/index.tsx deleted file mode 100644 index 9553d24..0000000 --- a/web/src/components/molecules/EstimationStatusFilter/index.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import styles from '@/styles/Filter.module.css'; -import { FormControl, InputLabel, MenuItem, Select } from '@mui/material'; -import { useHooks } from './hooks'; - -const EstimationStatusFilter = () => { - const { status, statusOptions, handleChange } = useHooks(); - - return ( - - - Estimation Status - - - - ); -}; - -export default EstimationStatusFilter; diff --git a/web/src/components/molecules/NavBarItem.tsx b/web/src/components/molecules/NavBarItem.tsx index c0ad149..f8e620e 100644 --- a/web/src/components/molecules/NavBarItem.tsx +++ b/web/src/components/molecules/NavBarItem.tsx @@ -1,70 +1,72 @@ -'use client' +'use client'; -import Link from 'next/link' -import React, { FC } from 'react' -import { usePathname } from 'next/navigation' -import { Box, Typography } from '@mui/material' -import {SvgIconComponent} from '@mui/icons-material' +import { SvgIconComponent } from '@mui/icons-material'; +import { Box, Typography } from '@mui/material'; +import Link from 'next/link'; +import { usePathname } from 'next/navigation'; +import { FC } from 'react'; interface Props { - label: string, - Icon: SvgIconComponent - active?: boolean, - width?: number | string, - height?: number | string, - paddingX?: number | string, - gap?: number, - linkTo?: string + label: string; + Icon: SvgIconComponent; + active?: boolean; + width?: number | string; + height?: number | string; + paddingX?: number | string; + gap?: number; + linkTo?: string; } const NavBarItem: FC = ({ - label, - Icon, - active = false, - width = 200, - height = 40, - paddingX = 2, - gap = 1, - linkTo = "/" - }) => { + label, + Icon, + active = false, + width = 200, + height = 40, + paddingX = 2, + gap = 1, + linkTo = '/' +}) => { + const pathname = usePathname(); + active = pathname === linkTo ? true : false; - const pathname = usePathname() - active = pathname === linkTo ? true : false + const backgroundColor = active ? 'primary.100' : 'primary.700'; + const textColor = active ? 'dark' : 'white'; - const backgroundColor = active ? 'primary.100' : 'primary.700' - const textColor = active? 'dark': 'white' + return ( + + div > span': { + color: 'dark' + }, + '> div > svg': { + color: 'dark' + } + } + }}> + + + + {label} + + + + + ); +}; - return( - - div > span": { - color: 'dark', - }, - "> div > svg": { - color: 'dark', - }, - } - }}> - - - - {label} - - - - ) -} - -export default NavBarItem +export default NavBarItem; diff --git a/web/src/components/molecules/Pagination/index.tsx b/web/src/components/molecules/Pagination/index.tsx new file mode 100644 index 0000000..ba88df3 --- /dev/null +++ b/web/src/components/molecules/Pagination/index.tsx @@ -0,0 +1,27 @@ +import { Pagination as MUIPagination } from '@mui/material'; +import { FC } from 'react'; + +interface Props { + count: number; + page: number; + onChange: (page: number) => void; +} + +const Pagination: FC = ({ count, page, onChange }: Props) => { + return ( + , newPage: number) => + onChange(newPage) + } + color='primary' + shape='rounded' + showFirstButton + showLastButton + /> + ); +}; + +export default Pagination; diff --git a/web/src/components/molecules/SearchBar/hooks.ts b/web/src/components/molecules/SearchBar/hooks.ts index ab13b6d..e0e5782 100644 --- a/web/src/components/molecules/SearchBar/hooks.ts +++ b/web/src/components/molecules/SearchBar/hooks.ts @@ -1,15 +1,15 @@ import { ChangeEvent, useState } from 'react'; export const useHooks = () => { - const [searchKeyword, setSearchKeyword] = useState(''); + const [searchKeyword, setSearchKeyword] = useState(''); - const handleSearch = (e: ChangeEvent) => { - setSearchKeyword(e.target.value); - }; + const handleSearch = (e: ChangeEvent) => { + setSearchKeyword(e.target.value); + }; - return { - searchKeyword, - handleSearch - }; + return { + searchKeyword, + handleSearch + }; }; diff --git a/web/src/components/molecules/SearchBar/index.tsx b/web/src/components/molecules/SearchBar/index.tsx index 83af8f4..75a6e0c 100644 --- a/web/src/components/molecules/SearchBar/index.tsx +++ b/web/src/components/molecules/SearchBar/index.tsx @@ -1,37 +1,37 @@ import styles from '@/styles/Filter.module.css'; import SearchIcon from '@mui/icons-material/Search'; import { - FormControl, - InputAdornment, - InputLabel, - OutlinedInput + FormControl, + InputAdornment, + InputLabel, + OutlinedInput } from '@mui/material'; import { useHooks } from './hooks'; const SearchBar = () => { - const { searchKeyword, handleSearch } = useHooks(); + const { searchKeyword, handleSearch } = useHooks(); - return ( - - Search Job - - - - } - /> - - ); + return ( + + Search Job + + + + } + /> + + ); }; export default SearchBar; diff --git a/web/src/components/molecules/SelectDropdown/index.tsx b/web/src/components/molecules/SelectDropdown/index.tsx new file mode 100644 index 0000000..85ce273 --- /dev/null +++ b/web/src/components/molecules/SelectDropdown/index.tsx @@ -0,0 +1,56 @@ +import styles from '@/styles/Filter.module.css'; +import { + FormControl, + InputLabel, + MenuItem, + Select, + SelectChangeEvent +} from '@mui/material'; + +interface Props { + label: string; + name: string; + value: string; + options: Array<{ value: string; name: string }>; + clearable?: boolean; + width?: number; + handleChange: (e: SelectChangeEvent) => void; +} + +const SelectDropdown = ({ + label, + name, + value, + options, + handleChange, + clearable = false, + width = 180 +}: Props) => { + return ( + + {label} + + + ); +}; + +export default SelectDropdown; diff --git a/web/src/components/molecules/StatusDisplay/index.tsx b/web/src/components/molecules/StatusDisplay/index.tsx index f45a18d..2a7609d 100644 --- a/web/src/components/molecules/StatusDisplay/index.tsx +++ b/web/src/components/molecules/StatusDisplay/index.tsx @@ -1,27 +1,26 @@ import { Box, CircularProgress, Typography } from '@mui/material'; interface Props { - isLoading?: boolean; - error?: string; + isLoading?: boolean; + error?: string; } const StatusDisplay = ({ isLoading = false, error }: Props): JSX.Element => { - return ( - - {isLoading ? ( - - ) : ( - error && {error} - )} - - ); + return ( + + {isLoading ? ( + + ) : ( + error && {error} + )} + + ); }; export default StatusDisplay; diff --git a/web/src/components/molecules/TagFilter/hooks.ts b/web/src/components/molecules/TagFilter/hooks.ts deleted file mode 100644 index eb706cd..0000000 --- a/web/src/components/molecules/TagFilter/hooks.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { formatEnum, useJobQueryContext } from '@/app/job/hooks'; -import { TagsEnum } from '@/utils/constants/tagsEnum'; - -import { SelectChangeEvent } from '@mui/material'; - -export const useHooks = () => { - const { tag, setTag } = useJobQueryContext(); - - const handleChange = (e: SelectChangeEvent) => { - setTag(e.target.value); - }; - - const tagOptions = Object.keys(TagsEnum).map((key) => { - return { - value: TagsEnum[key as keyof typeof TagsEnum], - name: formatEnum(key) - } - }); - - return { - tag, - tagOptions, - handleChange - }; -}; diff --git a/web/src/components/molecules/TagFilter/index.tsx b/web/src/components/molecules/TagFilter/index.tsx deleted file mode 100644 index 6594b36..0000000 --- a/web/src/components/molecules/TagFilter/index.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import styles from '@/styles/Filter.module.css'; -import { FormControl, InputLabel, MenuItem, Select } from '@mui/material'; -import { useHooks } from './hooks'; - -const TagFilter = () => { - const { tag, tagOptions, handleChange } = useHooks(); - - return ( - - - Tag - - - - ); -}; - -export default TagFilter; diff --git a/web/src/components/organisms/Header.tsx b/web/src/components/organisms/Header.tsx index 3d751f8..0bedbc0 100644 --- a/web/src/components/organisms/Header.tsx +++ b/web/src/components/organisms/Header.tsx @@ -1,35 +1,41 @@ -import React, { FC } from 'react' -import { Box, Stack, Typography } from '@mui/material' +import { Box, Stack, Typography } from '@mui/material'; +import { FC } from 'react'; -import {Logo} from '../../assets/icons/Logo' -import HeaderProfile from './HeaderProfile' +import { Logo } from '../../assets/icons/Logo'; +import HeaderProfile from './HeaderProfile'; interface Props { - height?: number | string, + height?: number | string; } -const Header: FC = ({height = 64}) => { - return( - = ({ height = 64 }) => { + return ( + + - - - MeetsOne - - - - ) -} + + MeetsOne + + + + ); +}; -export default Header +export default Header; diff --git a/web/src/components/organisms/HeaderProfile.tsx b/web/src/components/organisms/HeaderProfile.tsx index e620be6..3eac6dd 100644 --- a/web/src/components/organisms/HeaderProfile.tsx +++ b/web/src/components/organisms/HeaderProfile.tsx @@ -1,125 +1,141 @@ -'use client' +'use client'; -import React, { FC } from 'react' +import { ArrowDropDown, Create, Logout } from '@mui/icons-material'; +import { + Avatar, + Box, + Button, + Grow, + IconButton, + Paper, + Popper, + Stack, + Typography +} from '@mui/material'; import ClickAwayListener from '@mui/material/ClickAwayListener'; -import {Create, Logout, ArrowDropDown} from '@mui/icons-material' -import { Box, Typography, Paper, Button, Stack, Avatar, Popper, Grow, IconButton } from '@mui/material' +import React, { FC } from 'react'; interface Props { - name?: string, - email?: string, - avatar?: string, + name?: string; + email?: string; + avatar?: string; } const HeaderProfile: FC = ({ - name = "John Doe", - email = "johndoe@sun-asterisk.com" + name = 'John Doe', + email = 'johndoe@sun-asterisk.com' }) => { - const [open, setOpen] = React.useState(false); - const anchorRef = React.useRef(null); + const [open, setOpen] = React.useState(false); + const anchorRef = React.useRef(null); - const handleToggle = () => { - setOpen((prevOpen) => !prevOpen); - }; + const handleToggle = () => { + setOpen((prevOpen) => !prevOpen); + }; - const handleClose = (event: Event | React.SyntheticEvent) => { - if ( - anchorRef.current && - anchorRef.current.contains(event.target as HTMLElement) - ) { - return; - } + const handleClose = (event: Event | React.SyntheticEvent) => { + if ( + anchorRef.current && + anchorRef.current.contains(event.target as HTMLElement) + ) { + return; + } - setOpen(false); - }; + setOpen(false); + }; - return( - - - - - - - + return ( + + + + + + + - - {({ TransitionProps }) => ( - - + {({ TransitionProps }) => ( + + + + + - - - - - - - {name} - {email} - - - - - - - - )} - - - ) -} + + + + {name} + {email} + + + + + + + + + )} + + + ); +}; -export default HeaderProfile +export default HeaderProfile; diff --git a/web/src/components/organisms/JobListTable/hooks.ts b/web/src/components/organisms/JobListTable/hooks.ts new file mode 100644 index 0000000..385c250 --- /dev/null +++ b/web/src/components/organisms/JobListTable/hooks.ts @@ -0,0 +1,22 @@ +import { JobListContext } from '@/app/job/context'; +import { JobTable } from '@/utils/types/job'; +import { useContext } from 'react'; + +const useJobListContext = (): JobTable => { + const jobs = useContext(JobListContext); + + if (jobs === undefined) { + throw new Error('Missing JobListContext'); + } + + return jobs; +}; + +export const useHooks = () => { + const { columns, data } = useJobListContext(); + + return { + columns, + data + }; +}; \ No newline at end of file diff --git a/web/src/components/organisms/JobListTable/index.tsx b/web/src/components/organisms/JobListTable/index.tsx index 4793763..d82e076 100644 --- a/web/src/components/organisms/JobListTable/index.tsx +++ b/web/src/components/organisms/JobListTable/index.tsx @@ -1,122 +1,108 @@ -/* eslint-disable no-mixed-spaces-and-tabs */ -import { useJobListContext } from '@/app/job/hooks'; import StatusChip from '@/components/atoms/StatusChip'; import { JobTableRow, TableColumn } from '@/utils/types/job'; import { - Box, - Paper, - Stack, - Table, - TableBody, - TableCell, - TableContainer, - TableHead, - TableRow, - Typography + Box, + Paper, + Stack, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + Typography } from '@mui/material'; +import { useHooks } from './hooks'; const JobListTable = (): JSX.Element => { - const { columns, data } = useJobListContext(); + const { columns, data } = useHooks(); - const renderTableCellContent = ( - column: TableColumn, - row: JobTableRow - ): JSX.Element => { - switch (column.key) { - case 'tags': - return ( - - {row.tags.map((tag, key) => ( - - ))} - - ); - case 'schedules': - return ( - - {row.schedules.map((schedule, key) => ( - - {schedule} - - ))} - - ); - case 'pipelinePhase': - return ; - case 'estimationStatus': - return ; - case 'cost': - return ( - - {row.estimation.cost} - - ); - default: - return ( - - {row[column.key] as string} - - ); - } - }; + const renderTableCellContent = ( + column: TableColumn, + row: JobTableRow + ): JSX.Element => { + switch (column.key) { + case 'tags': + return ( + + {row.tags.map((tag, key) => ( + + ))} + + ); + case 'schedules': + return ( + + {row.schedules.map((schedule, key) => ( + + {schedule} + + ))} + + ); + case 'pipelinePhase': + return ; + case 'estimationStatus': + return ; + case 'cost': + return {row.estimation.cost}; + default: + return ( + {row[column.key] as string} + ); + } + }; - return ( - - - - - - - - {columns.map((column) => ( - - - {column.label} - - - ))} - - - - {data?.map((row: JobTableRow) => ( - - {columns.map( - (column: TableColumn, index) => ( - - {renderTableCellContent( - column, - row - )} - - ) - )} - - ))} - -
-
-
-
-
- ); + return ( + + + + + + + + {columns.map((column) => ( + + + {column.label} + + + ))} + + + + {data?.map((row: JobTableRow) => ( + + {columns.map((column: TableColumn, index) => ( + + {renderTableCellContent(column, row)} + + ))} + + ))} + +
+
+
+
+
+ ); }; export default JobListTable; diff --git a/web/src/components/organisms/Navbar.tsx b/web/src/components/organisms/Navbar.tsx index 0fa7687..0cd018c 100644 --- a/web/src/components/organisms/Navbar.tsx +++ b/web/src/components/organisms/Navbar.tsx @@ -1,72 +1,73 @@ -"use client" +'use client'; -import React, { FC } from 'react' -import { Menu } from '@mui/icons-material' -import { Box, ButtonBase } from '@mui/material' +import { Menu } from '@mui/icons-material'; +import { Box, ButtonBase } from '@mui/material'; +import { FC } from 'react'; -import NavBarItem from '../molecules/NavBarItem' -import { Menus } from '@/utils/constants/navbarMenu' +import { Menus } from '@/utils/constants/navbarMenu'; +import NavBarItem from '../molecules/NavBarItem'; interface Props { - expanded?: boolean, - width?: number |string, - height?: number | string, - paddingY?: number |string, - rowGap?: number |string, - handleToggle: () => void + expanded?: boolean; + width?: number | string; + height?: number | string; + paddingY?: number | string; + rowGap?: number | string; + handleToggle: () => void; } const NavBar: FC = ({ - expanded = true, - width = 200, - paddingY = 2, - rowGap = 2, - handleToggle - }) => { - - return( - { + return ( + + - - - - - + + + + - - {Menus.map((item, index) => ( - - - - ) - )} - - - ) -} + + {Menus.map((item, index) => ( + + + + ))} + + + ); +}; -export default NavBar +export default NavBar; diff --git a/web/src/components/organisms/SearchFilterHeader/hooks.ts b/web/src/components/organisms/SearchFilterHeader/hooks.ts index d499f3c..fe26cf7 100644 --- a/web/src/components/organisms/SearchFilterHeader/hooks.ts +++ b/web/src/components/organisms/SearchFilterHeader/hooks.ts @@ -1,17 +1,64 @@ -import { useJobQueryContext } from '@/app/job/hooks'; -import { useState } from 'react'; +import { JobQueryContext } from '@/app/job/context'; +import { EstimationStatusEnum } from '@/utils/constants/estimationStatusEnum'; +import { TagsEnum } from '@/utils/constants/tagsEnum'; +import { convertEnumToOptions } from '@/utils/helpers'; +import { JobQuery } from '@/utils/types/job'; +import { SelectChangeEvent } from '@mui/material'; +import { useContext, useState } from 'react'; + +const useJobQueryContext = (): JobQuery => { + const query = useContext(JobQueryContext); + + if (query === undefined) { + throw new Error('Missing JobQueryContext'); + } + + return query; +}; export const useHooks = () => { - const { isFilter, setIsFilter } = useJobQueryContext(); - const [isExpanded, setIsExpanded] = useState(false); - - const toggleFilters = () => { - setIsExpanded(!isExpanded); - setIsFilter(!isFilter); - }; - - return { - isExpanded, - toggleFilters - }; + const { + isFilter, + setIsFilter, + tag, + setTag, + status, + setStatus, + startDate, + endDate, + setStartDate, + setEndDate + } = useJobQueryContext(); + const [isExpanded, setIsExpanded] = useState(false); + + const toggleFilters = () => { + setIsExpanded(!isExpanded); + setIsFilter(!isFilter); + }; + + const handleTagChange = (e: SelectChangeEvent) => { + setTag(e.target.value); + }; + + const handleStatusChange = (e: SelectChangeEvent) => { + setStatus(e.target.value); + }; + + const tagOptions = convertEnumToOptions(TagsEnum); + const statusOptions = convertEnumToOptions(EstimationStatusEnum); + + return { + isExpanded, + toggleFilters, + tag, + handleTagChange, + status, + handleStatusChange, + tagOptions, + statusOptions, + startDate, + endDate, + setStartDate, + setEndDate + }; }; diff --git a/web/src/components/organisms/SearchFilterHeader/index.tsx b/web/src/components/organisms/SearchFilterHeader/index.tsx index 0b4b406..cf718c0 100644 --- a/web/src/components/organisms/SearchFilterHeader/index.tsx +++ b/web/src/components/organisms/SearchFilterHeader/index.tsx @@ -2,65 +2,97 @@ import { FilterCog } from '@/assets/icons/FilterCog'; import { FilterRemove } from '@/assets/icons/FilterRemove'; +import SelectDropdown from '@/components/molecules/SelectDropdown'; import styles from '@/styles/Filter.module.css'; import TableRows from '@mui/icons-material/TableRows'; import { Box, Button, Collapse } from '@mui/material'; -import CreateDateRangeFilter from '../../molecules/CreatedDateRangeFilter'; -import EstimationStatusFilter from '../../molecules/EstimationStatusFilter'; +import DateRangeField from '../../molecules/DateRangeField'; import SearchBar from '../../molecules/SearchBar'; -import TagFilter from '../../molecules/TagFilter'; import { useHooks } from './hooks'; const SearchFilterHeader = (): JSX.Element => { - const { isExpanded, toggleFilters } = useHooks(); + const { + isExpanded, + toggleFilters, + tag, + tagOptions, + handleTagChange, + status, + handleStatusChange, + statusOptions, + startDate, + endDate, + setStartDate, + setEndDate + } = useHooks(); - return ( - - - - - - - - - - - - - - - ); + return ( + + + + + + + + + + + + + + + ); }; export default SearchFilterHeader; diff --git a/web/src/utils/constants/estimationStatusEnum.ts b/web/src/utils/constants/estimationStatusEnum.ts index 5493ce0..9b45eed 100644 --- a/web/src/utils/constants/estimationStatusEnum.ts +++ b/web/src/utils/constants/estimationStatusEnum.ts @@ -1,8 +1,8 @@ export enum EstimationStatusEnum { - NOT_YET_CREATED = "NOT_YET_CREATED", - MAKING = "MAKING", - APPROVED = "APPROVED", - SENT_TO_CUSTOMER = "SENT_TO_CUSTOMER", - CLOSED = "CLOSED", + NOT_YET_CREATED = "NOT_YET_CREATED", + MAKING = "MAKING", + APPROVED = "APPROVED", + SENT_TO_CUSTOMER = "SENT_TO_CUSTOMER", + CLOSED = "CLOSED", } diff --git a/web/src/utils/constants/jobTableData.ts b/web/src/utils/constants/jobTableData.ts index 56ac2e7..17d8aa8 100644 --- a/web/src/utils/constants/jobTableData.ts +++ b/web/src/utils/constants/jobTableData.ts @@ -1,13 +1,13 @@ import { TableColumn } from '../types/job'; -export const JobColumns : TableColumn[] = [ - { key: 'title', label: 'Job Title', width: 200 }, - { key: 'customer', label: 'Customer Name', width: 170 }, - { key: 'tags', label: 'Tags', width: 160 }, - { key: 'schedules', label: 'Work Schedule', width: 200 }, - { key: 'estimationStatus', label: 'Estimation Status', width: 180 }, - { key: 'personInCharge', label: 'Person in Charge', width: 170 }, - { key: 'pipelinePhase', label: 'Pipeline Phase', width: 150 }, - { key: 'cost', label: 'Cost', width: 120 }, - { key: 'createdAt', label: 'Created At', width: 120 } +export const JobColumns: TableColumn[] = [ + { key: 'title', label: 'Job Title', width: 200 }, + { key: 'customer', label: 'Customer Name', width: 170 }, + { key: 'tags', label: 'Tags', width: 160 }, + { key: 'schedules', label: 'Work Schedule', width: 200 }, + { key: 'estimationStatus', label: 'Estimation Status', width: 180 }, + { key: 'personInCharge', label: 'Person in Charge', width: 170 }, + { key: 'pipelinePhase', label: 'Pipeline Phase', width: 150 }, + { key: 'cost', label: 'Cost', width: 120 }, + { key: 'createdAt', label: 'Created At', width: 120 } ]; diff --git a/web/src/utils/constants/navbarMenu.ts b/web/src/utils/constants/navbarMenu.ts index 0613bb7..460d94b 100644 --- a/web/src/utils/constants/navbarMenu.ts +++ b/web/src/utils/constants/navbarMenu.ts @@ -1,30 +1,30 @@ import { - CalendarMonth, - Home, - Person, - SvgIconComponent, + CalendarMonth, + Home, + Person, + SvgIconComponent, } from "@mui/icons-material"; export interface IMenu { - name: string; - Icon: SvgIconComponent; - href: string; + name: string; + Icon: SvgIconComponent; + href: string; } export const Menus: IMenu[] = [ - { - name: "Job List", - Icon: Home, - href: "/job", - }, - { - name: "Calendar", - Icon: CalendarMonth, - href: "/calendar", - }, - { - name: "Customer", - Icon: Person, - href: "/customer", - }, + { + name: "Job List", + Icon: Home, + href: "/job", + }, + { + name: "Calendar", + Icon: CalendarMonth, + href: "/calendar", + }, + { + name: "Customer", + Icon: Person, + href: "/customer", + }, ]; diff --git a/web/src/utils/constants/statusChipColor.ts b/web/src/utils/constants/statusChipColor.ts index a31ff2c..83fd947 100644 --- a/web/src/utils/constants/statusChipColor.ts +++ b/web/src/utils/constants/statusChipColor.ts @@ -1,11 +1,11 @@ export interface ChipProps { - label: string; + label: string; } export const ChipColors: Record = { - 'Not Yet Created': '#FFB4AF', - Making: '#FDFF8F', - Approved: '#8AFFB2', - 'Sent To Customer': '#84C1FF', - Closed: '#65707b33' + 'Not Yet Created': '#FFB4AF', + Making: '#FDFF8F', + Approved: '#8AFFB2', + 'Sent To Customer': '#84C1FF', + Closed: '#65707b33' }; diff --git a/web/src/utils/constants/tagsEnum.ts b/web/src/utils/constants/tagsEnum.ts index 7d380b7..7960d02 100644 --- a/web/src/utils/constants/tagsEnum.ts +++ b/web/src/utils/constants/tagsEnum.ts @@ -1,5 +1,5 @@ export enum TagsEnum { - TAG_A = "TAG_A", - TAG_B = "TAG_B", - TAG_C = "TAG_C", + TAG_A = "TAG_A", + TAG_B = "TAG_B", + TAG_C = "TAG_C", } diff --git a/web/src/utils/helpers/index.tsx b/web/src/utils/helpers/index.tsx new file mode 100644 index 0000000..7245f14 --- /dev/null +++ b/web/src/utils/helpers/index.tsx @@ -0,0 +1,70 @@ +import { JobSchema, JobTableRow } from '../types/job'; +import { ScheduleSchema } from '../types/schedule'; + +export const formatEnum = (value: string): string => { + let words = value.split('_'); + words = words.map( + (word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase() + ); + return words.join(' '); +}; + +export const convertEnumToOptions = ( + enumObj: Record +): Array<{ value: string; name: string }> => { + const options = Object.keys(enumObj).map((key) => ({ + value: enumObj[key], + name: formatEnum(key) + })); + + return options; +}; + +export const convertTableData = (data: JobSchema[]): JobTableRow[] => { + const tableData = data.map((job: JobSchema) => ({ + id: job.id, + title: job.title, + customer: `${job.customer.firstName} ${job.customer.lastName}`, + tags: job.tags.map((tag) => formatEnum(tag)).sort(), + schedules: formatSchedules(job.schedules).sort(), + estimation: { + status: job.estimation?.status + ? formatEnum(job.estimation?.status) + : 'Not Yet Created', + cost: job.estimation?.totalCost ? `₱ ${job.estimation?.totalCost}` : '-' + }, + personInCharge: `${job.personInCharge.firstName} ${job.personInCharge.lastName}`, + pipelinePhase: formatEnum(job.pipelinePhase), + createdAt: formatDate(job.createdAt) + })); + + return tableData; +}; + +const formatDate = (date: string): string => { + return new Date(date).toLocaleDateString('en-US', { + year: 'numeric', + month: '2-digit', + day: '2-digit' + }); +}; + +const formatTime = (date: string): string => { + return new Date(date).toLocaleTimeString('en-US', { + hour: '2-digit', + minute: '2-digit', + hour12: false + }); +}; + +const formatSchedules = (schedules: ScheduleSchema[]): string[] => { + const scheduleData = schedules.map((schedule: ScheduleSchema) => { + const startDateTime = `${formatDate(schedule.startDate)} ${formatTime( + schedule.startTime + )}`; + const endDateTime = `${formatTime(schedule.endTime)}`; + return `${startDateTime} - ${endDateTime}`; + }); + + return scheduleData; +}; diff --git a/web/src/utils/services/axios.ts b/web/src/utils/services/axios.ts index 65be5c8..5584191 100644 --- a/web/src/utils/services/axios.ts +++ b/web/src/utils/services/axios.ts @@ -1,8 +1,8 @@ import axios from 'axios'; export const axiosInstance = axios.create({ - baseURL: process.env.NEXT_PUBLIC_API_BASE_URL, - headers: { - 'Content-Type': 'application/json', - }, + baseURL: process.env.NEXT_PUBLIC_API_BASE_URL, + headers: { + 'Content-Type': 'application/json', + }, }); diff --git a/web/src/utils/types/customer.ts b/web/src/utils/types/customer.ts index c7392ff..a1e6147 100644 --- a/web/src/utils/types/customer.ts +++ b/web/src/utils/types/customer.ts @@ -1,8 +1,8 @@ export interface CustomerSchema { - id: number; - firstName: string; - lastName: string; - email: string; - contact: string; - address: string; + id: number; + firstName: string; + lastName: string; + email: string; + contact: string; + address: string; } diff --git a/web/src/utils/types/estimation.ts b/web/src/utils/types/estimation.ts index 6d08484..f9598a6 100644 --- a/web/src/utils/types/estimation.ts +++ b/web/src/utils/types/estimation.ts @@ -1,7 +1,7 @@ export interface EstimationSchema { - id: number; - title: string; - document: string; - totalCost: number; - status: string; + id: number; + title: string; + document: string; + totalCost: number; + status: string; } diff --git a/web/src/utils/types/job.ts b/web/src/utils/types/job.ts index 8cd0af4..017da45 100644 --- a/web/src/utils/types/job.ts +++ b/web/src/utils/types/job.ts @@ -5,64 +5,64 @@ import { ScheduleSchema } from './schedule'; import { UserSchema } from './user'; export interface TableColumn { - key: string; - label: string; - width?: number; + key: string; + label: string; + width?: number; } export interface EstimationType { - status: string; - cost: string | number; + status: string; + cost: string | number; } export interface JobTableRow { - id: number; - title: string; - customer: string; - tags: Array; - schedules: Array; - estimation: EstimationType; - personInCharge: string; - pipelinePhase: string; - createdAt: string; - [key: string]: string | number | string[] | EstimationType; + id: number; + title: string; + customer: string; + tags: Array; + schedules: Array; + estimation: EstimationType; + personInCharge: string; + pipelinePhase: string; + createdAt: string; + [key: string]: string | number | string[] | EstimationType; } export interface JobTable { - columns: TableColumn[]; - data?: JobTableRow[]; + columns: TableColumn[]; + data?: JobTableRow[]; } export interface JobSchema { - id: number; - title: string; - type: string; - tags: string[]; - remarks?: string; - customer: CustomerSchema; - paymentMethod: string; - personInCharge: UserSchema; - schedules: ScheduleSchema[]; - estimation?: EstimationSchema; - pipelinePhase: string; - createdAt: string; - updatedAt: string; + id: number; + title: string; + type: string; + tags: string[]; + remarks?: string; + customer: CustomerSchema; + paymentMethod: string; + personInCharge: UserSchema; + schedules: ScheduleSchema[]; + estimation?: EstimationSchema; + pipelinePhase: string; + createdAt: string; + updatedAt: string; } export interface JobListType { - jobs: JobSchema[]; - count: number + jobs: JobSchema[]; + count: number; } export interface JobQuery { - tag: string; - setTag: (value: string) => void; - status: string; - setStatus: (value: string) => void; - startDate: Moment | null; - setStartDate: (date: Moment | null) => void; - endDate: Moment | null; - setEndDate: (date: Moment | null) => void; - isFilter: boolean; - setIsFilter: (value: boolean) => void; + tag: string; + setTag: (value: string) => void; + status: string; + setStatus: (value: string) => void; + startDate: Moment | null; + setStartDate: (date: Moment | null) => void; + endDate: Moment | null; + setEndDate: (date: Moment | null) => void; + isFilter: boolean; + setIsFilter: (value: boolean) => void; } diff --git a/web/src/utils/types/schedule.ts b/web/src/utils/types/schedule.ts index dd2f53b..1d98f7d 100644 --- a/web/src/utils/types/schedule.ts +++ b/web/src/utils/types/schedule.ts @@ -1,7 +1,7 @@ export interface ScheduleSchema { - id: number; - startDate: string; - endDate: string; - startTime: string; - endTime: string; + id: number; + startDate: string; + endDate: string; + startTime: string; + endTime: string; } diff --git a/web/src/utils/types/user.ts b/web/src/utils/types/user.ts index 2db10dd..cd25e32 100644 --- a/web/src/utils/types/user.ts +++ b/web/src/utils/types/user.ts @@ -1,7 +1,7 @@ export interface UserSchema { - id: number; - firstName: string; - lastName: string; - email: string; - role: string; + id: number; + firstName: string; + lastName: string; + email: string; + role: string; }