parts and areas tested

This commit is contained in:
2025-11-24 21:04:14 +03:00
parent a5a7a7e7b5
commit eedfed1a65
131 changed files with 5429 additions and 471 deletions

View File

@@ -9,21 +9,12 @@ import { DateTimePicker } from "@/components/ui/date-time-picker"
import { BuildAreasAdd, buildAreasAddSchema } from "./schema"
import { useAddBuildAreasMutation } from "./queries"
const BuildAreasForm = ({ refetchTable }: { refetchTable: () => void }) => {
const BuildAreasForm = ({ refetchTable, buildId }: { refetchTable: () => void, buildId: string }) => {
const form = useForm<BuildAreasAdd>({
resolver: zodResolver(buildAreasAddSchema),
defaultValues: {
areaName: "",
areaCode: "",
areaType: "",
areaDirection: "",
areaGrossSize: 0,
areaNetSize: 0,
width: 0,
size: 0,
expiryStarts: "",
expiryEnds: "",
areaName: "", areaCode: "", areaType: "", areaDirection: "", areaGrossSize: 0, areaNetSize: 0, width: 0, size: 0, expiryStarts: "", expiryEnds: "",
},
});
@@ -31,17 +22,13 @@ const BuildAreasForm = ({ refetchTable }: { refetchTable: () => void }) => {
const mutation = useAddBuildAreasMutation();
function onSubmit(values: BuildAreasAdd) { mutation.mutate({ data: values }); setTimeout(() => refetchTable(), 400) };
function onSubmit(values: BuildAreasAdd) { mutation.mutate({ data: values, buildId }); setTimeout(() => refetchTable(), 400) };
return (
<Form {...form}>
<form
onSubmit={handleSubmit(onSubmit)}
className="space-y-6 p-4"
>
<form onSubmit={handleSubmit(onSubmit)} className="space-y-6 p-4">
{/* ROW 1 */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<FormField
control={form.control}
name="areaName"
@@ -110,7 +97,7 @@ const BuildAreasForm = ({ refetchTable }: { refetchTable: () => void }) => {
<FormItem>
<FormLabel>Area Gross Size</FormLabel>
<FormControl>
<Input placeholder="Area Gross Size" {...field} />
<Input type="number" placeholder="Area Gross Size" {...field} onChange={(e) => field.onChange(e.target.value === "" ? undefined : Number(e.target.value))} />
</FormControl>
<FormMessage />
</FormItem>
@@ -124,7 +111,7 @@ const BuildAreasForm = ({ refetchTable }: { refetchTable: () => void }) => {
<FormItem>
<FormLabel>Area Net Size</FormLabel>
<FormControl>
<Input placeholder="Area Net Size" {...field} />
<Input type="number" placeholder="Area Net Size" {...field} onChange={(e) => field.onChange(e.target.value === "" ? undefined : Number(e.target.value))} />
</FormControl>
<FormMessage />
</FormItem>
@@ -140,7 +127,7 @@ const BuildAreasForm = ({ refetchTable }: { refetchTable: () => void }) => {
<FormItem>
<FormLabel>Width</FormLabel>
<FormControl>
<Input placeholder="Width" {...field} />
<Input type="number" placeholder="Width" {...field} onChange={(e) => field.onChange(e.target.value === "" ? undefined : Number(e.target.value))} />
</FormControl>
<FormMessage />
</FormItem>
@@ -154,7 +141,7 @@ const BuildAreasForm = ({ refetchTable }: { refetchTable: () => void }) => {
<FormItem>
<FormLabel>Size</FormLabel>
<FormControl>
<Input placeholder="Size" {...field} />
<Input type="number" placeholder="Size" {...field} onChange={(e) => field.onChange(e.target.value === "" ? undefined : Number(e.target.value))} />
</FormControl>
<FormMessage />
</FormItem>

View File

@@ -3,6 +3,8 @@ import { useState } from 'react';
import { BuildAreasDataTableAdd } from './table/data-table';
import { useGraphQlBuildAreasList } from '@/pages/build-areas/queries';
import { BuildAreasForm } from './form';
import { Button } from '@/components/ui/button';
import { useSearchParams, useRouter } from 'next/navigation';
const PageAddBuildAreas = () => {
const [page, setPage] = useState(1);
@@ -10,14 +12,19 @@ const PageAddBuildAreas = () => {
const [sort, setSort] = useState({ createdAt: 'desc' });
const [filters, setFilters] = useState({});
const { data, isLoading, error, refetch } = useGraphQlBuildAreasList({ limit, skip: (page - 1) * limit, sort, filters });
const searchParams = useSearchParams();
const router = useRouter();
const buildId = searchParams?.get('build');
const { data, isLoading, error, refetch } = useGraphQlBuildAreasList({ limit, skip: (page - 1) * limit, sort, filters: { ...filters, buildId: buildId } });
const noUUIDFound = <><div>Back To Builds. No uuid is found on headers</div><Button onClick={() => router.push('/builds')}>Back to Builds</Button></>
if (!buildId) { return noUUIDFound }
return (
<>
<BuildAreasDataTableAdd
data={data?.data || []} totalCount={data?.totalCount || 0} currentPage={page} pageSize={limit} onPageChange={setPage} onPageSizeChange={setLimit} refetchTable={refetch}
data={data?.data || []} totalCount={data?.totalCount || 0} currentPage={page} pageSize={limit} onPageChange={setPage} onPageSizeChange={setLimit} refetchTable={refetch} buildId={buildId}
/>
<BuildAreasForm refetchTable={refetch} />
<BuildAreasForm refetchTable={refetch} buildId={buildId} />
</>
)
}

View File

@@ -3,13 +3,12 @@ import { useMutation } from '@tanstack/react-query'
import { toISOIfNotZ } from '@/lib/utils';
import { BuildAreasAdd } from './schema';
const fetchGraphQlBuildSitesAdd = async (record: BuildAreasAdd): Promise<{ data: BuildAreasAdd | null; status: number }> => {
const fetchGraphQlBuildSitesAdd = async (record: BuildAreasAdd, buildId: string): Promise<{ data: BuildAreasAdd | null; status: number }> => {
console.log('Fetching test data from local API');
record.expiryStarts = record.expiryStarts ? toISOIfNotZ(record.expiryStarts) : undefined;
record.expiryEnds = record.expiryEnds ? toISOIfNotZ(record.expiryEnds) : undefined;
console.dir({ record })
try {
const res = await fetch('/api/build-areas/add', { method: 'POST', cache: 'no-store', credentials: "include", body: JSON.stringify(record) });
const res = await fetch('/api/build-areas/add', { method: 'POST', cache: 'no-store', credentials: "include", body: JSON.stringify({ ...record, buildId }) });
if (!res.ok) { const errorText = await res.text(); console.error('Test data API error:', errorText); throw new Error(`API error: ${res.status} ${res.statusText}`) }
const data = await res.json();
return { data: data.data, status: res.status }
@@ -18,7 +17,7 @@ const fetchGraphQlBuildSitesAdd = async (record: BuildAreasAdd): Promise<{ data:
export function useAddBuildAreasMutation() {
return useMutation({
mutationFn: ({ data }: { data: BuildAreasAdd }) => fetchGraphQlBuildSitesAdd(data),
mutationFn: ({ data, buildId }: { data: BuildAreasAdd, buildId: string }) => fetchGraphQlBuildSitesAdd(data, buildId),
onSuccess: () => { console.log("Build areas created successfully") },
onError: (error) => { console.error("Add build areas failed:", error) },
})

View File

@@ -105,9 +105,6 @@ function getColumns(router: any, deleteHandler: (id: string) => void): ColumnDef
cell: ({ row }) => {
return (
<div>
<Button className="bg-amber-400 text-black border-amber-400" variant="outline" size="sm" onClick={() => { router.push(`/build-sites/update?uuid=${row.original.uuid}`) }}>
<Pencil />
</Button>
<Button className="bg-red-700 text-white border-red-700 mx-4" variant="outline" size="sm" onClick={() => { deleteHandler(row.original.uuid || "") }}>
<Trash />
</Button>

View File

@@ -55,6 +55,7 @@ export function BuildAreasDataTableAdd({
onPageChange,
onPageSizeChange,
refetchTable,
buildId
}: {
data: schemaType[],
totalCount: number,
@@ -62,7 +63,8 @@ export function BuildAreasDataTableAdd({
pageSize: number,
onPageChange: (page: number) => void,
onPageSizeChange: (size: number) => void,
refetchTable: () => void
refetchTable: () => void,
buildId: string
}) {
const router = useRouter();
@@ -142,7 +144,7 @@ export function BuildAreasDataTableAdd({
})}
</DropdownMenuContent>
</DropdownMenu>
<Button variant="outline" size="sm" onClick={() => { router.push("/build-areas") }}>
<Button variant="outline" size="sm" onClick={() => { router.push(`/build-areas?build=${buildId}`) }}>
<Home />
<span className="hidden lg:inline">Back to Build Areas</span>
</Button>

View File

@@ -23,7 +23,7 @@ export function DraggableRow({ row }: { row: Row<z.infer<typeof schema>> }) {
)
}
function getColumns(router: any, deleteHandler: (id: string) => void): ColumnDef<schemaType>[] {
function getColumns(router: any, deleteHandler: (id: string) => void, buildID: string): ColumnDef<schemaType>[] {
return [
{
accessorKey: "uuid",
@@ -88,7 +88,7 @@ function getColumns(router: any, deleteHandler: (id: string) => void): ColumnDef
cell: ({ row }) => {
return (
<div>
<Button className="bg-amber-400 text-black border-amber-400" variant="outline" size="sm" onClick={() => { router.push(`/build-areas/update?uuid=${row.original.uuid}`) }}>
<Button className="bg-amber-400 text-black border-amber-400" variant="outline" size="sm" onClick={() => { router.push(`/build-areas/update?uuid=${row.original.uuid}&build=${buildID}`) }}>
<Pencil />
</Button>
<Button className="bg-red-700 text-white border-red-700 mx-4" variant="outline" size="sm" onClick={() => { deleteHandler(row.original.uuid || "") }}>

View File

@@ -78,7 +78,8 @@ export function BuildAreasDataTable({
pageSize = 10,
onPageChange,
onPageSizeChange,
refetchTable
refetchTable,
buildId
}: {
data: schemaType[],
totalCount: number,
@@ -87,6 +88,7 @@ export function BuildAreasDataTable({
onPageChange: (page: number) => void,
onPageSizeChange: (size: number) => void,
refetchTable: () => void,
buildId: string
}) {
const router = useRouter();
@@ -100,7 +102,7 @@ export function BuildAreasDataTable({
const deleteMutation = useDeleteBuildAreaMutation()
const deleteHandler = (id: string) => { deleteMutation.mutate({ uuid: id }); setTimeout(() => { refetchTable() }, 400) }
const columns = getColumns(router, deleteHandler);
const columns = getColumns(router, deleteHandler, buildId);
const pagination = React.useMemo(() => ({ pageIndex: currentPage - 1, pageSize: pageSize }), [currentPage, pageSize])
const totalPages = Math.ceil(totalCount / pageSize)
@@ -163,7 +165,7 @@ export function BuildAreasDataTable({
})}
</DropdownMenuContent>
</DropdownMenu>
<Button variant="outline" size="sm" onClick={() => { router.push("/build-areas/add") }}>
<Button variant="outline" size="sm" onClick={() => { router.push(`/build-areas/add?build=${buildId}`) }}>
<IconPlus />
<span className="hidden lg:inline">Add Build Areas</span>
</Button>

View File

@@ -1,7 +1,9 @@
'use client';
import { Button } from '@/components/ui/button';
import { BuildAreasDataTable } from './list/data-table';
import { useGraphQlBuildAreasList } from './queries';
import { useState } from 'react';
import { useSearchParams, useRouter } from 'next/navigation';
const PageBuildAreas = () => {
@@ -9,16 +11,17 @@ const PageBuildAreas = () => {
const [limit, setLimit] = useState(10);
const [sort, setSort] = useState({ createdAt: 'desc' });
const [filters, setFilters] = useState({});
const { data, isLoading, error, refetch } = useGraphQlBuildAreasList({ limit, skip: (page - 1) * limit, sort, filters });
const searchParams = useSearchParams();
const router = useRouter();
const buildId = searchParams?.get('build');
const noUUIDFound = <><div>Back To Builds. No uuid is found on headers</div><Button onClick={() => router.push('/builds')}>Back to Builds</Button></>
if (!buildId) { return noUUIDFound }
const { data, isLoading, error, refetch } = useGraphQlBuildAreasList({ limit, skip: (page - 1) * limit, sort, filters: { ...filters, buildId: buildId } });
const handlePageChange = (newPage: number) => { setPage(newPage) };
const handlePageSizeChange = (newSize: number) => { setLimit(newSize); setPage(1) };
if (isLoading) { return <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 build areas</div> }
return <BuildAreasDataTable data={data?.data || []} totalCount={data?.totalCount || 0} currentPage={page} pageSize={limit} onPageChange={handlePageChange} onPageSizeChange={handlePageSizeChange} refetchTable={refetch} />;
return <BuildAreasDataTable data={data?.data || []} totalCount={data?.totalCount || 0} currentPage={page} pageSize={limit} onPageChange={handlePageChange} onPageSizeChange={handlePageSizeChange} refetchTable={refetch} buildId={buildId} />;
};

View File

@@ -9,7 +9,7 @@ import { DateTimePicker } from "@/components/ui/date-time-picker"
import { useUpdateBuildSitesMutation } from "@/pages/build-sites/update/queries"
import { BuildAreasUpdate, buildAreasUpdateSchema } from "@/pages/build-areas/update/schema"
const BuildAreasForm = ({ refetchTable, initData, selectedUuid }: { refetchTable: () => void, initData: BuildAreasUpdate, selectedUuid: string }) => {
const BuildAreasForm = ({ refetchTable, initData, selectedUuid, buildId }: { refetchTable: () => void, initData: BuildAreasUpdate, selectedUuid: string, buildId: string }) => {
const form = useForm<BuildAreasUpdate>({ resolver: zodResolver(buildAreasUpdateSchema), defaultValues: { ...initData } })
@@ -17,7 +17,7 @@ const BuildAreasForm = ({ refetchTable, initData, selectedUuid }: { refetchTable
const mutation = useUpdateBuildSitesMutation();
function onSubmit(values: BuildAreasUpdate) { mutation.mutate({ data: values as any || initData, uuid: selectedUuid }); setTimeout(() => refetchTable(), 400) }
function onSubmit(values: BuildAreasUpdate) { mutation.mutate({ data: values as any || initData, uuid: selectedUuid, buildId }); setTimeout(() => refetchTable(), 400) }
return (
<Form {...form}>

View File

@@ -6,7 +6,7 @@ import { Button } from '@/components/ui/button';
import { BuildAreasDataTableUpdate } from './table/data-table';
import { useGraphQlBuildAreasList } from '../queries';
const PageUpdateBuildSites = () => {
const PageUpdateBuildAreas = () => {
const [page, setPage] = useState(1);
const [limit, setLimit] = useState(10);
const [sort, setSort] = useState({ createdAt: 'desc' });
@@ -14,23 +14,21 @@ const PageUpdateBuildSites = () => {
const searchParams = useSearchParams()
const router = useRouter()
const uuid = searchParams?.get('uuid') || null
const backToBuildAddress = <>
<div>UUID not found in search params</div>
<Button onClick={() => router.push('/build-areas')}>Back to Build Areas</Button>
</>
if (!uuid) { return backToBuildAddress }
const { data, isLoading, error, refetch } = useGraphQlBuildAreasList({ limit, skip: (page - 1) * limit, sort, filters: { ...filters, uuid } });
const backToBuildAddress = <><div>UUID not found in search params</div><Button onClick={() => router.push('/build-areas')}>Back to Build Areas</Button></>
const buildId = searchParams?.get('build');
const noUUIDFound = <><div>Back To Builds. No uuid is found on headers</div><Button onClick={() => router.push('/builds')}>Back to Builds</Button></>
if (!buildId) { return noUUIDFound }; if (!uuid) { return backToBuildAddress }
const { data, isLoading, error, refetch } = useGraphQlBuildAreasList({ limit, skip: (page - 1) * limit, sort, filters: { ...filters, buildId, uuid } });
const initData = data?.data?.[0] || null;
if (!initData) { return backToBuildAddress }
return (
<>
<BuildAreasDataTableUpdate
data={data?.data || []} totalCount={data?.totalCount || 0} currentPage={page} pageSize={limit}
onPageChange={setPage} onPageSizeChange={setLimit} refetchTable={refetch}
data={data?.data || []} totalCount={data?.totalCount || 0} currentPage={page} pageSize={limit} onPageChange={setPage} onPageSizeChange={setLimit} refetchTable={refetch} buildID={buildId}
/>
<BuildAreasForm refetchTable={refetch} initData={initData} selectedUuid={uuid} />
<BuildAreasForm refetchTable={refetch} initData={initData} selectedUuid={uuid} buildId={buildId} />
</>
)
}
export { PageUpdateBuildSites };
export { PageUpdateBuildAreas };

View File

@@ -30,7 +30,7 @@ export function DraggableRow({ row }: { row: Row<z.infer<typeof schema>> }) {
)
}
function getColumns(router: any, deleteHandler: (id: string) => void): ColumnDef<schemaType>[] {
function getColumns(deleteHandler: (id: string) => void): ColumnDef<schemaType>[] {
return [
{
accessorKey: "uuid",

View File

@@ -80,6 +80,7 @@ export function BuildAreasDataTableUpdate({
onPageChange,
onPageSizeChange,
refetchTable,
buildID
}: {
data: schemaType[],
totalCount: number,
@@ -87,7 +88,8 @@ export function BuildAreasDataTableUpdate({
pageSize: number,
onPageChange: (page: number) => void,
onPageSizeChange: (size: number) => void,
refetchTable: () => void
refetchTable: () => void,
buildID: string
}) {
const router = useRouter();
@@ -101,7 +103,7 @@ export function BuildAreasDataTableUpdate({
const deleteMutation = useDeletePersonMutation()
const deleteHandler = (id: string) => { deleteMutation.mutate({ uuid: id }); setTimeout(() => { refetchTable() }, 200) }
const columns = getColumns(router, deleteHandler);
const columns = getColumns(deleteHandler);
const pagination = React.useMemo(() => ({ pageIndex: currentPage - 1, pageSize: pageSize, }), [currentPage, pageSize])
const totalPages = Math.ceil(totalCount / pageSize)
@@ -167,7 +169,7 @@ export function BuildAreasDataTableUpdate({
})}
</DropdownMenuContent>
</DropdownMenu>
<Button variant="outline" size="sm" onClick={() => { router.push("/build-areas") }}>
<Button variant="outline" size="sm" onClick={() => { router.push(`/build-areas?build=${buildID}`) }}>
<Home />
<span className="hidden lg:inline">Back to Build Areas</span>
</Button>

View File

@@ -169,7 +169,7 @@ export function BuildIbansDataTableUpdate({
</DropdownMenu>
<Button variant="outline" size="sm" onClick={() => { router.push("/build-ibans") }}>
<Home />
<span className="hidden lg:inline">Back to Build Areas</span>
<span className="hidden lg:inline">Back to Build IBANs</span>
</Button>
</div>
</div>

View File

@@ -1,6 +1,5 @@
interface UpdateBuildIbansUpdate {
iban: string;
startDate: string;
stopDate: string;

View File

@@ -0,0 +1,306 @@
"use client"
import { useForm } from "react-hook-form"
import { zodResolver } from "@hookform/resolvers/zod"
import { Form, FormField, FormItem, FormLabel, FormControl, FormMessage } from "@/components/ui/form"
import { Input } from "@/components/ui/input"
import { Button } from "@/components/ui/button"
import { Separator } from "@/components/ui/separator"
import { DateTimePicker } from "@/components/ui/date-time-picker"
import { BuildPartsAdd, buildPartsAddSchema } from "./schema"
import { useAddBuildAreasMutation } from "./queries"
import { Checkbox } from "@/components/ui/checkbox"
const BuildPartsForm = ({ refetchTable, selectedBuildId }: { refetchTable: () => void, selectedBuildId: string }) => {
const form = useForm<BuildPartsAdd>({
resolver: zodResolver(buildPartsAddSchema),
defaultValues: {
addressGovCode: "", no: 0, level: 0, code: "", grossSize: 0, netSize: 0, defaultAccessory: "", humanLivability: false, key: "",
directionId: "", typeId: "", active: false, isConfirmed: false, expiryStarts: "", expiryEnds: "",
},
});
const { handleSubmit } = form;
const mutation = useAddBuildAreasMutation();
function onSubmit(values: BuildPartsAdd) { mutation.mutate({ data: values, buildId: selectedBuildId }); setTimeout(() => refetchTable(), 400) };
return (
<Form {...form}>
<form
onSubmit={handleSubmit(onSubmit)}
className="space-y-6 p-4"
>
{/* ROW 1 */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<FormField
control={form.control}
name="addressGovCode"
render={({ field }) => (
<FormItem>
<FormLabel>Address Gov Code</FormLabel>
<FormControl>
<Input placeholder="TR-34-XXX-XXXX" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="no"
render={({ field }) => (
<FormItem>
<FormLabel>No</FormLabel>
<FormControl>
<Input
type="number"
placeholder="Area No"
{...field}
onChange={(e) => field.onChange(e.target.value === "" ? undefined : Number(e.target.value))}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
{/* ROW 2 */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<FormField
control={form.control}
name="level"
render={({ field }) => (
<FormItem>
<FormLabel>Level</FormLabel>
<FormControl>
<Input
type="number"
placeholder="Floor / Level"
{...field}
onChange={(e) => field.onChange(e.target.value === "" ? undefined : Number(e.target.value))}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="code"
render={({ field }) => (
<FormItem>
<FormLabel>Code</FormLabel>
<FormControl>
<Input placeholder="Part Code (e.g. A5)" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
{/* ROW 3 */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<FormField
control={form.control}
name="key"
render={({ field }) => (
<FormItem>
<FormLabel>Key</FormLabel>
<FormControl>
<Input placeholder="Unique Key (e.g. BLK-A5-L5-N3)" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="grossSize"
render={({ field }) => (
<FormItem>
<FormLabel>Gross Size</FormLabel>
<FormControl>
<Input
type="number"
placeholder="Gross Size (m²)"
{...field}
onBlur={(e) => field.onChange(e.target.value === "" ? undefined : Number(e.target.value))}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
{/* ROW 4 */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<FormField
control={form.control}
name="netSize"
render={({ field }) => (
<FormItem>
<FormLabel>Net Size</FormLabel>
<FormControl>
<Input
type="number"
placeholder="Net Size (m²)"
{...field}
onBlur={(e) => field.onChange(e.target.value === "" ? undefined : Number(e.target.value))}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="defaultAccessory"
render={({ field }) => (
<FormItem>
<FormLabel>Default Accessory</FormLabel>
<FormControl>
<Input placeholder="e.g. balcony, terrace..." {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
{/* ROW 5 */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<FormField
control={form.control}
name="directionId"
render={({ field }) => (
<FormItem>
<FormLabel>Direction Id</FormLabel>
<FormControl>
<Input placeholder="Direction ObjectId (optional)" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="typeId"
render={({ field }) => (
<FormItem>
<FormLabel>Type Id</FormLabel>
<FormControl>
<Input placeholder="Type ObjectId (optional)" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
{/* BOOLEAN ROW */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
{/* boşluk kalmasın diye aktif alanını buraya koydum */}
<FormField
control={form.control}
name="active"
render={({ field }) => (
<FormItem className="flex flex-row items-center justify-between rounded-md border p-3">
<FormLabel className="text-sm font-medium">
Active
</FormLabel>
<FormControl>
<Checkbox
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="humanLivability"
render={({ field }) => (
<FormItem className="flex flex-row items-center justify-between rounded-md border p-3">
<FormLabel className="text-sm font-medium">
Human Livability
</FormLabel>
<FormControl>
<Checkbox
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="isConfirmed"
render={({ field }) => (
<FormItem className="flex flex-row items-center justify-between rounded-md border p-3">
<FormLabel className="text-sm font-medium">
Is Confirmed
</FormLabel>
<FormControl>
<Checkbox
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<Separator />
{/* EXPIRY 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>
<Button type="submit" className="w-full">Add Build Address</Button>
</form>
</Form>
);
};
export { BuildPartsForm }

View File

@@ -0,0 +1,41 @@
'use client';
import { useState } from 'react';
import { BuildPartsDataTableAdd } from './table/data-table';
import { BuildPartsForm } from './form';
import { useGraphQlBuildPartsList } from '@/pages/build-parts/queries';
import { useRouter, useSearchParams } from 'next/navigation';
import { Button } from '@/components/ui/button';
const PageAddBuildParts = () => {
const [page, setPage] = useState(1);
const [limit, setLimit] = useState(10);
const [sort, setSort] = useState({ createdAt: 'desc' });
const [filters, setFilters] = useState({});
const searchParams = useSearchParams();
const router = useRouter();
const buildId = searchParams?.get('build');
const { data, isLoading, error, refetch } = useGraphQlBuildPartsList({ limit: 10, skip: 0, sort: { createdAt: -1 }, filters: { ...filters, buildId } });
const handlePageChange = (newPage: number) => { setPage(newPage) };
const handlePageSizeChange = (newSize: number) => { setLimit(newSize); setPage(1) };
const noUUIDFound = <>
<div>Back To Builds. No uuid is found on headers</div>
<Button onClick={() => router.push('/builds')}>Back to Builds</Button>
</>
if (!buildId) { return noUUIDFound }
return (
<>
<BuildPartsDataTableAdd
data={data?.data || []} totalCount={data?.totalCount || 0} currentPage={page} pageSize={limit} onPageChange={handlePageChange} onPageSizeChange={handlePageSizeChange} refetchTable={refetch} buildId={buildId}
/>
<BuildPartsForm refetchTable={refetch} selectedBuildId={buildId} />
</>
)
}
export { PageAddBuildParts };

View File

@@ -0,0 +1,24 @@
'use client'
import { useMutation } from '@tanstack/react-query'
import { toISOIfNotZ } from '@/lib/utils';
import { BuildPartsAdd } from './schema';
const fetchGraphQlBuildSitesAdd = async (record: BuildPartsAdd, buildId: string): Promise<{ data: BuildPartsAdd | null; status: number }> => {
console.log('Fetching test data from local API');
record.expiryStarts = record.expiryStarts ? toISOIfNotZ(record.expiryStarts) : undefined;
record.expiryEnds = record.expiryEnds ? toISOIfNotZ(record.expiryEnds) : undefined;
try {
const res = await fetch('/api/builds-parts/add', { method: 'POST', cache: 'no-store', credentials: "include", body: JSON.stringify({ data: record, buildId }) });
if (!res.ok) { const errorText = await res.text(); console.error('Test data API error:', errorText); throw new Error(`API error: ${res.status} ${res.statusText}`) }
const data = await res.json();
return { data: data.data, status: res.status }
} catch (error) { console.error('Error fetching test data:', error); throw error }
};
export function useAddBuildAreasMutation() {
return useMutation({
mutationFn: ({ data, buildId }: { data: BuildPartsAdd, buildId: string }) => fetchGraphQlBuildSitesAdd(data, buildId),
onSuccess: () => { console.log("Build areas created successfully") },
onError: (error) => { console.error("Add build areas failed:", error) },
})
}

View File

@@ -0,0 +1,23 @@
import { z } from "zod"
export const buildPartsAddSchema = z.object({
addressGovCode: z.string(),
no: z.number(),
level: z.number(),
code: z.string(),
grossSize: z.number(),
netSize: z.number(),
defaultAccessory: z.string(),
humanLivability: z.boolean(),
key: z.string(),
directionId: z.string().optional(),
typeId: z.string().optional(),
active: z.boolean(),
isConfirmed: z.boolean(),
expiryStarts: z.string().optional(),
expiryEnds: z.string().optional()
});
export type BuildPartsAdd = z.infer<typeof buildPartsAddSchema>;

View File

@@ -0,0 +1,115 @@
"use client"
import { z } from "zod"
import { Button } from "@/components/ui/button"
import { Drawer, DrawerClose, DrawerContent, DrawerFooter, DrawerHeader, DrawerTrigger } from "@/components/ui/drawer"
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { useSortable } from "@dnd-kit/sortable"
import { IconGripVertical } from "@tabler/icons-react"
import { useIsMobile } from "@/hooks/use-mobile"
import { Separator } from "@/components/ui/separator"
import { ColumnDef, flexRender, Row } from "@tanstack/react-table"
import { TableCell, TableRow } from "@/components/ui/table"
import { CSS } from "@dnd-kit/utilities"
import { schema, schemaType } from "./schema"
import { dateToLocaleString } from "@/lib/utils"
import { Pencil, Trash } from "lucide-react"
export function DraggableRow({ row }: { row: Row<z.infer<typeof schema>> }) {
const { transform, transition, setNodeRef, isDragging } = useSortable({ id: row.original._id })
return (
<TableRow data-state={row.getIsSelected() && "selected"} data-dragging={isDragging} ref={setNodeRef}
className="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(router: any, deleteHandler: (id: string) => void): ColumnDef<schemaType>[] {
return [
{
accessorKey: "addressGovCode",
header: "Address Gov Code",
},
{
accessorKey: "no",
header: "No",
},
{
accessorKey: "level",
header: "Level",
},
{
accessorKey: "code",
header: "Code",
},
{
accessorKey: "grossSize",
header: "Gross Size",
},
{
accessorKey: "netSize",
header: "Net Size",
},
{
accessorKey: "defaultAccessory",
header: "Default Accessory",
},
{
accessorKey: "humanLivability",
header: "Human Livability",
},
{
accessorKey: "key",
header: "Key",
},
{
accessorKey: "directionId",
header: "Direction ID",
},
{
accessorKey: "typeId",
header: "Type ID",
},
{
accessorKey: "createdAt",
header: "Created",
cell: ({ getValue }) => dateToLocaleString(getValue() as string),
},
{
accessorKey: "updatedAt",
header: "Updated",
cell: ({ getValue }) => dateToLocaleString(getValue() as string),
},
{
accessorKey: "expiryStarts",
header: "Expiry Starts",
cell: ({ getValue }) => getValue() ? dateToLocaleString(getValue() as string) : "-",
},
{
accessorKey: "expiryEnds",
header: "Expiry Ends",
cell: ({ getValue }) => getValue() ? dateToLocaleString(getValue() as string) : "-",
},
{
id: "actions",
header: "Actions",
cell: ({ row }) => {
return (
<div>
<Button className="bg-red-700 text-white border-red-700 mx-4" variant="outline" size="sm" onClick={() => { deleteHandler(row.original._id || "") }}>
<Trash />
</Button>
</div>
);
},
}
]
}
export { getColumns };

View File

@@ -0,0 +1,256 @@
"use client"
import * as React from "react"
import {
closestCenter,
DndContext,
KeyboardSensor,
MouseSensor,
TouchSensor,
useSensor,
useSensors,
type UniqueIdentifier,
} from "@dnd-kit/core"
import { restrictToVerticalAxis } from "@dnd-kit/modifiers"
import {
SortableContext,
verticalListSortingStrategy,
} from "@dnd-kit/sortable"
import {
IconChevronDown,
IconChevronLeft,
IconChevronRight,
IconChevronsLeft,
IconChevronsRight,
IconLayoutColumns,
} from "@tabler/icons-react"
import {
ColumnFiltersState,
flexRender,
getCoreRowModel,
getFacetedRowModel,
getFacetedUniqueValues,
getFilteredRowModel,
getSortedRowModel,
SortingState,
useReactTable,
VisibilityState,
} from "@tanstack/react-table"
import { Button } from "@/components/ui/button"
import { DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
import { Label } from "@/components/ui/label"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
import { Tabs, TabsContent } from "@/components/ui/tabs"
import { schemaType } from "./schema"
import { getColumns, DraggableRow } from "./columns"
import { useRouter } from "next/navigation"
import { Home } from "lucide-react"
import { useDeleteBuildPartMutation } from "@/pages/build-parts/queries"
export function BuildPartsDataTableAdd({
data,
totalCount,
currentPage,
pageSize,
onPageChange,
onPageSizeChange,
refetchTable,
buildId
}: {
data: schemaType[],
totalCount: number,
currentPage: number,
pageSize: number,
onPageChange: (page: number) => void,
onPageSizeChange: (size: number) => void,
refetchTable: () => void,
buildId: string
}) {
const router = useRouter();
const [rowSelection, setRowSelection] = React.useState({})
const [columnVisibility, setColumnVisibility] = React.useState<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 deleteMutation = useDeleteBuildPartMutation()
const deleteHandler = (id: string) => { deleteMutation.mutate({ uuid: id }); setTimeout(() => { refetchTable() }, 400) }
const columns = getColumns(router, deleteHandler);
const pagination = React.useMemo(() => ({ pageIndex: currentPage - 1, pageSize: pageSize, }), [currentPage, pageSize])
const totalPages = Math.ceil(totalCount / pageSize)
const table = useReactTable({
data,
columns,
pageCount: totalPages,
state: { sorting, columnVisibility, rowSelection, columnFilters, pagination },
manualPagination: true,
enableRowSelection: true,
getRowId: (row) => row._id.toString(),
onRowSelectionChange: setRowSelection,
onSortingChange: setSorting,
onColumnFiltersChange: setColumnFilters,
onColumnVisibilityChange: setColumnVisibility,
onPaginationChange: (updater) => {
const nextPagination = typeof updater === "function" ? updater(pagination) : updater;
onPageChange(nextPagination.pageIndex + 1); onPageSizeChange(nextPagination.pageSize);
},
getCoreRowModel: getCoreRowModel(),
getFilteredRowModel: getFilteredRowModel(),
getSortedRowModel: getSortedRowModel(),
getFacetedRowModel: getFacetedRowModel(),
getFacetedUniqueValues: getFacetedUniqueValues(),
})
const handlePageSizeChange = (value: string) => { const newSize = Number(value); onPageSizeChange(newSize); onPageChange(1) }
return (
<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>
<Button variant="outline" size="sm" onClick={() => { router.push(`/build-parts?build=${buildId}`) }}>
<Home />
<span className="hidden lg:inline">Back to Build Parts</span>
</Button>
</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 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,28 @@
import { z } from "zod";
export const schema = z.object({
_id: z.string(),
uuid: z.string(),
buildId: z.string(),
addressGovCode: z.string(),
no: z.number(),
level: z.number(),
code: z.string(),
grossSize: z.number(),
netSize: z.number(),
defaultAccessory: z.string(),
humanLivability: z.boolean(),
key: z.string(),
directionId: z.string().nullable().optional(),
typeId: z.string().nullable().optional(),
createdAt: z.string(),
updatedAt: z.string(),
expiryStarts: z.string(),
expiryEnds: z.string(),
active: z.boolean(),
isConfirmed: z.boolean(),
});
export type schemaType = z.infer<typeof schema>;

View File

@@ -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 };

View File

@@ -0,0 +1,111 @@
"use client"
import { z } from "zod"
import { Button } from "@/components/ui/button"
import { useSortable } from "@dnd-kit/sortable"
import { ColumnDef, flexRender, Row } from "@tanstack/react-table"
import { TableCell, TableRow } from "@/components/ui/table"
import { CSS } from "@dnd-kit/utilities"
import { schema, schemaType } from "./schema"
import { dateToLocaleString } from "@/lib/utils"
import { Pencil, Trash } from "lucide-react"
export function DraggableRow({ row }: { row: Row<z.infer<typeof schema>> }) {
const { transform, transition, setNodeRef, isDragging } = useSortable({ id: row.original._id })
return (
<TableRow data-state={row.getIsSelected() && "selected"} data-dragging={isDragging} ref={setNodeRef}
className="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(router: any, deleteHandler: (id: string) => void, buildId: string): ColumnDef<schemaType>[] {
return [
{
accessorKey: "addressGovCode",
header: "Address Gov Code",
},
{
accessorKey: "no",
header: "No",
},
{
accessorKey: "level",
header: "Level",
},
{
accessorKey: "code",
header: "Code",
},
{
accessorKey: "grossSize",
header: "Gross Size",
},
{
accessorKey: "netSize",
header: "Net Size",
},
{
accessorKey: "defaultAccessory",
header: "Default Accessory",
},
{
accessorKey: "humanLivability",
header: "Human Livability",
},
{
accessorKey: "key",
header: "Key",
},
{
accessorKey: "directionId",
header: "Direction ID",
},
{
accessorKey: "typeId",
header: "Type ID",
},
{
accessorKey: "createdAt",
header: "Created",
cell: ({ getValue }) => dateToLocaleString(getValue() as string),
},
{
accessorKey: "updatedAt",
header: "Updated",
cell: ({ getValue }) => dateToLocaleString(getValue() as string),
},
{
accessorKey: "expiryStarts",
header: "Expiry Starts",
cell: ({ getValue }) => getValue() ? dateToLocaleString(getValue() as string) : "-",
},
{
accessorKey: "expiryEnds",
header: "Expiry Ends",
cell: ({ getValue }) => getValue() ? dateToLocaleString(getValue() as string) : "-",
},
{
id: "actions",
header: "Actions",
cell: ({ row }) => {
return (
<div>
<Button className="bg-amber-400 text-black border-amber-400" variant="outline" size="sm" onClick={() => { router.push(`/build-parts/update?build=${buildId}&uuid=${row.original._id}`) }}>
<Pencil />
</Button>
<Button className="bg-red-700 text-white border-red-700 mx-4" variant="outline" size="sm" onClick={() => { deleteHandler(row.original._id || "") }}>
<Trash />
</Button>
</div>
);
},
}
]
}
export { getColumns };

View File

@@ -0,0 +1,277 @@
"use client"
import * as React from "react"
import {
closestCenter,
DndContext,
KeyboardSensor,
MouseSensor,
TouchSensor,
useSensor,
useSensors,
type UniqueIdentifier,
} from "@dnd-kit/core"
import { restrictToVerticalAxis } from "@dnd-kit/modifiers"
import {
SortableContext,
verticalListSortingStrategy,
} from "@dnd-kit/sortable"
import {
IconChevronDown,
IconChevronLeft,
IconChevronRight,
IconChevronsLeft,
IconChevronsRight,
IconLayoutColumns,
IconPlus,
} from "@tabler/icons-react"
import {
ColumnFiltersState,
flexRender,
getCoreRowModel,
getFacetedRowModel,
getFacetedUniqueValues,
getFilteredRowModel,
getSortedRowModel,
SortingState,
useReactTable,
VisibilityState,
} from "@tanstack/react-table"
import { Button } from "@/components/ui/button"
import {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import { Label } from "@/components/ui/label"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table"
import {
Tabs,
TabsContent,
TabsList,
TabsTrigger,
} from "@/components/ui/tabs"
import { schemaType } from "./schema"
import { getColumns, DraggableRow } from "./columns"
import { useRouter } from "next/navigation"
import { useDeleteBuildPartMutation } from "@/pages/build-parts/queries"
import { Home } from "lucide-react"
export function BuildPartsDataTable({
data,
totalCount,
currentPage = 1,
pageSize = 10,
onPageChange,
onPageSizeChange,
refetchTable,
buildId
}: {
data: schemaType[],
totalCount: number,
currentPage?: number,
pageSize?: number,
onPageChange: (page: number) => void,
onPageSizeChange: (size: number) => void,
refetchTable: () => void,
buildId: string,
}) {
const router = useRouter();
const [rowSelection, setRowSelection] = React.useState({})
const [columnVisibility, setColumnVisibility] = React.useState<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 deleteMutation = useDeleteBuildPartMutation()
const deleteHandler = (id: string) => { deleteMutation.mutate({ uuid: id }); setTimeout(() => { refetchTable() }, 400) }
const columns = getColumns(router, deleteHandler, buildId);
const pagination = React.useMemo(() => ({ pageIndex: currentPage - 1, pageSize: pageSize }), [currentPage, pageSize])
const totalPages = Math.ceil(totalCount / pageSize)
const table = useReactTable({
data,
columns,
pageCount: totalPages,
state: { sorting, columnVisibility, rowSelection, columnFilters, pagination },
manualPagination: true,
getRowId: (row) => row._id.toString(),
enableRowSelection: true,
onRowSelectionChange: setRowSelection,
onSortingChange: setSorting,
onColumnFiltersChange: setColumnFilters,
onColumnVisibilityChange: setColumnVisibility,
onPaginationChange: (updater) => { const nextPagination = typeof updater === "function" ? updater(pagination) : updater; onPageChange(nextPagination.pageIndex + 1); onPageSizeChange(nextPagination.pageSize) },
getCoreRowModel: getCoreRowModel(),
getFilteredRowModel: getFilteredRowModel(),
getSortedRowModel: getSortedRowModel(),
getFacetedRowModel: getFacetedRowModel(),
getFacetedUniqueValues: getFacetedUniqueValues(),
})
const handlePageSizeChange = (value: string) => { const newSize = Number(value); onPageSizeChange(newSize); onPageChange(1) }
return (
<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>
<Button variant="outline" size="sm" onClick={() => { router.push(`/build-parts/add?build=${buildId}`) }}>
<IconPlus />
<span className="hidden lg:inline">Add Build Part</span>
</Button>
<Button variant="outline" size="sm" onClick={() => { router.push(`/builds`) }}>
<Home />
<span className="hidden lg:inline">Back to Builds</span>
</Button>
</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 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,28 @@
import { z } from "zod";
export const schema = z.object({
_id: z.string(),
uuid: z.string(),
buildId: z.string(),
addressGovCode: z.string(),
no: z.number(),
level: z.number(),
code: z.string(),
grossSize: z.number(),
netSize: z.number(),
defaultAccessory: z.string(),
humanLivability: z.boolean(),
key: z.string(),
directionId: z.string().nullable().optional(),
typeId: z.string().nullable().optional(),
createdAt: z.string(),
updatedAt: z.string(),
expiryStarts: z.string(),
expiryEnds: z.string(),
active: z.boolean(),
isConfirmed: z.boolean(),
});
export type schemaType = z.infer<typeof schema>;

View File

@@ -0,0 +1,32 @@
'use client';
import { useState } from "react";
import { Button } from "@/components/ui/button";
import { useSearchParams, useRouter } from "next/navigation";
import { useGraphQlBuildPartsList } from "./queries";
import { BuildPartsDataTable } from "./list/data-table";
const PageBuildPartsToBuild = () => {
const [page, setPage] = useState(1);
const [limit, setLimit] = useState(10);
const [sort, setSort] = useState({ createdAt: 'desc' });
const [filters, setFilters] = useState({});
const searchParams = useSearchParams();
const router = useRouter();
const buildId = searchParams?.get('build');
const { data, isLoading, error, refetch } = useGraphQlBuildPartsList({ limit: 10, skip: 0, sort: { createdAt: -1 }, filters: { ...filters, buildId } });
const handlePageChange = (newPage: number) => { setPage(newPage) };
const handlePageSizeChange = (newSize: number) => { setLimit(newSize); setPage(1) };
const noUUIDFound = <>
<div>Back To Builds. No uuid is found on headers</div>
<Button onClick={() => router.push('/builds')}>Back to Builds</Button>
</>
if (!buildId) { return noUUIDFound }
return <><BuildPartsDataTable data={data?.data || []} totalCount={data?.totalCount || 0} currentPage={page} pageSize={limit} onPageChange={handlePageChange} onPageSizeChange={handlePageSizeChange} refetchTable={refetch} buildId={buildId} /></>
}
export default PageBuildPartsToBuild;

View File

@@ -0,0 +1,36 @@
'use client'
import { useQuery, useMutation } from '@tanstack/react-query'
import { ListArguments } from '@/types/listRequest'
const fetchGraphQlBuildPartsList = 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-parts/list', { method: 'POST', cache: 'no-store', credentials: "include", body: JSON.stringify({ limit, skip, sort, filters }) });
if (!res.ok) { const errorText = await res.text(); console.error('Test data API error:', errorText); throw new Error(`API error: ${res.status} ${res.statusText}`) }
const data = await res.json();
return { data: data.data, totalCount: data.totalCount }
} catch (error) { console.error('Error fetching test data:', error); throw error }
};
const fetchGraphQlDeleteBuildPart = async (uuid: string): Promise<boolean> => {
console.log('Fetching test data from local API');
try {
const res = await fetch(`/api/builds-parts/delete?uuid=${uuid}`, { method: 'GET', cache: 'no-store', credentials: "include" });
if (!res.ok) { const errorText = await res.text(); console.error('Test data API error:', errorText); throw new Error(`API error: ${res.status} ${res.statusText}`) }
const data = await res.json();
return data
} catch (error) { console.error('Error fetching test data:', error); throw error }
};
export function useGraphQlBuildPartsList(params: ListArguments) {
return useQuery({ queryKey: ['graphql-build-parts-list', params], queryFn: () => fetchGraphQlBuildPartsList(params) })
}
export function useDeleteBuildPartMutation() {
return useMutation({
mutationFn: ({ uuid }: { uuid: string }) => fetchGraphQlDeleteBuildPart(uuid),
onSuccess: () => { console.log("Build part deleted successfully") },
onError: (error) => { console.error("Delete build part failed:", error) },
})
}

View File

@@ -0,0 +1,299 @@
"use client"
import { useForm } from "react-hook-form"
import { zodResolver } from "@hookform/resolvers/zod"
import { Form, FormField, FormItem, FormLabel, FormControl, FormMessage } from "@/components/ui/form"
import { Input } from "@/components/ui/input"
import { Button } from "@/components/ui/button"
import { Separator } from "@/components/ui/separator"
import { DateTimePicker } from "@/components/ui/date-time-picker"
import { BuildPartsUpdate, buildPartsUpdateSchema } from "@/pages/build-parts/update/schema"
import { Checkbox } from "@/components/ui/checkbox"
import { useUpdateBuildPartsMutation } from "@/pages/build-parts/update/queries"
const BuildPartsForm = ({ refetchTable, initData, selectedUuid, buildId }: { refetchTable: () => void, initData: BuildPartsUpdate, selectedUuid: string, buildId: string }) => {
const form = useForm<BuildPartsUpdate>({
resolver: zodResolver(buildPartsUpdateSchema), defaultValues: { ...initData, directionId: initData.directionId ?? '', typeId: initData.typeId ?? '' }
})
const { handleSubmit } = form
const mutation = useUpdateBuildPartsMutation();
function onSubmit(values: BuildPartsUpdate) { mutation.mutate({ data: values as any || initData, uuid: selectedUuid, buildId, refetchTable }) }
return (
<Form {...form}>
<form onSubmit={handleSubmit(onSubmit)} className="space-y-6 p-4">
{/* ROW 1 */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<FormField
control={form.control}
name="addressGovCode"
render={({ field }) => (
<FormItem>
<FormLabel>Address Gov Code</FormLabel>
<FormControl>
<Input placeholder="TR-34-XXX-XXXX" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="no"
render={({ field }) => (
<FormItem>
<FormLabel>No</FormLabel>
<FormControl>
<Input
type="number"
placeholder="Area No"
{...field}
onChange={(e) => field.onChange(e.target.value === "" ? undefined : Number(e.target.value))}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
{/* ROW 2 */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<FormField
control={form.control}
name="level"
render={({ field }) => (
<FormItem>
<FormLabel>Level</FormLabel>
<FormControl>
<Input
type="number"
placeholder="Floor / Level"
{...field}
onChange={(e) => field.onChange(e.target.value === "" ? undefined : Number(e.target.value))}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="code"
render={({ field }) => (
<FormItem>
<FormLabel>Code</FormLabel>
<FormControl>
<Input placeholder="Part Code (e.g. A5)" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
{/* ROW 3 */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<FormField
control={form.control}
name="key"
render={({ field }) => (
<FormItem>
<FormLabel>Key</FormLabel>
<FormControl>
<Input placeholder="Unique Key (e.g. BLK-A5-L5-N3)" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="grossSize"
render={({ field }) => (
<FormItem>
<FormLabel>Gross Size</FormLabel>
<FormControl>
<Input
type="number"
placeholder="Gross Size (m²)"
{...field}
onBlur={(e) => field.onChange(e.target.value === "" ? undefined : Number(e.target.value))}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
{/* ROW 4 */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<FormField
control={form.control}
name="netSize"
render={({ field }) => (
<FormItem>
<FormLabel>Net Size</FormLabel>
<FormControl>
<Input
type="number"
placeholder="Net Size (m²)"
{...field}
onBlur={(e) => field.onChange(e.target.value === "" ? undefined : Number(e.target.value))}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="defaultAccessory"
render={({ field }) => (
<FormItem>
<FormLabel>Default Accessory</FormLabel>
<FormControl>
<Input placeholder="e.g. balcony, terrace..." {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
{/* ROW 5 */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<FormField
control={form.control}
name="directionId"
render={({ field }) => (
<FormItem>
<FormLabel>Direction Id</FormLabel>
<FormControl>
<Input placeholder="Direction ObjectId (optional)" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="typeId"
render={({ field }) => (
<FormItem>
<FormLabel>Type Id</FormLabel>
<FormControl>
<Input placeholder="Type ObjectId (optional)" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
{/* BOOLEAN ROW */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
{/* boşluk kalmasın diye aktif alanını buraya koydum */}
<FormField
control={form.control}
name="active"
render={({ field }) => (
<FormItem className="flex flex-row items-center justify-between rounded-md border p-3">
<FormLabel className="text-sm font-medium">
Active
</FormLabel>
<FormControl>
<Checkbox
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="humanLivability"
render={({ field }) => (
<FormItem className="flex flex-row items-center justify-between rounded-md border p-3">
<FormLabel className="text-sm font-medium">
Human Livability
</FormLabel>
<FormControl>
<Checkbox
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="isConfirmed"
render={({ field }) => (
<FormItem className="flex flex-row items-center justify-between rounded-md border p-3">
<FormLabel className="text-sm font-medium">
Is Confirmed
</FormLabel>
<FormControl>
<Checkbox
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<Separator />
{/* EXPIRY 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>
<Button type="submit" className="w-full">Update Build Address</Button>
</form>
</Form>
);
}
export { BuildPartsForm }

View File

@@ -0,0 +1,36 @@
'use client';
import { useState } from 'react';
import { BuildPartsForm } from '@/pages/build-parts/update/form';
import { useSearchParams, useRouter } from 'next/navigation'
import { Button } from '@/components/ui/button';
import { BuildPartsDataTableUpdate } from './table/data-table';
import { useGraphQlBuildPartsList } from '../queries';
const PageUpdateBuildParts = () => {
const [page, setPage] = useState(1);
const [limit, setLimit] = useState(10);
const [sort, setSort] = useState({ createdAt: 'desc' });
const [filters, setFilters] = useState({});
const searchParams = useSearchParams()
const router = useRouter()
const uuid = searchParams?.get('uuid') || null
const buildId = searchParams?.get('build') || null
const backToBuildAddress = <>
<div>UUID not found in search params</div>
<Button onClick={() => router.push('/build-parts')}>Back to Build IBANs</Button>
</>
if (!uuid || !buildId) { return backToBuildAddress }
const { data, isLoading, error, refetch } = useGraphQlBuildPartsList({ limit, skip: (page - 1) * limit, sort, filters: { ...filters, _id: uuid } });
const initData = data?.data?.[0] || null;
if (!initData) { return backToBuildAddress }
return (
<>
<BuildPartsDataTableUpdate
data={data?.data || []} totalCount={data?.totalCount || 0} currentPage={page} pageSize={limit} onPageChange={setPage} onPageSizeChange={setLimit} refetchTable={refetch} buildId={buildId}
/>
<BuildPartsForm refetchTable={refetch} initData={initData} selectedUuid={uuid} buildId={buildId} />
</>
)
}
export { PageUpdateBuildParts };

View File

@@ -0,0 +1,25 @@
'use client'
import { useMutation } from '@tanstack/react-query'
import { UpdateBuildPartsUpdate } from './types';
import { toISOIfNotZ } from '@/lib/utils';
const fetchGraphQlBuildPartsUpdate = async (record: UpdateBuildPartsUpdate, uuid: string, buildId: string, refetchTable: () => void): Promise<{ data: UpdateBuildPartsUpdate | null; status: number }> => {
console.log('Fetching test data from local API');
record.expiryStarts = record.expiryStarts ? toISOIfNotZ(record.expiryStarts) : undefined;
record.expiryEnds = record.expiryEnds ? toISOIfNotZ(record.expiryEnds) : undefined;
try {
const res = await fetch(`/api/builds-parts/update?uuid=${uuid || ''}`, { method: 'POST', cache: 'no-store', credentials: "include", body: JSON.stringify({ ...record, buildId }) });
if (!res.ok) { const errorText = await res.text(); console.error('Test data API error:', errorText); throw new Error(`API error: ${res.status} ${res.statusText}`) }
const data = await res.json();
refetchTable();
return { data: data.data, status: res.status }
} catch (error) { console.error('Error fetching test data:', error); throw error }
};
export function useUpdateBuildPartsMutation() {
return useMutation({
mutationFn: ({ data, uuid, buildId, refetchTable }: { data: UpdateBuildPartsUpdate, uuid: string, buildId: string, refetchTable: () => void }) => fetchGraphQlBuildPartsUpdate(data, uuid, buildId, refetchTable),
onSuccess: () => { console.log("Build Parts updated successfully") },
onError: (error) => { console.error("Update Build Parts failed:", error) },
})
}

View File

@@ -0,0 +1,23 @@
import { z } from "zod"
export const buildPartsUpdateSchema = z.object({
addressGovCode: z.string(),
no: z.number(),
level: z.number(),
code: z.string(),
grossSize: z.number(),
netSize: z.number(),
defaultAccessory: z.string(),
humanLivability: z.boolean(),
key: z.string(),
directionId: z.string().optional(),
typeId: z.string().optional(),
active: z.boolean(),
isConfirmed: z.boolean(),
expiryStarts: z.string().optional(),
expiryEnds: z.string().optional()
});
export type BuildPartsUpdate = z.infer<typeof buildPartsUpdateSchema>;

View File

@@ -0,0 +1,113 @@
"use client"
import { z } from "zod"
import { Button } from "@/components/ui/button"
import { Drawer, DrawerClose, DrawerContent, DrawerFooter, DrawerHeader, DrawerTrigger } from "@/components/ui/drawer"
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { useSortable } from "@dnd-kit/sortable"
import { IconGripVertical } from "@tabler/icons-react"
import { useIsMobile } from "@/hooks/use-mobile"
import { Separator } from "@/components/ui/separator"
import { ColumnDef, flexRender, Row } from "@tanstack/react-table"
import { TableCell, TableRow } from "@/components/ui/table"
import { CSS } from "@dnd-kit/utilities"
import { schema, schemaType } from "./schema"
import { dateToLocaleString } from "@/lib/utils"
import { Pencil, Trash } from "lucide-react"
export function DraggableRow({ row }: { row: Row<z.infer<typeof schema>> }) {
const { transform, transition, setNodeRef, isDragging } = useSortable({ id: row.original._id })
return (
<TableRow data-state={row.getIsSelected() && "selected"} data-dragging={isDragging} ref={setNodeRef}
className="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(deleteHandler: (id: string) => void): ColumnDef<schemaType>[] {
return [
{
accessorKey: "addressGovCode",
header: "Address Gov Code",
},
{
accessorKey: "no",
header: "No",
},
{
accessorKey: "level",
header: "Level",
},
{
accessorKey: "code",
header: "Code",
},
{
accessorKey: "grossSize",
header: "Gross Size",
},
{
accessorKey: "netSize",
header: "Net Size",
},
{
accessorKey: "defaultAccessory",
header: "Default Accessory",
},
{
accessorKey: "humanLivability",
header: "Human Livability",
},
{
accessorKey: "key",
header: "Key",
},
{
accessorKey: "directionId",
header: "Direction ID",
},
{
accessorKey: "typeId",
header: "Type ID",
},
{
accessorKey: "createdAt",
header: "Created",
cell: ({ getValue }) => dateToLocaleString(getValue() as string),
},
{
accessorKey: "updatedAt",
header: "Updated",
cell: ({ getValue }) => dateToLocaleString(getValue() as string),
},
{
accessorKey: "expiryStarts",
header: "Expiry Starts",
cell: ({ getValue }) => getValue() ? dateToLocaleString(getValue() as string) : "-",
},
{
accessorKey: "expiryEnds",
header: "Expiry Ends",
cell: ({ getValue }) => getValue() ? dateToLocaleString(getValue() as string) : "-",
},
{
id: "actions",
header: "Actions",
cell: ({ row }) => {
return (
<div>
<Button className="bg-red-700 text-white border-red-700 mx-4" variant="outline" size="sm" onClick={() => { deleteHandler(row.original._id || "") }}>
<Trash />
</Button>
</div>
);
},
}
]
}
export { getColumns };

View File

@@ -0,0 +1,281 @@
"use client"
import * as React from "react"
import {
closestCenter,
DndContext,
KeyboardSensor,
MouseSensor,
TouchSensor,
useSensor,
useSensors,
type UniqueIdentifier,
} from "@dnd-kit/core"
import { restrictToVerticalAxis } from "@dnd-kit/modifiers"
import {
SortableContext,
verticalListSortingStrategy,
} from "@dnd-kit/sortable"
import {
IconChevronDown,
IconChevronLeft,
IconChevronRight,
IconChevronsLeft,
IconChevronsRight,
IconLayoutColumns,
} from "@tabler/icons-react"
import {
ColumnFiltersState,
flexRender,
getCoreRowModel,
getFacetedRowModel,
getFacetedUniqueValues,
getFilteredRowModel,
getSortedRowModel,
SortingState,
useReactTable,
VisibilityState,
} from "@tanstack/react-table"
import { Button } from "@/components/ui/button"
import {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import { Label } from "@/components/ui/label"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table"
import {
Tabs,
TabsContent,
TabsList,
TabsTrigger,
} from "@/components/ui/tabs"
import { schemaType } from "./schema"
import { getColumns, DraggableRow } from "./columns"
import { useRouter } from "next/navigation"
import { Home } from "lucide-react"
import { useDeletePersonMutation } from "@/pages/people/queries"
export function BuildPartsDataTableUpdate({
data,
totalCount,
currentPage,
pageSize,
onPageChange,
onPageSizeChange,
refetchTable,
buildId,
}: {
data: schemaType[],
totalCount: number,
currentPage: number,
pageSize: number,
onPageChange: (page: number) => void,
onPageSizeChange: (size: number) => void,
refetchTable: () => void,
buildId: string
}) {
const router = useRouter();
const [rowSelection, setRowSelection] = React.useState({})
const [columnVisibility, setColumnVisibility] = React.useState<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 deleteMutation = useDeletePersonMutation()
const deleteHandler = (id: string) => { deleteMutation.mutate({ uuid: id }); setTimeout(() => { refetchTable() }, 200) }
const columns = getColumns(deleteHandler);
const pagination = React.useMemo(() => ({ pageIndex: currentPage - 1, pageSize: pageSize, }), [currentPage, pageSize])
const totalPages = Math.ceil(totalCount / pageSize)
const table = useReactTable({
data,
columns,
pageCount: totalPages,
state: { sorting, columnVisibility, rowSelection, columnFilters, pagination },
manualPagination: true,
enableRowSelection: true,
getRowId: (row) => row._id.toString(),
onRowSelectionChange: setRowSelection,
onSortingChange: setSorting,
onColumnFiltersChange: setColumnFilters,
onColumnVisibilityChange: setColumnVisibility,
onPaginationChange: (updater) => {
const nextPagination = typeof updater === "function" ? updater(pagination) : updater;
onPageChange(nextPagination.pageIndex + 1); onPageSizeChange(nextPagination.pageSize);
},
getCoreRowModel: getCoreRowModel(),
getFilteredRowModel: getFilteredRowModel(),
getSortedRowModel: getSortedRowModel(),
getFacetedRowModel: getFacetedRowModel(),
getFacetedUniqueValues: getFacetedUniqueValues(),
})
const handlePageSizeChange = (value: string) => { const newSize = Number(value); onPageSizeChange(newSize); onPageChange(1) }
return (
<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>
<Button variant="outline" size="sm" onClick={() => { router.push(`/build-parts?build=${buildId}`) }}>
<Home />
<span className="hidden lg:inline">Back to Build Parts</span>
</Button>
</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 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,26 @@
import { z } from "zod";
export const schema = z.object({
_id: z.string(),
uuid: z.string(),
buildId: z.string(),
addressGovCode: z.string(),
no: z.number(),
level: z.number(),
code: z.string(),
grossSize: z.number(),
netSize: z.number(),
defaultAccessory: z.string(),
humanLivability: z.boolean(),
key: z.string(),
directionId: z.string().optional(),
typeId: z.string().optional(),
createdAt: z.string(),
updatedAt: z.string(),
expiryStarts: z.string(),
expiryEnds: z.string(),
active: z.boolean(),
isConfirmed: z.boolean(),
});
export type schemaType = z.infer<typeof schema>;

View File

@@ -0,0 +1,22 @@
interface UpdateBuildPartsUpdate {
addressGovCode: string;
no: number;
level: number;
code: string;
key: string;
grossSize: number;
netSize: number;
defaultAccessory: string;
directionId: string;
typeId: string;
buildId: string;
expiryStarts?: string;
expiryEnds?: string;
active: boolean;
isConfirmed: boolean;
}
export type { UpdateBuildPartsUpdate };

View File

@@ -3,6 +3,8 @@ import { useState } from 'react';
import { BuildSitesForm } from '@/pages/build-sites/add/form';
import { BuildSitesDataTableAdd } from './table/data-table';
import { useGraphQlBuildSitesList } from '@/pages/build-sites/queries';
import { useSearchParams, useRouter } from 'next/navigation';
import { Button } from '@/components/ui/button';
const PageAddBuildSites = () => {
const [page, setPage] = useState(1);
@@ -10,7 +12,12 @@ const PageAddBuildSites = () => {
const [sort, setSort] = useState({ createdAt: 'desc' });
const [filters, setFilters] = useState({});
const { data, isLoading, error, refetch } = useGraphQlBuildSitesList({ limit, skip: (page - 1) * limit, sort, filters });
const searchParams = useSearchParams();
const router = useRouter();
const buildId = searchParams?.get('build');
const noUUIDFound = <><div>Back To Builds. No uuid is found on headers</div><Button onClick={() => router.push('/builds')}>Back to Builds</Button></>
if (!buildId) { return noUUIDFound };
const { data, isLoading, error, refetch } = useGraphQlBuildSitesList({ limit, skip: (page - 1) * limit, sort, filters: { ...filters, buildID: buildId } });
return (
<>

View File

@@ -78,7 +78,8 @@ export function BuildSitesDataTable({
pageSize = 10,
onPageChange,
onPageSizeChange,
refetchTable
refetchTable,
buildId
}: {
data: schemaType[],
totalCount: number,
@@ -87,6 +88,7 @@ export function BuildSitesDataTable({
onPageChange: (page: number) => void,
onPageSizeChange: (size: number) => void,
refetchTable: () => void,
buildId?: string
}) {
const router = useRouter();
@@ -163,7 +165,7 @@ export function BuildSitesDataTable({
})}
</DropdownMenuContent>
</DropdownMenu>
<Button variant="outline" size="sm" onClick={() => { router.push("/build-sites/add") }}>
<Button variant="outline" size="sm" onClick={() => { router.push(`/build-sites/add`) }}>
<IconPlus />
<span className="hidden lg:inline">Add Build Sites</span>
</Button>

View File

@@ -2,6 +2,8 @@
import { BuildSitesDataTable } from './list/data-table';
import { useGraphQlBuildSitesList } from './queries';
import { useState } from 'react';
import { useRouter, useSearchParams } from 'next/navigation';
import { Button } from '@/components/ui/button';
const PageBuildSites = () => {
@@ -10,14 +12,16 @@ const PageBuildSites = () => {
const [sort, setSort] = useState({ createdAt: 'desc' });
const [filters, setFilters] = useState({});
const { data, isLoading, error, refetch } = useGraphQlBuildSitesList({ limit, skip: (page - 1) * limit, sort, filters });
const searchParams = useSearchParams();
const router = useRouter();
const noUUIDFound = <><div>Back To Builds. No uuid is found on headers</div><Button onClick={() => router.push('/builds')}>Back to Builds</Button></>
const handlePageChange = (newPage: number) => { setPage(newPage) };
const handlePageSizeChange = (newSize: number) => { setLimit(newSize); setPage(1) };
const { data, isLoading, error, refetch } = useGraphQlBuildSitesList({ limit, skip: (page - 1) * limit, sort, filters: { ...filters } });
if (isLoading) { return <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 build sites</div> }
return <BuildSitesDataTable data={data?.data || []} totalCount={data?.totalCount || 0} currentPage={page} pageSize={limit} onPageChange={handlePageChange} onPageSizeChange={handlePageSizeChange} refetchTable={refetch} />;
};

View File

@@ -7,17 +7,17 @@ import { Button } from "@/components/ui/button"
import { Separator } from "@/components/ui/separator"
import { DateTimePicker } from "@/components/ui/date-time-picker"
import { useUpdateBuildSitesMutation } from "@/pages/build-sites/update/queries"
import { BuildAddressUpdate, buildAddressUpdateSchema } from "@/pages/build-sites/update/schema"
import { BuildSitesUpdate, buildSitesUpdateSchema } from "@/pages/build-sites/update/schema"
const BuildAddressForm = ({ refetchTable, initData, selectedUuid }: { refetchTable: () => void, initData: BuildAddressUpdate, selectedUuid: string }) => {
const BuildAddressForm = ({ refetchTable, initData, selectedUuid }: { refetchTable: () => void, initData: BuildSitesUpdate, selectedUuid: string }) => {
const form = useForm<BuildAddressUpdate>({ resolver: zodResolver(buildAddressUpdateSchema), defaultValues: { ...initData } })
const form = useForm<BuildSitesUpdate>({ resolver: zodResolver(buildSitesUpdateSchema), defaultValues: { ...initData } })
const { handleSubmit } = form
const mutation = useUpdateBuildSitesMutation();
function onSubmit(values: BuildAddressUpdate) { mutation.mutate({ data: values as any || initData, uuid: selectedUuid }); setTimeout(() => refetchTable(), 400) }
function onSubmit(values: BuildSitesUpdate) { mutation.mutate({ data: values as any || initData, uuid: selectedUuid }); setTimeout(() => refetchTable(), 400) }
return (
<Form {...form}>
@@ -25,7 +25,6 @@ const BuildAddressForm = ({ refetchTable, initData, selectedUuid }: { refetchTab
{/* ROW 1 */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<FormField
control={form.control}
name="siteNo"

View File

@@ -11,22 +11,18 @@ const PageUpdateBuildSites = () => {
const [limit, setLimit] = useState(10);
const [sort, setSort] = useState({ createdAt: 'desc' });
const [filters, setFilters] = useState({});
const searchParams = useSearchParams()
const router = useRouter()
const searchParams = useSearchParams();
const router = useRouter();
const uuid = searchParams?.get('uuid') || null
const backToBuildAddress = <>
<div>UUID not found in search params</div>
<Button onClick={() => router.push('/build-sites')}>Back to Build Sites</Button>
</>
if (!uuid) { return backToBuildAddress }
const backToBuildSites = <><div>UUID not found in search params</div><Button onClick={() => router.push(`/build-sites`)}>Back to Build Sites</Button></>
if (!uuid) { return backToBuildSites }
const { data, isLoading, error, refetch } = useGraphQlBuildSitesList({ limit, skip: (page - 1) * limit, sort, filters: { ...filters, uuid } });
const initData = data?.data?.[0] || null;
if (!initData) { return backToBuildAddress }
if (!initData) { return backToBuildSites }
return (
<>
<BuildSitesDataTableUpdate
data={data?.data || []} totalCount={data?.totalCount || 0} currentPage={page} pageSize={limit}
onPageChange={setPage} onPageSizeChange={setLimit} refetchTable={refetch}
data={data?.data || []} totalCount={data?.totalCount || 0} currentPage={page} pageSize={limit} onPageChange={setPage} onPageSizeChange={setLimit} refetchTable={refetch}
/>
<BuildAddressForm refetchTable={refetch} initData={initData} selectedUuid={uuid} />
</>

View File

@@ -8,7 +8,7 @@ const fetchGraphQlBuildAddressUpdate = async (record: UpdateBuildSitesUpdate, uu
record.expiryStarts = record.expiryStarts ? toISOIfNotZ(record.expiryStarts) : undefined;
record.expiryEnds = record.expiryEnds ? toISOIfNotZ(record.expiryEnds) : undefined;
try {
const res = await fetch(`/api/build-sites/update?uuid=${uuid || ''}`, { method: 'POST', cache: 'no-store', credentials: "include", body: JSON.stringify(record) });
const res = await fetch(`/api/build-sites/update?uuid=${uuid || ''}`, { method: 'POST', cache: 'no-store', credentials: "include", body: JSON.stringify({ ...record }) });
if (!res.ok) { const errorText = await res.text(); console.error('Test data API error:', errorText); throw new Error(`API error: ${res.status} ${res.statusText}`) }
const data = await res.json();
return { data: data.data, status: res.status }

View File

@@ -1,6 +1,6 @@
import { z } from "zod"
export const buildAddressUpdateSchema = z.object({
export const buildSitesUpdateSchema = z.object({
siteNo: z.string().optional(),
siteName: z.string().optional(),
@@ -9,4 +9,4 @@ export const buildAddressUpdateSchema = z.object({
});
export type BuildAddressUpdate = z.infer<typeof buildAddressUpdateSchema>;
export type BuildSitesUpdate = z.infer<typeof buildSitesUpdateSchema>;

View File

@@ -87,7 +87,7 @@ export function BuildSitesDataTableUpdate({
pageSize: number,
onPageChange: (page: number) => void,
onPageSizeChange: (size: number) => void,
refetchTable: () => void
refetchTable: () => void,
}) {
const router = useRouter();
@@ -167,9 +167,9 @@ export function BuildSitesDataTableUpdate({
})}
</DropdownMenuContent>
</DropdownMenu>
<Button variant="outline" size="sm" onClick={() => { router.push("/build-areas") }}>
<Button variant="outline" size="sm" onClick={() => { router.push(`/build-sites`) }}>
<Home />
<span className="hidden lg:inline">Back to Build Areas</span>
<span className="hidden lg:inline">Back to Build Sites</span>
</Button>
</div>
</div>

View File

@@ -1,9 +1,13 @@
'use client'
import { useMutation } from '@tanstack/react-query'
import { BuildTypesUpdate } from './types';
import { toISOIfNotZ } from '@/lib/utils';
const fetchGraphQlBuildTypesUpdate = async (record: BuildTypesUpdate, uuid: string): Promise<{ data: BuildTypesUpdate | null; status: number }> => {
console.log('Fetching test data from local API');
record.expiryStarts = record.expiryStarts ? toISOIfNotZ(record.expiryStarts) : undefined;
record.expiryEnds = record.expiryEnds ? toISOIfNotZ(record.expiryEnds) : undefined;
console.dir({ record }, { depth: Infinity });
try {
const res = await fetch(`/api/build-types/update?uuid=${uuid || ''}`, { method: 'POST', cache: 'no-store', credentials: "include", body: JSON.stringify(record) });
if (!res.ok) { const errorText = await res.text(); console.error('Test data API error:', errorText); throw new Error(`API error: ${res.status} ${res.statusText}`) }

View File

@@ -4,6 +4,8 @@ interface BuildTypesUpdate {
token: string;
typeToken: string;
description: string;
expiryStarts?: string;
expiryEnds?: string;
}

View File

@@ -0,0 +1,392 @@
"use client"
import { useForm } from "react-hook-form"
import { zodResolver } from "@hookform/resolvers/zod"
import { Form, FormField, FormItem, FormLabel, FormControl, FormMessage } from "@/components/ui/form"
import { Input } from "@/components/ui/input"
import { Button } from "@/components/ui/button"
import { Separator } from "@/components/ui/separator"
import { DateTimePicker } from "@/components/ui/date-time-picker"
import { BuildAdd, buildAddSchema } from "./schema"
import { useAddBuildMutation } from "./queries"
import { Checkbox } from "@/components/ui/checkbox"
const BuildsForm = ({ refetchTable }: { refetchTable: () => void }) => {
const form = useForm<BuildAdd>({
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 (
<Form {...form}>
<form onSubmit={handleSubmit(onSubmit)} className="space-y-6 p-4" >
{/* ROW 1 */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<FormField
control={form.control}
name="buildType"
render={({ field }) => (
<FormItem>
<FormLabel>Token</FormLabel>
<FormControl>
<Input placeholder="Token" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="collectionToken"
render={({ field }) => (
<FormItem>
<FormLabel>Collection Token</FormLabel>
<FormControl>
<Input placeholder="Collection Token" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<FormField
control={form.control}
name="info.govAddressCode"
render={({ field }) => (
<FormItem>
<FormLabel>Gov Address Code</FormLabel>
<FormControl>
<Input placeholder="Gov Address Code" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="info.buildName"
render={({ field }) => (
<FormItem>
<FormLabel>Build Name</FormLabel>
<FormControl>
<Input placeholder="Build Name" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="info.buildNo"
render={({ field }) => (
<FormItem>
<FormLabel>Build No</FormLabel>
<FormControl>
<Input placeholder="Build No" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<FormField
control={form.control}
name="info.maxFloor"
render={({ field }) => (
<FormItem>
<FormLabel>Max Floor</FormLabel>
<FormControl>
<Input
type="number"
placeholder="Max Floor"
{...field}
onBlur={(e) => {
field.onBlur();
const numValue = parseFloat(e.target.value);
if (!isNaN(numValue)) {
field.onChange(numValue);
}
}}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="info.undergroundFloor"
render={({ field }) => (
<FormItem>
<FormLabel>Underground Floor</FormLabel>
<FormControl>
<Input
type="number"
placeholder="Underground Floor"
{...field}
onBlur={(e) => {
field.onBlur();
const numValue = parseFloat(e.target.value);
if (!isNaN(numValue)) {
field.onChange(numValue);
}
}}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<FormField
control={form.control}
name="info.taxNo"
render={({ field }) => (
<FormItem>
<FormLabel>Tax No</FormLabel>
<FormControl>
<Input placeholder="Tax No" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="info.liftCount"
render={({ field }) => (
<FormItem>
<FormLabel>Lift Count</FormLabel>
<FormControl>
<Input
type="number"
placeholder="Lift Count"
{...field}
onBlur={(e) => {
field.onBlur();
const numValue = parseFloat(e.target.value);
if (!isNaN(numValue)) {
field.onChange(numValue);
}
}}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className="flex flex-row justify-evenly">
<FormField
control={form.control}
name="info.heatingSystem"
render={({ field }) => (
<FormItem className="flex flex-row items-center space-x-3 space-y-0">
<FormControl>
<Checkbox checked={field.value} onCheckedChange={field.onChange} />
</FormControl>
<div className="leading-none">
<FormLabel>
Heating System
</FormLabel>
</div>
</FormItem>
)}
/>
<FormField
control={form.control}
name="info.coolingSystem"
render={({ field }) => (
<FormItem className="flex flex-row items-center space-x-3 space-y-0">
<FormControl>
<Checkbox checked={field.value} onCheckedChange={field.onChange} />
</FormControl>
<div className="leading-none">
<FormLabel>
Cooling System
</FormLabel>
</div>
</FormItem>
)}
/>
<FormField
control={form.control}
name="info.hotWaterSystem"
render={({ field }) => (
<FormItem className="flex flex-row items-center space-x-3 space-y-0">
<FormControl>
<Checkbox checked={field.value} onCheckedChange={field.onChange} />
</FormControl>
<div className="leading-none">
<FormLabel>
Hot Water System
</FormLabel>
</div>
</FormItem>
)}
/>
</div>
<Separator />
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<FormField
control={form.control}
name="info.buildDate"
render={({ field }) => (
<FormItem>
<FormLabel>Build Date</FormLabel>
<FormControl>
<DateTimePicker {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="info.decisionPeriodDate"
render={({ field }) => (
<FormItem>
<FormLabel>Decision Period Date</FormLabel>
<FormControl>
<DateTimePicker {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<FormField
control={form.control}
name="info.blockServiceManCount"
render={({ field }) => (
<FormItem>
<FormLabel>Block Service Man Count</FormLabel>
<FormControl>
<Input
type="number"
placeholder="Block Service Man Count"
{...field}
onBlur={(e) => {
field.onBlur();
const numValue = parseFloat(e.target.value);
if (!isNaN(numValue)) {
field.onChange(numValue);
}
}}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="info.securityServiceManCount"
render={({ field }) => (
<FormItem>
<FormLabel>Security Service Man Count</FormLabel>
<FormControl>
<Input
type="number"
placeholder="Security Service Man Count"
{...field}
onBlur={(e) => {
field.onBlur();
const numValue = parseFloat(e.target.value);
if (!isNaN(numValue)) {
field.onChange(numValue);
}
}}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="info.garageCount"
render={({ field }) => (
<FormItem>
<FormLabel>Total Garage Count In Numbers</FormLabel>
<FormControl>
<Input
type="number"
placeholder="Total Garage Count"
{...field}
onBlur={(e) => {
field.onBlur();
const numValue = parseFloat(e.target.value);
if (!isNaN(numValue)) {
field.onChange(numValue);
}
}}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="info.managementRoomId"
render={({ field }) => (
<FormItem>
<FormLabel>Management Room ID Assign</FormLabel>
<FormControl>
<Input placeholder="Management Room ID" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<Button type="submit" className="w-full">Add Build </Button>
</form>
</Form>
);
};
export { BuildsForm }

View File

@@ -0,0 +1,25 @@
'use client';
import { useState } from 'react';
import { BuildDataTableAdd } from './table/data-table';
import { BuildsForm } from './form';
import { useGraphQlBuildsList } from '../queries';
const PageAddBuilds = () => {
const [page, setPage] = useState(1);
const [limit, setLimit] = useState(10);
const [sort, setSort] = useState({ createdAt: 'desc' });
const [filters, setFilters] = useState({});
const { data, isLoading, error, refetch } = useGraphQlBuildsList({ limit, skip: (page - 1) * limit, sort, filters });
return (
<>
<BuildDataTableAdd
data={data?.data || []} totalCount={data?.totalCount || 0} currentPage={page} pageSize={limit} onPageChange={setPage} onPageSizeChange={setLimit} refetchTable={refetch}
/>
<BuildsForm refetchTable={refetch} />
</>
)
}
export { PageAddBuilds };

View File

@@ -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) },
})
}

View File

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

View File

@@ -0,0 +1,155 @@
"use client"
import { z } from "zod"
import { Button } from "@/components/ui/button"
import { Drawer, DrawerClose, DrawerContent, DrawerFooter, DrawerHeader, DrawerTrigger } from "@/components/ui/drawer"
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { useSortable } from "@dnd-kit/sortable"
import { IconGripVertical } from "@tabler/icons-react"
import { useIsMobile } from "@/hooks/use-mobile"
import { Separator } from "@/components/ui/separator"
import { ColumnDef, flexRender, Row } from "@tanstack/react-table"
import { TableCell, TableRow } from "@/components/ui/table"
import { CSS } from "@dnd-kit/utilities"
import { schema, schemaType } from "./schema"
import { dateToLocaleString } from "@/lib/utils"
import { Pencil, Trash } from "lucide-react"
function DragHandle({ id }: { id: number }) {
const { attributes, listeners } = useSortable({ id })
return (
<Button {...attributes} {...listeners} variant="ghost" size="icon" className="text-muted-foreground size-7 hover:bg-transparent">
<IconGripVertical className="text-muted-foreground size-3" />
<span className="sr-only">Drag to reorder</span>
</Button>
)
}
export function DraggableRow({ row }: { row: Row<z.infer<typeof schema>> }) {
const { transform, transition, setNodeRef, isDragging } = useSortable({ id: row.original._id })
return (
<TableRow data-state={row.getIsSelected() && "selected"} data-dragging={isDragging} ref={setNodeRef}
className="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(router: any, deleteHandler: (id: string) => void): ColumnDef<schemaType>[] {
return [
{
accessorKey: "buildType.token",
header: "Token",
cell: ({ getValue }) => getValue(),
},
{
accessorKey: "collectionToken",
header: "Collection Token",
cell: ({ getValue }) => getValue(),
},
{
accessorKey: "info.govAddressCode",
header: "Gov Address Code",
cell: ({ getValue }) => getValue(),
},
{
accessorKey: "info.buildName",
header: "Build Name",
cell: ({ getValue }) => getValue(),
},
{
accessorKey: "info.buildNo",
header: "Build No",
cell: ({ getValue }) => getValue(),
},
{
accessorKey: "info.maxFloor",
header: "Max Floor",
cell: ({ getValue }) => getValue(),
},
{
accessorKey: "info.undergroundFloor",
header: "Underground Floor",
cell: ({ getValue }) => getValue(),
},
{
accessorKey: "info.buildDate",
header: "Build Date",
cell: ({ getValue }) => getValue(),
},
{
accessorKey: "info.decisionPeriodDate",
header: "Decision Period Date",
cell: ({ getValue }) => getValue(),
},
{
accessorKey: "info.taxNo",
header: "Tax No",
cell: ({ getValue }) => getValue(),
},
{
accessorKey: "info.liftCount",
header: "Lift Count",
cell: ({ getValue }) => getValue(),
},
{
accessorKey: "info.heatingSystem",
header: "Heating System",
cell: ({ getValue }) => getValue(),
},
{
accessorKey: "info.coolingSystem",
header: "Cooling System",
cell: ({ getValue }) => getValue(),
},
{
accessorKey: "info.hotWaterSystem",
header: "Hot Water System",
cell: ({ getValue }) => getValue(),
},
{
accessorKey: "info.blockServiceManCount",
header: "Block Service Man Count",
cell: ({ getValue }) => getValue(),
},
{
accessorKey: "info.securityServiceManCount",
header: "Security Service Man Count",
cell: ({ getValue }) => getValue(),
},
{
accessorKey: "info.garageCount",
header: "Garage Count",
cell: ({ getValue }) => getValue(),
},
{
accessorKey: "info.managementRoomId",
header: "Management Room ID",
cell: ({ getValue }) => getValue(),
},
{
id: "actions",
header: "Actions",
cell: ({ row }) => {
return (
<div className="flex flex-row gap-2">
<Button className="bg-amber-400 text-black border-amber-400" variant="outline" size="sm" onClick={() => { router.push(`/builds/update?uuid=${row.original._id}`) }}>
<Pencil />
</Button>
<Button className="bg-red-700 text-white border-red-700" variant="outline" size="sm" onClick={() => { deleteHandler(row.original._id || "") }}>
<Trash />
</Button>
</div>
);
},
}
]
}
export { getColumns };

View File

@@ -0,0 +1,254 @@
"use client"
import * as React from "react"
import {
closestCenter,
DndContext,
KeyboardSensor,
MouseSensor,
TouchSensor,
useSensor,
useSensors,
type UniqueIdentifier,
} from "@dnd-kit/core"
import { restrictToVerticalAxis } from "@dnd-kit/modifiers"
import {
SortableContext,
verticalListSortingStrategy,
} from "@dnd-kit/sortable"
import {
IconChevronDown,
IconChevronLeft,
IconChevronRight,
IconChevronsLeft,
IconChevronsRight,
IconLayoutColumns,
} from "@tabler/icons-react"
import {
ColumnFiltersState,
flexRender,
getCoreRowModel,
getFacetedRowModel,
getFacetedUniqueValues,
getFilteredRowModel,
getSortedRowModel,
SortingState,
useReactTable,
VisibilityState,
} from "@tanstack/react-table"
import { Button } from "@/components/ui/button"
import { DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
import { Label } from "@/components/ui/label"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
import { Tabs, TabsContent } from "@/components/ui/tabs"
import { schemaType } from "./schema"
import { getColumns, DraggableRow } from "./columns"
import { useRouter } from "next/navigation"
import { Home } from "lucide-react"
import { useDeleteBuildMutation } from "@/pages/builds/queries"
export function BuildDataTableAdd({
data,
totalCount,
currentPage,
pageSize,
onPageChange,
onPageSizeChange,
refetchTable,
}: {
data: schemaType[],
totalCount: number,
currentPage: number,
pageSize: number,
onPageChange: (page: number) => void,
onPageSizeChange: (size: number) => void,
refetchTable: () => void
}) {
const router = useRouter();
const [rowSelection, setRowSelection] = React.useState({})
const [columnVisibility, setColumnVisibility] = React.useState<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 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 (
<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>
<Button variant="outline" size="sm" onClick={() => { router.push("/builds") }}>
<Home />
<span className="hidden lg:inline">Back to Build</span>
</Button>
</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 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,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,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 };

View File

@@ -6,7 +6,7 @@ import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel,
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { useSortable } from "@dnd-kit/sortable"
import { IconGripVertical } from "@tabler/icons-react"
import { IconGripVertical, IconHandClick } from "@tabler/icons-react"
import { useIsMobile } from "@/hooks/use-mobile"
import { Separator } from "@/components/ui/separator"
import { ColumnDef, flexRender, Row } from "@tanstack/react-table"
@@ -14,119 +14,7 @@ import { TableCell, TableRow } from "@/components/ui/table"
import { CSS } from "@dnd-kit/utilities"
import { schema, schemaType } from "./schema"
import { dateToLocaleString } from "@/lib/utils"
import { Pencil, Trash } from "lucide-react"
// function TableCellViewer({ item }: { item: schemaType }) {
// const isMobile = useIsMobile();
// return (
// <Drawer direction={isMobile ? "bottom" : "right"}>
// <DrawerTrigger asChild>
// <Button variant="link" className="text-foreground w-fit px-0 text-left">
// {item.email ?? "Unknown Email"}
// </Button>
// </DrawerTrigger>
// <DrawerContent>
// <DrawerHeader className="gap-1">
// <h2 className="text-lg font-semibold">{item.email}</h2>
// <p className="text-sm text-muted-foreground">
// User details
// </p>
// </DrawerHeader>
// <div className="flex flex-col gap-4 overflow-y-auto px-4 text-sm">
// {/* BASIC INFO */}
// <div className="grid grid-cols-2 gap-4">
// <div>
// <Label>Email</Label>
// <Input value={item.email ?? ""} readOnly />
// </div>
// <div>
// <Label>Phone</Label>
// <Input value={item.phone ?? ""} readOnly />
// </div>
// <div>
// <Label>Tag</Label>
// <Input value={item.tag ?? ""} readOnly />
// </div>
// <div>
// <Label>Active</Label>
// <Input value={item.active ? "Active" : "Inactive"} readOnly />
// </div>
// </div>
// <Separator />
// {/* DATES */}
// <div className="grid grid-cols-2 gap-4">
// <div>
// <Label>Created At</Label>
// <Input
// value={item.createdAt ? new Date(item.createdAt).toLocaleString() : ""}
// readOnly
// />
// </div>
// <div>
// <Label>Updated At</Label>
// <Input
// value={item.updatedAt ? new Date(item.updatedAt).toLocaleString() : ""}
// readOnly
// />
// </div>
// </div>
// <Separator />
// {/* TOKENS */}
// <DropdownMenu>
// <DropdownMenuTrigger asChild>
// <Button variant="outline" className="w-full">
// Collection Tokens ({item.collectionTokens?.tokens?.length ?? 0})
// </Button>
// </DropdownMenuTrigger>
// <DropdownMenuContent className="w-72">
// <DropdownMenuLabel>Tokens</DropdownMenuLabel>
// <DropdownMenuSeparator />
// {(item.collectionTokens?.tokens ?? []).length === 0 && (<DropdownMenuItem disabled>No tokens found</DropdownMenuItem>)}
// {(item.collectionTokens?.tokens ?? []).map((t, i) => (
// <DropdownMenuItem key={i} className="flex flex-col gap-2">
// <div className="grid grid-cols-2 gap-2 w-full">
// <Input value={t.prefix} readOnly />
// <Input value={t.token} readOnly />
// </div>
// </DropdownMenuItem>
// ))}
// </DropdownMenuContent>
// </DropdownMenu>
// </div>
// <DrawerFooter>
// <DrawerClose asChild>
// <Button variant="outline">Close</Button>
// </DrawerClose>
// </DrawerFooter>
// </DrawerContent>
// </Drawer>
// );
// }
function DragHandle({ id }: { id: number }) {
const { attributes, listeners } = useSortable({ id })
return (
<Button {...attributes} {...listeners} variant="ghost" size="icon" className="text-muted-foreground size-7 hover:bg-transparent">
<IconGripVertical className="text-muted-foreground size-3" />
<span className="sr-only">Drag to reorder</span>
</Button>
)
}
import { Pencil, Trash, TextSelect } from "lucide-react"
export function DraggableRow({ row }: { row: Row<z.infer<typeof schema>> }) {
const { transform, transition, setNodeRef, isDragging } = useSortable({ id: row.original._id })
@@ -142,102 +30,112 @@ export function DraggableRow({ row }: { row: Row<z.infer<typeof schema>> }) {
)
}
function getColumns(router: any, deleteHandler: (id: string) => void): ColumnDef<schemaType>[] {
function getColumns(router: any, activeRoute: string, deleteHandler: (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: "buildType.token",
header: "Token",
cell: ({ getValue }) => getValue(),
},
{
accessorKey: "firstName",
header: "First Name",
accessorKey: "collectionToken",
header: "Collection Token",
cell: ({ getValue }) => getValue(),
},
{
accessorKey: "surname",
header: "Surname",
accessorKey: "info.govAddressCode",
header: "Gov Address Code",
cell: ({ getValue }) => getValue(),
},
{
accessorKey: "middleName",
header: "Middle Name",
accessorKey: "info.buildName",
header: "Build Name",
cell: ({ getValue }) => getValue(),
},
{
accessorKey: "sexCode",
header: "Sex",
accessorKey: "info.buildNo",
header: "Build No",
cell: ({ getValue }) => getValue(),
},
{
accessorKey: "personRef",
header: "Person Ref",
accessorKey: "info.maxFloor",
header: "Max Floor",
cell: ({ getValue }) => getValue(),
},
{
accessorKey: "personTag",
header: "Person Tag",
accessorKey: "info.undergroundFloor",
header: "Underground Floor",
cell: ({ getValue }) => getValue(),
},
{
accessorKey: "fatherName",
header: "Father Name",
accessorKey: "info.buildDate",
header: "Build Date",
cell: ({ getValue }) => getValue(),
},
{
accessorKey: "motherName",
header: "Mother Name",
accessorKey: "info.decisionPeriodDate",
header: "Decision Period Date",
cell: ({ getValue }) => getValue(),
},
{
accessorKey: "countryCode",
header: "Country",
accessorKey: "info.taxNo",
header: "Tax No",
cell: ({ getValue }) => getValue(),
},
{
accessorKey: "nationalIdentityId",
header: "National ID",
accessorKey: "info.liftCount",
header: "Lift Count",
cell: ({ getValue }) => getValue(),
},
{
accessorKey: "birthPlace",
header: "Birth Place",
accessorKey: "info.heatingSystem",
header: "Heating System",
cell: ({ getValue }) => getValue(),
},
{
accessorKey: "active",
header: "Active",
cell: ({ getValue }) => getValue() ? (<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(),
},
{
accessorKey: "isConfirmed",
header: "Confirmed",
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(),
},
{
accessorKey: "birthDate",
header: "Birth Date",
cell: ({ getValue }) => dateToLocaleString(getValue() as string),
accessorKey: "info.blockServiceManCount",
header: "Block Service Man Count",
cell: ({ getValue }) => getValue(),
},
{
accessorKey: "createdAt",
header: "Created",
cell: ({ getValue }) => dateToLocaleString(getValue() as string),
accessorKey: "info.securityServiceManCount",
header: "Security Service Man Count",
cell: ({ getValue }) => getValue(),
},
{
accessorKey: "updatedAt",
header: "Updated",
cell: ({ getValue }) => dateToLocaleString(getValue() as string),
accessorKey: "info.garageCount",
header: "Garage Count",
cell: ({ getValue }) => getValue(),
},
{
accessorKey: "expiryStarts",
header: "Expiry Starts",
cell: ({ getValue }) => getValue() ? dateToLocaleString(getValue() as string) : "-",
},
{
accessorKey: "expiryEnds",
header: "Expiry Ends",
cell: ({ getValue }) => getValue() ? dateToLocaleString(getValue() as string) : "-",
accessorKey: "info.managementRoomId",
header: "Management Room ID",
cell: ({ getValue }) => getValue(),
},
{
id: "actions",
header: "Actions",
cell: ({ row }) => {
return (
<div>
<Button className="bg-amber-400 text-black border-amber-400" variant="outline" size="sm" onClick={() => { router.push(`/people/update?uuid=${row.original.uuid}`) }}>
<div className="flex flex-row gap-2">
<Button className="bg-blue-600 border-blue-600 text-white" variant="outline" size="sm" onClick={() => { router.push(`/${activeRoute}/add?build=${row.original._id}`) }}>
<IconHandClick />
</Button>
<Button className="bg-amber-400 text-black border-amber-400" variant="outline" size="sm" onClick={() => { router.push(`/builds/update?uuid=${row.original._id}`) }}>
<Pencil />
</Button>
<Button className="bg-red-700 text-white border-red-700 mx-4" variant="outline" size="sm" onClick={() => { deleteHandler(row.original.uuid || "") }}>
<Button className="bg-red-700 text-white border-red-700" variant="outline" size="sm" onClick={() => { deleteHandler(row.original._id || "") }}>
<Trash />
</Button>
</div>

View File

@@ -17,6 +17,10 @@ import {
verticalListSortingStrategy,
} from "@dnd-kit/sortable"
import {
IconBorderLeftPlus,
IconBuildingBank,
IconBuildingBridge,
IconBuildingChurch,
IconChevronDown,
IconChevronLeft,
IconChevronRight,
@@ -70,7 +74,7 @@ import {
import { schemaType } from "./schema"
import { getColumns, DraggableRow } from "./columns"
import { useRouter } from "next/navigation"
import { useDeleteUserMutation } from "@/pages/users/queries"
import { useDeleteBuildMutation } from "@/pages/builds/queries"
export function BuildDataTable({
data,
@@ -87,10 +91,29 @@ export function BuildDataTable({
pageSize?: number,
onPageChange: (page: number) => void,
onPageSizeChange: (size: number) => void,
refetchTable: () => void,
refetchTable: () => void
}) {
const router = useRouter();
const routeSelections = [
{
url: 'build-parts',
name: 'Build Parts',
icon: <IconBuildingBank />
},
{
url: 'build-areas',
name: 'Build Areas',
icon: <IconBuildingChurch />
},
{
url: 'build-sites',
name: 'Build Sites',
icon: <IconBuildingBridge />
},
]
const [activeRoute, setActiveRoute] = React.useState(routeSelections[0].url);
const [rowSelection, setRowSelection] = React.useState({})
const [columnVisibility, setColumnVisibility] = React.useState<VisibilityState>({})
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>([])
@@ -99,9 +122,9 @@ export function BuildDataTable({
const sensors = useSensors(useSensor(MouseSensor, {}), useSensor(TouchSensor, {}), useSensor(KeyboardSensor, {}))
const dataIds = React.useMemo<UniqueIdentifier[]>(() => data?.map(({ _id }) => _id) || [], [data])
const deleteMutation = useDeleteUserMutation()
const deleteMutation = useDeleteBuildMutation()
const deleteHandler = (id: string) => { deleteMutation.mutate({ uuid: id }); setTimeout(() => { refetchTable() }, 400) }
const columns = getColumns(router, deleteHandler);
const columns = getColumns(router, activeRoute, deleteHandler);
const pagination = React.useMemo(() => ({ pageIndex: currentPage - 1, pageSize: pageSize }), [currentPage, pageSize])
const totalPages = Math.ceil(totalCount / pageSize)
@@ -128,14 +151,9 @@ export function BuildDataTable({
const handlePageSizeChange = (value: string) => { const newSize = Number(value); onPageSizeChange(newSize); onPageChange(1) }
return (
<Tabs
defaultValue="outline"
className="w-full flex-col justify-start gap-6"
>
<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>
<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" />
@@ -157,7 +175,6 @@ export function BuildDataTable({
<IconChevronDown />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-56">
{table.getAllColumns().filter((column) => typeof column.accessorFn !== "undefined" && column.getCanHide()).map((column) => {
return (
@@ -168,15 +185,34 @@ export function BuildDataTable({
})}
</DropdownMenuContent>
</DropdownMenu>
<Button variant="outline" size="sm" onClick={() => { router.push("/people/add") }}>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="sm">
<IconBorderLeftPlus />
<span className="hidden lg:inline">Selected To Add</span>
<span className="lg:hidden">Add</span>
<IconChevronDown />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-56">
{routeSelections.map((column) => {
return (
<DropdownMenuCheckboxItem key={column.url} className="capitalize" checked={activeRoute === column.url} onCheckedChange={(value) => setActiveRoute(column.url)} >
<div className="flex items-center gap-2">
{column.icon}{column.name}
</div>
</DropdownMenuCheckboxItem>
)
})}
</DropdownMenuContent>
</DropdownMenu>
<Button variant="outline" size="sm" onClick={() => { router.push("/builds/add") }}>
<IconPlus />
<span className="hidden lg:inline">Add Person</span>
<span className="hidden lg:inline">Add Build</span>
</Button>
</div>
</div>
<TabsContent value="outline"
className="relative flex flex-col gap-4 overflow-auto px-4 lg:px-6"
>
<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>

View File

@@ -2,26 +2,30 @@ import { z } from "zod";
export const schema = z.object({
_id: z.string(),
uuid: z.string(),
firstName: z.string().nullable().optional(),
surname: z.string().nullable().optional(),
middleName: z.string().nullable().optional(),
sexCode: z.string().nullable().optional(),
personRef: z.string().nullable().optional(),
personTag: z.string().nullable().optional(),
fatherName: z.string().nullable().optional(),
motherName: z.string().nullable().optional(),
countryCode: z.string().nullable().optional(),
nationalIdentityId: z.string().nullable().optional(),
birthPlace: z.string().nullable().optional(),
birthDate: z.string().nullable().optional(),
taxNo: z.string().nullable().optional(),
birthname: z.string().nullable().optional(),
expiryStarts: z.string().nullable().optional(),
expiryEnds: z.string().nullable().optional(),
createdAt: z.string().nullable().optional(),
updatedAt: z.string().nullable().optional(),
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>;
export type schemaType = z.infer<typeof schema>;

View File

@@ -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 BuildupdateForm = ({ refetchTable, initData, selectedUuid }: { refetchTable: () => void, initData: BuildUpdate, selectedUuid: string }) => {
const form = useForm<BuildUpdate>({ 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 (
<Form {...form}>
<form onSubmit={handleSubmit(onSubmit)} className="space-y-6 p-4" >
{/* ROW 1 */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<FormField
control={form.control}
name="buildType.token"
render={({ field }) => (
<FormItem>
<FormLabel>Token</FormLabel>
<FormControl>
<Input placeholder="Token" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="collectionToken"
render={({ field }) => (
<FormItem>
<FormLabel>Collection Token</FormLabel>
<FormControl>
<Input placeholder="Collection Token" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<FormField
control={form.control}
name="info.govAddressCode"
render={({ field }) => (
<FormItem>
<FormLabel>Gov Address Code</FormLabel>
<FormControl>
<Input placeholder="Gov Address Code" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="info.buildName"
render={({ field }) => (
<FormItem>
<FormLabel>Build Name</FormLabel>
<FormControl>
<Input placeholder="Build Name" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="info.buildNo"
render={({ field }) => (
<FormItem>
<FormLabel>Build No</FormLabel>
<FormControl>
<Input placeholder="Build No" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<FormField
control={form.control}
name="info.maxFloor"
render={({ field }) => (
<FormItem>
<FormLabel>Max Floor</FormLabel>
<FormControl>
<Input placeholder="Max Floor" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="info.undergroundFloor"
render={({ field }) => (
<FormItem>
<FormLabel>Underground Floor</FormLabel>
<FormControl>
<Input placeholder="Underground Floor" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<FormField
control={form.control}
name="info.taxNo"
render={({ field }) => (
<FormItem>
<FormLabel>Tax No</FormLabel>
<FormControl>
<Input placeholder="Tax No" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="info.liftCount"
render={({ field }) => (
<FormItem>
<FormLabel>Lift Count</FormLabel>
<FormControl>
<Input placeholder="Lift Count" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className="flex flex-row justify-evenly">
<FormField
control={form.control}
name="info.heatingSystem"
render={({ field }) => (
<FormItem className="flex flex-row items-center space-x-3 space-y-0">
<FormControl>
<Checkbox
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
<div className="leading-none">
<FormLabel>
Heating System
</FormLabel>
</div>
</FormItem>
)}
/>
<FormField
control={form.control}
name="info.coolingSystem"
render={({ field }) => (
<FormItem className="flex flex-row items-center space-x-3 space-y-0">
<FormControl>
<Checkbox
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
<div className="leading-none">
<FormLabel>
Cooling System
</FormLabel>
</div>
</FormItem>
)}
/>
<FormField
control={form.control}
name="info.hotWaterSystem"
render={({ field }) => (
<FormItem className="flex flex-row items-center space-x-3 space-y-0">
<FormControl>
<Checkbox
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
<div className="leading-none">
<FormLabel>
Hot Water System
</FormLabel>
</div>
</FormItem>
)}
/>
</div>
<Separator />
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<FormField
control={form.control}
name="info.buildDate"
render={({ field }) => (
<FormItem>
<FormLabel>Build Date</FormLabel>
<FormControl>
<DateTimePicker {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="info.decisionPeriodDate"
render={({ field }) => (
<FormItem>
<FormLabel>Decision Period Date</FormLabel>
<FormControl>
<DateTimePicker {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<FormField
control={form.control}
name="info.blockServiceManCount"
render={({ field }) => (
<FormItem>
<FormLabel>Block Service Man Count</FormLabel>
<FormControl>
<Input placeholder="Block Service Man Count" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="info.securityServiceManCount"
render={({ field }) => (
<FormItem>
<FormLabel>Security Service Man Count</FormLabel>
<FormControl>
<Input placeholder="Security Service Man Count" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="info.garageCount"
render={({ field }) => (
<FormItem>
<FormLabel>Total Garage Count In Numbers</FormLabel>
<FormControl>
<Input placeholder="Total Garage Count" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="info.managementRoomId"
render={({ field }) => (
<FormItem>
<FormLabel>Management Room ID Assign</FormLabel>
<FormControl>
<Input placeholder="Management Room ID" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<Button type="submit" className="w-full">Update Build</Button>
</form>
</Form>
);
}
export { BuildupdateForm }

View File

@@ -0,0 +1,36 @@
'use client';
import { useState } from 'react';
import { BuildupdateForm } from '@/pages/builds/update/form';
import { useSearchParams, useRouter } from 'next/navigation'
import { Button } from '@/components/ui/button';
import { BuildDataTableUpdate } from './table/data-table';
import { useGraphQlBuildsList } from '../queries';
const PageUpdateBuild = () => {
const [page, setPage] = useState(1);
const [limit, setLimit] = useState(10);
const [sort, setSort] = useState({ createdAt: 'desc' });
const [filters, setFilters] = useState({});
const searchParams = useSearchParams()
const router = useRouter()
const uuid = searchParams?.get('uuid') || null
const backToBuildAddress = <>
<div>UUID not found in search params</div>
<Button onClick={() => router.push('/builds')}>Back to Build</Button>
</>
if (!uuid) { return backToBuildAddress }
const { data, isLoading, error, refetch } = useGraphQlBuildsList({ limit, skip: (page - 1) * limit, sort, filters: { ...filters, _id: uuid } });
const initData = data?.data?.[0] || null;
if (!initData) { return backToBuildAddress }
return (
<>
<BuildDataTableUpdate
data={data?.data || []} totalCount={data?.totalCount || 0} currentPage={page} pageSize={limit}
onPageChange={setPage} onPageSizeChange={setLimit} refetchTable={refetch}
/>
<BuildupdateForm refetchTable={refetch} initData={initData} selectedUuid={uuid} />
</>
)
}
export { PageUpdateBuild };

View File

@@ -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) },
})
}

View File

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

View File

@@ -0,0 +1,145 @@
"use client"
import { z } from "zod"
import { Button } from "@/components/ui/button"
import { Drawer, DrawerClose, DrawerContent, DrawerFooter, DrawerHeader, DrawerTrigger } from "@/components/ui/drawer"
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { useSortable } from "@dnd-kit/sortable"
import { IconGripVertical } from "@tabler/icons-react"
import { useIsMobile } from "@/hooks/use-mobile"
import { Separator } from "@/components/ui/separator"
import { ColumnDef, flexRender, Row } from "@tanstack/react-table"
import { TableCell, TableRow } from "@/components/ui/table"
import { CSS } from "@dnd-kit/utilities"
import { schema, schemaType } from "./schema"
import { dateToLocaleString } from "@/lib/utils"
import { Pencil, Trash } from "lucide-react"
export function DraggableRow({ row }: { row: Row<z.infer<typeof schema>> }) {
const { transform, transition, setNodeRef, isDragging } = useSortable({ id: row.original._id })
return (
<TableRow data-state={row.getIsSelected() && "selected"} data-dragging={isDragging} ref={setNodeRef}
className="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(router: any, deleteHandler: (id: string) => void): ColumnDef<schemaType>[] {
return [
{
accessorKey: "buildType.token",
header: "Token",
cell: ({ getValue }) => getValue(),
},
{
accessorKey: "collectionToken",
header: "Collection Token",
cell: ({ getValue }) => getValue(),
},
{
accessorKey: "info.govAddressCode",
header: "Gov Address Code",
cell: ({ getValue }) => getValue(),
},
{
accessorKey: "info.buildName",
header: "Build Name",
cell: ({ getValue }) => getValue(),
},
{
accessorKey: "info.buildNo",
header: "Build No",
cell: ({ getValue }) => getValue(),
},
{
accessorKey: "info.maxFloor",
header: "Max Floor",
cell: ({ getValue }) => getValue(),
},
{
accessorKey: "info.undergroundFloor",
header: "Underground Floor",
cell: ({ getValue }) => getValue(),
},
{
accessorKey: "info.buildDate",
header: "Build Date",
cell: ({ getValue }) => getValue(),
},
{
accessorKey: "info.decisionPeriodDate",
header: "Decision Period Date",
cell: ({ getValue }) => getValue(),
},
{
accessorKey: "info.taxNo",
header: "Tax No",
cell: ({ getValue }) => getValue(),
},
{
accessorKey: "info.liftCount",
header: "Lift Count",
cell: ({ getValue }) => getValue(),
},
{
accessorKey: "info.heatingSystem",
header: "Heating System",
cell: ({ getValue }) => getValue(),
},
{
accessorKey: "info.coolingSystem",
header: "Cooling System",
cell: ({ getValue }) => getValue(),
},
{
accessorKey: "info.hotWaterSystem",
header: "Hot Water System",
cell: ({ getValue }) => getValue(),
},
{
accessorKey: "info.blockServiceManCount",
header: "Block Service Man Count",
cell: ({ getValue }) => getValue(),
},
{
accessorKey: "info.securityServiceManCount",
header: "Security Service Man Count",
cell: ({ getValue }) => getValue(),
},
{
accessorKey: "info.garageCount",
header: "Garage Count",
cell: ({ getValue }) => getValue(),
},
{
accessorKey: "info.managementRoomId",
header: "Management Room ID",
cell: ({ getValue }) => getValue(),
},
{
id: "actions",
header: "Actions",
cell: ({ row }) => {
return (
<div className="flex flex-row gap-2">
<Button className="bg-amber-400 text-black border-amber-400" variant="outline" size="sm" onClick={() => { router.push(`/builds/update?uuid=${row.original._id}`) }}>
<Pencil />
</Button>
<Button className="bg-red-700 text-white border-red-700" variant="outline" size="sm" onClick={() => { deleteHandler(row.original._id || "") }}>
<Trash />
</Button>
</div>
);
},
}
]
}
export { getColumns };

View File

@@ -0,0 +1,279 @@
"use client"
import * as React from "react"
import {
closestCenter,
DndContext,
KeyboardSensor,
MouseSensor,
TouchSensor,
useSensor,
useSensors,
type UniqueIdentifier,
} from "@dnd-kit/core"
import { restrictToVerticalAxis } from "@dnd-kit/modifiers"
import {
SortableContext,
verticalListSortingStrategy,
} from "@dnd-kit/sortable"
import {
IconChevronDown,
IconChevronLeft,
IconChevronRight,
IconChevronsLeft,
IconChevronsRight,
IconLayoutColumns,
} from "@tabler/icons-react"
import {
ColumnFiltersState,
flexRender,
getCoreRowModel,
getFacetedRowModel,
getFacetedUniqueValues,
getFilteredRowModel,
getSortedRowModel,
SortingState,
useReactTable,
VisibilityState,
} from "@tanstack/react-table"
import { Button } from "@/components/ui/button"
import {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import { Label } from "@/components/ui/label"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table"
import {
Tabs,
TabsContent,
TabsList,
TabsTrigger,
} from "@/components/ui/tabs"
import { schemaType } from "./schema"
import { getColumns, DraggableRow } from "./columns"
import { useRouter } from "next/navigation"
import { Home } from "lucide-react"
import { useDeleteBuildMutation } from "@/pages/builds/queries"
export function BuildDataTableUpdate({
data,
totalCount,
currentPage,
pageSize,
onPageChange,
onPageSizeChange,
refetchTable,
}: {
data: schemaType[],
totalCount: number,
currentPage: number,
pageSize: number,
onPageChange: (page: number) => void,
onPageSizeChange: (size: number) => void,
refetchTable: () => void
}) {
const router = useRouter();
const [rowSelection, setRowSelection] = React.useState({})
const [columnVisibility, setColumnVisibility] = React.useState<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 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 (
<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>
<Button variant="outline" size="sm" onClick={() => { router.push("/builds") }}>
<Home />
<span className="hidden lg:inline">Back to Build</span>
</Button>
</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 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,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,13 @@
interface UpdateBuildIbansUpdate {
iban: string;
startDate: string;
stopDate: string;
bankCode: string;
xcomment: string;
expiryStarts?: string;
expiryEnds?: string;
}
export type { UpdateBuildIbansUpdate };