updated build

This commit is contained in:
Berkay 2025-11-29 16:57:17 +03:00
parent d22fc017df
commit 0394d42d02
28 changed files with 1502 additions and 551 deletions

View File

@ -1,5 +1,5 @@
import { ExpiryBaseInput } from "@/models/base.model"; import { ExpiryBaseInput } from "@/models/base.model";
import { InputType, Field, ID } from "@nestjs/graphql"; import { InputType, Field } from "@nestjs/graphql";
@InputType() @InputType()
export class CreateBuildInfoInput { export class CreateBuildInfoInput {
@ -60,9 +60,6 @@ export class CreateBuildInput extends ExpiryBaseInput {
@Field() @Field()
buildType: string; buildType: string;
@Field()
collectionToken: string;
@Field(() => CreateBuildInfoInput) @Field(() => CreateBuildInfoInput)
info: CreateBuildInfoInput; info: CreateBuildInfoInput;

View File

@ -39,6 +39,7 @@ export class ListPartialIbanResponse {
@ObjectType() @ObjectType()
class ResponsibleCompanyPerson { class ResponsibleCompanyPerson {
@Field(() => Company, { nullable: true }) @Field(() => Company, { nullable: true })
company?: Company; company?: Company;

View File

@ -102,9 +102,6 @@ export class UpdateBuildInput extends ExpiryBaseInput {
@Field({ nullable: true }) @Field({ nullable: true })
buildType?: string; buildType?: string;
@Field({ nullable: true })
collectionToken?: string;
@Field(() => UpdateBuildInfoInput, { nullable: true }) @Field(() => UpdateBuildInfoInput, { nullable: true })
info?: UpdateBuildInfoInput; info?: UpdateBuildInfoInput;

View File

@ -4,30 +4,21 @@ import { Document, Types } from 'mongoose';
import { Base } from '@/models/base.model'; import { Base } from '@/models/base.model';
import { Person } from '@/models/person.model'; import { Person } from '@/models/person.model';
@ObjectType()
export class CollectionTokenItem {
@Field()
@Prop({ required: true })
prefix: string;
@Field()
@Prop({ required: true })
token: string;
}
@ObjectType() @ObjectType()
export class CollectionToken { export class CollectionToken {
@Field(() => [CollectionTokenItem])
@Prop({ type: [CollectionTokenItem], default: [] })
tokens: CollectionTokenItem[];
@Field() @Field()
@Prop({ required: true, default: '' }) defaultSelection: string;
default: string;
@Field(() => [String])
selectedBuildIDS: string[];
@Field(() => [String])
selectedCompanyIDS: string[];
} }
@ObjectType() @ObjectType()
@Schema({ timestamps: true }) @Schema({ timestamps: true })
export class User extends Base { export class User extends Base {
@ -68,7 +59,7 @@ export class User extends Base {
phone: string; phone: string;
@Field(() => CollectionToken) @Field(() => CollectionToken)
@Prop({ type: CollectionToken, default: () => ({ tokens: [], default: '' }) }) @Prop({ type: CollectionToken, default: () => ({ defaultSelection: '', selectedBuildIDS: [], selectedCompanyIDS: [] }) })
collectionTokens: CollectionToken; collectionTokens: CollectionToken;
@Field(() => ID) @Field(() => ID)

View File

@ -1,21 +1,17 @@
import { InputType, Field, ID } from '@nestjs/graphql'; import { InputType, Field, ID } from '@nestjs/graphql';
@InputType() @InputType()
export class CollectionTokenItemInput { export class CreateCollectionTokenInput {
@Field()
prefix: string;
@Field() @Field()
token: string; defaultSelection: string;
}
@InputType() @Field(() => [String])
export class CollectionTokenInput { selectedBuildIDS: string[];
@Field(() => [CollectionTokenItemInput])
tokens: CollectionTokenItemInput[]; @Field(() => [String])
selectedCompanyIDS: string[];
@Field()
default: string;
} }
@InputType() @InputType()
@ -39,8 +35,8 @@ export class CreateUserInput {
@Field() @Field()
phone: string; phone: string;
@Field(() => CollectionTokenInput) @Field(() => CreateCollectionTokenInput)
collectionTokens: CollectionTokenInput; collectionTokens: CreateCollectionTokenInput;
@Field(() => ID) @Field(() => ID)
person: string; person: string;
@ -59,4 +55,5 @@ export class CreateUserInput {
@Field(() => Boolean, { nullable: true }) @Field(() => Boolean, { nullable: true })
isNotificationSend?: boolean; isNotificationSend?: boolean;
} }

View File

@ -1,23 +1,21 @@
import { InputType, Field, ID } from '@nestjs/graphql'; import { InputType, Field, ID } from '@nestjs/graphql';
@InputType()
export class UpdateCollectionTokenItemInput {
@Field({ nullable: true })
prefix?: string;
@Field({ nullable: true })
token?: string;
}
@InputType() @InputType()
export class UpdateCollectionTokenInput { export class UpdateCollectionTokenInput {
@Field(() => [UpdateCollectionTokenItemInput], { nullable: true })
tokens?: UpdateCollectionTokenItemInput[];
@Field({ nullable: true }) @Field()
default?: string; defaultSelection: string;
@Field(() => [String])
selectedBuildIDS: string[];
@Field(() => [String])
selectedCompanyIDS: string[];
} }
@InputType() @InputType()
export class UpdateUserInput { export class UpdateUserInput {

View File

@ -1,9 +1,57 @@
# ToDo's # ToDo's
Build: Addition of Living Space Test Page
BuildPart:
Build Living Space: ## RoadMap of Living Space Addition
People:
User: ### 1 User Types
Company:
ADD UPDATE yapan page: -- Create User Types
Type: Employee
Token: L9wBdwV9OlxsLAghlo3vj2pAQpqFDBw7cm9znQ
Type Token: L9wBdwV9OlxsLAgh
Description: Application Manager Employee
### 2 People
-- Create People
First Name: Berkay
Surname: Karatay
Middle Name:
Sex Code: M
Person Ref: SomeRef1
Person Tag: SomeTag1
Father Name: Mehmet
Mother Name: Selma
Country Code: TR
National Identity Id: 12345678901
Birth Place: Ankara
Birth Date: 1999-01-01
Tax No: 123456789
Birthname:
Expiry Starts: 01/01/1990
Expiry Ends: 01/01/2099
### 3 Users
-- Create Users
password: string
rePassword:string
tag: berkai
email: @url:karatay.berkay@gmail.com
phone: +905555555555
defaultSelection: "SomeBuild|CompanyID"
selectedBuildingIDS | selectedCompanyIDS: ["SomeBuild1|CompanyID1", "SomeBuild2|CompanyID2", "SomeBuild3|CompanyID3"]
expiryStarts: "01/01/2025"
expiryEnds: "01/01/2026"
isConfirmed: true
isNotificationSend: true
### 4 Building
-- Create Building
asd

View File

@ -4,11 +4,7 @@ import { type Icon } from "@tabler/icons-react"
import { SidebarGroup, SidebarGroupContent, SidebarMenu, SidebarMenuButton, SidebarMenuItem } from "@/components/ui/sidebar" import { SidebarGroup, SidebarGroupContent, SidebarMenu, SidebarMenuButton, SidebarMenuItem } from "@/components/ui/sidebar"
import { usePathname } from 'next/navigation' import { usePathname } from 'next/navigation'
interface NavMainProps { interface NavMainProps { title: string, url: string, icon?: Icon }
title: string
url: string
icon?: Icon
}
export function NavMain({ items }: { items: NavMainProps[] }) { export function NavMain({ items }: { items: NavMainProps[] }) {
const pathname = usePathname()?.split("/")[1] const pathname = usePathname()?.split("/")[1]
@ -17,7 +13,7 @@ export function NavMain({ items }: { items: NavMainProps[] }) {
<SidebarMenuButton tooltip={item.title}>{item.icon && <item.icon />}<span>{item.title}</span></SidebarMenuButton> <SidebarMenuButton tooltip={item.title}>{item.icon && <item.icon />}<span>{item.title}</span></SidebarMenuButton>
</SidebarMenuItem></Link> </SidebarMenuItem></Link>
const linkRenderDisabled = (item: NavMainProps) => const linkRenderDisabled = (item: NavMainProps) =>
<SidebarMenuItem key={`${item.title}-${item.url}`} className="opacity-50 bg-gray-300 border-gray-300 border-2 rounded-2xl"> <SidebarMenuItem key={`${item.title}-${item.url}`} className="opacity-50 bg-gray-300 border-gray-300 border-2 rounded-2xl ">
<SidebarMenuButton disabled tooltip={item.title}>{item.icon && <item.icon />}<span>{item.title}</span></SidebarMenuButton> <SidebarMenuButton disabled tooltip={item.title}>{item.icon && <item.icon />}<span>{item.title}</span></SidebarMenuButton>
</SidebarMenuItem> </SidebarMenuItem>
return ( return (

View File

@ -28,15 +28,20 @@ const data = {
}, },
navMain: [ navMain: [
{ {
title: "Users", title: "User Types",
url: "/users", url: "/user-types",
icon: IconUsers icon: IconFileCertificate
}, },
{ {
title: "People", title: "People",
url: "/people", url: "/people",
icon: IconListDetails icon: IconListDetails
}, },
{
title: "Users",
url: "/users",
icon: IconUsers
},
{ {
title: "Build", title: "Build",
url: "/builds", url: "/builds",
@ -77,11 +82,6 @@ const data = {
url: "/living-spaces", url: "/living-spaces",
icon: IconHome2 icon: IconHome2
}, },
{
title: "User Types",
url: "/user-types",
icon: IconFileCertificate
}
], ],
navClouds: [ navClouds: [
// { // {
@ -173,10 +173,7 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
<SidebarHeader> <SidebarHeader>
<SidebarMenu> <SidebarMenu>
<SidebarMenuItem> <SidebarMenuItem>
<SidebarMenuButton <SidebarMenuButton asChild className="data-[slot=sidebar-menu-button]:p-1.5!">
asChild
className="data-[slot=sidebar-menu-button]:p-1.5!"
>
<a href="#"> <a href="#">
<IconInnerShadowTop className="size-5!" /> <IconInnerShadowTop className="size-5!" />
<span className="text-base font-semibold">Acme Inc.</span> <span className="text-base font-semibold">Acme Inc.</span>

View File

@ -1,5 +1,6 @@
"use client" "use client"
import { useFieldArray, useForm } from "react-hook-form" import { useState, useEffect } from "react"
import { useForm } from "react-hook-form"
import { zodResolver } from "@hookform/resolvers/zod" import { zodResolver } from "@hookform/resolvers/zod"
import { userAddSchema, type UserAdd } from "./schema" import { userAddSchema, type UserAdd } from "./schema"
import { Form, FormField, FormItem, FormLabel, FormControl, FormMessage } from "@/components/ui/form" import { Form, FormField, FormItem, FormLabel, FormControl, FormMessage } from "@/components/ui/form"
@ -9,6 +10,7 @@ import { Checkbox } from "@/components/ui/checkbox"
import { Separator } from "@/components/ui/separator" import { Separator } from "@/components/ui/separator"
import { useAddUserMutation } from "./queries" import { useAddUserMutation } from "./queries"
import { DateTimePicker } from "@/components/ui/date-time-picker" import { DateTimePicker } from "@/components/ui/date-time-picker"
import PageAddUserSelections from "../selections/addPage"
const UserForm = ({ refetchTable }: { refetchTable: () => void }) => { const UserForm = ({ refetchTable }: { refetchTable: () => void }) => {
const form = useForm<UserAdd>({ const form = useForm<UserAdd>({
@ -22,23 +24,30 @@ const UserForm = ({ refetchTable }: { refetchTable: () => void }) => {
rePassword: "", rePassword: "",
tag: "", tag: "",
email: "", email: "",
phone: "", phone: ""
collectionTokens: {
default: "",
tokens: [],
},
}, },
}) })
const { control, handleSubmit } = form const [defaultSelection, setDefaultSelection] = useState<string>("")
const [selectedBuildIDS, setSelectedBuildIDS] = useState<string[]>([])
const [selectedCompanyIDS, setSelectedCompanyIDS] = useState<string[]>([])
const { fields, append, remove } = useFieldArray({ control, name: "collectionTokens.tokens" }) const appendBuildID = (id: string) => setSelectedBuildIDS((prev) => (id && !selectedBuildIDS.includes(id) ? [...prev, id] : prev))
const appendCompanyID = (id: string) => setSelectedCompanyIDS((prev) => (id && !selectedCompanyIDS.includes(id) ? [...prev, id] : prev))
const removeBuildID = (id: string) => setSelectedBuildIDS((prev) => prev.filter((item) => item !== id))
const removeCompanyID = (id: string) => setSelectedCompanyIDS((prev) => prev.filter((item) => item !== id))
const { handleSubmit } = form
const mutation = useAddUserMutation(); const mutation = useAddUserMutation();
function onSubmit(values: UserAdd) { mutation.mutate({ data: values as any, selectedBuildIDS, selectedCompanyIDS, defaultSelection, refetchTable }); }
function onSubmit(values: UserAdd) { mutation.mutate(values as any); setTimeout(() => refetchTable(), 400) }
return ( return (
<div>
<PageAddUserSelections
selectedCompanyIDS={selectedCompanyIDS} selectedBuildingIDS={selectedBuildIDS} appendCompanyID={appendCompanyID} appendBuildingID={appendBuildID}
removeCompanyID={removeCompanyID} removeBuildingID={removeBuildID} defaultSelection={defaultSelection} setDefaultSelection={setDefaultSelection}
/>
<Form {...form}> <Form {...form}>
<form onSubmit={handleSubmit(onSubmit)} className="space-y-6 p-4"> <form onSubmit={handleSubmit(onSubmit)} className="space-y-6 p-4">
{/* BASIC INFO */} {/* BASIC INFO */}
@ -56,7 +65,6 @@ const UserForm = ({ refetchTable }: { refetchTable: () => void }) => {
</FormItem> </FormItem>
)} )}
/> />
<FormField <FormField
control={form.control} control={form.control}
name="phone" name="phone"
@ -114,40 +122,6 @@ const UserForm = ({ refetchTable }: { refetchTable: () => void }) => {
</FormItem> </FormItem>
)} )}
/> />
</div>
{/* DATES */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<FormField
control={form.control}
name="expiryStarts"
render={({ field }) => (
<FormItem>
<FormLabel>Expiry Starts</FormLabel>
<FormControl>
<DateTimePicker {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="expiryEnds"
render={({ field }) => (
<FormItem>
<FormLabel>Expiry Ends</FormLabel>
<FormControl>
<DateTimePicker {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
{/* SWITCHES */} {/* SWITCHES */}
<div className="flex items-center gap-6"> <div className="flex items-center gap-6">
<FormField <FormField
@ -165,7 +139,6 @@ const UserForm = ({ refetchTable }: { refetchTable: () => void }) => {
</FormItem> </FormItem>
)} )}
/> />
<FormField <FormField
control={form.control} control={form.control}
name="isNotificationSend" name="isNotificationSend"
@ -182,55 +155,31 @@ const UserForm = ({ refetchTable }: { refetchTable: () => void }) => {
)} )}
/> />
</div> </div>
</div>
<Separator /> {/* DATES */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{/* COLLECTION TOKENS */}
<FormField <FormField
control={form.control} control={form.control}
name="collectionTokens.default" name="expiryStarts"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>Default Token</FormLabel> <FormLabel>Expiry Starts</FormLabel>
<FormControl> <FormControl>
<Input placeholder="Default token..." {...field} /> <DateTimePicker {...field} />
</FormControl> </FormControl>
<FormMessage />
</FormItem> </FormItem>
)} )}
/> />
<div className="space-y-3">
<div className="flex justify-between items-center">
<FormLabel>Tokens</FormLabel>
<Button
type="button"
variant="outline"
size="sm"
onClick={() => append({ prefix: "", token: "" })}
>
+ Add Token
</Button>
</div>
{fields.length === 0 && (
<p className="text-sm text-muted-foreground">No tokens added.</p>
)}
{fields.map((fieldItem, i) => (
<div
key={fieldItem.id}
className="grid grid-cols-12 gap-2 items-end"
>
<div className="col-span-4">
<FormField <FormField
control={form.control} control={form.control}
name={`collectionTokens.tokens.${i}.prefix`} name="expiryEnds"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>Prefix</FormLabel> <FormLabel>Expiry Ends</FormLabel>
<FormControl> <FormControl>
<Input placeholder="prefix" {...field} /> <DateTimePicker {...field} />
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
@ -238,42 +187,11 @@ const UserForm = ({ refetchTable }: { refetchTable: () => void }) => {
/> />
</div> </div>
<div className="col-span-6">
<FormField
control={form.control}
name={`collectionTokens.tokens.${i}.token`}
render={({ field }) => (
<FormItem>
<FormLabel>Token</FormLabel>
<FormControl>
<Input placeholder="token" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<Button
type="button"
variant="destructive"
size="icon"
className="col-span-2 h-10"
onClick={() => remove(i)}
>
</Button>
</div>
))}
</div>
<Separator /> <Separator />
<Button type="submit" className="w-full">Create User</Button>
<Button type="submit" className="w-full">
Create User
</Button>
</form> </form>
</Form> </Form>
</div>
) )
} }

View File

@ -3,21 +3,29 @@ import { useMutation } from '@tanstack/react-query'
import { UserAdd } from './types' import { UserAdd } from './types'
import { toISOIfNotZ } from '@/lib/utils' import { toISOIfNotZ } from '@/lib/utils'
const fetchGraphQlUsersAdd = async (record: UserAdd): Promise<{ data: UserAdd | null; status: number }> => { const fetchGraphQlUsersAdd = async (
console.log('Fetching test data from local API'); record: UserAdd,
selectedBuildIDS: string[],
selectedCompanyIDS: string[],
defaultSelection: string,
refetchTable: () => void
): Promise<{ data: UserAdd | null; status: number }> => {
record.expiryStarts = toISOIfNotZ(record.expiryStarts); record.expiryStarts = toISOIfNotZ(record.expiryStarts);
record.expiryEnds = toISOIfNotZ(record.expiryEnds); record.expiryEnds = toISOIfNotZ(record.expiryEnds);
try { try {
const res = await fetch('/api/users/add', { method: 'POST', cache: 'no-store', credentials: "include", body: JSON.stringify(record) }); const res = await fetch('/api/users/add', { method: 'POST', cache: 'no-store', credentials: "include", body: JSON.stringify({ ...record, selectedBuildIDS, selectedCompanyIDS, defaultSelection }) });
if (!res.ok) { const errorText = await res.text(); console.error('Test data API error:', errorText); throw new Error(`API error: ${res.status} ${res.statusText}`) } 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(); const data = await res.json();
refetchTable();
return { data: data.data, status: res.status } return { data: data.data, status: res.status }
} catch (error) { console.error('Error fetching test data:', error); throw error } } catch (error) { console.error('Error fetching test data:', error); throw error }
}; };
export function useAddUserMutation() { export function useAddUserMutation() {
return useMutation({ return useMutation({
mutationFn: (data: UserAdd) => fetchGraphQlUsersAdd(data), mutationFn: (
{ data, selectedBuildIDS, selectedCompanyIDS, defaultSelection, refetchTable }: { data: UserAdd, selectedBuildIDS: string[], selectedCompanyIDS: string[], defaultSelection: string, refetchTable: () => void }
) => fetchGraphQlUsersAdd(data, selectedBuildIDS, selectedCompanyIDS, defaultSelection, refetchTable),
onSuccess: () => { console.log("User created successfully") }, onSuccess: () => { console.log("User created successfully") },
onError: (error) => { console.error("Create user failed:", error) }, onError: (error) => { console.error("Create user failed:", error) },
}) })

View File

@ -1,14 +1,5 @@
import { z } from "zod" import { z } from "zod"
export const tokenSchema = z.object({
prefix: z.string().min(1, "Prefix is required"),
token: z.string().min(1, "Token is required"),
})
export const collectionTokensSchema = z.object({
default: z.string().optional(),
tokens: z.array(tokenSchema)
})
export const userAddSchema = z.object({ export const userAddSchema = z.object({
expiryStarts: z.string().optional(), expiryStarts: z.string().optional(),
@ -23,7 +14,6 @@ export const userAddSchema = z.object({
email: z.string().email(), email: z.string().email(),
phone: z.string().min(5), phone: z.string().min(5),
collectionTokens: collectionTokensSchema,
}) })
export type UserAdd = z.infer<typeof userAddSchema> export type UserAdd = z.infer<typeof userAddSchema>

View File

@ -17,7 +17,6 @@ interface UserAdd {
tag: string; tag: string;
email: string; email: string;
phone: string; phone: string;
collectionTokens: CollectionTokens;
} }

View File

@ -4,6 +4,7 @@ import { useState } from 'react';
import { UserDataTable } from '@/pages/users/table/data-table'; import { UserDataTable } from '@/pages/users/table/data-table';
const PageUsers = () => { const PageUsers = () => {
const [page, setPage] = useState(1); const [page, setPage] = useState(1);
const [limit, setLimit] = useState(10); const [limit, setLimit] = useState(10);
const [sort, setSort] = useState({ createdAt: 'desc' }); const [sort, setSort] = useState({ createdAt: 'desc' });
@ -13,6 +14,7 @@ const PageUsers = () => {
const handlePageChange = (newPage: number) => { setPage(newPage) }; const handlePageChange = (newPage: number) => { setPage(newPage) };
const handlePageSizeChange = (newSize: number) => { setLimit(newSize); setPage(1) }; const handlePageSizeChange = (newSize: number) => { setLimit(newSize); setPage(1) };
if (isLoading) { return <div className="flex items-center justify-center p-8">Loading...</div> } if (isLoading) { return <div className="flex items-center justify-center p-8">Loading...</div> }
if (error) { return <div className="flex items-center justify-center p-8 text-red-500">Error loading users</div> } if (error) { return <div className="flex items-center justify-center p-8 text-red-500">Error loading users</div> }

View File

@ -0,0 +1,42 @@
'use client';
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import PageUsersCompanyTableSection from "./companies/page";
import PageUsersBuildingTableSection from "./builds/page";
const PageAddUserSelections = ({
selectedCompanyIDS,
selectedBuildingIDS,
defaultSelection,
appendCompanyID,
appendBuildingID,
removeCompanyID,
removeBuildingID,
setDefaultSelection
}: {
selectedCompanyIDS: string[];
selectedBuildingIDS: string[];
defaultSelection: string;
appendCompanyID: (id: string) => void;
appendBuildingID: (id: string) => void;
removeCompanyID: (id: string) => void;
removeBuildingID: (id: string) => void;
setDefaultSelection: (id: string) => void;
}) => {
const tabsClassName = "border border-gray-300 rounded-sm h-10"
return (
<div className="flex flex-col gap-4">
<Tabs defaultValue="builds" className="w-full">
<TabsList className="grid w-full grid-flow-col auto-cols-fr gap-1.5 mx-5">
<TabsTrigger className={tabsClassName} value="builds">Append Builds</TabsTrigger>
<TabsTrigger className={tabsClassName} value="company">Append Company</TabsTrigger>
</TabsList>
<div className="mt-6">
<TabsContent value="builds"><PageUsersBuildingTableSection selectedBuildIDS={selectedBuildingIDS} appendBuildID={appendBuildingID} removeBuildID={removeBuildingID} defaultSelection={defaultSelection} setDefaultSelection={setDefaultSelection} /></TabsContent>
<TabsContent value="company"><PageUsersCompanyTableSection selectedCompanyIDS={selectedCompanyIDS} appendCompanyID={appendCompanyID} removeCompanyID={removeCompanyID} defaultSelection={defaultSelection} setDefaultSelection={setDefaultSelection} /></TabsContent>
</div>
</Tabs>
</div>
);
};
export default PageAddUserSelections;

View File

@ -0,0 +1,146 @@
"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 { IconCircleMinus, IconHandClick } from "@tabler/icons-react"
export function DraggableRow({ row, selectedIDs }: { row: Row<z.infer<typeof schema>>; selectedIDs: string[] }) {
const { transform, transition, setNodeRef, isDragging } = useSortable({ id: row.original._id })
return (
<TableRow data-state={row.getIsSelected() && "selected"} data-dragging={isDragging} ref={setNodeRef}
className={`${selectedIDs.includes(row.original._id) ? "bg-blue-700/50 hover:bg-blue-700/50" : ""} relative z-0 data-[dragging=true]:z-10 data-[dragging=true]:opacity-80`}
style={{ transform: CSS.Transform.toString(transform), transition: transition }}
>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>{flexRender(cell.column.columnDef.cell, cell.getContext())}</TableCell>
))}
</TableRow>
)
}
function getColumns(appendBuildID: (id: string) => void, removeBuildID: (id: string) => void, selectedBuildIDS: string[], defaultSelection: string, setDefaultSelection: (id: string) => void): ColumnDef<schemaType>[] {
return [
{
accessorKey: "_id",
header: "ID",
},
{
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() ? (<div className="text-green-600 font-medium">Yes</div>) : (<div className="text-red-600 font-medium">No</div>),
},
{
accessorKey: "info.heatingSystem",
header: "Heating System",
cell: ({ getValue }) => getValue() ? (<div className="text-green-600 font-medium">Yes</div>) : (<div className="text-red-600 font-medium">No</div>),
},
{
accessorKey: "info.coolingSystem",
header: "Cooling System",
cell: ({ getValue }) => getValue() ? (<div className="text-green-600 font-medium">Yes</div>) : (<div className="text-red-600 font-medium">No</div>),
},
{
accessorKey: "info.hotWaterSystem",
header: "Hot Water System",
cell: ({ getValue }) => getValue() ? (<div className="text-green-600 font-medium">Yes</div>) : (<div className="text-red-600 font-medium">No</div>),
},
{
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 (
selectedBuildIDS.includes(row.original._id) ? (
<div>
<Button className="bg-blue-600 border-blue-600 text-white" variant="outline" size="sm" onClick={() => { appendBuildID(row.original._id) }}>
<IconHandClick />
</Button>
</div>
) :
<div>
<Button className="bg-blue-600 border-blue-600 text-white" variant="outline" size="sm" onClick={() => { removeBuildID(row.original._id) }}>
<IconCircleMinus />
</Button>
</div>
);
},
}
]
}
export { getColumns };

View File

@ -0,0 +1,276 @@
"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 UsersBuildDataTable({
data,
totalCount,
currentPage = 1,
pageSize = 10,
onPageChange,
onPageSizeChange,
refetchTable,
selectedBuildIDS,
appendBuildID,
removeBuildID,
additionButtons,
defaultSelection,
setDefaultSelection
}: {
data: schemaType[],
totalCount: number,
currentPage?: number,
pageSize?: number,
onPageChange: (page: number) => void,
onPageSizeChange: (size: number) => void,
refetchTable: () => void,
selectedBuildIDS: string[],
appendBuildID: (id: string) => void,
removeBuildID: (id: string) => void,
additionButtons: React.ReactNode,
defaultSelection: string,
setDefaultSelection: (id: string) => void
}) {
const router = useRouter();
const [rowSelection, setRowSelection] = React.useState({})
const [columnVisibility, setColumnVisibility] = React.useState<VisibilityState>({})
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>([])
const [sorting, setSorting] = React.useState<SortingState>([])
const sortableId = React.useId()
const sensors = useSensors(useSensor(MouseSensor, {}), useSensor(TouchSensor, {}), useSensor(KeyboardSensor, {}))
const dataIds = React.useMemo<UniqueIdentifier[]>(() => data?.map(({ _id }) => _id) || [], [data])
const columns = getColumns(appendBuildID, removeBuildID, selectedBuildIDS, defaultSelection, setDefaultSelection);
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 (
<Tabs defaultValue="outline" className="w-full flex-col justify-start gap-6">
<div className="flex items-center justify-between px-4 lg:px-6">
<Label htmlFor="view-selector" className="sr-only">
View
</Label>
<Select defaultValue="outline">
<SelectTrigger className="flex w-fit @4xl/main:hidden" size="sm" id="view-selector">
<SelectValue placeholder="Select a view" />
</SelectTrigger>
<SelectContent>
<SelectItem value="outline">Outline</SelectItem>
<SelectItem value="past-performance">Past Performance</SelectItem>
<SelectItem value="key-personnel">Key Personnel</SelectItem>
<SelectItem value="focus-documents">Focus Documents</SelectItem>
</SelectContent>
</Select>
<div className="flex items-center gap-2">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="sm">
<IconLayoutColumns />
<span className="hidden lg:inline">Customize Columns</span>
<span className="lg:hidden">Columns</span>
<IconChevronDown />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-56">
{table.getAllColumns().filter((column) => typeof column.accessorFn !== "undefined" && column.getCanHide()).map((column) => {
return (
<DropdownMenuCheckboxItem key={column.id} className="capitalize" checked={column.getIsVisible()} onCheckedChange={(value) => column.toggleVisibility(!!value)} >
{column.id}
</DropdownMenuCheckboxItem>
)
})}
</DropdownMenuContent>
{additionButtons && additionButtons}
</DropdownMenu>
</div>
</div>
<TabsContent value="outline" className="relative flex flex-col gap-4 overflow-auto px-4 lg:px-6">
<div className="overflow-hidden rounded-lg border">
<DndContext collisionDetection={closestCenter} modifiers={[restrictToVerticalAxis]} sensors={sensors} id={sortableId} >
<Table>
<TableHeader className="bg-muted sticky top-0 z-10">
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => {
return (
<TableHead key={header.id} colSpan={header.colSpan}>
{header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())}
</TableHead>
)
})}
</TableRow>
))}
</TableHeader>
<TableBody className="**:data-[slot=table-cell]:first:w-8">
{table.getRowModel().rows?.length ? (<SortableContext items={dataIds} strategy={verticalListSortingStrategy} >
{table.getRowModel().rows.map((row) => <DraggableRow selectedIDs={selectedBuildIDS} key={row.id} row={row} />)}
</SortableContext>) : (
<TableRow><TableCell colSpan={columns.length} className="h-24 text-center">No results.</TableCell></TableRow>
)}
</TableBody>
</Table>
</DndContext>
</div>
<div className="flex items-center justify-between px-4">
<div className="text-muted-foreground hidden flex-1 text-sm lg:flex">
{table.getFilteredSelectedRowModel().rows.length} of{" "}
{table.getFilteredRowModel().rows.length} row(s) selected.
</div>
<div className="flex w-full items-center gap-8 lg:w-fit">
<div className="hidden items-center gap-2 lg:flex">
<Label htmlFor="rows-per-page" className="text-sm font-medium">Rows per page</Label>
<Select value={`${pageSize}`} onValueChange={handlePageSizeChange}>
<SelectTrigger size="sm" className="w-20" id="rows-per-page">
<SelectValue placeholder={pageSize} />
</SelectTrigger>
<SelectContent side="top">
{[10, 20, 30].map((size) => <SelectItem key={size} value={`${size}`}>{size}</SelectItem>)}
</SelectContent>
</Select>
</div>
<div className="flex w-fit items-center justify-center text-sm font-medium">
Page {currentPage} of {totalPages}
</div>
<div className="flex w-fit items-center justify-center text-sm font-medium">
Total Count: {totalCount}
</div>
<div className="ml-auto flex items-center gap-2 lg:ml-0">
<Button
variant="outline"
size="icon"
onClick={() => onPageChange(1)}
disabled={currentPage === 1}
>
<IconChevronsLeft />
</Button>
<Button
variant="outline"
size="icon"
onClick={() => onPageChange(currentPage - 1)}
disabled={currentPage === 1}
>
<IconChevronLeft />
</Button>
<Button
variant="outline"
size="icon"
onClick={() => onPageChange(currentPage + 1)}
disabled={currentPage >= totalPages}
>
<IconChevronRight />
</Button>
<Button
variant="outline"
size="icon"
onClick={() => onPageChange(totalPages)}
disabled={currentPage >= totalPages}
>
<IconChevronsRight />
</Button>
</div>
</div>
</div>
</TabsContent>
<TabsContent value="past-performance" className="flex flex-col px-4 lg:px-6">
<div className="aspect-video w-full flex-1 rounded-lg border border-dashed"></div>
</TabsContent>
<TabsContent value="key-personnel" className="flex flex-col px-4 lg:px-6">
<div className="aspect-video w-full flex-1 rounded-lg border border-dashed"></div>
</TabsContent>
<TabsContent value="focus-documents" className="flex flex-col px-4 lg:px-6">
<div className="aspect-video w-full flex-1 rounded-lg border border-dashed"></div>
</TabsContent>
</Tabs>
)
}

View File

@ -0,0 +1,45 @@
'use client';
import { useState } from "react";
import { useGraphQlBuildsList } from "./queries";
import { UsersBuildDataTable } from "./data-table";
const PageUsersBuildsTableSection = (
{
selectedBuildIDS,
defaultSelection,
appendBuildID,
removeBuildID,
additionButtons,
setDefaultSelection
}: {
selectedBuildIDS: string[];
defaultSelection: string;
appendBuildID: (id: string) => void;
removeBuildID: (id: string) => void;
additionButtons?: React.ReactNode | null;
setDefaultSelection: (id: string) => 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 <div className="flex items-center justify-center p-8">Loading...</div> }
if (error) { return <div className="flex items-center justify-center p-8 text-red-500">Error loading users</div> }
return <>
<UsersBuildDataTable
data={data?.data || []} totalCount={data?.totalCount || 0} currentPage={page} pageSize={limit} onPageChange={handlePageChange} onPageSizeChange={handlePageSizeChange}
refetchTable={refetch} selectedBuildIDS={selectedBuildIDS} appendBuildID={appendBuildID} removeBuildID={removeBuildID} additionButtons={additionButtons} defaultSelection={defaultSelection} setDefaultSelection={setDefaultSelection}
/>
</>;
}
export default PageUsersBuildsTableSection;

View File

@ -0,0 +1,36 @@
'use client'
import { useQuery, useMutation } from '@tanstack/react-query'
import { ListArguments } from '@/types/listRequest'
const fetchGraphQlBuildsList = async (params: ListArguments): Promise<any> => {
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<boolean> => {
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) },
})
}

View File

@ -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<typeof schema>;

View File

@ -0,0 +1,134 @@
"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 { IconCircleMinus, 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 } from "lucide-react"
export function DraggableRow({ row, selectedIDs }: { row: Row<z.infer<typeof schema>>; selectedIDs: string[] }) {
const { transform, transition, setNodeRef, isDragging } = useSortable({ id: row.original._id })
return (
<TableRow data-state={row.getIsSelected() && "selected"} data-dragging={isDragging} ref={setNodeRef}
className={`${selectedIDs.includes(row.original._id) ? "bg-blue-700/50 hover:bg-blue-700/50" : ""} relative z-0 data-[dragging=true]:z-10 data-[dragging=true]:opacity-80`}
style={{ transform: CSS.Transform.toString(transform), transition: transition }}
>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>{flexRender(cell.column.columnDef.cell, cell.getContext())}</TableCell>
))}
</TableRow>
)
}
function getColumns(appendCompanyID: (id: string) => void, removeCompanyID: (id: string) => void, selectedCompanyIDS: string[], defaultSelection: string, setDefaultSelection: (id: string) => void): ColumnDef<schemaType>[] {
return [
{
accessorKey: "uuid",
header: "UUID",
cell: ({ getValue }) => (<div className="font-mono text-xs bg-muted px-2 py-1 rounded break-all">{String(getValue())}</div>),
},
{
accessorKey: "formal_name",
header: "Formal Name",
cell: ({ getValue }) => getValue(),
},
{
accessorKey: "company_type",
header: "Company Type",
cell: ({ getValue }) => getValue(),
},
{
accessorKey: "commercial_type",
header: "Commercial Type",
cell: ({ getValue }) => getValue(),
},
{
accessorKey: "tax_no",
header: "Tax No",
cell: ({ getValue }) => getValue(),
},
{
accessorKey: "public_name",
header: "Public Name",
cell: ({ getValue }) => getValue(),
},
{
accessorKey: "company_tag",
header: "Company Tag",
cell: ({ getValue }) => getValue(),
},
{
accessorKey: "default_lang_type",
header: "Default Language",
cell: ({ getValue }) => getValue(),
},
{
accessorKey: "default_money_type",
header: "Default Money Type",
cell: ({ getValue }) => getValue(),
},
{
accessorKey: "is_commercial",
header: "Is Commercial",
cell: ({ getValue }) => getValue() ? (<div className="text-green-600 font-medium">Yes</div>) : (<div className="text-red-600 font-medium">No</div>),
},
{
accessorKey: "is_blacklist",
header: "Is Blacklist",
cell: ({ getValue }) => getValue() ? (<div className="text-green-600 font-medium">Yes</div>) : (<div className="text-red-600 font-medium">No</div>),
},
{
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 (
selectedCompanyIDS.includes(row.original._id) ? (
<div>
<Button className="bg-blue-600 border-blue-600 text-white" variant="outline" size="sm" onClick={() => { appendCompanyID(row.original._id) }}>
<IconHandClick />
</Button>
</div>
) :
<div>
<Button className="bg-blue-600 border-blue-600 text-white" variant="outline" size="sm" onClick={() => { removeCompanyID(row.original._id) }}>
<IconCircleMinus />
</Button>
</div>
);
},
}
]
}
export { getColumns };

View File

@ -0,0 +1,272 @@
"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 LivingSpaceCompanyDataTable({
data,
totalCount,
currentPage = 1,
pageSize = 10,
onPageChange,
onPageSizeChange,
refetchTable,
defaultSelection,
selectedCompanyIDS,
appendCompanyID,
removeCompanyID,
setDefaultSelection
}: {
data: schemaType[],
totalCount: number,
currentPage?: number,
pageSize?: number,
onPageChange: (page: number) => void,
onPageSizeChange: (size: number) => void,
refetchTable: () => void,
defaultSelection: string,
selectedCompanyIDS: string[],
appendCompanyID: (id: string) => void,
removeCompanyID: (id: string) => void,
setDefaultSelection: (id: string) => void
}) {
const [rowSelection, setRowSelection] = React.useState({})
const [columnVisibility, setColumnVisibility] = React.useState<VisibilityState>({})
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>([])
const [sorting, setSorting] = React.useState<SortingState>([])
const sortableId = React.useId()
const sensors = useSensors(useSensor(MouseSensor, {}), useSensor(TouchSensor, {}), useSensor(KeyboardSensor, {}))
const dataIds = React.useMemo<UniqueIdentifier[]>(() => data?.map(({ _id }) => _id) || [], [data])
const columns = getColumns(appendCompanyID, removeCompanyID, selectedCompanyIDS, defaultSelection, setDefaultSelection);
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 (
<Tabs defaultValue="outline" className="w-full flex-col justify-start gap-6">
<div className="flex items-center justify-between px-4 lg:px-6">
<Label htmlFor="view-selector" className="sr-only">View</Label>
<Select defaultValue="outline">
<SelectTrigger className="flex w-fit @4xl/main:hidden" size="sm" id="view-selector">
<SelectValue placeholder="Select a view" />
</SelectTrigger>
<SelectContent>
<SelectItem value="outline">Outline</SelectItem>
<SelectItem value="past-performance">Past Performance</SelectItem>
<SelectItem value="key-personnel">Key Personnel</SelectItem>
<SelectItem value="focus-documents">Focus Documents</SelectItem>
</SelectContent>
</Select>
<div className="flex items-center gap-2">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="sm">
<IconLayoutColumns />
<span className="hidden lg:inline">Customize Columns</span>
<span className="lg:hidden">Columns</span>
<IconChevronDown />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-56">
{table.getAllColumns().filter((column) => typeof column.accessorFn !== "undefined" && column.getCanHide()).map((column) => {
return (
<DropdownMenuCheckboxItem key={column.id} className="capitalize" checked={column.getIsVisible()} onCheckedChange={(value) => column.toggleVisibility(!!value)} >
{column.id}
</DropdownMenuCheckboxItem>
)
})}
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
<TabsContent value="outline" className="relative flex flex-col gap-4 overflow-auto px-4 lg:px-6">
<div className="overflow-hidden rounded-lg border">
<DndContext collisionDetection={closestCenter} modifiers={[restrictToVerticalAxis]} sensors={sensors} id={sortableId} >
<Table>
<TableHeader className="bg-muted sticky top-0 z-10">
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => {
return (
<TableHead key={header.id} colSpan={header.colSpan}>
{header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())}
</TableHead>
)
})}
</TableRow>
))}
</TableHeader>
<TableBody className="**:data-[slot=table-cell]:first:w-8">
{table.getRowModel().rows?.length ? (<SortableContext items={dataIds} strategy={verticalListSortingStrategy} >
{table.getRowModel().rows.map((row) => <DraggableRow selectedIDs={selectedCompanyIDS} key={row.id} row={row} />)}
</SortableContext>) : (
<TableRow><TableCell colSpan={columns.length} className="h-24 text-center">No results.</TableCell></TableRow>
)}
</TableBody>
</Table>
</DndContext>
</div>
<div className="flex items-center justify-between px-4">
<div className="text-muted-foreground hidden flex-1 text-sm lg:flex">
{table.getFilteredSelectedRowModel().rows.length} of{" "}
{table.getFilteredRowModel().rows.length} row(s) selected.
</div>
<div className="flex w-full items-center gap-8 lg:w-fit">
<div className="hidden items-center gap-2 lg:flex">
<Label htmlFor="rows-per-page" className="text-sm font-medium">Rows per page</Label>
<Select value={`${pageSize}`} onValueChange={handlePageSizeChange}>
<SelectTrigger size="sm" className="w-20" id="rows-per-page">
<SelectValue placeholder={pageSize} />
</SelectTrigger>
<SelectContent side="top">
{[10, 20, 30].map((size) => <SelectItem key={size} value={`${size}`}>{size}</SelectItem>)}
</SelectContent>
</Select>
</div>
<div className="flex w-fit items-center justify-center text-sm font-medium">
Page {currentPage} of {totalPages}
</div>
<div className="flex w-fit items-center justify-center text-sm font-medium">
Total Count: {totalCount}
</div>
<div className="ml-auto flex items-center gap-2 lg:ml-0">
<Button
variant="outline"
size="icon"
onClick={() => onPageChange(1)}
disabled={currentPage === 1}
>
<IconChevronsLeft />
</Button>
<Button
variant="outline"
size="icon"
onClick={() => onPageChange(currentPage - 1)}
disabled={currentPage === 1}
>
<IconChevronLeft />
</Button>
<Button
variant="outline"
size="icon"
onClick={() => onPageChange(currentPage + 1)}
disabled={currentPage >= totalPages}
>
<IconChevronRight />
</Button>
<Button
variant="outline"
size="icon"
onClick={() => onPageChange(totalPages)}
disabled={currentPage >= totalPages}
>
<IconChevronsRight />
</Button>
</div>
</div>
</div>
</TabsContent>
<TabsContent value="past-performance" className="flex flex-col px-4 lg:px-6">
<div className="aspect-video w-full flex-1 rounded-lg border border-dashed"></div>
</TabsContent>
<TabsContent value="key-personnel" className="flex flex-col px-4 lg:px-6">
<div className="aspect-video w-full flex-1 rounded-lg border border-dashed"></div>
</TabsContent>
<TabsContent value="focus-documents" className="flex flex-col px-4 lg:px-6">
<div className="aspect-video w-full flex-1 rounded-lg border border-dashed"></div>
</TabsContent>
</Tabs>
)
}

View File

@ -0,0 +1,41 @@
'use client';
import { useState } from "react";
import { useGraphQlCompanyList } from "./queries";
import { LivingSpaceCompanyDataTable } from "./data-table";
const PageUsersCompanyTableSection = (
{
selectedCompanyIDS,
appendCompanyID,
removeCompanyID,
defaultSelection,
setDefaultSelection
}: {
selectedCompanyIDS: string[],
appendCompanyID: (id: string) => void,
removeCompanyID: (id: string) => void,
defaultSelection: string,
setDefaultSelection: (id: string) => 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 } = useGraphQlCompanyList({ limit, skip: (page - 1) * limit, sort, filters });
const handlePageChange = (newPage: number) => { setPage(newPage) };
const handlePageSizeChange = (newSize: number) => { setLimit(newSize); setPage(1) };
if (isLoading) { return <div className="flex items-center justify-center p-8">Loading...</div> }
if (error) { return <div className="flex items-center justify-center p-8 text-red-500">Error loading users</div> }
return < LivingSpaceCompanyDataTable
data={data?.data || []} totalCount={data?.totalCount || 0} currentPage={page} pageSize={limit} onPageChange={handlePageChange} onPageSizeChange={handlePageSizeChange}
refetchTable={refetch} selectedCompanyIDS={selectedCompanyIDS} appendCompanyID={appendCompanyID} removeCompanyID={removeCompanyID} defaultSelection={defaultSelection} setDefaultSelection={setDefaultSelection}
/>
}
export default PageUsersCompanyTableSection;

View File

@ -0,0 +1,19 @@
'use client'
import { useQuery } from '@tanstack/react-query'
import { ListArguments } from '@/types/listRequest'
import { schemaType } from "./schema";
const fetchGraphQlCompanyList = async (params: ListArguments): Promise<{ data: schemaType[], totalCount: number }> => {
console.log('Fetching test data from local API');
const { limit, skip, sort, filters } = params;
try {
const res = await fetch('/api/company/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 }
};
export function useGraphQlCompanyList(params: ListArguments) {
return useQuery({ queryKey: ['graphql-company-list', params], queryFn: () => fetchGraphQlCompanyList(params) })
}

View File

@ -0,0 +1,27 @@
import { z } from "zod";
export const schema = z.object({
_id: z.string(),
uuid: z.string(),
formal_name: z.string(),
company_type: z.string(),
commercial_type: z.string(),
tax_no: z.string(),
public_name: z.string(),
company_tag: z.string(),
default_lang_type: z.string().default("TR"),
default_money_type: z.string().default("TL"),
is_commercial: z.boolean().default(false),
is_blacklist: z.boolean().default(false),
parent_id: z.string().optional(),
workplace_no: z.string().optional(),
official_address: z.string().optional(),
top_responsible_company: z.string().optional(),
expiryStarts: z.string().optional(),
expiryEnds: z.string().optional(),
createdAt: z.string().nullable().optional(),
updatedAt: z.string().nullable().optional(),
});
export type schemaType = z.infer<typeof schema>;

View File

@ -1,4 +1,5 @@
"use client" "use client"
import { useState } from "react"
import { useFieldArray, useForm } from "react-hook-form" import { useFieldArray, useForm } from "react-hook-form"
import { zodResolver } from "@hookform/resolvers/zod" import { zodResolver } from "@hookform/resolvers/zod"
import { Form, FormField, FormItem, FormLabel, FormControl, FormMessage } from "@/components/ui/form" import { Form, FormField, FormItem, FormLabel, FormControl, FormMessage } from "@/components/ui/form"
@ -9,6 +10,7 @@ import { Separator } from "@/components/ui/separator"
import { DateTimePicker } from "@/components/ui/date-time-picker" import { DateTimePicker } from "@/components/ui/date-time-picker"
import { useUpdateUserMutation } from "@/pages/users/update/queries" import { useUpdateUserMutation } from "@/pages/users/update/queries"
import { userUpdateSchema, type UserUpdate } from "@/pages/users/update/schema" import { userUpdateSchema, type UserUpdate } from "@/pages/users/update/schema"
import PageAddUserSelections from "../selections/addPage"
const UserForm = ({ refetchTable, initData, selectedUuid }: { refetchTable: () => void, initData: UserUpdate, selectedUuid: string }) => { const UserForm = ({ refetchTable, initData, selectedUuid }: { refetchTable: () => void, initData: UserUpdate, selectedUuid: string }) => {
@ -22,19 +24,30 @@ const UserForm = ({ refetchTable, initData, selectedUuid }: { refetchTable: () =
tag: initData.tag, tag: initData.tag,
email: initData.email, email: initData.email,
phone: initData.phone, phone: initData.phone,
collectionTokens: initData.collectionTokens,
}, },
}) })
const { control, handleSubmit } = form const { handleSubmit } = form
const { fields, append, remove } = useFieldArray({ control, name: "collectionTokens.tokens" }) const [defaultSelection, setDefaultSelection] = useState<string>("")
const [selectedBuildIDS, setSelectedBuildIDS] = useState<string[]>([])
const [selectedCompanyIDS, setSelectedCompanyIDS] = useState<string[]>([])
const appendBuildID = (id: string) => setSelectedBuildIDS((prev) => (id && !selectedBuildIDS.includes(id) ? [...prev, id] : prev))
const appendCompanyID = (id: string) => setSelectedCompanyIDS((prev) => (id && !selectedCompanyIDS.includes(id) ? [...prev, id] : prev))
const removeBuildID = (id: string) => setSelectedBuildIDS((prev) => prev.filter((item) => item !== id))
const removeCompanyID = (id: string) => setSelectedCompanyIDS((prev) => prev.filter((item) => item !== id))
const mutation = useUpdateUserMutation(); const mutation = useUpdateUserMutation();
function onSubmit(values: UserUpdate) { mutation.mutate({ data: values as any || initData, uuid: selectedUuid, selectedBuildIDS, selectedCompanyIDS, defaultSelection, refetchTable }); setTimeout(() => refetchTable(), 400) }
function onSubmit(values: UserUpdate) { mutation.mutate({ data: values as any || initData, uuid: selectedUuid }); setTimeout(() => refetchTable(), 400) }
return ( return (
<div>
<PageAddUserSelections
selectedCompanyIDS={selectedCompanyIDS} selectedBuildingIDS={selectedBuildIDS} appendCompanyID={appendCompanyID} appendBuildingID={appendBuildID}
removeCompanyID={removeCompanyID} removeBuildingID={removeBuildID} defaultSelection={defaultSelection} setDefaultSelection={setDefaultSelection}
/>
<Form {...form}> <Form {...form}>
<form onSubmit={handleSubmit(onSubmit)} className="space-y-6 p-4"> <form onSubmit={handleSubmit(onSubmit)} className="space-y-6 p-4">
{/* BASIC INFO */} {/* BASIC INFO */}
@ -52,7 +65,6 @@ const UserForm = ({ refetchTable, initData, selectedUuid }: { refetchTable: () =
</FormItem> </FormItem>
)} )}
/> />
<FormField <FormField
control={form.control} control={form.control}
name="phone" name="phone"
@ -70,6 +82,33 @@ const UserForm = ({ refetchTable, initData, selectedUuid }: { refetchTable: () =
{/* PASSWORD / TAG */} {/* PASSWORD / TAG */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>Password</FormLabel>
<FormControl>
<Input type="password" placeholder="•••••••" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="rePassword"
render={({ field }) => (
<FormItem>
<FormLabel>Re-Password</FormLabel>
<FormControl>
<Input type="password" placeholder="•••••••" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField <FormField
control={form.control} control={form.control}
name="tag" name="tag"
@ -83,40 +122,6 @@ const UserForm = ({ refetchTable, initData, selectedUuid }: { refetchTable: () =
</FormItem> </FormItem>
)} )}
/> />
</div>
{/* DATES */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<FormField
control={form.control}
name="expiryStarts"
render={({ field }) => (
<FormItem>
<FormLabel>Expiry Starts</FormLabel>
<FormControl>
<DateTimePicker {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="expiryEnds"
render={({ field }) => (
<FormItem>
<FormLabel>Expiry Ends</FormLabel>
<FormControl>
<DateTimePicker {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
{/* SWITCHES */} {/* SWITCHES */}
<div className="flex items-center gap-6"> <div className="flex items-center gap-6">
<FormField <FormField
@ -134,7 +139,6 @@ const UserForm = ({ refetchTable, initData, selectedUuid }: { refetchTable: () =
</FormItem> </FormItem>
)} )}
/> />
<FormField <FormField
control={form.control} control={form.control}
name="isNotificationSend" name="isNotificationSend"
@ -151,55 +155,31 @@ const UserForm = ({ refetchTable, initData, selectedUuid }: { refetchTable: () =
)} )}
/> />
</div> </div>
</div>
<Separator /> {/* DATES */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{/* COLLECTION TOKENS */}
<FormField <FormField
control={form.control} control={form.control}
name="collectionTokens.default" name="expiryStarts"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>Default Token</FormLabel> <FormLabel>Expiry Starts</FormLabel>
<FormControl> <FormControl>
<Input placeholder="Default token..." {...field} /> <DateTimePicker {...field} />
</FormControl> </FormControl>
<FormMessage />
</FormItem> </FormItem>
)} )}
/> />
<div className="space-y-3">
<div className="flex justify-between items-center">
<FormLabel>Tokens</FormLabel>
<Button
type="button"
variant="outline"
size="sm"
onClick={() => append({ prefix: "", token: "" })}
>
+ Add Token
</Button>
</div>
{fields.length === 0 && (
<p className="text-sm text-muted-foreground">No tokens added.</p>
)}
{fields.map((fieldItem, i) => (
<div
key={fieldItem.id}
className="grid grid-cols-12 gap-2 items-end"
>
<div className="col-span-4">
<FormField <FormField
control={form.control} control={form.control}
name={`collectionTokens.tokens.${i}.prefix`} name="expiryEnds"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>Prefix</FormLabel> <FormLabel>Expiry Ends</FormLabel>
<FormControl> <FormControl>
<Input placeholder="prefix" {...field} /> <DateTimePicker {...field} />
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
@ -207,42 +187,12 @@ const UserForm = ({ refetchTable, initData, selectedUuid }: { refetchTable: () =
/> />
</div> </div>
<div className="col-span-6">
<FormField
control={form.control}
name={`collectionTokens.tokens.${i}.token`}
render={({ field }) => (
<FormItem>
<FormLabel>Token</FormLabel>
<FormControl>
<Input placeholder="token" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<Button
type="button"
variant="destructive"
size="icon"
className="col-span-2 h-10"
onClick={() => remove(i)}
>
</Button>
</div>
))}
</div>
<Separator /> <Separator />
<Button type="submit" className="w-full">Create User</Button>
<Button type="submit" className="w-full">
Update User
</Button>
</form> </form>
</Form> </Form>
</div>
) )
} }

View File

@ -2,10 +2,10 @@
import { useMutation } from '@tanstack/react-query' import { useMutation } from '@tanstack/react-query'
import { UserUpdate } from './types'; import { UserUpdate } from './types';
const fetchGraphQlUsersUpdate = async (record: UserUpdate, uuid: string): Promise<{ data: UserUpdate | null; status: number }> => { const fetchGraphQlUsersUpdate = async (record: UserUpdate, uuid: string, selectedBuildIDS: string[], selectedCompanyIDS: string[], defaultSelection: string, refetchTable: () => void): Promise<{ data: UserUpdate | null; status: number }> => {
console.log('Fetching test data from local API'); console.log('Fetching test data from local API');
try { try {
const res = await fetch(`/api/users/update?uuid=${uuid || ''}`, { method: 'POST', cache: 'no-store', credentials: "include", body: JSON.stringify(record) }); const res = await fetch(`/api/users/update?uuid=${uuid || ''}`, { method: 'POST', cache: 'no-store', credentials: "include", body: JSON.stringify({ ...record, selectedBuildIDS, selectedCompanyIDS, defaultSelection }) });
if (!res.ok) { const errorText = await res.text(); console.error('Test data API error:', errorText); throw new Error(`API error: ${res.status} ${res.statusText}`) } 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(); const data = await res.json();
return { data: data.data, status: res.status } return { data: data.data, status: res.status }
@ -14,7 +14,9 @@ const fetchGraphQlUsersUpdate = async (record: UserUpdate, uuid: string): Promis
export function useUpdateUserMutation() { export function useUpdateUserMutation() {
return useMutation({ return useMutation({
mutationFn: ({ data, uuid }: { data: UserUpdate, uuid: string }) => fetchGraphQlUsersUpdate(data, uuid), mutationFn: (
{ data, uuid, selectedBuildIDS, selectedCompanyIDS, defaultSelection, refetchTable }: { data: UserUpdate, uuid: string, selectedBuildIDS: string[], selectedCompanyIDS: string[], defaultSelection: string, refetchTable: () => void }
) => fetchGraphQlUsersUpdate(data, uuid, selectedBuildIDS, selectedCompanyIDS, defaultSelection, refetchTable),
onSuccess: () => { console.log("User updated successfully") }, onSuccess: () => { console.log("User updated successfully") },
onError: (error) => { console.error("Update user failed:", error) }, onError: (error) => { console.error("Update user failed:", error) },
}) })

View File

@ -1,14 +1,5 @@
import { z } from "zod" import { z } from "zod"
export const tokenSchema = z.object({
prefix: z.string().min(1, "Prefix is required"),
token: z.string().min(1, "Token is required"),
})
export const collectionTokensSchema = z.object({
default: z.string().optional(),
tokens: z.array(tokenSchema)
})
export const userUpdateSchema = z.object({ export const userUpdateSchema = z.object({
expiryStarts: z.string().optional(), expiryStarts: z.string().optional(),
@ -17,11 +8,11 @@ export const userUpdateSchema = z.object({
isConfirmed: z.boolean(), isConfirmed: z.boolean(),
isNotificationSend: z.boolean(), isNotificationSend: z.boolean(),
password: z.string().min(6),
rePassword: z.string().min(6),
tag: z.string().optional(), tag: z.string().optional(),
email: z.string().email(), email: z.string().email(),
phone: z.string().min(5), phone: z.string().min(5),
collectionTokens: collectionTokensSchema,
}) })
export type UserUpdate = z.infer<typeof userUpdateSchema> export type UserUpdate = z.infer<typeof userUpdateSchema>