From 688576a1de804e271baffb2c4c9b8f764a61467e Mon Sep 17 00:00:00 2001 From: Berkay Date: Tue, 18 Nov 2025 16:25:03 +0300 Subject: [PATCH] build-address added tested --- backend/src/app.module.ts | 2 + .../src/build-address/build-address.module.ts | 11 + .../build-address.resolver.spec.ts | 18 + .../build-address/build-address.resolver.ts | 37 +++ .../build-address.service.spec.ts | 18 + .../build-address/build-address.service.ts | 31 ++ .../dto/create-build-address.input.ts | 32 ++ .../dto/list-build-address.response.ts | 14 + .../dto/update-build-address.input.ts | 34 ++ backend/src/dto/list.input.ts | 1 + backend/src/models/build-address.model.ts | 11 +- .../src/people/dto/list-people.response.ts | 2 + backend/src/users/users.module.ts | 4 +- frontend/README.md | 45 +-- frontend/app/api/a.txt | 0 frontend/app/api/build-address/add/route.ts | 36 ++ frontend/app/api/build-address/add/schema.ts | 15 + .../app/api/build-address/delete/route.ts | 23 ++ frontend/app/api/build-address/list/route.ts | 43 +++ .../app/api/build-address/update/route.ts | 39 +++ .../app/api/build-address/update/schema.ts | 15 + frontend/app/api/build-types/add/route.ts | 30 ++ frontend/app/api/build-types/add/schema.ts | 10 + frontend/app/api/build-types/delete/route.ts | 23 ++ frontend/app/api/build-types/list/route.ts | 37 +++ frontend/app/api/build-types/update/route.ts | 33 ++ frontend/app/api/build-types/update/schema.ts | 22 ++ frontend/app/api/people/add/route.ts | 39 +++ frontend/app/api/people/add/schema.ts | 22 ++ frontend/app/api/people/delete/route.ts | 23 ++ frontend/app/api/people/list/route.ts | 48 +++ frontend/app/api/people/update/route.ts | 34 ++ frontend/app/api/people/update/schema.ts | 22 ++ frontend/app/build-address/add/page.tsx | 7 + frontend/app/build-address/page.tsx | 7 + frontend/app/build-address/update/page.tsx | 7 + frontend/app/build-types/add/page.tsx | 5 + frontend/app/build-types/page.tsx | 5 + frontend/app/build-types/update/page.tsx | 5 + frontend/app/people/add/page.tsx | 5 + frontend/app/people/page.tsx | 9 +- frontend/app/people/update/page.tsx | 5 + frontend/components/dashboard/nav-main.tsx | 53 +-- frontend/components/sidebar/app-sidebar.tsx | 177 +++++----- frontend/components/ui/date-time-picker.tsx | 4 +- frontend/lib/utils.ts | 18 +- frontend/pages/build-address/add/form.tsx | 250 ++++++++++++++ frontend/pages/build-address/add/page.tsx | 26 ++ frontend/pages/build-address/add/queries.tsx | 22 ++ frontend/pages/build-address/add/schema.ts | 17 + .../pages/build-address/add/table/columns.tsx | 124 +++++++ .../build-address/add/table/data-table.tsx | 282 ++++++++++++++++ .../pages/build-address/add/table/schema.tsx | 24 ++ frontend/pages/build-address/add/types.ts | 24 ++ frontend/pages/build-address/list/columns.tsx | 108 ++++++ .../pages/build-address/list/data-table.tsx | 270 +++++++++++++++ frontend/pages/build-address/list/schema.tsx | 20 ++ frontend/pages/build-address/page.tsx | 25 ++ frontend/pages/build-address/queries.tsx | 36 ++ frontend/pages/build-address/types.ts | 20 ++ frontend/pages/build-address/update/form.tsx | 238 ++++++++++++++ frontend/pages/build-address/update/page.tsx | 36 ++ .../pages/build-address/update/queries.tsx | 21 ++ frontend/pages/build-address/update/schema.ts | 17 + .../build-address/update/table/columns.tsx | 225 +++++++++++++ .../build-address/update/table/data-table.tsx | 279 ++++++++++++++++ .../build-address/update/table/schema.tsx | 47 +++ frontend/pages/build-address/update/types.ts | 17 + frontend/pages/build-types/add/form.tsx | 144 ++++++++ frontend/pages/build-types/add/page.tsx | 26 ++ frontend/pages/build-types/add/queries.tsx | 25 ++ frontend/pages/build-types/add/schema.ts | 12 + .../pages/build-types/add/table/columns.tsx | 250 ++++++++++++++ .../build-types/add/table/data-table.tsx | 287 ++++++++++++++++ .../pages/build-types/add/table/schema.tsx | 24 ++ frontend/pages/build-types/add/types.ts | 14 + frontend/pages/build-types/list/columns.tsx | 88 +++++ .../pages/build-types/list/data-table.tsx | 270 +++++++++++++++ frontend/pages/build-types/list/schema.tsx | 16 + frontend/pages/build-types/page.tsx | 25 ++ frontend/pages/build-types/queries.tsx | 36 ++ frontend/pages/build-types/types.ts | 9 + frontend/pages/build-types/update/form.tsx | 135 ++++++++ frontend/pages/build-types/update/page.tsx | 36 ++ frontend/pages/build-types/update/queries.tsx | 21 ++ frontend/pages/build-types/update/schema.ts | 12 + .../build-types/update/table/columns.tsx | 206 ++++++++++++ .../build-types/update/table/data-table.tsx | 280 ++++++++++++++++ .../pages/build-types/update/table/schema.tsx | 47 +++ frontend/pages/build-types/update/types.ts | 10 + frontend/pages/builds/list/columns.tsx | 250 ++++++++++++++ frontend/pages/builds/list/data-table.tsx | 280 ++++++++++++++++ frontend/pages/builds/list/schema.tsx | 27 ++ frontend/pages/builds/page.tsx | 23 ++ frontend/pages/builds/queries.tsx | 36 ++ frontend/pages/builds/types.ts | 44 +++ frontend/pages/people/add/form.tsx | 309 ++++++++++++++++++ frontend/pages/people/add/page.tsx | 26 ++ frontend/pages/people/add/queries.tsx | 25 ++ frontend/pages/people/add/schema.ts | 22 ++ frontend/pages/people/add/table/columns.tsx | 105 ++++++ .../pages/people/add/table/data-table.tsx | 287 ++++++++++++++++ frontend/pages/people/add/table/schema.tsx | 24 ++ frontend/pages/people/add/types.ts | 24 ++ frontend/pages/people/list/columns.tsx | 149 +++++++++ frontend/pages/people/list/data-table.tsx | 280 ++++++++++++++++ frontend/pages/people/list/schema.tsx | 27 ++ frontend/pages/people/page.tsx | 22 ++ frontend/pages/people/queries.tsx | 37 +++ frontend/pages/people/types.ts | 31 ++ frontend/pages/people/update/form.tsx | 294 +++++++++++++++++ frontend/pages/people/update/page.tsx | 36 ++ frontend/pages/people/update/queries.tsx | 21 ++ frontend/pages/people/update/schema.ts | 22 ++ .../pages/people/update/table/columns.tsx | 251 ++++++++++++++ .../pages/people/update/table/data-table.tsx | 280 ++++++++++++++++ frontend/pages/people/update/table/schema.tsx | 47 +++ frontend/pages/people/update/types.ts | 24 ++ frontend/pages/users/add/form.tsx | 15 +- frontend/pages/users/add/queries.tsx | 3 + frontend/pages/users/queries.tsx | 2 - frontend/pages/users/update/form.tsx | 9 +- frontend/pages/users/update/table/columns.tsx | 227 +++---------- .../pages/users/update/table/data-table.tsx | 8 +- src/build-iban/build-iban.module.ts | 8 + src/build-iban/build-iban.resolver.spec.ts | 18 + src/build-iban/build-iban.resolver.ts | 4 + src/build-iban/build-iban.service.spec.ts | 18 + src/build-iban/build-iban.service.ts | 4 + 129 files changed, 8116 insertions(+), 385 deletions(-) create mode 100644 backend/src/build-address/build-address.module.ts create mode 100644 backend/src/build-address/build-address.resolver.spec.ts create mode 100644 backend/src/build-address/build-address.resolver.ts create mode 100644 backend/src/build-address/build-address.service.spec.ts create mode 100644 backend/src/build-address/build-address.service.ts create mode 100644 backend/src/build-address/dto/create-build-address.input.ts create mode 100644 backend/src/build-address/dto/list-build-address.response.ts create mode 100644 backend/src/build-address/dto/update-build-address.input.ts delete mode 100644 frontend/app/api/a.txt create mode 100644 frontend/app/api/build-address/add/route.ts create mode 100644 frontend/app/api/build-address/add/schema.ts create mode 100644 frontend/app/api/build-address/delete/route.ts create mode 100644 frontend/app/api/build-address/list/route.ts create mode 100644 frontend/app/api/build-address/update/route.ts create mode 100644 frontend/app/api/build-address/update/schema.ts create mode 100644 frontend/app/api/build-types/add/route.ts create mode 100644 frontend/app/api/build-types/add/schema.ts create mode 100644 frontend/app/api/build-types/delete/route.ts create mode 100644 frontend/app/api/build-types/list/route.ts create mode 100644 frontend/app/api/build-types/update/route.ts create mode 100644 frontend/app/api/build-types/update/schema.ts create mode 100644 frontend/app/api/people/add/route.ts create mode 100644 frontend/app/api/people/add/schema.ts create mode 100644 frontend/app/api/people/delete/route.ts create mode 100644 frontend/app/api/people/list/route.ts create mode 100644 frontend/app/api/people/update/route.ts create mode 100644 frontend/app/api/people/update/schema.ts create mode 100644 frontend/app/build-address/add/page.tsx create mode 100644 frontend/app/build-address/page.tsx create mode 100644 frontend/app/build-address/update/page.tsx create mode 100644 frontend/app/build-types/add/page.tsx create mode 100644 frontend/app/build-types/page.tsx create mode 100644 frontend/app/build-types/update/page.tsx create mode 100644 frontend/app/people/add/page.tsx create mode 100644 frontend/app/people/update/page.tsx create mode 100644 frontend/pages/build-address/add/form.tsx create mode 100644 frontend/pages/build-address/add/page.tsx create mode 100644 frontend/pages/build-address/add/queries.tsx create mode 100644 frontend/pages/build-address/add/schema.ts create mode 100644 frontend/pages/build-address/add/table/columns.tsx create mode 100644 frontend/pages/build-address/add/table/data-table.tsx create mode 100644 frontend/pages/build-address/add/table/schema.tsx create mode 100644 frontend/pages/build-address/add/types.ts create mode 100644 frontend/pages/build-address/list/columns.tsx create mode 100644 frontend/pages/build-address/list/data-table.tsx create mode 100644 frontend/pages/build-address/list/schema.tsx create mode 100644 frontend/pages/build-address/page.tsx create mode 100644 frontend/pages/build-address/queries.tsx create mode 100644 frontend/pages/build-address/types.ts create mode 100644 frontend/pages/build-address/update/form.tsx create mode 100644 frontend/pages/build-address/update/page.tsx create mode 100644 frontend/pages/build-address/update/queries.tsx create mode 100644 frontend/pages/build-address/update/schema.ts create mode 100644 frontend/pages/build-address/update/table/columns.tsx create mode 100644 frontend/pages/build-address/update/table/data-table.tsx create mode 100644 frontend/pages/build-address/update/table/schema.tsx create mode 100644 frontend/pages/build-address/update/types.ts create mode 100644 frontend/pages/build-types/add/form.tsx create mode 100644 frontend/pages/build-types/add/page.tsx create mode 100644 frontend/pages/build-types/add/queries.tsx create mode 100644 frontend/pages/build-types/add/schema.ts create mode 100644 frontend/pages/build-types/add/table/columns.tsx create mode 100644 frontend/pages/build-types/add/table/data-table.tsx create mode 100644 frontend/pages/build-types/add/table/schema.tsx create mode 100644 frontend/pages/build-types/add/types.ts create mode 100644 frontend/pages/build-types/list/columns.tsx create mode 100644 frontend/pages/build-types/list/data-table.tsx create mode 100644 frontend/pages/build-types/list/schema.tsx create mode 100644 frontend/pages/build-types/page.tsx create mode 100644 frontend/pages/build-types/queries.tsx create mode 100644 frontend/pages/build-types/types.ts create mode 100644 frontend/pages/build-types/update/form.tsx create mode 100644 frontend/pages/build-types/update/page.tsx create mode 100644 frontend/pages/build-types/update/queries.tsx create mode 100644 frontend/pages/build-types/update/schema.ts create mode 100644 frontend/pages/build-types/update/table/columns.tsx create mode 100644 frontend/pages/build-types/update/table/data-table.tsx create mode 100644 frontend/pages/build-types/update/table/schema.tsx create mode 100644 frontend/pages/build-types/update/types.ts create mode 100644 frontend/pages/builds/list/columns.tsx create mode 100644 frontend/pages/builds/list/data-table.tsx create mode 100644 frontend/pages/builds/list/schema.tsx create mode 100644 frontend/pages/builds/page.tsx create mode 100644 frontend/pages/builds/queries.tsx create mode 100644 frontend/pages/builds/types.ts create mode 100644 frontend/pages/people/add/form.tsx create mode 100644 frontend/pages/people/add/page.tsx create mode 100644 frontend/pages/people/add/queries.tsx create mode 100644 frontend/pages/people/add/schema.ts create mode 100644 frontend/pages/people/add/table/columns.tsx create mode 100644 frontend/pages/people/add/table/data-table.tsx create mode 100644 frontend/pages/people/add/table/schema.tsx create mode 100644 frontend/pages/people/add/types.ts create mode 100644 frontend/pages/people/list/columns.tsx create mode 100644 frontend/pages/people/list/data-table.tsx create mode 100644 frontend/pages/people/list/schema.tsx create mode 100644 frontend/pages/people/page.tsx create mode 100644 frontend/pages/people/queries.tsx create mode 100644 frontend/pages/people/types.ts create mode 100644 frontend/pages/people/update/form.tsx create mode 100644 frontend/pages/people/update/page.tsx create mode 100644 frontend/pages/people/update/queries.tsx create mode 100644 frontend/pages/people/update/schema.ts create mode 100644 frontend/pages/people/update/table/columns.tsx create mode 100644 frontend/pages/people/update/table/data-table.tsx create mode 100644 frontend/pages/people/update/table/schema.tsx create mode 100644 frontend/pages/people/update/types.ts create mode 100644 src/build-iban/build-iban.module.ts create mode 100644 src/build-iban/build-iban.resolver.spec.ts create mode 100644 src/build-iban/build-iban.resolver.ts create mode 100644 src/build-iban/build-iban.service.spec.ts create mode 100644 src/build-iban/build-iban.service.ts diff --git a/backend/src/app.module.ts b/backend/src/app.module.ts index 4fc384a..62bbc4d 100644 --- a/backend/src/app.module.ts +++ b/backend/src/app.module.ts @@ -11,6 +11,7 @@ import { BuildPartsModule } from './build-parts/build-parts.module'; import { BuildAreaModule } from './build-area/build-area.module'; import { UserTypesModule } from './user-types/user-types.module'; import { BuildTypesModule } from './build-types/build-types.module'; +import { BuildAddressModule } from './build-address/build-address.module'; @Module({ imports: [ @@ -27,6 +28,7 @@ import { BuildTypesModule } from './build-types/build-types.module'; BuildAreaModule, UserTypesModule, BuildTypesModule, + BuildAddressModule, ], controllers: [AppController], providers: [AppService], diff --git a/backend/src/build-address/build-address.module.ts b/backend/src/build-address/build-address.module.ts new file mode 100644 index 0000000..67862b7 --- /dev/null +++ b/backend/src/build-address/build-address.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { BuildAddressService } from './build-address.service'; +import { BuildAddressResolver } from './build-address.resolver'; +import { MongooseModule } from '@nestjs/mongoose'; +import { BuildAddress, BuildAddressSchema } from '@/models/build-address.model'; + +@Module({ + imports: [MongooseModule.forFeature([{ name: BuildAddress.name, schema: BuildAddressSchema }])], + providers: [BuildAddressService, BuildAddressResolver] +}) +export class BuildAddressModule { } diff --git a/backend/src/build-address/build-address.resolver.spec.ts b/backend/src/build-address/build-address.resolver.spec.ts new file mode 100644 index 0000000..4c61ad1 --- /dev/null +++ b/backend/src/build-address/build-address.resolver.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { BuildAddressResolver } from './build-address.resolver'; + +describe('BuildAddressResolver', () => { + let resolver: BuildAddressResolver; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [BuildAddressResolver], + }).compile(); + + resolver = module.get(BuildAddressResolver); + }); + + it('should be defined', () => { + expect(resolver).toBeDefined(); + }); +}); diff --git a/backend/src/build-address/build-address.resolver.ts b/backend/src/build-address/build-address.resolver.ts new file mode 100644 index 0000000..21a579a --- /dev/null +++ b/backend/src/build-address/build-address.resolver.ts @@ -0,0 +1,37 @@ +import { Resolver, Query, Args, ID, Info, Mutation } from '@nestjs/graphql'; +import { Types } from 'mongoose'; +import { BuildAddress } from '@/models/build-address.model'; +import { ListArguments } from '@/dto/list.input'; +import { UpdateBuildAddressInput } from './dto/update-build-address.input'; +import { ListBuildAddressInput } from './dto/list-build-address.response'; +import { CreateBuildAddressInput } from './dto/create-build-address.input'; +import graphqlFields from 'graphql-fields'; +import { BuildAddressService } from './build-address.service'; +import type { GraphQLResolveInfo } from 'graphql'; + +@Resolver() +export class BuildAddressResolver { + + constructor(private readonly buildAddressService: BuildAddressService) { } + + @Query(() => ListBuildAddressInput, { name: "buildAddresses" }) + async getBuildAddresses(@Info() info: GraphQLResolveInfo, @Args('input') input: ListArguments): Promise { + const fields = graphqlFields(info); const projection = this.buildAddressService.buildProjection(fields?.data); const { skip, limit, sort, filters } = input; + return await this.buildAddressService.findAll(projection, skip, limit, sort, filters); + } + + @Query(() => BuildAddress, { name: 'buildAddress', nullable: true }) + async getBuildAddress(@Args('id', { type: () => ID }) id: string, @Info() info: GraphQLResolveInfo): Promise { + const fields = graphqlFields(info); const projection = this.buildAddressService.buildProjection(fields); return this.buildAddressService.findById(new Types.ObjectId(id), projection); + } + + @Mutation(() => BuildAddress, { name: 'createBuildAddress' }) + async createBuildAddress(@Args('input') input: CreateBuildAddressInput): Promise { return this.buildAddressService.create(input) } + + @Mutation(() => BuildAddress, { name: 'updateBuildAddress' }) + async updateBuildAddress(@Args('uuid') uuid: string, @Args('input') input: UpdateBuildAddressInput): Promise { return this.buildAddressService.update(uuid, input) } + + @Mutation(() => Boolean, { name: 'deleteBuildAddress' }) + async deleteBuildAddress(@Args('uuid') uuid: string): Promise { return this.buildAddressService.delete(uuid) } + +} diff --git a/backend/src/build-address/build-address.service.spec.ts b/backend/src/build-address/build-address.service.spec.ts new file mode 100644 index 0000000..f92dee6 --- /dev/null +++ b/backend/src/build-address/build-address.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { BuildAddressService } from './build-address.service'; + +describe('BuildAddressService', () => { + let service: BuildAddressService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [BuildAddressService], + }).compile(); + + service = module.get(BuildAddressService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/backend/src/build-address/build-address.service.ts b/backend/src/build-address/build-address.service.ts new file mode 100644 index 0000000..994ef71 --- /dev/null +++ b/backend/src/build-address/build-address.service.ts @@ -0,0 +1,31 @@ +import { Injectable } from '@nestjs/common'; +import { UpdateBuildAddressInput } from './dto/update-build-address.input'; +import { ListBuildAddressInput } from './dto/list-build-address.response'; +import { CreateBuildAddressInput } from './dto/create-build-address.input'; +import { InjectModel } from '@nestjs/mongoose'; +import { Types, Model } from 'mongoose'; +import { BuildAddress, BuildAddressDocument } from '@/models/build-address.model'; + +@Injectable() +export class BuildAddressService { + + constructor(@InjectModel(BuildAddress.name) private readonly buildAddressModel: 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.buildAddressModel.countDocuments(query).exec(); + const data = await this.buildAddressModel.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.buildAddressModel.findById(id, projection, { lean: false }).populate({ path: 'person', select: projection?.person }).exec() } + + async create(input: CreateBuildAddressInput): Promise { const user = new this.buildAddressModel(input); return user.save() } + + async update(uuid: string, input: UpdateBuildAddressInput): Promise { const user = await this.buildAddressModel.findOne({ uuid }); if (!user) { throw new Error('User not found') }; user.set(input); console.dir({ uuid, input }, { depth: null }); return user.save() } + + async delete(uuid: string): Promise { const user = await this.buildAddressModel.deleteMany({ uuid }); return user.deletedCount > 0 } + + buildProjection(fields: Record): Record { const projection: Record = {}; for (const key in fields) { projection[key] = 1 }; return projection } + +} diff --git a/backend/src/build-address/dto/create-build-address.input.ts b/backend/src/build-address/dto/create-build-address.input.ts new file mode 100644 index 0000000..8104966 --- /dev/null +++ b/backend/src/build-address/dto/create-build-address.input.ts @@ -0,0 +1,32 @@ +import { InputType, Field, Float, ID } from "@nestjs/graphql"; +import { Types } from "mongoose"; + +@InputType() +export class CreateBuildAddressInput { + @Field() + buildNumber: string; + + @Field() + doorNumber: string; + + @Field() + floorNumber: string; + + @Field() + commentAddress: string; + + @Field() + letterAddress: string; + + @Field() + shortLetterAddress: string; + + @Field(() => Float) + latitude: number; + + @Field(() => Float) + longitude: number; + + @Field(() => ID, { nullable: true }) + street?: Types.ObjectId; +} diff --git a/backend/src/build-address/dto/list-build-address.response.ts b/backend/src/build-address/dto/list-build-address.response.ts new file mode 100644 index 0000000..047775a --- /dev/null +++ b/backend/src/build-address/dto/list-build-address.response.ts @@ -0,0 +1,14 @@ +import { ObjectType, Field, Int } from "@nestjs/graphql"; +import { BuildAddress } from "@/models/build-address.model"; + +@ObjectType() +export class ListBuildAddressInput { + + @Field(() => [BuildAddress], { nullable: true }) + data?: BuildAddress[]; + + @Field(() => Int, { nullable: true }) + totalCount?: number; + +} + diff --git a/backend/src/build-address/dto/update-build-address.input.ts b/backend/src/build-address/dto/update-build-address.input.ts new file mode 100644 index 0000000..927584e --- /dev/null +++ b/backend/src/build-address/dto/update-build-address.input.ts @@ -0,0 +1,34 @@ +import { InputType, Field, Float, ID } from "@nestjs/graphql"; +import { Types } from "mongoose"; + +@InputType() +export class UpdateBuildAddressInput { + + @Field({ nullable: true }) + buildNumber?: string; + + @Field({ nullable: true }) + doorNumber?: string; + + @Field({ nullable: true }) + floorNumber?: string; + + @Field({ nullable: true }) + commentAddress?: string; + + @Field({ nullable: true }) + letterAddress?: string; + + @Field({ nullable: true }) + shortLetterAddress?: string; + + @Field(() => Float, { nullable: true }) + latitude?: number; + + @Field(() => Float, { nullable: true }) + longitude?: number; + + @Field(() => ID, { nullable: true }) + street?: Types.ObjectId; + +} diff --git a/backend/src/dto/list.input.ts b/backend/src/dto/list.input.ts index d317a63..fa76663 100644 --- a/backend/src/dto/list.input.ts +++ b/backend/src/dto/list.input.ts @@ -3,6 +3,7 @@ import { Field, Int, InputType, ObjectType } from '@nestjs/graphql'; @InputType() export class ListArguments { + @Field(() => GraphQLJSONObject, { nullable: true }) filters?: any; diff --git a/backend/src/models/build-address.model.ts b/backend/src/models/build-address.model.ts index 289e14d..37d569b 100644 --- a/backend/src/models/build-address.model.ts +++ b/backend/src/models/build-address.model.ts @@ -6,6 +6,10 @@ import { Base } from '@/models/base.model'; @ObjectType() @Schema({ timestamps: true }) export class BuildAddress extends Base { + + @Field(() => ID) + readonly _id: string; + @Field() @Prop({ required: true }) buildNumber: string; @@ -38,9 +42,10 @@ export class BuildAddress extends Base { @Prop({ required: true }) longitude: number; - @Field(() => ID) - @Prop({ type: Types.ObjectId, ref: 'Street', required: true }) - street: Types.ObjectId; + @Field(() => ID, { nullable: true }) + @Prop({ type: Types.ObjectId, ref: 'Street', required: false }) + street?: Types.ObjectId; + } export type BuildAddressDocument = BuildAddress & Document; diff --git a/backend/src/people/dto/list-people.response.ts b/backend/src/people/dto/list-people.response.ts index 75f2dd5..072ff34 100644 --- a/backend/src/people/dto/list-people.response.ts +++ b/backend/src/people/dto/list-people.response.ts @@ -3,9 +3,11 @@ import { Person } from "@/models/person.model"; @ObjectType() export class ListPeopleResponse { + @Field(() => [Person], { nullable: true }) data?: Person[]; @Field(() => Int, { nullable: true }) totalCount?: number; + } diff --git a/backend/src/users/users.module.ts b/backend/src/users/users.module.ts index 28c9716..e57ac58 100644 --- a/backend/src/users/users.module.ts +++ b/backend/src/users/users.module.ts @@ -5,9 +5,7 @@ import { UsersService } from './users.service'; import { UserSchema, User } from '@/models/user.model'; @Module({ - imports: [ - MongooseModule.forFeature([{ name: User.name, schema: UserSchema }]) - ], + imports: [MongooseModule.forFeature([{ name: User.name, schema: UserSchema }])], providers: [UsersService, UsersResolver], }) export class UsersModule { } diff --git a/frontend/README.md b/frontend/README.md index aa24e9a..d1d4503 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -1,38 +1,9 @@ -This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). +# ToDo's -## Getting Started - -First, run the development server: - -```bash -npm run dev -# or -yarn dev -# or -pnpm dev -# or -bun dev -``` - -Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. - -You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. - -This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. - -## Learn More - -To learn more about Next.js, take a look at the following resources: - -- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. -- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. - -You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! - -## Deploy on Vercel - -The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. - -Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. - -new Date('2025-11-14T18:21:35.212Z').toLocaleString("en-US", { timeZone: "Europe/Istanbul", hour12: false, year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", second: "2-digit" }); +Build: +BuildPart: +Build Living Space: +People: +User: +Company: +ADD UPDATE yapan page: diff --git a/frontend/app/api/a.txt b/frontend/app/api/a.txt deleted file mode 100644 index e69de29..0000000 diff --git a/frontend/app/api/build-address/add/route.ts b/frontend/app/api/build-address/add/route.ts new file mode 100644 index 0000000..6cbc5ef --- /dev/null +++ b/frontend/app/api/build-address/add/route.ts @@ -0,0 +1,36 @@ +'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 CreateBuildAddress($input: CreateBuildAddressInput!) { + createBuildAddress(input: $input) { + _id + buildNumber + doorNumber + floorNumber + commentAddress + letterAddress + shortLetterAddress + latitude + longitude + street + } + } + `; + const variables = { input: validatedBody }; + const data = await client.request(query, variables); + return NextResponse.json({ data: data.createBuildAddress, status: 200 }); + } catch (err: any) { + console.error(err); + return NextResponse.json({ error: err.message }, { status: 500 }); + } +} diff --git a/frontend/app/api/build-address/add/schema.ts b/frontend/app/api/build-address/add/schema.ts new file mode 100644 index 0000000..3260ff2 --- /dev/null +++ b/frontend/app/api/build-address/add/schema.ts @@ -0,0 +1,15 @@ +import { z } from "zod" + +export const buildTypesAddSchema = z.object({ + buildNumber: z.string(), + doorNumber: z.string(), + floorNumber: z.string(), + commentAddress: z.string(), + letterAddress: z.string(), + shortLetterAddress: z.string(), + latitude: z.number(), + longitude: z.number(), + street: z.string().optional(), +}); + +export type BuildTypesAdd = z.infer; diff --git a/frontend/app/api/build-address/delete/route.ts b/frontend/app/api/build-address/delete/route.ts new file mode 100644 index 0000000..ac8d9c3 --- /dev/null +++ b/frontend/app/api/build-address/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 DeleteBuildAddress($uuid: String!) { deleteBuildAddress(uuid: $uuid) }`; + const variables = { uuid: uuid }; + const data = await client.request(query, variables); + return NextResponse.json({ data: data.deleteBuildAddress, status: 200 }); + } catch (err: any) { + console.error(err); + return NextResponse.json({ error: err.message }, { status: 500 }); + } + +} diff --git a/frontend/app/api/build-address/list/route.ts b/frontend/app/api/build-address/list/route.ts new file mode 100644 index 0000000..090742c --- /dev/null +++ b/frontend/app/api/build-address/list/route.ts @@ -0,0 +1,43 @@ +'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 GetBuildAddresses($input: ListArguments!) { + buildAddresses(input: $input) { + totalCount + data { + _id + uuid + buildNumber + doorNumber + floorNumber + commentAddress + letterAddress + shortLetterAddress + latitude + longitude + street + createdAt + updatedAt + expiryStarts + expiryEnds + } + } + }`; + const variables = { input: { limit, skip, sort, filters } }; + console.dir({ variables }) + const data = await client.request(query, variables); + return NextResponse.json({ data: data.buildAddresses.data, totalCount: data.buildAddresses.totalCount }); + } catch (err: any) { + console.error(err); + return NextResponse.json({ error: err.message }, { status: 500 }); + } +} diff --git a/frontend/app/api/build-address/update/route.ts b/frontend/app/api/build-address/update/route.ts new file mode 100644 index 0000000..2877930 --- /dev/null +++ b/frontend/app/api/build-address/update/route.ts @@ -0,0 +1,39 @@ +'use server'; +import { NextResponse } from 'next/server'; +import { GraphQLClient, gql } from 'graphql-request'; +import { UpdateBuildAddressSchema } 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 = UpdateBuildAddressSchema.parse(body); + if (uuid === "") { return NextResponse.json({ error: "UUID is required" }, { status: 400 }) } + 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 + } + } + `; + const variables = { uuid: uuid, input: validatedBody }; + const data = await client.request(query, variables); + return NextResponse.json({ data: data.updateBuildAddress, status: 200 }); + } catch (err: any) { + console.error(err); + return NextResponse.json({ error: err.message }, { status: 500 }); + } +} diff --git a/frontend/app/api/build-address/update/schema.ts b/frontend/app/api/build-address/update/schema.ts new file mode 100644 index 0000000..f86efd4 --- /dev/null +++ b/frontend/app/api/build-address/update/schema.ts @@ -0,0 +1,15 @@ +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(), +}); + +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 new file mode 100644 index 0000000..80d636d --- /dev/null +++ b/frontend/app/api/build-types/add/route.ts @@ -0,0 +1,30 @@ +'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 CreateBuildType($input: CreateBuildTypesInput!) { + createBuildType(input: $input) { + type + token + typeToken + description + } + } + `; + const variables = { input: validatedBody }; + const data = await client.request(query, variables); + return NextResponse.json({ data: data.createBuildType, status: 200 }); + } catch (err: any) { + console.error(err); + return NextResponse.json({ error: err.message }, { status: 500 }); + } +} diff --git a/frontend/app/api/build-types/add/schema.ts b/frontend/app/api/build-types/add/schema.ts new file mode 100644 index 0000000..d231918 --- /dev/null +++ b/frontend/app/api/build-types/add/schema.ts @@ -0,0 +1,10 @@ +import { z } from "zod" + +export const buildTypesAddSchema = z.object({ + type: z.string(), + token: z.string(), + typeToken: z.string(), + description: z.string().optional().default(''), +}); + +export type BuildTypesAdd = z.infer; diff --git a/frontend/app/api/build-types/delete/route.ts b/frontend/app/api/build-types/delete/route.ts new file mode 100644 index 0000000..05f0138 --- /dev/null +++ b/frontend/app/api/build-types/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 DeletePerson($uuid: String!) { deletePerson(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/build-types/list/route.ts b/frontend/app/api/build-types/list/route.ts new file mode 100644 index 0000000..13686be --- /dev/null +++ b/frontend/app/api/build-types/list/route.ts @@ -0,0 +1,37 @@ +'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 GetBuildTypes($input: ListArguments!) { + buildTypes(input: $input) { + totalCount + data { + _id + uuid + type + token + typeToken + description + createdAt + updatedAt + expiryStarts + expiryEnds + } + } + }`; + const variables = { input: { limit, skip, sort, filters } }; + const data = await client.request(query, variables); + return NextResponse.json({ data: data.buildTypes.data, totalCount: data.buildTypes.totalCount }); + } catch (err: any) { + console.error(err); + return NextResponse.json({ error: err.message }, { status: 500 }); + } +} diff --git a/frontend/app/api/build-types/update/route.ts b/frontend/app/api/build-types/update/route.ts new file mode 100644 index 0000000..44f0072 --- /dev/null +++ b/frontend/app/api/build-types/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/build-types/update/schema.ts b/frontend/app/api/build-types/update/schema.ts new file mode 100644 index 0000000..f0b4a37 --- /dev/null +++ b/frontend/app/api/build-types/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/api/people/add/route.ts b/frontend/app/api/people/add/route.ts new file mode 100644 index 0000000..c464611 --- /dev/null +++ b/frontend/app/api/people/add/route.ts @@ -0,0 +1,39 @@ +'use server'; +import { NextResponse } from 'next/server'; +import { GraphQLClient, gql } from 'graphql-request'; +import { personAddSchema } from './schema'; + +const endpoint = "http://localhost:3001/graphql"; + +export async function POST(request: Request) { + const body = await request.json(); + const validatedBody = personAddSchema.parse(body); + try { + const client = new GraphQLClient(endpoint); + const query = gql` + mutation CreatePerson($input: CreatePersonInput!) { + createPerson(input: $input) { + uuid + firstName + surname + birthDate + } + } + `; + const variables = { input: validatedBody }; + const data = await client.request(query, variables); + return NextResponse.json({ data: data.createPerson, status: 200 }); + } catch (err: any) { + console.error(err); + return NextResponse.json({ error: err.message }, { status: 500 }); + } +} + +// export async function POST(request: Request) { + +// const body = await request.json(); +// const validatedBody = userAddSchema.parse(body); +// console.log("VALIDATED") +// console.dir({ validatedBody }) +// return NextResponse.json({ data: validatedBody }) +// } diff --git a/frontend/app/api/people/add/schema.ts b/frontend/app/api/people/add/schema.ts new file mode 100644 index 0000000..f9616ee --- /dev/null +++ b/frontend/app/api/people/add/schema.ts @@ -0,0 +1,22 @@ +import { z } from "zod" + +export const personAddSchema = z.object({ + firstName: z.string(), + surname: z.string(), + middleName: z.string().optional(), + sexCode: z.string(), + personRef: z.string().optional(), + personTag: z.string().optional(), + fatherName: z.string().optional(), + motherName: z.string().optional(), + countryCode: z.string(), + nationalIdentityId: z.string(), + birthPlace: z.string(), + birthDate: z.string(), + taxNo: z.string().optional(), + birthname: z.string().optional(), + expiryStarts: z.string().optional(), + expiryEnds: z.string().optional(), +}); + +export type PeopleAdd = z.infer; diff --git a/frontend/app/api/people/delete/route.ts b/frontend/app/api/people/delete/route.ts new file mode 100644 index 0000000..05f0138 --- /dev/null +++ b/frontend/app/api/people/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 DeletePerson($uuid: String!) { deletePerson(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/people/list/route.ts b/frontend/app/api/people/list/route.ts new file mode 100644 index 0000000..7a3fe93 --- /dev/null +++ b/frontend/app/api/people/list/route.ts @@ -0,0 +1,48 @@ +'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 GetPeople($input: ListArguments!) { + people(input: $input) { + totalCount + data { + _id + uuid + firstName + surname + middleName + sexCode + personRef + personTag + fatherName + motherName + countryCode + nationalIdentityId + birthPlace + birthDate + taxNo + birthname + expiryStarts + expiryEnds + createdAt + updatedAt + } + } + } + `; + const variables = { input: { limit, skip, sort, filters } }; + const data = await client.request(query, variables); + return NextResponse.json({ data: data.people.data, totalCount: data.people.totalCount }); + } catch (err: any) { + console.error(err); + return NextResponse.json({ error: err.message }, { status: 500 }); + } +} diff --git a/frontend/app/api/people/update/route.ts b/frontend/app/api/people/update/route.ts new file mode 100644 index 0000000..ee404d2 --- /dev/null +++ b/frontend/app/api/people/update/route.ts @@ -0,0 +1,34 @@ +'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); + console.dir({ validatedBody }); + if (uuid === "") { return NextResponse.json({ error: "UUID is required" }, { status: 400 }) } + try { + const client = new GraphQLClient(endpoint); + const query = gql` + mutation updatePerson($uuid: String! $input: UpdatePersonInput!) { + updatePerson(uuid: $uuid, input: $input) { + uuid + firstName + surname + sexCode + } + } + `; + const variables = { uuid: uuid, input: validatedBody }; + const data = await client.request(query, variables); + return NextResponse.json({ data: data.updatePerson, status: 200 }); + } catch (err: any) { + console.error(err); + return NextResponse.json({ error: err.message }, { status: 500 }); + } +} diff --git a/frontend/app/api/people/update/schema.ts b/frontend/app/api/people/update/schema.ts new file mode 100644 index 0000000..f0b4a37 --- /dev/null +++ b/frontend/app/api/people/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-address/add/page.tsx b/frontend/app/build-address/add/page.tsx new file mode 100644 index 0000000..e994ce4 --- /dev/null +++ b/frontend/app/build-address/add/page.tsx @@ -0,0 +1,7 @@ +import { PageAddBuildAddress } from "@/pages/build-address/add/page"; + +const BuildAddress = () => { + return +} + +export default BuildAddress; \ No newline at end of file diff --git a/frontend/app/build-address/page.tsx b/frontend/app/build-address/page.tsx new file mode 100644 index 0000000..213e6d5 --- /dev/null +++ b/frontend/app/build-address/page.tsx @@ -0,0 +1,7 @@ +import { PageBuildAddress } from "@/pages/build-address/page"; + +const BuildAddress = () => { + return +} + +export default BuildAddress; \ No newline at end of file diff --git a/frontend/app/build-address/update/page.tsx b/frontend/app/build-address/update/page.tsx new file mode 100644 index 0000000..8561522 --- /dev/null +++ b/frontend/app/build-address/update/page.tsx @@ -0,0 +1,7 @@ +import { PageUpdateBuildAddress } from "@/pages/build-address/update/page"; + +const BuildAddress = () => { + return +} + +export default BuildAddress; \ No newline at end of file diff --git a/frontend/app/build-types/add/page.tsx b/frontend/app/build-types/add/page.tsx new file mode 100644 index 0000000..fa3229d --- /dev/null +++ b/frontend/app/build-types/add/page.tsx @@ -0,0 +1,5 @@ +import { PageAddBuildTypes } from "@/pages/build-types/add/page"; + +const AddBuildTypesPage = () => { return <> } + +export default AddBuildTypesPage; diff --git a/frontend/app/build-types/page.tsx b/frontend/app/build-types/page.tsx new file mode 100644 index 0000000..6cfdaba --- /dev/null +++ b/frontend/app/build-types/page.tsx @@ -0,0 +1,5 @@ +import { PageBuildTypes } from "@/pages/build-types/page"; + +const BuildTypesPage = () => { return <> } + +export default BuildTypesPage; diff --git a/frontend/app/build-types/update/page.tsx b/frontend/app/build-types/update/page.tsx new file mode 100644 index 0000000..83ebd8c --- /dev/null +++ b/frontend/app/build-types/update/page.tsx @@ -0,0 +1,5 @@ +import { PageUpdateBuildTypes } from '@/pages/build-types/update/page'; + +const UpdateBuildTypesPage = async () => { return } + +export default UpdateBuildTypesPage; diff --git a/frontend/app/people/add/page.tsx b/frontend/app/people/add/page.tsx new file mode 100644 index 0000000..31aa502 --- /dev/null +++ b/frontend/app/people/add/page.tsx @@ -0,0 +1,5 @@ +import { PageAddPerson } from "@/pages/people/add/page"; + +const AddPersonPage = () => { return <> } + +export default AddPersonPage; diff --git a/frontend/app/people/page.tsx b/frontend/app/people/page.tsx index 189bb34..ec647cc 100644 --- a/frontend/app/people/page.tsx +++ b/frontend/app/people/page.tsx @@ -1,10 +1,5 @@ +import { PagePeople } from "@/pages/people/page"; -const PeoplePage = () => { - return ( -
-

People

-
- ) -} +const PeoplePage = () => { return <> } export default PeoplePage; diff --git a/frontend/app/people/update/page.tsx b/frontend/app/people/update/page.tsx new file mode 100644 index 0000000..0e4c0c4 --- /dev/null +++ b/frontend/app/people/update/page.tsx @@ -0,0 +1,5 @@ +import { PageUpdatePeople } from '@/pages/people/update/page'; + +const UpdatePeoplePage = async () => { return } + +export default UpdatePeoplePage; diff --git a/frontend/components/dashboard/nav-main.tsx b/frontend/components/dashboard/nav-main.tsx index ed73076..4c1a374 100644 --- a/frontend/components/dashboard/nav-main.tsx +++ b/frontend/components/dashboard/nav-main.tsx @@ -1,51 +1,30 @@ "use client" - -import { IconCirclePlusFilled, IconMail, type Icon } from "@tabler/icons-react" - -import { Button } from "@/components/ui/button" -import { - SidebarGroup, - SidebarGroupContent, - SidebarMenu, - SidebarMenuButton, - SidebarMenuItem, -} from "@/components/ui/sidebar" import Link from "next/link" +import { type Icon } from "@tabler/icons-react" +import { SidebarGroup, SidebarGroupContent, SidebarMenu, SidebarMenuButton, SidebarMenuItem } from "@/components/ui/sidebar" import { usePathname } from 'next/navigation' -export function NavMain({ - items, -}: { - items: { - title: string - url: string - icon?: Icon - }[] -}) { - const pathname = usePathname() - const linkRenderActive = (item: { title: string; url: string; icon?: Icon }) => - - - - {item.icon && } - {item.title} - - - +interface NavMainProps { + title: string + url: string + icon?: Icon +} - const linkRenderDisabled = (item: { title: string; url: string; icon?: Icon }) => +export function NavMain({ items }: { items: NavMainProps[] }) { + const pathname = usePathname()?.split("/")[1] + const linkRenderActive = (item: NavMainProps) => + + {item.icon && }{item.title} + + const linkRenderDisabled = (item: NavMainProps) => - - {item.icon && } - {item.title} - + {item.icon && }{item.title} - return ( - {items.map((item) => pathname?.includes(item.url) ? linkRenderDisabled(item) : linkRenderActive(item))} + {items.map((item) => pathname?.toLocaleLowerCase() === item.url?.split("/")[1].toLocaleLowerCase() ? linkRenderDisabled(item) : linkRenderActive(item))} diff --git a/frontend/components/sidebar/app-sidebar.tsx b/frontend/components/sidebar/app-sidebar.tsx index 7b44991..f36c922 100644 --- a/frontend/components/sidebar/app-sidebar.tsx +++ b/frontend/components/sidebar/app-sidebar.tsx @@ -14,10 +14,11 @@ import { IconInnerShadowTop, IconListDetails, IconReport, - IconSearch, + IconAddressBook, IconSettings, IconUsers, - IconBuilding + IconBuilding, + IconTypeface } from "@tabler/icons-react" import { NavMain } from "@/components/dashboard/nav-main" @@ -57,98 +58,98 @@ const data = { url: "/build", icon: IconBuilding, }, - // { - // title: "Projects", - // url: "#", - // icon: IconFolder, - // }, - // { - // title: "Team", - // url: "#", - // icon: IconUsers, - // }, + { + title: "Build Types", + url: "/build-types", + icon: IconTypeface, + }, + { + title: "Build Addresses", + url: "/build-address", + icon: IconAddressBook, + } ], navClouds: [ - { - title: "Capture", - icon: IconCamera, - isActive: true, - url: "#", - items: [ - { - title: "Active Proposals", - url: "#", - }, - { - title: "Archived", - url: "#", - }, - ], - }, - { - title: "Proposal", - icon: IconFileDescription, - url: "#", - items: [ - { - title: "Active Proposals", - url: "#", - }, - { - title: "Archived", - url: "#", - }, - ], - }, - { - title: "Prompts", - icon: IconFileAi, - url: "#", - items: [ - { - title: "Active Proposals", - url: "#", - }, - { - title: "Archived", - url: "#", - }, - ], - }, + // { + // title: "Capture", + // icon: IconCamera, + // isActive: true, + // url: "#", + // items: [ + // { + // title: "Active Proposals", + // url: "#", + // }, + // { + // title: "Archived", + // url: "#", + // }, + // ], + // }, + // { + // title: "Proposal", + // icon: IconFileDescription, + // url: "#", + // items: [ + // { + // title: "Active Proposals", + // url: "#", + // }, + // { + // title: "Archived", + // url: "#", + // }, + // ], + // }, + // { + // title: "Prompts", + // icon: IconFileAi, + // url: "#", + // items: [ + // { + // title: "Active Proposals", + // url: "#", + // }, + // { + // title: "Archived", + // url: "#", + // }, + // ], + // }, ], navSecondary: [ - { - title: "Settings", - url: "#", - icon: IconSettings, - }, - { - title: "Get Help", - url: "#", - icon: IconHelp, - }, - { - title: "Search", - url: "#", - icon: IconSearch, - }, + // { + // title: "Settings", + // url: "#", + // icon: IconSettings, + // }, + // { + // title: "Get Help", + // url: "#", + // icon: IconHelp, + // }, + // { + // title: "Search", + // url: "#", + // icon: IconSearch, + // }, ], documents: [ - { - name: "Data Library", - url: "#", - icon: IconDatabase, - }, - { - name: "Reports", - url: "#", - icon: IconReport, - }, - { - name: "Word Assistant", - url: "#", - icon: IconFileWord, - }, + // { + // name: "Data Library", + // url: "#", + // icon: IconDatabase, + // }, + // { + // name: "Reports", + // url: "#", + // icon: IconReport, + // }, + // { + // name: "Word Assistant", + // url: "#", + // icon: IconFileWord, + // }, ], } diff --git a/frontend/components/ui/date-time-picker.tsx b/frontend/components/ui/date-time-picker.tsx index 2fa9dca..c91d174 100644 --- a/frontend/components/ui/date-time-picker.tsx +++ b/frontend/components/ui/date-time-picker.tsx @@ -213,7 +213,5 @@ export const dateGetter = (str: string | undefined): Date | null => { export const dateSetter = (date: Date | null): string => { if (!date) return ""; const fmt = (n: number) => String(n).padStart(2, "0"); - return `${fmt(date.getDate())}/${fmt(date.getMonth() + 1)}/${date.getFullYear()} ${fmt( - date.getHours() - )}:${fmt(date.getMinutes())}:${fmt(date.getSeconds())}`; + return `${fmt(date.getDate())}/${fmt(date.getMonth() + 1)}/${date.getFullYear()} ${fmt(date.getHours())}:${fmt(date.getMinutes())}:${fmt(date.getSeconds())}`; }; diff --git a/frontend/lib/utils.ts b/frontend/lib/utils.ts index 026514c..bef10c7 100644 --- a/frontend/lib/utils.ts +++ b/frontend/lib/utils.ts @@ -9,4 +9,20 @@ export function dateToLocaleString(date: string) { return new Date(date).toLocaleString("en-US", { timeZone: "Europe/Istanbul", hour12: false, year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", second: "2-digit" } ); -} \ No newline at end of file +} + +export function toISOIfNotZ(val: string): string { + if (val.endsWith("Z")) return val; + const [d, m, yAndTime] = val.split("/"); + if (!yAndTime) return val; + const [y, time] = yAndTime.split(" "); + if (!time) return val; + const [h, min, s] = time.split(":").map(Number); + try { + const date = new Date(Number(y), Number(m) - 1, Number(d), h, min, s); + return date ? date.toISOString() : ""; + } catch (error) { + console.error("Error converting date:", error); + return val; + } +} diff --git a/frontend/pages/build-address/add/form.tsx b/frontend/pages/build-address/add/form.tsx new file mode 100644 index 0000000..c6e2cf4 --- /dev/null +++ b/frontend/pages/build-address/add/form.tsx @@ -0,0 +1,250 @@ +"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 { buildAddressAddSchema, BuildAddressAdd } from "./schema" +import { useAddBuildAddressMutation } from "./queries" + +const BuildAddressForm = ({ refetchTable }: { refetchTable: () => void }) => { + + const form = useForm({ + resolver: zodResolver(buildAddressAddSchema), + defaultValues: { + buildNumber: "", + doorNumber: "", + floorNumber: "", + commentAddress: "", + letterAddress: "", + shortLetterAddress: "", + latitude: 0, + longitude: 0, + street: "", + expiryStarts: "", + expiryEnds: "", + }, + }); + + const { handleSubmit } = form; + + const mutation = useAddBuildAddressMutation(); + + function onSubmit(values: BuildAddressAdd) { mutation.mutate({ data: values }); setTimeout(() => refetchTable(), 400) }; + + return ( +
+ + {/* ROW 1 */} +
+ + ( + + Build Number + + + + + + )} + /> + + ( + + Door Number + + + + + + )} + /> + +
+ + {/* ROW 2 */} +
+ + ( + + Floor Number + + + + + + )} + /> + + ( + + Street ID + + + + + + )} + /> + +
+ + {/* ROW 3 */} +
+ + ( + + Comment Address + + + + + + )} + /> + + ( + + Letter Address + + + + + + )} + /> + +
+ + {/* ROW 4 */} +
+ + ( + + Short Letter Address + + + + + + )} + /> + + ( + + Latitude + + + field.onChange(e.target.value ? Number(e.target.value) : undefined) + } + /> + + + + )} + /> + +
+ + {/* ROW 5 */} +
+ + ( + + Longitude + + + field.onChange(e.target.value ? Number(e.target.value) : undefined) + } + /> + + + + )} + /> + +
+ + + + {/* EXPIRY DATES */} +
+ + ( + + Expiry Starts + + + + + + )} + /> + + ( + + Expiry Ends + + + + + + )} + /> +
+ + + + + ); +}; + +export { BuildAddressForm } diff --git a/frontend/pages/build-address/add/page.tsx b/frontend/pages/build-address/add/page.tsx new file mode 100644 index 0000000..6ea1ba4 --- /dev/null +++ b/frontend/pages/build-address/add/page.tsx @@ -0,0 +1,26 @@ +'use client'; +import { useState } from 'react'; +import { BuildAddressForm } from '@/pages/build-address/add/form'; +import { PeopleDataTableUpdate } from './table/data-table'; +import { useGraphQlBuildAddressesList } from '@/pages/build-address/queries'; + +const PageAddBuildAddress = () => { + 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 } = useGraphQlBuildAddressesList({ limit, skip: (page - 1) * limit, sort, filters }); + + return ( + <> + + + + ) +} + +export { PageAddBuildAddress }; diff --git a/frontend/pages/build-address/add/queries.tsx b/frontend/pages/build-address/add/queries.tsx new file mode 100644 index 0000000..e330beb --- /dev/null +++ b/frontend/pages/build-address/add/queries.tsx @@ -0,0 +1,22 @@ +'use client' +import { useMutation } from '@tanstack/react-query' +import { toISOIfNotZ } from '@/lib/utils'; +import { BuildAddressAdd } from './schema'; + +const fetchGraphQlBuildAddressAdd = async (record: BuildAddressAdd): Promise<{ data: BuildAddressAdd | null; status: number }> => { + console.log('Fetching test data from local API'); + try { + const res = await fetch('/api/build-address/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 useAddBuildAddressMutation() { + return useMutation({ + mutationFn: ({ data }: { data: BuildAddressAdd }) => fetchGraphQlBuildAddressAdd(data), + onSuccess: () => { console.log("Build address created successfully") }, + onError: (error) => { console.error("Create build address failed:", error) }, + }) +} diff --git a/frontend/pages/build-address/add/schema.ts b/frontend/pages/build-address/add/schema.ts new file mode 100644 index 0000000..b889636 --- /dev/null +++ b/frontend/pages/build-address/add/schema.ts @@ -0,0 +1,17 @@ +import { z } from "zod" + +export const buildAddressAddSchema = z.object({ + buildNumber: z.string(), + doorNumber: z.string(), + floorNumber: z.string(), + commentAddress: z.string(), + letterAddress: z.string(), + shortLetterAddress: z.string(), + latitude: z.number(), + longitude: z.number(), + street: z.string().optional(), + expiryStarts: z.string().optional(), + expiryEnds: z.string().optional(), +}); + +export type BuildAddressAdd = z.infer; diff --git a/frontend/pages/build-address/add/table/columns.tsx b/frontend/pages/build-address/add/table/columns.tsx new file mode 100644 index 0000000..cba858c --- /dev/null +++ b/frontend/pages/build-address/add/table/columns.tsx @@ -0,0 +1,124 @@ +"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: "uuid", + header: "UUID", + cell: ({ getValue }) => (
{String(getValue())}
), + }, + { + accessorKey: "buildNumber", + header: "Build Number", + }, + { + accessorKey: "doorNumber", + header: "Door Number", + }, + { + accessorKey: "floorNumber", + header: "Floor Number", + }, + { + accessorKey: "commentAddress", + header: "Comment Address", + }, + { + accessorKey: "letterAddress", + header: "Letter Address", + }, + { + accessorKey: "shortLetterAddress", + header: "Short Letter Address", + }, + { + accessorKey: "latitude", + header: "Latitude", + }, + { + accessorKey: "longitude", + header: "Longitude", + }, + { + accessorKey: "street", + header: "StreetId", + }, + { + 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-address/add/table/data-table.tsx b/frontend/pages/build-address/add/table/data-table.tsx new file mode 100644 index 0000000..a8f4782 --- /dev/null +++ b/frontend/pages/build-address/add/table/data-table.tsx @@ -0,0 +1,282 @@ +"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 { useDeleteBuildAddressMutation } from "@/pages/build-address/queries" + +export function PeopleDataTableUpdate({ + 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 = useDeleteBuildAddressMutation() + 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/build-address/add/table/schema.tsx b/frontend/pages/build-address/add/table/schema.tsx new file mode 100644 index 0000000..a20e89e --- /dev/null +++ b/frontend/pages/build-address/add/table/schema.tsx @@ -0,0 +1,24 @@ +import { z } from "zod"; + +export const schema = z.object({ + _id: z.string(), + uuid: z.string(), + 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(), + birthname: z.string().optional(), + expiryStarts: z.string().optional(), + expiryEnds: z.string().optional(), +}); + +export type schemaType = z.infer; \ No newline at end of file diff --git a/frontend/pages/build-address/add/types.ts b/frontend/pages/build-address/add/types.ts new file mode 100644 index 0000000..e4508a4 --- /dev/null +++ b/frontend/pages/build-address/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-address/list/columns.tsx b/frontend/pages/build-address/list/columns.tsx new file mode 100644 index 0000000..f168cd1 --- /dev/null +++ b/frontend/pages/build-address/list/columns.tsx @@ -0,0 +1,108 @@ +"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): ColumnDef[] { + return [ + { + accessorKey: "uuid", + header: "UUID", + cell: ({ getValue }) => (
{String(getValue())}
), + }, + { + accessorKey: "buildNumber", + header: "Build Number", + }, + { + accessorKey: "doorNumber", + header: "Door Number", + }, + { + accessorKey: "floorNumber", + header: "Floor Number", + }, + { + accessorKey: "commentAddress", + header: "Comment Address", + }, + { + accessorKey: "letterAddress", + header: "Letter Address", + }, + { + accessorKey: "shortLetterAddress", + header: "Short Letter Address", + }, + { + accessorKey: "latitude", + header: "Latitude", + }, + { + accessorKey: "longitude", + header: "Longitude", + }, + { + accessorKey: "street", + header: "StreetId", + }, + { + 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-address/list/data-table.tsx b/frontend/pages/build-address/list/data-table.tsx new file mode 100644 index 0000000..b1e4525 --- /dev/null +++ b/frontend/pages/build-address/list/data-table.tsx @@ -0,0 +1,270 @@ +"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 { useDeleteBuildAddressMutation } from "../queries" + +export function BuildAddressDataTable({ + data, + totalCount, + currentPage = 1, + pageSize = 10, + 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 = useDeleteBuildAddressMutation() + 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, + 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-address/list/schema.tsx b/frontend/pages/build-address/list/schema.tsx new file mode 100644 index 0000000..c4a0399 --- /dev/null +++ b/frontend/pages/build-address/list/schema.tsx @@ -0,0 +1,20 @@ +import { z } from "zod"; + +export const schema = z.object({ + _id: z.string(), + uuid: z.string(), + 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(), + expiryStarts: z.string().optional(), + expiryEnds: z.string().optional(), +}); + + +export type schemaType = z.infer; \ No newline at end of file diff --git a/frontend/pages/build-address/page.tsx b/frontend/pages/build-address/page.tsx new file mode 100644 index 0000000..d64d0f3 --- /dev/null +++ b/frontend/pages/build-address/page.tsx @@ -0,0 +1,25 @@ +'use client'; +import { BuildAddressDataTable } from './list/data-table'; +import { useGraphQlBuildAddressesList } from './queries'; +import { useState } from 'react'; + +const PageBuildAddress = () => { + + 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 } = useGraphQlBuildAddressesList({ limit, skip: (page - 1) * limit, sort, filters }); + + const handlePageChange = (newPage: number) => { setPage(newPage) }; + const handlePageSizeChange = (newSize: number) => { setLimit(newSize); setPage(1) }; + + if (isLoading) { return
Loading...
} + if (error) { return
Error loading users
} + + return ; + +}; + +export { PageBuildAddress }; diff --git a/frontend/pages/build-address/queries.tsx b/frontend/pages/build-address/queries.tsx new file mode 100644 index 0000000..fafb20a --- /dev/null +++ b/frontend/pages/build-address/queries.tsx @@ -0,0 +1,36 @@ +'use client' +import { useQuery, useMutation } from '@tanstack/react-query' +import { ListArguments } from '@/types/listRequest' + +const fetchGraphQlBuildAddressesList = async (params: ListArguments): Promise => { + console.log('Fetching test data from local API'); + const { limit, skip, sort, filters } = params; + try { + const res = await fetch('/api/build-address/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 fetchGraphQlDeleteBuildAddress = async (uuid: string): Promise => { + console.log('Fetching test data from local API'); + try { + const res = await fetch(`/api/build-address/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 useGraphQlBuildAddressesList(params: ListArguments) { + return useQuery({ queryKey: ['graphql-build-addresses-list', params], queryFn: () => fetchGraphQlBuildAddressesList(params) }) +} + +export function useDeleteBuildAddressMutation() { + return useMutation({ + mutationFn: ({ uuid }: { uuid: string }) => fetchGraphQlDeleteBuildAddress(uuid), + onSuccess: () => { console.log("Build address deleted successfully") }, + onError: (error) => { console.error("Delete build address failed:", error) }, + }) +} diff --git a/frontend/pages/build-address/types.ts b/frontend/pages/build-address/types.ts new file mode 100644 index 0000000..f98bbc4 --- /dev/null +++ b/frontend/pages/build-address/types.ts @@ -0,0 +1,20 @@ + +interface BuildAddress { + + _id: string; + uuid: string; + buildNumber: string; + doorNumber: string; + floorNumber: string; + commentAddress: string; + letterAddress: string; + shortLetterAddress: string; + latitude: number; + longitude: number; + street: string; + expiryStarts: string | null; + expiryEnds: string | null; + +} + +export type { BuildAddress }; diff --git a/frontend/pages/build-address/update/form.tsx b/frontend/pages/build-address/update/form.tsx new file mode 100644 index 0000000..0a64523 --- /dev/null +++ b/frontend/pages/build-address/update/form.tsx @@ -0,0 +1,238 @@ +"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 { useUpdateBuildAddressMutation } from "@/pages/build-address/update/queries" +import { buildAddressUpdateSchema, type BuildAddressUpdate } from "@/pages/build-address/update/schema" + +const BuildAddressForm = ({ refetchTable, initData, selectedUuid }: { refetchTable: () => void, initData: BuildAddressUpdate, selectedUuid: string }) => { + + const form = useForm({ resolver: zodResolver(buildAddressUpdateSchema), defaultValues: { ...initData, street: "" } }) + + const { handleSubmit } = form + + const mutation = useUpdateBuildAddressMutation(); + + function onSubmit(values: BuildAddressUpdate) { mutation.mutate({ data: values as any || initData, uuid: selectedUuid }); setTimeout(() => refetchTable(), 400) } + + return ( +
+ + {/* ROW 1 */} +
+ + ( + + Build Number + + + + + + )} + /> + + ( + + Door Number + + + + + + )} + /> + +
+ + {/* ROW 2 */} +
+ + ( + + Floor Number + + + + + + )} + /> + + ( + + Street ID + + + + + + )} + /> + +
+ + {/* ROW 3 */} +
+ + ( + + Comment Address + + + + + + )} + /> + + ( + + Letter Address + + + + + + )} + /> + +
+ + {/* ROW 4 */} +
+ + ( + + Short Letter Address + + + + + + )} + /> + + ( + + Latitude + + + field.onChange(e.target.value ? Number(e.target.value) : undefined) + } + /> + + + + )} + /> + +
+ + {/* ROW 5 */} +
+ + ( + + Longitude + + + field.onChange(e.target.value ? Number(e.target.value) : undefined) + } + /> + + + + )} + /> + +
+ + + + {/* EXPIRY DATES */} +
+ + ( + + Expiry Starts + + + + + + )} + /> + + ( + + Expiry Ends + + + + + + )} + /> + +
+ + + + + + ); + +} + +export { BuildAddressForm } diff --git a/frontend/pages/build-address/update/page.tsx b/frontend/pages/build-address/update/page.tsx new file mode 100644 index 0000000..88f0aed --- /dev/null +++ b/frontend/pages/build-address/update/page.tsx @@ -0,0 +1,36 @@ +'use client'; +import { useState } from 'react'; +import { useGraphQlBuildAddressesList } from '@/pages/build-address/queries'; +import { BuildAddressDataTableUpdate } from '@/pages/build-address/update/table/data-table'; +import { BuildAddressForm } from '@/pages/build-address/update/form'; +import { useSearchParams, useRouter } from 'next/navigation' +import { Button } from '@/components/ui/button'; + +const PageUpdateBuildAddress = () => { + 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 } = useGraphQlBuildAddressesList({ limit, skip: (page - 1) * limit, sort, filters: { ...filters, uuid } }); + const initData = data?.data?.[0] || null; + if (!initData) { return backToBuildAddress } + return ( + <> + + + + ) +} + +export { PageUpdateBuildAddress }; diff --git a/frontend/pages/build-address/update/queries.tsx b/frontend/pages/build-address/update/queries.tsx new file mode 100644 index 0000000..56bc650 --- /dev/null +++ b/frontend/pages/build-address/update/queries.tsx @@ -0,0 +1,21 @@ +'use client' +import { useMutation } from '@tanstack/react-query' +import { BuildAddressUpdate } from './types'; + +const fetchGraphQlBuildAddressUpdate = async (record: BuildAddressUpdate, uuid: string): Promise<{ data: BuildAddressUpdate | null; status: number }> => { + console.log('Fetching test data from local API'); + try { + const res = await fetch(`/api/build-address/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 useUpdateBuildAddressMutation() { + return useMutation({ + mutationFn: ({ data, uuid }: { data: BuildAddressUpdate, uuid: string }) => fetchGraphQlBuildAddressUpdate(data, uuid), + onSuccess: () => { console.log("Build Address updated successfully") }, + onError: (error) => { console.error("Update Build Address failed:", error) }, + }) +} diff --git a/frontend/pages/build-address/update/schema.ts b/frontend/pages/build-address/update/schema.ts new file mode 100644 index 0000000..6ea3572 --- /dev/null +++ b/frontend/pages/build-address/update/schema.ts @@ -0,0 +1,17 @@ +import { z } from "zod" + +export const buildAddressUpdateSchema = 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(), + expiryStarts: z.string().optional(), + expiryEnds: z.string().optional(), +}); + +export type BuildAddressUpdate = z.infer; diff --git a/frontend/pages/build-address/update/table/columns.tsx b/frontend/pages/build-address/update/table/columns.tsx new file mode 100644 index 0000000..f9c3171 --- /dev/null +++ b/frontend/pages/build-address/update/table/columns.tsx @@ -0,0 +1,225 @@ +"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 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 ( + + ) +} + +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: "uuid", + header: "UUID", + cell: ({ getValue }) => (
{String(getValue())}
), + }, + { + accessorKey: "buildNumber", + header: "Build Number", + }, + { + accessorKey: "doorNumber", + header: "Door Number", + }, + { + accessorKey: "floorNumber", + header: "Floor Number", + }, + { + accessorKey: "commentAddress", + header: "Comment Address", + }, + { + accessorKey: "letterAddress", + header: "Letter Address", + }, + { + accessorKey: "shortLetterAddress", + header: "Short Letter Address", + }, + { + accessorKey: "latitude", + header: "Latitude", + }, + { + accessorKey: "longitude", + header: "Longitude", + }, + { + accessorKey: "street", + header: "StreetId", + }, + { + 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-address/update/table/data-table.tsx b/frontend/pages/build-address/update/table/data-table.tsx new file mode 100644 index 0000000..2c21c68 --- /dev/null +++ b/frontend/pages/build-address/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 { useDeletePersonMutation } from "@/pages/people/queries" + +export function BuildAddressDataTableUpdate({ + 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 = useDeletePersonMutation() + 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/build-address/update/table/schema.tsx b/frontend/pages/build-address/update/table/schema.tsx new file mode 100644 index 0000000..52413d2 --- /dev/null +++ b/frontend/pages/build-address/update/table/schema.tsx @@ -0,0 +1,47 @@ +import { z } from "zod"; + +export const schema = z.object({ + _id: z.string(), + uuid: z.string().nullable().optional(), + expiryStarts: z.string().nullable().optional(), + expiryEnds: z.string().nullable().optional(), + isConfirmed: z.boolean().nullable().optional(), + deleted: z.boolean().nullable().optional(), + active: z.boolean().nullable().optional(), + crypUuId: z.string().nullable().optional(), + createdCredentialsToken: z.string().nullable().optional(), + updatedCredentialsToken: z.string().nullable().optional(), + confirmedCredentialsToken: z.string().nullable().optional(), + isNotificationSend: z.boolean().nullable().optional(), + isEmailSend: z.boolean().nullable().optional(), + refInt: z.number().nullable().optional(), + refId: z.string().nullable().optional(), + replicationId: z.number().nullable().optional(), + expiresAt: z.string().nullable().optional(), + resetToken: z.string().nullable().optional(), + password: z.string().nullable().optional(), + history: z.array(z.string()).optional(), + tag: z.string().nullable().optional(), + email: z.string().nullable().optional(), + phone: z.string().nullable().optional(), + + collectionTokens: z + .object({ + default: z.string().nullable().optional(), + tokens: z + .array( + z.object({ + prefix: z.string(), + token: z.string(), + }) + ) + .optional(), + }) + .nullable() + .optional(), + + createdAt: z.string().nullable().optional(), + updatedAt: z.string().nullable().optional(), +}); + +export type schemaType = z.infer; diff --git a/frontend/pages/build-address/update/types.ts b/frontend/pages/build-address/update/types.ts new file mode 100644 index 0000000..740c49a --- /dev/null +++ b/frontend/pages/build-address/update/types.ts @@ -0,0 +1,17 @@ + +interface BuildAddressUpdate { + buildNumber: string; + doorNumber: string; + floorNumber: string; + commentAddress: string; + letterAddress: string; + shortLetterAddress: string; + latitude: number; + longitude: number; + street: string; + expiryStarts: string | null; + expiryEnds: string | null; +} + + +export type { BuildAddressUpdate }; \ No newline at end of file diff --git a/frontend/pages/build-types/add/form.tsx b/frontend/pages/build-types/add/form.tsx new file mode 100644 index 0000000..1e93aba --- /dev/null +++ b/frontend/pages/build-types/add/form.tsx @@ -0,0 +1,144 @@ +"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 { BuildTypesAdd, buildTypesAddSchema } from "./schema" +import { useAddBuildTypesMutation } from "./queries" +import { DateTimePicker } from "@/components/ui/date-time-picker" + +const BuildTypesForm = ({ refetchTable }: { refetchTable: () => void }) => { + + const form = useForm({ + resolver: zodResolver(buildTypesAddSchema), + defaultValues: { + type: "", + token: "", + typeToken: "", + description: "", + expiryStarts: "", + expiryEnds: "", + }, + }); + + const { handleSubmit } = form; + + const mutation = useAddBuildTypesMutation(); + + function onSubmit(values: BuildTypesAdd) { mutation.mutate({ data: values }); setTimeout(() => refetchTable(), 400) }; + + return ( +
+ + {/* TYPE + TOKEN */} +
+ + ( + + Type + + + + + + )} + /> + + ( + + Token + + + + + + )} + /> +
+ + {/* TYPE TOKEN + DESCRIPTION */} +
+ + ( + + Type Token + + + + + + )} + /> + + ( + + Description + + + + + + )} + /> +
+ + + + {/* EXPIRY DATES */} +
+ + ( + + Expiry Starts + + + + + + )} + /> + + ( + + Expiry Ends + + + + + + )} + /> +
+ + + + + ); +}; + +export { BuildTypesForm } diff --git a/frontend/pages/build-types/add/page.tsx b/frontend/pages/build-types/add/page.tsx new file mode 100644 index 0000000..b92a7b2 --- /dev/null +++ b/frontend/pages/build-types/add/page.tsx @@ -0,0 +1,26 @@ +'use client'; +import { useState } from 'react'; +import { useGraphQlBuildTypesList } from '@/pages/build-types/queries'; +import { BuildTypesForm } from './form'; +import { BuildTypesDataTableAdd } from './table/data-table'; + +const PageAddBuildTypes = () => { + 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 } = useGraphQlBuildTypesList({ limit, skip: (page - 1) * limit, sort, filters }); + + return ( + <> + + + + ) +} + +export { PageAddBuildTypes }; diff --git a/frontend/pages/build-types/add/queries.tsx b/frontend/pages/build-types/add/queries.tsx new file mode 100644 index 0000000..c393f36 --- /dev/null +++ b/frontend/pages/build-types/add/queries.tsx @@ -0,0 +1,25 @@ +'use client' +import { useMutation } from '@tanstack/react-query' +import { BuildTypesAdd } from './types' +import { toISOIfNotZ } from '@/lib/utils'; + +const fetchGraphQlBuildTypesAdd = async (record: BuildTypesAdd): Promise<{ data: BuildTypesAdd | null; status: number }> => { + console.log('Fetching test data from local API'); + // record.birthDate = toISOIfNotZ(record.birthDate || ""); + // record.expiryStarts = toISOIfNotZ(record.expiryStarts || ""); + // record.expiryEnds = toISOIfNotZ(record.expiryEnds || ""); + try { + const res = await fetch('/api/build-types/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 useAddBuildTypesMutation() { + return useMutation({ + mutationFn: ({ data }: { data: BuildTypesAdd }) => fetchGraphQlBuildTypesAdd(data), + onSuccess: () => { console.log("Build Types created successfully") }, + onError: (error) => { console.error("Create build types failed:", error) }, + }) +} diff --git a/frontend/pages/build-types/add/schema.ts b/frontend/pages/build-types/add/schema.ts new file mode 100644 index 0000000..30051ea --- /dev/null +++ b/frontend/pages/build-types/add/schema.ts @@ -0,0 +1,12 @@ +import { z } from "zod" + +export const buildTypesAddSchema = z.object({ + type: z.string(), + token: z.string(), + typeToken: z.string(), + description: z.string(), + expiryStarts: z.string().optional(), + expiryEnds: z.string().optional(), +}); + +export type BuildTypesAdd = z.infer; diff --git a/frontend/pages/build-types/add/table/columns.tsx b/frontend/pages/build-types/add/table/columns.tsx new file mode 100644 index 0000000..ca1ca30 --- /dev/null +++ b/frontend/pages/build-types/add/table/columns.tsx @@ -0,0 +1,250 @@ +"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 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 ( + + ) +} + +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: "uuid", + header: "UUID", + cell: ({ getValue }) => (
{String(getValue())}
), + }, + { + accessorKey: "firstName", + header: "First Name", + }, + { + accessorKey: "surname", + header: "Surname", + }, + { + accessorKey: "middleName", + header: "Middle Name", + }, + { + accessorKey: "sexCode", + header: "Sex", + }, + { + accessorKey: "personRef", + header: "Person Ref", + }, + { + accessorKey: "personTag", + header: "Person Tag", + }, + { + accessorKey: "fatherName", + header: "Father Name", + }, + { + accessorKey: "motherName", + header: "Mother Name", + }, + { + accessorKey: "countryCode", + header: "Country", + }, + { + accessorKey: "nationalIdentityId", + header: "National ID", + }, + { + accessorKey: "birthPlace", + header: "Birth Place", + }, + { + accessorKey: "active", + header: "Active", + cell: ({ getValue }) => getValue() ? (
Yes
) : (
No
), + }, + { + accessorKey: "isConfirmed", + header: "Confirmed", + cell: ({ getValue }) => getValue() ? (
Yes
) : (
No
), + }, + { + accessorKey: "birthDate", + header: "Birth Date", + cell: ({ getValue }) => dateToLocaleString(getValue() as string), + }, + { + 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-types/add/table/data-table.tsx b/frontend/pages/build-types/add/table/data-table.tsx new file mode 100644 index 0000000..62ec7e8 --- /dev/null +++ b/frontend/pages/build-types/add/table/data-table.tsx @@ -0,0 +1,287 @@ +"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 { useDeleteUserMutation } from "@/pages/users/queries" + +export function BuildTypesDataTableAdd({ + 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 = useDeleteUserMutation() + 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/build-types/add/table/schema.tsx b/frontend/pages/build-types/add/table/schema.tsx new file mode 100644 index 0000000..a20e89e --- /dev/null +++ b/frontend/pages/build-types/add/table/schema.tsx @@ -0,0 +1,24 @@ +import { z } from "zod"; + +export const schema = z.object({ + _id: z.string(), + uuid: z.string(), + 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(), + birthname: z.string().optional(), + expiryStarts: z.string().optional(), + expiryEnds: z.string().optional(), +}); + +export type schemaType = z.infer; \ No newline at end of file diff --git a/frontend/pages/build-types/add/types.ts b/frontend/pages/build-types/add/types.ts new file mode 100644 index 0000000..6021c5e --- /dev/null +++ b/frontend/pages/build-types/add/types.ts @@ -0,0 +1,14 @@ + +interface BuildTypesAdd { + + type: string; + token: string; + typeToken: string; + description: string; + expiryStarts?: string; + expiryEnds?: string; + +} + + +export type { BuildTypesAdd }; \ No newline at end of file diff --git a/frontend/pages/build-types/list/columns.tsx b/frontend/pages/build-types/list/columns.tsx new file mode 100644 index 0000000..0322ad6 --- /dev/null +++ b/frontend/pages/build-types/list/columns.tsx @@ -0,0 +1,88 @@ +"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): ColumnDef[] { + return [ + { + accessorKey: "uuid", + header: "UUID", + cell: ({ getValue }) => (
{String(getValue())}
), + }, + { + accessorKey: "type", + header: "Type", + }, + { + accessorKey: "token", + header: "Token", + }, + { + accessorKey: "typeToken", + header: "Type Token", + }, + { + accessorKey: "description", + header: "Description", + }, + { + 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-types/list/data-table.tsx b/frontend/pages/build-types/list/data-table.tsx new file mode 100644 index 0000000..b3b8f68 --- /dev/null +++ b/frontend/pages/build-types/list/data-table.tsx @@ -0,0 +1,270 @@ +"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 { useDeleteBuildTypeMutation } from "../queries" + +export function BuildTypesDataTable({ + data, + totalCount, + currentPage = 1, + pageSize = 10, + 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 = useDeleteBuildTypeMutation() + 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, + 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-types/list/schema.tsx b/frontend/pages/build-types/list/schema.tsx new file mode 100644 index 0000000..8c373f0 --- /dev/null +++ b/frontend/pages/build-types/list/schema.tsx @@ -0,0 +1,16 @@ +import { z } from "zod"; + +export const schema = z.object({ + _id: z.string(), + uuid: z.string(), + type: z.string(), + token: z.string(), + typeToken: z.string(), + description: z.string(), + createdAt: z.string(), + updatedAt: z.string(), + expiryStarts: z.string().nullable().optional(), + expiryEnds: z.string().nullable().optional(), +}); + +export type schemaType = z.infer; \ No newline at end of file diff --git a/frontend/pages/build-types/page.tsx b/frontend/pages/build-types/page.tsx new file mode 100644 index 0000000..167d9c1 --- /dev/null +++ b/frontend/pages/build-types/page.tsx @@ -0,0 +1,25 @@ +'use client'; +import { BuildTypesDataTable } from './list/data-table'; +import { useGraphQlBuildTypesList } from './queries'; +import { useState } from 'react'; + +const PageBuildTypes = () => { + + 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 } = useGraphQlBuildTypesList({ limit, skip: (page - 1) * limit, sort, filters }); + + const handlePageChange = (newPage: number) => { setPage(newPage) }; + const handlePageSizeChange = (newSize: number) => { setLimit(newSize); setPage(1) }; + + if (isLoading) { return
Loading...
} + if (error) { return
Error loading users
} + + return ; + +}; + +export { PageBuildTypes }; diff --git a/frontend/pages/build-types/queries.tsx b/frontend/pages/build-types/queries.tsx new file mode 100644 index 0000000..ab26e7f --- /dev/null +++ b/frontend/pages/build-types/queries.tsx @@ -0,0 +1,36 @@ +'use client' +import { useQuery, useMutation } from '@tanstack/react-query' +import { ListArguments } from '@/types/listRequest' + +const fetchGraphQlBuildTypesList = async (params: ListArguments): Promise => { + console.log('Fetching test data from local API'); + const { limit, skip, sort, filters } = params; + try { + const res = await fetch('/api/build-types/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 fetchGraphQlDeleteBuildType = async (uuid: string): Promise => { + console.log('Fetching test data from local API'); + try { + const res = await fetch(`/api/build-types/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 useGraphQlBuildTypesList(params: ListArguments) { + return useQuery({ queryKey: ['graphql-build-types-list', params], queryFn: () => fetchGraphQlBuildTypesList(params) }) +} + +export function useDeleteBuildTypeMutation() { + return useMutation({ + mutationFn: ({ uuid }: { uuid: string }) => fetchGraphQlDeleteBuildType(uuid), + onSuccess: () => { console.log("Build type deleted successfully") }, + onError: (error) => { console.error("Delete build type failed:", error) }, + }) +} diff --git a/frontend/pages/build-types/types.ts b/frontend/pages/build-types/types.ts new file mode 100644 index 0000000..25fc703 --- /dev/null +++ b/frontend/pages/build-types/types.ts @@ -0,0 +1,9 @@ + +interface BuildType { + type: string; + token: string; + typeToken: string; + description: string; +} + +export type { BuildType } diff --git a/frontend/pages/build-types/update/form.tsx b/frontend/pages/build-types/update/form.tsx new file mode 100644 index 0000000..01d544b --- /dev/null +++ b/frontend/pages/build-types/update/form.tsx @@ -0,0 +1,135 @@ +"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 { useUpdateBuildTypesMutation } from "@/pages/build-types/update/queries" +import { buildTypesUpdateSchema, type BuildTypesUpdate } from "@/pages/build-types/update/schema" + +const BuildTypesForm = ({ refetchTable, initData, selectedUuid }: { refetchTable: () => void, initData: BuildTypesUpdate, selectedUuid: string }) => { + + const form = useForm({ resolver: zodResolver(buildTypesUpdateSchema), defaultValues: { ...initData } }) + + const { handleSubmit } = form + + const mutation = useUpdateBuildTypesMutation(); + + function onSubmit(values: BuildTypesUpdate) { mutation.mutate({ data: values as any || initData, uuid: selectedUuid }); setTimeout(() => refetchTable(), 400) } + + return ( +
+ + {/* TYPE + TOKEN */} +
+ + ( + + Type + + + + + + )} + /> + + ( + + Token + + + + + + )} + /> +
+ + {/* TYPE TOKEN + DESCRIPTION */} +
+ + ( + + Type Token + + + + + + )} + /> + + ( + + Description + + + + + + )} + /> +
+ + + + {/* EXPIRY STARTS + ENDS */} +
+ + ( + + Expiry Starts + + + + + + )} + /> + + ( + + Expiry Ends + + + + + + )} + /> +
+ + + + + ); + +} + +export { BuildTypesForm } diff --git a/frontend/pages/build-types/update/page.tsx b/frontend/pages/build-types/update/page.tsx new file mode 100644 index 0000000..41931e3 --- /dev/null +++ b/frontend/pages/build-types/update/page.tsx @@ -0,0 +1,36 @@ +'use client'; +import { useState } from 'react'; +import { useGraphQlBuildTypesList } from '@/pages/build-types/queries'; +import { BuildTypesDataTableUpdate } from '@/pages/build-types/update/table/data-table'; +import { BuildTypesForm } from '@/pages/build-types/update/form'; +import { useSearchParams, useRouter } from 'next/navigation' +import { Button } from '@/components/ui/button'; + +const PageUpdateBuildTypes = () => { + 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 backToBuildTypes = <> +
UUID not found in search params
+ + + if (!uuid) { return backToBuildTypes } + const { data, isLoading, error, refetch } = useGraphQlBuildTypesList({ limit, skip: (page - 1) * limit, sort, filters: { ...filters, uuid } }); + const initData = data?.data?.[0] || null; + if (!initData) { return backToBuildTypes } + return ( + <> + + + + ) +} + +export { PageUpdateBuildTypes }; diff --git a/frontend/pages/build-types/update/queries.tsx b/frontend/pages/build-types/update/queries.tsx new file mode 100644 index 0000000..b014902 --- /dev/null +++ b/frontend/pages/build-types/update/queries.tsx @@ -0,0 +1,21 @@ +'use client' +import { useMutation } from '@tanstack/react-query' +import { BuildTypesUpdate } from './types'; + +const fetchGraphQlBuildTypesUpdate = async (record: BuildTypesUpdate, uuid: string): Promise<{ data: BuildTypesUpdate | null; status: number }> => { + console.log('Fetching test data from local API'); + 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}`) } + 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 useUpdateBuildTypesMutation() { + return useMutation({ + mutationFn: ({ data, uuid }: { data: BuildTypesUpdate, uuid: string }) => fetchGraphQlBuildTypesUpdate(data, uuid), + onSuccess: () => { console.log("Build Types updated successfully") }, + onError: (error) => { console.error("Update build types failed:", error) }, + }) +} diff --git a/frontend/pages/build-types/update/schema.ts b/frontend/pages/build-types/update/schema.ts new file mode 100644 index 0000000..96b824b --- /dev/null +++ b/frontend/pages/build-types/update/schema.ts @@ -0,0 +1,12 @@ +import { z } from "zod" + +export const buildTypesUpdateSchema = z.object({ + type: z.string(), + token: z.string(), + typeToken: z.string(), + description: z.string(), + expiryStarts: z.string(), + expiryEnds: z.string(), +}); + +export type BuildTypesUpdate = z.infer; \ No newline at end of file diff --git a/frontend/pages/build-types/update/table/columns.tsx b/frontend/pages/build-types/update/table/columns.tsx new file mode 100644 index 0000000..6c603e6 --- /dev/null +++ b/frontend/pages/build-types/update/table/columns.tsx @@ -0,0 +1,206 @@ +"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 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 ( + + ) +} + +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: "uuid", + header: "UUID", + cell: ({ getValue }) => (
{String(getValue())}
), + }, + { + accessorKey: "type", + header: "Type", + }, + { + accessorKey: "token", + header: "Token", + }, + { + accessorKey: "typeToken", + header: "Type Token", + }, + { + accessorKey: "description", + header: "Description", + }, + { + 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-types/update/table/data-table.tsx b/frontend/pages/build-types/update/table/data-table.tsx new file mode 100644 index 0000000..f0b79e9 --- /dev/null +++ b/frontend/pages/build-types/update/table/data-table.tsx @@ -0,0 +1,280 @@ +"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 BuildTypesDataTableUpdate({ + 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 = useDeletePersonMutation() + 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/build-types/update/table/schema.tsx b/frontend/pages/build-types/update/table/schema.tsx new file mode 100644 index 0000000..52413d2 --- /dev/null +++ b/frontend/pages/build-types/update/table/schema.tsx @@ -0,0 +1,47 @@ +import { z } from "zod"; + +export const schema = z.object({ + _id: z.string(), + uuid: z.string().nullable().optional(), + expiryStarts: z.string().nullable().optional(), + expiryEnds: z.string().nullable().optional(), + isConfirmed: z.boolean().nullable().optional(), + deleted: z.boolean().nullable().optional(), + active: z.boolean().nullable().optional(), + crypUuId: z.string().nullable().optional(), + createdCredentialsToken: z.string().nullable().optional(), + updatedCredentialsToken: z.string().nullable().optional(), + confirmedCredentialsToken: z.string().nullable().optional(), + isNotificationSend: z.boolean().nullable().optional(), + isEmailSend: z.boolean().nullable().optional(), + refInt: z.number().nullable().optional(), + refId: z.string().nullable().optional(), + replicationId: z.number().nullable().optional(), + expiresAt: z.string().nullable().optional(), + resetToken: z.string().nullable().optional(), + password: z.string().nullable().optional(), + history: z.array(z.string()).optional(), + tag: z.string().nullable().optional(), + email: z.string().nullable().optional(), + phone: z.string().nullable().optional(), + + collectionTokens: z + .object({ + default: z.string().nullable().optional(), + tokens: z + .array( + z.object({ + prefix: z.string(), + token: z.string(), + }) + ) + .optional(), + }) + .nullable() + .optional(), + + createdAt: z.string().nullable().optional(), + updatedAt: z.string().nullable().optional(), +}); + +export type schemaType = z.infer; diff --git a/frontend/pages/build-types/update/types.ts b/frontend/pages/build-types/update/types.ts new file mode 100644 index 0000000..e1173f0 --- /dev/null +++ b/frontend/pages/build-types/update/types.ts @@ -0,0 +1,10 @@ + +interface BuildTypesUpdate { + type: string; + token: string; + typeToken: string; + description: string; +} + + +export type { BuildTypesUpdate }; \ No newline at end of file diff --git a/frontend/pages/builds/list/columns.tsx b/frontend/pages/builds/list/columns.tsx new file mode 100644 index 0000000..ca1ca30 --- /dev/null +++ b/frontend/pages/builds/list/columns.tsx @@ -0,0 +1,250 @@ +"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 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 ( + + ) +} + +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: "uuid", + header: "UUID", + cell: ({ getValue }) => (
{String(getValue())}
), + }, + { + accessorKey: "firstName", + header: "First Name", + }, + { + accessorKey: "surname", + header: "Surname", + }, + { + accessorKey: "middleName", + header: "Middle Name", + }, + { + accessorKey: "sexCode", + header: "Sex", + }, + { + accessorKey: "personRef", + header: "Person Ref", + }, + { + accessorKey: "personTag", + header: "Person Tag", + }, + { + accessorKey: "fatherName", + header: "Father Name", + }, + { + accessorKey: "motherName", + header: "Mother Name", + }, + { + accessorKey: "countryCode", + header: "Country", + }, + { + accessorKey: "nationalIdentityId", + header: "National ID", + }, + { + accessorKey: "birthPlace", + header: "Birth Place", + }, + { + accessorKey: "active", + header: "Active", + cell: ({ getValue }) => getValue() ? (
Yes
) : (
No
), + }, + { + accessorKey: "isConfirmed", + header: "Confirmed", + cell: ({ getValue }) => getValue() ? (
Yes
) : (
No
), + }, + { + accessorKey: "birthDate", + header: "Birth Date", + cell: ({ getValue }) => dateToLocaleString(getValue() as string), + }, + { + 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/builds/list/data-table.tsx b/frontend/pages/builds/list/data-table.tsx new file mode 100644 index 0000000..b270a4d --- /dev/null +++ b/frontend/pages/builds/list/data-table.tsx @@ -0,0 +1,280 @@ +"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 { useDeleteUserMutation } from "@/pages/users/queries" + +export function BuildDataTable({ + data, + totalCount, + currentPage = 1, + pageSize = 10, + 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 = useDeleteUserMutation() + 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, + 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/builds/list/schema.tsx b/frontend/pages/builds/list/schema.tsx new file mode 100644 index 0000000..02d3e98 --- /dev/null +++ b/frontend/pages/builds/list/schema.tsx @@ -0,0 +1,27 @@ +import { z } from "zod"; + +export const schema = z.object({ + _id: z.string(), + uuid: z.string(), + firstName: z.string().nullable().optional(), + surname: z.string().nullable().optional(), + middleName: z.string().nullable().optional(), + sexCode: z.string().nullable().optional(), + personRef: z.string().nullable().optional(), + personTag: z.string().nullable().optional(), + fatherName: z.string().nullable().optional(), + motherName: z.string().nullable().optional(), + countryCode: z.string().nullable().optional(), + nationalIdentityId: z.string().nullable().optional(), + birthPlace: z.string().nullable().optional(), + birthDate: z.string().nullable().optional(), + taxNo: z.string().nullable().optional(), + birthname: z.string().nullable().optional(), + expiryStarts: z.string().nullable().optional(), + expiryEnds: z.string().nullable().optional(), + createdAt: z.string().nullable().optional(), + updatedAt: z.string().nullable().optional(), +}); + + +export type schemaType = z.infer; \ No newline at end of file diff --git a/frontend/pages/builds/page.tsx b/frontend/pages/builds/page.tsx new file mode 100644 index 0000000..50cdf69 --- /dev/null +++ b/frontend/pages/builds/page.tsx @@ -0,0 +1,23 @@ +'use client'; +import { useGraphQlBuildsList } from './queries'; +import { useState } from 'react'; +import { BuildDataTable } from '@/pages/builds/list/data-table'; + +const PageBuilds = () => { + 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 }); + + const handlePageChange = (newPage: number) => { setPage(newPage) }; + const handlePageSizeChange = (newSize: number) => { setLimit(newSize); setPage(1) }; + if (isLoading) { return
Loading...
} + if (error) { return
Error loading users
} + + return ; + +}; + +export { PageBuilds }; diff --git a/frontend/pages/builds/queries.tsx b/frontend/pages/builds/queries.tsx new file mode 100644 index 0000000..90e53e2 --- /dev/null +++ b/frontend/pages/builds/queries.tsx @@ -0,0 +1,36 @@ +'use client' +import { useQuery, useMutation } from '@tanstack/react-query' +import { ListArguments } from '@/types/listRequest' + +const fetchGraphQlBuildsList = 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/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 fetchGraphQlDeleteBuild = async (uuid: string): Promise => { + console.log('Fetching test data from local API'); + try { + const res = await fetch(`/api/builds/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 useGraphQlBuildsList(params: ListArguments) { + return useQuery({ queryKey: ['graphql-builds-list', params], queryFn: () => fetchGraphQlBuildsList(params) }) +} + +export function useDeleteBuildMutation() { + return useMutation({ + mutationFn: ({ uuid }: { uuid: string }) => fetchGraphQlDeleteBuild(uuid), + onSuccess: () => { console.log("Person deleted successfully") }, + onError: (error) => { console.error("Delete person failed:", error) }, + }) +} diff --git a/frontend/pages/builds/types.ts b/frontend/pages/builds/types.ts new file mode 100644 index 0000000..985ce88 --- /dev/null +++ b/frontend/pages/builds/types.ts @@ -0,0 +1,44 @@ +interface Build { + buildType: string; + collectionToken: string; + info: BuildInfo; + site?: string; + address?: string; + areas?: string[]; + ibans?: BuildIban[]; + responsibles?: BuildResponsible[]; +} + +interface BuildInfo { + govAddressCode: string; + buildName: string; + buildNo: string; + maxFloor: number; + undergroundFloor: number; + buildDate: Date; + decisionPeriodDate: Date; + taxNo: string; + liftCount: number; + heatingSystem: boolean; + coolingSystem: boolean; + hotWaterSystem: boolean; + blockServiceManCount: number; + securityServiceManCount: number; + garageCount: number; + managementRoomId: number; +} + +interface BuildIban { + iban: string; + startDate: Date; + stopDate: Date; + bankCode: string; + xcomment: string; +} + +interface BuildResponsible { + company: string; + person: string; +} + +export type { Build, BuildInfo, BuildIban, BuildResponsible } diff --git a/frontend/pages/people/add/form.tsx b/frontend/pages/people/add/form.tsx new file mode 100644 index 0000000..e02b9ea --- /dev/null +++ b/frontend/pages/people/add/form.tsx @@ -0,0 +1,309 @@ +"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 { PeopleAdd, personAddSchema } from "./schema" +import { useAddPeopleMutation } from "./queries" +import { DateTimePicker } from "@/components/ui/date-time-picker" + +const PeopleForm = ({ refetchTable }: { refetchTable: () => void }) => { + + const form = useForm({ + resolver: zodResolver(personAddSchema), + defaultValues: { + firstName: "", + surname: "", + middleName: "", + sexCode: "", + personRef: "", + personTag: "", + fatherName: "", + motherName: "", + countryCode: "", + nationalIdentityId: "", + birthPlace: "", + birthDate: "", + taxNo: "", + birthname: "", + expiryStarts: "", + expiryEnds: "", + }, + }); + + const { handleSubmit } = form; + + const mutation = useAddPeopleMutation(); + + function onSubmit(values: PeopleAdd) { mutation.mutate({ data: values }); setTimeout(() => refetchTable(), 400) }; + + return ( +
+ + {/* BASIC INFO */} +
+ + ( + + First Name + + + + + + )} + /> + + ( + + Surname + + + + + + )} + /> +
+ + {/* SECOND ROW */} +
+ + ( + + Middle Name + + + + + + )} + /> + + ( + + Sex Code + + + + + + )} + /> +
+ + {/* THIRD ROW */} +
+ + ( + + Person Ref + + + + + + )} + /> + + ( + + Person Tag + + + + + + )} + /> +
+ + {/* FAMILY INFO */} +
+ + ( + + Father Name + + + + + + )} + /> + + ( + + Mother Name + + + + + + )} + /> +
+ + {/* COUNTRY + NATIONAL ID */} +
+ + ( + + Country Code + + + + + + )} + /> + + ( + + National Identity ID + + + + + + )} + /> +
+ + {/* BIRTH INFO */} +
+ + ( + + Birth Place + + + + + + )} + /> + + ( + + Birth Date + + + + + + )} + /> +
+ + {/* TAX/BIRTHNAME */} +
+ + ( + + Tax No + + + + + + )} + /> + + ( + + Birth Name + + + + + + )} + /> +
+ + + + {/* EXPIRY DATES */} +
+ + ( + + Expiry Starts + + + + + + )} + /> + + ( + + Expiry Ends + + + + + + )} + /> +
+ + + + + ); +}; + +export { PeopleForm } diff --git a/frontend/pages/people/add/page.tsx b/frontend/pages/people/add/page.tsx new file mode 100644 index 0000000..6e951bb --- /dev/null +++ b/frontend/pages/people/add/page.tsx @@ -0,0 +1,26 @@ +'use client'; +import { useState } from 'react'; +import { useGraphQlPeopleList } from '@/pages/people/queries'; +import { PeopleDataTableAdd } from '@/pages/people/add/table/data-table'; +import { PeopleForm } from '@/pages/people/add/form'; + +const PageAddPerson = () => { + 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 } = useGraphQlPeopleList({ limit, skip: (page - 1) * limit, sort, filters }); + + return ( + <> + + + + ) +} + +export { PageAddPerson }; diff --git a/frontend/pages/people/add/queries.tsx b/frontend/pages/people/add/queries.tsx new file mode 100644 index 0000000..f43e498 --- /dev/null +++ b/frontend/pages/people/add/queries.tsx @@ -0,0 +1,25 @@ +'use client' +import { useMutation } from '@tanstack/react-query' +import { PeopleAdd } from './types' +import { toISOIfNotZ } from '@/lib/utils'; + +const fetchGraphQlPeopleAdd = async (record: PeopleAdd): Promise<{ data: PeopleAdd | null; status: number }> => { + console.log('Fetching test data from local API'); + record.birthDate = toISOIfNotZ(record.birthDate || ""); + record.expiryStarts = toISOIfNotZ(record.expiryStarts || ""); + record.expiryEnds = toISOIfNotZ(record.expiryEnds || ""); + try { + const res = await fetch('/api/people/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 useAddPeopleMutation() { + return useMutation({ + mutationFn: ({ data }: { data: PeopleAdd }) => fetchGraphQlPeopleAdd(data), + onSuccess: () => { console.log("People created successfully") }, + onError: (error) => { console.error("Create people failed:", error) }, + }) +} diff --git a/frontend/pages/people/add/schema.ts b/frontend/pages/people/add/schema.ts new file mode 100644 index 0000000..c934757 --- /dev/null +++ b/frontend/pages/people/add/schema.ts @@ -0,0 +1,22 @@ +import { z } from "zod" + +export const personAddSchema = z.object({ + firstName: z.string(), + surname: z.string(), + middleName: z.string().optional(), + sexCode: z.string(), + personRef: z.string().optional(), + personTag: z.string().optional(), + fatherName: z.string().optional(), + motherName: z.string().optional(), + countryCode: z.string(), + nationalIdentityId: z.string(), + birthPlace: z.string(), + birthDate: z.string(), + taxNo: z.string().optional(), + birthname: z.string().optional(), + expiryStarts: z.string(), + expiryEnds: z.string(), +}); + +export type PeopleAdd = z.infer; diff --git a/frontend/pages/people/add/table/columns.tsx b/frontend/pages/people/add/table/columns.tsx new file mode 100644 index 0000000..4e5a52c --- /dev/null +++ b/frontend/pages/people/add/table/columns.tsx @@ -0,0 +1,105 @@ +"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: "uuid", + header: "UUID", + cell: ({ getValue }) => (
{String(getValue())}
), + }, + { + accessorKey: "type", + header: "Type", + }, + { + accessorKey: "token", + header: "Token", + }, + { + accessorKey: "typeToken", + header: "Type Token", + }, + { + accessorKey: "description", + header: "Description", + }, + { + 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/people/add/table/data-table.tsx b/frontend/pages/people/add/table/data-table.tsx new file mode 100644 index 0000000..1a535e2 --- /dev/null +++ b/frontend/pages/people/add/table/data-table.tsx @@ -0,0 +1,287 @@ +"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 { useDeleteUserMutation } from "@/pages/users/queries" + +export function PeopleDataTableAdd({ + 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 = useDeleteUserMutation() + 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/people/add/table/schema.tsx b/frontend/pages/people/add/table/schema.tsx new file mode 100644 index 0000000..a20e89e --- /dev/null +++ b/frontend/pages/people/add/table/schema.tsx @@ -0,0 +1,24 @@ +import { z } from "zod"; + +export const schema = z.object({ + _id: z.string(), + uuid: z.string(), + 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(), + birthname: z.string().optional(), + expiryStarts: z.string().optional(), + expiryEnds: z.string().optional(), +}); + +export type schemaType = z.infer; \ No newline at end of file diff --git a/frontend/pages/people/add/types.ts b/frontend/pages/people/add/types.ts new file mode 100644 index 0000000..e4508a4 --- /dev/null +++ b/frontend/pages/people/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/people/list/columns.tsx b/frontend/pages/people/list/columns.tsx new file mode 100644 index 0000000..dc1f284 --- /dev/null +++ b/frontend/pages/people/list/columns.tsx @@ -0,0 +1,149 @@ +"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: "uuid", + header: "UUID", + cell: ({ getValue }) => (
{String(getValue())}
), + }, + { + accessorKey: "firstName", + header: "First Name", + }, + { + accessorKey: "surname", + header: "Surname", + }, + { + accessorKey: "middleName", + header: "Middle Name", + }, + { + accessorKey: "sexCode", + header: "Sex", + }, + { + accessorKey: "personRef", + header: "Person Ref", + }, + { + accessorKey: "personTag", + header: "Person Tag", + }, + { + accessorKey: "fatherName", + header: "Father Name", + }, + { + accessorKey: "motherName", + header: "Mother Name", + }, + { + accessorKey: "countryCode", + header: "Country", + }, + { + accessorKey: "nationalIdentityId", + header: "National ID", + }, + { + accessorKey: "birthPlace", + header: "Birth Place", + }, + { + accessorKey: "active", + header: "Active", + cell: ({ getValue }) => getValue() ? (
Yes
) : (
No
), + }, + { + accessorKey: "isConfirmed", + header: "Confirmed", + cell: ({ getValue }) => getValue() ? (
Yes
) : (
No
), + }, + { + accessorKey: "birthDate", + header: "Birth Date", + cell: ({ getValue }) => dateToLocaleString(getValue() as string), + }, + { + 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/people/list/data-table.tsx b/frontend/pages/people/list/data-table.tsx new file mode 100644 index 0000000..d07e108 --- /dev/null +++ b/frontend/pages/people/list/data-table.tsx @@ -0,0 +1,280 @@ +"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 { useDeleteUserMutation } from "@/pages/users/queries" + +export function PeopleDataTable({ + data, + totalCount, + currentPage = 1, + pageSize = 10, + 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 = useDeleteUserMutation() + 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, + 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/people/list/schema.tsx b/frontend/pages/people/list/schema.tsx new file mode 100644 index 0000000..02d3e98 --- /dev/null +++ b/frontend/pages/people/list/schema.tsx @@ -0,0 +1,27 @@ +import { z } from "zod"; + +export const schema = z.object({ + _id: z.string(), + uuid: z.string(), + firstName: z.string().nullable().optional(), + surname: z.string().nullable().optional(), + middleName: z.string().nullable().optional(), + sexCode: z.string().nullable().optional(), + personRef: z.string().nullable().optional(), + personTag: z.string().nullable().optional(), + fatherName: z.string().nullable().optional(), + motherName: z.string().nullable().optional(), + countryCode: z.string().nullable().optional(), + nationalIdentityId: z.string().nullable().optional(), + birthPlace: z.string().nullable().optional(), + birthDate: z.string().nullable().optional(), + taxNo: z.string().nullable().optional(), + birthname: z.string().nullable().optional(), + expiryStarts: z.string().nullable().optional(), + expiryEnds: z.string().nullable().optional(), + createdAt: z.string().nullable().optional(), + updatedAt: z.string().nullable().optional(), +}); + + +export type schemaType = z.infer; \ No newline at end of file diff --git a/frontend/pages/people/page.tsx b/frontend/pages/people/page.tsx new file mode 100644 index 0000000..77cbb18 --- /dev/null +++ b/frontend/pages/people/page.tsx @@ -0,0 +1,22 @@ +'use client'; +import { useGraphQlPeopleList } from './queries'; +import { useState } from 'react'; +import { PeopleDataTable } from '@/pages/people/list/data-table'; + +const PagePeople = () => { + 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 } = useGraphQlPeopleList({ limit, skip: (page - 1) * limit, sort, filters }); + + const handlePageChange = (newPage: number) => { setPage(newPage) }; + const handlePageSizeChange = (newSize: number) => { setLimit(newSize); setPage(1) }; + if (isLoading) { return
Loading...
} + if (error) { return
Error loading users
} + + return ; +}; + +export { PagePeople }; diff --git a/frontend/pages/people/queries.tsx b/frontend/pages/people/queries.tsx new file mode 100644 index 0000000..622cc8e --- /dev/null +++ b/frontend/pages/people/queries.tsx @@ -0,0 +1,37 @@ +'use client' +import { useQuery, useMutation } from '@tanstack/react-query' +import { PeopleListResponse } from './types' +import { ListArguments } from '@/types/listRequest' + +const fetchGraphQlPeopleList = async (params: ListArguments): Promise => { + console.log('Fetching test data from local API'); + const { limit, skip, sort, filters } = params; + try { + const res = await fetch('/api/people/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 fetchGraphQlDeletePerson = async (uuid: string): Promise => { + console.log('Fetching test data from local API'); + try { + const res = await fetch(`/api/people/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 useGraphQlPeopleList(params: ListArguments) { + return useQuery({ queryKey: ['graphql-people-list', params], queryFn: () => fetchGraphQlPeopleList(params) }) +} + +export function useDeletePersonMutation() { + return useMutation({ + mutationFn: ({ uuid }: { uuid: string }) => fetchGraphQlDeletePerson(uuid), + onSuccess: () => { console.log("Person deleted successfully") }, + onError: (error) => { console.error("Delete person failed:", error) }, + }) +} diff --git a/frontend/pages/people/types.ts b/frontend/pages/people/types.ts new file mode 100644 index 0000000..e04a431 --- /dev/null +++ b/frontend/pages/people/types.ts @@ -0,0 +1,31 @@ + +interface People { + _id: string; + uuid: string; + 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; + createdAt: string; + updatedAt: string; + expiryStarts: string; + expiryEnds: string; +} + + +interface PeopleListResponse { + data: People[] | null; + totalCount: number | null; +} + +export type { People, PeopleListResponse } \ No newline at end of file diff --git a/frontend/pages/people/update/form.tsx b/frontend/pages/people/update/form.tsx new file mode 100644 index 0000000..7ffac4c --- /dev/null +++ b/frontend/pages/people/update/form.tsx @@ -0,0 +1,294 @@ +"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 { useUpdatePeopleMutation } from "@/pages/people/update/queries" +import { personUpdateSchema, type PeopleUpdate } from "@/pages/people/update/schema" + +const PeopleForm = ({ refetchTable, initData, selectedUuid }: { refetchTable: () => void, initData: PeopleUpdate, selectedUuid: string }) => { + + const form = useForm({ resolver: zodResolver(personUpdateSchema), defaultValues: { ...initData } }) + + const { handleSubmit } = form + + const mutation = useUpdatePeopleMutation(); + + function onSubmit(values: PeopleUpdate) { mutation.mutate({ data: values as any || initData, uuid: selectedUuid }); setTimeout(() => refetchTable(), 400) } + + return ( +
+ + {/* BASIC INFO */} +
+ + ( + + First Name + + + + + + )} + /> + + ( + + Surname + + + + + + )} + /> +
+ + {/* SECOND ROW */} +
+ + ( + + Middle Name + + + + + + )} + /> + + ( + + Sex Code + + + + + + )} + /> +
+ + {/* THIRD ROW */} +
+ + ( + + Person Ref + + + + + + )} + /> + + ( + + Person Tag + + + + + + )} + /> +
+ + {/* FAMILY INFO */} +
+ + ( + + Father Name + + + + + + )} + /> + + ( + + Mother Name + + + + + + )} + /> +
+ + {/* COUNTRY + NATIONAL ID */} +
+ + ( + + Country Code + + + + + + )} + /> + + ( + + National Identity ID + + + + + + )} + /> +
+ + {/* BIRTH INFO */} +
+ + ( + + Birth Place + + + + + + )} + /> + + ( + + Birth Date + + + + + + )} + /> +
+ + {/* TAX/BIRTHNAME */} +
+ + ( + + Tax No + + + + + + )} + /> + + ( + + Birth Name + + + + + + )} + /> +
+ + + + {/* EXPIRY DATES */} +
+ + ( + + Expiry Starts + + + + + + )} + /> + + ( + + Expiry Ends + + + + + + )} + /> +
+ + + + + ); +} + +export { PeopleForm } diff --git a/frontend/pages/people/update/page.tsx b/frontend/pages/people/update/page.tsx new file mode 100644 index 0000000..280caeb --- /dev/null +++ b/frontend/pages/people/update/page.tsx @@ -0,0 +1,36 @@ +'use client'; +import { useState } from 'react'; +import { useGraphQlPeopleList } from '@/pages/people/queries'; +import { PeopleDataTableUpdate } from '@/pages/people/update/table/data-table'; +import { PeopleForm } from '@/pages/people/update/form'; +import { useSearchParams, useRouter } from 'next/navigation' +import { Button } from '@/components/ui/button'; + +const PageUpdatePeople = () => { + 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 backToPeople = <> +
UUID not found in search params
+ + + if (!uuid) { return backToPeople } + const { data, isLoading, error, refetch } = useGraphQlPeopleList({ limit, skip: (page - 1) * limit, sort, filters: { ...filters, uuid } }); + const initData = data?.data?.[0] || null; + if (!initData) { return backToPeople } + return ( + <> + + + + ) +} + +export { PageUpdatePeople }; diff --git a/frontend/pages/people/update/queries.tsx b/frontend/pages/people/update/queries.tsx new file mode 100644 index 0000000..fe1ffa6 --- /dev/null +++ b/frontend/pages/people/update/queries.tsx @@ -0,0 +1,21 @@ +'use client' +import { useMutation } from '@tanstack/react-query' +import { UserUpdate } from './types'; + +const fetchGraphQlPeopleUpdate = async (record: UserUpdate, uuid: string): Promise<{ data: UserUpdate | null; status: number }> => { + console.log('Fetching test data from local API'); + try { + const res = await fetch(`/api/people/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 useUpdatePeopleMutation() { + return useMutation({ + mutationFn: ({ data, uuid }: { data: UserUpdate, uuid: string }) => fetchGraphQlPeopleUpdate(data, uuid), + onSuccess: () => { console.log("People updated successfully") }, + onError: (error) => { console.error("Update people failed:", error) }, + }) +} diff --git a/frontend/pages/people/update/schema.ts b/frontend/pages/people/update/schema.ts new file mode 100644 index 0000000..15a76a6 --- /dev/null +++ b/frontend/pages/people/update/schema.ts @@ -0,0 +1,22 @@ +import { z } from "zod" + +export const personUpdateSchema = z.object({ + firstName: z.string(), + surname: z.string(), + middleName: z.string().optional(), + sexCode: z.string(), + 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(), + birthname: z.string().optional(), + expiryStarts: z.string().optional(), + expiryEnds: z.string().optional(), +}); + +export type PeopleUpdate = z.infer; diff --git a/frontend/pages/people/update/table/columns.tsx b/frontend/pages/people/update/table/columns.tsx new file mode 100644 index 0000000..36c9dd0 --- /dev/null +++ b/frontend/pages/people/update/table/columns.tsx @@ -0,0 +1,251 @@ +"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 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 ( + + ) +} + +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: "uuid", + header: "UUID", + cell: ({ getValue }) => (
{String(getValue())}
), + }, + { + accessorKey: "firstName", + header: "First Name", + }, + { + accessorKey: "surname", + header: "Surname", + }, + { + accessorKey: "middleName", + header: "Middle Name", + }, + { + accessorKey: "sexCode", + header: "Sex", + }, + { + accessorKey: "personRef", + header: "Person Ref", + }, + { + accessorKey: "personTag", + header: "Person Tag", + }, + { + accessorKey: "fatherName", + header: "Father Name", + }, + { + accessorKey: "motherName", + header: "Mother Name", + }, + { + accessorKey: "countryCode", + header: "Country", + }, + { + accessorKey: "nationalIdentityId", + header: "National ID", + }, + { + accessorKey: "birthPlace", + header: "Birth Place", + }, + { + accessorKey: "active", + header: "Active", + cell: ({ getValue }) => getValue() ? (
Yes
) : (
No
), + }, + { + accessorKey: "isConfirmed", + header: "Confirmed", + cell: ({ getValue }) => getValue() ? (
Yes
) : (
No
), + }, + { + accessorKey: "birthDate", + header: "Birth Date", + cell: ({ getValue }) => dateToLocaleString(getValue() as string), + }, + { + 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/people/update/table/data-table.tsx b/frontend/pages/people/update/table/data-table.tsx new file mode 100644 index 0000000..17f2eeb --- /dev/null +++ b/frontend/pages/people/update/table/data-table.tsx @@ -0,0 +1,280 @@ +"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 PeopleDataTableUpdate({ + 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 = useDeletePersonMutation() + 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/people/update/table/schema.tsx b/frontend/pages/people/update/table/schema.tsx new file mode 100644 index 0000000..52413d2 --- /dev/null +++ b/frontend/pages/people/update/table/schema.tsx @@ -0,0 +1,47 @@ +import { z } from "zod"; + +export const schema = z.object({ + _id: z.string(), + uuid: z.string().nullable().optional(), + expiryStarts: z.string().nullable().optional(), + expiryEnds: z.string().nullable().optional(), + isConfirmed: z.boolean().nullable().optional(), + deleted: z.boolean().nullable().optional(), + active: z.boolean().nullable().optional(), + crypUuId: z.string().nullable().optional(), + createdCredentialsToken: z.string().nullable().optional(), + updatedCredentialsToken: z.string().nullable().optional(), + confirmedCredentialsToken: z.string().nullable().optional(), + isNotificationSend: z.boolean().nullable().optional(), + isEmailSend: z.boolean().nullable().optional(), + refInt: z.number().nullable().optional(), + refId: z.string().nullable().optional(), + replicationId: z.number().nullable().optional(), + expiresAt: z.string().nullable().optional(), + resetToken: z.string().nullable().optional(), + password: z.string().nullable().optional(), + history: z.array(z.string()).optional(), + tag: z.string().nullable().optional(), + email: z.string().nullable().optional(), + phone: z.string().nullable().optional(), + + collectionTokens: z + .object({ + default: z.string().nullable().optional(), + tokens: z + .array( + z.object({ + prefix: z.string(), + token: z.string(), + }) + ) + .optional(), + }) + .nullable() + .optional(), + + createdAt: z.string().nullable().optional(), + updatedAt: z.string().nullable().optional(), +}); + +export type schemaType = z.infer; diff --git a/frontend/pages/people/update/types.ts b/frontend/pages/people/update/types.ts new file mode 100644 index 0000000..55d8649 --- /dev/null +++ b/frontend/pages/people/update/types.ts @@ -0,0 +1,24 @@ +interface UserToken { + prefix: string; + token: string; +} + +interface CollectionTokens { + default: string; + tokens: UserToken[]; +} + +interface UserUpdate { + expiryStarts: string; + expiryEnds: string; + isConfirmed: boolean; + isNotificationSend: boolean; + password: string; + tag: string; + email: string; + phone: string; + collectionTokens: CollectionTokens; +} + + +export type { UserUpdate }; \ No newline at end of file diff --git a/frontend/pages/users/add/form.tsx b/frontend/pages/users/add/form.tsx index d9fc4a0..19db98c 100644 --- a/frontend/pages/users/add/form.tsx +++ b/frontend/pages/users/add/form.tsx @@ -32,24 +32,15 @@ const UserForm = ({ refetchTable }: { refetchTable: () => void }) => { const { control, handleSubmit } = form - const { fields, append, remove } = useFieldArray({ - control, - name: "collectionTokens.tokens", - }) + const { fields, append, remove } = useFieldArray({ control, name: "collectionTokens.tokens" }) const mutation = useAddUserMutation(); - function onSubmit(values: UserAdd) { - mutation.mutate(values as any); - setTimeout(() => refetchTable(), 200); - } + function onSubmit(values: UserAdd) { mutation.mutate(values as any); setTimeout(() => refetchTable(), 400) } return (
- + {/* BASIC INFO */}
=> { console.log('Fetching test data from local API'); + record.expiryStarts = toISOIfNotZ(record.expiryStarts); + record.expiryEnds = toISOIfNotZ(record.expiryEnds); try { const res = await fetch('/api/users/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}`) } diff --git a/frontend/pages/users/queries.tsx b/frontend/pages/users/queries.tsx index 5b423ab..226e25a 100644 --- a/frontend/pages/users/queries.tsx +++ b/frontend/pages/users/queries.tsx @@ -35,5 +35,3 @@ export function useDeleteUserMutation() { onError: (error) => { console.error("Delete user failed:", error) }, }) } - - diff --git a/frontend/pages/users/update/form.tsx b/frontend/pages/users/update/form.tsx index 7a1735c..9c939e8 100644 --- a/frontend/pages/users/update/form.tsx +++ b/frontend/pages/users/update/form.tsx @@ -1,16 +1,17 @@ "use client" import { useFieldArray, useForm } from "react-hook-form" import { zodResolver } from "@hookform/resolvers/zod" -import { userUpdateSchema, type UserUpdate } from "./schema" import { Form, FormField, FormItem, FormLabel, FormControl, FormMessage } from "@/components/ui/form" import { Input } from "@/components/ui/input" import { Button } from "@/components/ui/button" import { Checkbox } from "@/components/ui/checkbox" import { Separator } from "@/components/ui/separator" import { DateTimePicker } from "@/components/ui/date-time-picker" -import { useUpdateUserMutation } from "./queries" +import { useUpdateUserMutation } from "@/pages/users/update/queries" +import { userUpdateSchema, type UserUpdate } from "@/pages/users/update/schema" const UserForm = ({ refetchTable, initData, selectedUuid }: { refetchTable: () => void, initData: UserUpdate, selectedUuid: string }) => { + const form = useForm({ resolver: zodResolver(userUpdateSchema), defaultValues: { @@ -31,9 +32,7 @@ const UserForm = ({ refetchTable, initData, selectedUuid }: { refetchTable: () = const mutation = useUpdateUserMutation(); - function onSubmit(values: UserUpdate) { - mutation.mutate({ data: values as any || initData, uuid: selectedUuid }); setTimeout(() => refetchTable(), 200) - } + function onSubmit(values: UserUpdate) { mutation.mutate({ data: values as any || initData, uuid: selectedUuid }); setTimeout(() => refetchTable(), 400) } return ( diff --git a/frontend/pages/users/update/table/columns.tsx b/frontend/pages/users/update/table/columns.tsx index e34fae6..1098cff 100644 --- a/frontend/pages/users/update/table/columns.tsx +++ b/frontend/pages/users/update/table/columns.tsx @@ -1,14 +1,7 @@ "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" @@ -16,117 +9,6 @@ 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 ( - - ) -} export function DraggableRow({ row }: { row: Row> }) { const { transform, transition, setNodeRef, isDragging } = useSortable({ id: row.original._id }) @@ -142,7 +24,7 @@ export function DraggableRow({ row }: { row: Row> }) { ) } -function getColumns(deleteHandler: (id: string) => void): ColumnDef[] { +function getColumns(router: any, deleteHandler: (id: string) => void): ColumnDef[] { return [ { accessorKey: "uuid", @@ -150,16 +32,48 @@ function getColumns(deleteHandler: (id: string) => void): ColumnDef[ cell: ({ getValue }) => (
{String(getValue())}
), }, { - accessorKey: "email", - header: "Email", + accessorKey: "firstName", + header: "First Name", }, { - accessorKey: "phone", - header: "Phone", + accessorKey: "surname", + header: "Surname", }, { - accessorKey: "tag", - header: "Tag", + accessorKey: "middleName", + header: "Middle Name", + }, + { + accessorKey: "sexCode", + header: "Sex", + }, + { + accessorKey: "personRef", + header: "Person Ref", + }, + { + accessorKey: "personTag", + header: "Person Tag", + }, + { + accessorKey: "fatherName", + header: "Father Name", + }, + { + accessorKey: "motherName", + header: "Mother Name", + }, + { + accessorKey: "countryCode", + header: "Country", + }, + { + accessorKey: "nationalIdentityId", + header: "National ID", + }, + { + accessorKey: "birthPlace", + header: "Birth Place", }, { accessorKey: "active", @@ -171,6 +85,11 @@ function getColumns(deleteHandler: (id: string) => void): ColumnDef[ header: "Confirmed", cell: ({ getValue }) => getValue() ? (
Yes
) : (
No
), }, + { + accessorKey: "birthDate", + header: "Birth Date", + cell: ({ getValue }) => dateToLocaleString(getValue() as string), + }, { accessorKey: "createdAt", header: "Created", @@ -191,69 +110,15 @@ function getColumns(deleteHandler: (id: string) => void): ColumnDef[ header: "Expiry Ends", cell: ({ getValue }) => getValue() ? dateToLocaleString(getValue() as string) : "-", }, - { - accessorKey: "crypUuId", - header: "Encrypted UUID", - }, - { - accessorKey: "history", - header: "History", - cell: ({ getValue }) => (
{String(getValue() ?? "")}
), - }, - { - accessorKey: "collectionTokens.tokens", - header: "Tokens", - cell: ({ row }) => { - const tokens = row.original.collectionTokens?.tokens ?? []; - const defaultToken = row.original.collectionTokens?.default; - if (!tokens.length) return "-"; - return ( - - - - - - Collection Tokens - - {defaultToken && ( - <> - -
- Default - - {defaultToken} - -
-
- - - )} - {tokens.map((t, i) => ( - -
- {t.prefix} - - {t.token} - -
-
- ))} -
-
- ); - }, - }, { id: "actions", header: "Actions", cell: ({ row }) => { return (
- {/* */} + diff --git a/frontend/pages/users/update/table/data-table.tsx b/frontend/pages/users/update/table/data-table.tsx index ee9c267..c9fe529 100644 --- a/frontend/pages/users/update/table/data-table.tsx +++ b/frontend/pages/users/update/table/data-table.tsx @@ -101,7 +101,7 @@ export function UserDataTableUpdate({ const deleteMutation = useDeleteUserMutation() const deleteHandler = (id: string) => { deleteMutation.mutate({ uuid: id }); setTimeout(() => { refetchTable() }, 200) } - const columns = getColumns(deleteHandler); + const columns = getColumns(router, deleteHandler); const pagination = React.useMemo(() => ({ pageIndex: currentPage - 1, pageSize: pageSize, }), [currentPage, pageSize]) const totalPages = Math.ceil(totalCount / pageSize) @@ -129,11 +129,7 @@ export function UserDataTableUpdate({ getFacetedUniqueValues: getFacetedUniqueValues(), }) - const handlePageSizeChange = (value: string) => { - const newSize = Number(value); - onPageSizeChange(newSize); - onPageChange(1); - } + const handlePageSizeChange = (value: string) => { const newSize = Number(value); onPageSizeChange(newSize); onPageChange(1) } return ( { + let resolver: BuildIbanResolver; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [BuildIbanResolver], + }).compile(); + + resolver = module.get(BuildIbanResolver); + }); + + it('should be defined', () => { + expect(resolver).toBeDefined(); + }); +}); diff --git a/src/build-iban/build-iban.resolver.ts b/src/build-iban/build-iban.resolver.ts new file mode 100644 index 0000000..7b93325 --- /dev/null +++ b/src/build-iban/build-iban.resolver.ts @@ -0,0 +1,4 @@ +import { Resolver } from '@nestjs/graphql'; + +@Resolver() +export class BuildIbanResolver {} diff --git a/src/build-iban/build-iban.service.spec.ts b/src/build-iban/build-iban.service.spec.ts new file mode 100644 index 0000000..c9e449c --- /dev/null +++ b/src/build-iban/build-iban.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { BuildIbanService } from './build-iban.service'; + +describe('BuildIbanService', () => { + let service: BuildIbanService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [BuildIbanService], + }).compile(); + + service = module.get(BuildIbanService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/src/build-iban/build-iban.service.ts b/src/build-iban/build-iban.service.ts new file mode 100644 index 0000000..1369f3f --- /dev/null +++ b/src/build-iban/build-iban.service.ts @@ -0,0 +1,4 @@ +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class BuildIbanService {}