diff --git a/backend/src/models/living-spaces.model.ts b/backend/src/models/living-spaces.model.ts index 3e1bc0d..6706a65 100644 --- a/backend/src/models/living-spaces.model.ts +++ b/backend/src/models/living-spaces.model.ts @@ -3,6 +3,7 @@ import { randomUUID } from 'crypto'; import { Prop, SchemaFactory } from '@nestjs/mongoose'; import { Field, ID, ObjectType } from '@nestjs/graphql'; import { Base } from '@/models/base.model'; +import { Build } from '@/models/build.model'; import { BuildParts } from '@/models/build-parts.model'; import { Person } from '@/models/person.model'; import { Company } from '@/models/company.model'; @@ -11,10 +12,18 @@ import { UserType } from '@/models/user-type.model'; @ObjectType() export class LivingSpaces extends Base { + @Field(() => ID) + @Prop({ type: Types.ObjectId, ref: Build.name, required: true }) + build: Types.ObjectId; + @Field(() => ID) @Prop({ type: Types.ObjectId, ref: BuildParts.name, required: true }) part: Types.ObjectId; + @Field(() => ID) + @Prop({ type: Types.ObjectId, ref: UserType.name, required: true }) + userType: Types.ObjectId; + @Field(() => ID) @Prop({ type: Types.ObjectId, ref: Company.name, required: true }) company: Types.ObjectId; @@ -23,10 +32,6 @@ export class LivingSpaces extends Base { @Prop({ type: Types.ObjectId, ref: Person.name, required: true }) person: Types.ObjectId; - @Field(() => ID) - @Prop({ type: Types.ObjectId, ref: UserType.name, required: true }) - userType: Types.ObjectId; - } export type LivingSpacesDocument = LivingSpaces & Document; @@ -36,7 +41,5 @@ export function getDynamicLivingSpaceModel(collectionToken: string, conn?: Conne const connection = conn || defaultConnection; const collectionName = `LivingSpaces${collectionToken}`; if (connection.models[collectionName]) { return connection.models[collectionName] as Model } - const schema = SchemaFactory.createForClass(LivingSpaces); - schema.add({ uuid: { type: String, required: false, unique: true, default: () => randomUUID() } }); - return connection.model(collectionName, schema, collectionName) as unknown as Model; + throw new Error('No Living Spaces is found') } diff --git a/backend/src/models/user-type.model.ts b/backend/src/models/user-type.model.ts index f18a6d8..48422a3 100644 --- a/backend/src/models/user-type.model.ts +++ b/backend/src/models/user-type.model.ts @@ -1,25 +1,35 @@ import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; import { Document } from 'mongoose'; import { ObjectType, Field, ID } from '@nestjs/graphql'; +import { Base } from './base.model'; @ObjectType() @Schema({ timestamps: true }) -export class UserType { - @Field() +export class UserType extends Base { + + @Field(() => ID) + readonly _id: string + + @Field({ nullable: false }) @Prop({ required: true }) type: string; - @Field() + @Field({ nullable: false }) @Prop({ required: true }) token: string; - @Field() + @Field({ nullable: false }) @Prop({ required: true }) typeToken: string; - @Field() + @Field({ nullable: false }) @Prop({ required: true }) description: string; + + @Field({ nullable: false }) + @Prop({ required: true }) + isProperty: boolean; + } export type UserTypeDocument = UserType & Document; diff --git a/backend/src/models/user.model.ts b/backend/src/models/user.model.ts index 26aadd7..a70bb71 100644 --- a/backend/src/models/user.model.ts +++ b/backend/src/models/user.model.ts @@ -3,7 +3,6 @@ import { ObjectType, Field, ID } from '@nestjs/graphql'; import { Document, Types } from 'mongoose'; import { Base } from '@/models/base.model'; import { Person } from '@/models/person.model'; -import { UserType } from '@/models/user-type.model'; @ObjectType() export class CollectionTokenItem { diff --git a/backend/src/user-types/dto/create-user-types.input.ts b/backend/src/user-types/dto/create-user-types.input.ts index 42c2f26..ca26d41 100644 --- a/backend/src/user-types/dto/create-user-types.input.ts +++ b/backend/src/user-types/dto/create-user-types.input.ts @@ -1,9 +1,22 @@ +import { ExpiryBaseInput } from '@/models/base.model'; import { InputType, Field } from '@nestjs/graphql'; @InputType() -export class CreateUserTypesInput { +export class CreateUserTypesInput extends ExpiryBaseInput { - @Field() - uuid: string; + @Field({ nullable: false }) + type: string; + + @Field({ nullable: false }) + token: string; + + @Field({ nullable: false }) + typeToken: string; + + @Field({ nullable: false }) + description: string; + + @Field({ nullable: false }) + isProperty: boolean; } diff --git a/backend/src/user-types/dto/list-build-sites.response.ts b/backend/src/user-types/dto/list-build-sites.response.ts new file mode 100644 index 0000000..d84140e --- /dev/null +++ b/backend/src/user-types/dto/list-build-sites.response.ts @@ -0,0 +1,13 @@ +import { ObjectType, Field } from "@nestjs/graphql"; +import { UserType } from "@/models/user-type.model"; + +@ObjectType() +export class ListUserTypeResponse { + + @Field(() => [UserType], { nullable: true }) + data: UserType[]; + + @Field(() => Number, { nullable: true }) + totalCount: number; + +} diff --git a/backend/src/user-types/dto/update-build-sites.input.ts b/backend/src/user-types/dto/update-build-sites.input.ts new file mode 100644 index 0000000..f476c85 --- /dev/null +++ b/backend/src/user-types/dto/update-build-sites.input.ts @@ -0,0 +1,22 @@ +import { InputType, Field } from "@nestjs/graphql"; +import { ExpiryBaseInput } from "@/models/base.model"; + +@InputType() +export class UpdateUserTypeInput extends ExpiryBaseInput { + + @Field({ nullable: true }) + type?: string; + + @Field({ nullable: true }) + token?: string; + + @Field({ nullable: true }) + typeToken?: string; + + @Field({ nullable: true }) + description?: string; + + @Field({ nullable: true }) + isProperty?: boolean; + +} \ No newline at end of file diff --git a/backend/src/user-types/user-types.resolver.ts b/backend/src/user-types/user-types.resolver.ts index e7c5d4c..508e3eb 100644 --- a/backend/src/user-types/user-types.resolver.ts +++ b/backend/src/user-types/user-types.resolver.ts @@ -2,28 +2,36 @@ import { Resolver, Query, Args, ID, Info, Mutation } from '@nestjs/graphql'; import { Types } from 'mongoose'; import { UserType } from '@/models/user-type.model'; import { UserTypesService } from '@/user-types/user-types.service'; +import { CreateUserTypesInput } from './dto/create-user-types.input'; +import { ListUserTypeResponse } from './dto/list-build-sites.response'; +import { UpdateUserTypeInput } from './dto/update-build-sites.input'; +import { ListArguments } from '@/dto/list.input'; import type { GraphQLResolveInfo } from 'graphql'; import graphqlFields from 'graphql-fields'; -import { CreateUserTypesInput } from './dto/create-user-types.input'; @Resolver() export class UserTypesResolver { constructor(private readonly userTypesService: UserTypesService) { } - @Query(() => [UserType], { name: 'userTypes' }) - async getUsers(@Info() info: GraphQLResolveInfo): Promise { - const fields = graphqlFields(info); const projection = this.userTypesService.buildProjection(fields); return this.userTypesService.findAll(projection); + @Query(() => ListUserTypeResponse, { name: 'getUserTypes' }) + async getUserTypes(@Info() info: GraphQLResolveInfo, @Args('input') input: ListArguments): Promise { + const fields = graphqlFields(info); const projection = this.userTypesService.buildProjection(fields?.data); const { skip, limit, sort, filters } = input; + return await this.userTypesService.findAll(projection, skip, limit, sort, filters); } - @Query(() => UserType, { name: 'userType', nullable: true }) + @Query(() => UserType, { name: 'getUserType', nullable: true }) async getUserType(@Args('id', { type: () => ID }) id: string, @Info() info: GraphQLResolveInfo): Promise { const fields = graphqlFields(info); const projection = this.userTypesService.buildProjection(fields); return this.userTypesService.findById(new Types.ObjectId(id), projection); } @Mutation(() => UserType, { name: 'createUserType' }) - async createUserType(@Args('input') input: CreateUserTypesInput): Promise { - return this.userTypesService.create(input); - } + async createUserType(@Args('input') input: CreateUserTypesInput): Promise { return this.userTypesService.create(input) } + + @Mutation(() => UserType, { name: 'updateUserType' }) + async updateUserType(@Args('uuid') uuid: string, @Args('input') input: UpdateUserTypeInput): Promise { return this.userTypesService.update(uuid, input) } + + @Mutation(() => Boolean, { name: 'deleteUserType' }) + async deleteUserType(@Args('uuid') uuid: string): Promise { return this.userTypesService.delete(uuid) } } diff --git a/backend/src/user-types/user-types.service.ts b/backend/src/user-types/user-types.service.ts index 2f6d594..f44c7a5 100644 --- a/backend/src/user-types/user-types.service.ts +++ b/backend/src/user-types/user-types.service.ts @@ -1,8 +1,10 @@ import { Injectable } from '@nestjs/common'; -import { CreateUserTypesInput } from './dto/create-user-types.input'; import { InjectModel } from '@nestjs/mongoose'; import { Types, Model } from 'mongoose'; -import { UserType, UserTypeDocument } from '@/models/user-type.model'; +import { CreateUserTypesInput } from './dto/create-user-types.input'; +import { ListUserTypeResponse } from './dto/list-build-sites.response'; +import { UpdateUserTypeInput } from './dto/update-build-sites.input'; +import { UserType, UserTypeDocument } from '@/models/user-type.model' @Injectable() export class UserTypesService { @@ -11,25 +13,29 @@ export class UserTypesService { @InjectModel(UserType.name) private readonly userTypesModel: Model ) { } - async findAll(projection?: any): Promise { - return this.userTypesModel.find({}, projection, { lean: false }).populate({ path: 'person', select: projection?.person }).exec(); + async findAll(projection: any, skip: number, limit: number, sort?: Record, filters?: Record): Promise { + const query: any = {}; if (filters && Object.keys(filters).length > 0) { Object.assign(query, filters) }; + const totalCount = await this.userTypesModel.countDocuments(query).exec(); + const data = await this.userTypesModel.find(query, projection, { lean: true }).skip(skip).limit(limit).sort(sort).exec(); + return { data, totalCount }; } async findById(id: Types.ObjectId, projection?: any): Promise { - return this.userTypesModel.findById(id, projection, { lean: false }).populate({ path: 'person', select: projection?.person }).exec(); + return this.userTypesModel.findById(id, projection, { lean: false }).populate({ path: 'buildSites', select: projection?.buildSites }).exec(); } - async create(input: CreateUserTypesInput): Promise { - const user = new this.userTypesModel(input); - return user.save(); - } + async create(input: CreateUserTypesInput): Promise { const buildSite = new this.userTypesModel(input); return buildSite.save() } + + async update(uuid: string, input: UpdateUserTypeInput): Promise { const buildSite = await this.userTypesModel.findOne({ uuid }); if (!buildSite) { throw new Error('BuildSite not found') }; buildSite.set(input); return buildSite.save() } + + async delete(uuid: string): Promise { const buildSite = await this.userTypesModel.deleteMany({ uuid }); return buildSite.deletedCount > 0 } buildProjection(fields: Record): any { const projection: any = {}; for (const key in fields) { - if (key === 'user' && typeof fields[key] === 'object') { for (const subField of Object.keys(fields[key])) { projection[`user.${subField}`] = 1 } } + if (key === 'buildSites' && typeof fields[key] === 'object') { for (const subField of Object.keys(fields[key])) { projection[`buildSites.${subField}`] = 1 } } else { projection[key] = 1 } - } - return projection; + }; return projection; } + } diff --git a/frontend/components/menu/LeftMenu.tsx b/frontend/app/api/living-space/a.txt similarity index 100% rename from frontend/components/menu/LeftMenu.tsx rename to frontend/app/api/living-space/a.txt diff --git a/frontend/app/api/user-types/add/route.ts b/frontend/app/api/user-types/add/route.ts new file mode 100644 index 0000000..e0a95a6 --- /dev/null +++ b/frontend/app/api/user-types/add/route.ts @@ -0,0 +1,21 @@ +'use server'; +import { NextResponse } from 'next/server'; +import { GraphQLClient, gql } from 'graphql-request'; +import { buildPartsAddSchema } from './schema'; + +const endpoint = "http://localhost:3001/graphql"; + +export async function POST(request: Request) { + const body = await request.json(); + const validatedBody = buildPartsAddSchema.parse({ ...body.data, buildId: body.buildId }); + try { + const client = new GraphQLClient(endpoint); + const query = gql`mutation CreateUserType($input: CreateUserTypesInput!) { createUserType(input: $input) { _id }}`; + const variables = { input: validatedBody }; + const data = await client.request(query, variables); + return NextResponse.json({ data: data.createUserType, status: 200 }); + } catch (err: any) { + console.error(err); + return NextResponse.json({ error: err.message }, { status: 500 }); + } +} diff --git a/frontend/app/api/user-types/add/schema.ts b/frontend/app/api/user-types/add/schema.ts new file mode 100644 index 0000000..c54cafd --- /dev/null +++ b/frontend/app/api/user-types/add/schema.ts @@ -0,0 +1,23 @@ +import { z } from "zod" + +export const buildPartsAddSchema = z.object({ + buildId: z.string(), + addressGovCode: z.string(), + no: z.number(), + level: z.number(), + code: z.string(), + grossSize: z.number(), + netSize: z.number(), + defaultAccessory: z.string(), + humanLivability: z.boolean(), + key: z.string(), + directionId: z.string().optional(), + typeId: z.string().optional(), + active: z.boolean().default(true), + isConfirmed: z.boolean().default(false), + expiryStarts: z.string().optional(), + expiryEnds: z.string().optional() + +}); + +export type BuildPartsAdd = z.infer; diff --git a/frontend/app/api/user-types/delete/route.ts b/frontend/app/api/user-types/delete/route.ts new file mode 100644 index 0000000..53f64ac --- /dev/null +++ b/frontend/app/api/user-types/delete/route.ts @@ -0,0 +1,23 @@ +'use server'; +import { NextResponse } from 'next/server'; +import { GraphQLClient, gql } from 'graphql-request'; + +const endpoint = "http://localhost:3001/graphql"; + +export async function GET(request: Request) { + + const searchParams = new URL(request.url).searchParams; + const uuid = searchParams.get('uuid'); + if (!uuid) { return NextResponse.json({ error: 'UUID not found in search params' }, { status: 400 }) } + try { + const client = new GraphQLClient(endpoint); + const query = gql`mutation DeleteUserType($uuid: String!) { deleteUserType(uuid: $uuid) }`; + const variables = { uuid: uuid }; + const data = await client.request(query, variables); + return NextResponse.json({ data: data.deleteUserType, status: 200 }); + } catch (err: any) { + console.error(err); + return NextResponse.json({ error: err.message }, { status: 500 }); + } + +} diff --git a/frontend/app/api/user-types/list/route.ts b/frontend/app/api/user-types/list/route.ts new file mode 100644 index 0000000..21851ce --- /dev/null +++ b/frontend/app/api/user-types/list/route.ts @@ -0,0 +1,38 @@ +'use server'; +import { NextResponse } from 'next/server'; +import { GraphQLClient, gql } from 'graphql-request'; + +const endpoint = "http://localhost:3001/graphql"; + +export async function POST(request: Request) { + const body = await request.json(); + const { limit, skip, sort, filters } = body; + try { + const client = new GraphQLClient(endpoint); + const query = gql` + query GetUserTypes($input: ListArguments!) { + getUserTypes(input: $input) { + data { + _id + uuid + createdAt + expiryStarts + expiryEnds + type + token + typeToken + description + isProperty + } + totalCount + } + } + `; + const variables = { input: { limit, skip, sort, filters } }; + const data = await client.request(query, variables); + return NextResponse.json({ data: data.getUserTypes.data, totalCount: data.getUserTypes.totalCount }); + } catch (err: any) { + console.error(err); + return NextResponse.json({ error: err.message }, { status: 500 }); + } +} diff --git a/frontend/app/api/user-types/update/route.ts b/frontend/app/api/user-types/update/route.ts new file mode 100644 index 0000000..e5f2215 --- /dev/null +++ b/frontend/app/api/user-types/update/route.ts @@ -0,0 +1,24 @@ +'use server'; +import { NextResponse } from 'next/server'; +import { GraphQLClient, gql } from 'graphql-request'; +import { UpdateBuildPartsSchema } from './schema'; + +const endpoint = "http://localhost:3001/graphql"; + +export async function POST(request: Request) { + const searchUrl = new URL(request.url); + const uuid = searchUrl.searchParams.get("uuid") || ""; + const body = await request.json(); + const validatedBody = UpdateBuildPartsSchema.parse(body); + if (uuid === "") { return NextResponse.json({ error: "UUID is required" }, { status: 400 }) } + try { + const client = new GraphQLClient(endpoint); + const query = gql`mutation UpdateUserType($uuid: String!, $input: UpdateUserTypeInput!) { updateUserType(uuid: $uuid, input: $input) { _id } }`; + const variables = { uuid: uuid, input: { ...validatedBody } }; + const data = await client.request(query, variables); + return NextResponse.json({ data: data.updateUserType, status: 200 }); + } catch (err: any) { + console.error(err); + return NextResponse.json({ error: err.message }, { status: 500 }); + } +} diff --git a/frontend/app/api/user-types/update/schema.ts b/frontend/app/api/user-types/update/schema.ts new file mode 100644 index 0000000..d19fd5e --- /dev/null +++ b/frontend/app/api/user-types/update/schema.ts @@ -0,0 +1,24 @@ +import { z } from "zod" + +export const UpdateBuildPartsSchema = z.object({ + + buildId: z.string().optional(), + addressGovCode: z.string(), + no: z.number(), + level: z.number(), + code: z.string(), + grossSize: z.number(), + netSize: z.number(), + defaultAccessory: z.string(), + humanLivability: z.boolean(), + key: z.string(), + directionId: z.string().optional(), + typeId: z.string().optional(), + active: z.boolean(), + isConfirmed: z.boolean(), + expiryStarts: z.string().optional(), + expiryEnds: z.string().optional() + +}); + +export type UpdateBuildParts = z.infer; diff --git a/frontend/app/living-spaces/page.tsx b/frontend/app/living-spaces/page.tsx new file mode 100644 index 0000000..a1f3e30 --- /dev/null +++ b/frontend/app/living-spaces/page.tsx @@ -0,0 +1,5 @@ +import { PageLivingSpace } from "@/pages/living-space/tables/page" + +const SSRPageLivingSpace = () => { return <> } + +export default SSRPageLivingSpace; diff --git a/frontend/app/user-types/add/page.tsx b/frontend/app/user-types/add/page.tsx new file mode 100644 index 0000000..5b1b947 --- /dev/null +++ b/frontend/app/user-types/add/page.tsx @@ -0,0 +1,5 @@ +import { PageAddUserTypes } from "@/pages/user-types/add/page"; + +const UserTypesAddPage = () => { return } + +export default UserTypesAddPage; diff --git a/frontend/app/user-types/page.tsx b/frontend/app/user-types/page.tsx new file mode 100644 index 0000000..1b8c202 --- /dev/null +++ b/frontend/app/user-types/page.tsx @@ -0,0 +1,5 @@ +import { PageUserTypes } from '@/pages/user-types/page'; + +const UserTypesPage = () => { return } + +export default UserTypesPage; diff --git a/frontend/app/user-types/update/page.tsx b/frontend/app/user-types/update/page.tsx new file mode 100644 index 0000000..49f729d --- /dev/null +++ b/frontend/app/user-types/update/page.tsx @@ -0,0 +1,5 @@ +import { PageUpdateUserTypes } from "@/pages/user-types/update/page"; + +const UserTypesUpdatePage = () => { return <> } + +export default UserTypesUpdatePage; diff --git a/frontend/components/sidebar/app-sidebar.tsx b/frontend/components/sidebar/app-sidebar.tsx index 00e947b..2698752 100644 --- a/frontend/components/sidebar/app-sidebar.tsx +++ b/frontend/components/sidebar/app-sidebar.tsx @@ -10,13 +10,14 @@ import { IconMessageCircle, IconChartArea, IconCreditCard, - IconBoxModel + IconBoxModel, + IconHome2, + IconFileCertificate, } 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" const data = { @@ -70,6 +71,16 @@ const data = { title: "Build ibans", url: "/build-ibans", icon: IconCreditCard + }, + { + title: "Living Spaces", + url: "/living-spaces", + icon: IconHome2 + }, + { + title: "User Types", + url: "/user-types", + icon: IconFileCertificate } ], navClouds: [ diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 1eb25b7..6a144d1 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -5642,6 +5642,7 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", diff --git a/frontend/pages/build-sites/add/page.tsx b/frontend/pages/build-sites/add/page.tsx index 2a7315f..ed263f5 100644 --- a/frontend/pages/build-sites/add/page.tsx +++ b/frontend/pages/build-sites/add/page.tsx @@ -4,7 +4,6 @@ import { BuildSitesForm } from '@/pages/build-sites/add/form'; import { BuildSitesDataTableAdd } from './table/data-table'; import { useGraphQlBuildSitesList } from '@/pages/build-sites/queries'; import { useSearchParams, useRouter } from 'next/navigation'; -import { Button } from '@/components/ui/button'; const PageAddBuildSites = () => { const [page, setPage] = useState(1); @@ -14,10 +13,7 @@ const PageAddBuildSites = () => { const searchParams = useSearchParams(); const router = useRouter(); - const buildId = searchParams?.get('build'); - const noUUIDFound = <>
Back To Builds. No uuid is found on headers
- if (!buildId) { return noUUIDFound }; - const { data, isLoading, error, refetch } = useGraphQlBuildSitesList({ limit, skip: (page - 1) * limit, sort, filters: { ...filters, buildID: buildId } }); + const { data, isLoading, error, refetch } = useGraphQlBuildSitesList({ limit, skip: (page - 1) * limit, sort, filters: { ...filters } }); return ( <> diff --git a/frontend/pages/builds/list/data-table.tsx b/frontend/pages/builds/list/data-table.tsx index 59bdfcb..4824ffd 100644 --- a/frontend/pages/builds/list/data-table.tsx +++ b/frontend/pages/builds/list/data-table.tsx @@ -1,5 +1,4 @@ -"use client" - +"use client"; import * as React from "react" import { closestCenter, diff --git a/frontend/pages/living-space/ReadMe.txt b/frontend/pages/living-space/ReadMe.txt new file mode 100644 index 0000000..b278986 --- /dev/null +++ b/frontend/pages/living-space/ReadMe.txt @@ -0,0 +1,28 @@ +# Todo List + +First Step: Get build data && Make build selection +build: Types.ObjectId; + +Second Step: Get user types data && Make user types selection +Condition: Unlock if build is selected +userType: Types.ObjectId; + +Third Step: Get parts data && Make parts selection +Condition: Unlock if user type is selected && UserType.isProperty === true +part: Types.ObjectId; + +Fourt Step: Get company data && Make company selection +Condition: Unlock if build is selected +company: Types.ObjectId; + +Fifth Step: Get person data && Make person selection +Condition: Unlock if build is selected +person: Types.ObjectId; + +Condition: + + if collectionToken is selected @Build && + if buildID, userTypeID, partID, companyID, personID is selected then=> + + Add Living Spaces to Mongo + + Update related collections + + Validate data integrity diff --git a/frontend/pages/living-space/tables/builds/columns.tsx b/frontend/pages/living-space/tables/builds/columns.tsx new file mode 100644 index 0000000..7b72c66 --- /dev/null +++ b/frontend/pages/living-space/tables/builds/columns.tsx @@ -0,0 +1,135 @@ +"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" +import { IconHandClick } from "@tabler/icons-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(selectionHandler: (id: string, token: string) => void): ColumnDef[] { + return [ + { + accessorKey: "buildType.token", + header: "Token", + cell: ({ getValue }) => getValue(), + }, + { + accessorKey: "collectionToken", + header: "Collection Token", + cell: ({ getValue }) => getValue(), + }, + { + accessorKey: "info.govAddressCode", + header: "Gov Address Code", + cell: ({ getValue }) => getValue(), + }, + { + accessorKey: "info.buildName", + header: "Build Name", + cell: ({ getValue }) => getValue(), + }, + { + accessorKey: "info.buildNo", + header: "Build No", + cell: ({ getValue }) => getValue(), + }, + { + accessorKey: "info.maxFloor", + header: "Max Floor", + cell: ({ getValue }) => getValue(), + }, + { + accessorKey: "info.undergroundFloor", + header: "Underground Floor", + cell: ({ getValue }) => getValue(), + }, + { + accessorKey: "info.buildDate", + header: "Build Date", + cell: ({ getValue }) => getValue() ? dateToLocaleString(getValue() as string) : "-", + }, + { + accessorKey: "info.decisionPeriodDate", + header: "Decision Period Date", + cell: ({ getValue }) => getValue() ? dateToLocaleString(getValue() as string) : "-", + }, + { + accessorKey: "info.taxNo", + header: "Tax No", + cell: ({ getValue }) => getValue(), + }, + { + accessorKey: "info.liftCount", + header: "Lift Count", + cell: ({ getValue }) => getValue(), + }, + { + accessorKey: "info.heatingSystem", + header: "Heating System", + cell: ({ getValue }) => getValue(), + }, + { + accessorKey: "info.coolingSystem", + header: "Cooling System", + cell: ({ getValue }) => getValue(), + }, + { + accessorKey: "info.hotWaterSystem", + header: "Hot Water System", + cell: ({ getValue }) => getValue(), + }, + { + accessorKey: "info.blockServiceManCount", + header: "Block Service Man Count", + cell: ({ getValue }) => getValue(), + }, + { + accessorKey: "info.securityServiceManCount", + header: "Security Service Man Count", + cell: ({ getValue }) => getValue(), + }, + { + accessorKey: "info.garageCount", + header: "Garage Count", + cell: ({ getValue }) => getValue(), + }, + { + accessorKey: "info.managementRoomId", + header: "Management Room ID", + cell: ({ getValue }) => getValue(), + }, + { + id: "actions", + header: "Actions", + cell: ({ row }) => { + return ( +
+ +
+ ); + }, + } + ] +} + +export { getColumns }; \ No newline at end of file diff --git a/frontend/pages/living-space/tables/builds/data-table.tsx b/frontend/pages/living-space/tables/builds/data-table.tsx new file mode 100644 index 0000000..6679a4f --- /dev/null +++ b/frontend/pages/living-space/tables/builds/data-table.tsx @@ -0,0 +1,278 @@ +"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" + +export function LivingSpaceBuildDataTable({ + data, + totalCount, + currentPage = 1, + pageSize = 10, + onPageChange, + onPageSizeChange, + refetchTable, + buildId, + setBuildId, + collectionToken, + setCollectionToken, + setIsUserTypeEnabled, +}: { + data: schemaType[], + totalCount: number, + currentPage?: number, + pageSize?: number, + onPageChange: (page: number) => void, + onPageSizeChange: (size: number) => void, + refetchTable: () => void, + buildId: string, + setBuildId: (id: string) => void, + collectionToken: string, + setCollectionToken: (collectionToken: string) => void, + setIsUserTypeEnabled: (enabled: boolean) => 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 setSelection = (id: string, token: string) => { setBuildId(id); setCollectionToken(token); setIsUserTypeEnabled(true) } + const columns = getColumns(setSelection); + 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/living-space/tables/builds/page.tsx b/frontend/pages/living-space/tables/builds/page.tsx new file mode 100644 index 0000000..24ec114 --- /dev/null +++ b/frontend/pages/living-space/tables/builds/page.tsx @@ -0,0 +1,35 @@ +'use client'; +import { useState } from "react"; +import { useGraphQlBuildsList } from "./queries"; +import { LivingSpaceBuildDataTable } from "./data-table"; + +const PageLivingSpaceBuildsTableSection = ( + { buildID, setBuildID, collectionToken, setCollectionToken, setIsUserTypeEnabled }: { + buildID: string | null; + setBuildID: (id: string | null) => void; + collectionToken: string | null; + setCollectionToken: (token: string | null) => void; + setIsUserTypeEnabled: (enabled: boolean) => void; + } +) => { + const [page, setPage] = useState(1); + const [limit, setLimit] = useState(10); + const [sort, setSort] = useState({ createdAt: 'desc' }); + const [filters, setFilters] = useState({}); + + const { data, isLoading, error, refetch } = useGraphQlBuildsList({ limit, skip: (page - 1) * limit, sort, filters }); + + const handlePageChange = (newPage: number) => { setPage(newPage) }; + const handlePageSizeChange = (newSize: number) => { setLimit(newSize); setPage(1) }; + if (isLoading) { return
Loading...
} + if (error) { return
Error loading users
} + + return <> + + ; + +} + +export default PageLivingSpaceBuildsTableSection; \ No newline at end of file diff --git a/frontend/pages/living-space/tables/builds/queries.tsx b/frontend/pages/living-space/tables/builds/queries.tsx new file mode 100644 index 0000000..90e53e2 --- /dev/null +++ b/frontend/pages/living-space/tables/builds/queries.tsx @@ -0,0 +1,36 @@ +'use client' +import { useQuery, useMutation } from '@tanstack/react-query' +import { ListArguments } from '@/types/listRequest' + +const fetchGraphQlBuildsList = async (params: ListArguments): Promise => { + console.log('Fetching test data from local API'); + const { limit, skip, sort, filters } = params; + try { + const res = await fetch('/api/builds/list', { method: 'POST', cache: 'no-store', credentials: "include", body: JSON.stringify({ limit, skip, sort, filters }) }); + if (!res.ok) { const errorText = await res.text(); console.error('Test data API error:', errorText); throw new Error(`API error: ${res.status} ${res.statusText}`) } + const data = await res.json(); + return { data: data.data, totalCount: data.totalCount } + } catch (error) { console.error('Error fetching test data:', error); throw error } +}; + +const fetchGraphQlDeleteBuild = async (uuid: string): Promise => { + console.log('Fetching test data from local API'); + try { + const res = await fetch(`/api/builds/delete?uuid=${uuid}`, { method: 'GET', cache: 'no-store', credentials: "include" }); + if (!res.ok) { const errorText = await res.text(); console.error('Test data API error:', errorText); throw new Error(`API error: ${res.status} ${res.statusText}`) } + const data = await res.json(); + return data + } catch (error) { console.error('Error fetching test data:', error); throw error } +}; + +export function useGraphQlBuildsList(params: ListArguments) { + return useQuery({ queryKey: ['graphql-builds-list', params], queryFn: () => fetchGraphQlBuildsList(params) }) +} + +export function useDeleteBuildMutation() { + return useMutation({ + mutationFn: ({ uuid }: { uuid: string }) => fetchGraphQlDeleteBuild(uuid), + onSuccess: () => { console.log("Person deleted successfully") }, + onError: (error) => { console.error("Delete person failed:", error) }, + }) +} diff --git a/frontend/pages/living-space/tables/builds/schema.tsx b/frontend/pages/living-space/tables/builds/schema.tsx new file mode 100644 index 0000000..563da7b --- /dev/null +++ b/frontend/pages/living-space/tables/builds/schema.tsx @@ -0,0 +1,31 @@ +import { z } from "zod"; + +export const schema = z.object({ + _id: z.string(), + buildType: z.object({ + token: z.string(), + typeToken: z.string(), + type: z.string(), + }), + collectionToken: z.string(), + info: z.object({ + govAddressCode: z.string(), + buildName: z.string(), + buildNo: z.string(), + maxFloor: z.number(), + undergroundFloor: z.number(), + buildDate: z.string(), + decisionPeriodDate: z.string(), + taxNo: z.string(), + liftCount: z.number(), + heatingSystem: z.boolean(), + coolingSystem: z.boolean(), + hotWaterSystem: z.boolean(), + blockServiceManCount: z.number(), + securityServiceManCount: z.number(), + garageCount: z.number(), + managementRoomId: z.number(), + }) +}); + +export type schemaType = z.infer; diff --git a/frontend/pages/living-space/tables/page.tsx b/frontend/pages/living-space/tables/page.tsx new file mode 100644 index 0000000..9459380 --- /dev/null +++ b/frontend/pages/living-space/tables/page.tsx @@ -0,0 +1,48 @@ +'use client'; +import { useState } from "react"; +import PageLivingSpaceBuildsTableSection from "./builds/page"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; + + +const PageLivingSpace = () => { + + const [collectionToken, setCollectionToken] = useState(null); + const [buildID, setBuildID] = useState(null); + const [userTypeID, setUserTypeID] = useState(null); + const [partID, setPartID] = useState(null); + const [companyID, setCompanyID] = useState(null); + const [personID, setPersonID] = useState(null); + + const [isUserTypeEnabled, setIsUserTypeEnabled] = useState(false); + const [isPartsEnabled, setIsPartsEnabled] = useState(false); + const [isCompanyEnabled, setIsCompanyEnabled] = useState(false); + const [isPersonEnabled, setIsPersonEnabled] = useState(false); + + const tabsClassName = "border border-gray-300 rounded-sm h-10" + + return <> +
{JSON.stringify({ buildID, collectionToken, userTypeID, partID, companyID, personID })}
+
+ + + Builds + {isUserTypeEnabled && User Type} + {isPartsEnabled && Parts} + {isCompanyEnabled && Company} + {isPersonEnabled && Person} + +
+ + + + {isUserTypeEnabled && {/* Add UserType section component here */}} + {isPartsEnabled && {/* Add Parts section component here */}} + {isCompanyEnabled && {/* Add Company section component here */}} + {isPersonEnabled && {/* Add Person section component here */}} +
+
+
+ +} + +export { PageLivingSpace }; \ No newline at end of file diff --git a/frontend/pages/user-types/add/form.tsx b/frontend/pages/user-types/add/form.tsx new file mode 100644 index 0000000..397b865 --- /dev/null +++ b/frontend/pages/user-types/add/form.tsx @@ -0,0 +1,392 @@ +"use client" +import { useForm } from "react-hook-form" +import { zodResolver } from "@hookform/resolvers/zod" +import { Form, FormField, FormItem, FormLabel, FormControl, FormMessage } from "@/components/ui/form" +import { Input } from "@/components/ui/input" +import { Button } from "@/components/ui/button" +import { Separator } from "@/components/ui/separator" +import { DateTimePicker } from "@/components/ui/date-time-picker" +import { BuildAdd, buildAddSchema } from "./schema" +import { useAddBuildMutation } from "./queries" +import { Checkbox } from "@/components/ui/checkbox" + +const UserTypesForm = ({ refetchTable }: { refetchTable: () => void }) => { + + const form = useForm({ + resolver: zodResolver(buildAddSchema), + defaultValues: { + buildType: "", + collectionToken: "", + info: { + govAddressCode: "", + buildName: "", + buildNo: "", + maxFloor: 0, + undergroundFloor: 0, + buildDate: "", + decisionPeriodDate: "", + taxNo: "", + liftCount: 0, + heatingSystem: false, + coolingSystem: false, + hotWaterSystem: false, + blockServiceManCount: 0, + securityServiceManCount: 0, + garageCount: 0, + managementRoomId: "", + } + }, + }); + + const { handleSubmit } = form; + + const mutation = useAddBuildMutation(); + + function onSubmit(values: BuildAdd) { mutation.mutate({ data: values }); setTimeout(() => refetchTable(), 400) }; + + return ( +
+ + + {/* ROW 1 */} +
+ ( + + Token + + + + + + )} + /> + + ( + + Collection Token + + + + + + )} + /> +
+ +
+ ( + + Gov Address Code + + + + + + )} + /> + ( + + Build Name + + + + + + )} + /> + ( + + Build No + + + + + + )} + /> +
+ + +
+ ( + + Max Floor + + { + field.onBlur(); + const numValue = parseFloat(e.target.value); + if (!isNaN(numValue)) { + field.onChange(numValue); + } + }} + /> + + + + )} + /> + ( + + Underground Floor + + { + field.onBlur(); + const numValue = parseFloat(e.target.value); + if (!isNaN(numValue)) { + field.onChange(numValue); + } + }} + /> + + + + )} + /> +
+ +
+ ( + + Tax No + + + + + + )} + /> + ( + + Lift Count + + { + field.onBlur(); + const numValue = parseFloat(e.target.value); + if (!isNaN(numValue)) { + field.onChange(numValue); + } + }} + /> + + + + )} + /> +
+ +
+ ( + + + + +
+ + Heating System + +
+
+ )} + /> + ( + + + + +
+ + Cooling System + +
+
+ )} + /> + ( + + + + +
+ + Hot Water System + +
+
+ )} + /> +
+ + +
+ ( + + Build Date + + + + + + )} + /> + ( + + Decision Period Date + + + + + + )} + /> +
+ +
+ ( + + Block Service Man Count + + { + field.onBlur(); + const numValue = parseFloat(e.target.value); + if (!isNaN(numValue)) { + field.onChange(numValue); + } + }} + /> + + + + )} + /> + ( + + Security Service Man Count + + { + field.onBlur(); + const numValue = parseFloat(e.target.value); + if (!isNaN(numValue)) { + field.onChange(numValue); + } + }} + /> + + + + )} + /> + ( + + Total Garage Count In Numbers + + { + field.onBlur(); + const numValue = parseFloat(e.target.value); + if (!isNaN(numValue)) { + field.onChange(numValue); + } + }} + /> + + + + )} + /> + ( + + Management Room ID Assign + + + + + + )} + /> +
+ + + + + ); + +}; + +export { UserTypesForm } diff --git a/frontend/pages/user-types/add/page.tsx b/frontend/pages/user-types/add/page.tsx new file mode 100644 index 0000000..aa93a5a --- /dev/null +++ b/frontend/pages/user-types/add/page.tsx @@ -0,0 +1,26 @@ +'use client'; +import { useState } from 'react'; +import { UserTypesDataTableAdd } from './table/data-table'; +import { UserTypesForm } from './form'; +import { useGraphQlUserTypesList } from '../queries'; + +const PageAddUserTypes = () => { + + 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 } = useGraphQlUserTypesList({ limit, skip: (page - 1) * limit, sort, filters }); + + return ( + <> + + + + ) +} + +export { PageAddUserTypes }; diff --git a/frontend/pages/user-types/add/queries.tsx b/frontend/pages/user-types/add/queries.tsx new file mode 100644 index 0000000..2b2bb3e --- /dev/null +++ b/frontend/pages/user-types/add/queries.tsx @@ -0,0 +1,25 @@ +'use client' +import { useMutation } from '@tanstack/react-query' +import { toISOIfNotZ } from '@/lib/utils'; +import { BuildAdd } from './schema'; + +const fetchGraphQlBuildAdd = async (record: BuildAdd): Promise<{ data: BuildAdd | null; status: number }> => { + console.log('Fetching test data from local API'); + record.info.buildDate = toISOIfNotZ(record.info.buildDate); + record.info.decisionPeriodDate = toISOIfNotZ(record.info.decisionPeriodDate); + console.dir({ record }) + try { + const res = await fetch('/api/builds/add', { method: 'POST', cache: 'no-store', credentials: "include", body: JSON.stringify(record) }); + if (!res.ok) { const errorText = await res.text(); console.error('Test data API error:', errorText); throw new Error(`API error: ${res.status} ${res.statusText}`) } + const data = await res.json(); + return { data: data.data, status: res.status } + } catch (error) { console.error('Error fetching test data:', error); throw error } +}; + +export function useAddBuildMutation() { + return useMutation({ + mutationFn: ({ data }: { data: BuildAdd }) => fetchGraphQlBuildAdd(data), + onSuccess: () => { console.log("Build created successfully") }, + onError: (error) => { console.error("Add build failed:", error) }, + }) +} diff --git a/frontend/pages/user-types/add/schema.ts b/frontend/pages/user-types/add/schema.ts new file mode 100644 index 0000000..431438d --- /dev/null +++ b/frontend/pages/user-types/add/schema.ts @@ -0,0 +1,28 @@ +import { z } from "zod" + +export const buildAddSchema = z.object({ + + buildType: z.string(), + collectionToken: z.string(), + info: z.object({ + govAddressCode: z.string(), + buildName: z.string(), + buildNo: z.string(), + maxFloor: z.number(), + undergroundFloor: z.number(), + buildDate: z.string(), + decisionPeriodDate: z.string(), + taxNo: z.string(), + liftCount: z.number(), + heatingSystem: z.boolean(), + coolingSystem: z.boolean(), + hotWaterSystem: z.boolean(), + blockServiceManCount: z.number(), + securityServiceManCount: z.number(), + garageCount: z.number(), + managementRoomId: z.string(), + }) + +}); + +export type BuildAdd = z.infer; diff --git a/frontend/pages/user-types/add/table/columns.tsx b/frontend/pages/user-types/add/table/columns.tsx new file mode 100644 index 0000000..e453692 --- /dev/null +++ b/frontend/pages/user-types/add/table/columns.tsx @@ -0,0 +1,109 @@ +"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: "type", + header: "Type", + cell: ({ getValue }) => getValue(), + }, + { + accessorKey: "token", + header: "Token", + cell: ({ getValue }) => getValue(), + }, + { + accessorKey: "typeToken", + header: "Type Token", + cell: ({ getValue }) => getValue(), + }, + { + accessorKey: "description", + header: "Description", + cell: ({ getValue }) => getValue(), + }, + { + accessorKey: "isProperty", + header: "Is Property", + cell: ({ getValue }) => getValue() ? (
Yes
) : (
No
), + }, + { + accessorKey: "expiryStarts", + header: "Expiry Starts", + cell: ({ getValue }) => dateToLocaleString(getValue() as string), + }, + { + accessorKey: "expiryEnds", + header: "Expiry Ends", + cell: ({ getValue }) => dateToLocaleString(getValue() as string), + }, + { + accessorKey: "createdAt", + header: "Created At", + cell: ({ getValue }) => dateToLocaleString(getValue() as string), + }, + { + accessorKey: "updateAt", + header: "Updated At", + cell: ({ 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/user-types/add/table/data-table.tsx b/frontend/pages/user-types/add/table/data-table.tsx new file mode 100644 index 0000000..d23fe2b --- /dev/null +++ b/frontend/pages/user-types/add/table/data-table.tsx @@ -0,0 +1,254 @@ +"use client" +import * as React from "react" +import { + closestCenter, + DndContext, + KeyboardSensor, + MouseSensor, + TouchSensor, + useSensor, + useSensors, + type UniqueIdentifier, +} from "@dnd-kit/core" +import { restrictToVerticalAxis } from "@dnd-kit/modifiers" +import { + SortableContext, + verticalListSortingStrategy, +} from "@dnd-kit/sortable" +import { + IconChevronDown, + IconChevronLeft, + IconChevronRight, + IconChevronsLeft, + IconChevronsRight, + IconLayoutColumns, +} from "@tabler/icons-react" +import { + ColumnFiltersState, + flexRender, + getCoreRowModel, + getFacetedRowModel, + getFacetedUniqueValues, + getFilteredRowModel, + getSortedRowModel, + SortingState, + useReactTable, + VisibilityState, +} from "@tanstack/react-table" +import { Button } from "@/components/ui/button" +import { DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuTrigger } from "@/components/ui/dropdown-menu" +import { Label } from "@/components/ui/label" +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" +import { Tabs, TabsContent } from "@/components/ui/tabs" +import { schemaType } from "./schema" +import { getColumns, DraggableRow } from "./columns" +import { useRouter } from "next/navigation" +import { Home } from "lucide-react" +import { useDeleteBuildMutation } from "@/pages/builds/queries" + +export function UserTypesDataTableAdd({ + data, + totalCount, + currentPage, + pageSize, + onPageChange, + onPageSizeChange, + refetchTable, +}: { + data: schemaType[], + totalCount: number, + currentPage: number, + pageSize: number, + onPageChange: (page: number) => void, + onPageSizeChange: (size: number) => void, + refetchTable: () => void +}) { + + const router = useRouter(); + const [rowSelection, setRowSelection] = React.useState({}) + const [columnVisibility, setColumnVisibility] = React.useState({}) + const [columnFilters, setColumnFilters] = React.useState([]) + const [sorting, setSorting] = React.useState([]) + const sortableId = React.useId() + const sensors = useSensors(useSensor(MouseSensor, {}), useSensor(TouchSensor, {}), useSensor(KeyboardSensor, {})) + const dataIds = React.useMemo(() => data?.map(({ _id }) => _id) || [], [data]) + + const deleteMutation = useDeleteBuildMutation() + const deleteHandler = (id: string) => { deleteMutation.mutate({ uuid: id }); setTimeout(() => { refetchTable() }, 400) } + const columns = getColumns(router, deleteHandler); + const pagination = React.useMemo(() => ({ pageIndex: currentPage - 1, pageSize: pageSize, }), [currentPage, pageSize]) + const totalPages = Math.ceil(totalCount / pageSize) + + const table = useReactTable({ + data, + columns, + pageCount: totalPages, + state: { sorting, columnVisibility, rowSelection, columnFilters, pagination }, + manualPagination: true, + enableRowSelection: true, + getRowId: (row) => row._id.toString(), + onRowSelectionChange: setRowSelection, + onSortingChange: setSorting, + onColumnFiltersChange: setColumnFilters, + onColumnVisibilityChange: setColumnVisibility, + onPaginationChange: (updater) => { + const nextPagination = typeof updater === "function" ? updater(pagination) : updater; + onPageChange(nextPagination.pageIndex + 1); onPageSizeChange(nextPagination.pageSize); + }, + getCoreRowModel: getCoreRowModel(), + getFilteredRowModel: getFilteredRowModel(), + getSortedRowModel: getSortedRowModel(), + getFacetedRowModel: getFacetedRowModel(), + getFacetedUniqueValues: getFacetedUniqueValues(), + }) + + const handlePageSizeChange = (value: string) => { const newSize = Number(value); onPageSizeChange(newSize); onPageChange(1) } + + return ( + +
+ + +
+ + + + + + {table.getAllColumns().filter((column) => typeof column.accessorFn !== "undefined" && column.getCanHide()).map((column) => { + return ( + column.toggleVisibility(!!value)} > + {column.id} + + ) + })} + + + +
+
+ +
+ + + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + return ( + + {header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())} + + ) + })} + + ))} + + + {table.getRowModel().rows?.length ? ( + {table.getRowModel().rows.map((row) => )} + ) : ( + No results. + )} + +
+
+
+
+
+ {table.getFilteredSelectedRowModel().rows.length} of{" "} + {table.getFilteredRowModel().rows.length} row(s) selected. +
+
+
+ + +
+
+ Page {currentPage} of {totalPages} +
+
+ Total Count: {totalCount} +
+
+ + + + + + + +
+
+
+
+ +
+
+ +
+
+ +
+
+
+ ) +} \ No newline at end of file diff --git a/frontend/pages/user-types/add/table/schema.tsx b/frontend/pages/user-types/add/table/schema.tsx new file mode 100644 index 0000000..299a862 --- /dev/null +++ b/frontend/pages/user-types/add/table/schema.tsx @@ -0,0 +1,16 @@ +import { z } from "zod"; + +export const schema = z.object({ + + _id: z.string(), + type: z.string(), + token: z.string(), + typeToken: z.string(), + description: z.string(), + isProperty: z.boolean(), + expiryStarts: z.string(), + expiryEnds: z.string(), + +}); + +export type schemaType = z.infer; \ No newline at end of file diff --git a/frontend/pages/user-types/add/types.ts b/frontend/pages/user-types/add/types.ts new file mode 100644 index 0000000..e4508a4 --- /dev/null +++ b/frontend/pages/user-types/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/user-types/list/columns.tsx b/frontend/pages/user-types/list/columns.tsx new file mode 100644 index 0000000..48746c3 --- /dev/null +++ b/frontend/pages/user-types/list/columns.tsx @@ -0,0 +1,101 @@ +"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, IconHandClick } from "@tabler/icons-react" +import { useIsMobile } from "@/hooks/use-mobile" +import { Separator } from "@/components/ui/separator" +import { ColumnDef, flexRender, Row } from "@tanstack/react-table" +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, TextSelect } 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: "type", + header: "Type", + cell: ({ getValue }) => getValue(), + }, + { + accessorKey: "token", + header: "Token", + cell: ({ getValue }) => getValue(), + }, + { + accessorKey: "typeToken", + header: "Type Token", + cell: ({ getValue }) => getValue(), + }, + { + accessorKey: "description", + header: "Description", + cell: ({ getValue }) => getValue(), + }, + { + accessorKey: "isProperty", + header: "Is Property", + cell: ({ getValue }) => getValue() ? (
Yes
) : (
No
), + }, + { + accessorKey: "expiryStarts", + header: "Expiry Starts", + cell: ({ getValue }) => dateToLocaleString(getValue() as string), + }, + { + accessorKey: "expiryEnds", + header: "Expiry Ends", + cell: ({ getValue }) => dateToLocaleString(getValue() as string), + }, + { + accessorKey: "createdAt", + header: "Created At", + cell: ({ getValue }) => dateToLocaleString(getValue() as string), + }, + { + accessorKey: "updateAt", + header: "Updated At", + cell: ({ 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/user-types/list/data-table.tsx b/frontend/pages/user-types/list/data-table.tsx new file mode 100644 index 0000000..25a4025 --- /dev/null +++ b/frontend/pages/user-types/list/data-table.tsx @@ -0,0 +1,316 @@ +"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 { + IconBorderLeftPlus, + IconBuildingBank, + IconBuildingBridge, + IconBuildingChurch, + 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 { useDeleteBuildMutation } from "@/pages/builds/queries" + +export function UserTypesDataTable({ + 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 routeSelections = [ + { + url: 'build-parts', + name: 'Build Parts', + icon: + }, + { + url: 'build-areas', + name: 'Build Areas', + icon: + }, + { + url: 'build-sites', + name: 'Build Sites', + icon: + }, + ] + + const [activeRoute, setActiveRoute] = React.useState(routeSelections[0].url); + const [rowSelection, setRowSelection] = React.useState({}) + const [columnVisibility, setColumnVisibility] = React.useState({}) + const [columnFilters, setColumnFilters] = React.useState([]) + const [sorting, setSorting] = React.useState([]) + const sortableId = React.useId() + const sensors = useSensors(useSensor(MouseSensor, {}), useSensor(TouchSensor, {}), useSensor(KeyboardSensor, {})) + const dataIds = React.useMemo(() => data?.map(({ _id }) => _id) || [], [data]) + + const deleteMutation = useDeleteBuildMutation() + const deleteHandler = (id: string) => { deleteMutation.mutate({ uuid: id }); setTimeout(() => { refetchTable() }, 400) } + const columns = getColumns(router, activeRoute, 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} + + ) + })} + + + + + + + + {routeSelections.map((column) => { + return ( + setActiveRoute(column.url)} > +
+ {column.icon}{column.name} +
+
+ ) + })} +
+
+ +
+
+ +
+ + + + {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/user-types/list/schema.tsx b/frontend/pages/user-types/list/schema.tsx new file mode 100644 index 0000000..62d7cbc --- /dev/null +++ b/frontend/pages/user-types/list/schema.tsx @@ -0,0 +1,16 @@ +import { z } from "zod"; + +export const schema = z.object({ + + _id: z.string(), + type: z.string(), + token: z.string(), + typeToken: z.string(), + description: z.string(), + isProperty: z.boolean(), + expiryStarts: z.string(), + expiryEnds: z.string(), + +}); + +export type schemaType = z.infer; diff --git a/frontend/pages/user-types/page.tsx b/frontend/pages/user-types/page.tsx new file mode 100644 index 0000000..de82242 --- /dev/null +++ b/frontend/pages/user-types/page.tsx @@ -0,0 +1,23 @@ +'use client'; +import { useGraphQlUserTypesList } from './queries'; +import { useState } from 'react'; +import { UserTypesDataTable } from './list/data-table'; + +const PageUserTypes = () => { + + 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 } = useGraphQlUserTypesList({ limit, skip: (page - 1) * limit, sort, filters }); + + const handlePageChange = (newPage: number) => { setPage(newPage) }; + const handlePageSizeChange = (newSize: number) => { setLimit(newSize); setPage(1) }; + if (isLoading) { return
Loading...
} + if (error) { return
Error loading users
} + return ; + +}; + +export { PageUserTypes }; diff --git a/frontend/pages/user-types/queries.tsx b/frontend/pages/user-types/queries.tsx new file mode 100644 index 0000000..85f233c --- /dev/null +++ b/frontend/pages/user-types/queries.tsx @@ -0,0 +1,36 @@ +'use client' +import { useQuery, useMutation } from '@tanstack/react-query' +import { ListArguments } from '@/types/listRequest' + +const fetchGraphQlUserTypesList = async (params: ListArguments): Promise => { + console.log('Fetching test data from local API'); + const { limit, skip, sort, filters } = params; + try { + const res = await fetch('/api/user-types/list', { method: 'POST', cache: 'no-store', credentials: "include", body: JSON.stringify({ limit, skip, sort, filters }) }); + if (!res.ok) { const errorText = await res.text(); console.error('Test data API error:', errorText); throw new Error(`API error: ${res.status} ${res.statusText}`) } + const data = await res.json(); + return { data: data.data, totalCount: data.totalCount } + } catch (error) { console.error('Error fetching test data:', error); throw error } +}; + +const fetchGraphQlDeleteUserTypes = async (uuid: string): Promise => { + console.log('Fetching test data from local API'); + try { + const res = await fetch(`/api/user-types/delete?uuid=${uuid}`, { method: 'GET', cache: 'no-store', credentials: "include" }); + if (!res.ok) { const errorText = await res.text(); console.error('Test data API error:', errorText); throw new Error(`API error: ${res.status} ${res.statusText}`) } + const data = await res.json(); + return data + } catch (error) { console.error('Error fetching test data:', error); throw error } +}; + +export function useGraphQlUserTypesList(params: ListArguments) { + return useQuery({ queryKey: ['graphql-user-types-list', params], queryFn: () => fetchGraphQlUserTypesList(params) }) +} + +export function useDeleteUserTypeMutation() { + return useMutation({ + mutationFn: ({ uuid }: { uuid: string }) => fetchGraphQlDeleteUserTypes(uuid), + onSuccess: () => { console.log("User type deleted successfully") }, + onError: (error) => { console.error("Delete user type failed:", error) }, + }) +} diff --git a/frontend/pages/user-types/types.ts b/frontend/pages/user-types/types.ts new file mode 100644 index 0000000..985ce88 --- /dev/null +++ b/frontend/pages/user-types/types.ts @@ -0,0 +1,44 @@ +interface Build { + buildType: string; + collectionToken: string; + info: BuildInfo; + site?: string; + address?: string; + areas?: string[]; + ibans?: BuildIban[]; + responsibles?: BuildResponsible[]; +} + +interface BuildInfo { + govAddressCode: string; + buildName: string; + buildNo: string; + maxFloor: number; + undergroundFloor: number; + buildDate: Date; + decisionPeriodDate: Date; + taxNo: string; + liftCount: number; + heatingSystem: boolean; + coolingSystem: boolean; + hotWaterSystem: boolean; + blockServiceManCount: number; + securityServiceManCount: number; + garageCount: number; + managementRoomId: number; +} + +interface BuildIban { + iban: string; + startDate: Date; + stopDate: Date; + bankCode: string; + xcomment: string; +} + +interface BuildResponsible { + company: string; + person: string; +} + +export type { Build, BuildInfo, BuildIban, BuildResponsible } diff --git a/frontend/pages/user-types/update/form.tsx b/frontend/pages/user-types/update/form.tsx new file mode 100644 index 0000000..4af71f2 --- /dev/null +++ b/frontend/pages/user-types/update/form.tsx @@ -0,0 +1,311 @@ +"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 { BuildUpdate, buildUpdateSchema } from "@/pages/builds/update/schema" +import { useUpdateBuildMutation } from "@/pages/builds/update/queries" +import { Checkbox } from "@/components/ui/checkbox" + +const UserTypesUpdateForm = ({ refetchTable, initData, selectedUuid }: { refetchTable: () => void, initData: BuildUpdate, selectedUuid: string }) => { + + const form = useForm({ resolver: zodResolver(buildUpdateSchema), defaultValues: { ...initData } }) + + const { handleSubmit } = form + + const mutation = useUpdateBuildMutation(); + + function onSubmit(values: BuildUpdate) { mutation.mutate({ data: values as any || initData, uuid: selectedUuid }); setTimeout(() => refetchTable(), 400) } + + return ( +
+ + + {/* ROW 1 */} +
+ ( + + Token + + + + + + )} + /> + + ( + + Collection Token + + + + + + )} + /> +
+ +
+ ( + + Gov Address Code + + + + + + )} + /> + ( + + Build Name + + + + + + )} + /> + ( + + Build No + + + + + + )} + /> +
+ + +
+ ( + + Max Floor + + + + + + )} + /> + ( + + Underground Floor + + + + + + )} + /> +
+ +
+ ( + + Tax No + + + + + + )} + /> + ( + + Lift Count + + + + + + )} + /> +
+ +
+ ( + + + + +
+ + Heating System + +
+
+ )} + /> + ( + + + + +
+ + Cooling System + +
+
+ )} + /> + ( + + + + +
+ + Hot Water System + +
+
+ )} + /> +
+ + +
+ ( + + Build Date + + + + + + )} + /> + ( + + Decision Period Date + + + + + + )} + /> +
+ +
+ ( + + Block Service Man Count + + + + + + )} + /> + ( + + Security Service Man Count + + + + + + )} + /> + ( + + Total Garage Count In Numbers + + + + + + )} + /> + ( + + Management Room ID Assign + + + + + + )} + /> +
+ + + + + ); + +} + +export { UserTypesUpdateForm } diff --git a/frontend/pages/user-types/update/page.tsx b/frontend/pages/user-types/update/page.tsx new file mode 100644 index 0000000..c0e14eb --- /dev/null +++ b/frontend/pages/user-types/update/page.tsx @@ -0,0 +1,36 @@ +'use client'; +import { useState } from 'react'; +import { Button } from '@/components/ui/button'; +import { useSearchParams, useRouter } from 'next/navigation' +import { UsersTypeDataTableUpdate } from './table/data-table'; +import { useGraphQlUserTypesList } from '../queries'; +import { UserTypesUpdateForm } from './form'; + +const PageUpdateUserTypes = () => { + + 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 backToUserTypes = <>
UUID not found in search params
+ + if (!uuid) { return backToUserTypes } + const { data, isLoading, error, refetch } = useGraphQlUserTypesList({ limit, skip: (page - 1) * limit, sort, filters: { ...filters, _id: uuid } }); + const initData = data?.data?.[0] || null; + if (!initData) { return backToUserTypes } + + return ( + <> + + + + ) +} + +export { PageUpdateUserTypes }; diff --git a/frontend/pages/user-types/update/queries.tsx b/frontend/pages/user-types/update/queries.tsx new file mode 100644 index 0000000..6fd1511 --- /dev/null +++ b/frontend/pages/user-types/update/queries.tsx @@ -0,0 +1,26 @@ +'use client' +import { useMutation } from '@tanstack/react-query' +import { UpdateBuildIbansUpdate } from './types'; +import { toISOIfNotZ } from '@/lib/utils'; + +const fetchGraphQlBuildUpdate = async (record: UpdateBuildIbansUpdate, uuid: string): Promise<{ data: UpdateBuildIbansUpdate | null; status: number }> => { + console.log('Fetching test data from local API'); + record.expiryStarts = record.expiryStarts ? toISOIfNotZ(record.expiryStarts) : undefined; + record.expiryEnds = record.expiryEnds ? toISOIfNotZ(record.expiryEnds) : undefined; + record.startDate = toISOIfNotZ(record.startDate); + record.stopDate = toISOIfNotZ(record.stopDate); + try { + const res = await fetch(`/api/build/update?uuid=${uuid || ''}`, { method: 'POST', cache: 'no-store', credentials: "include", body: JSON.stringify(record) }); + if (!res.ok) { const errorText = await res.text(); console.error('Test data API error:', errorText); throw new Error(`API error: ${res.status} ${res.statusText}`) } + const data = await res.json(); + return { data: data.data, status: res.status } + } catch (error) { console.error('Error fetching test data:', error); throw error } +}; + +export function useUpdateBuildMutation() { + return useMutation({ + mutationFn: ({ data, uuid }: { data: UpdateBuildIbansUpdate, uuid: string }) => fetchGraphQlBuildUpdate(data, uuid), + onSuccess: () => { console.log("Build updated successfully") }, + onError: (error) => { console.error("Update Build failed:", error) }, + }) +} diff --git a/frontend/pages/user-types/update/schema.ts b/frontend/pages/user-types/update/schema.ts new file mode 100644 index 0000000..3bf0f36 --- /dev/null +++ b/frontend/pages/user-types/update/schema.ts @@ -0,0 +1,30 @@ +import { z } from "zod" + +export const buildUpdateSchema = z.object({ + buildType: z.object({ + token: z.string(), + typeToken: z.string(), + type: z.string(), + }), + collectionToken: z.string(), + info: z.object({ + govAddressCode: z.string(), + buildName: z.string(), + buildNo: z.string(), + maxFloor: z.number(), + undergroundFloor: z.number(), + buildDate: z.string(), + decisionPeriodDate: z.string(), + taxNo: z.string(), + liftCount: z.number(), + heatingSystem: z.boolean(), + coolingSystem: z.boolean(), + hotWaterSystem: z.boolean(), + blockServiceManCount: z.number(), + securityServiceManCount: z.number(), + garageCount: z.number(), + managementRoomId: z.number(), + }), +}); + +export type BuildUpdate = z.infer; diff --git a/frontend/pages/user-types/update/table/columns.tsx b/frontend/pages/user-types/update/table/columns.tsx new file mode 100644 index 0000000..941a73f --- /dev/null +++ b/frontend/pages/user-types/update/table/columns.tsx @@ -0,0 +1,100 @@ +"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: "type", + header: "Type", + cell: ({ getValue }) => getValue(), + }, + { + accessorKey: "token", + header: "Token", + cell: ({ getValue }) => getValue(), + }, + { + accessorKey: "typeToken", + header: "Type Token", + cell: ({ getValue }) => getValue(), + }, + { + accessorKey: "description", + header: "Description", + cell: ({ getValue }) => getValue(), + }, + { + accessorKey: "isProperty", + header: "Is Property", + cell: ({ getValue }) => getValue() ? (
Yes
) : (
No
), + }, + { + accessorKey: "expiryStarts", + header: "Expiry Starts", + cell: ({ getValue }) => dateToLocaleString(getValue() as string), + }, + { + accessorKey: "expiryEnds", + header: "Expiry Ends", + cell: ({ getValue }) => dateToLocaleString(getValue() as string), + }, + { + accessorKey: "createdAt", + header: "Created At", + cell: ({ getValue }) => dateToLocaleString(getValue() as string), + }, + { + accessorKey: "updateAt", + header: "Updated At", + cell: ({ 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/user-types/update/table/data-table.tsx b/frontend/pages/user-types/update/table/data-table.tsx new file mode 100644 index 0000000..5fda45d --- /dev/null +++ b/frontend/pages/user-types/update/table/data-table.tsx @@ -0,0 +1,279 @@ +"use client" + +import * as React from "react" +import { + closestCenter, + DndContext, + KeyboardSensor, + MouseSensor, + TouchSensor, + useSensor, + useSensors, + type UniqueIdentifier, +} from "@dnd-kit/core" +import { restrictToVerticalAxis } from "@dnd-kit/modifiers" +import { + SortableContext, + verticalListSortingStrategy, +} from "@dnd-kit/sortable" +import { + IconChevronDown, + IconChevronLeft, + IconChevronRight, + IconChevronsLeft, + IconChevronsRight, + IconLayoutColumns, +} from "@tabler/icons-react" +import { + ColumnFiltersState, + flexRender, + getCoreRowModel, + getFacetedRowModel, + getFacetedUniqueValues, + getFilteredRowModel, + getSortedRowModel, + SortingState, + useReactTable, + VisibilityState, +} from "@tanstack/react-table" + +import { Button } from "@/components/ui/button" +import { + DropdownMenu, + DropdownMenuCheckboxItem, + DropdownMenuContent, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu" +import { Label } from "@/components/ui/label" +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select" +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table" +import { + Tabs, + TabsContent, + TabsList, + TabsTrigger, +} from "@/components/ui/tabs" +import { schemaType } from "./schema" +import { getColumns, DraggableRow } from "./columns" +import { useRouter } from "next/navigation" +import { Home } from "lucide-react" +import { useDeleteBuildMutation } from "@/pages/builds/queries" + +export function UsersTypeDataTableUpdate({ + data, + totalCount, + currentPage, + pageSize, + onPageChange, + onPageSizeChange, + refetchTable, +}: { + data: schemaType[], + totalCount: number, + currentPage: number, + pageSize: number, + onPageChange: (page: number) => void, + onPageSizeChange: (size: number) => void, + refetchTable: () => void +}) { + + const router = useRouter(); + const [rowSelection, setRowSelection] = React.useState({}) + const [columnVisibility, setColumnVisibility] = React.useState({}) + const [columnFilters, setColumnFilters] = React.useState([]) + const [sorting, setSorting] = React.useState([]) + const sortableId = React.useId() + const sensors = useSensors(useSensor(MouseSensor, {}), useSensor(TouchSensor, {}), useSensor(KeyboardSensor, {})) + const dataIds = React.useMemo(() => data?.map(({ _id }) => _id) || [], [data]) + + const deleteMutation = useDeleteBuildMutation() + const deleteHandler = (id: string) => { deleteMutation.mutate({ uuid: id }); setTimeout(() => { refetchTable() }, 200) } + const columns = getColumns(router, deleteHandler); + const pagination = React.useMemo(() => ({ pageIndex: currentPage - 1, pageSize: pageSize, }), [currentPage, pageSize]) + const totalPages = Math.ceil(totalCount / pageSize) + + const table = useReactTable({ + data, + columns, + pageCount: totalPages, + state: { sorting, columnVisibility, rowSelection, columnFilters, pagination }, + manualPagination: true, + enableRowSelection: true, + getRowId: (row) => row._id.toString(), + onRowSelectionChange: setRowSelection, + onSortingChange: setSorting, + onColumnFiltersChange: setColumnFilters, + onColumnVisibilityChange: setColumnVisibility, + onPaginationChange: (updater) => { + const nextPagination = typeof updater === "function" ? updater(pagination) : updater; + onPageChange(nextPagination.pageIndex + 1); onPageSizeChange(nextPagination.pageSize); + }, + getCoreRowModel: getCoreRowModel(), + getFilteredRowModel: getFilteredRowModel(), + getSortedRowModel: getSortedRowModel(), + getFacetedRowModel: getFacetedRowModel(), + getFacetedUniqueValues: getFacetedUniqueValues(), + }) + + const handlePageSizeChange = (value: string) => { const newSize = Number(value); onPageSizeChange(newSize); onPageChange(1) } + + return ( + +
+ + +
+ + + + + + {table.getAllColumns().filter((column) => typeof column.accessorFn !== "undefined" && column.getCanHide()).map((column) => { + return ( + column.toggleVisibility(!!value)} > + {column.id} + + ) + })} + + + +
+
+ +
+ + + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + return ( + + {header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())} + + ) + })} + + ))} + + + {table.getRowModel().rows?.length ? ( + {table.getRowModel().rows.map((row) => )} + ) : ( + No results. + )} + +
+
+
+
+
+ {table.getFilteredSelectedRowModel().rows.length} of{" "} + {table.getFilteredRowModel().rows.length} row(s) selected. +
+
+
+ + +
+
+ Page {currentPage} of {totalPages} +
+
+ Total Count: {totalCount} +
+
+ + + + + + + +
+
+
+
+ +
+
+ +
+
+ +
+
+
+ ) +} \ No newline at end of file diff --git a/frontend/pages/user-types/update/table/schema.tsx b/frontend/pages/user-types/update/table/schema.tsx new file mode 100644 index 0000000..62d7cbc --- /dev/null +++ b/frontend/pages/user-types/update/table/schema.tsx @@ -0,0 +1,16 @@ +import { z } from "zod"; + +export const schema = z.object({ + + _id: z.string(), + type: z.string(), + token: z.string(), + typeToken: z.string(), + description: z.string(), + isProperty: z.boolean(), + expiryStarts: z.string(), + expiryEnds: z.string(), + +}); + +export type schemaType = z.infer; diff --git a/frontend/pages/user-types/update/types.ts b/frontend/pages/user-types/update/types.ts new file mode 100644 index 0000000..1a88475 --- /dev/null +++ b/frontend/pages/user-types/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