diff --git a/backend/src/app.module.ts b/backend/src/app.module.ts index c074a9f..e51ef89 100644 --- a/backend/src/app.module.ts +++ b/backend/src/app.module.ts @@ -6,7 +6,7 @@ import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo'; import { MongooseModule } from '@nestjs/mongoose'; import { UsersModule } from './users/users.module'; import { PeopleModule } from './people/people.module'; -import { BuildModule } from './build/build.module'; +import { BuildModule } from './builds/build.module'; import { BuildPartsModule } from './build-parts/build-parts.module'; import { BuildAreaModule } from './build-area/build-area.module'; import { UserTypesModule } from './user-types/user-types.module'; diff --git a/backend/src/build-iban/dto/update-build-ibans.input.ts b/backend/src/build-iban/dto/update-build-ibans.input.ts index f5fc2c9..f7fc57e 100644 --- a/backend/src/build-iban/dto/update-build-ibans.input.ts +++ b/backend/src/build-iban/dto/update-build-ibans.input.ts @@ -1,7 +1,8 @@ +import { ExpiryBaseInput } from "@/models/base.model"; import { InputType, Field } from "@nestjs/graphql"; @InputType() -export class UpdateBuildIbanInput { +export class UpdateBuildIbanInput extends ExpiryBaseInput { @Field({ nullable: true }) iban?: string; diff --git a/backend/src/build-parts/dto/create-build-part.input.ts b/backend/src/build-parts/dto/create-build-part.input.ts index d8cea58..534f6d5 100644 --- a/backend/src/build-parts/dto/create-build-part.input.ts +++ b/backend/src/build-parts/dto/create-build-part.input.ts @@ -1,10 +1,10 @@ -import { InputType, Field } from '@nestjs/graphql'; +import { InputType, Field, ID } from '@nestjs/graphql'; @InputType() export class CreateBuildPartsInput { - @Field() - uuid: string; + @Field(() => ID) + build: string; } diff --git a/backend/src/build-sites/dto/list-build-sites.response.ts b/backend/src/build-sites/dto/list-build-sites.response.ts index 817c205..ecfbd33 100644 --- a/backend/src/build-sites/dto/list-build-sites.response.ts +++ b/backend/src/build-sites/dto/list-build-sites.response.ts @@ -1,7 +1,6 @@ import { ObjectType, Field } from "@nestjs/graphql"; import { BuildSites } from "@/models/build-site.model"; - @ObjectType() export class ListBuildSitesResponse { diff --git a/backend/src/build/build.resolver.ts b/backend/src/build/build.resolver.ts deleted file mode 100644 index 7cf767e..0000000 --- a/backend/src/build/build.resolver.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { Resolver, Query, Args, ID, Info, Mutation } from '@nestjs/graphql'; -import { Types } from 'mongoose'; -import { Build } from '@/models/build.model'; -import { CreateBuildInput } from './dto/create-build.input'; -import graphqlFields from 'graphql-fields'; -import { BuildService } from './build.service'; -import type { GraphQLResolveInfo } from 'graphql'; - -@Resolver() -export class BuildResolver { - - constructor(private readonly buildService: BuildService) { } - - @Query(() => [Build], { name: 'Builds' }) - async getBuilds(@Info() info: GraphQLResolveInfo): Promise { - const fields = graphqlFields(info); const projection = this.buildService.buildProjection(fields); return this.buildService.findAll(projection); - } - - @Query(() => Build, { name: 'Build', nullable: true }) - async getBuild(@Args('id', { type: () => ID }) id: string, @Info() info: GraphQLResolveInfo): Promise { - const fields = graphqlFields(info); const projection = this.buildService.buildProjection(fields); return this.buildService.findById(new Types.ObjectId(id), projection); - } - - @Mutation(() => Build, { name: 'createBuild' }) - async createBuild(@Args('input') input: CreateBuildInput): Promise { - return this.buildService.create(input); - } - -} diff --git a/backend/src/build/build.service.ts b/backend/src/build/build.service.ts deleted file mode 100644 index cd989b0..0000000 --- a/backend/src/build/build.service.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { InjectModel } from '@nestjs/mongoose'; -import { Types, Model } from 'mongoose'; -import { Build, BuildDocument } from '@/models/build.model'; -import { CreateBuildInput } from './dto/create-build.input'; - -@Injectable() -export class BuildService { - - constructor(@InjectModel(Build.name) private readonly buildModel: Model) { } - - async findAll(projection?: any): Promise { - return this.buildModel.find({}, projection, { lean: false }).populate({ path: 'buildArea', select: projection?.buildArea }).exec(); - } - - async findById(id: Types.ObjectId, projection?: any): Promise { - return this.buildModel.findById(id, projection, { lean: false }).populate({ path: 'buildArea', select: projection?.buildArea }).exec(); - } - - async create(input: CreateBuildInput): Promise { const buildArea = new this.buildModel(input); return buildArea.save() } - - buildProjection(fields: Record): any { - const projection: any = {}; - for (const key in fields) { - if (key === 'buildArea' && typeof fields[key] === 'object') { for (const subField of Object.keys(fields[key])) { projection[`buildArea.${subField}`] = 1 } } - else { projection[key] = 1 } - } - return projection; - } -} diff --git a/backend/src/build/dto/update-build.input.ts b/backend/src/build/dto/update-build.input.ts deleted file mode 100644 index 4b3e83c..0000000 --- a/backend/src/build/dto/update-build.input.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { InputType, Field } from "@nestjs/graphql"; - -@InputType() -export class UpdateBuildAttributeInput { - - @Field({ nullable: true }) - buildType?: string; - - @Field({ nullable: true }) - site?: string; - - @Field({ nullable: true }) - address?: string; - - @Field({ nullable: true }) - areas?: string[]; - -} - -@InputType() -export class UpdateBuildResponsibleInput { - - @Field({ nullable: true }) - company?: string; - - @Field({ nullable: true }) - person?: string; - -} diff --git a/backend/src/build/build.module.ts b/backend/src/builds/build.module.ts similarity index 100% rename from backend/src/build/build.module.ts rename to backend/src/builds/build.module.ts diff --git a/backend/src/build/build.queries.graphql b/backend/src/builds/build.queries.graphql similarity index 100% rename from backend/src/build/build.queries.graphql rename to backend/src/builds/build.queries.graphql diff --git a/backend/src/build/build.resolver.spec.ts b/backend/src/builds/build.resolver.spec.ts similarity index 100% rename from backend/src/build/build.resolver.spec.ts rename to backend/src/builds/build.resolver.spec.ts diff --git a/backend/src/builds/build.resolver.ts b/backend/src/builds/build.resolver.ts new file mode 100644 index 0000000..5394862 --- /dev/null +++ b/backend/src/builds/build.resolver.ts @@ -0,0 +1,40 @@ +import { Resolver, Query, Args, ID, Info, Mutation } from '@nestjs/graphql'; +import { Types } from 'mongoose'; +import { Build } from '@/models/build.model'; +import { CreateBuildInput } from './dto/create-build.input'; +import { UpdateBuildResponsibleInput, UpdateBuildAttributeInput } from './dto/update-build.input'; +import graphqlFields from 'graphql-fields'; +import { BuildService } from './build.service'; +import type { GraphQLResolveInfo } from 'graphql'; +import { ListArguments } from '@/dto/list.input'; +import { ListBuildResponse } from './dto/list-build.response'; + +@Resolver() +export class BuildResolver { + + constructor(private readonly buildService: BuildService) { } + + @Query(() => ListBuildResponse, { name: 'builds' }) + async getBuilds(@Info() info: GraphQLResolveInfo, @Args('input') input: ListArguments): Promise { + const fields = graphqlFields(info); const projection = this.buildService.buildProjection(fields); return this.buildService.findAll(projection, input.skip, input.limit, input.sort, input.filters); + } + + @Query(() => Build, { name: 'build', nullable: true }) + async getBuild(@Args('id', { type: () => ID }) id: string, @Info() info: GraphQLResolveInfo): Promise { + const fields = graphqlFields(info); const projection = this.buildService.buildProjection(fields); return this.buildService.findById(new Types.ObjectId(id), projection); + } + + @Mutation(() => Build, { name: 'createBuild' }) + async createBuild(@Args('input') input: CreateBuildInput): Promise { return this.buildService.create(input) } + + @Mutation(() => Build, { name: 'updateBuildResponsible' }) + async updateBuildResponsible(@Args('uuid', { type: () => ID }) uuid: string, @Args('input') input: UpdateBuildResponsibleInput): Promise { + return this.buildService.updateResponsible(uuid, input); + } + + @Mutation(() => Build, { name: 'updateBuildAttribute' }) + async updateBuildAttribute(@Args('uuid', { type: () => ID }) uuid: string, @Args('input') input: UpdateBuildAttributeInput): Promise { + return this.buildService.updateAttribute(uuid, input); + } + +} diff --git a/backend/src/build/build.service.spec.ts b/backend/src/builds/build.service.spec.ts similarity index 100% rename from backend/src/build/build.service.spec.ts rename to backend/src/builds/build.service.spec.ts diff --git a/backend/src/builds/build.service.ts b/backend/src/builds/build.service.ts new file mode 100644 index 0000000..73ee045 --- /dev/null +++ b/backend/src/builds/build.service.ts @@ -0,0 +1,44 @@ +import { Injectable } from '@nestjs/common'; +import { InjectModel } from '@nestjs/mongoose'; +import { Types, Model } from 'mongoose'; +import { Build, BuildDocument } from '@/models/build.model'; +import { CreateBuildInput } from './dto/create-build.input'; +import { UpdateBuildAttributeInput, UpdateBuildResponsibleInput } from './dto/update-build.input'; +import { ListBuildResponse } from './dto/list-build.response'; + +@Injectable() +export class BuildService { + + constructor(@InjectModel(Build.name) private readonly buildModel: 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.buildModel.countDocuments(query).exec(); + const data = await this.buildModel.find(query).skip(skip).limit(limit).sort(sort).exec(); + return { data, totalCount }; + } + + async findById(id: Types.ObjectId, projection?: any): Promise { + return this.buildModel.findById(id, projection, { lean: false }).populate({ path: 'buildArea', select: projection?.buildArea }).exec(); + } + + async create(input: CreateBuildInput): Promise { const buildArea = new this.buildModel(input); return buildArea.save() } + + async updateResponsible(uuid: string, input: UpdateBuildResponsibleInput): Promise { + const build = await this.buildModel.findOne({ uuid }); if (!build) { throw new Error('Build not found') }; build.set({ responsible: input }); return build.save() + } + + async updateAttribute(uuid: string, input: UpdateBuildAttributeInput): Promise { + const build = await this.buildModel.findOne({ uuid }); if (!build) { throw new Error('Build not found') }; build.set({ attribute: input }); return build.save() + } + + buildProjection(fields: Record): any { + const projection: any = {}; + for (const key in fields) { + if (key === 'build' && typeof fields[key] === 'object') { for (const subField of Object.keys(fields[key])) { projection[`build.${subField}`] = 1 } } + else { projection[key] = 1 } + } + return projection; + } + +} diff --git a/backend/src/build/dto/create-build.input.ts b/backend/src/builds/dto/create-build.input.ts similarity index 89% rename from backend/src/build/dto/create-build.input.ts rename to backend/src/builds/dto/create-build.input.ts index cac2b71..5d625a1 100644 --- a/backend/src/build/dto/create-build.input.ts +++ b/backend/src/builds/dto/create-build.input.ts @@ -1,3 +1,4 @@ +import { ExpiryBaseInput } from "@/models/base.model"; import { InputType, Field, ID } from "@nestjs/graphql"; @InputType() @@ -54,7 +55,7 @@ export class CreateBuildInfoInput { } @InputType() -export class CreateBuildInput { +export class CreateBuildInput extends ExpiryBaseInput { @Field(() => ID) buildType: string; diff --git a/backend/src/builds/dto/list-build.response.ts b/backend/src/builds/dto/list-build.response.ts new file mode 100644 index 0000000..d3a508c --- /dev/null +++ b/backend/src/builds/dto/list-build.response.ts @@ -0,0 +1,13 @@ +import { Build } from "@/models/build.model"; +import { Field, Int, ObjectType } from "@nestjs/graphql"; + +@ObjectType() +export class ListBuildResponse { + + @Field(() => [Build], { nullable: true }) + data?: Build[]; + + @Field(() => Int, { nullable: true }) + totalCount?: number; + +} diff --git a/backend/src/builds/dto/update-build.input.ts b/backend/src/builds/dto/update-build.input.ts new file mode 100644 index 0000000..dea51bc --- /dev/null +++ b/backend/src/builds/dto/update-build.input.ts @@ -0,0 +1,43 @@ +import { InputType, Field, ID } from "@nestjs/graphql"; + +@InputType() +export class UpdateResponsibleInput { + + @Field(() => ID, { nullable: true }) + company?: string; + + @Field(() => ID, { nullable: true }) + person?: string; + +} + +@InputType() +export class UpdateBuildAttributeInput { + + @Field(() => ID, { nullable: true }) + site?: string; + + @Field(() => ID, { nullable: true }) + address?: string; + + @Field(() => [ID], { nullable: true }) + areas?: string[]; + + @Field(() => [ID], { nullable: true }) + ibans?: string[]; + + @Field(() => [UpdateResponsibleInput], { nullable: true }) + responsibles?: UpdateResponsibleInput[]; + +} + +@InputType() +export class UpdateBuildResponsibleInput { + + @Field(() => ID, { nullable: true }) + company?: string; + + @Field(() => ID, { nullable: true }) + person?: string; + +} diff --git a/backend/src/builds/example.graphql b/backend/src/builds/example.graphql new file mode 100644 index 0000000..599bca8 --- /dev/null +++ b/backend/src/builds/example.graphql @@ -0,0 +1,62 @@ +mutation CreateBuild($input: CreateBuildInput!) { + createBuild(input: $input) { + _id + uuid + createdAt + expiryStarts + expiryEnds + buildType + collectionToken + ibans + responsibles { + company + person + } + areas + info { + govAddressCode + buildName + buildNo + maxFloor + undergroundFloor + buildDate + decisionPeriodDate + taxNo + liftCount + heatingSystem + coolingSystem + hotWaterSystem + blockServiceManCount + securityServiceManCount + garageCount + managementRoomId + } + } +} + +{ + "input": { + "expiryStarts": "2025-01-10T08:00:00.000Z", + "expiryEnds": "2099-12-31T00:00:00.000Z", + "buildType": "675abc12ef90123456789aaa", + "collectionToken": "COLL-TEST-2025-XYZ", + "info": { + "govAddressCode": "TR-34-12345", + "buildName": "Aether Residence", + "buildNo": "B-12", + "maxFloor": 12, + "undergroundFloor": 2, + "buildDate": "2020-06-15T00:00:00.000Z", + "decisionPeriodDate": "2020-05-01T00:00:00.000Z", + "taxNo": "12345678901", + "liftCount": 3, + "heatingSystem": true, + "coolingSystem": true, + "hotWaterSystem": true, + "blockServiceManCount": 2, + "securityServiceManCount": 1, + "garageCount": 30, + "managementRoomId": 101 + } + } +} diff --git a/backend/src/models/base.model.ts b/backend/src/models/base.model.ts index 031077f..1a31f3f 100644 --- a/backend/src/models/base.model.ts +++ b/backend/src/models/base.model.ts @@ -1,75 +1,75 @@ import { Prop } from '@nestjs/mongoose'; import { randomUUID } from 'crypto'; -import { ObjectType, InputType, Field, ID } from '@nestjs/graphql'; +import { ObjectType, InputType, Field } from '@nestjs/graphql'; @ObjectType({ isAbstract: true }) export class Base { - @Field() - @Prop({ default: randomUUID, unique: true }) + @Field({ nullable: true }) + @Prop({ default: () => randomUUID(), unique: true }) uuid: string; - @Field() + @Field({ nullable: true }) @Prop({ default: () => new Date(Date.now()) }) createdAt: Date; - @Field() + @Field({ nullable: true }) @Prop({ default: () => new Date(Date.now()) }) updatedAt: Date; - @Field() + @Field({ nullable: true }) @Prop({ default: false, required: false }) isConfirmed?: boolean; - @Field() + @Field({ nullable: true }) @Prop({ default: false, required: false }) deleted?: boolean; - @Field() + @Field({ nullable: true }) @Prop({ default: true, required: false }) active?: boolean; - @Field() + @Field({ nullable: true }) @Prop({ default: randomUUID }) crypUuId: string; - @Field() + @Field({ nullable: true }) @Prop({ default: randomUUID }) createdCredentialsToken: string; - @Field() + @Field({ nullable: true }) @Prop({ default: randomUUID }) updatedCredentialsToken: string; - @Field() + @Field({ nullable: true }) @Prop({ default: randomUUID }) confirmedCredentialsToken: string; - @Field() + @Field({ nullable: true }) @Prop({ default: false, required: false }) isNotificationSend?: boolean; - @Field() + @Field({ nullable: true }) @Prop({ default: false, required: false }) isEmailSend?: boolean; - @Field() + @Field({ nullable: true }) @Prop({ default: 0 }) refInt: number; - @Field() + @Field({ nullable: true }) @Prop({ default: randomUUID }) refId: string; - @Field() + @Field({ nullable: true }) @Prop({ default: 0 }) replicationId: number; - @Field() + @Field({ nullable: true }) @Prop({ default: () => new Date(Date.now()), required: false }) expiryStarts?: Date; - @Field() + @Field({ nullable: true }) @Prop({ default: () => new Date('2099-12-31'), required: false }) expiryEnds?: Date; diff --git a/backend/src/models/build.model.ts b/backend/src/models/build.model.ts index 058c218..8c99366 100644 --- a/backend/src/models/build.model.ts +++ b/backend/src/models/build.model.ts @@ -1,13 +1,13 @@ import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; import { Document, Types } from 'mongoose'; import { ObjectType, Field, ID, Int } from '@nestjs/graphql'; -import { Base, ChangableBase } from '@/models/base.model'; +import { Base } from '@/models/base.model'; import { Person } from '@/models/person.model'; import { Company } from '@/models/company.model'; @ObjectType() @Schema({ timestamps: true }) -export class BuildIban extends ChangableBase { +export class BuildIban extends Base { @Field(() => ID) readonly _id: string; @@ -37,13 +37,13 @@ export class BuildIban extends ChangableBase { @ObjectType() export class BuildResponsible { - @Field(() => ID) - @Prop({ type: Types.ObjectId, ref: Company.name, required: true }) - company: Types.ObjectId; + @Field(() => [String], { nullable: true, defaultValue: [] }) + @Prop({ type: [String], default: [] }) + company?: string[]; - @Field(() => ID) - @Prop({ type: Types.ObjectId, ref: Person.name, required: true }) - person: Types.ObjectId; + @Field(() => [String], { nullable: true, defaultValue: [] }) + @Prop({ type: [String], default: [] }) + person?: string[]; } @@ -118,38 +118,45 @@ export class BuildInfo { @ObjectType() @Schema({ timestamps: true }) -export class Build extends Base { - - @Field(() => ID) - @Prop({ type: Types.ObjectId, ref: 'BuildType', required: true }) - buildType: Types.ObjectId; +export class Build { @Field() - @Prop({ required: true, unique: true }) + readonly _id: string; + + // @Field(() => ID) + // @Prop({ type: Types.ObjectId, ref: 'BuildType', required: true }) + // buildType: Types.ObjectId; + + @Field(() => String, { nullable: true }) + @Prop({ required: true }) + buildType: string; + + @Field(() => String, { nullable: true }) + @Prop({ required: true }) collectionToken: string; - @Field(() => BuildInfo) + @Field(() => BuildInfo, { nullable: true }) @Prop({ type: BuildInfo, required: true }) info: BuildInfo; - @Field(() => ID, { nullable: true }) - @Prop({ type: Types.ObjectId, ref: 'BuildSites' }) - site?: Types.ObjectId; + @Field(() => String, { nullable: true }) + @Prop({ type: String }) + site?: string; - @Field(() => ID, { nullable: true }) - @Prop({ type: Types.ObjectId, ref: 'BuildAddress' }) - address?: Types.ObjectId; + @Field(() => String, { nullable: true }) + @Prop({ type: String }) + address?: string; - @Field(() => [ID], { nullable: true }) - @Prop({ type: [{ type: Types.ObjectId, ref: 'BuildArea' }] }) - areas?: Types.ObjectId[]; + @Field(() => [String], { nullable: true, defaultValue: [] }) + @Prop({ type: [String], default: [] }) + areas?: string[]; - @Field(() => [BuildIban], { nullable: true }) - @Prop({ type: [BuildIban] }) - ibans?: BuildIban[]; + @Field(() => [String], { nullable: true, defaultValue: [] }) + @Prop({ type: [String], default: [] }) + ibans?: string[]; - @Field(() => [BuildResponsible], { nullable: true }) - @Prop({ type: [BuildResponsible] }) + @Field(() => [BuildResponsible], { nullable: true, defaultValue: [] }) + @Prop({ type: [BuildResponsible], default: [] }) responsibles?: BuildResponsible[]; } diff --git a/frontend/app/api/build-ibans/add/route.ts b/frontend/app/api/build-ibans/add/route.ts index 6cbc5ef..ae7147b 100644 --- a/frontend/app/api/build-ibans/add/route.ts +++ b/frontend/app/api/build-ibans/add/route.ts @@ -1,34 +1,30 @@ 'use server'; import { NextResponse } from 'next/server'; import { GraphQLClient, gql } from 'graphql-request'; -import { buildTypesAddSchema } from './schema'; +import { buildIbansAddSchema } from './schema'; const endpoint = "http://localhost:3001/graphql"; export async function POST(request: Request) { const body = await request.json(); - const validatedBody = buildTypesAddSchema.parse(body); + const validatedBody = buildIbansAddSchema.parse(body); try { const client = new GraphQLClient(endpoint); const query = gql` - mutation CreateBuildAddress($input: CreateBuildAddressInput!) { - createBuildAddress(input: $input) { + mutation CreateBuildIban($input: CreateBuildIbanInput!) { + createBuildIban(input: $input) { _id - buildNumber - doorNumber - floorNumber - commentAddress - letterAddress - shortLetterAddress - latitude - longitude - street + uuid + createdAt + updatedAt + expiryEnds + expiryStarts } } `; const variables = { input: validatedBody }; const data = await client.request(query, variables); - return NextResponse.json({ data: data.createBuildAddress, status: 200 }); + return NextResponse.json({ data: data.createBuildIban, status: 200 }); } catch (err: any) { console.error(err); return NextResponse.json({ error: err.message }, { status: 500 }); diff --git a/frontend/app/api/build-ibans/add/schema.ts b/frontend/app/api/build-ibans/add/schema.ts index 3260ff2..8d1ca2f 100644 --- a/frontend/app/api/build-ibans/add/schema.ts +++ b/frontend/app/api/build-ibans/add/schema.ts @@ -1,15 +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 const buildIbansAddSchema = z.object({ + + iban: z.string(), + startDate: z.string(), + stopDate: z.string(), + bankCode: z.string(), + xcomment: z.string(), + expiryStarts: z.string().optional(), + expiryEnds: z.string().optional(), + }); -export type BuildTypesAdd = z.infer; +export type BuildIbansAdd = z.infer; diff --git a/frontend/app/api/build-ibans/list/route.ts b/frontend/app/api/build-ibans/list/route.ts index 0076bf0..f9d4f40 100644 --- a/frontend/app/api/build-ibans/list/route.ts +++ b/frontend/app/api/build-ibans/list/route.ts @@ -13,11 +13,17 @@ export async function POST(request: Request) { query BuildIbans($input: ListArguments!) { buildIbans(input: $input) { data { + _id + uuid iban startDate stopDate bankCode xcomment + createdAt + updatedAt + expiryStarts + expiryEnds } totalCount } diff --git a/frontend/app/api/build-ibans/update/route.ts b/frontend/app/api/build-ibans/update/route.ts index 2877930..024fca6 100644 --- a/frontend/app/api/build-ibans/update/route.ts +++ b/frontend/app/api/build-ibans/update/route.ts @@ -1,7 +1,7 @@ 'use server'; import { NextResponse } from 'next/server'; import { GraphQLClient, gql } from 'graphql-request'; -import { UpdateBuildAddressSchema } from './schema'; +import { UpdateBuildIbansSchema } from './schema'; const endpoint = "http://localhost:3001/graphql"; @@ -9,29 +9,26 @@ 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); + const validatedBody = UpdateBuildIbansSchema.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) { + mutation UpdateBuildIban($uuid:String!,$input: UpdateBuildIbanInput!) { + updateBuildIban(uuid: $uuid,input: $input) { _id - buildNumber - doorNumber - floorNumber - commentAddress - letterAddress - shortLetterAddress - latitude - longitude - street + uuid + xcomment + createdAt + updatedAt + expiryEnds + expiryStarts } } `; const variables = { uuid: uuid, input: validatedBody }; const data = await client.request(query, variables); - return NextResponse.json({ data: data.updateBuildAddress, status: 200 }); + return NextResponse.json({ data: data.updateBuildIban, status: 200 }); } catch (err: any) { console.error(err); return NextResponse.json({ error: err.message }, { status: 500 }); diff --git a/frontend/app/api/build-ibans/update/schema.ts b/frontend/app/api/build-ibans/update/schema.ts index f86efd4..82500a1 100644 --- a/frontend/app/api/build-ibans/update/schema.ts +++ b/frontend/app/api/build-ibans/update/schema.ts @@ -1,15 +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 const UpdateBuildIbansSchema = z.object({ + + iban: z.string().optional(), + startDate: z.string().optional(), + stopDate: z.string().optional(), + bankCode: z.string().optional(), + xcomment: z.string().optional(), + expiryStarts: z.string().optional(), + expiryEnds: z.string().optional(), + }); -export type UpdateBuildAddress = z.infer; +export type UpdateBuildIbans = z.infer; diff --git a/frontend/app/build-areas/add/page.tsx b/frontend/app/build-areas/add/page.tsx index 5eb81de..f2ecbc8 100644 --- a/frontend/app/build-areas/add/page.tsx +++ b/frontend/app/build-areas/add/page.tsx @@ -1,4 +1,5 @@ +import { PageAddBuildAreas } from "@/pages/build-areas/add/page"; -const BuildAreasAdd = () => { return <>
BuildAreasAdd
} +const BuildAreasAdd = () => { return <>
} export default BuildAreasAdd; diff --git a/frontend/app/build-areas/page.tsx b/frontend/app/build-areas/page.tsx index b9c3846..39bb508 100644 --- a/frontend/app/build-areas/page.tsx +++ b/frontend/app/build-areas/page.tsx @@ -1,6 +1,7 @@ +import { PageBuildAreas } from "@/pages/build-areas/page"; const BuildAreas = () => { - return <>
BuildAreas
+ return <>
} export default BuildAreas; diff --git a/frontend/app/build-areas/update/page.tsx b/frontend/app/build-areas/update/page.tsx index 3db66e8..8e4527b 100644 --- a/frontend/app/build-areas/update/page.tsx +++ b/frontend/app/build-areas/update/page.tsx @@ -1,6 +1,5 @@ +import { PageUpdateBuildSites } from "@/pages/build-areas/update/page"; -const BuildAreasUpdate = () => { - return <>
BuildAreasUpdate
-} +const BuildAreasUpdate = () => { return <>
} export default BuildAreasUpdate; diff --git a/frontend/app/build-ibans/add/page.tsx b/frontend/app/build-ibans/add/page.tsx index ff9ecef..0855fae 100644 --- a/frontend/app/build-ibans/add/page.tsx +++ b/frontend/app/build-ibans/add/page.tsx @@ -1,4 +1,5 @@ +import { PageAddBuildIbans } from "@/pages/build-ibans/add/page"; -const BuildIbansAdd = () => { return <>
BuildIbansAdd
} +const BuildIbansAdd = () => { return <> } -export default BuildIbansAdd; \ No newline at end of file +export default BuildIbansAdd; diff --git a/frontend/app/build-ibans/page.tsx b/frontend/app/build-ibans/page.tsx index 7deac9a..87a3f65 100644 --- a/frontend/app/build-ibans/page.tsx +++ b/frontend/app/build-ibans/page.tsx @@ -1,6 +1,5 @@ +import { PageBuildIbans } from "@/pages/build-ibans/page"; -const BuildIbans = () => { - return <>
BuildIbans
-} +const BuildIbans = () => { return <> } export default BuildIbans; \ No newline at end of file diff --git a/frontend/app/build-ibans/update/page.tsx b/frontend/app/build-ibans/update/page.tsx index af29649..916db60 100644 --- a/frontend/app/build-ibans/update/page.tsx +++ b/frontend/app/build-ibans/update/page.tsx @@ -1,6 +1,7 @@ +import { PageUpdateBuildIbans } from "@/pages/build-ibans/update/page"; const BuildIbansUpdate = () => { - return <>
BuildIbansUpdate
+ return <> } export default BuildIbansUpdate; \ No newline at end of file diff --git a/frontend/app/build-parts/add/page.tsx b/frontend/app/build-parts/add/page.tsx new file mode 100644 index 0000000..6ac407d --- /dev/null +++ b/frontend/app/build-parts/add/page.tsx @@ -0,0 +1,5 @@ +import { PageAddBuildIbans } from "@/pages/build-ibans/add/page"; + +const BuildPartsAdd = () => { return <> } + +export default BuildPartsAdd; diff --git a/frontend/app/build-parts/page.tsx b/frontend/app/build-parts/page.tsx new file mode 100644 index 0000000..2d16af3 --- /dev/null +++ b/frontend/app/build-parts/page.tsx @@ -0,0 +1,5 @@ +import { PageBuildIbans } from "@/pages/build-ibans/page"; + +const BuildParts = () => { return <> } + +export default BuildParts; \ No newline at end of file diff --git a/frontend/app/build-parts/update/page.tsx b/frontend/app/build-parts/update/page.tsx new file mode 100644 index 0000000..3088984 --- /dev/null +++ b/frontend/app/build-parts/update/page.tsx @@ -0,0 +1,7 @@ +import { PageUpdateBuildIbans } from "@/pages/build-ibans/update/page"; + +const BuildPartsUpdate = () => { + return <> +} + +export default BuildPartsUpdate; \ No newline at end of file diff --git a/frontend/app/builds/add/page.tsx b/frontend/app/builds/add/page.tsx new file mode 100644 index 0000000..1905e35 --- /dev/null +++ b/frontend/app/builds/add/page.tsx @@ -0,0 +1,5 @@ +import { PageAddBuildTypes } from "@/pages/build-types/add/page"; + +const AddBuildPage = () => { return <> } + +export default AddBuildPage; diff --git a/frontend/app/builds/page.tsx b/frontend/app/builds/page.tsx new file mode 100644 index 0000000..0eb484c --- /dev/null +++ b/frontend/app/builds/page.tsx @@ -0,0 +1,5 @@ +import { PageBuilds } from "@/pages/builds/page"; + +const BuildPage = () => { return <> } + +export default BuildPage; diff --git a/frontend/app/builds/update/page.tsx b/frontend/app/builds/update/page.tsx new file mode 100644 index 0000000..9288a51 --- /dev/null +++ b/frontend/app/builds/update/page.tsx @@ -0,0 +1,5 @@ +import { PageUpdateBuildTypes } from '@/pages/build-types/update/page'; + +const UpdateBuildPage = async () => { return } + +export default UpdateBuildPage; diff --git a/frontend/components/sidebar/app-sidebar.tsx b/frontend/components/sidebar/app-sidebar.tsx index 8798eb3..00e947b 100644 --- a/frontend/components/sidebar/app-sidebar.tsx +++ b/frontend/components/sidebar/app-sidebar.tsx @@ -1,5 +1,4 @@ "use client" - import * as React from "react" import { IconInnerShadowTop, @@ -11,23 +10,14 @@ import { IconMessageCircle, IconChartArea, IconCreditCard, - + IconBoxModel } from "@tabler/icons-react" - import { NavMain } from "@/components/dashboard/nav-main" import { NavSecondary } from "@/components/dashboard/nav-secondary" import { NavUser } from "@/components/dashboard/nav-user" import { NavDocuments } from "@/components/dashboard/nav-documents" -import { - Sidebar, - SidebarContent, - SidebarFooter, - SidebarHeader, - SidebarMenu, - SidebarMenuButton, - SidebarMenuItem, -} from "@/components/ui/sidebar" +import { Sidebar, SidebarContent, SidebarFooter, SidebarHeader, SidebarMenu, SidebarMenuButton, SidebarMenuItem } from "@/components/ui/sidebar" const data = { user: { @@ -48,9 +38,14 @@ const data = { }, { title: "Build", - url: "/build", + url: "/builds", icon: IconBuilding }, + { + title: "Build Parts", + url: "/build-parts", + icon: IconBoxModel + }, { title: "Build Types", url: "/build-types", diff --git a/frontend/pages/build-areas/add/form.tsx b/frontend/pages/build-areas/add/form.tsx new file mode 100644 index 0000000..910af65 --- /dev/null +++ b/frontend/pages/build-areas/add/form.tsx @@ -0,0 +1,207 @@ +"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 { BuildAreasAdd, buildAreasAddSchema } from "./schema" +import { useAddBuildAreasMutation } from "./queries" + +const BuildAreasForm = ({ refetchTable }: { refetchTable: () => void }) => { + + const form = useForm({ + resolver: zodResolver(buildAreasAddSchema), + defaultValues: { + areaName: "", + areaCode: "", + areaType: "", + areaDirection: "", + areaGrossSize: 0, + areaNetSize: 0, + width: 0, + size: 0, + expiryStarts: "", + expiryEnds: "", + }, + }); + + const { handleSubmit } = form; + + const mutation = useAddBuildAreasMutation(); + + function onSubmit(values: BuildAreasAdd) { mutation.mutate({ data: values }); setTimeout(() => refetchTable(), 400) }; + + return ( +
+ + {/* ROW 1 */} +
+ + ( + + Area Name + + + + + + )} + /> + + ( + + Area Code + + + + + + )} + /> +
+ +
+ ( + + Area Type + + + + + + )} + /> + + ( + + Area Direction + + + + + + )} + /> + +
+ +
+ ( + + Area Gross Size + + + + + + )} + /> + + ( + + Area Net Size + + + + + + )} + /> +
+ +
+ ( + + Width + + + + + + )} + /> + + ( + + Size + + + + + + )} + /> +
+ + + + {/* EXPIRY DATES */} +
+ + ( + + Expiry Starts + + + + + + )} + /> + + ( + + Expiry Ends + + + + + + )} + /> +
+ + + + + ); +}; + +export { BuildAreasForm } diff --git a/frontend/pages/build-areas/add/page.tsx b/frontend/pages/build-areas/add/page.tsx new file mode 100644 index 0000000..64b8f91 --- /dev/null +++ b/frontend/pages/build-areas/add/page.tsx @@ -0,0 +1,25 @@ +'use client'; +import { useState } from 'react'; +import { BuildAreasDataTableAdd } from './table/data-table'; +import { useGraphQlBuildAreasList } from '@/pages/build-areas/queries'; +import { BuildAreasForm } from './form'; + +const PageAddBuildAreas = () => { + 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 } = useGraphQlBuildAreasList({ limit, skip: (page - 1) * limit, sort, filters }); + + return ( + <> + + + + ) +} + +export { PageAddBuildAreas }; diff --git a/frontend/pages/build-areas/add/queries.tsx b/frontend/pages/build-areas/add/queries.tsx new file mode 100644 index 0000000..f06058c --- /dev/null +++ b/frontend/pages/build-areas/add/queries.tsx @@ -0,0 +1,25 @@ +'use client' +import { useMutation } from '@tanstack/react-query' +import { toISOIfNotZ } from '@/lib/utils'; +import { BuildAreasAdd } from './schema'; + +const fetchGraphQlBuildSitesAdd = async (record: BuildAreasAdd): Promise<{ data: BuildAreasAdd | null; status: number }> => { + console.log('Fetching test data from local API'); + record.expiryStarts = record.expiryStarts ? toISOIfNotZ(record.expiryStarts) : undefined; + record.expiryEnds = record.expiryEnds ? toISOIfNotZ(record.expiryEnds) : undefined; + console.dir({ record }) + try { + const res = await fetch('/api/build-areas/add', { method: 'POST', cache: 'no-store', credentials: "include", body: JSON.stringify(record) }); + if (!res.ok) { const errorText = await res.text(); console.error('Test data API error:', errorText); throw new Error(`API error: ${res.status} ${res.statusText}`) } + const data = await res.json(); + return { data: data.data, status: res.status } + } catch (error) { console.error('Error fetching test data:', error); throw error } +}; + +export function useAddBuildAreasMutation() { + return useMutation({ + mutationFn: ({ data }: { data: BuildAreasAdd }) => fetchGraphQlBuildSitesAdd(data), + onSuccess: () => { console.log("Build areas created successfully") }, + onError: (error) => { console.error("Add build areas failed:", error) }, + }) +} diff --git a/frontend/pages/build-areas/add/schema.ts b/frontend/pages/build-areas/add/schema.ts new file mode 100644 index 0000000..0c602a2 --- /dev/null +++ b/frontend/pages/build-areas/add/schema.ts @@ -0,0 +1,18 @@ +import { z } from "zod" + +export const buildAreasAddSchema = z.object({ + + areaName: z.string(), + areaCode: z.string(), + areaType: z.string(), + areaDirection: z.string(), + areaGrossSize: z.number(), + areaNetSize: z.number(), + width: z.number(), + size: z.number(), + expiryStarts: z.string().optional(), + expiryEnds: z.string().optional() + +}); + +export type BuildAreasAdd = z.infer; diff --git a/frontend/pages/build-areas/add/table/columns.tsx b/frontend/pages/build-areas/add/table/columns.tsx new file mode 100644 index 0000000..70741b8 --- /dev/null +++ b/frontend/pages/build-areas/add/table/columns.tsx @@ -0,0 +1,120 @@ +"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: "areaName", + header: "Area Name", + }, + { + accessorKey: "areaCode", + header: "Area Code", + }, + { + accessorKey: "areaType", + header: "Area Type", + }, + { + accessorKey: "areaDirection", + header: "Area Direction", + }, + { + accessorKey: "areaGrossSize", + header: "Area Gross Size", + }, + { + accessorKey: "areaNetSize", + header: "Area Net Size", + }, + { + accessorKey: "width", + header: "Width", + }, + { + accessorKey: "size", + header: "Size", + }, + { + 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-areas/add/table/data-table.tsx b/frontend/pages/build-areas/add/table/data-table.tsx new file mode 100644 index 0000000..58d1f0b --- /dev/null +++ b/frontend/pages/build-areas/add/table/data-table.tsx @@ -0,0 +1,254 @@ +"use client" +import * as React from "react" +import { + closestCenter, + DndContext, + KeyboardSensor, + MouseSensor, + TouchSensor, + useSensor, + useSensors, + type UniqueIdentifier, +} from "@dnd-kit/core" +import { restrictToVerticalAxis } from "@dnd-kit/modifiers" +import { + SortableContext, + verticalListSortingStrategy, +} from "@dnd-kit/sortable" +import { + IconChevronDown, + IconChevronLeft, + IconChevronRight, + IconChevronsLeft, + IconChevronsRight, + IconLayoutColumns, +} from "@tabler/icons-react" +import { + ColumnFiltersState, + flexRender, + getCoreRowModel, + getFacetedRowModel, + getFacetedUniqueValues, + getFilteredRowModel, + getSortedRowModel, + SortingState, + useReactTable, + VisibilityState, +} from "@tanstack/react-table" +import { Button } from "@/components/ui/button" +import { DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuTrigger } from "@/components/ui/dropdown-menu" +import { Label } from "@/components/ui/label" +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" +import { Tabs, TabsContent } from "@/components/ui/tabs" +import { schemaType } from "./schema" +import { getColumns, DraggableRow } from "./columns" +import { useRouter } from "next/navigation" +import { Home } from "lucide-react" +import { useDeleteBuildSiteMutation } from "@/pages/build-sites/queries" + +export function BuildAreasDataTableAdd({ + 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 = useDeleteBuildSiteMutation() + const deleteHandler = (id: string) => { deleteMutation.mutate({ uuid: id }); setTimeout(() => { refetchTable() }, 400) } + const columns = getColumns(router, deleteHandler); + const pagination = React.useMemo(() => ({ pageIndex: currentPage - 1, pageSize: pageSize, }), [currentPage, pageSize]) + const totalPages = Math.ceil(totalCount / pageSize) + + const table = useReactTable({ + data, + columns, + pageCount: totalPages, + state: { sorting, columnVisibility, rowSelection, columnFilters, pagination }, + manualPagination: true, + enableRowSelection: true, + getRowId: (row) => row._id.toString(), + onRowSelectionChange: setRowSelection, + onSortingChange: setSorting, + onColumnFiltersChange: setColumnFilters, + onColumnVisibilityChange: setColumnVisibility, + onPaginationChange: (updater) => { + const nextPagination = typeof updater === "function" ? updater(pagination) : updater; + onPageChange(nextPagination.pageIndex + 1); onPageSizeChange(nextPagination.pageSize); + }, + getCoreRowModel: getCoreRowModel(), + getFilteredRowModel: getFilteredRowModel(), + getSortedRowModel: getSortedRowModel(), + getFacetedRowModel: getFacetedRowModel(), + getFacetedUniqueValues: getFacetedUniqueValues(), + }) + + const handlePageSizeChange = (value: string) => { const newSize = Number(value); onPageSizeChange(newSize); onPageChange(1) } + + return ( + +
+ + +
+ + + + + + {table.getAllColumns().filter((column) => typeof column.accessorFn !== "undefined" && column.getCanHide()).map((column) => { + return ( + column.toggleVisibility(!!value)} > + {column.id} + + ) + })} + + + +
+
+ +
+ + + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + return ( + + {header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())} + + ) + })} + + ))} + + + {table.getRowModel().rows?.length ? ( + {table.getRowModel().rows.map((row) => )} + ) : ( + No results. + )} + +
+
+
+
+
+ {table.getFilteredSelectedRowModel().rows.length} of{" "} + {table.getFilteredRowModel().rows.length} row(s) selected. +
+
+
+ + +
+
+ Page {currentPage} of {totalPages} +
+
+ Total Count: {totalCount} +
+
+ + + + + + + +
+
+
+
+ +
+
+ +
+
+ +
+
+
+ ) +} \ No newline at end of file diff --git a/frontend/pages/build-areas/add/table/schema.tsx b/frontend/pages/build-areas/add/table/schema.tsx new file mode 100644 index 0000000..0c4aa86 --- /dev/null +++ b/frontend/pages/build-areas/add/table/schema.tsx @@ -0,0 +1,20 @@ +import { z } from "zod"; + +export const schema = z.object({ + _id: z.string(), + uuid: z.string(), + areaName: z.string(), + areaCode: z.string(), + areaType: z.string(), + areaDirection: z.string(), + areaGrossSize: z.number(), + areaNetSize: z.number(), + width: z.number(), + size: z.number(), + expiryStarts: z.string().optional(), + expiryEnds: z.string().optional(), + createdAt: z.string(), + updatedAt: z.string(), +}); + +export type schemaType = z.infer; \ No newline at end of file diff --git a/frontend/pages/build-areas/add/types.ts b/frontend/pages/build-areas/add/types.ts new file mode 100644 index 0000000..e4508a4 --- /dev/null +++ b/frontend/pages/build-areas/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-areas/list/columns.tsx b/frontend/pages/build-areas/list/columns.tsx new file mode 100644 index 0000000..c3e06bd --- /dev/null +++ b/frontend/pages/build-areas/list/columns.tsx @@ -0,0 +1,104 @@ +"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: "areaName", + header: "Area Name", + }, + { + accessorKey: "areaCode", + header: "Area Code", + }, + { + accessorKey: "areaType", + header: "Area Type", + }, + { + accessorKey: "areaDirection", + header: "Area Direction", + }, + { + accessorKey: "areaGrossSize", + header: "Area Gross Size", + }, + { + accessorKey: "areaNetSize", + header: "Area Net Size", + }, + { + accessorKey: "width", + header: "Width", + }, + { + accessorKey: "size", + header: "Size", + }, + { + 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-areas/list/data-table.tsx b/frontend/pages/build-areas/list/data-table.tsx new file mode 100644 index 0000000..065c212 --- /dev/null +++ b/frontend/pages/build-areas/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 { useDeleteBuildAreaMutation } from "../queries" + +export function BuildAreasDataTable({ + 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 = useDeleteBuildAreaMutation() + const deleteHandler = (id: string) => { deleteMutation.mutate({ uuid: id }); setTimeout(() => { refetchTable() }, 400) } + const columns = getColumns(router, deleteHandler); + const pagination = React.useMemo(() => ({ pageIndex: currentPage - 1, pageSize: pageSize }), [currentPage, pageSize]) + const totalPages = Math.ceil(totalCount / pageSize) + + const table = useReactTable({ + data, + columns, + pageCount: totalPages, + state: { sorting, columnVisibility, rowSelection, columnFilters, pagination }, + manualPagination: true, + 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-areas/list/schema.tsx b/frontend/pages/build-areas/list/schema.tsx new file mode 100644 index 0000000..37effd4 --- /dev/null +++ b/frontend/pages/build-areas/list/schema.tsx @@ -0,0 +1,20 @@ +import { z } from "zod"; + +export const schema = z.object({ + _id: z.string(), + uuid: z.string(), + areaName: z.string(), + areaCode: z.string(), + areaType: z.string(), + areaDirection: z.string(), + areaGrossSize: z.number(), + areaNetSize: z.number(), + width: z.number(), + size: z.number(), + expiryStarts: z.string().optional(), + expiryEnds: z.string().optional(), + createdAt: z.string(), + updatedAt: z.string(), +}); + +export type schemaType = z.infer; diff --git a/frontend/pages/build-areas/page.tsx b/frontend/pages/build-areas/page.tsx new file mode 100644 index 0000000..cda826a --- /dev/null +++ b/frontend/pages/build-areas/page.tsx @@ -0,0 +1,25 @@ +'use client'; +import { BuildAreasDataTable } from './list/data-table'; +import { useGraphQlBuildAreasList } from './queries'; +import { useState } from 'react'; + +const PageBuildAreas = () => { + + 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 } = useGraphQlBuildAreasList({ 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 build areas
} + + return ; + +}; + +export { PageBuildAreas }; diff --git a/frontend/pages/build-areas/queries.tsx b/frontend/pages/build-areas/queries.tsx new file mode 100644 index 0000000..e0b9005 --- /dev/null +++ b/frontend/pages/build-areas/queries.tsx @@ -0,0 +1,36 @@ +'use client' +import { useQuery, useMutation } from '@tanstack/react-query' +import { ListArguments } from '@/types/listRequest' + +const fetchGraphQlBuildAreasList = 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-areas/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 fetchGraphQlDeleteBuildArea = async (uuid: string): Promise => { + console.log('Fetching test data from local API'); + try { + const res = await fetch(`/api/build-areas/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 useGraphQlBuildAreasList(params: ListArguments) { + return useQuery({ queryKey: ['graphql-build-areas-list', params], queryFn: () => fetchGraphQlBuildAreasList(params) }) +} + +export function useDeleteBuildAreaMutation() { + return useMutation({ + mutationFn: ({ uuid }: { uuid: string }) => fetchGraphQlDeleteBuildArea(uuid), + onSuccess: () => { console.log("Build area deleted successfully") }, + onError: (error) => { console.error("Delete build area failed:", error) }, + }) +} diff --git a/frontend/pages/build-areas/types.ts b/frontend/pages/build-areas/types.ts new file mode 100644 index 0000000..f0ed606 --- /dev/null +++ b/frontend/pages/build-areas/types.ts @@ -0,0 +1,15 @@ +interface BuildSites { + + _id: string; + uuid: string; + siteName: string; + siteNo: string; + expiryStarts: string; + expiryEnds: string; + createdAt: string; + updatedAt: string; + deletedAt: string; + +} + +export type { BuildSites }; diff --git a/frontend/pages/build-areas/update/form.tsx b/frontend/pages/build-areas/update/form.tsx new file mode 100644 index 0000000..23f83f5 --- /dev/null +++ b/frontend/pages/build-areas/update/form.tsx @@ -0,0 +1,194 @@ +"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 { useUpdateBuildSitesMutation } from "@/pages/build-sites/update/queries" +import { BuildAreasUpdate, buildAreasUpdateSchema } from "@/pages/build-areas/update/schema" + +const BuildAreasForm = ({ refetchTable, initData, selectedUuid }: { refetchTable: () => void, initData: BuildAreasUpdate, selectedUuid: string }) => { + + const form = useForm({ resolver: zodResolver(buildAreasUpdateSchema), defaultValues: { ...initData } }) + + const { handleSubmit } = form + + const mutation = useUpdateBuildSitesMutation(); + + function onSubmit(values: BuildAreasUpdate) { mutation.mutate({ data: values as any || initData, uuid: selectedUuid }); setTimeout(() => refetchTable(), 400) } + + return ( +
+ + {/* ROW 1 */} +
+ + ( + + Area Name + + + + + + )} + /> + + ( + + Area Code + + + + + + )} + /> +
+ +
+ ( + + Area Type + + + + + + )} + /> + + ( + + Area Direction + + + + + + )} + /> + +
+ +
+ ( + + Area Gross Size + + + + + + )} + /> + + ( + + Area Net Size + + + + + + )} + /> +
+ +
+ ( + + Width + + + + + + )} + /> + + ( + + Size + + + + + + )} + /> +
+ + + + {/* EXPIRY DATES */} +
+ + ( + + Expiry Starts + + + + + + )} + /> + + ( + + Expiry Ends + + + + + + )} + /> +
+ + + + + ); + +} + +export { BuildAreasForm } diff --git a/frontend/pages/build-areas/update/page.tsx b/frontend/pages/build-areas/update/page.tsx new file mode 100644 index 0000000..9979f57 --- /dev/null +++ b/frontend/pages/build-areas/update/page.tsx @@ -0,0 +1,36 @@ +'use client'; +import { useState } from 'react'; +import { BuildAreasForm } from '@/pages/build-areas/update/form'; +import { useSearchParams, useRouter } from 'next/navigation' +import { Button } from '@/components/ui/button'; +import { BuildAreasDataTableUpdate } from './table/data-table'; +import { useGraphQlBuildAreasList } from '../queries'; + +const PageUpdateBuildSites = () => { + 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 } = useGraphQlBuildAreasList({ limit, skip: (page - 1) * limit, sort, filters: { ...filters, uuid } }); + const initData = data?.data?.[0] || null; + if (!initData) { return backToBuildAddress } + return ( + <> + + + + ) +} + +export { PageUpdateBuildSites }; diff --git a/frontend/pages/build-areas/update/queries.tsx b/frontend/pages/build-areas/update/queries.tsx new file mode 100644 index 0000000..ec6402b --- /dev/null +++ b/frontend/pages/build-areas/update/queries.tsx @@ -0,0 +1,24 @@ +'use client' +import { useMutation } from '@tanstack/react-query' +import { UpdateBuildAreasUpdate } from './types'; +import { toISOIfNotZ } from '@/lib/utils'; + +const fetchGraphQlBuildAreasUpdate = async (record: UpdateBuildAreasUpdate, uuid: string): Promise<{ data: UpdateBuildAreasUpdate | null; status: number }> => { + console.log('Fetching test data from local API'); + record.expiryStarts = record.expiryStarts ? toISOIfNotZ(record.expiryStarts) : undefined; + record.expiryEnds = record.expiryEnds ? toISOIfNotZ(record.expiryEnds) : undefined; + try { + const res = await fetch(`/api/build-areas/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 useUpdateBuildAreasMutation() { + return useMutation({ + mutationFn: ({ data, uuid }: { data: UpdateBuildAreasUpdate, uuid: string }) => fetchGraphQlBuildAreasUpdate(data, uuid), + onSuccess: () => { console.log("Build Areas updated successfully") }, + onError: (error) => { console.error("Update Build Areas failed:", error) }, + }) +} diff --git a/frontend/pages/build-areas/update/schema.ts b/frontend/pages/build-areas/update/schema.ts new file mode 100644 index 0000000..c6270dc --- /dev/null +++ b/frontend/pages/build-areas/update/schema.ts @@ -0,0 +1,16 @@ +import { z } from "zod" + +export const buildAreasUpdateSchema = z.object({ + areaName: z.string().optional(), + areaCode: z.string().optional(), + areaType: z.string().optional(), + areaDirection: z.string().optional(), + areaGrossSize: z.number().optional(), + areaNetSize: z.number().optional(), + width: z.number().optional(), + size: z.number().optional(), + expiryStarts: z.string().optional(), + expiryEnds: z.string().optional(), +}); + +export type BuildAreasUpdate = z.infer; diff --git a/frontend/pages/build-areas/update/table/columns.tsx b/frontend/pages/build-areas/update/table/columns.tsx new file mode 100644 index 0000000..53827cc --- /dev/null +++ b/frontend/pages/build-areas/update/table/columns.tsx @@ -0,0 +1,108 @@ +"use client" +import { z } from "zod" +import { Button } from "@/components/ui/button" +import { Drawer, DrawerClose, DrawerContent, DrawerFooter, DrawerHeader, DrawerTrigger } from "@/components/ui/drawer" +import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger } from "@/components/ui/dropdown-menu" +import { Input } from "@/components/ui/input" +import { Label } from "@/components/ui/label" +import { useSortable } from "@dnd-kit/sortable" +import { IconGripVertical } from "@tabler/icons-react" +import { useIsMobile } from "@/hooks/use-mobile" +import { Separator } from "@/components/ui/separator" +import { ColumnDef, flexRender, Row } from "@tanstack/react-table" +import { TableCell, TableRow } from "@/components/ui/table" +import { CSS } from "@dnd-kit/utilities" +import { schema, schemaType } from "./schema" +import { dateToLocaleString } from "@/lib/utils" +import { Pencil, Trash } from "lucide-react" + +export function DraggableRow({ row }: { row: Row> }) { + const { transform, transition, setNodeRef, isDragging } = useSortable({ id: row.original._id }) + return ( + + {row.getVisibleCells().map((cell) => ( + {flexRender(cell.column.columnDef.cell, cell.getContext())} + ))} + + ) +} + +function getColumns(router: any, deleteHandler: (id: string) => void): ColumnDef[] { + return [ + { + accessorKey: "uuid", + header: "UUID", + cell: ({ getValue }) => (
{String(getValue())}
), + }, + { + accessorKey: "areaName", + header: "Area Name", + }, + { + accessorKey: "areaCode", + header: "Area Code", + }, + { + accessorKey: "areaType", + header: "Area Type", + }, + { + accessorKey: "areaDirection", + header: "Area Direction", + }, + { + accessorKey: "areaGrossSize", + header: "Area Gross Size", + }, + { + accessorKey: "areaNetSize", + header: "Area Net Size", + }, + { + accessorKey: "width", + header: "Width", + }, + { + accessorKey: "size", + header: "Size", + }, + { + 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-areas/update/table/data-table.tsx b/frontend/pages/build-areas/update/table/data-table.tsx new file mode 100644 index 0000000..bdd02e2 --- /dev/null +++ b/frontend/pages/build-areas/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 BuildAreasDataTableUpdate({ + 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-areas/update/table/schema.tsx b/frontend/pages/build-areas/update/table/schema.tsx new file mode 100644 index 0000000..37effd4 --- /dev/null +++ b/frontend/pages/build-areas/update/table/schema.tsx @@ -0,0 +1,20 @@ +import { z } from "zod"; + +export const schema = z.object({ + _id: z.string(), + uuid: z.string(), + areaName: z.string(), + areaCode: z.string(), + areaType: z.string(), + areaDirection: z.string(), + areaGrossSize: z.number(), + areaNetSize: z.number(), + width: z.number(), + size: z.number(), + expiryStarts: z.string().optional(), + expiryEnds: z.string().optional(), + createdAt: z.string(), + updatedAt: z.string(), +}); + +export type schemaType = z.infer; diff --git a/frontend/pages/build-areas/update/types.ts b/frontend/pages/build-areas/update/types.ts new file mode 100644 index 0000000..04bb41d --- /dev/null +++ b/frontend/pages/build-areas/update/types.ts @@ -0,0 +1,16 @@ + +interface UpdateBuildAreasUpdate { + + areaName: string; + areaCode: string; + areaType: string; + areaDirection: string; + areaGrossSize: number; + areaNetSize: number; + width: number; + size: number; + expiryStarts?: string; + expiryEnds?: string; +} + +export type { UpdateBuildAreasUpdate }; \ No newline at end of file diff --git a/frontend/pages/build-ibans/add/form.tsx b/frontend/pages/build-ibans/add/form.tsx new file mode 100644 index 0000000..2ec39a2 --- /dev/null +++ b/frontend/pages/build-ibans/add/form.tsx @@ -0,0 +1,161 @@ +"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 { BuildIbansAdd, buildIbansAddSchema } from "./schema" +import { useAddBuildIbansMutation } from "./queries" + +const BuildIbansForm = ({ refetchTable }: { refetchTable: () => void }) => { + + const form = useForm({ + resolver: zodResolver(buildIbansAddSchema), + defaultValues: { + iban: "", + startDate: "", + stopDate: "", + bankCode: "", + xcomment: "", + expiryStarts: "", + expiryEnds: "", + }, + }); + + const { handleSubmit } = form; + + const mutation = useAddBuildIbansMutation(); + + function onSubmit(values: BuildIbansAdd) { mutation.mutate({ data: values }); setTimeout(() => refetchTable(), 400) }; + + return ( +
+ + {/* ROW 1 */} + +
+ ( + + IBAN + + + + + + )} + /> + + ( + + Bank Code + + + + + + )} + /> + +
+ +
+ ( + + Start Date + + + + + + )} + /> + ( + + Stop Date + + + + + + )} + /> + +
+ + +
+ ( + + Comment + + + + + + )} + /> +
+ + + + {/* EXPIRY DATES */} +
+ + ( + + Expiry Starts + + + + + + )} + /> + + ( + + Expiry Ends + + + + + + )} + /> +
+ + + + + ); +}; + +export { BuildIbansForm } diff --git a/frontend/pages/build-ibans/add/page.tsx b/frontend/pages/build-ibans/add/page.tsx new file mode 100644 index 0000000..22e8be9 --- /dev/null +++ b/frontend/pages/build-ibans/add/page.tsx @@ -0,0 +1,25 @@ +'use client'; +import { useState } from 'react'; +import { BuildIbansDataTableAdd } from './table/data-table'; +import { BuildIbansForm } from './form'; +import { useGraphQlBuildIbansList } from '../queries'; + +const PageAddBuildIbans = () => { + 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 } = useGraphQlBuildIbansList({ limit, skip: (page - 1) * limit, sort, filters }); + + return ( + <> + + + + ) +} + +export { PageAddBuildIbans }; diff --git a/frontend/pages/build-ibans/add/queries.tsx b/frontend/pages/build-ibans/add/queries.tsx new file mode 100644 index 0000000..f965c9c --- /dev/null +++ b/frontend/pages/build-ibans/add/queries.tsx @@ -0,0 +1,27 @@ +'use client' +import { useMutation } from '@tanstack/react-query' +import { toISOIfNotZ } from '@/lib/utils'; +import { BuildIbansAdd } from './schema'; + +const fetchGraphQlBuildIbansAdd = async (record: BuildIbansAdd): Promise<{ data: BuildIbansAdd | null; status: number }> => { + console.log('Fetching test data from local API'); + record.expiryStarts = record.expiryStarts ? toISOIfNotZ(record.expiryStarts) : undefined; + record.expiryEnds = record.expiryEnds ? toISOIfNotZ(record.expiryEnds) : undefined; + record.startDate = toISOIfNotZ(record.startDate); + record.stopDate = toISOIfNotZ(record.stopDate); + console.dir({ record }) + try { + const res = await fetch('/api/build-ibans/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 useAddBuildIbansMutation() { + return useMutation({ + mutationFn: ({ data }: { data: BuildIbansAdd }) => fetchGraphQlBuildIbansAdd(data), + onSuccess: () => { console.log("Build IBANs created successfully") }, + onError: (error) => { console.error("Add build IBANs failed:", error) }, + }) +} diff --git a/frontend/pages/build-ibans/add/schema.ts b/frontend/pages/build-ibans/add/schema.ts new file mode 100644 index 0000000..8d1ca2f --- /dev/null +++ b/frontend/pages/build-ibans/add/schema.ts @@ -0,0 +1,15 @@ +import { z } from "zod" + +export const buildIbansAddSchema = z.object({ + + iban: z.string(), + startDate: z.string(), + stopDate: z.string(), + bankCode: z.string(), + xcomment: z.string(), + expiryStarts: z.string().optional(), + expiryEnds: z.string().optional(), + +}); + +export type BuildIbansAdd = z.infer; diff --git a/frontend/pages/build-ibans/add/table/columns.tsx b/frontend/pages/build-ibans/add/table/columns.tsx new file mode 100644 index 0000000..236cba0 --- /dev/null +++ b/frontend/pages/build-ibans/add/table/columns.tsx @@ -0,0 +1,111 @@ +"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: "iban", + header: "IBAN", + }, + { + accessorKey: "startDate", + header: "Start Date", + cell: ({ getValue }) => dateToLocaleString(getValue() as string), + }, + { + accessorKey: "stopDate", + header: "Stop Date", + cell: ({ getValue }) => dateToLocaleString(getValue() as string), + }, + { + accessorKey: "bankCode", + header: "Bank Code", + }, + { + accessorKey: "xcomment", + header: "Comment", + }, + { + 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-ibans/add/table/data-table.tsx b/frontend/pages/build-ibans/add/table/data-table.tsx new file mode 100644 index 0000000..7933420 --- /dev/null +++ b/frontend/pages/build-ibans/add/table/data-table.tsx @@ -0,0 +1,254 @@ +"use client" +import * as React from "react" +import { + closestCenter, + DndContext, + KeyboardSensor, + MouseSensor, + TouchSensor, + useSensor, + useSensors, + type UniqueIdentifier, +} from "@dnd-kit/core" +import { restrictToVerticalAxis } from "@dnd-kit/modifiers" +import { + SortableContext, + verticalListSortingStrategy, +} from "@dnd-kit/sortable" +import { + IconChevronDown, + IconChevronLeft, + IconChevronRight, + IconChevronsLeft, + IconChevronsRight, + IconLayoutColumns, +} from "@tabler/icons-react" +import { + ColumnFiltersState, + flexRender, + getCoreRowModel, + getFacetedRowModel, + getFacetedUniqueValues, + getFilteredRowModel, + getSortedRowModel, + SortingState, + useReactTable, + VisibilityState, +} from "@tanstack/react-table" +import { Button } from "@/components/ui/button" +import { DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuTrigger } from "@/components/ui/dropdown-menu" +import { Label } from "@/components/ui/label" +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" +import { Tabs, TabsContent } from "@/components/ui/tabs" +import { schemaType } from "./schema" +import { getColumns, DraggableRow } from "./columns" +import { useRouter } from "next/navigation" +import { Home } from "lucide-react" +import { useDeleteBuildIbanMutation } from "@/pages/build-ibans/queries" + +export function BuildIbansDataTableAdd({ + 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 = useDeleteBuildIbanMutation() + const deleteHandler = (id: string) => { deleteMutation.mutate({ uuid: id }); setTimeout(() => { refetchTable() }, 400) } + const columns = getColumns(router, deleteHandler); + const pagination = React.useMemo(() => ({ pageIndex: currentPage - 1, pageSize: pageSize, }), [currentPage, pageSize]) + const totalPages = Math.ceil(totalCount / pageSize) + + const table = useReactTable({ + data, + columns, + pageCount: totalPages, + state: { sorting, columnVisibility, rowSelection, columnFilters, pagination }, + manualPagination: true, + enableRowSelection: true, + getRowId: (row) => row._id.toString(), + onRowSelectionChange: setRowSelection, + onSortingChange: setSorting, + onColumnFiltersChange: setColumnFilters, + onColumnVisibilityChange: setColumnVisibility, + onPaginationChange: (updater) => { + const nextPagination = typeof updater === "function" ? updater(pagination) : updater; + onPageChange(nextPagination.pageIndex + 1); onPageSizeChange(nextPagination.pageSize); + }, + getCoreRowModel: getCoreRowModel(), + getFilteredRowModel: getFilteredRowModel(), + getSortedRowModel: getSortedRowModel(), + getFacetedRowModel: getFacetedRowModel(), + getFacetedUniqueValues: getFacetedUniqueValues(), + }) + + const handlePageSizeChange = (value: string) => { const newSize = Number(value); onPageSizeChange(newSize); onPageChange(1) } + + return ( + +
+ + +
+ + + + + + {table.getAllColumns().filter((column) => typeof column.accessorFn !== "undefined" && column.getCanHide()).map((column) => { + return ( + column.toggleVisibility(!!value)} > + {column.id} + + ) + })} + + + +
+
+ +
+ + + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + return ( + + {header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())} + + ) + })} + + ))} + + + {table.getRowModel().rows?.length ? ( + {table.getRowModel().rows.map((row) => )} + ) : ( + No results. + )} + +
+
+
+
+
+ {table.getFilteredSelectedRowModel().rows.length} of{" "} + {table.getFilteredRowModel().rows.length} row(s) selected. +
+
+
+ + +
+
+ Page {currentPage} of {totalPages} +
+
+ Total Count: {totalCount} +
+
+ + + + + + + +
+
+
+
+ +
+
+ +
+
+ +
+
+
+ ) +} \ No newline at end of file diff --git a/frontend/pages/build-ibans/add/table/schema.tsx b/frontend/pages/build-ibans/add/table/schema.tsx new file mode 100644 index 0000000..f0b0f22 --- /dev/null +++ b/frontend/pages/build-ibans/add/table/schema.tsx @@ -0,0 +1,17 @@ +import { z } from "zod"; + +export const schema = z.object({ + _id: z.string(), + uuid: z.string(), + iban: z.string(), + startDate: z.string(), + stopDate: z.string(), + bankCode: z.string(), + xcomment: z.string(), + expiryStarts: z.string().optional(), + expiryEnds: z.string().optional(), + createdAt: z.string(), + updatedAt: z.string(), +}); + +export type schemaType = z.infer; \ No newline at end of file diff --git a/frontend/pages/build-ibans/add/types.ts b/frontend/pages/build-ibans/add/types.ts new file mode 100644 index 0000000..e4508a4 --- /dev/null +++ b/frontend/pages/build-ibans/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-ibans/list/columns.tsx b/frontend/pages/build-ibans/list/columns.tsx new file mode 100644 index 0000000..a5928a9 --- /dev/null +++ b/frontend/pages/build-ibans/list/columns.tsx @@ -0,0 +1,94 @@ +"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: "iban", + header: "IBAN", + }, + { + accessorKey: "startDate", + header: "Start Date", + cell: ({ getValue }) => dateToLocaleString(getValue() as string), + }, + { + accessorKey: "stopDate", + header: "Stop Date", + cell: ({ getValue }) => dateToLocaleString(getValue() as string), + }, + { + accessorKey: "bankCode", + header: "Bank Code", + }, + { + accessorKey: "xcomment", + header: "Comment", + }, + { + 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-ibans/list/data-table.tsx b/frontend/pages/build-ibans/list/data-table.tsx new file mode 100644 index 0000000..d9e3cfb --- /dev/null +++ b/frontend/pages/build-ibans/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 { useDeleteBuildIbanMutation } from "@/pages/build-ibans/queries" + +export function BuildIbansDataTable({ + 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 = useDeleteBuildIbanMutation() + const deleteHandler = (id: string) => { deleteMutation.mutate({ uuid: id }); setTimeout(() => { refetchTable() }, 400) } + const columns = getColumns(router, deleteHandler); + const pagination = React.useMemo(() => ({ pageIndex: currentPage - 1, pageSize: pageSize }), [currentPage, pageSize]) + const totalPages = Math.ceil(totalCount / pageSize) + + const table = useReactTable({ + data, + columns, + pageCount: totalPages, + state: { sorting, columnVisibility, rowSelection, columnFilters, pagination }, + manualPagination: true, + 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-ibans/list/schema.tsx b/frontend/pages/build-ibans/list/schema.tsx new file mode 100644 index 0000000..6d2d66b --- /dev/null +++ b/frontend/pages/build-ibans/list/schema.tsx @@ -0,0 +1,17 @@ +import { z } from "zod"; + +export const schema = z.object({ + _id: z.string(), + uuid: z.string(), + iban: z.string(), + startDate: z.string(), + stopDate: z.string(), + bankCode: z.string(), + xcomment: z.string(), + expiryStarts: z.string().optional(), + expiryEnds: z.string().optional(), + createdAt: z.string(), + updatedAt: z.string(), +}); + +export type schemaType = z.infer; diff --git a/frontend/pages/build-ibans/page.tsx b/frontend/pages/build-ibans/page.tsx new file mode 100644 index 0000000..62fa0bc --- /dev/null +++ b/frontend/pages/build-ibans/page.tsx @@ -0,0 +1,25 @@ +'use client'; +import { BuildIbansDataTable } from './list/data-table'; +import { useState } from 'react'; +import { useGraphQlBuildIbansList } from './queries'; + +const PageBuildIbans = () => { + + 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 } = useGraphQlBuildIbansList({ 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 build areas
} + + return ; + +}; + +export { PageBuildIbans }; diff --git a/frontend/pages/build-ibans/queries.tsx b/frontend/pages/build-ibans/queries.tsx new file mode 100644 index 0000000..4764f9d --- /dev/null +++ b/frontend/pages/build-ibans/queries.tsx @@ -0,0 +1,36 @@ +'use client' +import { useQuery, useMutation } from '@tanstack/react-query' +import { ListArguments } from '@/types/listRequest' + +const fetchGraphQlBuildIbansList = 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-ibans/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 fetchGraphQlDeleteBuildIban = async (uuid: string): Promise => { + console.log('Fetching test data from local API'); + try { + const res = await fetch(`/api/build-ibans/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 useGraphQlBuildIbansList(params: ListArguments) { + return useQuery({ queryKey: ['graphql-build-ibans-list', params], queryFn: () => fetchGraphQlBuildIbansList(params) }) +} + +export function useDeleteBuildIbanMutation() { + return useMutation({ + mutationFn: ({ uuid }: { uuid: string }) => fetchGraphQlDeleteBuildIban(uuid), + onSuccess: () => { console.log("Build iban deleted successfully") }, + onError: (error) => { console.error("Delete build iban failed:", error) }, + }) +} diff --git a/frontend/pages/build-ibans/types.ts b/frontend/pages/build-ibans/types.ts new file mode 100644 index 0000000..f0ed606 --- /dev/null +++ b/frontend/pages/build-ibans/types.ts @@ -0,0 +1,15 @@ +interface BuildSites { + + _id: string; + uuid: string; + siteName: string; + siteNo: string; + expiryStarts: string; + expiryEnds: string; + createdAt: string; + updatedAt: string; + deletedAt: string; + +} + +export type { BuildSites }; diff --git a/frontend/pages/build-ibans/update/form.tsx b/frontend/pages/build-ibans/update/form.tsx new file mode 100644 index 0000000..d10b69b --- /dev/null +++ b/frontend/pages/build-ibans/update/form.tsx @@ -0,0 +1,147 @@ +"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 { useUpdateBuildIbansMutation } from "@/pages/build-ibans/update/queries" +import { BuildIbansUpdate, buildIbansUpdateSchema } from "@/pages/build-ibans/update/schema" + +const BuildIbansForm = ({ refetchTable, initData, selectedUuid }: { refetchTable: () => void, initData: BuildIbansUpdate, selectedUuid: string }) => { + + const form = useForm({ resolver: zodResolver(buildIbansUpdateSchema), defaultValues: { ...initData } }) + + const { handleSubmit } = form + + const mutation = useUpdateBuildIbansMutation(); + + function onSubmit(values: BuildIbansUpdate) { mutation.mutate({ data: values as any || initData, uuid: selectedUuid }); setTimeout(() => refetchTable(), 400) } + + return ( +
+ + + {/* ROW 1 */} +
+ ( + + IBAN + + + + + + )} + /> + + ( + + Bank Code + + + + + + )} + /> +
+ +
+ ( + + Start Date + + + + + + )} + /> + ( + + Stop Date + + + + + + )} + /> + +
+ + +
+ ( + + Comment + + + + + + )} + /> +
+ + + + {/* EXPIRY DATES */} +
+ + ( + + Expiry Starts + + + + + + )} + /> + + ( + + Expiry Ends + + + + + + )} + /> +
+ + + + + ); + +} + +export { BuildIbansForm } diff --git a/frontend/pages/build-ibans/update/page.tsx b/frontend/pages/build-ibans/update/page.tsx new file mode 100644 index 0000000..a02811d --- /dev/null +++ b/frontend/pages/build-ibans/update/page.tsx @@ -0,0 +1,36 @@ +'use client'; +import { useState } from 'react'; +import { BuildIbansForm } from '@/pages/build-ibans/update/form'; +import { useSearchParams, useRouter } from 'next/navigation' +import { Button } from '@/components/ui/button'; +import { BuildIbansDataTableUpdate } from './table/data-table'; +import { useGraphQlBuildIbansList } from '../queries'; + +const PageUpdateBuildIbans = () => { + 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 } = useGraphQlBuildIbansList({ limit, skip: (page - 1) * limit, sort, filters: { ...filters, uuid } }); + const initData = data?.data?.[0] || null; + if (!initData) { return backToBuildAddress } + return ( + <> + + + + ) +} + +export { PageUpdateBuildIbans }; diff --git a/frontend/pages/build-ibans/update/queries.tsx b/frontend/pages/build-ibans/update/queries.tsx new file mode 100644 index 0000000..7feb194 --- /dev/null +++ b/frontend/pages/build-ibans/update/queries.tsx @@ -0,0 +1,26 @@ +'use client' +import { useMutation } from '@tanstack/react-query' +import { UpdateBuildIbansUpdate } from './types'; +import { toISOIfNotZ } from '@/lib/utils'; + +const fetchGraphQlBuildIbansUpdate = async (record: UpdateBuildIbansUpdate, uuid: string): Promise<{ data: UpdateBuildIbansUpdate | null; status: number }> => { + console.log('Fetching test data from local API'); + record.expiryStarts = record.expiryStarts ? toISOIfNotZ(record.expiryStarts) : undefined; + record.expiryEnds = record.expiryEnds ? toISOIfNotZ(record.expiryEnds) : undefined; + record.startDate = toISOIfNotZ(record.startDate); + record.stopDate = toISOIfNotZ(record.stopDate); + try { + const res = await fetch(`/api/build-ibans/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 useUpdateBuildIbansMutation() { + return useMutation({ + mutationFn: ({ data, uuid }: { data: UpdateBuildIbansUpdate, uuid: string }) => fetchGraphQlBuildIbansUpdate(data, uuid), + onSuccess: () => { console.log("Build IBANs updated successfully") }, + onError: (error) => { console.error("Update Build IBANs failed:", error) }, + }) +} diff --git a/frontend/pages/build-ibans/update/schema.ts b/frontend/pages/build-ibans/update/schema.ts new file mode 100644 index 0000000..f3df1e3 --- /dev/null +++ b/frontend/pages/build-ibans/update/schema.ts @@ -0,0 +1,15 @@ +import { z } from "zod" + +export const buildIbansUpdateSchema = z.object({ + + iban: z.string(), + startDate: z.string(), + stopDate: z.string(), + bankCode: z.string(), + xcomment: z.string(), + expiryStarts: z.string().optional(), + expiryEnds: z.string().optional(), + +}); + +export type BuildIbansUpdate = z.infer; diff --git a/frontend/pages/build-ibans/update/table/columns.tsx b/frontend/pages/build-ibans/update/table/columns.tsx new file mode 100644 index 0000000..5bc2500 --- /dev/null +++ b/frontend/pages/build-ibans/update/table/columns.tsx @@ -0,0 +1,98 @@ +"use client" +import { z } from "zod" +import { Button } from "@/components/ui/button" +import { Drawer, DrawerClose, DrawerContent, DrawerFooter, DrawerHeader, DrawerTrigger } from "@/components/ui/drawer" +import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger } from "@/components/ui/dropdown-menu" +import { Input } from "@/components/ui/input" +import { Label } from "@/components/ui/label" +import { useSortable } from "@dnd-kit/sortable" +import { IconGripVertical } from "@tabler/icons-react" +import { useIsMobile } from "@/hooks/use-mobile" +import { Separator } from "@/components/ui/separator" +import { ColumnDef, flexRender, Row } from "@tanstack/react-table" +import { TableCell, TableRow } from "@/components/ui/table" +import { CSS } from "@dnd-kit/utilities" +import { schema, schemaType } from "./schema" +import { dateToLocaleString } from "@/lib/utils" +import { Pencil, Trash } from "lucide-react" + +export function DraggableRow({ row }: { row: Row> }) { + const { transform, transition, setNodeRef, isDragging } = useSortable({ id: row.original._id }) + return ( + + {row.getVisibleCells().map((cell) => ( + {flexRender(cell.column.columnDef.cell, cell.getContext())} + ))} + + ) +} + +function getColumns(router: any, deleteHandler: (id: string) => void): ColumnDef[] { + return [ + { + accessorKey: "uuid", + header: "UUID", + cell: ({ getValue }) => (
{String(getValue())}
), + }, + { + accessorKey: "iban", + header: "IBAN", + }, + { + accessorKey: "startDate", + header: "Start Date", + cell: ({ getValue }) => dateToLocaleString(getValue() as string), + }, + { + accessorKey: "stopDate", + header: "Stop Date", + cell: ({ getValue }) => dateToLocaleString(getValue() as string), + }, + { + accessorKey: "bankCode", + header: "Bank Code", + }, + { + accessorKey: "xcomment", + header: "Comment", + }, + { + 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-ibans/update/table/data-table.tsx b/frontend/pages/build-ibans/update/table/data-table.tsx new file mode 100644 index 0000000..062bd99 --- /dev/null +++ b/frontend/pages/build-ibans/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 BuildIbansDataTableUpdate({ + 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-ibans/update/table/schema.tsx b/frontend/pages/build-ibans/update/table/schema.tsx new file mode 100644 index 0000000..6d2d66b --- /dev/null +++ b/frontend/pages/build-ibans/update/table/schema.tsx @@ -0,0 +1,17 @@ +import { z } from "zod"; + +export const schema = z.object({ + _id: z.string(), + uuid: z.string(), + iban: z.string(), + startDate: z.string(), + stopDate: z.string(), + bankCode: z.string(), + xcomment: z.string(), + expiryStarts: z.string().optional(), + expiryEnds: z.string().optional(), + createdAt: z.string(), + updatedAt: z.string(), +}); + +export type schemaType = z.infer; diff --git a/frontend/pages/build-ibans/update/types.ts b/frontend/pages/build-ibans/update/types.ts new file mode 100644 index 0000000..1a88475 --- /dev/null +++ b/frontend/pages/build-ibans/update/types.ts @@ -0,0 +1,13 @@ + +interface UpdateBuildIbansUpdate { + + iban: string; + startDate: string; + stopDate: string; + bankCode: string; + xcomment: string; + expiryStarts?: string; + expiryEnds?: string; +} + +export type { UpdateBuildIbansUpdate }; \ No newline at end of file diff --git a/frontend/pages/build-sites/update/page.tsx b/frontend/pages/build-sites/update/page.tsx index 9ca2e63..34cff2d 100644 --- a/frontend/pages/build-sites/update/page.tsx +++ b/frontend/pages/build-sites/update/page.tsx @@ -1,7 +1,7 @@ 'use client'; import { useState } from 'react'; import { useGraphQlBuildSitesList } from '@/pages/build-sites/queries'; -import { BuildAddressDataTableUpdate } from '@/pages/build-sites/update/table/data-table'; +import { BuildSitesDataTableUpdate } from '@/pages/build-sites/update/table/data-table'; import { BuildAddressForm } from '@/pages/build-sites/update/form'; import { useSearchParams, useRouter } from 'next/navigation' import { Button } from '@/components/ui/button'; @@ -24,7 +24,7 @@ const PageUpdateBuildSites = () => { if (!initData) { return backToBuildAddress } return ( <> - diff --git a/frontend/pages/build-sites/update/table/data-table.tsx b/frontend/pages/build-sites/update/table/data-table.tsx index eb39d18..e939a3e 100644 --- a/frontend/pages/build-sites/update/table/data-table.tsx +++ b/frontend/pages/build-sites/update/table/data-table.tsx @@ -72,7 +72,7 @@ import { useRouter } from "next/navigation" import { Home } from "lucide-react" import { useDeletePersonMutation } from "@/pages/people/queries" -export function BuildAddressDataTableUpdate({ +export function BuildSitesDataTableUpdate({ data, totalCount, currentPage, @@ -167,9 +167,9 @@ export function BuildAddressDataTableUpdate({ })} -