updated left menu and page template
This commit is contained in:
parent
9a4696af77
commit
dd4a8f333d
|
|
@ -6,9 +6,9 @@ const formatServiceUrl = (url: string) => {
|
|||
export const baseUrlAuth = formatServiceUrl(
|
||||
process.env.NEXT_PUBLIC_AUTH_SERVICE_URL || "auth_service:8001"
|
||||
);
|
||||
// export const baseUrlValidation = formatServiceUrl(
|
||||
// process.env.NEXT_PUBLIC_VALIDATION_SERVICE_URL || "validationservice:8888"
|
||||
// );
|
||||
export const baseUrlPeople = formatServiceUrl(
|
||||
process.env.NEXT_PUBLIC_VALIDATION_SERVICE_URL || "identity_service:8002"
|
||||
);
|
||||
// export const baseUrlEvent = formatServiceUrl(
|
||||
// process.env.NEXT_PUBLIC_EVENT_SERVICE_URL || "eventservice:8888"
|
||||
// );
|
||||
|
|
|
|||
|
|
@ -5,8 +5,6 @@ import { fetchData, fetchDataWithToken } from "../api-fetcher";
|
|||
import { baseUrlAuth, cookieObject, tokenSecret } from "../basics";
|
||||
import { cookies } from "next/headers";
|
||||
|
||||
// import { setAvailableEvents } from "../events/available";
|
||||
|
||||
const loginEndpoint = `${baseUrlAuth}/authentication/login`;
|
||||
const loginSelectEndpoint = `${baseUrlAuth}/authentication/select`;
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,31 @@
|
|||
"use server";
|
||||
import { fetchData, fetchDataWithToken } from "../api-fetcher";
|
||||
import { baseUrlPeople, cookieObject, tokenSecret } from "../basics";
|
||||
import { PageListOptions, PaginateOnly } from "../schemas/list";
|
||||
|
||||
const peopleListEndpoint = `${baseUrlPeople}/people/list`;
|
||||
const peopleCreateEndpoint = `${baseUrlPeople}/people/create`;
|
||||
const peopleUpdateEndpoint = `${baseUrlPeople}/people/update`;
|
||||
|
||||
async function peopleList(payload: PageListOptions) {
|
||||
try {
|
||||
const response = await fetchDataWithToken(
|
||||
peopleListEndpoint,
|
||||
{
|
||||
page: payload.page,
|
||||
size: payload.size,
|
||||
order_field: payload.orderField,
|
||||
order_type: payload.orderType,
|
||||
query: payload.query,
|
||||
},
|
||||
"POST",
|
||||
false
|
||||
);
|
||||
return response?.status === 200 ? response.data : null;
|
||||
} catch (error) {
|
||||
console.error("Error fetching people list:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export { peopleList };
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
export interface PaginateOnly {
|
||||
page?: number;
|
||||
size?: number;
|
||||
orderField?: string[];
|
||||
orderType?: string[];
|
||||
}
|
||||
|
||||
export interface PageListOptions {
|
||||
page?: number;
|
||||
size?: number;
|
||||
orderField?: string[];
|
||||
orderType?: string[];
|
||||
query?: any;
|
||||
}
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
"use server";
|
||||
import React from "react";
|
||||
import Template from "@/components/Pages/template/app";
|
||||
import ClientMenu from "@/components/menuCleint/menu";
|
||||
import { LanguageKey } from "@/components/Pages/template/language";
|
||||
|
||||
import { retrievePageList } from "@/apicalls/cookies/token";
|
||||
|
||||
interface TemplatePageProps {
|
||||
params: { lang?: string };
|
||||
searchParams: { [key: string]: string | string[] | undefined };
|
||||
}
|
||||
|
||||
async function TemplatePage({ params, searchParams }: TemplatePageProps) {
|
||||
// Get language from query params or default to 'en'
|
||||
const pParams = await params;
|
||||
const siteUrlsList = (await retrievePageList()) || [];
|
||||
const searchParamsInstance = await searchParams;
|
||||
const activePage = "/template";
|
||||
const lang = (searchParamsInstance?.lang as LanguageKey) || "en";
|
||||
|
||||
return (
|
||||
<>
|
||||
<>
|
||||
<div className="min-h-screen min-w-screen flex w-full">
|
||||
{/* Sidebar */}
|
||||
<aside className="w-1/4 border-r p-4 overflow-y-auto h-screen sticky top-0">
|
||||
<div className="w-full">
|
||||
<ClientMenu
|
||||
siteUrls={siteUrlsList}
|
||||
lang={lang}
|
||||
/>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
{/* Main Content Area */}
|
||||
<div className="flex flex-col w-3/4 overflow-y-auto">
|
||||
{/* Sticky Header */}
|
||||
<header className="sticky top-0 bg-white shadow-md z-10 p-4 flex justify-between items-center">
|
||||
<h1 className="text-2xl font-semibold">{activePage}</h1>
|
||||
<div className="flex items-center space-x-4">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search..."
|
||||
className="border px-3 py-2 rounded-lg"
|
||||
/>
|
||||
<div className="w-10 h-10 bg-gray-300 rounded-full"></div>
|
||||
</div>
|
||||
</header>
|
||||
<div className="p-4 overflow-y-auto">
|
||||
<Template lang={lang} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default TemplatePage;
|
||||
|
|
@ -1,45 +1,100 @@
|
|||
"use client";
|
||||
import React, { useEffect } from "react";
|
||||
import buildingsMockData from "./mock-data";
|
||||
|
||||
import { BuildingFormData } from "../Pages/build/buildschema1";
|
||||
import BuildPageForm1 from "../Pages/build/buildform1";
|
||||
import BuildPage1 from "../Pages/build/buildpage1";
|
||||
import BuildInfo1 from "../Pages/build/buildinfo1";
|
||||
import {
|
||||
PeopleFormData,
|
||||
PeopleSchema,
|
||||
} from "../Pages/people/superusers/peopleschema1";
|
||||
|
||||
import PeoplePageForm1 from "../Pages/people/superusers/peopleform1";
|
||||
import PeoplePage1 from "../Pages/people/superusers/peoplepage1";
|
||||
import PeopleInfo1 from "../Pages/people/superusers/peopleinfo1";
|
||||
import { peopleList } from "@/apicalls/people/people";
|
||||
|
||||
interface Pagination {
|
||||
page: number;
|
||||
size: number;
|
||||
totalCount: number;
|
||||
allCount: number;
|
||||
totalPages: number;
|
||||
orderField: string[];
|
||||
orderType: string[];
|
||||
pageCount: number;
|
||||
}
|
||||
|
||||
const defaultPagination: Pagination = {
|
||||
page: 1,
|
||||
size: 1,
|
||||
totalCount: 0,
|
||||
allCount: 0,
|
||||
totalPages: 0,
|
||||
orderField: ["uu_id"],
|
||||
orderType: ["asc"],
|
||||
pageCount: 0,
|
||||
};
|
||||
|
||||
function app000003() {
|
||||
const [modifyEnable, setModifyEnable] = React.useState<boolean | null>(false);
|
||||
const [isCreate, setIsCreate] = React.useState<boolean | null>(false);
|
||||
const [selectedId, setSelectedId] = React.useState<string | null>(null);
|
||||
const [tableData, setTableData] = React.useState<BuildingFormData[]>([]);
|
||||
const [tableData, setTableData] = React.useState<PeopleFormData[]>([]);
|
||||
const [pagination, setPagination] =
|
||||
React.useState<Pagination>(defaultPagination);
|
||||
|
||||
const fecthData = async ({
|
||||
// Add any parameters if needed
|
||||
page = 1,
|
||||
pageSize = 10,
|
||||
orderBy = "asc",
|
||||
orderType = "name",
|
||||
orderBy = ["asc"],
|
||||
orderType = ["uu_id"],
|
||||
query = {},
|
||||
}) => {
|
||||
// Simulate an API call
|
||||
const response = await new Promise((resolve) =>
|
||||
setTimeout(() => resolve(buildingsMockData), 1000)
|
||||
);
|
||||
setTableData(response as BuildingFormData[]);
|
||||
const result = await peopleList({
|
||||
page,
|
||||
size: pageSize,
|
||||
orderField: orderType,
|
||||
orderType: orderBy,
|
||||
query: query,
|
||||
});
|
||||
setTableData(result?.data || []);
|
||||
setPagination(result?.pagination || {});
|
||||
};
|
||||
// Fetch data when the component mounts
|
||||
|
||||
useEffect(() => {
|
||||
fecthData({
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
orderBy: "asc",
|
||||
orderType: "uu_id",
|
||||
query: {},
|
||||
const fetchDataRef = React.useCallback(({
|
||||
page = 1,
|
||||
pageSize = 10,
|
||||
orderBy = ["asc"],
|
||||
orderType = ["uu_id"],
|
||||
query = {},
|
||||
}) => {
|
||||
peopleList({
|
||||
page,
|
||||
size: pageSize,
|
||||
orderField: orderType,
|
||||
orderType: orderBy,
|
||||
query: query,
|
||||
}).then(result => {
|
||||
setTableData(result?.data || []);
|
||||
setPagination(result?.pagination || {});
|
||||
});
|
||||
}, []);
|
||||
|
||||
const onSubmit = (data: BuildingFormData) => {
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
fetchDataRef({
|
||||
page: pagination.page,
|
||||
pageSize: pagination.size,
|
||||
orderBy: pagination.orderField,
|
||||
orderType: pagination.orderType,
|
||||
query: {},
|
||||
});
|
||||
}, 300); // 300ms debounce
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, [pagination.page, pagination.size, fetchDataRef]);
|
||||
|
||||
const onSubmit = (data: PeopleFormData) => {
|
||||
console.log("Form data:", data);
|
||||
// Submit to API or do other operations
|
||||
};
|
||||
|
|
@ -57,21 +112,22 @@ function app000003() {
|
|||
return (
|
||||
<>
|
||||
<div className="h-screen overflow-y-auto">
|
||||
<BuildInfo1
|
||||
data={tableData}
|
||||
<PeopleInfo1
|
||||
pagination={pagination}
|
||||
setPagination={setPagination}
|
||||
selectedId={selectedId}
|
||||
setIsCreate={() => setIsCreate(true)}
|
||||
/>
|
||||
{!isCreate ? (
|
||||
<div className="min-w-full mx-4 p-6 rounded-lg shadow-md ">
|
||||
{!selectedId ? (
|
||||
<BuildPage1
|
||||
<PeoplePage1
|
||||
data={tableData}
|
||||
handleUpdateModify={handleUpdateModify}
|
||||
handleView={handleView}
|
||||
/>
|
||||
) : (
|
||||
<BuildPageForm1
|
||||
<PeoplePageForm1
|
||||
data={tableData.find((item) => item.uu_id === selectedId) || {}}
|
||||
onSubmit={onSubmit}
|
||||
modifyEnable={modifyEnable}
|
||||
|
|
@ -81,7 +137,7 @@ function app000003() {
|
|||
</div>
|
||||
) : (
|
||||
<>
|
||||
<BuildPageForm1
|
||||
<PeoplePageForm1
|
||||
data={{
|
||||
build_date: new Date(),
|
||||
decision_period_date: new Date(),
|
||||
|
|
|
|||
|
|
@ -0,0 +1,44 @@
|
|||
import React from "react";
|
||||
import { Pencil, ScanSearch } from "lucide-react";
|
||||
|
||||
interface CardProps {
|
||||
data: any;
|
||||
onUpdate: (uu_id: string) => void;
|
||||
onView: (uu_id: string) => void;
|
||||
}
|
||||
|
||||
// Card component
|
||||
const Card: React.FC<CardProps> = ({ data, onUpdate, onView }) => (
|
||||
|
||||
<div className="bg-white text-black rounded-lg shadow-md p-6 mb-4 hover:shadow-lg transition-shadow">
|
||||
<div className="flex justify-between items-start">
|
||||
<div>
|
||||
<h3 className="text-xl font-semibold mb-2">{data.person_tag}</h3>
|
||||
<p className="mb-2">
|
||||
Building Number: {data.firstname} {data.surname}
|
||||
</p>
|
||||
<div className="flex items-center gap-4">
|
||||
<span className="text-sm text-gray-500">UUID: {data.uu_id}</span>
|
||||
<span className="text-sm text-gray-500">
|
||||
Built: {new Date(data.created_at).toDateString()}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
className="text-blue-500 hover:text-blue-700 p-2"
|
||||
aria-label="Update"
|
||||
>
|
||||
<div className="flex flex-col">
|
||||
<div onClick={() => onUpdate(data.uu_id)}>
|
||||
<Pencil />
|
||||
</div>
|
||||
<div className="mt-5" onClick={() => onView(data.uu_id)}>
|
||||
<ScanSearch />
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default Card;
|
||||
|
|
@ -0,0 +1,431 @@
|
|||
import React from "react";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useForm } from "react-hook-form";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { ArrowLeft } from "lucide-react";
|
||||
import { DatePicker } from "@/components/ui/datepicker";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { PeopleFormData, PeopleSchema } from "./peopleschema1";
|
||||
|
||||
interface FormProps {
|
||||
data: any;
|
||||
modifyEnable: boolean | null;
|
||||
setSelectedId: () => void;
|
||||
onSubmit: (data: any) => void;
|
||||
}
|
||||
|
||||
const PeoplePageForm1: React.FC<FormProps> = ({
|
||||
data,
|
||||
modifyEnable,
|
||||
setSelectedId,
|
||||
onSubmit,
|
||||
}) => {
|
||||
const form = useForm<PeopleFormData>({
|
||||
resolver: zodResolver(PeopleSchema),
|
||||
defaultValues: {
|
||||
...data,
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<div onClick={() => setSelectedId()} className="flex items-center">
|
||||
<ArrowLeft /> Back
|
||||
</div>
|
||||
<div className="mx-auto min-w-full p-6 h-screen overflow-y-auto">
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
|
||||
<div className="flex">
|
||||
{/* Government Address Code */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="gov_address_code"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Government Address Code</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} disabled={Boolean(modifyEnable)} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
This is your public display name.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{/* Building Name */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="build_name"
|
||||
render={({ field }) => (
|
||||
<FormItem className="w-full">
|
||||
<FormLabel>Building Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
className="min-w-full ml-5"
|
||||
disabled={Boolean(modifyEnable)}
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
This is your public display name.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
{/* Building Number */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="build_no"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Building Number</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} disabled={Boolean(modifyEnable)} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
This is your public display name.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{/* Building Max Floor */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="max_floor"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Max Floor</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
type="number"
|
||||
disabled={Boolean(modifyEnable)}
|
||||
min={1}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
This is your public display name.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{/* Lift Count */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="lift_count"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Lift Count</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
type="number"
|
||||
disabled={Boolean(modifyEnable)}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
This is your public display name.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{/* Underground Floor */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="underground_floor"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Underground Floor</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
type="number"
|
||||
disabled={Boolean(modifyEnable)}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
This is your public display name.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{/* Block Service Man Count */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="block_service_man_count"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Block Service Man Count</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
type="number"
|
||||
disabled={Boolean(modifyEnable)}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
This is your public display name.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
{/* Build Date */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="build_date"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Build Date</FormLabel>
|
||||
<FormControl>
|
||||
<DatePicker
|
||||
control={form.control}
|
||||
name="date"
|
||||
initialDate={new Date(data?.build_date)}
|
||||
disabled={Boolean(modifyEnable)}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
This is your public display name.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{/* Decision Period Date */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="decision_period_date"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Decision Period Date</FormLabel>
|
||||
<FormControl>
|
||||
<DatePicker
|
||||
control={form.control}
|
||||
name="date"
|
||||
initialDate={new Date(data?.decision_period_date)}
|
||||
disabled={Boolean(modifyEnable)}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
This is your public display name.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{/* Tax Number */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="tax_no"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Tax Number</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} disabled={Boolean(modifyEnable)} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
This is your public display name.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{/* Garage Count */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="garage_count"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Garage Count</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
type="number"
|
||||
disabled={Boolean(modifyEnable)}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
This is your public display name.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{/* Security Service Man Count */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="security_service_man_count"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Security Service Man Count</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
type="number"
|
||||
disabled={Boolean(modifyEnable)}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
This is your public display name.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
{/* Systems */}
|
||||
<fieldset className="flex justify-evenly items-center border p-3 rounded space-x-4">
|
||||
<legend className="text-sm font-medium px-2">
|
||||
Building Systems
|
||||
</legend>
|
||||
{/* Heating System */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="heating_system"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Heating System</FormLabel>
|
||||
<FormControl>
|
||||
<Checkbox
|
||||
defaultChecked={data?.heating_system}
|
||||
disabled={Boolean(modifyEnable)}
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{/* Cooling System */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="cooling_system"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Cooling System</FormLabel>
|
||||
<FormControl>
|
||||
<Checkbox
|
||||
defaultChecked={data?.cooling_system}
|
||||
disabled={Boolean(modifyEnable)}
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{/* Hot Water System */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="hot_water_system"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Hot Water System</FormLabel>
|
||||
<FormControl>
|
||||
<Checkbox
|
||||
defaultChecked={data?.hot_water_system}
|
||||
disabled={Boolean(modifyEnable)}
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</fieldset>
|
||||
<div className="flex justify-evenly">
|
||||
{/* Site UUID */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="site_uu_id"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Site UUID</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} disabled={Boolean(modifyEnable)} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
This is your public display name.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* Address UUID */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="address_uu_id"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Address UUID</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} disabled={Boolean(modifyEnable)} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
This is your public display name.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{/* Building Type UUID */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="build_types_uu_id"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Building Type UUID</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} disabled={Boolean(modifyEnable)} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
This is your public display name.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{!modifyEnable && (
|
||||
<div className="flex justify-center items-center space-x-4 pt-4">
|
||||
<button
|
||||
type="submit"
|
||||
className="px-4 py-2 bg-indigo-600 text-white rounded w-1/3 hover:bg-white hover:text-indigo-700 transition-colors duration-300"
|
||||
>
|
||||
Submit
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
</Form>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default PeoplePageForm1;
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
import React from "react";
|
||||
import { ChevronFirst, ChevronLast, Plus } from "lucide-react";
|
||||
|
||||
interface InfoData {
|
||||
pagination: any;
|
||||
selectedId: string | null;
|
||||
setPagination: () => void;
|
||||
setIsCreate: () => void;
|
||||
}
|
||||
|
||||
|
||||
|
||||
const PeopleInfo1: React.FC<InfoData> = ({
|
||||
pagination,
|
||||
selectedId,
|
||||
setPagination,
|
||||
setIsCreate,
|
||||
}) => {
|
||||
console.log("PeopleInfo1", pagination, selectedId);
|
||||
return (
|
||||
<div>
|
||||
<div className="min-w-full mx-4 p-6 rounded-lg shadow-md m-6">
|
||||
<div className="flex justify-evenly items-center my-1">
|
||||
<h1 className="text-2xl font-bold ml-2">Individual Dashboard</h1>
|
||||
<h3 className="text-lg font-semibold text-gray-700 ml-2">
|
||||
Selected ID: {selectedId}
|
||||
</h3>
|
||||
<button
|
||||
onClick={() => setIsCreate()}
|
||||
className="bg-blue-500 text-white px-4 py-2 rounded-lg flex items-center gap-2 hover:bg-blue-600 transition-colors"
|
||||
>
|
||||
<Plus />
|
||||
Create New
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex justify-evenly items-center my-1">
|
||||
<h3 className="text-lg font-semibold text-gray-700 ml-2">
|
||||
Active Page: {pagination.page}
|
||||
</h3>
|
||||
<h3 className="text-lg font-semibold text-gray-700 ml-2">
|
||||
Size: {pagination.size}
|
||||
</h3>
|
||||
<h3 className="text-lg font-semibold text-gray-700 ml-2">
|
||||
Total Pages: {pagination.totalPages}
|
||||
</h3>
|
||||
<h3 className="text-lg font-semibold text-gray-700 ml-2">
|
||||
Order By: {JSON.stringify(pagination.orderField)}
|
||||
</h3>
|
||||
<h3 className="text-lg font-semibold text-gray-700 ml-2">
|
||||
Order Type: {JSON.stringify(pagination.orderType)}
|
||||
</h3>
|
||||
<h3 className="text-lg font-semibold text-gray-700 ml-2">
|
||||
All Count: {pagination.allCount}
|
||||
</h3>
|
||||
<h3 className="text-lg font-semibold text-gray-700 ml-2">
|
||||
Total Count: {pagination.totalCount}
|
||||
</h3>
|
||||
|
||||
<h3 className="text-lg font-semibold text-gray-700 ml-2">Page: 1</h3>
|
||||
<button className="bg-black text-white px-4 py-2 w-24 rounded-lg flex items-center gap-2 hover:bg-white hover:text-black transition-colors">
|
||||
Previous
|
||||
<ChevronFirst />
|
||||
</button>
|
||||
<button className="bg-black text-white px-4 py-2 w-24 rounded-lg flex items-center gap-2 hover:bg-white hover:text-black transition-colors">
|
||||
Next
|
||||
<ChevronLast />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PeopleInfo1;
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
"use client";
|
||||
import React from "react";
|
||||
import Card from "./card1";
|
||||
import { PeopleFormData } from "./peopleschema1";
|
||||
|
||||
interface PageData {
|
||||
data: PeopleFormData[];
|
||||
handleUpdateModify: (uu_id: string) => void;
|
||||
handleView: (uu_id: string) => void;
|
||||
}
|
||||
|
||||
const PeoplePage1: React.FC<PageData> = ({
|
||||
data,
|
||||
handleUpdateModify,
|
||||
handleView,
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<div className="grid gap-4">
|
||||
{data.map((item) => (
|
||||
<div key={`CardDiv-${item.uu_id}`}>
|
||||
<Card
|
||||
key={`Card-${item.uu_id}`}
|
||||
data={item}
|
||||
onUpdate={handleUpdateModify}
|
||||
onView={handleView}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default PeoplePage1;
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
import { z } from "zod";
|
||||
|
||||
export const PeopleSchema = z.object({
|
||||
uu_id: z.string(),
|
||||
created_at: z.string(),
|
||||
updated_at: z.string(),
|
||||
person_tag: z.string(),
|
||||
expiry_starts: z.string(),
|
||||
expiry_ends: z.string(),
|
||||
firstname: z.string(),
|
||||
middle_name: z.string(),
|
||||
surname: z.string(),
|
||||
birth_date: z.string(),
|
||||
birth_place: z.string(),
|
||||
sex_code: z.string(),
|
||||
country_code: z.string(),
|
||||
tax_no: z.string(),
|
||||
active: z.boolean(),
|
||||
deleted: z.boolean(),
|
||||
is_confirmed: z.boolean(),
|
||||
is_notification_send: z.boolean(),
|
||||
});
|
||||
|
||||
export type PeopleFormData = z.infer<typeof PeopleSchema>;
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
import React from 'react';
|
||||
import { getTranslation, LanguageKey } from './language';
|
||||
|
||||
interface ActionButtonsComponentProps {
|
||||
onCreateClick: () => void;
|
||||
lang?: LanguageKey;
|
||||
}
|
||||
|
||||
export function ActionButtonsComponent({
|
||||
onCreateClick,
|
||||
lang = 'en'
|
||||
}: ActionButtonsComponentProps) {
|
||||
const t = getTranslation(lang);
|
||||
|
||||
return (
|
||||
<div className="flex justify-end mb-4">
|
||||
<button
|
||||
onClick={onCreateClick}
|
||||
className="px-4 py-2 bg-green-500 text-white rounded hover:bg-green-600"
|
||||
>
|
||||
{t.create}
|
||||
</button>
|
||||
{/* Additional action buttons can be added here in the future */}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
import React from 'react';
|
||||
import { DataType } from './schema';
|
||||
import { getTranslation, LanguageKey } from './language';
|
||||
import { DataCard } from './ListInfoComponent';
|
||||
|
||||
interface DataDisplayComponentProps {
|
||||
data: DataType[];
|
||||
loading: boolean;
|
||||
error: Error | null;
|
||||
onViewClick: (item: DataType) => void;
|
||||
onUpdateClick: (item: DataType) => void;
|
||||
lang?: LanguageKey;
|
||||
}
|
||||
|
||||
export function DataDisplayComponent({
|
||||
data,
|
||||
loading,
|
||||
error,
|
||||
onViewClick,
|
||||
onUpdateClick,
|
||||
lang = 'en'
|
||||
}: DataDisplayComponentProps) {
|
||||
const t = getTranslation(lang);
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="p-4 bg-red-100 text-red-700 rounded">
|
||||
{t.error} {error.message}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex justify-center items-center py-12">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-blue-500"></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="grid gap-4">
|
||||
{data.map(item => (
|
||||
<DataCard
|
||||
key={item.id}
|
||||
item={item}
|
||||
onView={onViewClick}
|
||||
onUpdate={onUpdateClick}
|
||||
lang={lang}
|
||||
/>
|
||||
))}
|
||||
|
||||
{data.length === 0 && (
|
||||
<div className="text-center py-12 text-gray-500">
|
||||
{t.noItemsFound}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,127 @@
|
|||
import React, { useState } from 'react';
|
||||
import { z } from 'zod';
|
||||
import { DataType, DataSchema } from './schema';
|
||||
import { getTranslation, LanguageKey } from './language';
|
||||
|
||||
interface FormComponentProps {
|
||||
initialData?: Partial<DataType>;
|
||||
onSubmit: (data: DataType) => void;
|
||||
onCancel: () => void;
|
||||
readOnly?: boolean;
|
||||
lang?: LanguageKey;
|
||||
}
|
||||
|
||||
export function FormComponent({
|
||||
initialData,
|
||||
onSubmit,
|
||||
onCancel,
|
||||
readOnly = false,
|
||||
lang = 'en'
|
||||
}: FormComponentProps) {
|
||||
const t = getTranslation(lang);
|
||||
const [formData, setFormData] = useState<Partial<DataType>>(initialData || {
|
||||
title: '',
|
||||
description: '',
|
||||
status: 'active',
|
||||
});
|
||||
const [errors, setErrors] = useState<Record<string, string>>({});
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => {
|
||||
const { name, value } = e.target;
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
[name]: value,
|
||||
}));
|
||||
};
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
try {
|
||||
// Validate with Zod
|
||||
const validData = DataSchema.parse(formData);
|
||||
onSubmit(validData);
|
||||
setErrors({});
|
||||
} catch (err) {
|
||||
if (err instanceof z.ZodError) {
|
||||
const fieldErrors: Record<string, string> = {};
|
||||
err.errors.forEach(error => {
|
||||
const field = error.path[0];
|
||||
fieldErrors[field as string] = error.message;
|
||||
});
|
||||
setErrors(fieldErrors);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-white p-6 rounded-lg shadow">
|
||||
<h2 className="text-xl font-semibold mb-4">
|
||||
{readOnly ? t.view : initialData?.id ? t.update : t.createNew}
|
||||
</h2>
|
||||
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="mb-4">
|
||||
<label htmlFor="title" className="block text-sm font-medium mb-1">{t.formLabels.title}</label>
|
||||
<input
|
||||
type="text"
|
||||
id="title"
|
||||
name="title"
|
||||
value={formData.title || ''}
|
||||
onChange={handleChange}
|
||||
disabled={readOnly}
|
||||
className="w-full p-2 border rounded focus:ring-blue-500 focus:border-blue-500 disabled:bg-gray-100"
|
||||
/>
|
||||
{errors.title && <p className="text-red-500 text-sm mt-1">{errors.title}</p>}
|
||||
</div>
|
||||
|
||||
<div className="mb-4">
|
||||
<label htmlFor="description" className="block text-sm font-medium mb-1">{t.formLabels.description}</label>
|
||||
<textarea
|
||||
id="description"
|
||||
name="description"
|
||||
value={formData.description || ''}
|
||||
onChange={handleChange}
|
||||
disabled={readOnly}
|
||||
rows={3}
|
||||
className="w-full p-2 border rounded focus:ring-blue-500 focus:border-blue-500 disabled:bg-gray-100"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mb-4">
|
||||
<label htmlFor="status" className="block text-sm font-medium mb-1">{t.formLabels.status}</label>
|
||||
<select
|
||||
id="status"
|
||||
name="status"
|
||||
value={formData.status || 'active'}
|
||||
onChange={handleChange}
|
||||
disabled={readOnly}
|
||||
className="w-full p-2 border rounded focus:ring-blue-500 focus:border-blue-500 disabled:bg-gray-100"
|
||||
>
|
||||
<option value="active">{t.status.active}</option>
|
||||
<option value="inactive">{t.status.inactive}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end space-x-3 mt-6">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onCancel}
|
||||
className="px-4 py-2 bg-gray-200 rounded hover:bg-gray-300"
|
||||
>
|
||||
{readOnly ? t.back : t.cancel}
|
||||
</button>
|
||||
|
||||
{!readOnly && (
|
||||
<button
|
||||
type="submit"
|
||||
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
|
||||
>
|
||||
{initialData?.id ? t.buttons.update : t.buttons.create}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,136 @@
|
|||
import React from 'react';
|
||||
import { DataType, Pagination } from './schema';
|
||||
import { getTranslation, LanguageKey } from './language';
|
||||
import { ActionButtonsComponent } from './ActionButtonsComponent';
|
||||
import { SortingComponent } from './SortingComponent';
|
||||
import { PaginationToolsComponent } from './PaginationToolsComponent';
|
||||
|
||||
|
||||
|
||||
interface DataCardProps {
|
||||
item: DataType;
|
||||
onView: (item: DataType) => void;
|
||||
onUpdate: (item: DataType) => void;
|
||||
lang?: LanguageKey;
|
||||
}
|
||||
|
||||
export function DataCard({
|
||||
item,
|
||||
onView,
|
||||
onUpdate,
|
||||
lang = 'en'
|
||||
}: DataCardProps) {
|
||||
const t = getTranslation(lang);
|
||||
|
||||
return (
|
||||
<div className="bg-white p-4 rounded-lg shadow mb-4">
|
||||
<div className="flex justify-between items-start">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold">{item.title}</h3>
|
||||
<p className="text-gray-600 mt-1">{item.description}</p>
|
||||
<div className="mt-2 flex items-center">
|
||||
<span className={`px-2 py-1 rounded text-xs ${item.status === 'active' ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-800'}`}>
|
||||
{item.status}
|
||||
</span>
|
||||
<span className="text-xs text-gray-500 ml-2">
|
||||
{t.formLabels.createdAt}: {new Date(item.createdAt).toLocaleDateString()}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex space-x-2">
|
||||
<button
|
||||
onClick={() => onView(item)}
|
||||
className="px-3 py-1 bg-blue-100 text-blue-700 rounded hover:bg-blue-200"
|
||||
>
|
||||
{t.buttons.view}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => onUpdate(item)}
|
||||
className="px-3 py-1 bg-yellow-100 text-yellow-700 rounded hover:bg-yellow-200"
|
||||
>
|
||||
{t.buttons.update}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface ListInfoComponentProps {
|
||||
data: DataType[];
|
||||
pagination: Pagination;
|
||||
loading: boolean;
|
||||
error: Error | null;
|
||||
updatePagination: (updates: Partial<Pagination>) => void;
|
||||
onCreateClick: () => void;
|
||||
onViewClick: (item: DataType) => void;
|
||||
onUpdateClick: (item: DataType) => void;
|
||||
lang?: LanguageKey;
|
||||
}
|
||||
|
||||
export function ListInfoComponent({
|
||||
data,
|
||||
pagination,
|
||||
loading,
|
||||
error,
|
||||
updatePagination,
|
||||
onCreateClick,
|
||||
onViewClick,
|
||||
onUpdateClick,
|
||||
lang = 'en'
|
||||
}: ListInfoComponentProps) {
|
||||
const t = getTranslation(lang);
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="p-4 bg-red-100 text-red-700 rounded">
|
||||
{t.error} {error.message}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<ActionButtonsComponent
|
||||
onCreateClick={onCreateClick}
|
||||
lang={lang}
|
||||
/>
|
||||
|
||||
<SortingComponent
|
||||
pagination={pagination}
|
||||
updatePagination={updatePagination}
|
||||
lang={lang}
|
||||
/>
|
||||
|
||||
<PaginationToolsComponent
|
||||
pagination={pagination}
|
||||
updatePagination={updatePagination}
|
||||
lang={lang}
|
||||
/>
|
||||
|
||||
{loading ? (
|
||||
<div className="flex justify-center items-center py-12">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-blue-500"></div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid gap-4">
|
||||
{data.map(item => (
|
||||
<DataCard
|
||||
key={item.id}
|
||||
item={item}
|
||||
onView={onViewClick}
|
||||
onUpdate={onUpdateClick}
|
||||
lang={lang}
|
||||
/>
|
||||
))}
|
||||
|
||||
{data.length === 0 && (
|
||||
<div className="text-center py-12 text-gray-500">
|
||||
{t.noItemsFound}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
import React from 'react';
|
||||
import { Pagination } from './schema';
|
||||
import { getTranslation, LanguageKey } from './language';
|
||||
|
||||
interface PaginationToolsComponentProps {
|
||||
pagination: Pagination;
|
||||
updatePagination: (updates: Partial<Pagination>) => void;
|
||||
lang?: LanguageKey;
|
||||
}
|
||||
|
||||
export function PaginationToolsComponent({
|
||||
pagination,
|
||||
updatePagination,
|
||||
lang = 'en'
|
||||
}: PaginationToolsComponentProps) {
|
||||
const t = getTranslation(lang);
|
||||
|
||||
const handlePageChange = (newPage: number) => {
|
||||
if (newPage >= 1 && newPage <= pagination.totalPages) {
|
||||
updatePagination({ page: newPage });
|
||||
}
|
||||
};
|
||||
|
||||
const handleSizeChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
updatePagination({ size: Number(e.target.value), page: 1 });
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-white p-4 rounded-lg shadow mb-4">
|
||||
<div className="flex flex-wrap justify-between items-center gap-4">
|
||||
{/* Navigation buttons */}
|
||||
<div className="flex items-center space-x-2">
|
||||
<button
|
||||
onClick={() => handlePageChange(pagination.page - 1)}
|
||||
disabled={pagination.page <= 1}
|
||||
className="px-3 py-1 bg-gray-200 rounded disabled:opacity-50"
|
||||
>
|
||||
{t.previous}
|
||||
</button>
|
||||
|
||||
<span className="px-4 py-1">
|
||||
{t.page} {pagination.page} {t.of} {pagination.totalPages}
|
||||
</span>
|
||||
|
||||
<button
|
||||
onClick={() => handlePageChange(pagination.page + 1)}
|
||||
disabled={pagination.page >= pagination.totalPages}
|
||||
className="px-3 py-1 bg-gray-200 rounded disabled:opacity-50"
|
||||
>
|
||||
{t.next}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Items per page selector */}
|
||||
<div className="flex items-center space-x-2">
|
||||
<label htmlFor="page-size" className="text-sm font-medium">{t.itemsPerPage}</label>
|
||||
<select
|
||||
id="page-size"
|
||||
value={pagination.size}
|
||||
onChange={handleSizeChange}
|
||||
className="border rounded px-2 py-1"
|
||||
>
|
||||
{[5, 10, 20, 50].map(size => (
|
||||
<option key={size} value={size}>{size}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Pagination stats */}
|
||||
<div className="text-sm text-gray-600">
|
||||
<div>{t.showing} {pagination.pageCount} {t.of} {pagination.totalCount} {t.items}</div>
|
||||
<div>{t.total}: {pagination.allCount} {t.items}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,133 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import { DataSchema } from './schema';
|
||||
import { getTranslation, LanguageKey } from './language';
|
||||
|
||||
interface SearchComponentProps {
|
||||
onSearch: (query: Record<string, string>) => void;
|
||||
lang?: LanguageKey;
|
||||
}
|
||||
|
||||
export function SearchComponent({ onSearch, lang = 'en' }: SearchComponentProps) {
|
||||
const t = getTranslation(lang);
|
||||
const [searchValue, setSearchValue] = useState('');
|
||||
const [activeFields, setActiveFields] = useState<string[]>([]);
|
||||
const [searchQuery, setSearchQuery] = useState<Record<string, string>>({});
|
||||
|
||||
// Update search query when fields or search value changes
|
||||
useEffect(() => {
|
||||
// Only update if we have active fields and a search value
|
||||
// or if we have no active fields (to clear the search)
|
||||
if ((activeFields.length > 0 && searchValue) || activeFields.length === 0) {
|
||||
const newQuery: Record<string, string> = {};
|
||||
|
||||
// Only add fields if we have a search value
|
||||
if (searchValue) {
|
||||
activeFields.forEach(field => {
|
||||
newQuery[field] = searchValue;
|
||||
});
|
||||
}
|
||||
|
||||
// Update local state
|
||||
setSearchQuery(newQuery);
|
||||
|
||||
// Don't call onSearch here - it creates an infinite loop
|
||||
// We'll call it in a separate effect
|
||||
}
|
||||
}, [activeFields, searchValue]);
|
||||
|
||||
// This effect handles calling the onSearch callback
|
||||
// It runs when searchQuery changes, not when onSearch changes
|
||||
useEffect(() => {
|
||||
onSearch(searchQuery);
|
||||
}, [searchQuery]);
|
||||
|
||||
const handleSearchChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const value = e.target.value;
|
||||
setSearchValue(value);
|
||||
|
||||
if (!value) {
|
||||
setSearchQuery({});
|
||||
onSearch({});
|
||||
return;
|
||||
}
|
||||
|
||||
if (activeFields.length === 0) {
|
||||
// If no fields are selected, don't search
|
||||
return;
|
||||
}
|
||||
|
||||
const newQuery: Record<string, string> = {};
|
||||
activeFields.forEach(field => {
|
||||
newQuery[field] = value;
|
||||
});
|
||||
|
||||
setSearchQuery(newQuery);
|
||||
onSearch(newQuery);
|
||||
};
|
||||
|
||||
const toggleField = (field: string) => {
|
||||
setActiveFields(prev => {
|
||||
if (prev.includes(field)) {
|
||||
return prev.filter(f => f !== field);
|
||||
} else {
|
||||
return [...prev, field];
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-white p-4 rounded-lg shadow mb-4">
|
||||
<div className="mb-3">
|
||||
<label htmlFor="search" className="block text-sm font-medium mb-1">
|
||||
{t.search || 'Search'}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="search"
|
||||
value={searchValue}
|
||||
onChange={handleSearchChange}
|
||||
placeholder={t.searchPlaceholder || 'Enter search term...'}
|
||||
className="w-full p-2 border rounded focus:ring-blue-500 focus:border-blue-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="text-sm font-medium mb-1">{t.searchFields || 'Search in fields'}:</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{Object.keys(DataSchema.shape).map(field => (
|
||||
<button
|
||||
key={field}
|
||||
onClick={() => toggleField(field)}
|
||||
className={`px-3 py-1 text-sm rounded ${
|
||||
activeFields.includes(field)
|
||||
? 'bg-blue-500 text-white'
|
||||
: 'bg-gray-200 text-gray-700'
|
||||
}`}
|
||||
>
|
||||
{field}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{Object.keys(searchQuery).length > 0 && (
|
||||
<div className="mt-3 p-2 bg-gray-100 rounded">
|
||||
<div className="text-sm font-medium mb-1">{t.activeSearch || 'Active search'}:</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{Object.entries(searchQuery).map(([field, value]) => (
|
||||
<div key={field} className="bg-blue-100 text-blue-800 px-2 py-1 rounded text-sm">
|
||||
{field}: {value}
|
||||
<button
|
||||
onClick={() => toggleField(field)}
|
||||
className="ml-2 text-blue-500 hover:text-blue-700"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
import React from 'react';
|
||||
import { Pagination, DataSchema } from './schema';
|
||||
import { getTranslation, LanguageKey } from './language';
|
||||
|
||||
interface SortingComponentProps {
|
||||
pagination: Pagination;
|
||||
updatePagination: (updates: Partial<Pagination>) => void;
|
||||
lang?: LanguageKey;
|
||||
}
|
||||
|
||||
export function SortingComponent({
|
||||
pagination,
|
||||
updatePagination,
|
||||
lang = 'en'
|
||||
}: SortingComponentProps) {
|
||||
const t = getTranslation(lang);
|
||||
|
||||
const handleSortChange = (field: string) => {
|
||||
// Find if the field is already in the orderFields array
|
||||
const fieldIndex = pagination.orderFields.indexOf(field);
|
||||
|
||||
// Create copies of the arrays to modify
|
||||
const newOrderFields = [...pagination.orderFields];
|
||||
const newOrderTypes = [...pagination.orderTypes];
|
||||
|
||||
if (fieldIndex === -1) {
|
||||
// Field is not being sorted yet - add it with 'asc' direction
|
||||
newOrderFields.push(field);
|
||||
newOrderTypes.push('asc');
|
||||
} else if (pagination.orderTypes[fieldIndex] === 'asc') {
|
||||
// Field is being sorted ascending - change to descending
|
||||
newOrderTypes[fieldIndex] = 'desc';
|
||||
} else {
|
||||
// Field is being sorted descending - remove it from sorting
|
||||
newOrderFields.splice(fieldIndex, 1);
|
||||
newOrderTypes.splice(fieldIndex, 1);
|
||||
}
|
||||
|
||||
updatePagination({
|
||||
orderFields: newOrderFields,
|
||||
orderTypes: newOrderTypes,
|
||||
page: 1,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-white p-4 rounded-lg shadow mb-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<label className="text-sm font-medium">{t.sortBy}</label>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{Object.keys(DataSchema.shape).map(field => {
|
||||
// Find if this field is in the orderFields array
|
||||
const fieldIndex = pagination.orderFields.indexOf(field);
|
||||
const isActive = fieldIndex !== -1;
|
||||
const direction = isActive ? pagination.orderTypes[fieldIndex] : null;
|
||||
|
||||
return (
|
||||
<button
|
||||
key={field}
|
||||
onClick={() => handleSortChange(field)}
|
||||
className={`px-3 py-1 rounded ${isActive ? 'bg-blue-500 text-white' : 'bg-gray-200'}`}
|
||||
>
|
||||
{field}
|
||||
{isActive && (
|
||||
<span className="ml-1">
|
||||
{direction === 'asc' ? '↑' : '↓'}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,125 @@
|
|||
"use client";
|
||||
import React, { useState } from "react";
|
||||
import { DataType } from "./schema";
|
||||
import { usePaginatedData } from "./hooks";
|
||||
import { FormComponent } from "./FormComponent";
|
||||
import { SearchComponent } from "./SearchComponent";
|
||||
import { getTranslation, LanguageKey } from "./language";
|
||||
import { ActionButtonsComponent } from "./ActionButtonsComponent";
|
||||
import { SortingComponent } from "./SortingComponent";
|
||||
import { PaginationToolsComponent } from "./PaginationToolsComponent";
|
||||
import { DataDisplayComponent } from "./DataDisplayComponent";
|
||||
|
||||
interface TemplateProps {
|
||||
lang?: LanguageKey;
|
||||
}
|
||||
|
||||
// Main template component
|
||||
function TemplateApp({ lang = "en" }: TemplateProps) {
|
||||
const t = getTranslation(lang);
|
||||
const { data, pagination, loading, error, updatePagination, refetch } =
|
||||
usePaginatedData();
|
||||
|
||||
const [mode, setMode] = useState<"list" | "create" | "view" | "update">(
|
||||
"list"
|
||||
);
|
||||
const [selectedItem, setSelectedItem] = useState<DataType | null>(null);
|
||||
|
||||
const handleCreateClick = () => {
|
||||
setSelectedItem(null);
|
||||
setMode("create");
|
||||
};
|
||||
|
||||
const handleViewClick = (item: DataType) => {
|
||||
setSelectedItem(item);
|
||||
setMode("view");
|
||||
};
|
||||
|
||||
const handleUpdateClick = (item: DataType) => {
|
||||
setSelectedItem(item);
|
||||
setMode("update");
|
||||
};
|
||||
|
||||
const handleFormSubmit = async (data: DataType) => {
|
||||
console.log("Submitting form data:", data);
|
||||
// Here you would call your API to save the data
|
||||
// await saveData(data);
|
||||
|
||||
// After saving, refresh the list and go back to list view
|
||||
setMode("list");
|
||||
setSelectedItem(null);
|
||||
refetch();
|
||||
};
|
||||
|
||||
const handleFormCancel = () => {
|
||||
setMode("list");
|
||||
setSelectedItem(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="container mx-auto px-4 py-6">
|
||||
<h1 className="text-2xl font-bold mb-6">{t.title}</h1>
|
||||
|
||||
{/* Search Component */}
|
||||
{mode === "list" && (
|
||||
<SearchComponent
|
||||
onSearch={(query: Record<string, string>) => {
|
||||
// Update pagination with both page reset and new query
|
||||
updatePagination({
|
||||
page: 1,
|
||||
query: query,
|
||||
});
|
||||
}}
|
||||
lang={lang}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Action Buttons Component */}
|
||||
{mode === "list" && (
|
||||
<ActionButtonsComponent onCreateClick={handleCreateClick} lang={lang} />
|
||||
)}
|
||||
|
||||
{/* Sorting Component */}
|
||||
{mode === "list" && (
|
||||
<SortingComponent
|
||||
pagination={pagination}
|
||||
updatePagination={updatePagination}
|
||||
lang={lang}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Pagination Tools Component */}
|
||||
{mode === "list" && (
|
||||
<PaginationToolsComponent
|
||||
pagination={pagination}
|
||||
updatePagination={updatePagination}
|
||||
lang={lang}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Data Display - Only shown in list mode */}
|
||||
{mode === "list" && (
|
||||
<DataDisplayComponent
|
||||
data={data}
|
||||
loading={loading}
|
||||
error={error}
|
||||
onViewClick={handleViewClick}
|
||||
onUpdateClick={handleUpdateClick}
|
||||
lang={lang}
|
||||
/>
|
||||
)}
|
||||
|
||||
{(mode === "create" || mode === "update" || mode === "view") && (
|
||||
<FormComponent
|
||||
initialData={selectedItem || undefined}
|
||||
onSubmit={handleFormSubmit}
|
||||
onCancel={handleFormCancel}
|
||||
readOnly={mode === "view"}
|
||||
lang={lang}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default TemplateApp;
|
||||
|
|
@ -0,0 +1,124 @@
|
|||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { DataType, Pagination, fetchData, DataSchema } from './schema';
|
||||
|
||||
// Define request parameters interface
|
||||
interface RequestParams {
|
||||
page: number;
|
||||
size: number;
|
||||
orderFields: string[];
|
||||
orderTypes: string[];
|
||||
query: Record<string, string>;
|
||||
}
|
||||
|
||||
// Define response metadata interface
|
||||
interface ResponseMetadata {
|
||||
totalCount: number;
|
||||
allCount: number;
|
||||
totalPages: number;
|
||||
pageCount: number;
|
||||
}
|
||||
|
||||
// Custom hook for pagination and data fetching
|
||||
export function usePaginatedData() {
|
||||
const [data, setData] = useState<DataType[]>([]);
|
||||
|
||||
// Request parameters - these are controlled by the user
|
||||
const [requestParams, setRequestParams] = useState<RequestParams>({
|
||||
page: 1,
|
||||
size: 10,
|
||||
orderFields: ['createdAt'],
|
||||
orderTypes: ['desc'],
|
||||
query: {},
|
||||
});
|
||||
|
||||
// Response metadata - these come from the API
|
||||
const [responseMetadata, setResponseMetadata] = useState<ResponseMetadata>({
|
||||
totalCount: 0,
|
||||
allCount: 0,
|
||||
totalPages: 0,
|
||||
pageCount: 0,
|
||||
});
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
|
||||
const fetchDataFromApi = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const result = await fetchData({
|
||||
page: requestParams.page,
|
||||
size: requestParams.size,
|
||||
orderFields: requestParams.orderFields,
|
||||
orderTypes: requestParams.orderTypes,
|
||||
query: requestParams.query,
|
||||
});
|
||||
|
||||
// Validate data with Zod
|
||||
const validatedData = result.data.map(item => {
|
||||
try {
|
||||
return DataSchema.parse(item);
|
||||
} catch (err) {
|
||||
console.error('Validation error for item:', item, err);
|
||||
return null;
|
||||
}
|
||||
}).filter(Boolean) as DataType[];
|
||||
|
||||
setData(validatedData);
|
||||
|
||||
// Update response metadata from API response
|
||||
setResponseMetadata({
|
||||
totalCount: result.pagination.totalCount,
|
||||
allCount: result.pagination.allCount,
|
||||
totalPages: result.pagination.totalPages,
|
||||
pageCount: result.pagination.pageCount,
|
||||
});
|
||||
|
||||
setError(null);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err : new Error('Unknown error'));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [requestParams.page, requestParams.size, requestParams.orderFields, requestParams.orderTypes, requestParams.query]);
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
fetchDataFromApi();
|
||||
}, 300); // Debounce
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, [fetchDataFromApi]);
|
||||
|
||||
const updatePagination = (updates: Partial<RequestParams>) => {
|
||||
setRequestParams(prev => ({
|
||||
...prev,
|
||||
...updates,
|
||||
}));
|
||||
};
|
||||
|
||||
// Create a combined refetch object that includes the setQuery function
|
||||
const setQuery = (query: Record<string, string>) => {
|
||||
setRequestParams(prev => ({
|
||||
...prev,
|
||||
query,
|
||||
}));
|
||||
};
|
||||
|
||||
const refetch = Object.assign(fetchDataFromApi, { setQuery });
|
||||
|
||||
// Combine request params and response metadata for backward compatibility
|
||||
const pagination: Pagination = {
|
||||
...requestParams,
|
||||
...responseMetadata,
|
||||
};
|
||||
|
||||
return {
|
||||
data,
|
||||
pagination,
|
||||
loading,
|
||||
error,
|
||||
updatePagination,
|
||||
setQuery,
|
||||
refetch,
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
// Language dictionary for the template component
|
||||
const language = {
|
||||
en: {
|
||||
title: 'Data Management',
|
||||
create: 'Create New',
|
||||
view: 'View Item',
|
||||
update: 'Update Item',
|
||||
createNew: 'Create New Item',
|
||||
back: 'Back',
|
||||
cancel: 'Cancel',
|
||||
submit: 'Submit',
|
||||
noItemsFound: 'No items found',
|
||||
previous: 'Previous',
|
||||
next: 'Next',
|
||||
page: 'Page',
|
||||
of: 'of',
|
||||
itemsPerPage: 'Items per page:',
|
||||
sortBy: 'Sort by:',
|
||||
loading: 'Loading...',
|
||||
error: 'Error loading data:',
|
||||
showing: 'Showing',
|
||||
items: 'items',
|
||||
total: 'Total',
|
||||
// Search related translations
|
||||
search: 'Search',
|
||||
searchPlaceholder: 'Enter search term...',
|
||||
searchFields: 'Search in fields',
|
||||
activeSearch: 'Active search',
|
||||
clearSearch: 'Clear',
|
||||
formLabels: {
|
||||
title: 'Title',
|
||||
description: 'Description',
|
||||
status: 'Status',
|
||||
createdAt: 'Created'
|
||||
},
|
||||
status: {
|
||||
active: 'Active',
|
||||
inactive: 'Inactive'
|
||||
},
|
||||
buttons: {
|
||||
view: 'View',
|
||||
update: 'Update',
|
||||
create: 'Create',
|
||||
save: 'Save'
|
||||
}
|
||||
},
|
||||
// Add more languages as needed
|
||||
};
|
||||
|
||||
export type LanguageKey = keyof typeof language;
|
||||
|
||||
export const getTranslation = (lang: LanguageKey = 'en') => {
|
||||
return language[lang] || language.en;
|
||||
};
|
||||
|
||||
export default language;
|
||||
|
|
@ -0,0 +1,98 @@
|
|||
import { z } from "zod";
|
||||
|
||||
// Define the data schema using Zod
|
||||
export const DataSchema = z.object({
|
||||
id: z.string(),
|
||||
title: z.string(),
|
||||
description: z.string().optional(),
|
||||
status: z.string(),
|
||||
createdAt: z.string().or(z.date()),
|
||||
// Add more fields as needed
|
||||
});
|
||||
|
||||
export type DataType = z.infer<typeof DataSchema>;
|
||||
|
||||
// Define pagination interface
|
||||
export interface Pagination {
|
||||
page: number;
|
||||
size: number;
|
||||
totalCount: number;
|
||||
allCount: number;
|
||||
totalPages: number;
|
||||
orderFields: string[];
|
||||
orderTypes: string[];
|
||||
pageCount: number;
|
||||
query: Record<string, string>;
|
||||
}
|
||||
|
||||
// Mock API function (replace with your actual API call)
|
||||
export const fetchData = async ({
|
||||
page = 1,
|
||||
size = 10,
|
||||
orderFields = ["createdAt"],
|
||||
orderTypes = ["desc"],
|
||||
query = {},
|
||||
}: {
|
||||
page?: number;
|
||||
size?: number;
|
||||
orderFields?: string[];
|
||||
orderTypes?: string[];
|
||||
query?: Record<string, any>;
|
||||
}) => {
|
||||
// Replace with your actual API call
|
||||
console.log("Fetching data with:", {
|
||||
page,
|
||||
size,
|
||||
orderFields,
|
||||
orderTypes,
|
||||
query,
|
||||
});
|
||||
|
||||
// Simulated API response
|
||||
return new Promise<{ data: DataType[]; pagination: Pagination }>(
|
||||
(resolve) => {
|
||||
setTimeout(() => {
|
||||
// Generate mock data
|
||||
const mockData: DataType[] = Array.from({ length: 100 }, (_, i) => ({
|
||||
id: `id-${i + 1}`,
|
||||
title: `Title ${i + 1}`,
|
||||
description: `Description for item ${i + 1}`,
|
||||
status: i % 3 === 0 ? "active" : "inactive",
|
||||
createdAt: new Date(
|
||||
Date.now() - Math.floor(Math.random() * 10000000000)
|
||||
).toISOString(),
|
||||
}));
|
||||
|
||||
// Filter by query if provided
|
||||
const filteredData = Object.keys(query).length
|
||||
? mockData.filter((item) => {
|
||||
return Object.entries(query).every(([key, value]) => {
|
||||
return String(item[key as keyof DataType])
|
||||
.toLowerCase()
|
||||
.includes(String(value).toLowerCase());
|
||||
});
|
||||
})
|
||||
: mockData;
|
||||
|
||||
// Apply pagination
|
||||
const startIndex = (page - 1) * size;
|
||||
const paginatedData = filteredData.slice(startIndex, startIndex + size);
|
||||
|
||||
resolve({
|
||||
data: paginatedData,
|
||||
pagination: {
|
||||
page,
|
||||
size,
|
||||
totalCount: filteredData.length,
|
||||
allCount: mockData.length,
|
||||
totalPages: Math.ceil(filteredData.length / size),
|
||||
orderFields,
|
||||
orderTypes,
|
||||
pageCount: paginatedData.length,
|
||||
query, // Include the query in the pagination object
|
||||
},
|
||||
});
|
||||
}, 500);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
import React from "react";
|
||||
import { Pencil, ScanSearch } from "lucide-react";
|
||||
|
||||
interface CardProps {
|
||||
data: [string, string, string, string];
|
||||
langDictionary: [string, string, string, string];
|
||||
onUpdate: (uu_id: string) => void;
|
||||
onView: (uu_id: string) => void;
|
||||
}
|
||||
|
||||
const CardComponent: React.FC<CardProps> = ({
|
||||
data,
|
||||
langDictionary,
|
||||
onUpdate,
|
||||
onView,
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
<div className="bg-white text-black rounded-lg shadow-md p-6 mb-4 hover:shadow-lg transition-shadow">
|
||||
<div className="flex justify-between items-start">
|
||||
<div>
|
||||
<h3 className="text-xl font-semibold mb-2">
|
||||
{langDictionary[1]}:{data[1]}
|
||||
</h3>
|
||||
<p className="mb-2">
|
||||
{langDictionary[2]}: {data[2]}
|
||||
</p>
|
||||
<div className="flex items-center gap-4">
|
||||
<span className="text-sm text-gray-500">
|
||||
{langDictionary[0]} {data[0]}
|
||||
</span>
|
||||
<span className="text-sm text-gray-500">
|
||||
{langDictionary[3]}: {new Date(data[3]).toDateString()}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
className="text-blue-500 hover:text-blue-700 p-2"
|
||||
aria-label="Update"
|
||||
>
|
||||
<div className="flex flex-col">
|
||||
<div onClick={() => onUpdate(data[0])}>
|
||||
<Pencil />
|
||||
</div>
|
||||
<div className="mt-5" onClick={() => onView(data[0])}>
|
||||
<ScanSearch />
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default CardComponent;
|
||||
|
|
@ -0,0 +1,109 @@
|
|||
"use client";
|
||||
|
||||
import Menu from "./store";
|
||||
|
||||
// Define TypeScript interfaces for menu structure
|
||||
export interface LanguageTranslation {
|
||||
tr: string;
|
||||
en: string;
|
||||
}
|
||||
|
||||
export interface MenuThirdLevel {
|
||||
name: string;
|
||||
lg: LanguageTranslation;
|
||||
siteUrl: string;
|
||||
}
|
||||
|
||||
export interface MenuSecondLevel {
|
||||
name: string;
|
||||
lg: LanguageTranslation;
|
||||
subList: MenuThirdLevel[];
|
||||
}
|
||||
|
||||
export interface MenuFirstLevel {
|
||||
name: string;
|
||||
lg: LanguageTranslation;
|
||||
subList: MenuSecondLevel[];
|
||||
}
|
||||
|
||||
// Define interfaces for the filtered menu structure
|
||||
export interface FilteredMenuThirdLevel {
|
||||
name: string;
|
||||
lg: LanguageTranslation;
|
||||
siteUrl: string;
|
||||
}
|
||||
|
||||
export interface FilteredMenuSecondLevel {
|
||||
name: string;
|
||||
lg: LanguageTranslation;
|
||||
subList: FilteredMenuThirdLevel[];
|
||||
}
|
||||
|
||||
export interface FilteredMenuFirstLevel {
|
||||
name: string;
|
||||
lg: LanguageTranslation;
|
||||
subList: FilteredMenuSecondLevel[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters the menu structure based on intersections with provided URLs
|
||||
* @param {string[]} siteUrls - Array of site URLs to check for intersection
|
||||
* @returns {Array} - Filtered menu structure with only matching items
|
||||
*/
|
||||
export function transformMenu(siteUrls: string[]) {
|
||||
// Process the menu structure
|
||||
const filteredMenu: FilteredMenuFirstLevel[] = Menu.reduce(
|
||||
(acc: FilteredMenuFirstLevel[], firstLevel: MenuFirstLevel) => {
|
||||
// Create a new first level item with empty subList
|
||||
const newFirstLevel: FilteredMenuFirstLevel = {
|
||||
name: firstLevel.name,
|
||||
lg: { ...firstLevel.lg },
|
||||
subList: [],
|
||||
};
|
||||
|
||||
// Process second level items
|
||||
firstLevel.subList.forEach((secondLevel: MenuSecondLevel) => {
|
||||
// Create a new second level item with empty subList
|
||||
const newSecondLevel: FilteredMenuSecondLevel = {
|
||||
name: secondLevel.name,
|
||||
lg: { ...secondLevel.lg },
|
||||
subList: [],
|
||||
};
|
||||
|
||||
// Process third level items
|
||||
secondLevel.subList.forEach((thirdLevel: MenuThirdLevel) => {
|
||||
// Check if the third level's siteUrl matches exactly
|
||||
if (
|
||||
thirdLevel.siteUrl &&
|
||||
siteUrls.some((url) => url === thirdLevel.siteUrl)
|
||||
) {
|
||||
// Create a modified third level item
|
||||
const newThirdLevel: FilteredMenuThirdLevel = {
|
||||
name: thirdLevel.name,
|
||||
lg: { ...thirdLevel.lg },
|
||||
siteUrl: thirdLevel.siteUrl,
|
||||
};
|
||||
|
||||
// Add the modified third level to the second level's subList
|
||||
newSecondLevel.subList.push(newThirdLevel);
|
||||
}
|
||||
});
|
||||
|
||||
// Only add the second level to the first level if it has any matching third level items
|
||||
if (newSecondLevel.subList.length > 0) {
|
||||
newFirstLevel.subList.push(newSecondLevel);
|
||||
}
|
||||
});
|
||||
|
||||
// Only add the first level to the result if it has any matching second level items
|
||||
if (newFirstLevel.subList.length > 0) {
|
||||
acc.push(newFirstLevel);
|
||||
}
|
||||
|
||||
return acc;
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
return filteredMenu;
|
||||
}
|
||||
|
|
@ -0,0 +1,116 @@
|
|||
"use client";
|
||||
|
||||
import React, { useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { Home, ChevronDown, ChevronRight } from "lucide-react";
|
||||
import { transformMenu } from "./handler";
|
||||
import type { LanguageTranslation } from "./handler";
|
||||
|
||||
interface ClientMenuProps {
|
||||
siteUrls: string[];
|
||||
lang: keyof LanguageTranslation;
|
||||
}
|
||||
|
||||
const ClientMenu: React.FC<ClientMenuProps> = ({ siteUrls, lang }) => {
|
||||
const transformedMenu = transformMenu(siteUrls) || [];
|
||||
|
||||
// State to track which menu items are expanded
|
||||
const [firstLayerIndex, setFirstLayerIndex] = useState<number>(-1);
|
||||
const [secondLayerIndex, setSecondLayerIndex] = useState<number>(-1);
|
||||
|
||||
// Handle first level menu click
|
||||
const handleFirstLevelClick = (index: number) => {
|
||||
setFirstLayerIndex(index === firstLayerIndex ? -1 : index);
|
||||
setSecondLayerIndex(-1); // Reset second layer selection when first layer changes
|
||||
};
|
||||
|
||||
// Handle second level menu click
|
||||
const handleSecondLevelClick = (index: number) => {
|
||||
setSecondLayerIndex(index === secondLayerIndex ? -1 : index);
|
||||
};
|
||||
|
||||
// No need for a navigation handler since we'll use Link components
|
||||
|
||||
return (
|
||||
<div className="w-full bg-white shadow-sm rounded-lg overflow-hidden">
|
||||
<div className="p-4 border-b border-gray-200">
|
||||
<h2 className="text-xl font-bold text-center text-gray-800">
|
||||
Dashboard
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<nav className="flex flex-col">
|
||||
{transformedMenu &&
|
||||
transformedMenu.map((item, firstIndex) => (
|
||||
<div
|
||||
key={item.name}
|
||||
className="border-b border-gray-100 last:border-b-0"
|
||||
>
|
||||
<button
|
||||
onClick={() => handleFirstLevelClick(firstIndex)}
|
||||
className={`flex items-center justify-between w-full p-4 text-left transition-colors ${
|
||||
firstIndex === firstLayerIndex
|
||||
? "bg-emerald-50 text-emerald-700"
|
||||
: "text-gray-700 hover:bg-gray-50"
|
||||
}`}
|
||||
>
|
||||
<span className="font-medium">{item.lg[lang]}</span>
|
||||
{firstIndex === firstLayerIndex ? (
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
) : (
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
)}
|
||||
</button>
|
||||
|
||||
{/* First level separator and second layer */}
|
||||
{firstIndex === firstLayerIndex && (
|
||||
<div className="bg-gray-50 border-t border-gray-100">
|
||||
{item.subList.map((subItem, secondIndex) => (
|
||||
<div
|
||||
key={subItem.name}
|
||||
className="border-b border-gray-100 last:border-b-0"
|
||||
>
|
||||
<button
|
||||
onClick={() => handleSecondLevelClick(secondIndex)}
|
||||
className={`flex items-center justify-between w-full p-3 pl-8 text-left transition-colors ${
|
||||
secondIndex === secondLayerIndex
|
||||
? "bg-emerald-100 text-emerald-800"
|
||||
: "text-gray-600 hover:bg-gray-100"
|
||||
}`}
|
||||
>
|
||||
<span className="font-medium">{subItem.lg[lang]}</span>
|
||||
{secondIndex === secondLayerIndex ? (
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
) : (
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
)}
|
||||
</button>
|
||||
|
||||
{/* Second level separator and third layer */}
|
||||
{firstIndex === firstLayerIndex &&
|
||||
secondIndex === secondLayerIndex && (
|
||||
<div className="bg-gray-100 border-t border-gray-200">
|
||||
{subItem.subList.map((subSubItem) => (
|
||||
<Link
|
||||
key={subSubItem.name}
|
||||
href={subSubItem.siteUrl}
|
||||
className="flex items-center w-full p-3 pl-12 text-left text-gray-700 hover:bg-gray-200 transition-colors"
|
||||
>
|
||||
<Home className="h-4 w-4 mr-2 text-gray-500" />
|
||||
<span>{subSubItem.lg[lang]}</span>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</nav>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ClientMenu;
|
||||
|
|
@ -0,0 +1,203 @@
|
|||
const Individual = {
|
||||
name: "Individual",
|
||||
lg: {
|
||||
tr: "Birey",
|
||||
en: "Individual",
|
||||
},
|
||||
siteUrl: "/individual",
|
||||
};
|
||||
|
||||
const User = {
|
||||
name: "User",
|
||||
lg: {
|
||||
tr: "Kullanıcı",
|
||||
en: "User",
|
||||
},
|
||||
siteUrl: "/user",
|
||||
};
|
||||
|
||||
const Build = {
|
||||
name: "Build",
|
||||
lg: {
|
||||
tr: "Apartman",
|
||||
en: "Build",
|
||||
},
|
||||
siteUrl: "/build",
|
||||
};
|
||||
|
||||
const Dashboard = {
|
||||
name: "Dashboard",
|
||||
lg: {
|
||||
tr: "Pano",
|
||||
en: "Dashboard",
|
||||
},
|
||||
siteUrl: "/dashboard",
|
||||
};
|
||||
|
||||
const BuildParts = {
|
||||
name: "BuildParts",
|
||||
lg: {
|
||||
tr: "Daireler",
|
||||
en: "Build Parts",
|
||||
},
|
||||
siteUrl: "/build/parts",
|
||||
};
|
||||
|
||||
const BuildArea = {
|
||||
name: "BuildArea",
|
||||
lg: {
|
||||
tr: "Daire Alanları",
|
||||
en: "Build Area",
|
||||
},
|
||||
siteUrl: "/build/area",
|
||||
};
|
||||
|
||||
const ManagementAccounting = {
|
||||
name: "ManagementAccounting",
|
||||
lg: {
|
||||
tr: "Yönetim Cari Hareketler",
|
||||
en: "ManagementAccounting",
|
||||
},
|
||||
siteUrl: "/management/accounting",
|
||||
};
|
||||
|
||||
const ManagementBudget = {
|
||||
name: "ManagementBudget",
|
||||
lg: {
|
||||
tr: "Yönetim Bütçe İşlemleri",
|
||||
en: "Management Budget",
|
||||
},
|
||||
siteUrl: "/management/budget",
|
||||
};
|
||||
|
||||
const BuildPartsAccounting = {
|
||||
name: "BuildPartsAccounting",
|
||||
lg: {
|
||||
tr: "Daire Cari Hareketler",
|
||||
en: "Build Parts Accounting",
|
||||
},
|
||||
siteUrl: "/build/parts/accounting",
|
||||
};
|
||||
|
||||
const AnnualMeeting = {
|
||||
name: "AnnualMeeting",
|
||||
lg: {
|
||||
tr: "Yıllık Olağan Toplantı Tanımlama ve Davet",
|
||||
en: "Annual Meetings and Invitations",
|
||||
},
|
||||
siteUrl: "/annual/meeting",
|
||||
};
|
||||
|
||||
const AnnualMeetingClose = {
|
||||
name: "AnnualMeetingClose",
|
||||
lg: {
|
||||
tr: "Yıllık Olağan Toplantı kapatma ve Cari Yaratma",
|
||||
en: "Annual Meeting Close and Accountings",
|
||||
},
|
||||
siteUrl: "/annual/meeting/close",
|
||||
};
|
||||
|
||||
const EmergencyMeeting = {
|
||||
name: "EmergencyMeeting",
|
||||
lg: {
|
||||
tr: "Acil Toplantı Tanımlama ve Davet",
|
||||
en: "Emergency Meeting and Invitations",
|
||||
},
|
||||
siteUrl: "/emergency/meeting",
|
||||
};
|
||||
|
||||
const EmergencyMeetingClose = {
|
||||
name: "EmergencyMeetingClose",
|
||||
lg: {
|
||||
tr: "Acil Olağan Toplantı kapatma ve Cari Yaratma",
|
||||
en: "Emergency Meeting Close and Accountings",
|
||||
},
|
||||
siteUrl: "/emergency/meeting/close",
|
||||
};
|
||||
|
||||
const MeetingParticipations = {
|
||||
name: "MeetingParticipations",
|
||||
lg: {
|
||||
tr: "Toplantı Katılım İşlemleri",
|
||||
en: "Meeting Participations",
|
||||
},
|
||||
siteUrl: "/meeting/participation",
|
||||
};
|
||||
|
||||
const Menu = [
|
||||
{
|
||||
name: "Dashboard",
|
||||
lg: {
|
||||
tr: "Pano",
|
||||
en: "Dashboard",
|
||||
},
|
||||
subList: [
|
||||
{
|
||||
name: "Dashboard",
|
||||
lg: {
|
||||
tr: "Pano",
|
||||
en: "Dashboard",
|
||||
},
|
||||
subList: [Dashboard],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Definitions",
|
||||
lg: {
|
||||
tr: "Tanımlar",
|
||||
en: "Definitions",
|
||||
},
|
||||
subList: [
|
||||
{
|
||||
name: "People",
|
||||
lg: {
|
||||
tr: "Kişiler",
|
||||
en: "People",
|
||||
},
|
||||
subList: [Individual, User],
|
||||
},
|
||||
{
|
||||
name: "Building",
|
||||
lg: {
|
||||
tr: "Binalar",
|
||||
en: "Building",
|
||||
},
|
||||
subList: [Build, BuildParts, BuildArea],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Building Management",
|
||||
lg: {
|
||||
tr: "Bina Yönetimi",
|
||||
en: "Building Management",
|
||||
},
|
||||
subList: [
|
||||
{
|
||||
name: "Management Accounting",
|
||||
lg: {
|
||||
tr: "Cari işlemler",
|
||||
en: "Management Accounting",
|
||||
},
|
||||
subList: [ManagementAccounting, ManagementBudget, BuildPartsAccounting],
|
||||
},
|
||||
{
|
||||
name: "Meetings",
|
||||
lg: {
|
||||
tr: "Toplantılar",
|
||||
en: "Meetings",
|
||||
},
|
||||
subList: [
|
||||
AnnualMeeting,
|
||||
AnnualMeetingClose,
|
||||
EmergencyMeeting,
|
||||
EmergencyMeetingClose,
|
||||
MeetingParticipations,
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export default Menu;
|
||||
|
|
@ -51,22 +51,82 @@ services:
|
|||
- REDIS_DB=0
|
||||
ports:
|
||||
- "11222:6379"
|
||||
mem_limit: 512M
|
||||
cpus: 0.5
|
||||
|
||||
# client_frontend:
|
||||
# container_name: client_frontend
|
||||
# build:
|
||||
# context: .
|
||||
# dockerfile: WebServices/client-frontend/Dockerfile
|
||||
# networks:
|
||||
# - wag-services
|
||||
# ports:
|
||||
# - "3000:3000"
|
||||
# # volumes:
|
||||
# # - client-frontend:/WebServices/client-frontend
|
||||
# environment:
|
||||
# - NODE_ENV=development
|
||||
# mem_limit: 4096M
|
||||
# cpus: 2.0
|
||||
client_frontend:
|
||||
container_name: client_frontend
|
||||
build:
|
||||
context: .
|
||||
dockerfile: WebServices/client-frontend/Dockerfile
|
||||
networks:
|
||||
- wag-services
|
||||
ports:
|
||||
- "3000:3000"
|
||||
# volumes:
|
||||
# - client-frontend:/WebServices/client-frontend
|
||||
environment:
|
||||
- NODE_ENV=development
|
||||
mem_limit: 4096M
|
||||
cpus: 1.5
|
||||
|
||||
identity_service:
|
||||
container_name: identity_service
|
||||
build:
|
||||
context: .
|
||||
dockerfile: ApiServices/IdentityService/Dockerfile
|
||||
networks:
|
||||
- wag-services
|
||||
env_file:
|
||||
- api_env.env
|
||||
environment:
|
||||
- API_PATH=app:app
|
||||
- API_HOST=0.0.0.0
|
||||
- API_PORT=8002
|
||||
- API_LOG_LEVEL=info
|
||||
- API_RELOAD=1
|
||||
- API_APP_NAME=evyos-identity-api-gateway
|
||||
- API_TITLE=WAG API Identity Api Gateway
|
||||
- API_FORGOT_LINK=https://identity_service/forgot-password
|
||||
- API_DESCRIPTION=This api is serves as web identity api gateway only to evyos web services.
|
||||
- API_APP_URL=https://identity_service
|
||||
ports:
|
||||
- "8002:8002"
|
||||
depends_on:
|
||||
- postgres-service
|
||||
- mongo_service
|
||||
- redis_service
|
||||
mem_limit: 512M
|
||||
cpus: 0.5
|
||||
|
||||
auth_service:
|
||||
container_name: auth_service
|
||||
build:
|
||||
context: .
|
||||
dockerfile: ApiServices/AuthService/Dockerfile
|
||||
networks:
|
||||
- wag-services
|
||||
env_file:
|
||||
- api_env.env
|
||||
environment:
|
||||
- API_PATH=app:app
|
||||
- API_HOST=0.0.0.0
|
||||
- API_PORT=8001
|
||||
- API_LOG_LEVEL=info
|
||||
- API_RELOAD=1
|
||||
- API_APP_NAME=evyos-auth-api-gateway
|
||||
- API_TITLE=WAG API Auth Api Gateway
|
||||
- API_FORGOT_LINK=https://auth_service/forgot-password
|
||||
- API_DESCRIPTION=This api is serves as web auth api gateway only to evyos web services.
|
||||
- API_APP_URL=https://auth_service
|
||||
ports:
|
||||
- "8001:8001"
|
||||
depends_on:
|
||||
- postgres-service
|
||||
- mongo_service
|
||||
- redis_service
|
||||
mem_limit: 512M
|
||||
cpus: 0.5
|
||||
|
||||
# management_frontend:
|
||||
# container_name: management_frontend
|
||||
|
|
@ -137,74 +197,6 @@ services:
|
|||
# - mongo_service
|
||||
# - redis_service
|
||||
|
||||
# dealer_service:
|
||||
# container_name: dealer_service
|
||||
# build:
|
||||
# context: .
|
||||
# dockerfile: ApiServices/DealerService/Dockerfile
|
||||
# networks:
|
||||
# - wag-services
|
||||
# env_file:
|
||||
# - api_env.env
|
||||
# depends_on:
|
||||
# - postgres-service
|
||||
# - mongo_service
|
||||
# - redis_service
|
||||
|
||||
identity_service:
|
||||
container_name: identity_service
|
||||
build:
|
||||
context: .
|
||||
dockerfile: ApiServices/IdentityService/Dockerfile
|
||||
networks:
|
||||
- wag-services
|
||||
env_file:
|
||||
- api_env.env
|
||||
environment:
|
||||
- API_PATH=app:app
|
||||
- API_HOST=0.0.0.0
|
||||
- API_PORT=8002
|
||||
- API_LOG_LEVEL=info
|
||||
- API_RELOAD=1
|
||||
- API_APP_NAME=evyos-identity-api-gateway
|
||||
- API_TITLE=WAG API Identity Api Gateway
|
||||
- API_FORGOT_LINK=https://identity_service/forgot-password
|
||||
- API_DESCRIPTION=This api is serves as web identity api gateway only to evyos web services.
|
||||
- API_APP_URL=https://identity_service
|
||||
ports:
|
||||
- "8002:8002"
|
||||
depends_on:
|
||||
- postgres-service
|
||||
- mongo_service
|
||||
- redis_service
|
||||
|
||||
auth_service:
|
||||
container_name: auth_service
|
||||
build:
|
||||
context: .
|
||||
dockerfile: ApiServices/AuthService/Dockerfile
|
||||
networks:
|
||||
- wag-services
|
||||
env_file:
|
||||
- api_env.env
|
||||
environment:
|
||||
- API_PATH=app:app
|
||||
- API_HOST=0.0.0.0
|
||||
- API_PORT=8001
|
||||
- API_LOG_LEVEL=info
|
||||
- API_RELOAD=1
|
||||
- API_APP_NAME=evyos-auth-api-gateway
|
||||
- API_TITLE=WAG API Auth Api Gateway
|
||||
- API_FORGOT_LINK=https://auth_service/forgot-password
|
||||
- API_DESCRIPTION=This api is serves as web auth api gateway only to evyos web services.
|
||||
- API_APP_URL=https://auth_service
|
||||
ports:
|
||||
- "8001:8001"
|
||||
depends_on:
|
||||
- postgres-service
|
||||
- mongo_service
|
||||
- redis_service
|
||||
|
||||
# test_server:
|
||||
# container_name: test_server
|
||||
# build:
|
||||
|
|
|
|||
Loading…
Reference in New Issue