updated application pages build tested

This commit is contained in:
Berkay 2025-06-24 21:44:21 +03:00
parent a9655c5f48
commit c259ad3d99
16 changed files with 1087 additions and 699 deletions

View File

@ -60,6 +60,7 @@ SuperPartsDeleteEvent = Event(
def super_parts_list_callable(list_options: PaginateOnly, headers: CommonHeaders):
list_options = PaginateOnly(**list_options.model_dump())
# TODO: Pydantic Model must be implemnted for list_options.query
print('list_options', list_options.model_dump())
with Build.new_session() as db_session:
BuildParts.set_session(db_session)
base_query = BuildParts.query.filter()

View File

@ -63,6 +63,7 @@ export interface UseTableDataProps {
mapFormToRequestBody?: (values: TableFormValues) => any;
customFormSchema?: z.ZodType<any, any>;
customFormValues?: Record<string, any>;
baseQuery?: Record<string, any>; // Base query that will be merged with user query
pageField?: keyof TableFormValues;
sizeField?: keyof TableFormValues;
orderFieldField?: keyof TableFormValues;
@ -77,6 +78,7 @@ export function useTableData({
mapFormToRequestBody,
customFormSchema = defaultFormSchema,
customFormValues = defaultFormValues,
baseQuery = {}, // Default empty base query
pageField = "page" as keyof TableFormValues,
sizeField = "size" as keyof TableFormValues,
orderFieldField = "orderField" as keyof TableFormValues,
@ -161,6 +163,15 @@ export function useTableData({
query: values[queryField],
};
// Make sure query is not empty if baseQuery has values
if (Object.keys(baseQuery).length > 0) {
if (!requestBody.query || typeof requestBody.query !== "object") {
requestBody.query = {};
}
// Add baseQuery properties to the query object
Object.assign(requestBody.query, baseQuery);
}
const response = await apiPostFetcher<any>({
url: apiUrl,
isNoCache: true,

View File

@ -6,31 +6,18 @@ import { withCache } from "@/components/mutual/context/cache/withCache";
import { Button } from "@/components/mutual/ui/button";
import { Form } from "@/components/mutual/ui/form";
import { getCacheData, setCacheData, clearCacheData } from "@/components/mutual/context/cache/context";
import { X } from "lucide-react";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { buildPartsSchemaCreate, BuildPartsCreateInterface, buildPartsSchema } from "./schema";
import { buildPartsTranslations, translationsOfPage } from "./translations";
import { buildPartsSchemaCreate, createEmptyValues } from "./schema";
import { buildPartsTranslations, translationsOfPage, buildingTranslations } from "./translations";
import { LanguageTypes } from "@/validations/mutual/language/validations";
import { NumberInput } from "@/components/mutual/formInputs/NumberInput";
import { StringInput } from "@/components/mutual/formInputs/StringInput";
import { CheckBoxInput } from "@/components/mutual/formInputs/CheckBoxInput";
const emptyValues: BuildPartsCreateInterface = {
build_uu_id: null,
address_gov_code: "",
part_no: 0,
part_level: 0,
part_code: "",
part_gross_size: 0,
part_net_size: 0,
default_accessory: "0",
human_livable: true,
due_part_key: "",
part_direction_uu_id: null,
};
function CreateFromComponentBase({
onlineData,
cacheData,
@ -53,17 +40,29 @@ function CreateFromComponentBase({
const language: LanguageTypes = onlineData?.lang || 'en';
const router = useRouter();
const listUrl = `/panel/${activePageUrl?.replace("/create", "")}`;
const [selectedBuilding, setSelectedBuilding] = useState<any>(null);
const [formError, setFormError] = useState<string>("");
const [cacheLoaded, setCacheLoaded] = useState<boolean>(false);
const form = useForm({
resolver: zodResolver(buildPartsSchemaCreate),
defaultValues: emptyValues
});
const form = useForm({ resolver: zodResolver(buildPartsSchemaCreate), defaultValues: createEmptyValues });
useEffect(() => {
const checkCacheForSelectedBuild = async () => {
try {
const cachedData = await getCacheData("/build/list");
if (cachedData && cachedData.build) {
setSelectedBuilding(cachedData.build);
const formValues = form.getValues();
form.setValue("build_uu_id", cachedData.build.uu_id);
}
} catch (error) { console.error("Error checking cache for selected build:", error) }
};
checkCacheForSelectedBuild();
}, [form]);
useEffect(() => {
const fetchCacheDirectly = async () => {
if (!cacheLoaded) {
try {
console.log("Directly fetching cache for URL:", activePageUrl);
const cachedData = await getCacheData(activePageUrl);
if (cachedData) {
const formValues = form.getValues();
@ -102,16 +101,191 @@ function CreateFromComponentBase({
};
const onSubmit = async (data: any) => {
setFormError("");
if (!data.build_uu_id && selectedBuilding) { data.build_uu_id = selectedBuilding.uu_id }
if (!data.build_uu_id) {
const errorMessage = language === 'en' ? "Missing building ID. Please select a building first." : "Bina ID'si eksik. Lütfen önce bir bina seçin.";
console.error('errorMessage', errorMessage);
setFormError(errorMessage);
return;
}
console.log("Form submitted with data:", data);
try {
const response = await apiPostFetcher<any>({ url: `/api/parts/create`, isNoCache: true, body: data });
await clearCacheData(activePageUrl);
if (clearCache) { clearCache(activePageUrl) }
if (response.success) { form.reset(emptyValues); router.push(listUrl) }
} catch (error) { console.error("Error submitting form:", error) }
if (response.success) { form.reset(createEmptyValues); router.push(listUrl) }
} catch (error) {
console.error("Error submitting form:", error);
setFormError(language === 'en' ? "Error submitting form" : "Form gönderilirken hata oluştu");
}
}
const formComponent = <>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
{/* ----- Basic Information ----- */}
{/* Address Government Code */}
<StringInput
control={form.control}
name="address_gov_code"
label={buildPartsTranslations[language].address_gov_code}
placeholder={buildPartsTranslations[language].address_gov_code}
onBlurCallback={handleFieldBlur}
/>
{/* ----- Part Identification ----- */}
{/* Part Number */}
<NumberInput
control={form.control}
name="part_no"
label={buildPartsTranslations[language].part_no}
placeholder={buildPartsTranslations[language].part_no}
onBlurCallback={handleFieldBlur}
/>
{/* Part Level */}
<NumberInput
control={form.control}
name="part_level"
label={buildPartsTranslations[language].part_level}
placeholder={buildPartsTranslations[language].part_level}
onBlurCallback={handleFieldBlur}
/>
{/* Part Code */}
<StringInput
control={form.control}
name="part_code"
label={buildPartsTranslations[language].part_code}
placeholder={buildPartsTranslations[language].part_code}
onBlurCallback={handleFieldBlur}
/>
{/* ----- Size Information ----- */}
{/* Part Gross Size */}
<NumberInput
control={form.control}
name="part_gross_size"
label={buildPartsTranslations[language].part_gross_size}
placeholder={buildPartsTranslations[language].part_gross_size}
onBlurCallback={handleFieldBlur}
/>
{/* Part Net Size */}
<NumberInput
control={form.control}
name="part_net_size"
label={buildPartsTranslations[language].part_net_size}
placeholder={buildPartsTranslations[language].part_net_size}
onBlurCallback={handleFieldBlur}
/>
{/* ----- Additional Properties ----- */}
{/* Default Accessory */}
<StringInput
control={form.control}
name="default_accessory"
label={buildPartsTranslations[language].default_accessory}
placeholder={buildPartsTranslations[language].default_accessory}
onBlurCallback={handleFieldBlur}
/>
{/* Human Livable */}
<CheckBoxInput
control={form.control}
name="human_livable"
label={buildPartsTranslations[language].human_livable}
onBlurCallback={handleFieldBlur}
/>
{/* Due Part Key */}
<StringInput
control={form.control}
name="due_part_key"
label={buildPartsTranslations[language].due_part_key}
placeholder={buildPartsTranslations[language].due_part_key}
onBlurCallback={handleFieldBlur}
/>
{/* Part Direction UUID */}
<StringInput
control={form.control}
name="part_direction_uu_id"
label={buildPartsTranslations[language].part_direction_uu_id}
placeholder={buildPartsTranslations[language].part_direction_uu_id}
onBlurCallback={handleFieldBlur}
/>
{/* ----- Submit Button ----- */}
<Button type="submit" className="w-full mt-8">
{language === 'en' ? translationsOfPage.en.title : translationsOfPage.tr.title}
</Button>
</form>
</Form>
</>
const noSelectedBuildingComponent = <div className="mb-6 p-4 border rounded-md bg-yellow-50 text-center">
<div className="mb-4 text-gray-600">
{buildingTranslations[language].noSelectedBuilding}
</div>
<Button
onClick={() => router.push(`/panel/build`)}
className="bg-blue-600 text-white hover:bg-blue-700"
>
{buildingTranslations[language].selectBuilding}
</Button>
</div>
const mainComponent = <div className="mb-6 p-4 border rounded-md bg-blue-50 shadow-sm relative">
{/* Header with title and change button */}
<div className="flex justify-between items-center mb-2">
<h3 className="text-lg font-medium text-blue-800">
{buildingTranslations[language].selectedBuilding}
</h3>
<Button
onClick={() => router.push(`/panel/build`)}
className="bg-red-100 hover:bg-red-200 text-red-700 p-1 h-8 w-8 rounded-full flex items-center justify-center"
title={buildingTranslations[language].changeBuilding}
>
<X size={16} />
</Button>
</div>
{/* Building details grid */}
<div className="grid grid-cols-2 gap-2">
{/* UUID */}
<div>
<span className="font-semibold">UUID: </span>
{selectedBuilding.uu_id}
</div>
{/* Building Name */}
<div>
<span className="font-semibold">{buildingTranslations[language].buildName}: </span>
{selectedBuilding.build_name}
</div>
{/* Building Number */}
<div>
<span className="font-semibold">{buildingTranslations[language].buildNo}: </span>
{selectedBuilding.build_no}
</div>
{/* Address Code */}
<div>
<span className="font-semibold">{buildingTranslations[language].buildAddressCode}: </span>
{selectedBuilding.gov_address_code}
</div>
{/* Max Floor */}
<div>
<span className="font-semibold">{buildingTranslations[language].buildMaxFloor}: </span>
{selectedBuilding.max_floor}
</div>
</div>
</div>
return (
<div className="w-full max-w-2xl mx-auto p-6 bg-white rounded-lg shadow-md">
{/* back to list button */}
@ -119,100 +293,10 @@ function CreateFromComponentBase({
<Button onClick={() => router.push(listUrl)}>{translationsOfPage[language].back2List}</Button>
</div>
<h2 className="text-2xl font-bold mb-6">{translationsOfPage[language].title}</h2>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
{/* Address Government Code */}
<StringInput
control={form.control}
name="address_gov_code"
label={buildPartsTranslations[language].address_gov_code}
placeholder={buildPartsTranslations[language].address_gov_code}
onBlurCallback={handleFieldBlur}
/>
{/* Part Number */}
<NumberInput
control={form.control}
name="part_no"
label={buildPartsTranslations[language].part_no}
placeholder={buildPartsTranslations[language].part_no}
onBlurCallback={handleFieldBlur}
/>
{/* Part Level */}
<NumberInput
control={form.control}
name="part_level"
label={buildPartsTranslations[language].part_level}
placeholder={buildPartsTranslations[language].part_level}
onBlurCallback={handleFieldBlur}
/>
{/* Part Code */}
<StringInput
control={form.control}
name="part_code"
label={buildPartsTranslations[language].part_code}
placeholder={buildPartsTranslations[language].part_code}
onBlurCallback={handleFieldBlur}
/>
{/* Part Gross Size */}
<NumberInput
control={form.control}
name="part_gross_size"
label={buildPartsTranslations[language].part_gross_size}
placeholder={buildPartsTranslations[language].part_gross_size}
onBlurCallback={handleFieldBlur}
/>
{/* Part Net Size */}
<NumberInput
control={form.control}
name="part_net_size"
label={buildPartsTranslations[language].part_net_size}
placeholder={buildPartsTranslations[language].part_net_size}
onBlurCallback={handleFieldBlur}
/>
{/* Default Accessory */}
<StringInput
control={form.control}
name="default_accessory"
label={buildPartsTranslations[language].default_accessory}
placeholder={buildPartsTranslations[language].default_accessory}
onBlurCallback={handleFieldBlur}
/>
{/* Human Livable */}
<CheckBoxInput
control={form.control}
name="human_livable"
label={buildPartsTranslations[language].human_livable}
onBlurCallback={handleFieldBlur}
/>
{/* Due Part Key */}
<StringInput
control={form.control}
name="due_part_key"
label={buildPartsTranslations[language].due_part_key}
placeholder={buildPartsTranslations[language].due_part_key}
onBlurCallback={handleFieldBlur}
/>
{/* Part Direction UUID */}
<StringInput
control={form.control}
name="part_direction_uu_id"
label={buildPartsTranslations[language].part_direction_uu_id}
placeholder={buildPartsTranslations[language].part_direction_uu_id}
onBlurCallback={handleFieldBlur}
/>
<Button type="submit" className="w-full">{language === 'en' ? translationsOfPage.en.title : translationsOfPage.tr.title}</Button>
</form>
</Form>
{/* ===== BUILDING SELECTION SECTION ===== */}
{selectedBuilding ? mainComponent : noSelectedBuildingComponent}
{/* ===== FORM SECTION ===== */}
{selectedBuilding && formComponent}
</div>
);
}

View File

@ -1,542 +0,0 @@
'use client';
import React from "react";
import {
useReactTable,
getCoreRowModel,
getSortedRowModel,
getPaginationRowModel,
flexRender,
createColumnHelper,
ColumnDef,
} from "@tanstack/react-table";
import { buildPartsTranslations } from './translations';
import { UseFormReturn } from "react-hook-form";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage
} from "@/components/mutual/ui/form";
import { Button } from "@/components/mutual/ui/button";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/mutual/ui/select";
import { API_BASE_URL } from "@/config/config";
import { useTableData } from "@/hooks/useTableData";
import { LanguageTypes } from "@/validations/mutual/language/validations";
import {
Translations,
TableHeaderProps,
LoadingSpinnerProps,
ErrorDisplayProps,
MobilePaginationControlsProps,
DashboardPageProps,
} from "@/validations/mutual/table/validations";
import LoadingContent from "@/components/mutual/loader/component";
import { Pencil } from "lucide-react";
import { useRouter } from "next/navigation";
import { setCacheData } from "@/components/mutual/context/cache/context";
import { BuildPartsSchemaInterface } from "./schema";
interface DataTableProps {
table: ReturnType<typeof useReactTable>;
tableData: BuildPartsSchemaInterface[];
isLoading: boolean;
handleSortingChange: (columnId: string) => void;
getSortingIcon: (columnId: string) => React.ReactNode;
flexRender: typeof flexRender;
t: Translations;
}
interface TableFormProps {
form: UseFormReturn<any>;
handleFormSubmit: (e: React.FormEvent) => void;
handleSelectChange: (value: string, field: { onChange: (value: number) => void }) => void;
renderPageOptions: () => { key: string | number; value: string; label: string }[];
pageSizeOptions: number[];
apiPagination: {
size: number;
page: number;
totalCount: number;
totalPages: number;
pageCount: number;
};
handleFirstPage: () => void;
handlePreviousPage: () => void;
handleNextPage: () => void;
isPreviousDisabled: () => boolean;
isNextDisabled: () => boolean;
t: Translations;
}
const translations: Record<LanguageTypes, Translations> = {
en: {
dataTable: 'Data Table',
tableWithApiData: 'Table with API Data',
loading: 'Loading...',
noDataAvailable: 'No data available',
page: 'Page',
size: 'Size',
total: 'Total',
items: 'items',
first: 'First',
previous: 'Previous',
next: 'Next',
selectPage: 'Select page',
selectSize: 'Select size',
actionButtonGroup: 'Actions',
},
tr: {
dataTable: 'Veri Tablosu',
tableWithApiData: 'API Verili Tablo',
loading: 'Yükleniyor...',
noDataAvailable: 'Veri bulunamadı',
page: 'Sayfa',
size: 'Boyut',
total: 'Toplam',
items: 'öğe',
first: 'İlk',
previous: 'Önceki',
next: 'Sonraki',
selectPage: 'Sayfa seç',
selectSize: 'Boyut seç',
actionButtonGroup: 'Eylemler',
}
};
const DataTable: React.FC<DataTableProps> = React.memo(({
table,
tableData,
isLoading,
handleSortingChange,
getSortingIcon,
flexRender,
t,
}) => {
return (
<div className="overflow-x-auto relative">
{/* Semi-transparent loading overlay that preserves interactivity */}
{isLoading && (
<div className="absolute inset-0 bg-white bg-opacity-60 flex items-center justify-center z-10">
{/* We don't put anything here as we already have the loading indicator in the header */}
</div>
)}
<table className="min-w-full divide-y divide-gray-200">
<caption className="sr-only">{t.dataTable}</caption>
<thead className="bg-gray-50">
{table.getHeaderGroups().map((headerGroup) => (
<tr key={headerGroup.id}>
{headerGroup.headers.map((header) => {
return (
<th
key={header.id}
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer"
onClick={() => handleSortingChange(header.column.id)}
aria-sort={header.column.getIsSorted() ? (header.column.getIsSorted() === 'desc' ? 'descending' : 'ascending') : undefined}
scope="col"
>
<div className="flex items-center space-x-1">
{flexRender(header.column.columnDef.header, header.getContext())}
<span>{getSortingIcon(header.column.id)}</span>
</div>
</th>
);
})}
</tr>
))}
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{tableData.length === 0 && !isLoading ? (
<tr>
<td colSpan={table.getAllColumns().length} className="px-6 py-4 text-center text-gray-500">
{t.noDataAvailable}
</td>
</tr>
) : (
table.getRowModel().rows.map((row) => (
<tr key={row.id} className="hover:bg-gray-50">
{row.getVisibleCells().map((cell) => {
return (
<td key={cell.id} className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</td>
);
})}
</tr>
))
)}
</tbody>
</table>
</div>
);
});
const TableForm: React.FC<TableFormProps> = ({
form,
handleFormSubmit,
handleSelectChange,
renderPageOptions,
pageSizeOptions,
apiPagination,
handleFirstPage,
handlePreviousPage,
handleNextPage,
isPreviousDisabled,
isNextDisabled,
t
}) => {
return (
<div className="p-4 border-b border-gray-200">
<Form {...form}>
<form onSubmit={handleFormSubmit} className="grid grid-cols-1 md:grid-cols-4 gap-4">
<div className="md:col-span-1">
<FormField
control={form.control}
name="page"
render={({ field }) => (
<FormItem>
<FormLabel>{t.page}</FormLabel>
<Select
value={field.value.toString()}
onValueChange={(value: string) => handleSelectChange(value, field)}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder={t.selectPage} />
</SelectTrigger>
</FormControl>
<SelectContent>
{renderPageOptions().length > 0 ? (
renderPageOptions().map((option: { key: string | number; value: string; label: string }) => (
<SelectItem key={option.key} value={option.value}>
{option.label}
</SelectItem>
))
) : (
<SelectItem key="1" value="1">1</SelectItem>
)}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className="md:col-span-1">
<FormField
control={form.control}
name="size"
render={({ field }) => (
<FormItem>
<FormLabel>{t.size}</FormLabel>
<Select
value={field.value.toString()}
onValueChange={(value: string) => handleSelectChange(value, field)}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder={t.selectSize} />
</SelectTrigger>
</FormControl>
<SelectContent>
{pageSizeOptions.map((size: number) => (
<SelectItem key={size} value={size.toString()}>
{size}
</SelectItem>
))}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className="md:col-span-1 flex items-end">
<p className="text-sm text-gray-700">
{t.page}: <span className="font-medium">{apiPagination.page}</span><span>{" / "}</span> <span className="font-medium">{apiPagination.totalPages}</span> ·
{t.size}: <span className="font-medium">{apiPagination.pageCount}</span><span>{" / "}</span> <span className="font-medium">{apiPagination.size}</span> ·
{t.total}: <span className="font-medium">{apiPagination.totalCount}</span> {t.items}
</p>
</div>
<div className="md:col-span-1 flex items-end justify-end space-x-2">
<Button
onClick={handleFirstPage}
disabled={isPreviousDisabled()}
variant="outline"
size="sm"
aria-label="Go to first page"
>{t.first}</Button>
<Button
onClick={handlePreviousPage}
disabled={isPreviousDisabled()}
variant="outline"
size="sm"
aria-label="Go to previous page"
>{t.previous}</Button>
<Button
onClick={handleNextPage}
disabled={isNextDisabled()}
variant="outline"
size="sm"
aria-label="Go to next page"
>{t.next}</Button>
{/* <Button type="submit" disabled={isLoading} size="sm">Fetch</Button> */}
</div>
</form>
</Form>
</div>
);
};
const MobilePaginationControls: React.FC<MobilePaginationControlsProps> = ({
handlePreviousPage,
handleNextPage,
isPreviousDisabled,
isNextDisabled,
t
}) => {
return (
<div className="px-4 py-3 flex items-center justify-between border-t border-gray-200 sm:hidden">
<div className="flex-1 flex justify-between">
<Button
onClick={handlePreviousPage}
disabled={isPreviousDisabled()}
variant="outline"
size="sm"
aria-label="Go to previous page"
>{t.previous}</Button>
<Button
onClick={handleNextPage}
disabled={isNextDisabled()}
variant="outline"
size="sm"
className="ml-2"
aria-label="Go to next page"
>{t.next}</Button>
</div>
</div>
);
};
const TableHeader: React.FC<TableHeaderProps> = ({ title, description, isLoading, error, t }) => {
return (
<div className="p-4 border-b border-gray-200">
<div className="flex justify-between items-center">
<div>
<h2 className="text-lg font-semibold text-gray-800">{title}</h2>
<p className="text-sm text-gray-500">{description}</p>
</div>
{/* {isLoading && <LoadingContent height="h-16" size="w-36 h-48" plane="h-full w-full" />} */}
{error && <ErrorDisplay message={error} />}
</div>
</div>
);
};
const ErrorDisplay: React.FC<ErrorDisplayProps> = ({ message }) => {
return <div className="text-red-500">{message}</div>;
};
const BuildListPage: React.FC<DashboardPageProps> = React.memo((props) => {
// Initialize translation with English as default
const language = props.onlineData?.lang as LanguageTypes || 'en';
const t = translations[language];
const searchParams = props.searchParams;
console.log('searchParams', searchParams);
const router = useRouter();
const {
form,
tableData,
sorting,
isLoading,
error,
pagination,
apiPagination,
setSorting,
handleSortingChange,
handleSelectChange,
handlePageChange,
handleFirstPage,
handlePreviousPage,
handleNextPage,
getSortingIcon,
handleFormSubmit,
// Disabled states
isPreviousDisabled,
isNextDisabled,
pageSizeOptions,
renderPageOptions,
} = useTableData({ apiUrl: `${API_BASE_URL}/parts/list` });
const activePageUrl = props.activePageUrl || '';
const handleEditRow = async (row: BuildPartsSchemaInterface) => {
try {
// Store the row data in the cache
await setCacheData(`${activePageUrl}/update`, row);
console.log('Row data stored in cache:', row);
// Navigate to the update form
router.push(`/panel/${activePageUrl}/update`);
} catch (error) {
console.error('Error storing row data in cache:', error);
}
};
const actionButtonGroup = (info: any) => (
<Button
variant="ghost"
size="icon"
onClick={(e) => {
e.preventDefault();
console.log('Edit button clicked');
handleEditRow && handleEditRow(info.row.original);
}}
className="h-8 w-8 p-0"
>
<Pencil className="h-4 w-4" />
<span className="sr-only">Edit</span>
</Button>
);
const columnHelper = createColumnHelper<BuildPartsSchemaInterface>();
const columns = React.useMemo(() => [
columnHelper.display({
id: 'actions',
header: () => <span>{t.actionButtonGroup}</span>,
cell: (info) => { return (actionButtonGroup(info)) }
}),
// columnHelper.accessor('uu_id', {
// cell: info => info.getValue(),
// header: () => <span>{buildPartsTranslations[language].uu_id}</span>,
// footer: info => info.column.id
// }),
// columnHelper.accessor('build_uu_id', {
// cell: info => info.getValue(),
// header: () => <span>{buildPartsTranslations[language].build_uu_id}</span>,
// footer: info => info.column.id
// }),
columnHelper.accessor('address_gov_code', {
cell: info => info.getValue(),
header: () => <span>{buildPartsTranslations[language].address_gov_code}</span>,
footer: info => info.column.id
}),
columnHelper.accessor('part_no', {
cell: info => String(info.getValue()),
header: () => <span>{buildPartsTranslations[language].part_no}</span>,
footer: info => info.column.id
}),
columnHelper.accessor('part_level', {
cell: info => String(info.getValue()),
header: () => <span>{buildPartsTranslations[language].part_level}</span>,
footer: info => info.column.id
}),
columnHelper.accessor('part_code', {
cell: info => info.getValue(),
header: () => <span>{buildPartsTranslations[language].part_code}</span>,
footer: info => info.column.id
}),
columnHelper.accessor('part_gross_size', {
cell: info => String(info.getValue()),
header: () => <span>{buildPartsTranslations[language].part_gross_size}</span>,
footer: info => info.column.id
}),
columnHelper.accessor('part_net_size', {
cell: info => String(info.getValue()),
header: () => <span>{buildPartsTranslations[language].part_net_size}</span>,
footer: info => info.column.id
}),
columnHelper.accessor('default_accessory', {
cell: info => info.getValue(),
header: () => <span>{buildPartsTranslations[language].default_accessory}</span>,
footer: info => info.column.id
}),
columnHelper.accessor('human_livable', {
cell: info => info.getValue() ? 'Yes' : 'No',
header: () => <span>{buildPartsTranslations[language].human_livable}</span>,
footer: info => info.column.id
}),
columnHelper.accessor('due_part_key', {
cell: info => info.getValue(),
header: () => <span>{buildPartsTranslations[language].due_part_key}</span>,
footer: info => info.column.id
}),
columnHelper.accessor('part_direction_uu_id', {
cell: info => info.getValue() || '',
header: () => <span>{buildPartsTranslations[language].part_direction_uu_id}</span>,
footer: info => info.column.id
}),
], [columnHelper]) as ColumnDef<BuildPartsSchemaInterface>[];
const table = useReactTable({
data: tableData,
columns,
state: { sorting, pagination },
onSortingChange: setSorting,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
getPaginationRowModel: getPaginationRowModel(),
manualPagination: true,
pageCount: apiPagination.totalPages || 1,
})
return (
isLoading ? <LoadingContent height="h-48" size="w-36 h-36" plane="h-full w-full" /> :
<div className="bg-white rounded-lg shadow-md overflow-hidden mb-20">
<div className="flex justify-between items-center p-4">
<TableHeader
title={t.dataTable}
description={t.tableWithApiData}
isLoading={isLoading}
error={error}
t={t}
/>
<Button
onClick={() => router.push(`/panel/${activePageUrl}/create`)}
className="bg-primary text-white hover:bg-primary/90"
>
<span className="mr-2">Create New</span>
<span className="sr-only">Create new item</span>
</Button>
</div>
<TableForm
form={form}
handleFormSubmit={handleFormSubmit}
handleSelectChange={handleSelectChange}
renderPageOptions={renderPageOptions}
pageSizeOptions={pageSizeOptions}
apiPagination={apiPagination}
handleFirstPage={handleFirstPage}
handlePreviousPage={handlePreviousPage}
handleNextPage={handleNextPage}
isPreviousDisabled={isPreviousDisabled}
isNextDisabled={isNextDisabled}
t={t}
/>
{/* Mobile pagination controls - only visible on small screens */}
<MobilePaginationControls
handlePreviousPage={handlePreviousPage}
handleNextPage={handleNextPage}
isPreviousDisabled={isPreviousDisabled}
isNextDisabled={isNextDisabled}
t={t}
/>
<DataTable
table={table}
tableData={tableData}
isLoading={isLoading}
handleSortingChange={handleSortingChange}
getSortingIcon={getSortingIcon}
flexRender={flexRender}
t={t}
/>
</div>
);
});
export default BuildListPage;

View File

@ -11,28 +11,13 @@ import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { LanguageTypes } from "@/validations/mutual/language/validations";
import { buildPartsSchema, BuildPartsInterface } from "./schema";
import { buildPartsSchema, updateEmptyValues } from "./schema";
import { buildPartsTranslations, translationsOfPage } from "./translations";
import { CheckBoxInput } from "@/components/mutual/formInputs/CheckBoxInput";
import { NumberInput } from "@/components/mutual/formInputs/NumberInput";
import { StringInput } from "@/components/mutual/formInputs/StringInput";
const emptyValues: BuildPartsInterface = {
uu_id: "",
build_uu_id: null,
address_gov_code: "",
part_no: 0,
part_level: 0,
part_code: "",
part_gross_size: 0,
part_net_size: 0,
default_accessory: "0",
human_livable: true,
due_part_key: "",
part_direction_uu_id: null
};
function UpdateFromComponentBase({
onlineData,
cacheData,
@ -57,11 +42,7 @@ function UpdateFromComponentBase({
const [partUUID, setPartUUID] = useState<string>(""); const router = useRouter();
const listUrl = `/panel/${activePageUrl?.replace("/update", "")}`;
const form = useForm({
resolver: zodResolver(buildPartsSchema),
mode: "onSubmit",
defaultValues: emptyValues
});
const form = useForm({ resolver: zodResolver(buildPartsSchema), mode: "onSubmit", defaultValues: updateEmptyValues });
useEffect(() => {
const fetchData = async () => {
@ -108,10 +89,7 @@ function UpdateFromComponentBase({
const onSubmit = async (data: any) => {
try {
console.log('Form data received:', data);
const formattedData = {
...data,
uuid: partUUID
};
const formattedData = { ...data, uuid: partUUID };
const response = await apiPostFetcher<any>({
url: `/api/parts/update/${partUUID}`,
isNoCache: true,
@ -119,7 +97,7 @@ function UpdateFromComponentBase({
});
await clearCacheData(activePageUrl);
if (clearCache) { clearCache(activePageUrl); }
if (response.success) { form.reset(emptyValues); router.push(listUrl) }
if (response.success) { form.reset(updateEmptyValues); router.push(listUrl) }
} catch (error) { console.error("Error submitting form:", error); }
}

View File

@ -0,0 +1,142 @@
'use client';
import React from "react";
import LoadingContent from "@/components/mutual/loader/component";
import { useRouter } from "next/navigation";
import { useReactTable, getCoreRowModel, getSortedRowModel, getPaginationRowModel, flexRender, ColumnDef } from "@tanstack/react-table";
import { Button } from "@/components/mutual/ui/button";
import { getCacheData } from "@/components/mutual/context/cache/context";
import { API_BASE_URL } from "@/config/config";
import { useTableData } from "@/hooks/useTableData";
import { LanguageTypes } from "@/validations/mutual/language/validations";
import { DashboardPageProps } from "@/validations/mutual/table/validations";
import { BuildPartsSchemaInterface, listTranslations } from "../schema";
import DataTable from "./dataTable";
import TableForm from "./tableForm";
import MobilePaginationControls from "./mobilePaginationControls";
import TableHeader from "./tableHeader";
import { columnHelper, retrieveColumns } from "./columns";
const BuildListPage: React.FC<DashboardPageProps> = React.memo((props) => {
const router = useRouter();
const activePageUrl = props.activePageUrl || '';
const language = props.onlineData?.lang as LanguageTypes || 'en';
const t = listTranslations[language];
const apiUrl = `${API_BASE_URL}/parts/list`;
const [selectedBuilding, setSelectedBuilding] = React.useState<any>(null);
const {
tableData, isLoading, error, form, pagination, apiPagination, setSorting, handleSortingChange, handleSelectChange,
handleFirstPage, handlePreviousPage, handleNextPage, getSortingIcon, handleFormSubmit,
isPreviousDisabled, isNextDisabled, pageSizeOptions, renderPageOptions, sorting, // Disabled states
} = useTableData({
apiUrl,
mapFormToRequestBody: (values) => {
const requestBody = {
page: values.page, size: values.size, orderField: values.orderField, orderType: values.orderType,
query: selectedBuilding ? { build_uu_id: selectedBuilding.uu_id } : {}
}; return requestBody;
}
});
React.useEffect(() => {
const checkCacheForSelectedBuild = async () => {
try {
const cachedData = await getCacheData("/build/list");
if (cachedData && cachedData.build) { setSelectedBuilding(cachedData.build) }
} catch (error) { console.error("Error checking cache for selected build:", error) }
};
checkCacheForSelectedBuild();
}, []);
React.useEffect(() => {
if (selectedBuilding) { form.setValue("page", 1); form.setValue("query", { build_uu_id: selectedBuilding.uu_id }) }
}, [selectedBuilding, form]);
const columnsArray = retrieveColumns(language, activePageUrl, router);
const columns = React.useMemo(() => columnsArray, [columnHelper]) as ColumnDef<BuildPartsSchemaInterface>[];
const table = useReactTable({
data: tableData,
columns,
state: { sorting, pagination },
onSortingChange: setSorting,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
getPaginationRowModel: getPaginationRowModel(),
manualPagination: true,
pageCount: apiPagination.totalPages || 1,
})
const dataTableComponent = <DataTable
table={table}
tableData={tableData}
isLoading={isLoading}
handleSortingChange={handleSortingChange}
getSortingIcon={getSortingIcon}
flexRender={flexRender}
t={t}
selectedBuilding={selectedBuilding}
router={router}
/>
const noSelectedBuildingComponent = <div className="p-8 text-center">
<div className="mb-4 text-gray-600">{t.noSelectedBuilding || "No building selected. Please select a building first."}</div>
<Button onClick={() => router.push(`/panel/build`)} className="bg-blue-600 text-white hover:bg-blue-700">
{t.selectBuilding || "Select Building"}
</Button>
</div>
return (
isLoading ? <LoadingContent height="h-48" size="w-36 h-36" plane="h-full w-full" /> :
<div className="bg-white rounded-lg shadow-md overflow-hidden mb-20">
<div className="flex justify-between items-center p-4">
<TableHeader
title={t.dataTable}
description={t.tableWithApiData}
isLoading={isLoading}
error={error}
t={t}
/>
<Button onClick={() => router.push(`/panel/${activePageUrl}/create`)} className="bg-primary text-white hover:bg-primary/90">
<span className="mr-2">{t.createNewBuildPart}</span>
<span className="sr-only">{t.createNewBuildPart}</span>
</Button>
</div>
<TableForm
form={form}
handleFormSubmit={handleFormSubmit}
handleSelectChange={handleSelectChange}
renderPageOptions={renderPageOptions}
pageSizeOptions={pageSizeOptions}
apiPagination={apiPagination}
handleFirstPage={handleFirstPage}
handlePreviousPage={handlePreviousPage}
handleNextPage={handleNextPage}
isPreviousDisabled={isPreviousDisabled}
isNextDisabled={isNextDisabled}
t={t}
/>
{/* Mobile pagination controls - only visible on small screens */}
<MobilePaginationControls
handlePreviousPage={handlePreviousPage}
handleNextPage={handleNextPage}
isPreviousDisabled={isPreviousDisabled}
isNextDisabled={isNextDisabled}
t={t}
/>
{selectedBuilding ? dataTableComponent : noSelectedBuildingComponent}
</div >
);
});
export default BuildListPage;

View File

@ -0,0 +1,104 @@
import {
useReactTable,
getCoreRowModel,
getSortedRowModel,
getPaginationRowModel,
flexRender,
createColumnHelper,
ColumnDef,
} from "@tanstack/react-table";
import { BuildPartsSchemaInterface } from "../schema";
import { buildPartsTranslations, translationsOfPage } from '../translations';
import { LanguageTypes } from "@/validations/mutual/language/validations";
import { Button } from "@/components/mutual/ui/button";
import { Pencil } from "lucide-react";
import { setCacheData } from "@/components/mutual/context/cache/context";
const columnHelper = createColumnHelper<BuildPartsSchemaInterface>();
function retrieveColumns(language: LanguageTypes, activePageUrl: string, router: any) {
const handleEditRow = async (row: BuildPartsSchemaInterface) => {
try {
await setCacheData(`${activePageUrl}/update`, row);
router.push(`/panel/${activePageUrl}/update`);
} catch (error) { console.error('Error storing row data in cache:', error) }
};
const actionButtonGroup = (info: any) => (
<Button
variant="ghost"
size="icon"
onClick={(e) => {
e.preventDefault();
console.log('Edit button clicked');
handleEditRow && handleEditRow(info.row.original);
}}
className="h-8 w-8 p-0"
>
<Pencil className="h-4 w-4" />
<span className="sr-only">Edit</span>
</Button>
);
const columnsArray = [
columnHelper.display({
id: 'actions',
header: () => <span>{translationsOfPage[language].actionButtonGroup}</span>,
cell: (info) => { return (actionButtonGroup(info)) }
}),
columnHelper.accessor('address_gov_code', {
cell: info => info.getValue(),
header: () => <span>{buildPartsTranslations[language].address_gov_code}</span>,
footer: info => info.column.id
}),
columnHelper.accessor('part_no', {
cell: info => String(info.getValue()),
header: () => <span>{buildPartsTranslations[language].part_no}</span>,
footer: info => info.column.id
}),
columnHelper.accessor('part_level', {
cell: info => String(info.getValue()),
header: () => <span>{buildPartsTranslations[language].part_level}</span>,
footer: info => info.column.id
}),
columnHelper.accessor('part_code', {
cell: info => info.getValue(),
header: () => <span>{buildPartsTranslations[language].part_code}</span>,
footer: info => info.column.id
}),
columnHelper.accessor('part_gross_size', {
cell: info => String(info.getValue()),
header: () => <span>{buildPartsTranslations[language].part_gross_size}</span>,
footer: info => info.column.id
}),
columnHelper.accessor('part_net_size', {
cell: info => String(info.getValue()),
header: () => <span>{buildPartsTranslations[language].part_net_size}</span>,
footer: info => info.column.id
}),
columnHelper.accessor('default_accessory', {
cell: info => info.getValue(),
header: () => <span>{buildPartsTranslations[language].default_accessory}</span>,
footer: info => info.column.id
}),
columnHelper.accessor('human_livable', {
cell: info => info.getValue() ? 'Yes' : 'No',
header: () => <span>{buildPartsTranslations[language].human_livable}</span>,
footer: info => info.column.id
}),
columnHelper.accessor('due_part_key', {
cell: info => info.getValue(),
header: () => <span>{buildPartsTranslations[language].due_part_key}</span>,
footer: info => info.column.id
}),
columnHelper.accessor('part_direction_uu_id', {
cell: info => info.getValue() || '',
header: () => <span>{buildPartsTranslations[language].part_direction_uu_id}</span>,
footer: info => info.column.id
}),
]
return columnsArray;
}
export {
retrieveColumns,
columnHelper
}

View File

@ -0,0 +1,129 @@
'use client';
import React from "react";
import { Button } from "@/components/mutual/ui/button";
import { X } from "lucide-react";
import { useReactTable } from "@tanstack/react-table";
import { flexRender } from "@tanstack/react-table";
import { BuildPartsSchemaInterface } from "../schema";
import { Translations } from "./types";
interface DataTableProps {
table: ReturnType<typeof useReactTable>;
tableData: BuildPartsSchemaInterface[];
isLoading: boolean;
handleSortingChange: (columnId: string) => void;
getSortingIcon: (columnId: string) => React.ReactNode;
flexRender: typeof flexRender;
t: Translations;
selectedBuilding: any | null;
router: any;
}
const DataTable: React.FC<DataTableProps> = React.memo(({
table,
tableData,
isLoading,
handleSortingChange,
getSortingIcon,
flexRender,
t,
selectedBuilding,
router,
}) => {
return (
<div className="overflow-x-auto relative">
{/* Semi-transparent loading overlay that preserves interactivity */}
{isLoading && (
<div className="absolute inset-0 bg-white bg-opacity-60 flex items-center justify-center z-10">
{/* We don't put anything here as we already have the loading indicator in the header */}
</div>
)}
{selectedBuilding && (
<div className="mb-4 p-4 border rounded-md bg-blue-50 shadow-sm relative">
<div className="flex justify-between items-center mb-2">
<h3 className="text-lg font-medium text-blue-800">{t.selectedBuilding}</h3>
<Button
onClick={() => router.push(`/panel/build`)}
className="bg-red-100 hover:bg-red-200 text-red-700 p-1 h-8 w-8 rounded-full flex items-center justify-center"
title={t.changeBuilding || "Change Building"}
>
<X size={16} />
</Button>
</div>
<div className="grid grid-cols-2 gap-2">
<div>
<span className="font-semibold">UUID: </span>
{selectedBuilding.uu_id}
</div>
<div>
<span className="font-semibold">{t.buildName}: </span>
{selectedBuilding.build_name}
</div>
<div>
<span className="font-semibold">{t.buildNo}: </span>
{selectedBuilding.build_no}
</div>
<div>
<span className="font-semibold">{t.buildAddressCode}: </span>
{selectedBuilding.gov_address_code}
</div>
<div>
<span className="font-semibold">{t.buildMaxFloor}: </span>
{selectedBuilding.max_floor}
</div>
</div>
</div>
)}
<table className="min-w-full divide-y divide-gray-200">
<caption className="sr-only">{t.dataTable}</caption>
<thead className="bg-gray-50">
{table.getHeaderGroups().map((headerGroup) => (
<tr key={headerGroup.id}>
{headerGroup.headers.map((header) => {
return (
<th
key={header.id}
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer"
onClick={() => handleSortingChange(header.column.id)}
aria-sort={header.column.getIsSorted() ? (header.column.getIsSorted() === 'desc' ? 'descending' : 'ascending') : undefined}
scope="col"
>
<div className="flex items-center space-x-1">
{flexRender(header.column.columnDef.header, header.getContext())}
<span>{getSortingIcon(header.column.id)}</span>
</div>
</th>
);
})}
</tr>
))}
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{tableData.length === 0 && !isLoading ? (
<tr>
<td colSpan={table.getAllColumns().length} className="px-6 py-4 text-center text-gray-500">
{t.noDataAvailable}
</td>
</tr>
) : (
table.getRowModel().rows.map((row) => (
<tr key={row.id} className="hover:bg-gray-50">
{row.getVisibleCells().map((cell) => {
return (
<td key={cell.id} className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</td>
);
})}
</tr>
))
)}
</tbody>
</table>
</div>
);
});
export default DataTable;

View File

@ -0,0 +1,34 @@
import { MobilePaginationControlsProps } from "@/validations/mutual/table/validations";
import { Button } from "@/components/mutual/ui/button";
const MobilePaginationControls: React.FC<MobilePaginationControlsProps> = ({
handlePreviousPage,
handleNextPage,
isPreviousDisabled,
isNextDisabled,
t
}) => {
return (
<div className="px-4 py-3 flex items-center justify-between border-t border-gray-200 sm:hidden">
<div className="flex-1 flex justify-between">
<Button
onClick={handlePreviousPage}
disabled={isPreviousDisabled()}
variant="outline"
size="sm"
aria-label="Go to previous page"
>{t.previous}</Button>
<Button
onClick={handleNextPage}
disabled={isNextDisabled()}
variant="outline"
size="sm"
className="ml-2"
aria-label="Go to next page"
>{t.next}</Button>
</div>
</div>
);
};
export default MobilePaginationControls;

View File

@ -0,0 +1,152 @@
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage
} from "@/components/mutual/ui/form";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/mutual/ui/select";
import { UseFormReturn } from "react-hook-form";
import { Translations } from "./types";
import { Button } from "@/components/mutual/ui/button";
interface TableFormProps {
form: UseFormReturn<any>;
handleFormSubmit: (e: React.FormEvent) => void;
handleSelectChange: (value: string, field: { onChange: (value: number) => void }) => void;
renderPageOptions: () => { key: string | number; value: string; label: string }[];
pageSizeOptions: number[];
apiPagination: {
size: number;
page: number;
totalCount: number;
totalPages: number;
pageCount: number;
};
handleFirstPage: () => void;
handlePreviousPage: () => void;
handleNextPage: () => void;
isPreviousDisabled: () => boolean;
isNextDisabled: () => boolean;
t: Translations;
}
const TableForm: React.FC<TableFormProps> = ({
form,
handleFormSubmit,
handleSelectChange,
renderPageOptions,
pageSizeOptions,
apiPagination,
handleFirstPage,
handlePreviousPage,
handleNextPage,
isPreviousDisabled,
isNextDisabled,
t
}) => {
return (
<div className="p-4 border-b border-gray-200">
<Form {...form}>
<form onSubmit={handleFormSubmit} className="grid grid-cols-1 md:grid-cols-4 gap-4">
<div className="md:col-span-1">
<FormField
control={form.control}
name="page"
render={({ field }) => (
<FormItem>
<FormLabel>{t.page}</FormLabel>
<Select
value={field.value.toString()}
onValueChange={(value: string) => handleSelectChange(value, field)}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder={t.selectPage} />
</SelectTrigger>
</FormControl>
<SelectContent>
{renderPageOptions().length > 0 ? (
renderPageOptions().map((option: { key: string | number; value: string; label: string }) => (
<SelectItem key={option.key} value={option.value}>
{option.label}
</SelectItem>
))
) : (
<SelectItem key="1" value="1">1</SelectItem>
)}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className="md:col-span-1">
<FormField
control={form.control}
name="size"
render={({ field }) => (
<FormItem>
<FormLabel>{t.size}</FormLabel>
<Select
value={field.value.toString()}
onValueChange={(value: string) => handleSelectChange(value, field)}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder={t.selectSize} />
</SelectTrigger>
</FormControl>
<SelectContent>
{pageSizeOptions.map((size: number) => (
<SelectItem key={size} value={size.toString()}>
{size}
</SelectItem>
))}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className="md:col-span-1 flex items-end">
<p className="text-sm text-gray-700">
{t.page}: <span className="font-medium">{apiPagination.page}</span><span>{" / "}</span> <span className="font-medium">{apiPagination.totalPages}</span> ·
{t.size}: <span className="font-medium">{apiPagination.pageCount}</span><span>{" / "}</span> <span className="font-medium">{apiPagination.size}</span> ·
{t.total}: <span className="font-medium">{apiPagination.totalCount}</span> {t.items}
</p>
</div>
<div className="md:col-span-1 flex items-end justify-end space-x-2">
<Button
onClick={handleFirstPage}
disabled={isPreviousDisabled()}
variant="outline"
size="sm"
aria-label="Go to first page"
>{t.first}</Button>
<Button
onClick={handlePreviousPage}
disabled={isPreviousDisabled()}
variant="outline"
size="sm"
aria-label="Go to previous page"
>{t.previous}</Button>
<Button
onClick={handleNextPage}
disabled={isNextDisabled()}
variant="outline"
size="sm"
aria-label="Go to next page"
>{t.next}</Button>
{/* <Button type="submit" disabled={isLoading} size="sm">Fetch</Button> */}
</div>
</form>
</Form>
</div>
);
};
export default TableForm;

View File

@ -0,0 +1,24 @@
import { TableHeaderProps, ErrorDisplayProps } from "@/validations/mutual/table/validations";
const ErrorDisplay: React.FC<ErrorDisplayProps> = ({ message }) => {
return <div className="text-red-500">{message}</div>;
};
const TableHeader: React.FC<TableHeaderProps> = ({ title, description, isLoading, error, t }) => {
return (
<div className="p-4 border-b border-gray-200">
<div className="flex justify-between items-center">
<div>
<h2 className="text-lg font-semibold text-gray-800">{title}</h2>
<p className="text-sm text-gray-500">{description}</p>
</div>
{/* {isLoading && <LoadingContent height="h-16" size="w-36 h-48" plane="h-full w-full" />} */}
{error && <ErrorDisplay message={error} />}
</div>
</div>
);
};
export default TableHeader;

View File

@ -0,0 +1,15 @@
import { Translations as BaseTranslations } from "@/validations/mutual/table/validations";
// Extended Translations interface to include building-related translations
interface Translations extends BaseTranslations {
selectedBuilding: string;
buildName: string;
buildNo: string;
buildAddressCode: string;
buildMaxFloor: string;
noSelectedBuilding: string;
selectBuilding: string;
changeBuilding: string;
}
export type { Translations };

View File

@ -1,3 +1,4 @@
import { LanguageTypes } from "@/validations/mutual/language/validations";
import * as z from "zod";
/**
@ -48,9 +49,99 @@ interface BuildPartsSchemaInterface {
part_direction_uu_id: string | null;
}
const createEmptyValues: BuildPartsCreateInterface = {
build_uu_id: null,
address_gov_code: "",
part_no: 0,
part_level: 0,
part_code: "",
part_gross_size: 0,
part_net_size: 0,
default_accessory: "0",
human_livable: true,
due_part_key: "",
part_direction_uu_id: null,
};
const updateEmptyValues: BuildPartsInterface = {
uu_id: "",
build_uu_id: null,
address_gov_code: "",
part_no: 0,
part_level: 0,
part_code: "",
part_gross_size: 0,
part_net_size: 0,
default_accessory: "0",
human_livable: true,
due_part_key: "",
part_direction_uu_id: null,
};
const listTranslations: Record<LanguageTypes, any> = {
en: {
createNew: "Create New",
createNewBuildPart: "Create New Build Part",
dataTable: "Data Table",
tableWithApiData: "Table with API Data",
loading: "Loading...",
noDataAvailable: "No data available",
page: "Page",
size: "Size",
total: "Total",
items: "items",
first: "First",
previous: "Previous",
next: "Next",
selectPage: "Select page",
selectSize: "Select size",
actionButtonGroup: "Actions",
selectedBuilding: "Selected Building",
buildName: "Building Name",
buildNo: "Building No",
buildAddressCode: "Building Address Code",
buildMaxFloor: "Building Max Floor",
noSelectedBuilding: "No building selected. Please select a building first.",
selectBuilding: "Select Building",
changeBuilding: "Change Building",
},
tr: {
createNew: "Yeni Oluştur",
createNewBuildPart: "Yeni Bina Parçası Oluştur",
dataTable: "Veri Tablosu",
tableWithApiData: "API Verili Tablo",
loading: "Yükleniyor...",
noDataAvailable: "Veri bulunamadı",
page: "Sayfa",
size: "Boyut",
total: "Toplam",
items: "öğe",
first: "İlk",
previous: "Önceki",
next: "Sonraki",
selectPage: "Sayfa seç",
selectSize: "Boyut seç",
actionButtonGroup: "Eylemler",
selectedBuilding: "Seçilen Bina",
buildName: "Bina Adı",
buildNo: "Bina No",
buildAddressCode: "Bina Adres Kodu",
buildMaxFloor: "Bina Max Kat",
noSelectedBuilding: "Bina seçilmedi. Lütfen önce bir bina seçin.",
selectBuilding: "Bina Seç",
changeBuilding: "Bina Değiştir",
},
};
export type {
BuildPartsInterface,
BuildPartsSchemaInterface,
BuildPartsCreateInterface,
};
export { buildPartsSchema, buildPartsSchemaCreate };
export {
buildPartsSchema,
buildPartsSchemaCreate,
createEmptyValues,
updateEmptyValues,
listTranslations,
};

View File

@ -1,4 +1,4 @@
export const buildPartsTranslations = {
const buildPartsTranslations = {
tr: {
uu_id: "UUID",
build_uu_id: "Bina UUID",
@ -29,17 +29,42 @@ export const buildPartsTranslations = {
},
};
export const translationsOfPage = {
const translationsOfPage = {
tr: {
title: "Bina Bölümü Oluştur",
updateTitle: "Bina Bölümü Güncelle",
title: "Binaya Daire Oluştur",
updateTitle: "Binanın Dairesini Güncelle",
back2List: "Listeye Geri Dön",
actionButtonGroup: "İşlemler",
},
en: {
title: "Create Building Part",
updateTitle: "Update Building Part",
back2List: "Back to List",
actionButtonGroup: "Actions",
},
};
export default buildPartsTranslations;
const buildingTranslations = {
en: {
selectedBuilding: "Selected Building",
buildName: "Building Name",
buildNo: "Building No",
buildAddressCode: "Address Code",
buildMaxFloor: "Max Floor",
noSelectedBuilding: "No building selected. Please select a building first.",
selectBuilding: "Select Building",
changeBuilding: "Change Building",
},
tr: {
selectedBuilding: "Seçili Bina",
buildName: "Bina Adı",
buildNo: "Bina No",
buildAddressCode: "Adres Kodu",
buildMaxFloor: "Maksimum Kat",
noSelectedBuilding: "Bina seçilmedi. Lütfen önce bir bina seçin.",
selectBuilding: "Bina Seç",
changeBuilding: "Binayı Değiştir",
},
};
export { buildPartsTranslations, translationsOfPage, buildingTranslations };

View File

@ -25,17 +25,22 @@ import { API_BASE_URL } from "@/config/config";
import { useTableData } from "@/hooks/useTableData";
import { LanguageTypes } from "@/validations/mutual/language/validations";
import {
Translations,
Translations as BaseTranslations,
TableHeaderProps,
LoadingSpinnerProps,
ErrorDisplayProps,
MobilePaginationControlsProps,
DashboardPageProps,
} from "@/validations/mutual/table/validations";
// Extended Translations interface with selectedBuilding property
interface Translations extends BaseTranslations {
selectedBuilding: string;
}
import LoadingContent from "@/components/mutual/loader/component";
import { Pencil } from "lucide-react";
import { useRouter } from "next/navigation";
import { setCacheData } from "@/components/mutual/context/cache/context";
import { setCacheData, getCacheData, clearCacheData } from "@/components/mutual/context/cache/context";
import { BuildSchemaInterface } from "./schema";
interface DataTableProps {
@ -45,7 +50,11 @@ interface DataTableProps {
handleSortingChange: (columnId: string) => void;
getSortingIcon: (columnId: string) => React.ReactNode;
flexRender: typeof flexRender;
t: Translations;
t: any;
selected: string | null;
setSelected: React.Dispatch<React.SetStateAction<string | null>>;
activePageUrl: string;
router: ReturnType<typeof useRouter>;
}
interface TableFormProps {
@ -69,7 +78,7 @@ interface TableFormProps {
t: Translations;
}
const translations: Record<LanguageTypes, Translations> = {
const translations: Record<LanguageTypes, any> = {
en: {
dataTable: 'Data Table',
tableWithApiData: 'Table with API Data',
@ -85,6 +94,16 @@ const translations: Record<LanguageTypes, Translations> = {
selectPage: 'Select page',
selectSize: 'Select size',
actionButtonGroup: 'Actions',
selectedBuilding: 'Selected Building',
buildParts: 'Create Parts',
areas: 'Create Areas',
buildNo: 'Building No',
buildName: 'Building Name',
buildAddress: 'Building Address',
buildAddressCode: 'Building Address Code',
buildMaxFloor: 'Building Max Floor',
clearSelection: 'Clear Selection',
// lands: 'Create Lands',
},
tr: {
dataTable: 'Veri Tablosu',
@ -101,6 +120,16 @@ const translations: Record<LanguageTypes, Translations> = {
selectPage: 'Sayfa seç',
selectSize: 'Boyut seç',
actionButtonGroup: 'Eylemler',
selectedBuilding: 'Seçilen Bina',
buildParts: 'Daire Oluştur',
areas: 'Alanlar Oluştur',
buildNo: 'Bina No',
buildName: 'Bina Adı',
buildAddress: 'Bina Adresi',
buildAddressCode: 'Bina Adres Kodu',
buildMaxFloor: 'Bina Max Kat',
clearSelection: 'Seçimi Temizle',
// lands: 'Alanlar Oluştur',
}
};
@ -112,7 +141,32 @@ const DataTable: React.FC<DataTableProps> = React.memo(({
getSortingIcon,
flexRender,
t,
selected,
setSelected,
activePageUrl,
router,
}) => {
const handleRowClick = (rowData: BuildSchemaInterface) => {
setSelected(rowData.uu_id);
const selectedData = {
build: rowData,
// build_owner: null // This could be populated if you have owner data
};
setCacheData(`${activePageUrl}/list`, selectedData);
};
const handleClearSelection = async () => {
setSelected(null);
try {
// Use clearCacheData to properly remove the cache entry
await clearCacheData(`${activePageUrl}/list`);
console.log("Cache cleared successfully");
} catch (error) {
console.error("Error clearing selection cache:", error);
}
};
return (
<div className="overflow-x-auto relative">
{/* Semi-transparent loading overlay that preserves interactivity */}
@ -121,8 +175,64 @@ const DataTable: React.FC<DataTableProps> = React.memo(({
{/* We don't put anything here as we already have the loading indicator in the header */}
</div>
)}
{selected && (
<div className="mb-4 p-4 border rounded-md bg-blue-50 shadow-sm">
<h3 className="text-lg font-medium text-blue-800 mb-2">{t.selectedBuilding}</h3>
<div className="flex">
{/* 2/3 section for building details */}
<div className="w-2/3 pr-4">
<div className="grid grid-cols-2 gap-2">
<div>
<span className="font-semibold">UUID: </span>
{tableData.find(row => row.uu_id === selected)?.uu_id}
</div>
<div>
<span className="font-semibold">{t.buildName}: </span>
{tableData.find(row => row.uu_id === selected)?.build_name}
</div>
<div>
<span className="font-semibold">{t.buildNo}: </span>
{tableData.find(row => row.uu_id === selected)?.build_no}
</div>
<div>
<span className="font-semibold">{t.buildAddressCode}: </span>
{tableData.find(row => row.uu_id === selected)?.gov_address_code}
</div>
<div>
<span className="font-semibold">{t.buildMaxFloor}: </span>
{tableData.find(row => row.uu_id === selected)?.max_floor}
</div>
</div>
</div>
{/* 1/3 section for action buttons */}
<div className="w-1/3 border-l pl-4 flex flex-col justify-center space-y-2">
<Button
onClick={() => router.push(`/panel/build/parts/create`)}
className="bg-green-600 text-white hover:bg-green-700 w-full"
>
{t.buildParts}
</Button>
<Button
onClick={() => router.push(`/panel/build/area/create`)}
className="bg-blue-600 text-white hover:bg-blue-700 w-full"
>
{t.areas}
</Button>
<Button
onClick={handleClearSelection}
className="bg-red-600 text-white hover:bg-red-700 w-full"
>
{t.clearSelection}
</Button>
</div>
</div>
</div>
)}
<table className="min-w-full divide-y divide-gray-200">
<caption className="sr-only">{t.dataTable}</caption>
<thead className="bg-gray-50">
{table.getHeaderGroups().map((headerGroup) => (
<tr key={headerGroup.id}>
@ -154,7 +264,11 @@ const DataTable: React.FC<DataTableProps> = React.memo(({
</tr>
) : (
table.getRowModel().rows.map((row) => (
<tr key={row.id} className="hover:bg-gray-50">
<tr
key={row.id}
className={`hover:bg-gray-50 cursor-pointer ${selected === (row.original as BuildSchemaInterface).uu_id ? 'bg-blue-100' : ''}`}
onClick={() => handleRowClick(row.original as BuildSchemaInterface)}
>
{row.getVisibleCells().map((cell) => {
return (
<td key={cell.id} className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
@ -343,10 +457,31 @@ const ErrorDisplay: React.FC<ErrorDisplayProps> = ({ message }) => {
const BuildListPage: React.FC<DashboardPageProps> = React.memo((props) => {
// Initialize translation with English as default
const language = props.onlineData?.lang as LanguageTypes || 'en';
const [selected, setSelected] = React.useState<string | null>(null);
const [cacheLoaded, setCacheLoaded] = React.useState<boolean>(false);
const t = translations[language];
const router = useRouter();
// Check for selected build in cache when component mounts
React.useEffect(() => {
const checkCacheForSelectedBuild = async () => {
if (!cacheLoaded) {
try {
const activePageUrl = props.activePageUrl || '';
const cachedData = await getCacheData(`${activePageUrl}/list`);
if (cachedData && cachedData.build && cachedData.build.uu_id) {
setSelected(cachedData.build.uu_id);
}
setCacheLoaded(true);
} catch (error) {
console.error("Error checking cache for selected build:", error);
setCacheLoaded(true);
}
}
};
checkCacheForSelectedBuild();
}, [props.activePageUrl]);
const {
form,
tableData,
@ -373,6 +508,7 @@ const BuildListPage: React.FC<DashboardPageProps> = React.memo((props) => {
const activePageUrl = props.activePageUrl || '';
const handleEditRow = async (row: BuildSchemaInterface) => {
try {
setSelected(row.uu_id);
await setCacheData(`${activePageUrl}/update`, row);
router.push(`/panel/${activePageUrl}/update`);
} catch (error) {
@ -565,6 +701,10 @@ const BuildListPage: React.FC<DashboardPageProps> = React.memo((props) => {
getSortingIcon={getSortingIcon}
flexRender={flexRender}
t={t}
selected={selected}
setSelected={setSelected}
activePageUrl={activePageUrl}
router={router}
/>
</div>
);

View File

@ -4,7 +4,7 @@ import TableCardComponentImproved from "@/components/custom/content/TableCardCom
import BuildListPage from "./builds/superuser/ListPage";
import CreateFromBuildComponent from "./builds/superuser/CreatePage";
import UpdateFromBuildComponent from "./builds/superuser/UpdatePage";
import BuildPartsListPage from "./buildParts/superuser/ListPage";
import BuildPartsListPage from "./buildParts/superuser/main/ListPage";
import CreateFromBuildPartsComponent from "./buildParts/superuser/CreatePage";
import UpdateFromBuildPartsComponent from "./buildParts/superuser/UpdatePage";