From eedfed1a65d79370acb3d805586a3c1c68969327 Mon Sep 17 00:00:00 2001 From: Berkay Date: Mon, 24 Nov 2025 21:04:14 +0300 Subject: [PATCH] parts and areas tested --- backend/src/app.module.ts | 2 + backend/src/build-area/build-area.module.ts | 6 +- backend/src/build-area/build-area.service.ts | 17 +- .../build-area/dto/create-build-area.input.ts | 23 +- .../build-area/dto/update-build-area.input.ts | 12 +- backend/src/build-iban/build-iban.service.ts | 3 +- backend/src/build-parts/build-parts.module.ts | 5 +- .../src/build-parts/build-parts.resolver.ts | 25 +- .../src/build-parts/build-parts.service.ts | 32 +- .../dto/create-build-part.input.ts | 53 ++- .../dto/list-build-parts.response.ts | 13 + .../dto/update-build-parts.input.ts | 50 +++ .../src/build-sites/build-sites.service.ts | 6 +- .../dto/update-build-types.input.ts | 4 +- backend/src/builds/build.module.ts | 11 +- backend/src/builds/build.resolver.ts | 39 +- backend/src/builds/build.service.ts | 114 ++++- backend/src/builds/dto/create-build.input.ts | 6 +- backend/src/builds/dto/list-build.response.ts | 46 +- backend/src/builds/dto/update-build.input.ts | 68 +++ backend/src/builds/example.graphql | 50 +++ backend/src/company/company.module.ts | 11 + backend/src/company/company.resolver.spec.ts | 18 + backend/src/company/company.resolver.ts | 29 ++ backend/src/company/company.service.spec.ts | 18 + backend/src/company/company.service.ts | 39 ++ .../src/company/dto/create-company.input.ts | 11 + .../src/company/dto/list-company.response.ts | 14 + .../src/company/dto/update-company.input.ts | 9 + backend/src/lib/getListOfModelByIDs.ts | 25 ++ backend/src/models/build-area.model.ts | 12 +- backend/src/models/build-parts.model.ts | 12 +- backend/src/models/build.model.ts | 29 +- backend/src/models/company.model.ts | 4 + backend/src/models/living-spaces.model.ts | 3 +- backend/src/models/user.model.ts | 4 + backend/src/users/dto/create-user.input.ts | 3 + backend/src/users/dto/update-user.input.ts | 3 + frontend/app/api/build-areas/add/route.ts | 3 +- frontend/app/api/build-areas/add/schema.ts | 3 +- frontend/app/api/build-areas/list/route.ts | 31 +- frontend/app/api/build-areas/update/route.ts | 17 +- frontend/app/api/build-areas/update/schema.ts | 20 +- frontend/app/api/build-types/add/route.ts | 2 + frontend/app/api/build-types/add/schema.ts | 2 + frontend/app/api/build-types/update/route.ts | 6 +- frontend/app/api/build-types/update/schema.ts | 26 +- frontend/app/api/builds-parts/add/route.ts | 38 ++ frontend/app/api/builds-parts/add/schema.ts | 23 + frontend/app/api/builds-parts/delete/route.ts | 23 + frontend/app/api/builds-parts/list/route.ts | 47 +++ frontend/app/api/builds-parts/update/route.ts | 45 ++ .../app/api/builds-parts/update/schema.ts | 24 ++ frontend/app/api/builds/add/route.ts | 51 +++ frontend/app/api/builds/add/schema.ts | 26 ++ frontend/app/api/builds/delete/route.ts | 23 + frontend/app/api/builds/list/route.ts | 53 +++ frontend/app/api/builds/update/route.ts | 33 ++ frontend/app/api/builds/update/schema.ts | 22 + frontend/app/build-areas/update/page.tsx | 4 +- frontend/app/build-parts/add/page.tsx | 6 +- frontend/app/build-parts/page.tsx | 6 +- frontend/app/build-parts/update/page.tsx | 8 +- frontend/app/builds/add/page.tsx | 4 +- frontend/app/builds/update/page.tsx | 4 +- frontend/components/selections/a.txt | 0 .../selections/lists/personSelection.tsx | 56 +++ frontend/pages/build-areas/add/form.tsx | 29 +- frontend/pages/build-areas/add/page.tsx | 13 +- frontend/pages/build-areas/add/queries.tsx | 7 +- .../pages/build-areas/add/table/columns.tsx | 3 - .../build-areas/add/table/data-table.tsx | 6 +- frontend/pages/build-areas/list/columns.tsx | 4 +- .../pages/build-areas/list/data-table.tsx | 8 +- frontend/pages/build-areas/page.tsx | 15 +- frontend/pages/build-areas/update/form.tsx | 4 +- frontend/pages/build-areas/update/page.tsx | 20 +- .../build-areas/update/table/columns.tsx | 2 +- .../build-areas/update/table/data-table.tsx | 8 +- .../build-ibans/update/table/data-table.tsx | 2 +- frontend/pages/build-ibans/update/types.ts | 1 - frontend/pages/build-parts/add/form.tsx | 306 ++++++++++++++ frontend/pages/build-parts/add/page.tsx | 41 ++ frontend/pages/build-parts/add/queries.tsx | 24 ++ frontend/pages/build-parts/add/schema.ts | 23 + .../pages/build-parts/add/table/columns.tsx | 115 +++++ .../build-parts/add/table/data-table.tsx | 256 ++++++++++++ .../pages/build-parts/add/table/schema.tsx | 28 ++ frontend/pages/build-parts/add/types.ts | 24 ++ frontend/pages/build-parts/list/columns.tsx | 111 +++++ .../pages/build-parts/list/data-table.tsx | 277 +++++++++++++ frontend/pages/build-parts/list/schema.tsx | 28 ++ frontend/pages/build-parts/page.tsx | 32 ++ frontend/pages/build-parts/queries.tsx | 36 ++ frontend/pages/build-parts/update/form.tsx | 299 +++++++++++++ frontend/pages/build-parts/update/page.tsx | 36 ++ frontend/pages/build-parts/update/queries.tsx | 25 ++ frontend/pages/build-parts/update/schema.ts | 23 + .../build-parts/update/table/columns.tsx | 113 +++++ .../build-parts/update/table/data-table.tsx | 281 +++++++++++++ .../pages/build-parts/update/table/schema.tsx | 26 ++ frontend/pages/build-parts/update/types.ts | 22 + frontend/pages/build-sites/add/page.tsx | 9 +- .../pages/build-sites/list/data-table.tsx | 6 +- frontend/pages/build-sites/page.tsx | 8 +- frontend/pages/build-sites/update/form.tsx | 9 +- frontend/pages/build-sites/update/page.tsx | 16 +- frontend/pages/build-sites/update/queries.tsx | 2 +- frontend/pages/build-sites/update/schema.ts | 4 +- .../build-sites/update/table/data-table.tsx | 6 +- frontend/pages/build-types/update/queries.tsx | 4 + frontend/pages/build-types/update/types.ts | 2 + frontend/pages/builds/add/form.tsx | 392 ++++++++++++++++++ frontend/pages/builds/add/page.tsx | 25 ++ frontend/pages/builds/add/queries.tsx | 25 ++ frontend/pages/builds/add/schema.ts | 28 ++ frontend/pages/builds/add/table/columns.tsx | 155 +++++++ .../pages/builds/add/table/data-table.tsx | 254 ++++++++++++ frontend/pages/builds/add/table/schema.tsx | 31 ++ frontend/pages/builds/add/types.ts | 24 ++ frontend/pages/builds/list/columns.tsx | 230 +++------- frontend/pages/builds/list/data-table.tsx | 70 +++- frontend/pages/builds/list/schema.tsx | 46 +- frontend/pages/builds/update/form.tsx | 311 ++++++++++++++ frontend/pages/builds/update/page.tsx | 36 ++ frontend/pages/builds/update/queries.tsx | 26 ++ frontend/pages/builds/update/schema.ts | 30 ++ .../pages/builds/update/table/columns.tsx | 145 +++++++ .../pages/builds/update/table/data-table.tsx | 279 +++++++++++++ frontend/pages/builds/update/table/schema.tsx | 31 ++ frontend/pages/builds/update/types.ts | 13 + 131 files changed, 5429 insertions(+), 471 deletions(-) create mode 100644 backend/src/build-parts/dto/list-build-parts.response.ts create mode 100644 backend/src/build-parts/dto/update-build-parts.input.ts create mode 100644 backend/src/company/company.module.ts create mode 100644 backend/src/company/company.resolver.spec.ts create mode 100644 backend/src/company/company.resolver.ts create mode 100644 backend/src/company/company.service.spec.ts create mode 100644 backend/src/company/company.service.ts create mode 100644 backend/src/company/dto/create-company.input.ts create mode 100644 backend/src/company/dto/list-company.response.ts create mode 100644 backend/src/company/dto/update-company.input.ts create mode 100644 backend/src/lib/getListOfModelByIDs.ts create mode 100644 frontend/app/api/builds-parts/add/route.ts create mode 100644 frontend/app/api/builds-parts/add/schema.ts create mode 100644 frontend/app/api/builds-parts/delete/route.ts create mode 100644 frontend/app/api/builds-parts/list/route.ts create mode 100644 frontend/app/api/builds-parts/update/route.ts create mode 100644 frontend/app/api/builds-parts/update/schema.ts create mode 100644 frontend/app/api/builds/add/route.ts create mode 100644 frontend/app/api/builds/add/schema.ts create mode 100644 frontend/app/api/builds/delete/route.ts create mode 100644 frontend/app/api/builds/list/route.ts create mode 100644 frontend/app/api/builds/update/route.ts create mode 100644 frontend/app/api/builds/update/schema.ts create mode 100644 frontend/components/selections/a.txt create mode 100644 frontend/components/selections/lists/personSelection.tsx create mode 100644 frontend/pages/build-parts/add/form.tsx create mode 100644 frontend/pages/build-parts/add/page.tsx create mode 100644 frontend/pages/build-parts/add/queries.tsx create mode 100644 frontend/pages/build-parts/add/schema.ts create mode 100644 frontend/pages/build-parts/add/table/columns.tsx create mode 100644 frontend/pages/build-parts/add/table/data-table.tsx create mode 100644 frontend/pages/build-parts/add/table/schema.tsx create mode 100644 frontend/pages/build-parts/add/types.ts create mode 100644 frontend/pages/build-parts/list/columns.tsx create mode 100644 frontend/pages/build-parts/list/data-table.tsx create mode 100644 frontend/pages/build-parts/list/schema.tsx create mode 100644 frontend/pages/build-parts/page.tsx create mode 100644 frontend/pages/build-parts/queries.tsx create mode 100644 frontend/pages/build-parts/update/form.tsx create mode 100644 frontend/pages/build-parts/update/page.tsx create mode 100644 frontend/pages/build-parts/update/queries.tsx create mode 100644 frontend/pages/build-parts/update/schema.ts create mode 100644 frontend/pages/build-parts/update/table/columns.tsx create mode 100644 frontend/pages/build-parts/update/table/data-table.tsx create mode 100644 frontend/pages/build-parts/update/table/schema.tsx create mode 100644 frontend/pages/build-parts/update/types.ts create mode 100644 frontend/pages/builds/add/form.tsx create mode 100644 frontend/pages/builds/add/page.tsx create mode 100644 frontend/pages/builds/add/queries.tsx create mode 100644 frontend/pages/builds/add/schema.ts create mode 100644 frontend/pages/builds/add/table/columns.tsx create mode 100644 frontend/pages/builds/add/table/data-table.tsx create mode 100644 frontend/pages/builds/add/table/schema.tsx create mode 100644 frontend/pages/builds/add/types.ts create mode 100644 frontend/pages/builds/update/form.tsx create mode 100644 frontend/pages/builds/update/page.tsx create mode 100644 frontend/pages/builds/update/queries.tsx create mode 100644 frontend/pages/builds/update/schema.ts create mode 100644 frontend/pages/builds/update/table/columns.tsx create mode 100644 frontend/pages/builds/update/table/data-table.tsx create mode 100644 frontend/pages/builds/update/table/schema.tsx create mode 100644 frontend/pages/builds/update/types.ts diff --git a/backend/src/app.module.ts b/backend/src/app.module.ts index e51ef89..a8c3df0 100644 --- a/backend/src/app.module.ts +++ b/backend/src/app.module.ts @@ -14,6 +14,7 @@ import { BuildTypesModule } from './build-types/build-types.module'; import { BuildAddressModule } from './build-address/build-address.module'; import { BuildIbanModule } from './build-iban/build-iban.module'; import { BuildSitesModule } from './build-sites/build-sites.module'; +import { CompanyModule } from './company/company.module'; @Module({ imports: [ @@ -33,6 +34,7 @@ import { BuildSitesModule } from './build-sites/build-sites.module'; BuildAddressModule, BuildIbanModule, BuildSitesModule, + CompanyModule, ], controllers: [AppController], providers: [AppService], diff --git a/backend/src/build-area/build-area.module.ts b/backend/src/build-area/build-area.module.ts index c4462ed..ab923a2 100644 --- a/backend/src/build-area/build-area.module.ts +++ b/backend/src/build-area/build-area.module.ts @@ -3,9 +3,13 @@ import { BuildAreaResolver } from './build-area.resolver'; import { BuildAreaService } from './build-area.service'; import { MongooseModule } from '@nestjs/mongoose'; import { BuildArea, BuildAreaSchema } from '@/models/build-area.model'; +import { Build, BuildSchema } from '@/models/build.model'; @Module({ - imports: [MongooseModule.forFeature([{ name: BuildArea.name, schema: BuildAreaSchema }])], + imports: [MongooseModule.forFeature([ + { name: BuildArea.name, schema: BuildAreaSchema }, + { name: Build.name, schema: BuildSchema } + ])], providers: [BuildAreaResolver, BuildAreaService] }) export class BuildAreaModule { } diff --git a/backend/src/build-area/build-area.service.ts b/backend/src/build-area/build-area.service.ts index 909bfae..9ea7596 100644 --- a/backend/src/build-area/build-area.service.ts +++ b/backend/src/build-area/build-area.service.ts @@ -6,25 +6,32 @@ import { CreateBuildAreaInput } from './dto/create-build-area.input'; import { ListBuildAreaResponse } from './dto/list-build-area.response'; import { UpdateBuildAreaInput } from './dto/update-build-area.input'; import { ListArguments } from '@/dto/list.input'; +import { Build, BuildDocument } from '@/models/build.model'; @Injectable() export class BuildAreaService { - constructor(@InjectModel(BuildArea.name) private readonly buildAreaModel: Model) { } + constructor( + @InjectModel(BuildArea.name) private readonly buildAreaModel: Model, + @InjectModel(Build.name) private readonly buildModel: Model + ) { } async findAll(projection: any, listArguments: ListArguments): Promise { const { skip, limit, sort, filters } = listArguments; const query: any = {}; if (filters && Object.keys(filters).length > 0) { Object.assign(query, filters) }; + if (filters?.buildId) { query.buildId = new Types.ObjectId(filters?.buildId) }; const totalCount = await this.buildAreaModel.countDocuments(query).exec(); const data = await this.buildAreaModel.find(query, projection, { lean: true }).skip(skip).limit(limit).sort(sort).exec(); return { data, totalCount }; } - async findById(id: Types.ObjectId, projection?: any): Promise { - return this.buildAreaModel.findById(id, projection, { lean: false }).populate({ path: 'buildArea', select: projection?.buildArea }).exec(); - } + async findById(id: Types.ObjectId, projection?: any): Promise { return this.buildAreaModel.findById(id, projection, { lean: false }).populate({ path: 'buildArea', select: projection?.buildArea }).exec() } - async create(input: CreateBuildAreaInput): Promise { const buildArea = new this.buildAreaModel(input); return buildArea.save() } + async create(input: CreateBuildAreaInput): Promise { + const getBuildID = await this.buildModel.findById(input?.buildId, { _id: 1 }, { lean: true }).exec(); + if (getBuildID) { input.buildId = new Types.ObjectId(getBuildID._id) }; + const buildArea = new this.buildAreaModel(input); return buildArea.save() + } async update(uuid: string, input: UpdateBuildAreaInput): Promise { const buildArea = await this.buildAreaModel.findOne({ uuid }); if (!buildArea) { throw new Error('BuildArea not found') }; buildArea.set(input); return buildArea.save() } diff --git a/backend/src/build-area/dto/create-build-area.input.ts b/backend/src/build-area/dto/create-build-area.input.ts index 05d10e3..9957d0b 100644 --- a/backend/src/build-area/dto/create-build-area.input.ts +++ b/backend/src/build-area/dto/create-build-area.input.ts @@ -1,19 +1,20 @@ +import { ExpiryBaseInput } from "@/models/base.model"; import { InputType, Field, ID, Float } from "@nestjs/graphql"; import { Types } from "mongoose"; @InputType() -export class CreateBuildAreaInput { +export class CreateBuildAreaInput extends ExpiryBaseInput { + + @Field(() => ID, { nullable: false }) + buildId: Types.ObjectId; @Field(() => ID, { nullable: true }) - build?: Types.ObjectId; + partTypeId?: Types.ObjectId; - @Field(() => ID, { nullable: true }) - partType?: Types.ObjectId; - - @Field(() => Float) + @Field() areaName: string; - @Field(() => Float) + @Field() areaCode: string; @Field() @@ -22,16 +23,16 @@ export class CreateBuildAreaInput { @Field() areaDirection: string; - @Field() + @Field(() => Float) areaGrossSize: number; - @Field() + @Field(() => Float) areaNetSize: number; - @Field() + @Field(() => Float) width: number; - @Field() + @Field(() => Float) size: number; } diff --git a/backend/src/build-area/dto/update-build-area.input.ts b/backend/src/build-area/dto/update-build-area.input.ts index 648ee21..a01a35e 100644 --- a/backend/src/build-area/dto/update-build-area.input.ts +++ b/backend/src/build-area/dto/update-build-area.input.ts @@ -10,10 +10,10 @@ export class UpdateBuildAreaInput { @Field(() => ID, { nullable: true }) partType?: Types.ObjectId; - @Field(() => Float, { nullable: true }) + @Field({ nullable: true }) areaName?: string; - @Field(() => Float, { nullable: true }) + @Field({ nullable: true }) areaCode?: string; @Field({ nullable: true }) @@ -22,16 +22,16 @@ export class UpdateBuildAreaInput { @Field({ nullable: true }) areaDirection?: string; - @Field({ nullable: true }) + @Field(() => Float, { nullable: true }) areaGrossSize?: number; - @Field({ nullable: true }) + @Field(() => Float, { nullable: true }) areaNetSize?: number; - @Field({ nullable: true }) + @Field(() => Float, { nullable: true }) width?: number; - @Field({ nullable: true }) + @Field(() => Float, { nullable: true }) size?: number; } \ No newline at end of file diff --git a/backend/src/build-iban/build-iban.service.ts b/backend/src/build-iban/build-iban.service.ts index 633bd39..08804f2 100644 --- a/backend/src/build-iban/build-iban.service.ts +++ b/backend/src/build-iban/build-iban.service.ts @@ -31,8 +31,7 @@ export class BuildIbanService { buildProjection(fields: Record): any { const projection: any = {}; for (const key in fields) { - if (key === 'buildIban' && typeof fields[key] === 'object') { for (const subField of Object.keys(fields[key])) { projection[`buildIban.${subField}`] = 1 } } - else { projection[key] = 1 } + if (key === 'buildIban' && typeof fields[key] === 'object') { for (const subField of Object.keys(fields[key])) { projection[`buildIban.${subField}`] = 1 } } else { projection[key] = 1 } }; return projection; } diff --git a/backend/src/build-parts/build-parts.module.ts b/backend/src/build-parts/build-parts.module.ts index e089a21..d7cbf63 100644 --- a/backend/src/build-parts/build-parts.module.ts +++ b/backend/src/build-parts/build-parts.module.ts @@ -3,9 +3,12 @@ import { BuildPartsService } from './build-parts.service'; import { BuildPartsResolver } from './build-parts.resolver'; import { MongooseModule } from '@nestjs/mongoose'; import { BuildParts, BuildPartsSchema } from '@/models/build-parts.model'; +import { Build, BuildSchema } from '@/models/build.model'; @Module({ - imports: [MongooseModule.forFeature([{ name: BuildParts.name, schema: BuildPartsSchema }])], + imports: [MongooseModule.forFeature([ + { name: BuildParts.name, schema: BuildPartsSchema }, + { name: Build.name, schema: BuildSchema }])], providers: [BuildPartsService, BuildPartsResolver] }) export class BuildPartsModule { } \ No newline at end of file diff --git a/backend/src/build-parts/build-parts.resolver.ts b/backend/src/build-parts/build-parts.resolver.ts index ee19d3a..9079064 100644 --- a/backend/src/build-parts/build-parts.resolver.ts +++ b/backend/src/build-parts/build-parts.resolver.ts @@ -2,27 +2,36 @@ import { Resolver, Query, Args, ID, Info, Mutation } from '@nestjs/graphql'; import { Types } from 'mongoose'; import { BuildParts } from '@/models/build-parts.model'; import { CreateBuildPartsInput } from './dto/create-build-part.input'; +import { BuildPartsService } from './build-parts.service'; +import { ListArguments } from '@/dto/list.input'; +import { ListBuildPartsResponse } from './dto/list-build-parts.response'; import graphqlFields from 'graphql-fields'; import type { GraphQLResolveInfo } from 'graphql'; -import { BuildPartsService } from './build-parts.service'; +import { UpdateBuildPartsInput } from './dto/update-build-parts.input'; @Resolver() export class BuildPartsResolver { constructor(private readonly buildPartsService: BuildPartsService) { } - @Query(() => [BuildParts], { name: 'BuildParts' }) - async getBuildParts(@Info() info: GraphQLResolveInfo): Promise { - const fields = graphqlFields(info); const projection = this.buildPartsService.buildProjection(fields); return this.buildPartsService.findAll(projection); + @Query(() => ListBuildPartsResponse, { name: 'buildParts' }) + async getBuildParts(@Info() info: GraphQLResolveInfo, @Args('input') input: ListArguments): Promise { + const fields = graphqlFields(info); const projection = this.buildPartsService.buildProjection(fields?.data); const { skip, limit, sort, filters } = input; + return await this.buildPartsService.findAll(projection, skip, limit, sort, filters); } - @Query(() => BuildParts, { name: 'BuildParts', nullable: true }) + @Query(() => BuildParts, { name: 'buildPart', nullable: true }) async getBuildPart(@Args('id', { type: () => ID }) id: string, @Info() info: GraphQLResolveInfo): Promise { const fields = graphqlFields(info); const projection = this.buildPartsService.buildProjection(fields); return this.buildPartsService.findById(new Types.ObjectId(id), projection); } @Mutation(() => BuildParts, { name: 'createBuildPart' }) - async createBuildPart(@Args('input') input: CreateBuildPartsInput): Promise { - return this.buildPartsService.create(input); - } + async createBuildPart(@Args('input') input: CreateBuildPartsInput): Promise { return this.buildPartsService.create(input) } + + @Mutation(() => Boolean, { name: 'deleteBuildPart' }) + async deleteBuildPart(@Args('uuid') uuid: string): Promise { return this.buildPartsService.delete(uuid) } + + @Mutation(() => BuildParts, { name: 'updateBuildPart' }) + async updateBuildPart(@Args('uuid') uuid: string, @Args('input') input: UpdateBuildPartsInput): Promise { return this.buildPartsService.update(uuid, input) } + } diff --git a/backend/src/build-parts/build-parts.service.ts b/backend/src/build-parts/build-parts.service.ts index d106e64..933005f 100644 --- a/backend/src/build-parts/build-parts.service.ts +++ b/backend/src/build-parts/build-parts.service.ts @@ -3,14 +3,24 @@ import { InjectModel } from '@nestjs/mongoose'; import { Types, Model } from 'mongoose'; import { BuildParts, BuildPartsDocument } from '@/models/build-parts.model'; import { CreateBuildPartsInput } from './dto/create-build-part.input'; +import { ListBuildPartsResponse } from './dto/list-build-parts.response'; +import { UpdateBuildPartsInput } from './dto/update-build-parts.input'; +import { Build, BuildDocument } from '@/models/build.model'; @Injectable() export class BuildPartsService { - constructor(@InjectModel(BuildParts.name) private readonly buildPartsModel: Model) { } + constructor( + @InjectModel(BuildParts.name) private readonly buildPartsModel: Model, + @InjectModel(Build.name) private readonly buildModel: Model + ) { } - async findAll(projection?: any): Promise { - return this.buildPartsModel.find({}, projection, { lean: false }).populate({ path: 'buildParts', select: projection?.buildParts }).exec(); + async findAll(projection: any, skip: number, limit: number, sort?: Record, filters?: Record): Promise { + const query: any = {}; if (filters && Object.keys(filters).length > 0) { Object.assign(query, filters) }; + if (query?.buildId) { query.buildId = new Types.ObjectId(query?.buildId) }; + const totalCount = await this.buildPartsModel.countDocuments(query).exec(); + const data = await this.buildPartsModel.find(query, projection, { lean: true }).skip(skip).limit(limit).sort(sort).exec(); + return { data, totalCount }; } async findById(id: Types.ObjectId, projection?: any): Promise { @@ -18,10 +28,22 @@ export class BuildPartsService { } async create(input: CreateBuildPartsInput): Promise { - const buildParts = new this.buildPartsModel(input); - return buildParts.save(); + const getBuildID = await this.buildModel.findById(input?.buildId, { _id: 1 }, { lean: true }).exec(); + if (getBuildID) { input.buildId = new Types.ObjectId(getBuildID._id) }; + if (input?.directionId) { input.directionId = new Types.ObjectId(input?.directionId) }; + if (input?.typeId) { input.typeId = new Types.ObjectId(input?.typeId) }; + const buildParts = new this.buildPartsModel(input); return buildParts.save() } + async update(uuid: string, input: UpdateBuildPartsInput): Promise { + const buildParts = await this.buildPartsModel.findOne({ _id: new Types.ObjectId(uuid) }); if (!buildParts) { throw new Error('BuildParts not found') }; + if (input?.buildId) { const getBuildID = await this.buildModel.findById(input?.buildId, { _id: 1 }, { lean: true }).exec(); if (getBuildID) { input.buildId = new Types.ObjectId(getBuildID._id) } }; + if (input?.directionId) { input.directionId = new Types.ObjectId(input?.directionId) }; + if (input?.typeId) { input.typeId = new Types.ObjectId(input?.typeId) }; buildParts.set(input); return buildParts.save() + } + + async delete(uuid: string): Promise { console.log(uuid); const buildParts = await this.buildPartsModel.deleteMany({ _id: new Types.ObjectId(uuid) }); return buildParts.deletedCount > 0 } + buildProjection(fields: Record): any { const projection: any = {}; for (const key in fields) { diff --git a/backend/src/build-parts/dto/create-build-part.input.ts b/backend/src/build-parts/dto/create-build-part.input.ts index 534f6d5..a5139c9 100644 --- a/backend/src/build-parts/dto/create-build-part.input.ts +++ b/backend/src/build-parts/dto/create-build-part.input.ts @@ -1,10 +1,55 @@ -import { InputType, Field, ID } from '@nestjs/graphql'; - +import { InputType, Field, ID, Float } from '@nestjs/graphql'; +import { Types } from 'mongoose'; @InputType() export class CreateBuildPartsInput { - @Field(() => ID) - build: string; + @Field(() => ID, { nullable: false }) + buildId: Types.ObjectId; + + @Field({ nullable: false }) + addressGovCode: string; + + @Field(() => Float, { nullable: false }) + no: number; + + @Field(() => Float, { nullable: false }) + level: number; + + @Field({ nullable: false }) + code: string; + + @Field(() => Float, { nullable: false }) + grossSize: number; + + @Field(() => Float, { nullable: false }) + netSize: number; + + @Field({ nullable: false }) + defaultAccessory: string; + + @Field({ nullable: false }) + humanLivability: boolean; + + @Field({ nullable: false }) + key: string; + + @Field(() => ID, { nullable: true }) + directionId: Types.ObjectId; + + @Field(() => ID, { nullable: true }) + typeId: Types.ObjectId; + + @Field(() => Boolean, { nullable: false }) + active: boolean; + + @Field(() => Boolean, { nullable: false }) + isConfirmed: boolean; + + @Field(() => String, { nullable: true }) + expiryStarts: string; + + @Field(() => String, { nullable: true }) + expiryEnds: string; } diff --git a/backend/src/build-parts/dto/list-build-parts.response.ts b/backend/src/build-parts/dto/list-build-parts.response.ts new file mode 100644 index 0000000..bd3f030 --- /dev/null +++ b/backend/src/build-parts/dto/list-build-parts.response.ts @@ -0,0 +1,13 @@ +import { BuildParts } from "@/models/build-parts.model"; +import { Field, Int, ObjectType } from "@nestjs/graphql"; + +@ObjectType() +export class ListBuildPartsResponse { + + @Field(() => [BuildParts], { nullable: true }) + data?: BuildParts[]; + + @Field(() => Int, { nullable: true }) + totalCount?: number; + +} diff --git a/backend/src/build-parts/dto/update-build-parts.input.ts b/backend/src/build-parts/dto/update-build-parts.input.ts new file mode 100644 index 0000000..0d15e55 --- /dev/null +++ b/backend/src/build-parts/dto/update-build-parts.input.ts @@ -0,0 +1,50 @@ +import { ExpiryBaseInput } from "@/models/base.model"; +import { InputType, Field, ID, Float } from "@nestjs/graphql"; +import { Types } from "mongoose"; + +@InputType() +export class UpdateBuildPartsInput extends ExpiryBaseInput { + + @Field(() => ID, { nullable: true }) + buildId?: Types.ObjectId; + + @Field({ nullable: true }) + addressGovCode?: string; + + @Field(() => Float, { nullable: true }) + no?: number; + + @Field(() => Float, { nullable: true }) + level?: number; + + @Field({ nullable: true }) + code?: string; + + @Field(() => Float, { nullable: true }) + grossSize?: number; + + @Field(() => Float, { nullable: true }) + netSize?: number; + + @Field({ nullable: true }) + defaultAccessory?: string; + + @Field({ nullable: true }) + humanLivability?: boolean; + + @Field({ nullable: true }) + key?: string; + + @Field(() => ID, { nullable: true }) + directionId?: Types.ObjectId; + + @Field(() => ID, { nullable: true }) + typeId?: Types.ObjectId; + + @Field(() => Boolean, { nullable: true }) + active?: boolean; + + @Field(() => Boolean, { nullable: true }) + isConfirmed?: boolean; + +} diff --git a/backend/src/build-sites/build-sites.service.ts b/backend/src/build-sites/build-sites.service.ts index 4d0d9af..2083e57 100644 --- a/backend/src/build-sites/build-sites.service.ts +++ b/backend/src/build-sites/build-sites.service.ts @@ -6,6 +6,7 @@ import { UpdateBuildSitesInput } from './dto/update-build-sites.input'; import { InjectModel } from '@nestjs/mongoose'; import { BuildSites, BuildSiteDocument } from '@/models/build-site.model'; import { Model } from 'mongoose'; +import { Build, BuildDocument } from '@/models/build.model'; @Injectable() export class BuildSitesService { @@ -23,7 +24,10 @@ export class BuildSitesService { return this.buildSitesModel.findById(id, projection, { lean: false }).populate({ path: 'buildSites', select: projection?.buildSites }).exec(); } - async create(input: CreateBuildSitesInput): Promise { const buildSite = new this.buildSitesModel(input); return buildSite.save() } + async create(input: CreateBuildSitesInput): Promise { + + const buildSite = new this.buildSitesModel(input); return buildSite.save() + } async update(uuid: string, input: UpdateBuildSitesInput): Promise { const buildSite = await this.buildSitesModel.findOne({ uuid }); if (!buildSite) { throw new Error('BuildSite not found') }; buildSite.set(input); return buildSite.save() } diff --git a/backend/src/build-types/dto/update-build-types.input.ts b/backend/src/build-types/dto/update-build-types.input.ts index 9e7bd1f..1e53589 100644 --- a/backend/src/build-types/dto/update-build-types.input.ts +++ b/backend/src/build-types/dto/update-build-types.input.ts @@ -1,8 +1,8 @@ -import { ChangableBase } from "@/models/base.model"; +import { ExpiryBaseInput } from "@/models/base.model"; import { InputType, Field } from "@nestjs/graphql"; @InputType() -export class UpdateBuildTypesInput extends ChangableBase { +export class UpdateBuildTypesInput extends ExpiryBaseInput { @Field({ nullable: true }) type?: string; diff --git a/backend/src/builds/build.module.ts b/backend/src/builds/build.module.ts index 6911a4d..c91ecc5 100644 --- a/backend/src/builds/build.module.ts +++ b/backend/src/builds/build.module.ts @@ -1,11 +1,18 @@ import { Module } from '@nestjs/common'; import { BuildResolver } from './build.resolver'; import { MongooseModule } from '@nestjs/mongoose'; -import { BuildSchema } from '@/models/build.model'; +import { BuildSchema, BuildIbanSchema } from '@/models/build.model'; import { BuildService } from './build.service'; +import { BuildAreaSchema } from '@/models/build-area.model'; +import { CompanySchema } from '@/models/company.model'; +import { PersonSchema } from '@/models/person.model'; +import { BuildTypesSchema } from '@/models/build-types.model'; @Module({ - imports: [MongooseModule.forFeature([{ name: 'Build', schema: BuildSchema }])], + imports: [MongooseModule.forFeature([ + { name: 'Build', schema: BuildSchema }, { name: 'BuildArea', schema: BuildAreaSchema }, { name: 'BuildIban', schema: BuildIbanSchema }, + { name: 'Company', schema: CompanySchema }, { name: 'Person', schema: PersonSchema }, { name: 'BuildTypes', schema: BuildTypesSchema }, + ])], providers: [BuildResolver, BuildService] }) export class BuildModule { } diff --git a/backend/src/builds/build.resolver.ts b/backend/src/builds/build.resolver.ts index 5394862..847071b 100644 --- a/backend/src/builds/build.resolver.ts +++ b/backend/src/builds/build.resolver.ts @@ -1,13 +1,13 @@ +import graphqlFields from 'graphql-fields'; import { Resolver, Query, Args, ID, Info, Mutation } from '@nestjs/graphql'; import { Types } from 'mongoose'; import { Build } from '@/models/build.model'; import { CreateBuildInput } from './dto/create-build.input'; -import { UpdateBuildResponsibleInput, UpdateBuildAttributeInput } from './dto/update-build.input'; -import graphqlFields from 'graphql-fields'; +import { UpdateBuildResponsibleInput, UpdateBuildAttributeInput, UpdateBuildInput } from './dto/update-build.input'; import { BuildService } from './build.service'; -import type { GraphQLResolveInfo } from 'graphql'; import { ListArguments } from '@/dto/list.input'; -import { ListBuildResponse } from './dto/list-build.response'; +import { ListBuildResponse, ListPartialAreaResponse, ListPartialCompanyResponse, ListPartialIbanResponse } from './dto/list-build.response'; +import type { GraphQLResolveInfo } from 'graphql'; @Resolver() export class BuildResolver { @@ -16,7 +16,22 @@ export class BuildResolver { @Query(() => ListBuildResponse, { name: 'builds' }) async getBuilds(@Info() info: GraphQLResolveInfo, @Args('input') input: ListArguments): Promise { - const fields = graphqlFields(info); const projection = this.buildService.buildProjection(fields); return this.buildService.findAll(projection, input.skip, input.limit, input.sort, input.filters); + const fields = graphqlFields(info); const projection = this.buildService.buildProjection(fields); return this.buildService.findAll(input.skip, input.limit, input.sort, input.filters); + } + + @Query(() => ListPartialAreaResponse, { name: 'getBuildsAreas' }) + async getBuildsAreas(@Info() info: GraphQLResolveInfo, @Args('id') id: string, @Args('input') input: ListArguments): Promise { + const fields = graphqlFields(info); const projection = this.buildService.buildProjection(fields); return this.buildService.findAllAreas(id, input.skip, input.limit, input.sort, input.filters); + } + + @Query(() => ListPartialIbanResponse, { name: 'getBuildsIbans' }) + async getBuildsIbans(@Info() info: GraphQLResolveInfo, @Args('id') id: string, @Args('input') input: ListArguments): Promise { + const fields = graphqlFields(info); const projection = this.buildService.buildProjection(fields); return this.buildService.findAllIbans(id, input.skip, input.limit, input.sort, input.filters); + } + + @Query(() => ListPartialCompanyResponse, { name: 'getBuildsCompaniesAndResponsibles' }) + async getBuildsCompaniesAndResponsibles(@Info() info: GraphQLResolveInfo, @Args('id') id: string, @Args('input') input: ListArguments): Promise { + const fields = graphqlFields(info); const projection = this.buildService.buildProjection(fields); return this.buildService.findAllCompaniesAndResponsibles(id, input.skip, input.limit, input.sort, input.filters); } @Query(() => Build, { name: 'build', nullable: true }) @@ -24,17 +39,19 @@ export class BuildResolver { const fields = graphqlFields(info); const projection = this.buildService.buildProjection(fields); return this.buildService.findById(new Types.ObjectId(id), projection); } + @Mutation(() => Boolean, { name: 'deleteBuild' }) + async deleteBuild(@Args('uuid') uuid: string): Promise { return this.buildService.delete(uuid) } + @Mutation(() => Build, { name: 'createBuild' }) async createBuild(@Args('input') input: CreateBuildInput): Promise { return this.buildService.create(input) } + @Mutation(() => Build, { name: 'updateBuild' }) + async updateBuild(@Args('uuid') uuid: string, @Args('input') input: UpdateBuildInput): Promise { return this.buildService.update(uuid, input) } + @Mutation(() => Build, { name: 'updateBuildResponsible' }) - async updateBuildResponsible(@Args('uuid', { type: () => ID }) uuid: string, @Args('input') input: UpdateBuildResponsibleInput): Promise { - return this.buildService.updateResponsible(uuid, input); - } + async updateBuildResponsible(@Args('uuid') uuid: string, @Args('input') input: UpdateBuildResponsibleInput): Promise { return this.buildService.updateResponsible(uuid, input) } @Mutation(() => Build, { name: 'updateBuildAttribute' }) - async updateBuildAttribute(@Args('uuid', { type: () => ID }) uuid: string, @Args('input') input: UpdateBuildAttributeInput): Promise { - return this.buildService.updateAttribute(uuid, input); - } + async updateBuildAttribute(@Args('uuid') uuid: string, @Args('input') input: UpdateBuildAttributeInput): Promise { return this.buildService.updateAttribute(uuid, input) } } diff --git a/backend/src/builds/build.service.ts b/backend/src/builds/build.service.ts index 73ee045..c0979aa 100644 --- a/backend/src/builds/build.service.ts +++ b/backend/src/builds/build.service.ts @@ -1,20 +1,32 @@ import { Injectable } from '@nestjs/common'; import { InjectModel } from '@nestjs/mongoose'; import { Types, Model } from 'mongoose'; -import { Build, BuildDocument } from '@/models/build.model'; +import { Build, BuildDocument, BuildIban, BuildIbanDocument } from '@/models/build.model'; import { CreateBuildInput } from './dto/create-build.input'; -import { UpdateBuildAttributeInput, UpdateBuildResponsibleInput } from './dto/update-build.input'; -import { ListBuildResponse } from './dto/list-build.response'; +import { UpdateBuildAttributeInput, UpdateBuildInput, UpdateBuildResponsibleInput } from './dto/update-build.input'; +import { ListBuildResponse, ListPartialAreaResponse, ListPartialCompanyResponse, ListPartialIbanResponse } from './dto/list-build.response'; +import { cleanRefArrayField } from '@/lib/getListOfModelByIDs'; +import { BuildArea, BuildAreaDocument } from '@/models/build-area.model'; +import { Company, CompanyDocument } from '@/models/company.model'; +import { Person, PersonDocument } from '@/models/person.model'; +import { BuildTypes, BuildTypesDocument } from '@/models/build-types.model'; @Injectable() export class BuildService { - constructor(@InjectModel(Build.name) private readonly buildModel: Model) { } + constructor( + @InjectModel(Build.name) private readonly buildModel: Model, + @InjectModel(BuildArea.name) private readonly areaModel: Model, + @InjectModel(BuildIban.name) private readonly ibanModel: Model, + @InjectModel(Company.name) private readonly companyModel: Model, + @InjectModel(Person.name) private readonly personModel: Model, + @InjectModel(BuildTypes.name) private readonly buildTypeModel: Model, + ) { } - async findAll(projection: any, skip: number, limit: number, sort?: Record, filters?: Record): Promise { + async findAll(skip: number, limit: number, sort?: Record, filters?: Record): Promise { const query: any = {}; if (filters && Object.keys(filters).length > 0) { Object.assign(query, filters) }; const totalCount = await this.buildModel.countDocuments(query).exec(); - const data = await this.buildModel.find(query).skip(skip).limit(limit).sort(sort).exec(); + const data = await this.buildModel.find(query).populate('buildType').skip(skip).limit(limit).sort(sort).exec(); return { data, totalCount }; } @@ -22,14 +34,98 @@ export class BuildService { return this.buildModel.findById(id, projection, { lean: false }).populate({ path: 'buildArea', select: projection?.buildArea }).exec(); } - async create(input: CreateBuildInput): Promise { const buildArea = new this.buildModel(input); return buildArea.save() } + async create(input: CreateBuildInput): Promise { + const getObjectIDofToken = await this.buildTypeModel.findOne({ token: input.buildType }); + const build = new this.buildModel(input); + if (!getObjectIDofToken?._id) { throw new Error('Build type not found') } + const idOfBuildTypes = new Types.ObjectId(getObjectIDofToken._id) + build.set({ buildType: idOfBuildTypes }); + await build.populate('buildType') + return build.save() + } + + async update(uuid: string, input: UpdateBuildInput): Promise { + let idOfBuildTypes: Types.ObjectId | null = null; + if (input.buildType) { + const buildTypeDoc = await this.buildTypeModel.findOne({ token: input.buildType }); + if (!buildTypeDoc?._id) { throw new Error('Build type not found') }; idOfBuildTypes = new Types.ObjectId(buildTypeDoc._id); + } + const build = await this.buildModel.findById(new Types.ObjectId(uuid)); + if (!build) { throw new Error('Build not found') } + const { info, buildType: _ignoredBuildType, ...rest } = input; + if (Object.keys(rest).length > 0) { build.set(rest) } + if (info) { build.set({ info: { ...(build.info as any)?.toObject?.() ?? build.info ?? {}, ...info } }) } + if (idOfBuildTypes) { build.set({ buildType: idOfBuildTypes }) } + const saved = await build.save(); await saved.populate('buildType'); return saved; + } + + async delete(uuid: string): Promise { + const build = await this.buildModel.deleteMany({ _id: new Types.ObjectId(uuid) }); return build.deletedCount > 0; + } async updateResponsible(uuid: string, input: UpdateBuildResponsibleInput): Promise { - const build = await this.buildModel.findOne({ uuid }); if (!build) { throw new Error('Build not found') }; build.set({ responsible: input }); return build.save() + const build = await this.buildModel.findOne({ _id: new Types.ObjectId(uuid) }); if (!build) { throw new Error('Build not found') }; build.set({ responsible: input }); return build.save() } async updateAttribute(uuid: string, input: UpdateBuildAttributeInput): Promise { - const build = await this.buildModel.findOne({ uuid }); if (!build) { throw new Error('Build not found') }; build.set({ attribute: input }); return build.save() + const build = await this.buildModel.findOne({ _id: new Types.ObjectId(uuid) }); if (!build) { throw new Error('Build not found') }; build.set({ attribute: input }); return build.save() + } + + async findAllAreas(id: string, skip: number, limit: number, sort?: Record, filters?: Record): Promise { + const buildId = new Types.ObjectId(id); + const { keptIds } = await cleanRefArrayField({ parentModel: this.buildModel, refModel: this.areaModel, parentId: buildId, fieldName: "areas", }); + if (keptIds.length === 0) { return { data: [], totalCount: 0 } } + const query: any = { _id: { $in: keptIds } }; + if (filters && Object.keys(filters).length > 0) { Object.assign(query, filters) } + const totalCount = await this.areaModel.countDocuments(query).exec(); + const data = await this.areaModel.find(query).skip(skip).limit(limit).sort(sort).exec(); + return { data, totalCount }; + } + + async findAllIbans(id: string, skip: number, limit: number, sort?: Record, filters?: Record): Promise { + const buildId = new Types.ObjectId(id); + const { keptIds } = await cleanRefArrayField({ parentModel: this.buildModel, refModel: this.ibanModel, parentId: buildId, fieldName: "ibans", }); + if (keptIds.length === 0) { return { data: [], totalCount: 0 } } + const query: any = { _id: { $in: keptIds } }; + if (filters && Object.keys(filters).length > 0) { Object.assign(query, filters) } + const totalCount = await this.ibanModel.countDocuments(query).exec(); + const data = await this.ibanModel.find(query).skip(skip).limit(limit).sort(sort).exec(); + return { data, totalCount }; + } + + async findAllCompaniesAndResponsibles(id: string, skip: number, limit: number, sort?: Record, filters?: Record): Promise { + const buildId = new Types.ObjectId(id); + const build = await this.buildModel.findById(buildId).lean(); + if (!build) throw new Error('Build not found'); + const responsibles = (build.responsibles || []) as { company?: string; person?: string }[]; + if (responsibles.length === 0) { return { data: [], totalCount: 0 } } + console.dir({ responsibles }, { depth: Infinity }); + const companyIds = Array.from(new Set(responsibles.map(r => r.company).filter((id): id is string => !!id))).map(id => new Types.ObjectId(id)); + const personIds = Array.from(new Set(responsibles.map(r => r.person).filter((id): id is string => !!id))).map(id => new Types.ObjectId(id)); + console.dir({ companyIds, personIds }, { depth: Infinity }); + const t = await this.companyModel.find({ _id: { $in: companyIds } }) + const l = await this.companyModel.find({ _id: { $in: companyIds } }).lean() + console.dir({ t, l }, { depth: Infinity }) + const [companies, persons] = await Promise.all([this.companyModel.find({ _id: { $in: companyIds } }).lean(), this.personModel.find({ _id: { $in: personIds } }).lean(),]); + console.dir({ companies, persons }, { depth: Infinity }) + const companyMap = new Map(companies.map(c => [String(c._id), c])); + const personMap = new Map(persons.map(p => [String(p._id), p])); + const cleanedResponsibles: { company?: string; person?: string }[] = []; + const resolvedList: { company?: any; person?: any }[] = []; + for (const r of responsibles) { + const companyId = r.company && String(r.company); + const personId = r.person && String(r.person); + const companyDoc = companyId ? companyMap.get(companyId) : undefined; + const personDoc = personId ? personMap.get(personId) : undefined; + if ((companyId && !companyDoc) || (personId && !personDoc)) { continue } + cleanedResponsibles.push({ company: companyId, person: personId }); + resolvedList.push({ company: companyDoc, person: personDoc }); + } + console.dir({ buildId, cleanedResponsibles, resolvedList }, { depth: Infinity }); + await this.buildModel.updateOne({ _id: buildId }, { $set: { responsibles: cleanedResponsibles } }); + const totalCount = resolvedList.length; + const data = resolvedList.slice(skip, skip + limit); + return { data, totalCount }; } buildProjection(fields: Record): any { diff --git a/backend/src/builds/dto/create-build.input.ts b/backend/src/builds/dto/create-build.input.ts index 5d625a1..e7e1358 100644 --- a/backend/src/builds/dto/create-build.input.ts +++ b/backend/src/builds/dto/create-build.input.ts @@ -49,15 +49,15 @@ export class CreateBuildInfoInput { @Field() garageCount: number; - @Field() - managementRoomId: number; + @Field({ nullable: true }) + managementRoomId?: string; } @InputType() export class CreateBuildInput extends ExpiryBaseInput { - @Field(() => ID) + @Field() buildType: string; @Field() diff --git a/backend/src/builds/dto/list-build.response.ts b/backend/src/builds/dto/list-build.response.ts index d3a508c..31a8d12 100644 --- a/backend/src/builds/dto/list-build.response.ts +++ b/backend/src/builds/dto/list-build.response.ts @@ -1,5 +1,8 @@ -import { Build } from "@/models/build.model"; import { Field, Int, ObjectType } from "@nestjs/graphql"; +import { Build, BuildIban } from "@/models/build.model"; +import { BuildArea } from "@/models/build-area.model"; +import { Company } from "@/models/company.model"; +import { Person } from "@/models/person.model"; @ObjectType() export class ListBuildResponse { @@ -11,3 +14,44 @@ export class ListBuildResponse { totalCount?: number; } + +@ObjectType() +export class ListPartialAreaResponse { + + @Field(() => [BuildArea], { nullable: true }) + data?: BuildArea[]; + + @Field(() => Int, { nullable: true }) + totalCount?: number; + +} + +@ObjectType() +export class ListPartialIbanResponse { + + @Field(() => [BuildIban], { nullable: true }) + data?: BuildIban[]; + + @Field(() => Int, { nullable: true }) + totalCount?: number; + +} + +@ObjectType() +class ResponsibleCompanyPerson { + @Field(() => Company, { nullable: true }) + company?: Company; + + @Field(() => Person, { nullable: true }) + person?: Person; +} + +@ObjectType() +export class ListPartialCompanyResponse { + + @Field(() => [ResponsibleCompanyPerson]) + data: ResponsibleCompanyPerson[]; + + @Field(() => Int) + totalCount: number; +} \ No newline at end of file diff --git a/backend/src/builds/dto/update-build.input.ts b/backend/src/builds/dto/update-build.input.ts index dea51bc..aa16012 100644 --- a/backend/src/builds/dto/update-build.input.ts +++ b/backend/src/builds/dto/update-build.input.ts @@ -1,4 +1,5 @@ import { InputType, Field, ID } from "@nestjs/graphql"; +import { ExpiryBaseInput } from "@/models/base.model"; @InputType() export class UpdateResponsibleInput { @@ -41,3 +42,70 @@ export class UpdateBuildResponsibleInput { person?: string; } + +@InputType() +export class UpdateBuildInfoInput { + + @Field({ nullable: true }) + govAddressCode?: string; + + @Field({ nullable: true }) + buildName?: string; + + @Field({ nullable: true }) + buildNo?: string; + + @Field({ nullable: true }) + maxFloor?: number; + + @Field({ nullable: true }) + undergroundFloor?: number; + + @Field({ nullable: true }) + buildDate?: Date; + + @Field({ nullable: true }) + decisionPeriodDate?: Date; + + @Field({ nullable: true }) + taxNo?: string; + + @Field({ nullable: true }) + liftCount?: number; + + @Field({ nullable: true }) + heatingSystem?: boolean; + + @Field({ nullable: true }) + coolingSystem?: boolean; + + @Field({ nullable: true }) + hotWaterSystem?: boolean; + + @Field({ nullable: true }) + blockServiceManCount?: number; + + @Field({ nullable: true }) + securityServiceManCount?: number; + + @Field({ nullable: true }) + garageCount?: number; + + @Field({ nullable: true }) + managementRoomId?: number; + +} + +@InputType() +export class UpdateBuildInput extends ExpiryBaseInput { + + @Field({ nullable: true }) + buildType?: string; + + @Field({ nullable: true }) + collectionToken?: string; + + @Field(() => UpdateBuildInfoInput, { nullable: true }) + info?: UpdateBuildInfoInput; + +} diff --git a/backend/src/builds/example.graphql b/backend/src/builds/example.graphql index 599bca8..ece2382 100644 --- a/backend/src/builds/example.graphql +++ b/backend/src/builds/example.graphql @@ -60,3 +60,53 @@ mutation CreateBuild($input: CreateBuildInput!) { } } } + +query ListBuild($input: ListArguments!) { + builds(input: $input) { + data { + _id + buildType { + token + typeToken + type + } + collectionToken + info { + govAddressCode + buildName + buildNo + maxFloor + undergroundFloor + buildDate + decisionPeriodDate + taxNo + liftCount + heatingSystem + coolingSystem + hotWaterSystem + blockServiceManCount + securityServiceManCount + garageCount + managementRoomId + } + } + totalCount + } +} + +query GetBuildsAreas($id: String!, $input: ListArguments!) { + getBuildsAreas(id: $id, input: $input) { + data { + _id + uuid + areaName + areaCode + areaType + areaNetSize + areaDirection + areaGrossSize + } + totalCount + __typename + } +} diff --git a/backend/src/company/company.module.ts b/backend/src/company/company.module.ts new file mode 100644 index 0000000..36e6110 --- /dev/null +++ b/backend/src/company/company.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { CompanyService } from './company.service'; +import { CompanyResolver } from './company.resolver'; +import { MongooseModule } from '@nestjs/mongoose'; +import { Company, CompanySchema } from '@/models/company.model'; + +@Module({ + imports: [MongooseModule.forFeature([{ name: Company.name, schema: CompanySchema }])], + providers: [CompanyService, CompanyResolver] +}) +export class CompanyModule { } diff --git a/backend/src/company/company.resolver.spec.ts b/backend/src/company/company.resolver.spec.ts new file mode 100644 index 0000000..6124fe9 --- /dev/null +++ b/backend/src/company/company.resolver.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { CompanyResolver } from './company.resolver'; + +describe('CompanyResolver', () => { + let resolver: CompanyResolver; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [CompanyResolver], + }).compile(); + + resolver = module.get(CompanyResolver); + }); + + it('should be defined', () => { + expect(resolver).toBeDefined(); + }); +}); diff --git a/backend/src/company/company.resolver.ts b/backend/src/company/company.resolver.ts new file mode 100644 index 0000000..02f8274 --- /dev/null +++ b/backend/src/company/company.resolver.ts @@ -0,0 +1,29 @@ +import { Resolver, Args, ID, Info, Mutation, Query } from '@nestjs/graphql'; +import { CompanyService } from './company.service'; +import { Company } from '@/models/company.model'; +import { CreateCompanyInput } from './dto/create-company.input'; +import { Types } from 'mongoose'; +import graphqlFields from 'graphql-fields'; +import type { GraphQLResolveInfo } from 'graphql'; +import { ListCompanyResponse } from './dto/list-company.response'; +import { ListArguments } from '@/dto/list.input'; + +@Resolver() +export class CompanyResolver { + + constructor(private readonly companyService: CompanyService) { } + + @Query(() => ListCompanyResponse, { name: 'companies' }) + async getCompanies(@Info() info: GraphQLResolveInfo, @Args('input') input: ListArguments): Promise { + const fields = graphqlFields(info); const projection = this.companyService.buildProjection(fields); return this.companyService.findAll(input.skip, input.limit, input.sort, input.filters); + } + + @Query(() => Company, { name: 'company', nullable: true }) + async getCompany(@Args('id', { type: () => ID }) id: string, @Info() info: GraphQLResolveInfo): Promise { + const fields = graphqlFields(info); const projection = this.companyService.buildProjection(fields); return this.companyService.findById(new Types.ObjectId(id), projection); + } + + @Mutation(() => Company, { name: 'createCompany' }) + async createCompany(@Args('input') input: CreateCompanyInput): Promise { return this.companyService.create(input) } + +} diff --git a/backend/src/company/company.service.spec.ts b/backend/src/company/company.service.spec.ts new file mode 100644 index 0000000..0a42ab0 --- /dev/null +++ b/backend/src/company/company.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { CompanyService } from './company.service'; + +describe('CompanyService', () => { + let service: CompanyService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [CompanyService], + }).compile(); + + service = module.get(CompanyService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/backend/src/company/company.service.ts b/backend/src/company/company.service.ts new file mode 100644 index 0000000..681fa86 --- /dev/null +++ b/backend/src/company/company.service.ts @@ -0,0 +1,39 @@ +import { Injectable } from '@nestjs/common'; +import { InjectModel } from '@nestjs/mongoose'; +import { Types, Model } from 'mongoose'; +import { Company, CompanyDocument } from '@/models/company.model'; +import { CreateCompanyInput } from './dto/create-company.input'; +import { ListCompanyResponse } from './dto/list-company.response'; +import { UpdateCompanyInput } from './dto/update-company.input'; + +@Injectable() +export class CompanyService { + + constructor(@InjectModel(Company.name) private readonly companyModel: Model) { } + + async findAll(projection: any, skip: number, limit: number, sort?: Record, filters?: Record): Promise { + const query: any = {}; if (filters && Object.keys(filters).length > 0) { Object.assign(query, filters) }; + const totalCount = await this.companyModel.countDocuments(query).exec(); + const data = await this.companyModel.find(query, projection, { lean: true }).skip(skip).limit(limit).sort(sort).exec(); + return { data, totalCount }; + } + + async findById(id: Types.ObjectId, projection?: any): Promise { + return this.companyModel.findById(id, projection, { lean: false }).populate({ path: 'company', select: projection?.company }).exec(); + } + + async create(input: CreateCompanyInput): Promise { const company = new this.companyModel(input); return company.save() } + + async update(uuid: string, input: UpdateCompanyInput): Promise { const company = await this.companyModel.findOne({ uuid }); if (!company) { throw new Error('Company not found') }; company.set(input); return company.save() } + + async delete(uuid: string): Promise { const company = await this.companyModel.deleteMany({ uuid }); return company.deletedCount > 0 } + + buildProjection(fields: Record): any { + const projection: any = {}; + for (const key in fields) { + if (key === 'buildSites' && typeof fields[key] === 'object') { for (const subField of Object.keys(fields[key])) { projection[`buildSites.${subField}`] = 1 } } + else { projection[key] = 1 } + }; return projection; + } + +} diff --git a/backend/src/company/dto/create-company.input.ts b/backend/src/company/dto/create-company.input.ts new file mode 100644 index 0000000..388838c --- /dev/null +++ b/backend/src/company/dto/create-company.input.ts @@ -0,0 +1,11 @@ + +import { InputType, Field, ID } from '@nestjs/graphql'; + + +@InputType() +export class CreateCompanyInput { + + @Field() + name: string; + +} diff --git a/backend/src/company/dto/list-company.response.ts b/backend/src/company/dto/list-company.response.ts new file mode 100644 index 0000000..25b8a81 --- /dev/null +++ b/backend/src/company/dto/list-company.response.ts @@ -0,0 +1,14 @@ +import { Field, ObjectType } from "@nestjs/graphql"; +import { Int } from "@nestjs/graphql"; +import { Company } from "@/models/company.model"; + +@ObjectType() +export class ListCompanyResponse { + + @Field(() => [Company], { nullable: true }) + data?: Company[]; + + @Field(() => Int, { nullable: true }) + totalCount?: number; + +} \ No newline at end of file diff --git a/backend/src/company/dto/update-company.input.ts b/backend/src/company/dto/update-company.input.ts new file mode 100644 index 0000000..c0370bf --- /dev/null +++ b/backend/src/company/dto/update-company.input.ts @@ -0,0 +1,9 @@ +import { InputType, Field, ID } from '@nestjs/graphql'; + +@InputType() +export class UpdateCompanyInput { + + @Field({ nullable: true }) + name?: string; + +} diff --git a/backend/src/lib/getListOfModelByIDs.ts b/backend/src/lib/getListOfModelByIDs.ts new file mode 100644 index 0000000..3c1e46c --- /dev/null +++ b/backend/src/lib/getListOfModelByIDs.ts @@ -0,0 +1,25 @@ +import { Types, Model } from "mongoose"; + +type ObjectIdLike = Types.ObjectId | string; + +interface CleanRefArrayParams { + parentModel: Model; + refModel: Model; + parentId: ObjectIdLike; + fieldName: string; +} + +export async function cleanRefArrayField({ parentModel, refModel, parentId, fieldName }: CleanRefArrayParams): Promise<{ keptIds: Types.ObjectId[]; removedIds: Types.ObjectId[] }> { + const parent = await parentModel.findById(parentId).lean(); + if (!parent) { throw new Error("Parent document not found") } + const rawIds: ObjectIdLike[] = parent[fieldName] || []; + if (!Array.isArray(rawIds) || rawIds.length === 0) { return { keptIds: [], removedIds: [] } } + const ids = rawIds.map((id) => typeof id === "string" ? new Types.ObjectId(id) : id); + const existing = await refModel.find({ _id: { $in: ids } }).select("_id").lean(); + const existingSet = new Set(existing.map((d) => String(d._id))); + const keptIds = ids.filter((id) => existingSet.has(String(id))); + const removedIds = ids.filter((id) => !existingSet.has(String(id))); + await parentModel.updateOne({ _id: parentId }, { $set: { [fieldName]: keptIds } }); + return { keptIds, removedIds }; +} + diff --git a/backend/src/models/build-area.model.ts b/backend/src/models/build-area.model.ts index ef3f17c..285403a 100644 --- a/backend/src/models/build-area.model.ts +++ b/backend/src/models/build-area.model.ts @@ -10,11 +10,11 @@ export class BuildArea extends Base { @Field(() => ID) readonly _id: string; - @Field(() => Float) + @Field() @Prop({ required: true }) areaName: string; - @Field(() => Float) + @Field() @Prop({ required: true }) areaCode: string; @@ -26,19 +26,19 @@ export class BuildArea extends Base { @Prop({ required: true }) areaDirection: string; - @Field() + @Field(() => Float) @Prop({ required: true }) areaGrossSize: number; - @Field() + @Field(() => Float) @Prop({ required: true }) areaNetSize: number; - @Field() + @Field(() => Float) @Prop({ required: true }) width: number; - @Field() + @Field(() => Float) @Prop({ required: true }) size: number; diff --git a/backend/src/models/build-parts.model.ts b/backend/src/models/build-parts.model.ts index a8c05ab..4eb9a4d 100644 --- a/backend/src/models/build-parts.model.ts +++ b/backend/src/models/build-parts.model.ts @@ -7,6 +7,9 @@ import { Base } from '@/models/base.model'; @Schema({ timestamps: true }) export class BuildParts extends Base { + @Field(() => ID) + readonly _id: string; + @Field(() => ID) @Prop({ type: Types.ObjectId, ref: 'Build', required: true }) buildId: Types.ObjectId; @@ -47,13 +50,14 @@ export class BuildParts extends Base { @Prop({ required: true }) key: string; - @Field(() => ID) - @Prop({ type: Types.ObjectId, ref: 'ApiEnumDropdown', required: true }) + @Field(() => ID, { nullable: true }) + @Prop({ type: Types.ObjectId, ref: 'ApiEnumDropdown', required: false }) directionId: Types.ObjectId; - @Field(() => ID) - @Prop({ type: Types.ObjectId, ref: 'ApiEnumDropdown', required: true }) + @Field(() => ID, { nullable: true }) + @Prop({ type: Types.ObjectId, ref: 'ApiEnumDropdown', required: false }) typeId: Types.ObjectId; + } export type BuildPartsDocument = BuildParts & Document; diff --git a/backend/src/models/build.model.ts b/backend/src/models/build.model.ts index 8c99366..a8ff337 100644 --- a/backend/src/models/build.model.ts +++ b/backend/src/models/build.model.ts @@ -4,6 +4,7 @@ import { ObjectType, Field, ID, Int } from '@nestjs/graphql'; import { Base } from '@/models/base.model'; import { Person } from '@/models/person.model'; import { Company } from '@/models/company.model'; +import { BuildTypes } from './build-types.model'; @ObjectType() @Schema({ timestamps: true }) @@ -37,13 +38,13 @@ export class BuildIban extends Base { @ObjectType() export class BuildResponsible { - @Field(() => [String], { nullable: true, defaultValue: [] }) - @Prop({ type: [String], default: [] }) - company?: string[]; + @Field(() => String, { nullable: true }) + @Prop({ type: String, default: '' }) + company?: string; - @Field(() => [String], { nullable: true, defaultValue: [] }) - @Prop({ type: [String], default: [] }) - person?: string[]; + @Field(() => String, { nullable: true }) + @Prop({ type: String, default: '' }) + person?: string; } @@ -110,9 +111,9 @@ export class BuildInfo { @Prop({ required: true }) garageCount: number; - @Field(() => Int) - @Prop({ required: true }) - managementRoomId: number; + @Field(() => String, { nullable: true }) + @Prop({ required: false }) + managementRoomId?: string; } @@ -123,13 +124,9 @@ export class Build { @Field() readonly _id: string; - // @Field(() => ID) - // @Prop({ type: Types.ObjectId, ref: 'BuildType', required: true }) - // buildType: Types.ObjectId; - - @Field(() => String, { nullable: true }) - @Prop({ required: true }) - buildType: string; + @Field(() => BuildTypes) + @Prop({ type: Types.ObjectId, ref: BuildTypes.name, required: true }) + buildType: Types.ObjectId; @Field(() => String, { nullable: true }) @Prop({ required: true }) diff --git a/backend/src/models/company.model.ts b/backend/src/models/company.model.ts index c5ed688..de2c42c 100644 --- a/backend/src/models/company.model.ts +++ b/backend/src/models/company.model.ts @@ -6,6 +6,10 @@ import { Base } from '@/models/base.model'; @ObjectType() @Schema({ timestamps: true }) export class Company extends Base { + + @Field(() => ID) + readonly _id: string; + @Field(() => ID) @Prop({ required: true }) name: string; diff --git a/backend/src/models/living-spaces.model.ts b/backend/src/models/living-spaces.model.ts index 2e21157..3e1bc0d 100644 --- a/backend/src/models/living-spaces.model.ts +++ b/backend/src/models/living-spaces.model.ts @@ -10,6 +10,7 @@ import { UserType } from '@/models/user-type.model'; @ObjectType() export class LivingSpaces extends Base { + @Field(() => ID) @Prop({ type: Types.ObjectId, ref: BuildParts.name, required: true }) part: Types.ObjectId; @@ -25,8 +26,8 @@ export class LivingSpaces extends Base { @Field(() => ID) @Prop({ type: Types.ObjectId, ref: UserType.name, required: true }) userType: Types.ObjectId; -} +} export type LivingSpacesDocument = LivingSpaces & Document; export const LivingSpacesSchema = SchemaFactory.createForClass(LivingSpaces); diff --git a/backend/src/models/user.model.ts b/backend/src/models/user.model.ts index 67973b3..26aadd7 100644 --- a/backend/src/models/user.model.ts +++ b/backend/src/models/user.model.ts @@ -36,6 +36,10 @@ export class User extends Base { @Field(() => ID) _id: string; + @Field({ nullable: true }) + @Prop({ required: false, default: '' }) + avatar?: string; + @Field({ nullable: true }) @Prop({ default: () => new Date(Date.now() + 30 * 24 * 60 * 60 * 1000) }) expiresAt?: Date; diff --git a/backend/src/users/dto/create-user.input.ts b/backend/src/users/dto/create-user.input.ts index 12ab114..ed2ad34 100644 --- a/backend/src/users/dto/create-user.input.ts +++ b/backend/src/users/dto/create-user.input.ts @@ -21,6 +21,9 @@ export class CollectionTokenInput { @InputType() export class CreateUserInput { + @Field({ nullable: true }) + avatar?: string; + @Field() password: string; diff --git a/backend/src/users/dto/update-user.input.ts b/backend/src/users/dto/update-user.input.ts index b26b0b0..e60ab3c 100644 --- a/backend/src/users/dto/update-user.input.ts +++ b/backend/src/users/dto/update-user.input.ts @@ -21,6 +21,9 @@ export class UpdateCollectionTokenInput { @InputType() export class UpdateUserInput { + @Field({ nullable: true }) + avatar?: string; + @Field({ nullable: true }) tag?: string; diff --git a/frontend/app/api/build-areas/add/route.ts b/frontend/app/api/build-areas/add/route.ts index 46357f6..c7c7687 100644 --- a/frontend/app/api/build-areas/add/route.ts +++ b/frontend/app/api/build-areas/add/route.ts @@ -27,8 +27,7 @@ export async function POST(request: Request) { } } `; - const variables = { input: validatedBody }; - const data = await client.request(query, variables); + const data = await client.request(query, { input: validatedBody }); return NextResponse.json({ data: data.createBuildArea, status: 200 }); } catch (err: any) { console.error(err); diff --git a/frontend/app/api/build-areas/add/schema.ts b/frontend/app/api/build-areas/add/schema.ts index a4b9e1c..85ea2d8 100644 --- a/frontend/app/api/build-areas/add/schema.ts +++ b/frontend/app/api/build-areas/add/schema.ts @@ -1,6 +1,7 @@ import { z } from "zod" export const buildAreaAddSchema = z.object({ + buildId: z.string(), areaName: z.string(), areaCode: z.string(), areaType: z.string(), @@ -10,7 +11,7 @@ export const buildAreaAddSchema = z.object({ width: z.number(), size: z.number(), expiryStarts: z.string().optional(), - expiryEnds: z.string().optional(), + expiryEnds: z.string().optional() }); export type BuildAreaAdd = z.infer; diff --git a/frontend/app/api/build-areas/list/route.ts b/frontend/app/api/build-areas/list/route.ts index ee2591f..ac404f6 100644 --- a/frontend/app/api/build-areas/list/route.ts +++ b/frontend/app/api/build-areas/list/route.ts @@ -12,26 +12,25 @@ export async function POST(request: Request) { const query = gql` query BuildAreas($input: ListArguments!) { buildAreas(input: $input) { - data { - _id - uuid - createdAt - areaName - areaCode - areaType - areaDirection - areaGrossSize - areaNetSize - width - size - expiryStarts - expiryEnds - } + data { + _id + uuid + createdAt + areaName + areaCode + areaType + areaDirection + areaGrossSize + areaNetSize + width + size + expiryStarts + expiryEnds + } totalCount } }`; const variables = { input: { limit, skip, sort, filters } }; - console.dir({ variables }) const data = await client.request(query, variables); return NextResponse.json({ data: data.buildAreas.data, totalCount: data.buildAreas.totalCount }); } catch (err: any) { diff --git a/frontend/app/api/build-areas/update/route.ts b/frontend/app/api/build-areas/update/route.ts index 2877930..051ae6d 100644 --- a/frontend/app/api/build-areas/update/route.ts +++ b/frontend/app/api/build-areas/update/route.ts @@ -14,24 +14,13 @@ export async function POST(request: Request) { try { const client = new GraphQLClient(endpoint); const query = gql` - mutation UpdateBuildAddress($uuid: String!, $input: UpdateBuildAddressInput!) { - updateBuildAddress(uuid: $uuid, input: $input) { - _id - buildNumber - doorNumber - floorNumber - commentAddress - letterAddress - shortLetterAddress - latitude - longitude - street - } + mutation UpdateBuildArea($uuid:String!, $input: UpdateBuildAreaInput!) { + updateBuildArea(uuid: $uuid, input: $input) { _id } } `; const variables = { uuid: uuid, input: validatedBody }; const data = await client.request(query, variables); - return NextResponse.json({ data: data.updateBuildAddress, status: 200 }); + return NextResponse.json({ data: data.updateBuildArea, status: 200 }); } catch (err: any) { console.error(err); return NextResponse.json({ error: err.message }, { status: 500 }); diff --git a/frontend/app/api/build-areas/update/schema.ts b/frontend/app/api/build-areas/update/schema.ts index f86efd4..e55dc29 100644 --- a/frontend/app/api/build-areas/update/schema.ts +++ b/frontend/app/api/build-areas/update/schema.ts @@ -1,15 +1,17 @@ import { z } from "zod" export const UpdateBuildAddressSchema = z.object({ - buildNumber: z.string().optional(), - doorNumber: z.string().optional(), - floorNumber: z.string().optional(), - commentAddress: z.string().optional(), - letterAddress: z.string().optional(), - shortLetterAddress: z.string().optional(), - latitude: z.number().optional(), - longitude: z.number().optional(), - street: z.string().optional(), + buildId: z.string(), + areaName: z.string(), + areaCode: z.string(), + areaType: z.string(), + areaDirection: z.string(), + areaGrossSize: z.number(), + areaNetSize: z.number(), + width: z.number(), + size: z.number(), + expiryStarts: z.string().optional(), + expiryEnds: z.string().optional() }); export type UpdateBuildAddress = z.infer; diff --git a/frontend/app/api/build-types/add/route.ts b/frontend/app/api/build-types/add/route.ts index 80d636d..2bdb65a 100644 --- a/frontend/app/api/build-types/add/route.ts +++ b/frontend/app/api/build-types/add/route.ts @@ -17,6 +17,8 @@ export async function POST(request: Request) { token typeToken description + expiryStarts + expiryEnds } } `; diff --git a/frontend/app/api/build-types/add/schema.ts b/frontend/app/api/build-types/add/schema.ts index d231918..dfbd42b 100644 --- a/frontend/app/api/build-types/add/schema.ts +++ b/frontend/app/api/build-types/add/schema.ts @@ -5,6 +5,8 @@ export const buildTypesAddSchema = z.object({ token: z.string(), typeToken: z.string(), description: z.string().optional().default(''), + expiryStarts: z.string().optional(), + expiryEnds: z.string().optional(), }); export type BuildTypesAdd = z.infer; diff --git a/frontend/app/api/build-types/update/route.ts b/frontend/app/api/build-types/update/route.ts index 44f0072..a159145 100644 --- a/frontend/app/api/build-types/update/route.ts +++ b/frontend/app/api/build-types/update/route.ts @@ -1,7 +1,7 @@ 'use server'; import { NextResponse } from 'next/server'; import { GraphQLClient, gql } from 'graphql-request'; -import { personUpdateSchema } from './schema'; +import { buildTypesUpdateSchema } from './schema'; const endpoint = "http://localhost:3001/graphql"; @@ -9,7 +9,7 @@ export async function POST(request: Request) { const searchUrl = new URL(request.url); const uuid = searchUrl.searchParams.get("uuid") || ""; const body = await request.json(); - const validatedBody = personUpdateSchema.parse(body); + const validatedBody = buildTypesUpdateSchema.parse(body); if (uuid === "") { return NextResponse.json({ error: "UUID is required" }, { status: 400 }) } try { const client = new GraphQLClient(endpoint); @@ -20,6 +20,8 @@ export async function POST(request: Request) { token typeToken description + expiryStarts + expiryEnds } } `; diff --git a/frontend/app/api/build-types/update/schema.ts b/frontend/app/api/build-types/update/schema.ts index f0b4a37..5c86857 100644 --- a/frontend/app/api/build-types/update/schema.ts +++ b/frontend/app/api/build-types/update/schema.ts @@ -1,22 +1,12 @@ import { z } from "zod" -export const personUpdateSchema = z.object({ - firstName: z.string().optional(), - surname: z.string().optional(), - middleName: z.string().optional(), - sexCode: z.string().optional(), - personRef: z.string().optional(), - personTag: z.string().optional(), - fatherName: z.string().optional(), - motherName: z.string().optional(), - countryCode: z.string().optional(), - nationalIdentityId: z.string().optional(), - birthPlace: z.string().optional(), - birthDate: z.string().optional(), - taxNo: z.string().optional().optional(), - birthname: z.string().optional().optional(), - expiryStarts: z.string().optional().optional(), - expiryEnds: z.string().optional().optional(), +export const buildTypesUpdateSchema = z.object({ + type: z.string().optional(), + token: z.string().optional(), + typeToken: z.string().optional(), + description: z.string().optional().default(''), + expiryStarts: z.string().optional(), + expiryEnds: z.string().optional(), }); -export type PeopleUpdate = z.infer; +export type BuildTypesUpdate = z.infer; diff --git a/frontend/app/api/builds-parts/add/route.ts b/frontend/app/api/builds-parts/add/route.ts new file mode 100644 index 0000000..e15333d --- /dev/null +++ b/frontend/app/api/builds-parts/add/route.ts @@ -0,0 +1,38 @@ +'use server'; +import { NextResponse } from 'next/server'; +import { GraphQLClient, gql } from 'graphql-request'; +import { buildPartsAddSchema } from './schema'; + +const endpoint = "http://localhost:3001/graphql"; + +export async function POST(request: Request) { + const body = await request.json(); + const validatedBody = buildPartsAddSchema.parse({ ...body.data, buildId: body.buildId }); + try { + const client = new GraphQLClient(endpoint); + const query = gql` + mutation CreateBuildParts($input: CreateBuildPartsInput!) { + createBuildPart(input: $input) { + buildId + addressGovCode + level + no + code + grossSize + netSize + defaultAccessory + humanLivability + key + directionId + typeId + } + } + `; + const variables = { input: validatedBody }; + const data = await client.request(query, variables); + return NextResponse.json({ data: data.createBuildPart, status: 200 }); + } catch (err: any) { + console.error(err); + return NextResponse.json({ error: err.message }, { status: 500 }); + } +} diff --git a/frontend/app/api/builds-parts/add/schema.ts b/frontend/app/api/builds-parts/add/schema.ts new file mode 100644 index 0000000..c54cafd --- /dev/null +++ b/frontend/app/api/builds-parts/add/schema.ts @@ -0,0 +1,23 @@ +import { z } from "zod" + +export const buildPartsAddSchema = z.object({ + buildId: z.string(), + addressGovCode: z.string(), + no: z.number(), + level: z.number(), + code: z.string(), + grossSize: z.number(), + netSize: z.number(), + defaultAccessory: z.string(), + humanLivability: z.boolean(), + key: z.string(), + directionId: z.string().optional(), + typeId: z.string().optional(), + active: z.boolean().default(true), + isConfirmed: z.boolean().default(false), + expiryStarts: z.string().optional(), + expiryEnds: z.string().optional() + +}); + +export type BuildPartsAdd = z.infer; diff --git a/frontend/app/api/builds-parts/delete/route.ts b/frontend/app/api/builds-parts/delete/route.ts new file mode 100644 index 0000000..e51c35a --- /dev/null +++ b/frontend/app/api/builds-parts/delete/route.ts @@ -0,0 +1,23 @@ +'use server'; +import { NextResponse } from 'next/server'; +import { GraphQLClient, gql } from 'graphql-request'; + +const endpoint = "http://localhost:3001/graphql"; + +export async function GET(request: Request) { + + const searchParams = new URL(request.url).searchParams; + const uuid = searchParams.get('uuid'); + if (!uuid) { return NextResponse.json({ error: 'UUID not found in search params' }, { status: 400 }) } + try { + const client = new GraphQLClient(endpoint); + const query = gql`mutation DeleteBuildPart($uuid: String!) { deleteBuildPart(uuid: $uuid) }`; + const variables = { uuid: uuid }; + const data = await client.request(query, variables); + return NextResponse.json({ data: data.deletePerson, status: 200 }); + } catch (err: any) { + console.error(err); + return NextResponse.json({ error: err.message }, { status: 500 }); + } + +} diff --git a/frontend/app/api/builds-parts/list/route.ts b/frontend/app/api/builds-parts/list/route.ts new file mode 100644 index 0000000..c6b677a --- /dev/null +++ b/frontend/app/api/builds-parts/list/route.ts @@ -0,0 +1,47 @@ +'use server'; +import { NextResponse } from 'next/server'; +import { GraphQLClient, gql } from 'graphql-request'; + +const endpoint = "http://localhost:3001/graphql"; + +export async function POST(request: Request) { + const body = await request.json(); + const { limit, skip, sort, filters } = body; + try { + const client = new GraphQLClient(endpoint); + const query = gql` + query BuildParts($input: ListArguments!) { + buildParts(input: $input) { + data { + _id + uuid + addressGovCode + no + level + code + grossSize + netSize + defaultAccessory + humanLivability + key + directionId + typeId + createdAt + updatedAt + expiryStarts + expiryEnds + active + isConfirmed + } + totalCount + } + } + `; + const variables = { input: { limit, skip, sort, filters } }; + const data = await client.request(query, variables); + return NextResponse.json({ data: data.buildParts.data, totalCount: data.buildParts.totalCount }); + } catch (err: any) { + console.error(err); + return NextResponse.json({ error: err.message }, { status: 500 }); + } +} diff --git a/frontend/app/api/builds-parts/update/route.ts b/frontend/app/api/builds-parts/update/route.ts new file mode 100644 index 0000000..df15368 --- /dev/null +++ b/frontend/app/api/builds-parts/update/route.ts @@ -0,0 +1,45 @@ +'use server'; +import { NextResponse } from 'next/server'; +import { GraphQLClient, gql } from 'graphql-request'; +import { UpdateBuildPartsSchema } from './schema'; + +const endpoint = "http://localhost:3001/graphql"; + +export async function POST(request: Request) { + const searchUrl = new URL(request.url); + const uuid = searchUrl.searchParams.get("uuid") || ""; + const body = await request.json(); + const validatedBody = UpdateBuildPartsSchema.parse(body); + if (uuid === "") { return NextResponse.json({ error: "UUID is required" }, { status: 400 }) } + try { + const client = new GraphQLClient(endpoint); + const query = gql` + mutation UpdateBuildPart($uuid:String!, $input: UpdateBuildPartsInput!) { + updateBuildPart(uuid: $uuid, input: $input) { + _id + buildId + addressGovCode + code + directionId + expiryEnds + expiryStarts + grossSize + humanLivability + isConfirmed + key + level + netSize + no + active + typeId + } + } + `; + const variables = { uuid: uuid, input: { ...validatedBody } }; + const data = await client.request(query, variables); + return NextResponse.json({ data: data.updateBuildPart, status: 200 }); + } catch (err: any) { + console.error(err); + return NextResponse.json({ error: err.message }, { status: 500 }); + } +} diff --git a/frontend/app/api/builds-parts/update/schema.ts b/frontend/app/api/builds-parts/update/schema.ts new file mode 100644 index 0000000..d19fd5e --- /dev/null +++ b/frontend/app/api/builds-parts/update/schema.ts @@ -0,0 +1,24 @@ +import { z } from "zod" + +export const UpdateBuildPartsSchema = z.object({ + + buildId: z.string().optional(), + addressGovCode: z.string(), + no: z.number(), + level: z.number(), + code: z.string(), + grossSize: z.number(), + netSize: z.number(), + defaultAccessory: z.string(), + humanLivability: z.boolean(), + key: z.string(), + directionId: z.string().optional(), + typeId: z.string().optional(), + active: z.boolean(), + isConfirmed: z.boolean(), + expiryStarts: z.string().optional(), + expiryEnds: z.string().optional() + +}); + +export type UpdateBuildParts = z.infer; diff --git a/frontend/app/api/builds/add/route.ts b/frontend/app/api/builds/add/route.ts new file mode 100644 index 0000000..5592189 --- /dev/null +++ b/frontend/app/api/builds/add/route.ts @@ -0,0 +1,51 @@ +'use server'; +import { NextResponse } from 'next/server'; +import { GraphQLClient, gql } from 'graphql-request'; +import { buildTypesAddSchema } from './schema'; + +const endpoint = "http://localhost:3001/graphql"; + +export async function POST(request: Request) { + const body = await request.json(); + const validatedBody = buildTypesAddSchema.parse(body); + try { + const client = new GraphQLClient(endpoint); + const query = gql` + mutation CreateBuild($input: CreateBuildInput!) { + createBuild(input: $input) { + _id + buildType { + token + typeToken + type + } + collectionToken + info { + govAddressCode + buildName + buildNo + maxFloor + undergroundFloor + buildDate + decisionPeriodDate + taxNo + liftCount + heatingSystem + coolingSystem + hotWaterSystem + blockServiceManCount + securityServiceManCount + garageCount + managementRoomId + } + } + } + `; + const variables = { input: validatedBody }; + const data = await client.request(query, variables); + return NextResponse.json({ data: data.createBuild, status: 200 }); + } catch (err: any) { + console.error(err); + return NextResponse.json({ error: err.message }, { status: 500 }); + } +} diff --git a/frontend/app/api/builds/add/schema.ts b/frontend/app/api/builds/add/schema.ts new file mode 100644 index 0000000..a3f0a75 --- /dev/null +++ b/frontend/app/api/builds/add/schema.ts @@ -0,0 +1,26 @@ +import { z } from "zod" + +export const buildTypesAddSchema = z.object({ + buildType: z.string(), + collectionToken: z.string(), + info: z.object({ + govAddressCode: z.string(), + buildName: z.string(), + buildNo: z.string(), + maxFloor: z.number(), + undergroundFloor: z.number(), + buildDate: z.string(), + decisionPeriodDate: z.string(), + taxNo: z.string(), + liftCount: z.number(), + heatingSystem: z.boolean(), + coolingSystem: z.boolean(), + hotWaterSystem: z.boolean(), + blockServiceManCount: z.number(), + securityServiceManCount: z.number(), + garageCount: z.number(), + managementRoomId: z.string(), + }) +}); + +export type BuildTypesAdd = z.infer; diff --git a/frontend/app/api/builds/delete/route.ts b/frontend/app/api/builds/delete/route.ts new file mode 100644 index 0000000..d3adad2 --- /dev/null +++ b/frontend/app/api/builds/delete/route.ts @@ -0,0 +1,23 @@ +'use server'; +import { NextResponse } from 'next/server'; +import { GraphQLClient, gql } from 'graphql-request'; + +const endpoint = "http://localhost:3001/graphql"; + +export async function GET(request: Request) { + + const searchParams = new URL(request.url).searchParams; + const uuid = searchParams.get('uuid'); + if (!uuid) { return NextResponse.json({ error: 'UUID not found in search params' }, { status: 400 }) } + try { + const client = new GraphQLClient(endpoint); + const query = gql`mutation DeleteBuild($uuid: String!) { deleteBuild(uuid: $uuid) }`; + const variables = { uuid: uuid }; + const data = await client.request(query, variables); + return NextResponse.json({ data: data.deletePerson, status: 200 }); + } catch (err: any) { + console.error(err); + return NextResponse.json({ error: err.message }, { status: 500 }); + } + +} diff --git a/frontend/app/api/builds/list/route.ts b/frontend/app/api/builds/list/route.ts new file mode 100644 index 0000000..490f8a1 --- /dev/null +++ b/frontend/app/api/builds/list/route.ts @@ -0,0 +1,53 @@ +'use server'; +import { NextResponse } from 'next/server'; +import { GraphQLClient, gql } from 'graphql-request'; + +const endpoint = "http://localhost:3001/graphql"; + +export async function POST(request: Request) { + const body = await request.json(); + const { limit, skip, sort, filters } = body; + try { + const client = new GraphQLClient(endpoint); + const query = gql` + query ListBuild($input: ListArguments!) { + builds(input: $input) { + data { + _id + collectionToken + buildType { + token + typeToken + type + } + info { + govAddressCode + buildName + buildNo + maxFloor + undergroundFloor + buildDate + decisionPeriodDate + taxNo + liftCount + heatingSystem + coolingSystem + hotWaterSystem + blockServiceManCount + securityServiceManCount + garageCount + managementRoomId + } + } + totalCount + } + } + `; + const variables = { input: { limit, skip, sort, filters } }; + const data = await client.request(query, variables); + return NextResponse.json({ data: data.builds.data, totalCount: data.builds.totalCount }); + } catch (err: any) { + console.error(err); + return NextResponse.json({ error: err.message }, { status: 500 }); + } +} diff --git a/frontend/app/api/builds/update/route.ts b/frontend/app/api/builds/update/route.ts new file mode 100644 index 0000000..44f0072 --- /dev/null +++ b/frontend/app/api/builds/update/route.ts @@ -0,0 +1,33 @@ +'use server'; +import { NextResponse } from 'next/server'; +import { GraphQLClient, gql } from 'graphql-request'; +import { personUpdateSchema } from './schema'; + +const endpoint = "http://localhost:3001/graphql"; + +export async function POST(request: Request) { + const searchUrl = new URL(request.url); + const uuid = searchUrl.searchParams.get("uuid") || ""; + const body = await request.json(); + const validatedBody = personUpdateSchema.parse(body); + if (uuid === "") { return NextResponse.json({ error: "UUID is required" }, { status: 400 }) } + try { + const client = new GraphQLClient(endpoint); + const query = gql` + mutation UpdateBuildType($uuid: String!, $input: UpdateBuildTypesInput!) { + updateBuildType(uuid: $uuid, input: $input) { + type + token + typeToken + description + } + } + `; + const variables = { uuid: uuid, input: validatedBody }; + const data = await client.request(query, variables); + return NextResponse.json({ data: data.updateBuildType, status: 200 }); + } catch (err: any) { + console.error(err); + return NextResponse.json({ error: err.message }, { status: 500 }); + } +} diff --git a/frontend/app/api/builds/update/schema.ts b/frontend/app/api/builds/update/schema.ts new file mode 100644 index 0000000..f0b4a37 --- /dev/null +++ b/frontend/app/api/builds/update/schema.ts @@ -0,0 +1,22 @@ +import { z } from "zod" + +export const personUpdateSchema = z.object({ + firstName: z.string().optional(), + surname: z.string().optional(), + middleName: z.string().optional(), + sexCode: z.string().optional(), + personRef: z.string().optional(), + personTag: z.string().optional(), + fatherName: z.string().optional(), + motherName: z.string().optional(), + countryCode: z.string().optional(), + nationalIdentityId: z.string().optional(), + birthPlace: z.string().optional(), + birthDate: z.string().optional(), + taxNo: z.string().optional().optional(), + birthname: z.string().optional().optional(), + expiryStarts: z.string().optional().optional(), + expiryEnds: z.string().optional().optional(), +}); + +export type PeopleUpdate = z.infer; diff --git a/frontend/app/build-areas/update/page.tsx b/frontend/app/build-areas/update/page.tsx index 8e4527b..1d27312 100644 --- a/frontend/app/build-areas/update/page.tsx +++ b/frontend/app/build-areas/update/page.tsx @@ -1,5 +1,5 @@ -import { PageUpdateBuildSites } from "@/pages/build-areas/update/page"; +import { PageUpdateBuildAreas } from "@/pages/build-areas/update/page"; -const BuildAreasUpdate = () => { return <>
} +const BuildAreasUpdate = () => { return <>
} export default BuildAreasUpdate; diff --git a/frontend/app/build-parts/add/page.tsx b/frontend/app/build-parts/add/page.tsx index 6ac407d..db1a3bb 100644 --- a/frontend/app/build-parts/add/page.tsx +++ b/frontend/app/build-parts/add/page.tsx @@ -1,5 +1,5 @@ -import { PageAddBuildIbans } from "@/pages/build-ibans/add/page"; +import { PageAddBuildParts } from "@/pages/build-parts/add/page"; -const BuildPartsAdd = () => { return <> } +const BuildPartsAddToBuild = () => { return <> } -export default BuildPartsAdd; +export default BuildPartsAddToBuild; diff --git a/frontend/app/build-parts/page.tsx b/frontend/app/build-parts/page.tsx index 2d16af3..d4b3461 100644 --- a/frontend/app/build-parts/page.tsx +++ b/frontend/app/build-parts/page.tsx @@ -1,5 +1,5 @@ -import { PageBuildIbans } from "@/pages/build-ibans/page"; +import PageBuildPartsToBuild from "@/pages/build-parts/page"; -const BuildParts = () => { return <> } +const BuildParts = () => { return <> } -export default BuildParts; \ No newline at end of file +export default BuildParts; diff --git a/frontend/app/build-parts/update/page.tsx b/frontend/app/build-parts/update/page.tsx index 3088984..dc51955 100644 --- a/frontend/app/build-parts/update/page.tsx +++ b/frontend/app/build-parts/update/page.tsx @@ -1,7 +1,5 @@ -import { PageUpdateBuildIbans } from "@/pages/build-ibans/update/page"; +import { PageUpdateBuildParts } from "@/pages/build-parts/update/page"; -const BuildPartsUpdate = () => { - return <> -} +const BuildPartsUpdate = () => { return <> } -export default BuildPartsUpdate; \ No newline at end of file +export default BuildPartsUpdate; diff --git a/frontend/app/builds/add/page.tsx b/frontend/app/builds/add/page.tsx index 1905e35..c76a982 100644 --- a/frontend/app/builds/add/page.tsx +++ b/frontend/app/builds/add/page.tsx @@ -1,5 +1,5 @@ -import { PageAddBuildTypes } from "@/pages/build-types/add/page"; +import { PageAddBuilds } from "@/pages/builds/add/page"; -const AddBuildPage = () => { return <> } +const AddBuildPage = () => { return <> } export default AddBuildPage; diff --git a/frontend/app/builds/update/page.tsx b/frontend/app/builds/update/page.tsx index 9288a51..b58580f 100644 --- a/frontend/app/builds/update/page.tsx +++ b/frontend/app/builds/update/page.tsx @@ -1,5 +1,5 @@ -import { PageUpdateBuildTypes } from '@/pages/build-types/update/page'; +import { PageUpdateBuild } from "@/pages/builds/update/page"; -const UpdateBuildPage = async () => { return } +const UpdateBuildPage = async () => { return } export default UpdateBuildPage; diff --git a/frontend/components/selections/a.txt b/frontend/components/selections/a.txt new file mode 100644 index 0000000..e69de29 diff --git a/frontend/components/selections/lists/personSelection.tsx b/frontend/components/selections/lists/personSelection.tsx new file mode 100644 index 0000000..1ccad89 --- /dev/null +++ b/frontend/components/selections/lists/personSelection.tsx @@ -0,0 +1,56 @@ +'use client'; +import { useState } from 'react'; + +interface Person { + id: number; + firstName: string; + middleName: string; + surname: string; + avatar: string; +} + +const PersonSelections = () => { + + const [selectedId, setSelectedId] = useState(null); + + const people: Person[] = [ + { id: 1, firstName: 'John', middleName: 'Michael', surname: 'Doe', avatar: 'https://placehold.co/100x100/4F46E5/FFFFFF?text=JD' }, + { id: 2, firstName: 'Jane', middleName: 'Elizabeth', surname: 'Smith', avatar: 'https://placehold.co/100x100/EC4899/FFFFFF?text=JS' }, + { id: 3, firstName: 'Robert', middleName: 'James', surname: 'Johnson', avatar: 'https://placehold.co/100x100/10B981/FFFFFF?text=RJ' }, + { id: 4, firstName: 'Emily', middleName: 'Rose', surname: 'Williams', avatar: 'https://placehold.co/100x100/F59E0B/FFFFFF?text=EW' }, + { id: 5, firstName: 'Michael', middleName: 'Thomas', surname: 'Brown', avatar: 'https://placehold.co/100x100/EF4444/FFFFFF?text=MB' }, + { id: 6, firstName: 'Sarah', middleName: 'Ann', surname: 'Davis', avatar: 'https://placehold.co/100x100/8B5CF6/FFFFFF?text=SD' }, + { id: 7, firstName: 'David', middleName: 'Charles', surname: 'Miller', avatar: 'https://placehold.co/100x100/06B6D4/FFFFFF?text=DM' }, + { id: 8, firstName: 'Lisa', middleName: 'Marie', surname: 'Wilson', avatar: 'https://placehold.co/100x100/84CC16/FFFFFF?text=LW' }, + { id: 9, firstName: 'James', middleName: 'Patrick', surname: 'Moore', avatar: 'https://placehold.co/100x100/F97316/FFFFFF?text=JM' }, + { id: 10, firstName: 'Jennifer', middleName: 'Lynn', surname: 'Taylor', avatar: 'https://placehold.co/100x100/6366F1/FFFFFF?text=JT' }, + ]; + + const handleSelect = (id: number) => { console.log('Selected row ID:', id); setSelectedId(id) }; + + return ( +
+
+ {people.map((person) => ( +
+
+ {`${person.firstName} +
+ +
+
{person.firstName}
+
{person.middleName}
+
{person.surname}
+
+ +
+ +
+
+ ))} +
+
+ ); +}; + +export default PersonSelections; diff --git a/frontend/pages/build-areas/add/form.tsx b/frontend/pages/build-areas/add/form.tsx index 910af65..807b04e 100644 --- a/frontend/pages/build-areas/add/form.tsx +++ b/frontend/pages/build-areas/add/form.tsx @@ -9,21 +9,12 @@ import { DateTimePicker } from "@/components/ui/date-time-picker" import { BuildAreasAdd, buildAreasAddSchema } from "./schema" import { useAddBuildAreasMutation } from "./queries" -const BuildAreasForm = ({ refetchTable }: { refetchTable: () => void }) => { +const BuildAreasForm = ({ refetchTable, buildId }: { refetchTable: () => void, buildId: string }) => { const form = useForm({ resolver: zodResolver(buildAreasAddSchema), defaultValues: { - areaName: "", - areaCode: "", - areaType: "", - areaDirection: "", - areaGrossSize: 0, - areaNetSize: 0, - width: 0, - size: 0, - expiryStarts: "", - expiryEnds: "", + areaName: "", areaCode: "", areaType: "", areaDirection: "", areaGrossSize: 0, areaNetSize: 0, width: 0, size: 0, expiryStarts: "", expiryEnds: "", }, }); @@ -31,17 +22,13 @@ const BuildAreasForm = ({ refetchTable }: { refetchTable: () => void }) => { const mutation = useAddBuildAreasMutation(); - function onSubmit(values: BuildAreasAdd) { mutation.mutate({ data: values }); setTimeout(() => refetchTable(), 400) }; + function onSubmit(values: BuildAreasAdd) { mutation.mutate({ data: values, buildId }); setTimeout(() => refetchTable(), 400) }; return (
- + {/* ROW 1 */}
- void }) => { Area Gross Size - + field.onChange(e.target.value === "" ? undefined : Number(e.target.value))} /> @@ -124,7 +111,7 @@ const BuildAreasForm = ({ refetchTable }: { refetchTable: () => void }) => { Area Net Size - + field.onChange(e.target.value === "" ? undefined : Number(e.target.value))} /> @@ -140,7 +127,7 @@ const BuildAreasForm = ({ refetchTable }: { refetchTable: () => void }) => { Width - + field.onChange(e.target.value === "" ? undefined : Number(e.target.value))} /> @@ -154,7 +141,7 @@ const BuildAreasForm = ({ refetchTable }: { refetchTable: () => void }) => { Size - + field.onChange(e.target.value === "" ? undefined : Number(e.target.value))} /> diff --git a/frontend/pages/build-areas/add/page.tsx b/frontend/pages/build-areas/add/page.tsx index 64b8f91..7adb06c 100644 --- a/frontend/pages/build-areas/add/page.tsx +++ b/frontend/pages/build-areas/add/page.tsx @@ -3,6 +3,8 @@ import { useState } from 'react'; import { BuildAreasDataTableAdd } from './table/data-table'; import { useGraphQlBuildAreasList } from '@/pages/build-areas/queries'; import { BuildAreasForm } from './form'; +import { Button } from '@/components/ui/button'; +import { useSearchParams, useRouter } from 'next/navigation'; const PageAddBuildAreas = () => { const [page, setPage] = useState(1); @@ -10,14 +12,19 @@ const PageAddBuildAreas = () => { const [sort, setSort] = useState({ createdAt: 'desc' }); const [filters, setFilters] = useState({}); - const { data, isLoading, error, refetch } = useGraphQlBuildAreasList({ limit, skip: (page - 1) * limit, sort, filters }); + const searchParams = useSearchParams(); + const router = useRouter(); + const buildId = searchParams?.get('build'); + const { data, isLoading, error, refetch } = useGraphQlBuildAreasList({ limit, skip: (page - 1) * limit, sort, filters: { ...filters, buildId: buildId } }); + const noUUIDFound = <>
Back To Builds. No uuid is found on headers
+ if (!buildId) { return noUUIDFound } return ( <> - + ) } diff --git a/frontend/pages/build-areas/add/queries.tsx b/frontend/pages/build-areas/add/queries.tsx index f06058c..0eeedfe 100644 --- a/frontend/pages/build-areas/add/queries.tsx +++ b/frontend/pages/build-areas/add/queries.tsx @@ -3,13 +3,12 @@ import { useMutation } from '@tanstack/react-query' import { toISOIfNotZ } from '@/lib/utils'; import { BuildAreasAdd } from './schema'; -const fetchGraphQlBuildSitesAdd = async (record: BuildAreasAdd): Promise<{ data: BuildAreasAdd | null; status: number }> => { +const fetchGraphQlBuildSitesAdd = async (record: BuildAreasAdd, buildId: string): Promise<{ data: BuildAreasAdd | null; status: number }> => { console.log('Fetching test data from local API'); record.expiryStarts = record.expiryStarts ? toISOIfNotZ(record.expiryStarts) : undefined; record.expiryEnds = record.expiryEnds ? toISOIfNotZ(record.expiryEnds) : undefined; - console.dir({ record }) try { - const res = await fetch('/api/build-areas/add', { method: 'POST', cache: 'no-store', credentials: "include", body: JSON.stringify(record) }); + const res = await fetch('/api/build-areas/add', { method: 'POST', cache: 'no-store', credentials: "include", body: JSON.stringify({ ...record, buildId }) }); if (!res.ok) { const errorText = await res.text(); console.error('Test data API error:', errorText); throw new Error(`API error: ${res.status} ${res.statusText}`) } const data = await res.json(); return { data: data.data, status: res.status } @@ -18,7 +17,7 @@ const fetchGraphQlBuildSitesAdd = async (record: BuildAreasAdd): Promise<{ data: export function useAddBuildAreasMutation() { return useMutation({ - mutationFn: ({ data }: { data: BuildAreasAdd }) => fetchGraphQlBuildSitesAdd(data), + mutationFn: ({ data, buildId }: { data: BuildAreasAdd, buildId: string }) => fetchGraphQlBuildSitesAdd(data, buildId), onSuccess: () => { console.log("Build areas created successfully") }, onError: (error) => { console.error("Add build areas failed:", error) }, }) diff --git a/frontend/pages/build-areas/add/table/columns.tsx b/frontend/pages/build-areas/add/table/columns.tsx index 70741b8..e04831c 100644 --- a/frontend/pages/build-areas/add/table/columns.tsx +++ b/frontend/pages/build-areas/add/table/columns.tsx @@ -105,9 +105,6 @@ function getColumns(router: any, deleteHandler: (id: string) => void): ColumnDef cell: ({ row }) => { return (
- diff --git a/frontend/pages/build-areas/add/table/data-table.tsx b/frontend/pages/build-areas/add/table/data-table.tsx index 58d1f0b..a37e5e2 100644 --- a/frontend/pages/build-areas/add/table/data-table.tsx +++ b/frontend/pages/build-areas/add/table/data-table.tsx @@ -55,6 +55,7 @@ export function BuildAreasDataTableAdd({ onPageChange, onPageSizeChange, refetchTable, + buildId }: { data: schemaType[], totalCount: number, @@ -62,7 +63,8 @@ export function BuildAreasDataTableAdd({ pageSize: number, onPageChange: (page: number) => void, onPageSizeChange: (size: number) => void, - refetchTable: () => void + refetchTable: () => void, + buildId: string }) { const router = useRouter(); @@ -142,7 +144,7 @@ export function BuildAreasDataTableAdd({ })} - diff --git a/frontend/pages/build-areas/list/columns.tsx b/frontend/pages/build-areas/list/columns.tsx index c3e06bd..82cc1f5 100644 --- a/frontend/pages/build-areas/list/columns.tsx +++ b/frontend/pages/build-areas/list/columns.tsx @@ -23,7 +23,7 @@ export function DraggableRow({ row }: { row: Row> }) { ) } -function getColumns(router: any, deleteHandler: (id: string) => void): ColumnDef[] { +function getColumns(router: any, deleteHandler: (id: string) => void, buildID: string): ColumnDef[] { return [ { accessorKey: "uuid", @@ -88,7 +88,7 @@ function getColumns(router: any, deleteHandler: (id: string) => void): ColumnDef cell: ({ row }) => { return (
- diff --git a/frontend/pages/build-areas/page.tsx b/frontend/pages/build-areas/page.tsx index cda826a..f34f21a 100644 --- a/frontend/pages/build-areas/page.tsx +++ b/frontend/pages/build-areas/page.tsx @@ -1,7 +1,9 @@ 'use client'; +import { Button } from '@/components/ui/button'; import { BuildAreasDataTable } from './list/data-table'; import { useGraphQlBuildAreasList } from './queries'; import { useState } from 'react'; +import { useSearchParams, useRouter } from 'next/navigation'; const PageBuildAreas = () => { @@ -9,16 +11,17 @@ const PageBuildAreas = () => { const [limit, setLimit] = useState(10); const [sort, setSort] = useState({ createdAt: 'desc' }); const [filters, setFilters] = useState({}); - - const { data, isLoading, error, refetch } = useGraphQlBuildAreasList({ limit, skip: (page - 1) * limit, sort, filters }); - + const searchParams = useSearchParams(); + const router = useRouter(); + const buildId = searchParams?.get('build'); + const noUUIDFound = <>
Back To Builds. No uuid is found on headers
+ if (!buildId) { return noUUIDFound } + const { data, isLoading, error, refetch } = useGraphQlBuildAreasList({ limit, skip: (page - 1) * limit, sort, filters: { ...filters, buildId: buildId } }); const handlePageChange = (newPage: number) => { setPage(newPage) }; const handlePageSizeChange = (newSize: number) => { setLimit(newSize); setPage(1) }; - if (isLoading) { return
Loading...
} if (error) { return
Error loading build areas
} - - return ; + return ; }; diff --git a/frontend/pages/build-areas/update/form.tsx b/frontend/pages/build-areas/update/form.tsx index 23f83f5..130e34a 100644 --- a/frontend/pages/build-areas/update/form.tsx +++ b/frontend/pages/build-areas/update/form.tsx @@ -9,7 +9,7 @@ import { DateTimePicker } from "@/components/ui/date-time-picker" import { useUpdateBuildSitesMutation } from "@/pages/build-sites/update/queries" import { BuildAreasUpdate, buildAreasUpdateSchema } from "@/pages/build-areas/update/schema" -const BuildAreasForm = ({ refetchTable, initData, selectedUuid }: { refetchTable: () => void, initData: BuildAreasUpdate, selectedUuid: string }) => { +const BuildAreasForm = ({ refetchTable, initData, selectedUuid, buildId }: { refetchTable: () => void, initData: BuildAreasUpdate, selectedUuid: string, buildId: string }) => { const form = useForm({ resolver: zodResolver(buildAreasUpdateSchema), defaultValues: { ...initData } }) @@ -17,7 +17,7 @@ const BuildAreasForm = ({ refetchTable, initData, selectedUuid }: { refetchTable const mutation = useUpdateBuildSitesMutation(); - function onSubmit(values: BuildAreasUpdate) { mutation.mutate({ data: values as any || initData, uuid: selectedUuid }); setTimeout(() => refetchTable(), 400) } + function onSubmit(values: BuildAreasUpdate) { mutation.mutate({ data: values as any || initData, uuid: selectedUuid, buildId }); setTimeout(() => refetchTable(), 400) } return ( diff --git a/frontend/pages/build-areas/update/page.tsx b/frontend/pages/build-areas/update/page.tsx index 9979f57..22ff868 100644 --- a/frontend/pages/build-areas/update/page.tsx +++ b/frontend/pages/build-areas/update/page.tsx @@ -6,7 +6,7 @@ import { Button } from '@/components/ui/button'; import { BuildAreasDataTableUpdate } from './table/data-table'; import { useGraphQlBuildAreasList } from '../queries'; -const PageUpdateBuildSites = () => { +const PageUpdateBuildAreas = () => { const [page, setPage] = useState(1); const [limit, setLimit] = useState(10); const [sort, setSort] = useState({ createdAt: 'desc' }); @@ -14,23 +14,21 @@ const PageUpdateBuildSites = () => { const searchParams = useSearchParams() const router = useRouter() const uuid = searchParams?.get('uuid') || null - const backToBuildAddress = <> -
UUID not found in search params
- - - if (!uuid) { return backToBuildAddress } - const { data, isLoading, error, refetch } = useGraphQlBuildAreasList({ limit, skip: (page - 1) * limit, sort, filters: { ...filters, uuid } }); + const backToBuildAddress = <>
UUID not found in search params
+ const buildId = searchParams?.get('build'); + const noUUIDFound = <>
Back To Builds. No uuid is found on headers
+ if (!buildId) { return noUUIDFound }; if (!uuid) { return backToBuildAddress } + const { data, isLoading, error, refetch } = useGraphQlBuildAreasList({ limit, skip: (page - 1) * limit, sort, filters: { ...filters, buildId, uuid } }); const initData = data?.data?.[0] || null; if (!initData) { return backToBuildAddress } return ( <> - + ) } -export { PageUpdateBuildSites }; +export { PageUpdateBuildAreas }; diff --git a/frontend/pages/build-areas/update/table/columns.tsx b/frontend/pages/build-areas/update/table/columns.tsx index 53827cc..073a0ee 100644 --- a/frontend/pages/build-areas/update/table/columns.tsx +++ b/frontend/pages/build-areas/update/table/columns.tsx @@ -30,7 +30,7 @@ export function DraggableRow({ row }: { row: Row> }) { ) } -function getColumns(router: any, deleteHandler: (id: string) => void): ColumnDef[] { +function getColumns(deleteHandler: (id: string) => void): ColumnDef[] { return [ { accessorKey: "uuid", diff --git a/frontend/pages/build-areas/update/table/data-table.tsx b/frontend/pages/build-areas/update/table/data-table.tsx index bdd02e2..76f3fa5 100644 --- a/frontend/pages/build-areas/update/table/data-table.tsx +++ b/frontend/pages/build-areas/update/table/data-table.tsx @@ -80,6 +80,7 @@ export function BuildAreasDataTableUpdate({ onPageChange, onPageSizeChange, refetchTable, + buildID }: { data: schemaType[], totalCount: number, @@ -87,7 +88,8 @@ export function BuildAreasDataTableUpdate({ pageSize: number, onPageChange: (page: number) => void, onPageSizeChange: (size: number) => void, - refetchTable: () => void + refetchTable: () => void, + buildID: string }) { const router = useRouter(); @@ -101,7 +103,7 @@ export function BuildAreasDataTableUpdate({ const deleteMutation = useDeletePersonMutation() const deleteHandler = (id: string) => { deleteMutation.mutate({ uuid: id }); setTimeout(() => { refetchTable() }, 200) } - const columns = getColumns(router, deleteHandler); + const columns = getColumns(deleteHandler); const pagination = React.useMemo(() => ({ pageIndex: currentPage - 1, pageSize: pageSize, }), [currentPage, pageSize]) const totalPages = Math.ceil(totalCount / pageSize) @@ -167,7 +169,7 @@ export function BuildAreasDataTableUpdate({ })} - diff --git a/frontend/pages/build-ibans/update/table/data-table.tsx b/frontend/pages/build-ibans/update/table/data-table.tsx index 062bd99..3de63f8 100644 --- a/frontend/pages/build-ibans/update/table/data-table.tsx +++ b/frontend/pages/build-ibans/update/table/data-table.tsx @@ -169,7 +169,7 @@ export function BuildIbansDataTableUpdate({
diff --git a/frontend/pages/build-ibans/update/types.ts b/frontend/pages/build-ibans/update/types.ts index 1a88475..beec700 100644 --- a/frontend/pages/build-ibans/update/types.ts +++ b/frontend/pages/build-ibans/update/types.ts @@ -1,6 +1,5 @@ interface UpdateBuildIbansUpdate { - iban: string; startDate: string; stopDate: string; diff --git a/frontend/pages/build-parts/add/form.tsx b/frontend/pages/build-parts/add/form.tsx new file mode 100644 index 0000000..4162e59 --- /dev/null +++ b/frontend/pages/build-parts/add/form.tsx @@ -0,0 +1,306 @@ +"use client" +import { useForm } from "react-hook-form" +import { zodResolver } from "@hookform/resolvers/zod" +import { Form, FormField, FormItem, FormLabel, FormControl, FormMessage } from "@/components/ui/form" +import { Input } from "@/components/ui/input" +import { Button } from "@/components/ui/button" +import { Separator } from "@/components/ui/separator" +import { DateTimePicker } from "@/components/ui/date-time-picker" +import { BuildPartsAdd, buildPartsAddSchema } from "./schema" +import { useAddBuildAreasMutation } from "./queries" +import { Checkbox } from "@/components/ui/checkbox" + +const BuildPartsForm = ({ refetchTable, selectedBuildId }: { refetchTable: () => void, selectedBuildId: string }) => { + + const form = useForm({ + resolver: zodResolver(buildPartsAddSchema), + defaultValues: { + addressGovCode: "", no: 0, level: 0, code: "", grossSize: 0, netSize: 0, defaultAccessory: "", humanLivability: false, key: "", + directionId: "", typeId: "", active: false, isConfirmed: false, expiryStarts: "", expiryEnds: "", + }, + }); + + const { handleSubmit } = form; + + const mutation = useAddBuildAreasMutation(); + + function onSubmit(values: BuildPartsAdd) { mutation.mutate({ data: values, buildId: selectedBuildId }); setTimeout(() => refetchTable(), 400) }; + + return ( + + + {/* ROW 1 */} +
+ + ( + + Address Gov Code + + + + + + )} + /> + ( + + No + + field.onChange(e.target.value === "" ? undefined : Number(e.target.value))} + /> + + + + )} + /> +
+ + {/* ROW 2 */} +
+ ( + + Level + + field.onChange(e.target.value === "" ? undefined : Number(e.target.value))} + /> + + + + )} + /> + ( + + Code + + + + + + )} + /> +
+ + {/* ROW 3 */} +
+ ( + + Key + + + + + + )} + /> + ( + + Gross Size + + field.onChange(e.target.value === "" ? undefined : Number(e.target.value))} + /> + + + + )} + /> +
+ + {/* ROW 4 */} +
+ ( + + Net Size + + field.onChange(e.target.value === "" ? undefined : Number(e.target.value))} + /> + + + + )} + /> + ( + + Default Accessory + + + + + + )} + /> + +
+ + {/* ROW 5 */} +
+ ( + + Direction Id + + + + + + )} + /> + ( + + Type Id + + + + + + )} + /> + +
+ + {/* BOOLEAN ROW */} +
+ + {/* boşluk kalmasın diye aktif alanını buraya koydum */} + ( + + + Active + + + + + + + )} + /> + ( + + + Human Livability + + + + + + + )} + /> + + ( + + + Is Confirmed + + + + + + + )} + /> +
+ + + {/* EXPIRY DATES */} +
+ ( + + Expiry Starts + + + + + + )} + /> + + ( + + Expiry Ends + + + + + + )} + /> +
+ + + + ); +}; + +export { BuildPartsForm } diff --git a/frontend/pages/build-parts/add/page.tsx b/frontend/pages/build-parts/add/page.tsx new file mode 100644 index 0000000..1d9ce8f --- /dev/null +++ b/frontend/pages/build-parts/add/page.tsx @@ -0,0 +1,41 @@ +'use client'; +import { useState } from 'react'; +import { BuildPartsDataTableAdd } from './table/data-table'; +import { BuildPartsForm } from './form'; +import { useGraphQlBuildPartsList } from '@/pages/build-parts/queries'; +import { useRouter, useSearchParams } from 'next/navigation'; +import { Button } from '@/components/ui/button'; + +const PageAddBuildParts = () => { + const [page, setPage] = useState(1); + const [limit, setLimit] = useState(10); + const [sort, setSort] = useState({ createdAt: 'desc' }); + const [filters, setFilters] = useState({}); + + const searchParams = useSearchParams(); + const router = useRouter(); + const buildId = searchParams?.get('build'); + + const { data, isLoading, error, refetch } = useGraphQlBuildPartsList({ limit: 10, skip: 0, sort: { createdAt: -1 }, filters: { ...filters, buildId } }); + + const handlePageChange = (newPage: number) => { setPage(newPage) }; + const handlePageSizeChange = (newSize: number) => { setLimit(newSize); setPage(1) }; + + const noUUIDFound = <> +
Back To Builds. No uuid is found on headers
+ + + + if (!buildId) { return noUUIDFound } + + return ( + <> + + + + ) +} + +export { PageAddBuildParts }; diff --git a/frontend/pages/build-parts/add/queries.tsx b/frontend/pages/build-parts/add/queries.tsx new file mode 100644 index 0000000..240cdb6 --- /dev/null +++ b/frontend/pages/build-parts/add/queries.tsx @@ -0,0 +1,24 @@ +'use client' +import { useMutation } from '@tanstack/react-query' +import { toISOIfNotZ } from '@/lib/utils'; +import { BuildPartsAdd } from './schema'; + +const fetchGraphQlBuildSitesAdd = async (record: BuildPartsAdd, buildId: string): Promise<{ data: BuildPartsAdd | null; status: number }> => { + console.log('Fetching test data from local API'); + record.expiryStarts = record.expiryStarts ? toISOIfNotZ(record.expiryStarts) : undefined; + record.expiryEnds = record.expiryEnds ? toISOIfNotZ(record.expiryEnds) : undefined; + try { + const res = await fetch('/api/builds-parts/add', { method: 'POST', cache: 'no-store', credentials: "include", body: JSON.stringify({ data: record, buildId }) }); + if (!res.ok) { const errorText = await res.text(); console.error('Test data API error:', errorText); throw new Error(`API error: ${res.status} ${res.statusText}`) } + const data = await res.json(); + return { data: data.data, status: res.status } + } catch (error) { console.error('Error fetching test data:', error); throw error } +}; + +export function useAddBuildAreasMutation() { + return useMutation({ + mutationFn: ({ data, buildId }: { data: BuildPartsAdd, buildId: string }) => fetchGraphQlBuildSitesAdd(data, buildId), + onSuccess: () => { console.log("Build areas created successfully") }, + onError: (error) => { console.error("Add build areas failed:", error) }, + }) +} diff --git a/frontend/pages/build-parts/add/schema.ts b/frontend/pages/build-parts/add/schema.ts new file mode 100644 index 0000000..426510b --- /dev/null +++ b/frontend/pages/build-parts/add/schema.ts @@ -0,0 +1,23 @@ +import { z } from "zod" + +export const buildPartsAddSchema = z.object({ + + addressGovCode: z.string(), + no: z.number(), + level: z.number(), + code: z.string(), + grossSize: z.number(), + netSize: z.number(), + defaultAccessory: z.string(), + humanLivability: z.boolean(), + key: z.string(), + directionId: z.string().optional(), + typeId: z.string().optional(), + active: z.boolean(), + isConfirmed: z.boolean(), + expiryStarts: z.string().optional(), + expiryEnds: z.string().optional() + +}); + +export type BuildPartsAdd = z.infer; diff --git a/frontend/pages/build-parts/add/table/columns.tsx b/frontend/pages/build-parts/add/table/columns.tsx new file mode 100644 index 0000000..bffb2f4 --- /dev/null +++ b/frontend/pages/build-parts/add/table/columns.tsx @@ -0,0 +1,115 @@ +"use client" +import { z } from "zod" +import { Button } from "@/components/ui/button" +import { Drawer, DrawerClose, DrawerContent, DrawerFooter, DrawerHeader, DrawerTrigger } from "@/components/ui/drawer" +import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger } from "@/components/ui/dropdown-menu" +import { Input } from "@/components/ui/input" +import { Label } from "@/components/ui/label" +import { useSortable } from "@dnd-kit/sortable" +import { IconGripVertical } from "@tabler/icons-react" +import { useIsMobile } from "@/hooks/use-mobile" +import { Separator } from "@/components/ui/separator" +import { ColumnDef, flexRender, Row } from "@tanstack/react-table" +import { TableCell, TableRow } from "@/components/ui/table" +import { CSS } from "@dnd-kit/utilities" +import { schema, schemaType } from "./schema" +import { dateToLocaleString } from "@/lib/utils" +import { Pencil, Trash } from "lucide-react" + +export function DraggableRow({ row }: { row: Row> }) { + const { transform, transition, setNodeRef, isDragging } = useSortable({ id: row.original._id }) + return ( + + {row.getVisibleCells().map((cell) => ( + {flexRender(cell.column.columnDef.cell, cell.getContext())} + ))} + + ) +} + +function getColumns(router: any, deleteHandler: (id: string) => void): ColumnDef[] { + return [ + { + accessorKey: "addressGovCode", + header: "Address Gov Code", + }, + { + accessorKey: "no", + header: "No", + }, + { + accessorKey: "level", + header: "Level", + }, + { + accessorKey: "code", + header: "Code", + }, + { + accessorKey: "grossSize", + header: "Gross Size", + }, + { + accessorKey: "netSize", + header: "Net Size", + }, + { + accessorKey: "defaultAccessory", + header: "Default Accessory", + }, + { + accessorKey: "humanLivability", + header: "Human Livability", + }, + { + accessorKey: "key", + header: "Key", + }, + { + accessorKey: "directionId", + header: "Direction ID", + }, + { + accessorKey: "typeId", + header: "Type ID", + }, + { + accessorKey: "createdAt", + header: "Created", + cell: ({ getValue }) => dateToLocaleString(getValue() as string), + }, + { + accessorKey: "updatedAt", + header: "Updated", + cell: ({ getValue }) => dateToLocaleString(getValue() as string), + }, + { + accessorKey: "expiryStarts", + header: "Expiry Starts", + cell: ({ getValue }) => getValue() ? dateToLocaleString(getValue() as string) : "-", + }, + { + accessorKey: "expiryEnds", + header: "Expiry Ends", + cell: ({ getValue }) => getValue() ? dateToLocaleString(getValue() as string) : "-", + }, + { + id: "actions", + header: "Actions", + cell: ({ row }) => { + return ( +
+ +
+ ); + }, + } + ] +} + +export { getColumns }; \ No newline at end of file diff --git a/frontend/pages/build-parts/add/table/data-table.tsx b/frontend/pages/build-parts/add/table/data-table.tsx new file mode 100644 index 0000000..63d6032 --- /dev/null +++ b/frontend/pages/build-parts/add/table/data-table.tsx @@ -0,0 +1,256 @@ +"use client" +import * as React from "react" +import { + closestCenter, + DndContext, + KeyboardSensor, + MouseSensor, + TouchSensor, + useSensor, + useSensors, + type UniqueIdentifier, +} from "@dnd-kit/core" +import { restrictToVerticalAxis } from "@dnd-kit/modifiers" +import { + SortableContext, + verticalListSortingStrategy, +} from "@dnd-kit/sortable" +import { + IconChevronDown, + IconChevronLeft, + IconChevronRight, + IconChevronsLeft, + IconChevronsRight, + IconLayoutColumns, +} from "@tabler/icons-react" +import { + ColumnFiltersState, + flexRender, + getCoreRowModel, + getFacetedRowModel, + getFacetedUniqueValues, + getFilteredRowModel, + getSortedRowModel, + SortingState, + useReactTable, + VisibilityState, +} from "@tanstack/react-table" +import { Button } from "@/components/ui/button" +import { DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuTrigger } from "@/components/ui/dropdown-menu" +import { Label } from "@/components/ui/label" +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" +import { Tabs, TabsContent } from "@/components/ui/tabs" +import { schemaType } from "./schema" +import { getColumns, DraggableRow } from "./columns" +import { useRouter } from "next/navigation" +import { Home } from "lucide-react" +import { useDeleteBuildPartMutation } from "@/pages/build-parts/queries" + +export function BuildPartsDataTableAdd({ + data, + totalCount, + currentPage, + pageSize, + onPageChange, + onPageSizeChange, + refetchTable, + buildId +}: { + data: schemaType[], + totalCount: number, + currentPage: number, + pageSize: number, + onPageChange: (page: number) => void, + onPageSizeChange: (size: number) => void, + refetchTable: () => void, + buildId: string +}) { + + const router = useRouter(); + const [rowSelection, setRowSelection] = React.useState({}) + const [columnVisibility, setColumnVisibility] = React.useState({}) + const [columnFilters, setColumnFilters] = React.useState([]) + const [sorting, setSorting] = React.useState([]) + const sortableId = React.useId() + const sensors = useSensors(useSensor(MouseSensor, {}), useSensor(TouchSensor, {}), useSensor(KeyboardSensor, {})) + const dataIds = React.useMemo(() => data?.map(({ _id }) => _id) || [], [data]) + + const deleteMutation = useDeleteBuildPartMutation() + const deleteHandler = (id: string) => { deleteMutation.mutate({ uuid: id }); setTimeout(() => { refetchTable() }, 400) } + const columns = getColumns(router, deleteHandler); + const pagination = React.useMemo(() => ({ pageIndex: currentPage - 1, pageSize: pageSize, }), [currentPage, pageSize]) + const totalPages = Math.ceil(totalCount / pageSize) + + const table = useReactTable({ + data, + columns, + pageCount: totalPages, + state: { sorting, columnVisibility, rowSelection, columnFilters, pagination }, + manualPagination: true, + enableRowSelection: true, + getRowId: (row) => row._id.toString(), + onRowSelectionChange: setRowSelection, + onSortingChange: setSorting, + onColumnFiltersChange: setColumnFilters, + onColumnVisibilityChange: setColumnVisibility, + onPaginationChange: (updater) => { + const nextPagination = typeof updater === "function" ? updater(pagination) : updater; + onPageChange(nextPagination.pageIndex + 1); onPageSizeChange(nextPagination.pageSize); + }, + getCoreRowModel: getCoreRowModel(), + getFilteredRowModel: getFilteredRowModel(), + getSortedRowModel: getSortedRowModel(), + getFacetedRowModel: getFacetedRowModel(), + getFacetedUniqueValues: getFacetedUniqueValues(), + }) + + const handlePageSizeChange = (value: string) => { const newSize = Number(value); onPageSizeChange(newSize); onPageChange(1) } + + return ( + +
+ + +
+ + + + + + {table.getAllColumns().filter((column) => typeof column.accessorFn !== "undefined" && column.getCanHide()).map((column) => { + return ( + column.toggleVisibility(!!value)} > + {column.id} + + ) + })} + + + +
+
+ +
+ + + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + return ( + + {header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())} + + ) + })} + + ))} + + + {table.getRowModel().rows?.length ? ( + {table.getRowModel().rows.map((row) => )} + ) : ( + No results. + )} + +
+
+
+
+
+ {table.getFilteredSelectedRowModel().rows.length} of{" "} + {table.getFilteredRowModel().rows.length} row(s) selected. +
+
+
+ + +
+
+ Page {currentPage} of {totalPages} +
+
+ Total Count: {totalCount} +
+
+ + + + + + + +
+
+
+
+ +
+
+ +
+
+ +
+
+
+ ) +} \ No newline at end of file diff --git a/frontend/pages/build-parts/add/table/schema.tsx b/frontend/pages/build-parts/add/table/schema.tsx new file mode 100644 index 0000000..aeeba05 --- /dev/null +++ b/frontend/pages/build-parts/add/table/schema.tsx @@ -0,0 +1,28 @@ +import { z } from "zod"; + +export const schema = z.object({ + + _id: z.string(), + uuid: z.string(), + buildId: z.string(), + addressGovCode: z.string(), + no: z.number(), + level: z.number(), + code: z.string(), + grossSize: z.number(), + netSize: z.number(), + defaultAccessory: z.string(), + humanLivability: z.boolean(), + key: z.string(), + directionId: z.string().nullable().optional(), + typeId: z.string().nullable().optional(), + createdAt: z.string(), + updatedAt: z.string(), + expiryStarts: z.string(), + expiryEnds: z.string(), + active: z.boolean(), + isConfirmed: z.boolean(), + +}); + +export type schemaType = z.infer; diff --git a/frontend/pages/build-parts/add/types.ts b/frontend/pages/build-parts/add/types.ts new file mode 100644 index 0000000..e4508a4 --- /dev/null +++ b/frontend/pages/build-parts/add/types.ts @@ -0,0 +1,24 @@ + +interface PeopleAdd { + + firstName: string; + surname: string; + middleName?: string; + sexCode: string; + personRef?: string; + personTag?: string; + fatherName?: string; + motherName?: string; + countryCode: string; + nationalIdentityId: string; + birthPlace: string; + birthDate: string; + taxNo?: string; + birthname?: string; + expiryStarts?: string; + expiryEnds?: string; + +} + + +export type { PeopleAdd }; \ No newline at end of file diff --git a/frontend/pages/build-parts/list/columns.tsx b/frontend/pages/build-parts/list/columns.tsx new file mode 100644 index 0000000..1538cb1 --- /dev/null +++ b/frontend/pages/build-parts/list/columns.tsx @@ -0,0 +1,111 @@ +"use client" +import { z } from "zod" +import { Button } from "@/components/ui/button" +import { useSortable } from "@dnd-kit/sortable" +import { ColumnDef, flexRender, Row } from "@tanstack/react-table" +import { TableCell, TableRow } from "@/components/ui/table" +import { CSS } from "@dnd-kit/utilities" +import { schema, schemaType } from "./schema" +import { dateToLocaleString } from "@/lib/utils" +import { Pencil, Trash } from "lucide-react" + +export function DraggableRow({ row }: { row: Row> }) { + const { transform, transition, setNodeRef, isDragging } = useSortable({ id: row.original._id }) + return ( + + {row.getVisibleCells().map((cell) => ( + {flexRender(cell.column.columnDef.cell, cell.getContext())} + ))} + + ) +} + +function getColumns(router: any, deleteHandler: (id: string) => void, buildId: string): ColumnDef[] { + return [ + { + accessorKey: "addressGovCode", + header: "Address Gov Code", + }, + { + accessorKey: "no", + header: "No", + }, + { + accessorKey: "level", + header: "Level", + }, + { + accessorKey: "code", + header: "Code", + }, + { + accessorKey: "grossSize", + header: "Gross Size", + }, + { + accessorKey: "netSize", + header: "Net Size", + }, + { + accessorKey: "defaultAccessory", + header: "Default Accessory", + }, + { + accessorKey: "humanLivability", + header: "Human Livability", + }, + { + accessorKey: "key", + header: "Key", + }, + { + accessorKey: "directionId", + header: "Direction ID", + }, + { + accessorKey: "typeId", + header: "Type ID", + }, + { + accessorKey: "createdAt", + header: "Created", + cell: ({ getValue }) => dateToLocaleString(getValue() as string), + }, + { + accessorKey: "updatedAt", + header: "Updated", + cell: ({ getValue }) => dateToLocaleString(getValue() as string), + }, + { + accessorKey: "expiryStarts", + header: "Expiry Starts", + cell: ({ getValue }) => getValue() ? dateToLocaleString(getValue() as string) : "-", + }, + { + accessorKey: "expiryEnds", + header: "Expiry Ends", + cell: ({ getValue }) => getValue() ? dateToLocaleString(getValue() as string) : "-", + }, + { + id: "actions", + header: "Actions", + cell: ({ row }) => { + return ( +
+ + +
+ ); + }, + } + ] +} + +export { getColumns }; \ No newline at end of file diff --git a/frontend/pages/build-parts/list/data-table.tsx b/frontend/pages/build-parts/list/data-table.tsx new file mode 100644 index 0000000..eb148fd --- /dev/null +++ b/frontend/pages/build-parts/list/data-table.tsx @@ -0,0 +1,277 @@ +"use client" + +import * as React from "react" +import { + closestCenter, + DndContext, + KeyboardSensor, + MouseSensor, + TouchSensor, + useSensor, + useSensors, + type UniqueIdentifier, +} from "@dnd-kit/core" +import { restrictToVerticalAxis } from "@dnd-kit/modifiers" +import { + SortableContext, + verticalListSortingStrategy, +} from "@dnd-kit/sortable" +import { + IconChevronDown, + IconChevronLeft, + IconChevronRight, + IconChevronsLeft, + IconChevronsRight, + IconLayoutColumns, + IconPlus, +} from "@tabler/icons-react" +import { + ColumnFiltersState, + flexRender, + getCoreRowModel, + getFacetedRowModel, + getFacetedUniqueValues, + getFilteredRowModel, + getSortedRowModel, + SortingState, + useReactTable, + VisibilityState, +} from "@tanstack/react-table" +import { Button } from "@/components/ui/button" +import { + DropdownMenu, + DropdownMenuCheckboxItem, + DropdownMenuContent, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu" +import { Label } from "@/components/ui/label" +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select" +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table" +import { + Tabs, + TabsContent, + TabsList, + TabsTrigger, +} from "@/components/ui/tabs" +import { schemaType } from "./schema" +import { getColumns, DraggableRow } from "./columns" +import { useRouter } from "next/navigation" +import { useDeleteBuildPartMutation } from "@/pages/build-parts/queries" +import { Home } from "lucide-react" + +export function BuildPartsDataTable({ + data, + totalCount, + currentPage = 1, + pageSize = 10, + onPageChange, + onPageSizeChange, + refetchTable, + buildId +}: { + data: schemaType[], + totalCount: number, + currentPage?: number, + pageSize?: number, + onPageChange: (page: number) => void, + onPageSizeChange: (size: number) => void, + refetchTable: () => void, + buildId: string, +}) { + + const router = useRouter(); + const [rowSelection, setRowSelection] = React.useState({}) + const [columnVisibility, setColumnVisibility] = React.useState({}) + const [columnFilters, setColumnFilters] = React.useState([]) + const [sorting, setSorting] = React.useState([]) + const sortableId = React.useId() + const sensors = useSensors(useSensor(MouseSensor, {}), useSensor(TouchSensor, {}), useSensor(KeyboardSensor, {})) + const dataIds = React.useMemo(() => data?.map(({ _id }) => _id) || [], [data]) + + const deleteMutation = useDeleteBuildPartMutation() + const deleteHandler = (id: string) => { deleteMutation.mutate({ uuid: id }); setTimeout(() => { refetchTable() }, 400) } + const columns = getColumns(router, deleteHandler, buildId); + const pagination = React.useMemo(() => ({ pageIndex: currentPage - 1, pageSize: pageSize }), [currentPage, pageSize]) + const totalPages = Math.ceil(totalCount / pageSize) + + const table = useReactTable({ + data, + columns, + pageCount: totalPages, + state: { sorting, columnVisibility, rowSelection, columnFilters, pagination }, + manualPagination: true, + getRowId: (row) => row._id.toString(), + enableRowSelection: true, + onRowSelectionChange: setRowSelection, + onSortingChange: setSorting, + onColumnFiltersChange: setColumnFilters, + onColumnVisibilityChange: setColumnVisibility, + onPaginationChange: (updater) => { const nextPagination = typeof updater === "function" ? updater(pagination) : updater; onPageChange(nextPagination.pageIndex + 1); onPageSizeChange(nextPagination.pageSize) }, + getCoreRowModel: getCoreRowModel(), + getFilteredRowModel: getFilteredRowModel(), + getSortedRowModel: getSortedRowModel(), + getFacetedRowModel: getFacetedRowModel(), + getFacetedUniqueValues: getFacetedUniqueValues(), + }) + + const handlePageSizeChange = (value: string) => { const newSize = Number(value); onPageSizeChange(newSize); onPageChange(1) } + + return ( + +
+ + +
+ + + + + + {table.getAllColumns().filter((column) => typeof column.accessorFn !== "undefined" && column.getCanHide()).map((column) => { + return ( + column.toggleVisibility(!!value)} > + {column.id} + + ) + })} + + + + +
+
+ +
+ + + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + return ( + + {header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())} + + ) + })} + + ))} + + + {table.getRowModel().rows?.length ? ( + {table.getRowModel().rows.map((row) => )} + ) : ( + No results. + )} + +
+
+
+
+
+ {table.getFilteredSelectedRowModel().rows.length} of{" "} + {table.getFilteredRowModel().rows.length} row(s) selected. +
+
+
+ + +
+
+ Page {currentPage} of {totalPages} +
+
+ Total Count: {totalCount} +
+
+ + + + +
+
+
+
+ +
+
+ +
+
+ +
+
+
+ ) +} \ No newline at end of file diff --git a/frontend/pages/build-parts/list/schema.tsx b/frontend/pages/build-parts/list/schema.tsx new file mode 100644 index 0000000..aeeba05 --- /dev/null +++ b/frontend/pages/build-parts/list/schema.tsx @@ -0,0 +1,28 @@ +import { z } from "zod"; + +export const schema = z.object({ + + _id: z.string(), + uuid: z.string(), + buildId: z.string(), + addressGovCode: z.string(), + no: z.number(), + level: z.number(), + code: z.string(), + grossSize: z.number(), + netSize: z.number(), + defaultAccessory: z.string(), + humanLivability: z.boolean(), + key: z.string(), + directionId: z.string().nullable().optional(), + typeId: z.string().nullable().optional(), + createdAt: z.string(), + updatedAt: z.string(), + expiryStarts: z.string(), + expiryEnds: z.string(), + active: z.boolean(), + isConfirmed: z.boolean(), + +}); + +export type schemaType = z.infer; diff --git a/frontend/pages/build-parts/page.tsx b/frontend/pages/build-parts/page.tsx new file mode 100644 index 0000000..74a3f1c --- /dev/null +++ b/frontend/pages/build-parts/page.tsx @@ -0,0 +1,32 @@ +'use client'; +import { useState } from "react"; +import { Button } from "@/components/ui/button"; +import { useSearchParams, useRouter } from "next/navigation"; +import { useGraphQlBuildPartsList } from "./queries"; +import { BuildPartsDataTable } from "./list/data-table"; + +const PageBuildPartsToBuild = () => { + const [page, setPage] = useState(1); + const [limit, setLimit] = useState(10); + const [sort, setSort] = useState({ createdAt: 'desc' }); + const [filters, setFilters] = useState({}); + + const searchParams = useSearchParams(); + const router = useRouter(); + const buildId = searchParams?.get('build'); + + const { data, isLoading, error, refetch } = useGraphQlBuildPartsList({ limit: 10, skip: 0, sort: { createdAt: -1 }, filters: { ...filters, buildId } }); + + const handlePageChange = (newPage: number) => { setPage(newPage) }; + const handlePageSizeChange = (newSize: number) => { setLimit(newSize); setPage(1) }; + + const noUUIDFound = <> +
Back To Builds. No uuid is found on headers
+ + + + if (!buildId) { return noUUIDFound } + return <> +} + +export default PageBuildPartsToBuild; diff --git a/frontend/pages/build-parts/queries.tsx b/frontend/pages/build-parts/queries.tsx new file mode 100644 index 0000000..e472451 --- /dev/null +++ b/frontend/pages/build-parts/queries.tsx @@ -0,0 +1,36 @@ +'use client' +import { useQuery, useMutation } from '@tanstack/react-query' +import { ListArguments } from '@/types/listRequest' + +const fetchGraphQlBuildPartsList = async (params: ListArguments): Promise => { + console.log('Fetching test data from local API'); + const { limit, skip, sort, filters } = params; + try { + const res = await fetch('/api/builds-parts/list', { method: 'POST', cache: 'no-store', credentials: "include", body: JSON.stringify({ limit, skip, sort, filters }) }); + if (!res.ok) { const errorText = await res.text(); console.error('Test data API error:', errorText); throw new Error(`API error: ${res.status} ${res.statusText}`) } + const data = await res.json(); + return { data: data.data, totalCount: data.totalCount } + } catch (error) { console.error('Error fetching test data:', error); throw error } +}; + +const fetchGraphQlDeleteBuildPart = async (uuid: string): Promise => { + console.log('Fetching test data from local API'); + try { + const res = await fetch(`/api/builds-parts/delete?uuid=${uuid}`, { method: 'GET', cache: 'no-store', credentials: "include" }); + if (!res.ok) { const errorText = await res.text(); console.error('Test data API error:', errorText); throw new Error(`API error: ${res.status} ${res.statusText}`) } + const data = await res.json(); + return data + } catch (error) { console.error('Error fetching test data:', error); throw error } +}; + +export function useGraphQlBuildPartsList(params: ListArguments) { + return useQuery({ queryKey: ['graphql-build-parts-list', params], queryFn: () => fetchGraphQlBuildPartsList(params) }) +} + +export function useDeleteBuildPartMutation() { + return useMutation({ + mutationFn: ({ uuid }: { uuid: string }) => fetchGraphQlDeleteBuildPart(uuid), + onSuccess: () => { console.log("Build part deleted successfully") }, + onError: (error) => { console.error("Delete build part failed:", error) }, + }) +} diff --git a/frontend/pages/build-parts/update/form.tsx b/frontend/pages/build-parts/update/form.tsx new file mode 100644 index 0000000..8d1edc8 --- /dev/null +++ b/frontend/pages/build-parts/update/form.tsx @@ -0,0 +1,299 @@ +"use client" +import { useForm } from "react-hook-form" +import { zodResolver } from "@hookform/resolvers/zod" +import { Form, FormField, FormItem, FormLabel, FormControl, FormMessage } from "@/components/ui/form" +import { Input } from "@/components/ui/input" +import { Button } from "@/components/ui/button" +import { Separator } from "@/components/ui/separator" +import { DateTimePicker } from "@/components/ui/date-time-picker" +import { BuildPartsUpdate, buildPartsUpdateSchema } from "@/pages/build-parts/update/schema" +import { Checkbox } from "@/components/ui/checkbox" +import { useUpdateBuildPartsMutation } from "@/pages/build-parts/update/queries" + +const BuildPartsForm = ({ refetchTable, initData, selectedUuid, buildId }: { refetchTable: () => void, initData: BuildPartsUpdate, selectedUuid: string, buildId: string }) => { + + const form = useForm({ + resolver: zodResolver(buildPartsUpdateSchema), defaultValues: { ...initData, directionId: initData.directionId ?? '', typeId: initData.typeId ?? '' } + }) + + const { handleSubmit } = form + + const mutation = useUpdateBuildPartsMutation(); + + function onSubmit(values: BuildPartsUpdate) { mutation.mutate({ data: values as any || initData, uuid: selectedUuid, buildId, refetchTable }) } + + return ( +
+ + {/* ROW 1 */} +
+ ( + + Address Gov Code + + + + + + )} + /> + ( + + No + + field.onChange(e.target.value === "" ? undefined : Number(e.target.value))} + /> + + + + )} + /> +
+ + {/* ROW 2 */} +
+ ( + + Level + + field.onChange(e.target.value === "" ? undefined : Number(e.target.value))} + /> + + + + )} + /> + ( + + Code + + + + + + )} + /> +
+ + {/* ROW 3 */} +
+ ( + + Key + + + + + + )} + /> + ( + + Gross Size + + field.onChange(e.target.value === "" ? undefined : Number(e.target.value))} + /> + + + + )} + /> +
+ + {/* ROW 4 */} +
+ ( + + Net Size + + field.onChange(e.target.value === "" ? undefined : Number(e.target.value))} + /> + + + + )} + /> + ( + + Default Accessory + + + + + + )} + /> + +
+ + {/* ROW 5 */} +
+ ( + + Direction Id + + + + + + )} + /> + ( + + Type Id + + + + + + )} + /> + +
+ + {/* BOOLEAN ROW */} +
+ + {/* boşluk kalmasın diye aktif alanını buraya koydum */} + ( + + + Active + + + + + + + )} + /> + ( + + + Human Livability + + + + + + + )} + /> + + ( + + + Is Confirmed + + + + + + + )} + /> +
+ + + {/* EXPIRY DATES */} +
+ ( + + Expiry Starts + + + + + + )} + /> + + ( + + Expiry Ends + + + + + + )} + /> +
+ + + + ); + +} + +export { BuildPartsForm } diff --git a/frontend/pages/build-parts/update/page.tsx b/frontend/pages/build-parts/update/page.tsx new file mode 100644 index 0000000..0178650 --- /dev/null +++ b/frontend/pages/build-parts/update/page.tsx @@ -0,0 +1,36 @@ +'use client'; +import { useState } from 'react'; +import { BuildPartsForm } from '@/pages/build-parts/update/form'; +import { useSearchParams, useRouter } from 'next/navigation' +import { Button } from '@/components/ui/button'; +import { BuildPartsDataTableUpdate } from './table/data-table'; +import { useGraphQlBuildPartsList } from '../queries'; + +const PageUpdateBuildParts = () => { + const [page, setPage] = useState(1); + const [limit, setLimit] = useState(10); + const [sort, setSort] = useState({ createdAt: 'desc' }); + const [filters, setFilters] = useState({}); + const searchParams = useSearchParams() + const router = useRouter() + const uuid = searchParams?.get('uuid') || null + const buildId = searchParams?.get('build') || null + const backToBuildAddress = <> +
UUID not found in search params
+ + + if (!uuid || !buildId) { return backToBuildAddress } + const { data, isLoading, error, refetch } = useGraphQlBuildPartsList({ limit, skip: (page - 1) * limit, sort, filters: { ...filters, _id: uuid } }); + const initData = data?.data?.[0] || null; + if (!initData) { return backToBuildAddress } + return ( + <> + + + + ) +} + +export { PageUpdateBuildParts }; diff --git a/frontend/pages/build-parts/update/queries.tsx b/frontend/pages/build-parts/update/queries.tsx new file mode 100644 index 0000000..0acf49c --- /dev/null +++ b/frontend/pages/build-parts/update/queries.tsx @@ -0,0 +1,25 @@ +'use client' +import { useMutation } from '@tanstack/react-query' +import { UpdateBuildPartsUpdate } from './types'; +import { toISOIfNotZ } from '@/lib/utils'; + +const fetchGraphQlBuildPartsUpdate = async (record: UpdateBuildPartsUpdate, uuid: string, buildId: string, refetchTable: () => void): Promise<{ data: UpdateBuildPartsUpdate | null; status: number }> => { + console.log('Fetching test data from local API'); + record.expiryStarts = record.expiryStarts ? toISOIfNotZ(record.expiryStarts) : undefined; + record.expiryEnds = record.expiryEnds ? toISOIfNotZ(record.expiryEnds) : undefined; + try { + const res = await fetch(`/api/builds-parts/update?uuid=${uuid || ''}`, { method: 'POST', cache: 'no-store', credentials: "include", body: JSON.stringify({ ...record, buildId }) }); + if (!res.ok) { const errorText = await res.text(); console.error('Test data API error:', errorText); throw new Error(`API error: ${res.status} ${res.statusText}`) } + const data = await res.json(); + refetchTable(); + return { data: data.data, status: res.status } + } catch (error) { console.error('Error fetching test data:', error); throw error } +}; + +export function useUpdateBuildPartsMutation() { + return useMutation({ + mutationFn: ({ data, uuid, buildId, refetchTable }: { data: UpdateBuildPartsUpdate, uuid: string, buildId: string, refetchTable: () => void }) => fetchGraphQlBuildPartsUpdate(data, uuid, buildId, refetchTable), + onSuccess: () => { console.log("Build Parts updated successfully") }, + onError: (error) => { console.error("Update Build Parts failed:", error) }, + }) +} diff --git a/frontend/pages/build-parts/update/schema.ts b/frontend/pages/build-parts/update/schema.ts new file mode 100644 index 0000000..dbda9eb --- /dev/null +++ b/frontend/pages/build-parts/update/schema.ts @@ -0,0 +1,23 @@ +import { z } from "zod" + +export const buildPartsUpdateSchema = z.object({ + + addressGovCode: z.string(), + no: z.number(), + level: z.number(), + code: z.string(), + grossSize: z.number(), + netSize: z.number(), + defaultAccessory: z.string(), + humanLivability: z.boolean(), + key: z.string(), + directionId: z.string().optional(), + typeId: z.string().optional(), + active: z.boolean(), + isConfirmed: z.boolean(), + expiryStarts: z.string().optional(), + expiryEnds: z.string().optional() + +}); + +export type BuildPartsUpdate = z.infer; diff --git a/frontend/pages/build-parts/update/table/columns.tsx b/frontend/pages/build-parts/update/table/columns.tsx new file mode 100644 index 0000000..ab8323e --- /dev/null +++ b/frontend/pages/build-parts/update/table/columns.tsx @@ -0,0 +1,113 @@ +"use client" +import { z } from "zod" +import { Button } from "@/components/ui/button" +import { Drawer, DrawerClose, DrawerContent, DrawerFooter, DrawerHeader, DrawerTrigger } from "@/components/ui/drawer" +import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger } from "@/components/ui/dropdown-menu" +import { Input } from "@/components/ui/input" +import { Label } from "@/components/ui/label" +import { useSortable } from "@dnd-kit/sortable" +import { IconGripVertical } from "@tabler/icons-react" +import { useIsMobile } from "@/hooks/use-mobile" +import { Separator } from "@/components/ui/separator" +import { ColumnDef, flexRender, Row } from "@tanstack/react-table" +import { TableCell, TableRow } from "@/components/ui/table" +import { CSS } from "@dnd-kit/utilities" +import { schema, schemaType } from "./schema" +import { dateToLocaleString } from "@/lib/utils" +import { Pencil, Trash } from "lucide-react" + +export function DraggableRow({ row }: { row: Row> }) { + const { transform, transition, setNodeRef, isDragging } = useSortable({ id: row.original._id }) + return ( + + {row.getVisibleCells().map((cell) => ({flexRender(cell.column.columnDef.cell, cell.getContext())}))} + + ) +} + +function getColumns(deleteHandler: (id: string) => void): ColumnDef[] { + return [ + { + accessorKey: "addressGovCode", + header: "Address Gov Code", + }, + { + accessorKey: "no", + header: "No", + }, + { + accessorKey: "level", + header: "Level", + }, + { + accessorKey: "code", + header: "Code", + }, + { + accessorKey: "grossSize", + header: "Gross Size", + }, + { + accessorKey: "netSize", + header: "Net Size", + }, + { + accessorKey: "defaultAccessory", + header: "Default Accessory", + }, + { + accessorKey: "humanLivability", + header: "Human Livability", + }, + { + accessorKey: "key", + header: "Key", + }, + { + accessorKey: "directionId", + header: "Direction ID", + }, + { + accessorKey: "typeId", + header: "Type ID", + }, + { + accessorKey: "createdAt", + header: "Created", + cell: ({ getValue }) => dateToLocaleString(getValue() as string), + }, + { + accessorKey: "updatedAt", + header: "Updated", + cell: ({ getValue }) => dateToLocaleString(getValue() as string), + }, + { + accessorKey: "expiryStarts", + header: "Expiry Starts", + cell: ({ getValue }) => getValue() ? dateToLocaleString(getValue() as string) : "-", + }, + { + accessorKey: "expiryEnds", + header: "Expiry Ends", + cell: ({ getValue }) => getValue() ? dateToLocaleString(getValue() as string) : "-", + }, + { + id: "actions", + header: "Actions", + cell: ({ row }) => { + return ( +
+ +
+ ); + }, + } + ] +} + +export { getColumns }; \ No newline at end of file diff --git a/frontend/pages/build-parts/update/table/data-table.tsx b/frontend/pages/build-parts/update/table/data-table.tsx new file mode 100644 index 0000000..1c0228f --- /dev/null +++ b/frontend/pages/build-parts/update/table/data-table.tsx @@ -0,0 +1,281 @@ +"use client" + +import * as React from "react" +import { + closestCenter, + DndContext, + KeyboardSensor, + MouseSensor, + TouchSensor, + useSensor, + useSensors, + type UniqueIdentifier, +} from "@dnd-kit/core" +import { restrictToVerticalAxis } from "@dnd-kit/modifiers" +import { + SortableContext, + verticalListSortingStrategy, +} from "@dnd-kit/sortable" +import { + IconChevronDown, + IconChevronLeft, + IconChevronRight, + IconChevronsLeft, + IconChevronsRight, + IconLayoutColumns, +} from "@tabler/icons-react" +import { + ColumnFiltersState, + flexRender, + getCoreRowModel, + getFacetedRowModel, + getFacetedUniqueValues, + getFilteredRowModel, + getSortedRowModel, + SortingState, + useReactTable, + VisibilityState, +} from "@tanstack/react-table" + +import { Button } from "@/components/ui/button" +import { + DropdownMenu, + DropdownMenuCheckboxItem, + DropdownMenuContent, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu" +import { Label } from "@/components/ui/label" +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select" +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table" +import { + Tabs, + TabsContent, + TabsList, + TabsTrigger, +} from "@/components/ui/tabs" +import { schemaType } from "./schema" +import { getColumns, DraggableRow } from "./columns" +import { useRouter } from "next/navigation" +import { Home } from "lucide-react" +import { useDeletePersonMutation } from "@/pages/people/queries" + +export function BuildPartsDataTableUpdate({ + data, + totalCount, + currentPage, + pageSize, + onPageChange, + onPageSizeChange, + refetchTable, + buildId, +}: { + data: schemaType[], + totalCount: number, + currentPage: number, + pageSize: number, + onPageChange: (page: number) => void, + onPageSizeChange: (size: number) => void, + refetchTable: () => void, + buildId: string +}) { + + const router = useRouter(); + const [rowSelection, setRowSelection] = React.useState({}) + const [columnVisibility, setColumnVisibility] = React.useState({}) + const [columnFilters, setColumnFilters] = React.useState([]) + const [sorting, setSorting] = React.useState([]) + const sortableId = React.useId() + const sensors = useSensors(useSensor(MouseSensor, {}), useSensor(TouchSensor, {}), useSensor(KeyboardSensor, {})) + const dataIds = React.useMemo(() => data?.map(({ _id }) => _id) || [], [data]) + + const deleteMutation = useDeletePersonMutation() + const deleteHandler = (id: string) => { deleteMutation.mutate({ uuid: id }); setTimeout(() => { refetchTable() }, 200) } + const columns = getColumns(deleteHandler); + const pagination = React.useMemo(() => ({ pageIndex: currentPage - 1, pageSize: pageSize, }), [currentPage, pageSize]) + const totalPages = Math.ceil(totalCount / pageSize) + + const table = useReactTable({ + data, + columns, + pageCount: totalPages, + state: { sorting, columnVisibility, rowSelection, columnFilters, pagination }, + manualPagination: true, + enableRowSelection: true, + getRowId: (row) => row._id.toString(), + onRowSelectionChange: setRowSelection, + onSortingChange: setSorting, + onColumnFiltersChange: setColumnFilters, + onColumnVisibilityChange: setColumnVisibility, + onPaginationChange: (updater) => { + const nextPagination = typeof updater === "function" ? updater(pagination) : updater; + onPageChange(nextPagination.pageIndex + 1); onPageSizeChange(nextPagination.pageSize); + }, + getCoreRowModel: getCoreRowModel(), + getFilteredRowModel: getFilteredRowModel(), + getSortedRowModel: getSortedRowModel(), + getFacetedRowModel: getFacetedRowModel(), + getFacetedUniqueValues: getFacetedUniqueValues(), + }) + + const handlePageSizeChange = (value: string) => { const newSize = Number(value); onPageSizeChange(newSize); onPageChange(1) } + + return ( + +
+ + +
+ + + + + + {table.getAllColumns().filter((column) => typeof column.accessorFn !== "undefined" && column.getCanHide()).map((column) => { + return ( + column.toggleVisibility(!!value)} > + {column.id} + + ) + })} + + + +
+
+ +
+ + + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + return ( + + {header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())} + + ) + })} + + ))} + + + {table.getRowModel().rows?.length ? ( + {table.getRowModel().rows.map((row) => )} + ) : ( + No results. + )} + +
+
+
+
+
+ {table.getFilteredSelectedRowModel().rows.length} of{" "} + {table.getFilteredRowModel().rows.length} row(s) selected. +
+
+
+ + +
+
+ Page {currentPage} of {totalPages} +
+
+ Total Count: {totalCount} +
+
+ + + + + + + +
+
+
+
+ +
+
+ +
+
+ +
+
+
+ ) +} \ No newline at end of file diff --git a/frontend/pages/build-parts/update/table/schema.tsx b/frontend/pages/build-parts/update/table/schema.tsx new file mode 100644 index 0000000..67510ba --- /dev/null +++ b/frontend/pages/build-parts/update/table/schema.tsx @@ -0,0 +1,26 @@ +import { z } from "zod"; + +export const schema = z.object({ + _id: z.string(), + uuid: z.string(), + buildId: z.string(), + addressGovCode: z.string(), + no: z.number(), + level: z.number(), + code: z.string(), + grossSize: z.number(), + netSize: z.number(), + defaultAccessory: z.string(), + humanLivability: z.boolean(), + key: z.string(), + directionId: z.string().optional(), + typeId: z.string().optional(), + createdAt: z.string(), + updatedAt: z.string(), + expiryStarts: z.string(), + expiryEnds: z.string(), + active: z.boolean(), + isConfirmed: z.boolean(), +}); + +export type schemaType = z.infer; diff --git a/frontend/pages/build-parts/update/types.ts b/frontend/pages/build-parts/update/types.ts new file mode 100644 index 0000000..d46200f --- /dev/null +++ b/frontend/pages/build-parts/update/types.ts @@ -0,0 +1,22 @@ + +interface UpdateBuildPartsUpdate { + + addressGovCode: string; + no: number; + level: number; + code: string; + key: string; + grossSize: number; + netSize: number; + defaultAccessory: string; + directionId: string; + typeId: string; + buildId: string; + expiryStarts?: string; + expiryEnds?: string; + active: boolean; + isConfirmed: boolean; + +} + +export type { UpdateBuildPartsUpdate }; \ No newline at end of file diff --git a/frontend/pages/build-sites/add/page.tsx b/frontend/pages/build-sites/add/page.tsx index 228e192..2a7315f 100644 --- a/frontend/pages/build-sites/add/page.tsx +++ b/frontend/pages/build-sites/add/page.tsx @@ -3,6 +3,8 @@ import { useState } from 'react'; import { BuildSitesForm } from '@/pages/build-sites/add/form'; import { BuildSitesDataTableAdd } from './table/data-table'; import { useGraphQlBuildSitesList } from '@/pages/build-sites/queries'; +import { useSearchParams, useRouter } from 'next/navigation'; +import { Button } from '@/components/ui/button'; const PageAddBuildSites = () => { const [page, setPage] = useState(1); @@ -10,7 +12,12 @@ const PageAddBuildSites = () => { const [sort, setSort] = useState({ createdAt: 'desc' }); const [filters, setFilters] = useState({}); - const { data, isLoading, error, refetch } = useGraphQlBuildSitesList({ limit, skip: (page - 1) * limit, sort, filters }); + const searchParams = useSearchParams(); + const router = useRouter(); + const buildId = searchParams?.get('build'); + const noUUIDFound = <>
Back To Builds. No uuid is found on headers
+ if (!buildId) { return noUUIDFound }; + const { data, isLoading, error, refetch } = useGraphQlBuildSitesList({ limit, skip: (page - 1) * limit, sort, filters: { ...filters, buildID: buildId } }); return ( <> diff --git a/frontend/pages/build-sites/list/data-table.tsx b/frontend/pages/build-sites/list/data-table.tsx index 7854a37..d752f28 100644 --- a/frontend/pages/build-sites/list/data-table.tsx +++ b/frontend/pages/build-sites/list/data-table.tsx @@ -78,7 +78,8 @@ export function BuildSitesDataTable({ pageSize = 10, onPageChange, onPageSizeChange, - refetchTable + refetchTable, + buildId }: { data: schemaType[], totalCount: number, @@ -87,6 +88,7 @@ export function BuildSitesDataTable({ onPageChange: (page: number) => void, onPageSizeChange: (size: number) => void, refetchTable: () => void, + buildId?: string }) { const router = useRouter(); @@ -163,7 +165,7 @@ export function BuildSitesDataTable({ })} - diff --git a/frontend/pages/build-sites/page.tsx b/frontend/pages/build-sites/page.tsx index 6881808..beb8f5b 100644 --- a/frontend/pages/build-sites/page.tsx +++ b/frontend/pages/build-sites/page.tsx @@ -2,6 +2,8 @@ import { BuildSitesDataTable } from './list/data-table'; import { useGraphQlBuildSitesList } from './queries'; import { useState } from 'react'; +import { useRouter, useSearchParams } from 'next/navigation'; +import { Button } from '@/components/ui/button'; const PageBuildSites = () => { @@ -10,14 +12,16 @@ const PageBuildSites = () => { const [sort, setSort] = useState({ createdAt: 'desc' }); const [filters, setFilters] = useState({}); - const { data, isLoading, error, refetch } = useGraphQlBuildSitesList({ limit, skip: (page - 1) * limit, sort, filters }); + const searchParams = useSearchParams(); + const router = useRouter(); + const noUUIDFound = <>
Back To Builds. No uuid is found on headers
const handlePageChange = (newPage: number) => { setPage(newPage) }; const handlePageSizeChange = (newSize: number) => { setLimit(newSize); setPage(1) }; + const { data, isLoading, error, refetch } = useGraphQlBuildSitesList({ limit, skip: (page - 1) * limit, sort, filters: { ...filters } }); if (isLoading) { return
Loading...
} if (error) { return
Error loading build sites
} - return ; }; diff --git a/frontend/pages/build-sites/update/form.tsx b/frontend/pages/build-sites/update/form.tsx index 50cfeee..8f190ea 100644 --- a/frontend/pages/build-sites/update/form.tsx +++ b/frontend/pages/build-sites/update/form.tsx @@ -7,17 +7,17 @@ import { Button } from "@/components/ui/button" import { Separator } from "@/components/ui/separator" import { DateTimePicker } from "@/components/ui/date-time-picker" import { useUpdateBuildSitesMutation } from "@/pages/build-sites/update/queries" -import { BuildAddressUpdate, buildAddressUpdateSchema } from "@/pages/build-sites/update/schema" +import { BuildSitesUpdate, buildSitesUpdateSchema } from "@/pages/build-sites/update/schema" -const BuildAddressForm = ({ refetchTable, initData, selectedUuid }: { refetchTable: () => void, initData: BuildAddressUpdate, selectedUuid: string }) => { +const BuildAddressForm = ({ refetchTable, initData, selectedUuid }: { refetchTable: () => void, initData: BuildSitesUpdate, selectedUuid: string }) => { - const form = useForm({ resolver: zodResolver(buildAddressUpdateSchema), defaultValues: { ...initData } }) + const form = useForm({ resolver: zodResolver(buildSitesUpdateSchema), defaultValues: { ...initData } }) const { handleSubmit } = form const mutation = useUpdateBuildSitesMutation(); - function onSubmit(values: BuildAddressUpdate) { mutation.mutate({ data: values as any || initData, uuid: selectedUuid }); setTimeout(() => refetchTable(), 400) } + function onSubmit(values: BuildSitesUpdate) { mutation.mutate({ data: values as any || initData, uuid: selectedUuid }); setTimeout(() => refetchTable(), 400) } return (
@@ -25,7 +25,6 @@ const BuildAddressForm = ({ refetchTable, initData, selectedUuid }: { refetchTab {/* ROW 1 */}
- { const [limit, setLimit] = useState(10); const [sort, setSort] = useState({ createdAt: 'desc' }); const [filters, setFilters] = useState({}); - const searchParams = useSearchParams() - const router = useRouter() + const searchParams = useSearchParams(); + const router = useRouter(); const uuid = searchParams?.get('uuid') || null - const backToBuildAddress = <> -
UUID not found in search params
- - - if (!uuid) { return backToBuildAddress } + const backToBuildSites = <>
UUID not found in search params
+ if (!uuid) { return backToBuildSites } const { data, isLoading, error, refetch } = useGraphQlBuildSitesList({ limit, skip: (page - 1) * limit, sort, filters: { ...filters, uuid } }); const initData = data?.data?.[0] || null; - if (!initData) { return backToBuildAddress } + if (!initData) { return backToBuildSites } return ( <> diff --git a/frontend/pages/build-sites/update/queries.tsx b/frontend/pages/build-sites/update/queries.tsx index 99b3179..987b844 100644 --- a/frontend/pages/build-sites/update/queries.tsx +++ b/frontend/pages/build-sites/update/queries.tsx @@ -8,7 +8,7 @@ const fetchGraphQlBuildAddressUpdate = async (record: UpdateBuildSitesUpdate, uu record.expiryStarts = record.expiryStarts ? toISOIfNotZ(record.expiryStarts) : undefined; record.expiryEnds = record.expiryEnds ? toISOIfNotZ(record.expiryEnds) : undefined; try { - const res = await fetch(`/api/build-sites/update?uuid=${uuid || ''}`, { method: 'POST', cache: 'no-store', credentials: "include", body: JSON.stringify(record) }); + const res = await fetch(`/api/build-sites/update?uuid=${uuid || ''}`, { method: 'POST', cache: 'no-store', credentials: "include", body: JSON.stringify({ ...record }) }); if (!res.ok) { const errorText = await res.text(); console.error('Test data API error:', errorText); throw new Error(`API error: ${res.status} ${res.statusText}`) } const data = await res.json(); return { data: data.data, status: res.status } diff --git a/frontend/pages/build-sites/update/schema.ts b/frontend/pages/build-sites/update/schema.ts index 2d6508d..75816bc 100644 --- a/frontend/pages/build-sites/update/schema.ts +++ b/frontend/pages/build-sites/update/schema.ts @@ -1,6 +1,6 @@ import { z } from "zod" -export const buildAddressUpdateSchema = z.object({ +export const buildSitesUpdateSchema = z.object({ siteNo: z.string().optional(), siteName: z.string().optional(), @@ -9,4 +9,4 @@ export const buildAddressUpdateSchema = z.object({ }); -export type BuildAddressUpdate = z.infer; +export type BuildSitesUpdate = z.infer; diff --git a/frontend/pages/build-sites/update/table/data-table.tsx b/frontend/pages/build-sites/update/table/data-table.tsx index e939a3e..1b35c87 100644 --- a/frontend/pages/build-sites/update/table/data-table.tsx +++ b/frontend/pages/build-sites/update/table/data-table.tsx @@ -87,7 +87,7 @@ export function BuildSitesDataTableUpdate({ pageSize: number, onPageChange: (page: number) => void, onPageSizeChange: (size: number) => void, - refetchTable: () => void + refetchTable: () => void, }) { const router = useRouter(); @@ -167,9 +167,9 @@ export function BuildSitesDataTableUpdate({ })} -
diff --git a/frontend/pages/build-types/update/queries.tsx b/frontend/pages/build-types/update/queries.tsx index b014902..8e102b2 100644 --- a/frontend/pages/build-types/update/queries.tsx +++ b/frontend/pages/build-types/update/queries.tsx @@ -1,9 +1,13 @@ 'use client' import { useMutation } from '@tanstack/react-query' import { BuildTypesUpdate } from './types'; +import { toISOIfNotZ } from '@/lib/utils'; const fetchGraphQlBuildTypesUpdate = async (record: BuildTypesUpdate, uuid: string): Promise<{ data: BuildTypesUpdate | null; status: number }> => { console.log('Fetching test data from local API'); + record.expiryStarts = record.expiryStarts ? toISOIfNotZ(record.expiryStarts) : undefined; + record.expiryEnds = record.expiryEnds ? toISOIfNotZ(record.expiryEnds) : undefined; + console.dir({ record }, { depth: Infinity }); try { const res = await fetch(`/api/build-types/update?uuid=${uuid || ''}`, { method: 'POST', cache: 'no-store', credentials: "include", body: JSON.stringify(record) }); if (!res.ok) { const errorText = await res.text(); console.error('Test data API error:', errorText); throw new Error(`API error: ${res.status} ${res.statusText}`) } diff --git a/frontend/pages/build-types/update/types.ts b/frontend/pages/build-types/update/types.ts index e1173f0..980221f 100644 --- a/frontend/pages/build-types/update/types.ts +++ b/frontend/pages/build-types/update/types.ts @@ -4,6 +4,8 @@ interface BuildTypesUpdate { token: string; typeToken: string; description: string; + expiryStarts?: string; + expiryEnds?: string; } diff --git a/frontend/pages/builds/add/form.tsx b/frontend/pages/builds/add/form.tsx new file mode 100644 index 0000000..453b261 --- /dev/null +++ b/frontend/pages/builds/add/form.tsx @@ -0,0 +1,392 @@ +"use client" +import { useForm } from "react-hook-form" +import { zodResolver } from "@hookform/resolvers/zod" +import { Form, FormField, FormItem, FormLabel, FormControl, FormMessage } from "@/components/ui/form" +import { Input } from "@/components/ui/input" +import { Button } from "@/components/ui/button" +import { Separator } from "@/components/ui/separator" +import { DateTimePicker } from "@/components/ui/date-time-picker" +import { BuildAdd, buildAddSchema } from "./schema" +import { useAddBuildMutation } from "./queries" +import { Checkbox } from "@/components/ui/checkbox" + +const BuildsForm = ({ refetchTable }: { refetchTable: () => void }) => { + + const form = useForm({ + resolver: zodResolver(buildAddSchema), + defaultValues: { + buildType: "", + collectionToken: "", + info: { + govAddressCode: "", + buildName: "", + buildNo: "", + maxFloor: 0, + undergroundFloor: 0, + buildDate: "", + decisionPeriodDate: "", + taxNo: "", + liftCount: 0, + heatingSystem: false, + coolingSystem: false, + hotWaterSystem: false, + blockServiceManCount: 0, + securityServiceManCount: 0, + garageCount: 0, + managementRoomId: "", + } + }, + }); + + const { handleSubmit } = form; + + const mutation = useAddBuildMutation(); + + function onSubmit(values: BuildAdd) { mutation.mutate({ data: values }); setTimeout(() => refetchTable(), 400) }; + + return ( + + + + {/* ROW 1 */} +
+ ( + + Token + + + + + + )} + /> + + ( + + Collection Token + + + + + + )} + /> +
+ +
+ ( + + Gov Address Code + + + + + + )} + /> + ( + + Build Name + + + + + + )} + /> + ( + + Build No + + + + + + )} + /> +
+ + +
+ ( + + Max Floor + + { + field.onBlur(); + const numValue = parseFloat(e.target.value); + if (!isNaN(numValue)) { + field.onChange(numValue); + } + }} + /> + + + + )} + /> + ( + + Underground Floor + + { + field.onBlur(); + const numValue = parseFloat(e.target.value); + if (!isNaN(numValue)) { + field.onChange(numValue); + } + }} + /> + + + + )} + /> +
+ +
+ ( + + Tax No + + + + + + )} + /> + ( + + Lift Count + + { + field.onBlur(); + const numValue = parseFloat(e.target.value); + if (!isNaN(numValue)) { + field.onChange(numValue); + } + }} + /> + + + + )} + /> +
+ +
+ ( + + + + +
+ + Heating System + +
+
+ )} + /> + ( + + + + +
+ + Cooling System + +
+
+ )} + /> + ( + + + + +
+ + Hot Water System + +
+
+ )} + /> +
+ + +
+ ( + + Build Date + + + + + + )} + /> + ( + + Decision Period Date + + + + + + )} + /> +
+ +
+ ( + + Block Service Man Count + + { + field.onBlur(); + const numValue = parseFloat(e.target.value); + if (!isNaN(numValue)) { + field.onChange(numValue); + } + }} + /> + + + + )} + /> + ( + + Security Service Man Count + + { + field.onBlur(); + const numValue = parseFloat(e.target.value); + if (!isNaN(numValue)) { + field.onChange(numValue); + } + }} + /> + + + + )} + /> + ( + + Total Garage Count In Numbers + + { + field.onBlur(); + const numValue = parseFloat(e.target.value); + if (!isNaN(numValue)) { + field.onChange(numValue); + } + }} + /> + + + + )} + /> + ( + + Management Room ID Assign + + + + + + )} + /> +
+ + + + + ); + +}; + +export { BuildsForm } diff --git a/frontend/pages/builds/add/page.tsx b/frontend/pages/builds/add/page.tsx new file mode 100644 index 0000000..73ff1ae --- /dev/null +++ b/frontend/pages/builds/add/page.tsx @@ -0,0 +1,25 @@ +'use client'; +import { useState } from 'react'; +import { BuildDataTableAdd } from './table/data-table'; +import { BuildsForm } from './form'; +import { useGraphQlBuildsList } from '../queries'; + +const PageAddBuilds = () => { + const [page, setPage] = useState(1); + const [limit, setLimit] = useState(10); + const [sort, setSort] = useState({ createdAt: 'desc' }); + const [filters, setFilters] = useState({}); + + const { data, isLoading, error, refetch } = useGraphQlBuildsList({ limit, skip: (page - 1) * limit, sort, filters }); + + return ( + <> + + + + ) +} + +export { PageAddBuilds }; diff --git a/frontend/pages/builds/add/queries.tsx b/frontend/pages/builds/add/queries.tsx new file mode 100644 index 0000000..2b2bb3e --- /dev/null +++ b/frontend/pages/builds/add/queries.tsx @@ -0,0 +1,25 @@ +'use client' +import { useMutation } from '@tanstack/react-query' +import { toISOIfNotZ } from '@/lib/utils'; +import { BuildAdd } from './schema'; + +const fetchGraphQlBuildAdd = async (record: BuildAdd): Promise<{ data: BuildAdd | null; status: number }> => { + console.log('Fetching test data from local API'); + record.info.buildDate = toISOIfNotZ(record.info.buildDate); + record.info.decisionPeriodDate = toISOIfNotZ(record.info.decisionPeriodDate); + console.dir({ record }) + try { + const res = await fetch('/api/builds/add', { method: 'POST', cache: 'no-store', credentials: "include", body: JSON.stringify(record) }); + if (!res.ok) { const errorText = await res.text(); console.error('Test data API error:', errorText); throw new Error(`API error: ${res.status} ${res.statusText}`) } + const data = await res.json(); + return { data: data.data, status: res.status } + } catch (error) { console.error('Error fetching test data:', error); throw error } +}; + +export function useAddBuildMutation() { + return useMutation({ + mutationFn: ({ data }: { data: BuildAdd }) => fetchGraphQlBuildAdd(data), + onSuccess: () => { console.log("Build created successfully") }, + onError: (error) => { console.error("Add build failed:", error) }, + }) +} diff --git a/frontend/pages/builds/add/schema.ts b/frontend/pages/builds/add/schema.ts new file mode 100644 index 0000000..431438d --- /dev/null +++ b/frontend/pages/builds/add/schema.ts @@ -0,0 +1,28 @@ +import { z } from "zod" + +export const buildAddSchema = z.object({ + + buildType: z.string(), + collectionToken: z.string(), + info: z.object({ + govAddressCode: z.string(), + buildName: z.string(), + buildNo: z.string(), + maxFloor: z.number(), + undergroundFloor: z.number(), + buildDate: z.string(), + decisionPeriodDate: z.string(), + taxNo: z.string(), + liftCount: z.number(), + heatingSystem: z.boolean(), + coolingSystem: z.boolean(), + hotWaterSystem: z.boolean(), + blockServiceManCount: z.number(), + securityServiceManCount: z.number(), + garageCount: z.number(), + managementRoomId: z.string(), + }) + +}); + +export type BuildAdd = z.infer; diff --git a/frontend/pages/builds/add/table/columns.tsx b/frontend/pages/builds/add/table/columns.tsx new file mode 100644 index 0000000..b7dec5b --- /dev/null +++ b/frontend/pages/builds/add/table/columns.tsx @@ -0,0 +1,155 @@ +"use client" +import { z } from "zod" +import { Button } from "@/components/ui/button" +import { Drawer, DrawerClose, DrawerContent, DrawerFooter, DrawerHeader, DrawerTrigger } from "@/components/ui/drawer" +import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger } from "@/components/ui/dropdown-menu" +import { Input } from "@/components/ui/input" +import { Label } from "@/components/ui/label" +import { useSortable } from "@dnd-kit/sortable" +import { IconGripVertical } from "@tabler/icons-react" +import { useIsMobile } from "@/hooks/use-mobile" +import { Separator } from "@/components/ui/separator" +import { ColumnDef, flexRender, Row } from "@tanstack/react-table" +import { TableCell, TableRow } from "@/components/ui/table" +import { CSS } from "@dnd-kit/utilities" +import { schema, schemaType } from "./schema" +import { dateToLocaleString } from "@/lib/utils" +import { Pencil, Trash } from "lucide-react" + +function DragHandle({ id }: { id: number }) { + const { attributes, listeners } = useSortable({ id }) + return ( + + ) +} + +export function DraggableRow({ row }: { row: Row> }) { + const { transform, transition, setNodeRef, isDragging } = useSortable({ id: row.original._id }) + return ( + + {row.getVisibleCells().map((cell) => ( + {flexRender(cell.column.columnDef.cell, cell.getContext())} + ))} + + ) +} + + +function getColumns(router: any, deleteHandler: (id: string) => void): ColumnDef[] { + return [ + { + accessorKey: "buildType.token", + header: "Token", + cell: ({ getValue }) => getValue(), + }, + { + accessorKey: "collectionToken", + header: "Collection Token", + cell: ({ getValue }) => getValue(), + }, + { + accessorKey: "info.govAddressCode", + header: "Gov Address Code", + cell: ({ getValue }) => getValue(), + }, + { + accessorKey: "info.buildName", + header: "Build Name", + cell: ({ getValue }) => getValue(), + }, + { + accessorKey: "info.buildNo", + header: "Build No", + cell: ({ getValue }) => getValue(), + }, + { + accessorKey: "info.maxFloor", + header: "Max Floor", + cell: ({ getValue }) => getValue(), + }, + { + accessorKey: "info.undergroundFloor", + header: "Underground Floor", + cell: ({ getValue }) => getValue(), + }, + { + accessorKey: "info.buildDate", + header: "Build Date", + cell: ({ getValue }) => getValue(), + }, + { + accessorKey: "info.decisionPeriodDate", + header: "Decision Period Date", + cell: ({ getValue }) => getValue(), + }, + { + accessorKey: "info.taxNo", + header: "Tax No", + cell: ({ getValue }) => getValue(), + }, + { + accessorKey: "info.liftCount", + header: "Lift Count", + cell: ({ getValue }) => getValue(), + }, + { + accessorKey: "info.heatingSystem", + header: "Heating System", + cell: ({ getValue }) => getValue(), + }, + { + accessorKey: "info.coolingSystem", + header: "Cooling System", + cell: ({ getValue }) => getValue(), + }, + { + accessorKey: "info.hotWaterSystem", + header: "Hot Water System", + cell: ({ getValue }) => getValue(), + }, + { + accessorKey: "info.blockServiceManCount", + header: "Block Service Man Count", + cell: ({ getValue }) => getValue(), + }, + { + accessorKey: "info.securityServiceManCount", + header: "Security Service Man Count", + cell: ({ getValue }) => getValue(), + }, + { + accessorKey: "info.garageCount", + header: "Garage Count", + cell: ({ getValue }) => getValue(), + }, + { + accessorKey: "info.managementRoomId", + header: "Management Room ID", + cell: ({ getValue }) => getValue(), + }, + { + id: "actions", + header: "Actions", + cell: ({ row }) => { + return ( +
+ + +
+ ); + }, + } + ] +} + +export { getColumns }; \ No newline at end of file diff --git a/frontend/pages/builds/add/table/data-table.tsx b/frontend/pages/builds/add/table/data-table.tsx new file mode 100644 index 0000000..66e5cf7 --- /dev/null +++ b/frontend/pages/builds/add/table/data-table.tsx @@ -0,0 +1,254 @@ +"use client" +import * as React from "react" +import { + closestCenter, + DndContext, + KeyboardSensor, + MouseSensor, + TouchSensor, + useSensor, + useSensors, + type UniqueIdentifier, +} from "@dnd-kit/core" +import { restrictToVerticalAxis } from "@dnd-kit/modifiers" +import { + SortableContext, + verticalListSortingStrategy, +} from "@dnd-kit/sortable" +import { + IconChevronDown, + IconChevronLeft, + IconChevronRight, + IconChevronsLeft, + IconChevronsRight, + IconLayoutColumns, +} from "@tabler/icons-react" +import { + ColumnFiltersState, + flexRender, + getCoreRowModel, + getFacetedRowModel, + getFacetedUniqueValues, + getFilteredRowModel, + getSortedRowModel, + SortingState, + useReactTable, + VisibilityState, +} from "@tanstack/react-table" +import { Button } from "@/components/ui/button" +import { DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuTrigger } from "@/components/ui/dropdown-menu" +import { Label } from "@/components/ui/label" +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" +import { Tabs, TabsContent } from "@/components/ui/tabs" +import { schemaType } from "./schema" +import { getColumns, DraggableRow } from "./columns" +import { useRouter } from "next/navigation" +import { Home } from "lucide-react" +import { useDeleteBuildMutation } from "@/pages/builds/queries" + +export function BuildDataTableAdd({ + data, + totalCount, + currentPage, + pageSize, + onPageChange, + onPageSizeChange, + refetchTable, +}: { + data: schemaType[], + totalCount: number, + currentPage: number, + pageSize: number, + onPageChange: (page: number) => void, + onPageSizeChange: (size: number) => void, + refetchTable: () => void +}) { + + const router = useRouter(); + const [rowSelection, setRowSelection] = React.useState({}) + const [columnVisibility, setColumnVisibility] = React.useState({}) + const [columnFilters, setColumnFilters] = React.useState([]) + const [sorting, setSorting] = React.useState([]) + const sortableId = React.useId() + const sensors = useSensors(useSensor(MouseSensor, {}), useSensor(TouchSensor, {}), useSensor(KeyboardSensor, {})) + const dataIds = React.useMemo(() => data?.map(({ _id }) => _id) || [], [data]) + + const deleteMutation = useDeleteBuildMutation() + const deleteHandler = (id: string) => { deleteMutation.mutate({ uuid: id }); setTimeout(() => { refetchTable() }, 400) } + const columns = getColumns(router, deleteHandler); + const pagination = React.useMemo(() => ({ pageIndex: currentPage - 1, pageSize: pageSize, }), [currentPage, pageSize]) + const totalPages = Math.ceil(totalCount / pageSize) + + const table = useReactTable({ + data, + columns, + pageCount: totalPages, + state: { sorting, columnVisibility, rowSelection, columnFilters, pagination }, + manualPagination: true, + enableRowSelection: true, + getRowId: (row) => row._id.toString(), + onRowSelectionChange: setRowSelection, + onSortingChange: setSorting, + onColumnFiltersChange: setColumnFilters, + onColumnVisibilityChange: setColumnVisibility, + onPaginationChange: (updater) => { + const nextPagination = typeof updater === "function" ? updater(pagination) : updater; + onPageChange(nextPagination.pageIndex + 1); onPageSizeChange(nextPagination.pageSize); + }, + getCoreRowModel: getCoreRowModel(), + getFilteredRowModel: getFilteredRowModel(), + getSortedRowModel: getSortedRowModel(), + getFacetedRowModel: getFacetedRowModel(), + getFacetedUniqueValues: getFacetedUniqueValues(), + }) + + const handlePageSizeChange = (value: string) => { const newSize = Number(value); onPageSizeChange(newSize); onPageChange(1) } + + return ( + +
+ + +
+ + + + + + {table.getAllColumns().filter((column) => typeof column.accessorFn !== "undefined" && column.getCanHide()).map((column) => { + return ( + column.toggleVisibility(!!value)} > + {column.id} + + ) + })} + + + +
+
+ +
+ + + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + return ( + + {header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())} + + ) + })} + + ))} + + + {table.getRowModel().rows?.length ? ( + {table.getRowModel().rows.map((row) => )} + ) : ( + No results. + )} + +
+
+
+
+
+ {table.getFilteredSelectedRowModel().rows.length} of{" "} + {table.getFilteredRowModel().rows.length} row(s) selected. +
+
+
+ + +
+
+ Page {currentPage} of {totalPages} +
+
+ Total Count: {totalCount} +
+
+ + + + + + + +
+
+
+
+ +
+
+ +
+
+ +
+
+
+ ) +} \ No newline at end of file diff --git a/frontend/pages/builds/add/table/schema.tsx b/frontend/pages/builds/add/table/schema.tsx new file mode 100644 index 0000000..fa1a62a --- /dev/null +++ b/frontend/pages/builds/add/table/schema.tsx @@ -0,0 +1,31 @@ +import { z } from "zod"; + +export const schema = z.object({ + _id: z.string(), + buildType: z.object({ + token: z.string(), + typeToken: z.string(), + type: z.string(), + }), + collectionToken: z.string(), + info: z.object({ + govAddressCode: z.string(), + buildName: z.string(), + buildNo: z.string(), + maxFloor: z.number(), + undergroundFloor: z.number(), + buildDate: z.string(), + decisionPeriodDate: z.string(), + taxNo: z.string(), + liftCount: z.number(), + heatingSystem: z.boolean(), + coolingSystem: z.boolean(), + hotWaterSystem: z.boolean(), + blockServiceManCount: z.number(), + securityServiceManCount: z.number(), + garageCount: z.number(), + managementRoomId: z.number(), + }), +}); + +export type schemaType = z.infer; \ No newline at end of file diff --git a/frontend/pages/builds/add/types.ts b/frontend/pages/builds/add/types.ts new file mode 100644 index 0000000..e4508a4 --- /dev/null +++ b/frontend/pages/builds/add/types.ts @@ -0,0 +1,24 @@ + +interface PeopleAdd { + + firstName: string; + surname: string; + middleName?: string; + sexCode: string; + personRef?: string; + personTag?: string; + fatherName?: string; + motherName?: string; + countryCode: string; + nationalIdentityId: string; + birthPlace: string; + birthDate: string; + taxNo?: string; + birthname?: string; + expiryStarts?: string; + expiryEnds?: string; + +} + + +export type { PeopleAdd }; \ No newline at end of file diff --git a/frontend/pages/builds/list/columns.tsx b/frontend/pages/builds/list/columns.tsx index ca1ca30..933219f 100644 --- a/frontend/pages/builds/list/columns.tsx +++ b/frontend/pages/builds/list/columns.tsx @@ -6,7 +6,7 @@ import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, import { Input } from "@/components/ui/input" import { Label } from "@/components/ui/label" import { useSortable } from "@dnd-kit/sortable" -import { IconGripVertical } from "@tabler/icons-react" +import { IconGripVertical, IconHandClick } from "@tabler/icons-react" import { useIsMobile } from "@/hooks/use-mobile" import { Separator } from "@/components/ui/separator" import { ColumnDef, flexRender, Row } from "@tanstack/react-table" @@ -14,119 +14,7 @@ import { TableCell, TableRow } from "@/components/ui/table" import { CSS } from "@dnd-kit/utilities" import { schema, schemaType } from "./schema" import { dateToLocaleString } from "@/lib/utils" -import { Pencil, Trash } from "lucide-react" - -// function TableCellViewer({ item }: { item: schemaType }) { -// const isMobile = useIsMobile(); - -// return ( -// -// -// -// - -// -// -//

{item.email}

-//

-// User details -//

-//
- -//
- -// {/* BASIC INFO */} -//
-//
-// -// -//
- -//
-// -// -//
- -//
-// -// -//
- -//
-// -// -//
-//
- -// - -// {/* DATES */} -//
-//
-// -// -//
- -//
-// -// -//
-//
- -// - -// {/* TOKENS */} -// -// -// -// - -// -// Tokens -// -// {(item.collectionTokens?.tokens ?? []).length === 0 && (No tokens found)} -// {(item.collectionTokens?.tokens ?? []).map((t, i) => ( -// -//
-// -// -//
-//
-// ))} -//
-//
- -//
- -// -// -// -// -// -//
-//
-// ); -// } - -function DragHandle({ id }: { id: number }) { - const { attributes, listeners } = useSortable({ id }) - return ( - - ) -} +import { Pencil, Trash, TextSelect } from "lucide-react" export function DraggableRow({ row }: { row: Row> }) { const { transform, transition, setNodeRef, isDragging } = useSortable({ id: row.original._id }) @@ -142,102 +30,112 @@ export function DraggableRow({ row }: { row: Row> }) { ) } -function getColumns(router: any, deleteHandler: (id: string) => void): ColumnDef[] { + +function getColumns(router: any, activeRoute: string, deleteHandler: (id: string) => void): ColumnDef[] { return [ { - accessorKey: "uuid", - header: "UUID", - cell: ({ getValue }) => (
{String(getValue())}
), + accessorKey: "buildType.token", + header: "Token", + cell: ({ getValue }) => getValue(), }, { - accessorKey: "firstName", - header: "First Name", + accessorKey: "collectionToken", + header: "Collection Token", + cell: ({ getValue }) => getValue(), }, { - accessorKey: "surname", - header: "Surname", + accessorKey: "info.govAddressCode", + header: "Gov Address Code", + cell: ({ getValue }) => getValue(), }, { - accessorKey: "middleName", - header: "Middle Name", + accessorKey: "info.buildName", + header: "Build Name", + cell: ({ getValue }) => getValue(), }, { - accessorKey: "sexCode", - header: "Sex", + accessorKey: "info.buildNo", + header: "Build No", + cell: ({ getValue }) => getValue(), }, { - accessorKey: "personRef", - header: "Person Ref", + accessorKey: "info.maxFloor", + header: "Max Floor", + cell: ({ getValue }) => getValue(), }, { - accessorKey: "personTag", - header: "Person Tag", + accessorKey: "info.undergroundFloor", + header: "Underground Floor", + cell: ({ getValue }) => getValue(), }, { - accessorKey: "fatherName", - header: "Father Name", + accessorKey: "info.buildDate", + header: "Build Date", + cell: ({ getValue }) => getValue(), }, { - accessorKey: "motherName", - header: "Mother Name", + accessorKey: "info.decisionPeriodDate", + header: "Decision Period Date", + cell: ({ getValue }) => getValue(), }, { - accessorKey: "countryCode", - header: "Country", + accessorKey: "info.taxNo", + header: "Tax No", + cell: ({ getValue }) => getValue(), }, { - accessorKey: "nationalIdentityId", - header: "National ID", + accessorKey: "info.liftCount", + header: "Lift Count", + cell: ({ getValue }) => getValue(), }, { - accessorKey: "birthPlace", - header: "Birth Place", + accessorKey: "info.heatingSystem", + header: "Heating System", + cell: ({ getValue }) => getValue(), }, { - accessorKey: "active", - header: "Active", - cell: ({ getValue }) => getValue() ? (
Yes
) : (
No
), + accessorKey: "info.coolingSystem", + header: "Cooling System", + cell: ({ getValue }) => getValue(), }, { - accessorKey: "isConfirmed", - header: "Confirmed", - cell: ({ getValue }) => getValue() ? (
Yes
) : (
No
), + accessorKey: "info.hotWaterSystem", + header: "Hot Water System", + cell: ({ getValue }) => getValue(), }, { - accessorKey: "birthDate", - header: "Birth Date", - cell: ({ getValue }) => dateToLocaleString(getValue() as string), + accessorKey: "info.blockServiceManCount", + header: "Block Service Man Count", + cell: ({ getValue }) => getValue(), }, { - accessorKey: "createdAt", - header: "Created", - cell: ({ getValue }) => dateToLocaleString(getValue() as string), + accessorKey: "info.securityServiceManCount", + header: "Security Service Man Count", + cell: ({ getValue }) => getValue(), }, { - accessorKey: "updatedAt", - header: "Updated", - cell: ({ getValue }) => dateToLocaleString(getValue() as string), + accessorKey: "info.garageCount", + header: "Garage Count", + cell: ({ getValue }) => getValue(), }, { - accessorKey: "expiryStarts", - header: "Expiry Starts", - cell: ({ getValue }) => getValue() ? dateToLocaleString(getValue() as string) : "-", - }, - { - accessorKey: "expiryEnds", - header: "Expiry Ends", - cell: ({ getValue }) => getValue() ? dateToLocaleString(getValue() as string) : "-", + accessorKey: "info.managementRoomId", + header: "Management Room ID", + cell: ({ getValue }) => getValue(), }, { id: "actions", header: "Actions", cell: ({ row }) => { return ( -
- + -
diff --git a/frontend/pages/builds/list/data-table.tsx b/frontend/pages/builds/list/data-table.tsx index 186ac6b..59bdfcb 100644 --- a/frontend/pages/builds/list/data-table.tsx +++ b/frontend/pages/builds/list/data-table.tsx @@ -17,6 +17,10 @@ import { verticalListSortingStrategy, } from "@dnd-kit/sortable" import { + IconBorderLeftPlus, + IconBuildingBank, + IconBuildingBridge, + IconBuildingChurch, IconChevronDown, IconChevronLeft, IconChevronRight, @@ -70,7 +74,7 @@ import { import { schemaType } from "./schema" import { getColumns, DraggableRow } from "./columns" import { useRouter } from "next/navigation" -import { useDeleteUserMutation } from "@/pages/users/queries" +import { useDeleteBuildMutation } from "@/pages/builds/queries" export function BuildDataTable({ data, @@ -87,10 +91,29 @@ export function BuildDataTable({ pageSize?: number, onPageChange: (page: number) => void, onPageSizeChange: (size: number) => void, - refetchTable: () => void, + refetchTable: () => void }) { const router = useRouter(); + const routeSelections = [ + { + url: 'build-parts', + name: 'Build Parts', + icon: + }, + { + url: 'build-areas', + name: 'Build Areas', + icon: + }, + { + url: 'build-sites', + name: 'Build Sites', + icon: + }, + ] + + const [activeRoute, setActiveRoute] = React.useState(routeSelections[0].url); const [rowSelection, setRowSelection] = React.useState({}) const [columnVisibility, setColumnVisibility] = React.useState({}) const [columnFilters, setColumnFilters] = React.useState([]) @@ -99,9 +122,9 @@ export function BuildDataTable({ const sensors = useSensors(useSensor(MouseSensor, {}), useSensor(TouchSensor, {}), useSensor(KeyboardSensor, {})) const dataIds = React.useMemo(() => data?.map(({ _id }) => _id) || [], [data]) - const deleteMutation = useDeleteUserMutation() + const deleteMutation = useDeleteBuildMutation() const deleteHandler = (id: string) => { deleteMutation.mutate({ uuid: id }); setTimeout(() => { refetchTable() }, 400) } - const columns = getColumns(router, deleteHandler); + const columns = getColumns(router, activeRoute, deleteHandler); const pagination = React.useMemo(() => ({ pageIndex: currentPage - 1, pageSize: pageSize }), [currentPage, pageSize]) const totalPages = Math.ceil(totalCount / pageSize) @@ -128,14 +151,9 @@ export function BuildDataTable({ const handlePageSizeChange = (value: string) => { const newSize = Number(value); onPageSizeChange(newSize); onPageChange(1) } return ( - +
- + + + + + )} + /> + + ( + + Collection Token + + + + + + )} + /> +
+ +
+ ( + + Gov Address Code + + + + + + )} + /> + ( + + Build Name + + + + + + )} + /> + ( + + Build No + + + + + + )} + /> +
+ + +
+ ( + + Max Floor + + + + + + )} + /> + ( + + Underground Floor + + + + + + )} + /> +
+ +
+ ( + + Tax No + + + + + + )} + /> + ( + + Lift Count + + + + + + )} + /> +
+ +
+ ( + + + + +
+ + Heating System + +
+
+ )} + /> + ( + + + + +
+ + Cooling System + +
+
+ )} + /> + ( + + + + +
+ + Hot Water System + +
+
+ )} + /> +
+ + +
+ ( + + Build Date + + + + + + )} + /> + ( + + Decision Period Date + + + + + + )} + /> +
+ +
+ ( + + Block Service Man Count + + + + + + )} + /> + ( + + Security Service Man Count + + + + + + )} + /> + ( + + Total Garage Count In Numbers + + + + + + )} + /> + ( + + Management Room ID Assign + + + + + + )} + /> +
+ + + + + ); + +} + +export { BuildupdateForm } diff --git a/frontend/pages/builds/update/page.tsx b/frontend/pages/builds/update/page.tsx new file mode 100644 index 0000000..309b4fd --- /dev/null +++ b/frontend/pages/builds/update/page.tsx @@ -0,0 +1,36 @@ +'use client'; +import { useState } from 'react'; +import { BuildupdateForm } from '@/pages/builds/update/form'; +import { useSearchParams, useRouter } from 'next/navigation' +import { Button } from '@/components/ui/button'; +import { BuildDataTableUpdate } from './table/data-table'; +import { useGraphQlBuildsList } from '../queries'; + +const PageUpdateBuild = () => { + const [page, setPage] = useState(1); + const [limit, setLimit] = useState(10); + const [sort, setSort] = useState({ createdAt: 'desc' }); + const [filters, setFilters] = useState({}); + const searchParams = useSearchParams() + const router = useRouter() + const uuid = searchParams?.get('uuid') || null + const backToBuildAddress = <> +
UUID not found in search params
+ + + if (!uuid) { return backToBuildAddress } + const { data, isLoading, error, refetch } = useGraphQlBuildsList({ limit, skip: (page - 1) * limit, sort, filters: { ...filters, _id: uuid } }); + const initData = data?.data?.[0] || null; + if (!initData) { return backToBuildAddress } + return ( + <> + + + + ) +} + +export { PageUpdateBuild }; diff --git a/frontend/pages/builds/update/queries.tsx b/frontend/pages/builds/update/queries.tsx new file mode 100644 index 0000000..6fd1511 --- /dev/null +++ b/frontend/pages/builds/update/queries.tsx @@ -0,0 +1,26 @@ +'use client' +import { useMutation } from '@tanstack/react-query' +import { UpdateBuildIbansUpdate } from './types'; +import { toISOIfNotZ } from '@/lib/utils'; + +const fetchGraphQlBuildUpdate = async (record: UpdateBuildIbansUpdate, uuid: string): Promise<{ data: UpdateBuildIbansUpdate | null; status: number }> => { + console.log('Fetching test data from local API'); + record.expiryStarts = record.expiryStarts ? toISOIfNotZ(record.expiryStarts) : undefined; + record.expiryEnds = record.expiryEnds ? toISOIfNotZ(record.expiryEnds) : undefined; + record.startDate = toISOIfNotZ(record.startDate); + record.stopDate = toISOIfNotZ(record.stopDate); + try { + const res = await fetch(`/api/build/update?uuid=${uuid || ''}`, { method: 'POST', cache: 'no-store', credentials: "include", body: JSON.stringify(record) }); + if (!res.ok) { const errorText = await res.text(); console.error('Test data API error:', errorText); throw new Error(`API error: ${res.status} ${res.statusText}`) } + const data = await res.json(); + return { data: data.data, status: res.status } + } catch (error) { console.error('Error fetching test data:', error); throw error } +}; + +export function useUpdateBuildMutation() { + return useMutation({ + mutationFn: ({ data, uuid }: { data: UpdateBuildIbansUpdate, uuid: string }) => fetchGraphQlBuildUpdate(data, uuid), + onSuccess: () => { console.log("Build updated successfully") }, + onError: (error) => { console.error("Update Build failed:", error) }, + }) +} diff --git a/frontend/pages/builds/update/schema.ts b/frontend/pages/builds/update/schema.ts new file mode 100644 index 0000000..3bf0f36 --- /dev/null +++ b/frontend/pages/builds/update/schema.ts @@ -0,0 +1,30 @@ +import { z } from "zod" + +export const buildUpdateSchema = z.object({ + buildType: z.object({ + token: z.string(), + typeToken: z.string(), + type: z.string(), + }), + collectionToken: z.string(), + info: z.object({ + govAddressCode: z.string(), + buildName: z.string(), + buildNo: z.string(), + maxFloor: z.number(), + undergroundFloor: z.number(), + buildDate: z.string(), + decisionPeriodDate: z.string(), + taxNo: z.string(), + liftCount: z.number(), + heatingSystem: z.boolean(), + coolingSystem: z.boolean(), + hotWaterSystem: z.boolean(), + blockServiceManCount: z.number(), + securityServiceManCount: z.number(), + garageCount: z.number(), + managementRoomId: z.number(), + }), +}); + +export type BuildUpdate = z.infer; diff --git a/frontend/pages/builds/update/table/columns.tsx b/frontend/pages/builds/update/table/columns.tsx new file mode 100644 index 0000000..6acdc96 --- /dev/null +++ b/frontend/pages/builds/update/table/columns.tsx @@ -0,0 +1,145 @@ +"use client" +import { z } from "zod" +import { Button } from "@/components/ui/button" +import { Drawer, DrawerClose, DrawerContent, DrawerFooter, DrawerHeader, DrawerTrigger } from "@/components/ui/drawer" +import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger } from "@/components/ui/dropdown-menu" +import { Input } from "@/components/ui/input" +import { Label } from "@/components/ui/label" +import { useSortable } from "@dnd-kit/sortable" +import { IconGripVertical } from "@tabler/icons-react" +import { useIsMobile } from "@/hooks/use-mobile" +import { Separator } from "@/components/ui/separator" +import { ColumnDef, flexRender, Row } from "@tanstack/react-table" +import { TableCell, TableRow } from "@/components/ui/table" +import { CSS } from "@dnd-kit/utilities" +import { schema, schemaType } from "./schema" +import { dateToLocaleString } from "@/lib/utils" +import { Pencil, Trash } from "lucide-react" + +export function DraggableRow({ row }: { row: Row> }) { + const { transform, transition, setNodeRef, isDragging } = useSortable({ id: row.original._id }) + return ( + + {row.getVisibleCells().map((cell) => ( + {flexRender(cell.column.columnDef.cell, cell.getContext())} + ))} + + ) +} + +function getColumns(router: any, deleteHandler: (id: string) => void): ColumnDef[] { + return [ + { + accessorKey: "buildType.token", + header: "Token", + cell: ({ getValue }) => getValue(), + }, + { + accessorKey: "collectionToken", + header: "Collection Token", + cell: ({ getValue }) => getValue(), + }, + { + accessorKey: "info.govAddressCode", + header: "Gov Address Code", + cell: ({ getValue }) => getValue(), + }, + { + accessorKey: "info.buildName", + header: "Build Name", + cell: ({ getValue }) => getValue(), + }, + { + accessorKey: "info.buildNo", + header: "Build No", + cell: ({ getValue }) => getValue(), + }, + { + accessorKey: "info.maxFloor", + header: "Max Floor", + cell: ({ getValue }) => getValue(), + }, + { + accessorKey: "info.undergroundFloor", + header: "Underground Floor", + cell: ({ getValue }) => getValue(), + }, + { + accessorKey: "info.buildDate", + header: "Build Date", + cell: ({ getValue }) => getValue(), + }, + { + accessorKey: "info.decisionPeriodDate", + header: "Decision Period Date", + cell: ({ getValue }) => getValue(), + }, + { + accessorKey: "info.taxNo", + header: "Tax No", + cell: ({ getValue }) => getValue(), + }, + { + accessorKey: "info.liftCount", + header: "Lift Count", + cell: ({ getValue }) => getValue(), + }, + { + accessorKey: "info.heatingSystem", + header: "Heating System", + cell: ({ getValue }) => getValue(), + }, + { + accessorKey: "info.coolingSystem", + header: "Cooling System", + cell: ({ getValue }) => getValue(), + }, + { + accessorKey: "info.hotWaterSystem", + header: "Hot Water System", + cell: ({ getValue }) => getValue(), + }, + { + accessorKey: "info.blockServiceManCount", + header: "Block Service Man Count", + cell: ({ getValue }) => getValue(), + }, + { + accessorKey: "info.securityServiceManCount", + header: "Security Service Man Count", + cell: ({ getValue }) => getValue(), + }, + { + accessorKey: "info.garageCount", + header: "Garage Count", + cell: ({ getValue }) => getValue(), + }, + { + accessorKey: "info.managementRoomId", + header: "Management Room ID", + cell: ({ getValue }) => getValue(), + }, + { + id: "actions", + header: "Actions", + cell: ({ row }) => { + return ( +
+ + +
+ ); + }, + } + ] +} + + +export { getColumns }; \ No newline at end of file diff --git a/frontend/pages/builds/update/table/data-table.tsx b/frontend/pages/builds/update/table/data-table.tsx new file mode 100644 index 0000000..b6084a8 --- /dev/null +++ b/frontend/pages/builds/update/table/data-table.tsx @@ -0,0 +1,279 @@ +"use client" + +import * as React from "react" +import { + closestCenter, + DndContext, + KeyboardSensor, + MouseSensor, + TouchSensor, + useSensor, + useSensors, + type UniqueIdentifier, +} from "@dnd-kit/core" +import { restrictToVerticalAxis } from "@dnd-kit/modifiers" +import { + SortableContext, + verticalListSortingStrategy, +} from "@dnd-kit/sortable" +import { + IconChevronDown, + IconChevronLeft, + IconChevronRight, + IconChevronsLeft, + IconChevronsRight, + IconLayoutColumns, +} from "@tabler/icons-react" +import { + ColumnFiltersState, + flexRender, + getCoreRowModel, + getFacetedRowModel, + getFacetedUniqueValues, + getFilteredRowModel, + getSortedRowModel, + SortingState, + useReactTable, + VisibilityState, +} from "@tanstack/react-table" + +import { Button } from "@/components/ui/button" +import { + DropdownMenu, + DropdownMenuCheckboxItem, + DropdownMenuContent, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu" +import { Label } from "@/components/ui/label" +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select" +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table" +import { + Tabs, + TabsContent, + TabsList, + TabsTrigger, +} from "@/components/ui/tabs" +import { schemaType } from "./schema" +import { getColumns, DraggableRow } from "./columns" +import { useRouter } from "next/navigation" +import { Home } from "lucide-react" +import { useDeleteBuildMutation } from "@/pages/builds/queries" + +export function BuildDataTableUpdate({ + data, + totalCount, + currentPage, + pageSize, + onPageChange, + onPageSizeChange, + refetchTable, +}: { + data: schemaType[], + totalCount: number, + currentPage: number, + pageSize: number, + onPageChange: (page: number) => void, + onPageSizeChange: (size: number) => void, + refetchTable: () => void +}) { + + const router = useRouter(); + const [rowSelection, setRowSelection] = React.useState({}) + const [columnVisibility, setColumnVisibility] = React.useState({}) + const [columnFilters, setColumnFilters] = React.useState([]) + const [sorting, setSorting] = React.useState([]) + const sortableId = React.useId() + const sensors = useSensors(useSensor(MouseSensor, {}), useSensor(TouchSensor, {}), useSensor(KeyboardSensor, {})) + const dataIds = React.useMemo(() => data?.map(({ _id }) => _id) || [], [data]) + + const deleteMutation = useDeleteBuildMutation() + const deleteHandler = (id: string) => { deleteMutation.mutate({ uuid: id }); setTimeout(() => { refetchTable() }, 200) } + const columns = getColumns(router, deleteHandler); + const pagination = React.useMemo(() => ({ pageIndex: currentPage - 1, pageSize: pageSize, }), [currentPage, pageSize]) + const totalPages = Math.ceil(totalCount / pageSize) + + const table = useReactTable({ + data, + columns, + pageCount: totalPages, + state: { sorting, columnVisibility, rowSelection, columnFilters, pagination }, + manualPagination: true, + enableRowSelection: true, + getRowId: (row) => row._id.toString(), + onRowSelectionChange: setRowSelection, + onSortingChange: setSorting, + onColumnFiltersChange: setColumnFilters, + onColumnVisibilityChange: setColumnVisibility, + onPaginationChange: (updater) => { + const nextPagination = typeof updater === "function" ? updater(pagination) : updater; + onPageChange(nextPagination.pageIndex + 1); onPageSizeChange(nextPagination.pageSize); + }, + getCoreRowModel: getCoreRowModel(), + getFilteredRowModel: getFilteredRowModel(), + getSortedRowModel: getSortedRowModel(), + getFacetedRowModel: getFacetedRowModel(), + getFacetedUniqueValues: getFacetedUniqueValues(), + }) + + const handlePageSizeChange = (value: string) => { const newSize = Number(value); onPageSizeChange(newSize); onPageChange(1) } + + return ( + +
+ + +
+ + + + + + {table.getAllColumns().filter((column) => typeof column.accessorFn !== "undefined" && column.getCanHide()).map((column) => { + return ( + column.toggleVisibility(!!value)} > + {column.id} + + ) + })} + + + +
+
+ +
+ + + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + return ( + + {header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())} + + ) + })} + + ))} + + + {table.getRowModel().rows?.length ? ( + {table.getRowModel().rows.map((row) => )} + ) : ( + No results. + )} + +
+
+
+
+
+ {table.getFilteredSelectedRowModel().rows.length} of{" "} + {table.getFilteredRowModel().rows.length} row(s) selected. +
+
+
+ + +
+
+ Page {currentPage} of {totalPages} +
+
+ Total Count: {totalCount} +
+
+ + + + + + + +
+
+
+
+ +
+
+ +
+
+ +
+
+
+ ) +} \ No newline at end of file diff --git a/frontend/pages/builds/update/table/schema.tsx b/frontend/pages/builds/update/table/schema.tsx new file mode 100644 index 0000000..226f114 --- /dev/null +++ b/frontend/pages/builds/update/table/schema.tsx @@ -0,0 +1,31 @@ +import { z } from "zod"; + +export const schema = z.object({ + _id: z.string(), + buildType: z.object({ + token: z.string(), + typeToken: z.string(), + type: z.string(), + }), + collectionToken: z.string(), + info: z.object({ + govAddressCode: z.string(), + buildName: z.string(), + buildNo: z.string(), + maxFloor: z.number(), + undergroundFloor: z.number(), + buildDate: z.string(), + decisionPeriodDate: z.string(), + taxNo: z.string(), + liftCount: z.number(), + heatingSystem: z.boolean(), + coolingSystem: z.boolean(), + hotWaterSystem: z.boolean(), + blockServiceManCount: z.number(), + securityServiceManCount: z.number(), + garageCount: z.number(), + managementRoomId: z.number(), + }), +}); + +export type schemaType = z.infer; diff --git a/frontend/pages/builds/update/types.ts b/frontend/pages/builds/update/types.ts new file mode 100644 index 0000000..1a88475 --- /dev/null +++ b/frontend/pages/builds/update/types.ts @@ -0,0 +1,13 @@ + +interface UpdateBuildIbansUpdate { + + iban: string; + startDate: string; + stopDate: string; + bankCode: string; + xcomment: string; + expiryStarts?: string; + expiryEnds?: string; +} + +export type { UpdateBuildIbansUpdate }; \ No newline at end of file