updated components and page navigator
This commit is contained in:
parent
6ab4410a82
commit
a0daf45530
|
|
@ -181,7 +181,7 @@ def init_applications_for_tenant(super_user: BuildLivingSpace, db_session=None)
|
||||||
),
|
),
|
||||||
dict(
|
dict(
|
||||||
name="TenantSendMessageToBuildManager",
|
name="TenantSendMessageToBuildManager",
|
||||||
application_code="app000014",
|
application_code="app000022",
|
||||||
site_url="/tenant/messageToBM",
|
site_url="/tenant/messageToBM",
|
||||||
application_type="Dash",
|
application_type="Dash",
|
||||||
description="Individual Page for tenant send message to build manager",
|
description="Individual Page for tenant send message to build manager",
|
||||||
|
|
@ -200,6 +200,7 @@ def init_applications_for_tenant(super_user: BuildLivingSpace, db_session=None)
|
||||||
application_type="Dash",
|
application_type="Dash",
|
||||||
description="Individual Page for tenant account view",
|
description="Individual Page for tenant account view",
|
||||||
),
|
),
|
||||||
|
|
||||||
]
|
]
|
||||||
|
|
||||||
for list_of_created_app in list_of_created_apps:
|
for list_of_created_app in list_of_created_apps:
|
||||||
|
|
|
||||||
|
|
@ -63,7 +63,7 @@ version_path_separator = os
|
||||||
# are written from script.py.mako
|
# are written from script.py.mako
|
||||||
# output_encoding = utf-8
|
# output_encoding = utf-8
|
||||||
|
|
||||||
sqlalchemy.url = postgresql+psycopg2://berkay_wag_user:berkay_wag_user_password@postgres-service:5432/wag_database
|
sqlalchemy.url = postgresql+psycopg2://postgres:password@10.10.2.14:5432/postgres
|
||||||
|
|
||||||
|
|
||||||
[post_write_hooks]
|
[post_write_hooks]
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
from logging.config import fileConfig
|
from logging.config import fileConfig
|
||||||
|
import os
|
||||||
|
|
||||||
from sqlalchemy import engine_from_config
|
from sqlalchemy import engine_from_config
|
||||||
from sqlalchemy import pool
|
from sqlalchemy import pool
|
||||||
|
from sqlalchemy import create_engine
|
||||||
|
|
||||||
from alembic import context
|
from alembic import context
|
||||||
from Schemas import *
|
from Schemas import *
|
||||||
|
|
@ -11,6 +13,19 @@ from Controllers.Postgres.database import Base
|
||||||
# access to the values within the .ini file in use.
|
# access to the values within the .ini file in use.
|
||||||
config = context.config
|
config = context.config
|
||||||
|
|
||||||
|
# Override sqlalchemy.url with environment variables if they exist
|
||||||
|
db_host = os.getenv("POSTGRES_HOST", "10.10.2.14")
|
||||||
|
db_port = os.getenv("POSTGRES_PORT", "5432")
|
||||||
|
db_user = os.getenv("POSTGRES_USER", "postgres")
|
||||||
|
db_password = os.getenv("POSTGRES_PASSWORD", "password")
|
||||||
|
db_name = os.getenv("POSTGRES_DB", "postgres")
|
||||||
|
|
||||||
|
# Build the connection URL from environment variables
|
||||||
|
db_url = f"postgresql+psycopg2://{db_user}:{db_password}@{db_host}:{db_port}/{db_name}"
|
||||||
|
|
||||||
|
# Override the sqlalchemy.url in the alembic.ini file
|
||||||
|
config.set_main_option("sqlalchemy.url", db_url)
|
||||||
|
|
||||||
# Interpret the config file for Python logging.
|
# Interpret the config file for Python logging.
|
||||||
# This line sets up loggers basically.
|
# This line sets up loggers basically.
|
||||||
if config.config_file_name is not None:
|
if config.config_file_name is not None:
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import os
|
||||||
from Controllers.Postgres.database import get_db
|
from Controllers.Postgres.database import get_db
|
||||||
|
|
||||||
from init_app_defaults import create_application_defaults
|
from init_app_defaults import create_application_defaults
|
||||||
|
|
@ -8,7 +9,7 @@ from init_services import create_modules_and_services_and_actions
|
||||||
from init_address import create_one_address
|
from init_address import create_one_address
|
||||||
from init_occ_defaults import create_occupant_defaults
|
from init_occ_defaults import create_occupant_defaults
|
||||||
|
|
||||||
set_alembic = False
|
set_alembic = bool(os.getenv("set_alembic", 0))
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ interface LoginSelectOccupant {
|
||||||
|
|
||||||
async function logoutActiveSession() {
|
async function logoutActiveSession() {
|
||||||
const cookieStore = await cookies();
|
const cookieStore = await cookies();
|
||||||
const response = await fetchDataWithToken(logoutEndpoint, {}, "POST", false);
|
const response = await fetchDataWithToken(logoutEndpoint, {}, "GET", false);
|
||||||
cookieStore.delete("accessToken");
|
cookieStore.delete("accessToken");
|
||||||
cookieStore.delete("accessObject");
|
cookieStore.delete("accessObject");
|
||||||
cookieStore.delete("userProfile");
|
cookieStore.delete("userProfile");
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
"use server";
|
"use server";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import ClientMenu from "@/components/menu/menu";
|
|
||||||
import { retrievePageList, retrievePagebyUrl } from "@/apicalls/cookies/token";
|
import { retrievePageList, retrievePagebyUrl } from "@/apicalls/cookies/token";
|
||||||
import { retrievePage } from "@/components/NavigatePages";
|
|
||||||
|
import ClientMenu from "@/components/menu/menu";
|
||||||
import Header from "@/components/header/Header";
|
import Header from "@/components/header/Header";
|
||||||
|
import retrievePageByUrlAndPageId from "@/components/navigator/retriever";
|
||||||
|
|
||||||
export default async function DashboardLayout({
|
export default async function DashboardLayout({
|
||||||
searchParams,
|
searchParams,
|
||||||
|
|
@ -13,7 +14,7 @@ export default async function DashboardLayout({
|
||||||
const activePage = "/dashboard";
|
const activePage = "/dashboard";
|
||||||
const siteUrlsList = (await retrievePageList()) || [];
|
const siteUrlsList = (await retrievePageList()) || [];
|
||||||
const pageToDirect = await retrievePagebyUrl(activePage);
|
const pageToDirect = await retrievePagebyUrl(activePage);
|
||||||
const PageComponent = retrievePage(pageToDirect);
|
const PageComponent = retrievePageByUrlAndPageId(pageToDirect, activePage);
|
||||||
const searchParamsInstance = await searchParams;
|
const searchParamsInstance = await searchParams;
|
||||||
const lang = (searchParamsInstance?.lang as "en" | "tr") || "en";
|
const lang = (searchParamsInstance?.lang as "en" | "tr") || "en";
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import ClientMenu from "@/components/menu/menu";
|
import ClientMenu from "@/components/menu/menu";
|
||||||
import { retrievePageList, retrievePagebyUrl } from "@/apicalls/cookies/token";
|
import { retrievePageList, retrievePagebyUrl } from "@/apicalls/cookies/token";
|
||||||
import { retrievePage } from "@/components/NavigatePages";
|
import { retrievePageByUrlAndPageId } from "@/components/navigator/retriever";
|
||||||
import { searchPlaceholder } from "@/app/commons/pageDefaults";
|
import { searchPlaceholder } from "@/app/commons/pageDefaults";
|
||||||
|
|
||||||
const pageInfo = {
|
const pageInfo = {
|
||||||
|
|
@ -18,7 +18,7 @@ export default async function Dashboard({
|
||||||
const activePage = "/individual";
|
const activePage = "/individual";
|
||||||
const siteUrlsList = (await retrievePageList()) || [];
|
const siteUrlsList = (await retrievePageList()) || [];
|
||||||
const pageToDirect = await retrievePagebyUrl(activePage);
|
const pageToDirect = await retrievePagebyUrl(activePage);
|
||||||
const PageComponent = retrievePage(pageToDirect);
|
const PageComponent = retrievePageByUrlAndPageId(pageToDirect, activePage);
|
||||||
const searchParamsInstance = await searchParams;
|
const searchParamsInstance = await searchParams;
|
||||||
const lang = (searchParamsInstance?.lang as "en" | "tr") || "en";
|
const lang = (searchParamsInstance?.lang as "en" | "tr") || "en";
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
"use server";
|
"use server";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { retrievePageList, retrievePagebyUrl } from "@/apicalls/cookies/token";
|
import { retrievePageList, retrievePagebyUrl } from "@/apicalls/cookies/token";
|
||||||
import { retrievePage } from "@/components/NavigatePages";
|
import retrievePageByUrlAndPageId from "@/components/navigator/retriever";
|
||||||
import LeftMenu from "@/components/menu/leftMenu";
|
import ClientMenu from "@/components/menu/menu";
|
||||||
|
import Header from "@/components/header/Header";
|
||||||
|
|
||||||
export default async function Dashboard({
|
export default async function Dashboard({
|
||||||
searchParams,
|
searchParams,
|
||||||
|
|
@ -14,35 +15,20 @@ export default async function Dashboard({
|
||||||
const searchParamsInstance = await searchParams;
|
const searchParamsInstance = await searchParams;
|
||||||
const activePage = "/management/accounting";
|
const activePage = "/management/accounting";
|
||||||
const pageToDirect = await retrievePagebyUrl(activePage);
|
const pageToDirect = await retrievePagebyUrl(activePage);
|
||||||
const PageComponent = retrievePage(pageToDirect);
|
const PageComponent = retrievePageByUrlAndPageId(activePage, pageToDirect);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="min-h-screen min-w-screen flex h-screen w-screen overflow-hidden">
|
<div className="min-h-screen min-w-screen flex h-screen w-screen overflow-hidden">
|
||||||
{/* Sidebar */}
|
{/* Sidebar */}
|
||||||
<aside className="w-1/4 border-r p-4 overflow-y-auto">
|
<aside className="w-1/4 border-r p-4 overflow-y-auto">
|
||||||
<LeftMenu
|
<ClientMenu lang={lang} siteUrls={siteUrlsList} />
|
||||||
pageUuidList={siteUrlsList}
|
|
||||||
lang={lang}
|
|
||||||
searchParams={searchParamsInstance}
|
|
||||||
pageSelected={activePage}
|
|
||||||
/>
|
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
{/* Main Content Area */}
|
{/* Main Content Area */}
|
||||||
<div className="flex flex-col w-3/4">
|
<div className="flex flex-col w-3/4">
|
||||||
{/* Sticky Header */}
|
{/* Sticky Header */}
|
||||||
<header className="sticky top-0 bg-white shadow-md z-10 p-4 flex justify-between items-center">
|
<Header lang={lang} />
|
||||||
<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>
|
|
||||||
<PageComponent lang={lang} queryParams={searchParamsInstance} />
|
<PageComponent lang={lang} queryParams={searchParamsInstance} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
"use server";
|
"use server";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { retrievePageList, retrievePagebyUrl } from "@/apicalls/cookies/token";
|
import { retrievePageList, retrievePagebyUrl } from "@/apicalls/cookies/token";
|
||||||
import { retrievePage } from "@/components/NavigatePages";
|
import retrievePageByUrlAndPageId from "@/components/navigator/retriever";
|
||||||
import LeftMenu from "@/components/menu/leftMenu";
|
import ClientMenu from "@/components/menu/menu";
|
||||||
|
import Header from "@/components/header/Header";
|
||||||
|
|
||||||
export default async function Dashboard({
|
export default async function Dashboard({
|
||||||
searchParams,
|
searchParams,
|
||||||
|
|
@ -14,35 +15,20 @@ export default async function Dashboard({
|
||||||
const searchParamsInstance = await searchParams;
|
const searchParamsInstance = await searchParams;
|
||||||
const activePage = "/management/budget";
|
const activePage = "/management/budget";
|
||||||
const pageToDirect = await retrievePagebyUrl(activePage);
|
const pageToDirect = await retrievePagebyUrl(activePage);
|
||||||
const PageComponent = retrievePage(pageToDirect);
|
const PageComponent = retrievePageByUrlAndPageId(activePage, pageToDirect);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="min-h-screen min-w-screen flex h-screen w-screen overflow-hidden">
|
<div className="min-h-screen min-w-screen flex h-screen w-screen overflow-hidden">
|
||||||
{/* Sidebar */}
|
{/* Sidebar */}
|
||||||
<aside className="w-1/4 border-r p-4 overflow-y-auto">
|
<aside className="w-1/4 border-r p-4 overflow-y-auto">
|
||||||
<LeftMenu
|
<ClientMenu lang={lang} siteUrls={siteUrlsList} />
|
||||||
pageUuidList={siteUrlsList}
|
|
||||||
lang={lang}
|
|
||||||
searchParams={searchParamsInstance}
|
|
||||||
pageSelected={activePage}
|
|
||||||
/>
|
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
{/* Main Content Area */}
|
{/* Main Content Area */}
|
||||||
<div className="flex flex-col w-3/4">
|
<div className="flex flex-col w-3/4">
|
||||||
{/* Sticky Header */}
|
{/* Sticky Header */}
|
||||||
<header className="sticky top-0 bg-white shadow-md z-10 p-4 flex justify-between items-center">
|
<Header lang={lang} />
|
||||||
<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>
|
|
||||||
<PageComponent lang={lang} queryParams={searchParamsInstance} />
|
<PageComponent lang={lang} queryParams={searchParamsInstance} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,66 +0,0 @@
|
||||||
import React from "react";
|
|
||||||
import { PageProps } from "./interFaces";
|
|
||||||
|
|
||||||
import PeopleSuperUserApp from "../Pages/people/superusers/app";
|
|
||||||
|
|
||||||
// Ensure all components in PageIndex accept PageProps
|
|
||||||
type PageComponent = React.ComponentType<PageProps>;
|
|
||||||
|
|
||||||
export const PageIndex: Record<string, PageComponent> = {
|
|
||||||
app000003: PeopleSuperUserApp as unknown as PageComponent,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Language dictionary for internationalization
|
|
||||||
const languageDictionary = {
|
|
||||||
en: {
|
|
||||||
title: "Unauthorized Access",
|
|
||||||
message1: "You do not have permission to access this page.",
|
|
||||||
message2: "Please contact the administrator.",
|
|
||||||
footer: `© ${new Date().getFullYear()} My Application`
|
|
||||||
},
|
|
||||||
tr: {
|
|
||||||
title: "Yetkisiz Erişim",
|
|
||||||
message1: "Bu sayfaya erişim izniniz yok.",
|
|
||||||
message2: "Lütfen yönetici ile iletişime geçin.",
|
|
||||||
footer: `© ${new Date().getFullYear()} Uygulamam`
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const UnAuthorizedPage: React.FC<PageProps> = ({ lang = "en", queryParams }) => {
|
|
||||||
// Use the language dictionary based on the lang prop, defaulting to English
|
|
||||||
const t = languageDictionary[lang as keyof typeof languageDictionary] || languageDictionary.en;
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className="flex flex-col h-screen">
|
|
||||||
<header className="bg-gray-800 text-white p-4 text-center">
|
|
||||||
<h1 className="text-2xl font-bold">{t.title}</h1>
|
|
||||||
</header>
|
|
||||||
<main className="flex-grow p-4 bg-gray-100">
|
|
||||||
<p className="text-gray-700">
|
|
||||||
{t.message1}
|
|
||||||
</p>
|
|
||||||
<p className="text-gray-700">{t.message2}</p>
|
|
||||||
</main>
|
|
||||||
<footer className="bg-gray-800 text-white p-4 text-center">
|
|
||||||
<p>{t.footer}</p>
|
|
||||||
</footer>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export function retrievePage(pageId: string): React.ComponentType<PageProps> {
|
|
||||||
try {
|
|
||||||
const PageComponent = PageIndex[pageId as keyof typeof PageIndex];
|
|
||||||
if (!PageComponent) {
|
|
||||||
console.log(`Page component not found for pageId: ${pageId}`);
|
|
||||||
return UnAuthorizedPage;
|
|
||||||
}
|
|
||||||
return PageComponent;
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`Error retrieving page component for pageId: ${pageId}`, error);
|
|
||||||
return UnAuthorizedPage;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default retrievePage;
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
import { LanguageTranslation } from "@/components/menu/runner";
|
|
||||||
|
|
||||||
export interface PageProps {
|
|
||||||
lang: keyof LanguageTranslation;
|
|
||||||
queryParams: { [key: string]: string | undefined };
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
import { PageComponent } from "@/components/validations/translations/translation";
|
||||||
|
import DashboardSuperUserApp from "./superusers/app";
|
||||||
|
|
||||||
|
export const dashboardApplications: Record<string, PageComponent> = {
|
||||||
|
app000001: DashboardSuperUserApp,
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
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="bg-white p-4 rounded-lg shadow mb-4">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<label className="text-sm font-medium">{t.actions}</label>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
<button
|
||||||
|
onClick={onCreateClick}
|
||||||
|
className="px-4 py-2 bg-green-500 text-white rounded hover:bg-green-600"
|
||||||
|
>
|
||||||
|
{t.create}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,58 @@
|
||||||
|
import React from "react";
|
||||||
|
import { PeopleFormData } from "./schema";
|
||||||
|
import { getTranslation, LanguageKey } from "./language";
|
||||||
|
import { DataCard } from "./ListInfoComponent";
|
||||||
|
|
||||||
|
interface DataDisplayComponentProps {
|
||||||
|
data: PeopleFormData[];
|
||||||
|
loading: boolean;
|
||||||
|
error: Error | null;
|
||||||
|
onViewClick: (item: PeopleFormData) => void;
|
||||||
|
onUpdateClick: (item: PeopleFormData) => 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.uu_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,513 @@
|
||||||
|
"use client";
|
||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
import {
|
||||||
|
PeopleSchema,
|
||||||
|
PeopleFormData,
|
||||||
|
CreatePeopleSchema,
|
||||||
|
UpdatePeopleSchema,
|
||||||
|
ViewPeopleSchema,
|
||||||
|
fieldDefinitions,
|
||||||
|
fieldsByMode,
|
||||||
|
} from "./schema";
|
||||||
|
import { getTranslation, LanguageKey } from "./language";
|
||||||
|
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 { Button } from "@/components/ui/button";
|
||||||
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
interface FormComponentProps {
|
||||||
|
lang: LanguageKey;
|
||||||
|
mode: "create" | "update" | "view";
|
||||||
|
onCancel: () => void;
|
||||||
|
refetch: () => void;
|
||||||
|
setMode: React.Dispatch<
|
||||||
|
React.SetStateAction<"list" | "create" | "view" | "update">
|
||||||
|
>;
|
||||||
|
setSelectedItem: React.Dispatch<React.SetStateAction<PeopleFormData | null>>;
|
||||||
|
initialData?: Partial<PeopleFormData>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FormComponent({
|
||||||
|
lang,
|
||||||
|
mode,
|
||||||
|
onCancel,
|
||||||
|
refetch,
|
||||||
|
setMode,
|
||||||
|
setSelectedItem,
|
||||||
|
initialData,
|
||||||
|
}: FormComponentProps) {
|
||||||
|
// Derive readOnly from mode
|
||||||
|
const readOnly = mode === "view";
|
||||||
|
const t = getTranslation(lang);
|
||||||
|
const [formSubmitting, setFormSubmitting] = useState<boolean>(false);
|
||||||
|
|
||||||
|
// Select the appropriate schema based on the mode
|
||||||
|
const getSchemaForMode = () => {
|
||||||
|
switch (mode) {
|
||||||
|
case "create":
|
||||||
|
return CreatePeopleSchema;
|
||||||
|
case "update":
|
||||||
|
return UpdatePeopleSchema;
|
||||||
|
case "view":
|
||||||
|
return ViewPeopleSchema;
|
||||||
|
default:
|
||||||
|
return PeopleSchema;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get field definitions for the current mode
|
||||||
|
const modeFieldDefinitions = fieldDefinitions.getDefinitionsByMode(
|
||||||
|
mode
|
||||||
|
) as Record<string, any>;
|
||||||
|
|
||||||
|
// Define FormValues type based on the current mode to fix TypeScript errors
|
||||||
|
type FormValues = Record<string, any>;
|
||||||
|
|
||||||
|
// Get default values directly from the field definitions
|
||||||
|
const getDefaultValues = (): FormValues => {
|
||||||
|
// For view and update modes, use initialData if available
|
||||||
|
if ((mode === "view" || mode === "update") && initialData) {
|
||||||
|
return initialData as FormValues;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For create mode or when initialData is not available, use default values from schema
|
||||||
|
const defaults: FormValues = {};
|
||||||
|
|
||||||
|
Object.entries(modeFieldDefinitions).forEach(([key, field]) => {
|
||||||
|
if (field && typeof field === "object" && "defaultValue" in field) {
|
||||||
|
defaults[key] = field.defaultValue;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return defaults;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Define form with react-hook-form and zod validation
|
||||||
|
const form = useForm<FormValues>({
|
||||||
|
resolver: zodResolver(getSchemaForMode()) as any, // Type assertion to fix TypeScript errors
|
||||||
|
defaultValues: getDefaultValues(),
|
||||||
|
mode: "onChange",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update form values when initialData changes
|
||||||
|
useEffect(() => {
|
||||||
|
if (initialData && (mode === "update" || mode === "view")) {
|
||||||
|
// Reset the form with initialData
|
||||||
|
form.reset(initialData as FormValues);
|
||||||
|
}
|
||||||
|
}, [initialData, form, mode]);
|
||||||
|
|
||||||
|
// Define the submission handler function
|
||||||
|
const onSubmitHandler = async (data: FormValues) => {
|
||||||
|
setFormSubmitting(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Call different API methods based on the current mode
|
||||||
|
if (mode === "create") {
|
||||||
|
// Call create API
|
||||||
|
console.log("Creating new record:", data);
|
||||||
|
// Simulate API call
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||||
|
// In a real application, you would call your API here
|
||||||
|
// Example: await createPerson(data);
|
||||||
|
} else if (mode === "update") {
|
||||||
|
// Call update API
|
||||||
|
console.log("Updating existing record:", data);
|
||||||
|
// Simulate API call
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||||
|
// In a real application, you would call your API here
|
||||||
|
// Example: await updatePerson(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show success message or notification here
|
||||||
|
|
||||||
|
// Return to list view and reset selected item
|
||||||
|
handleReturnToList();
|
||||||
|
|
||||||
|
// Refresh data
|
||||||
|
refetch();
|
||||||
|
} catch (error) {
|
||||||
|
// Handle any errors from the API calls
|
||||||
|
console.error("Error saving data:", error);
|
||||||
|
// You could set an error state here to display to the user
|
||||||
|
} finally {
|
||||||
|
setFormSubmitting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper function to return to list view
|
||||||
|
const handleReturnToList = () => {
|
||||||
|
setMode("list");
|
||||||
|
setSelectedItem(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle cancel button click
|
||||||
|
const handleCancel = () => {
|
||||||
|
onCancel();
|
||||||
|
|
||||||
|
// Return to list view
|
||||||
|
handleReturnToList();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Filter fields based on the current mode
|
||||||
|
const activeFields = fieldsByMode[readOnly ? "view" : mode];
|
||||||
|
|
||||||
|
// Group fields by their section using mode-specific field definitions
|
||||||
|
const fieldGroups = activeFields.reduce(
|
||||||
|
(groups: Record<string, any[]>, fieldName: string) => {
|
||||||
|
const field = modeFieldDefinitions[fieldName];
|
||||||
|
if (field && typeof field === "object" && "group" in field) {
|
||||||
|
const group = field.group as string;
|
||||||
|
if (!groups[group]) {
|
||||||
|
groups[group] = [];
|
||||||
|
}
|
||||||
|
groups[group].push({
|
||||||
|
name: fieldName,
|
||||||
|
type: field.type as string,
|
||||||
|
readOnly: (field.readOnly as boolean) || readOnly, // Combine component readOnly with field readOnly
|
||||||
|
required: (field.required as boolean) || false,
|
||||||
|
label: (field.label as string) || fieldName,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return groups;
|
||||||
|
},
|
||||||
|
{} as Record<
|
||||||
|
string,
|
||||||
|
{
|
||||||
|
name: string;
|
||||||
|
type: string;
|
||||||
|
readOnly: boolean;
|
||||||
|
required: boolean;
|
||||||
|
label: string;
|
||||||
|
}[]
|
||||||
|
>
|
||||||
|
);
|
||||||
|
|
||||||
|
// Create helper variables for field group checks
|
||||||
|
const hasIdentificationFields = fieldGroups.identificationInfo?.length > 0;
|
||||||
|
const hasPersonalFields = fieldGroups.personalInfo?.length > 0;
|
||||||
|
const hasLocationFields = fieldGroups.locationInfo?.length > 0;
|
||||||
|
const hasExpiryFields = fieldGroups.expiryInfo?.length > 0;
|
||||||
|
const hasStatusFields = fieldGroups.statusInfo?.length > 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-white p-6 rounded-lg shadow">
|
||||||
|
{formSubmitting && (
|
||||||
|
<div className="fixed inset-0 bg-black bg-opacity-30 flex items-center justify-center z-50">
|
||||||
|
<div className="bg-white p-4 rounded-lg shadow-lg">
|
||||||
|
<div className="flex items-center space-x-3">
|
||||||
|
<svg
|
||||||
|
className="animate-spin h-5 w-5 text-blue-500"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<circle
|
||||||
|
className="opacity-25"
|
||||||
|
cx="12"
|
||||||
|
cy="12"
|
||||||
|
r="10"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="4"
|
||||||
|
></circle>
|
||||||
|
<path
|
||||||
|
className="opacity-75"
|
||||||
|
fill="currentColor"
|
||||||
|
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
<span>{mode === "create" ? t.creating : t.updating}...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<h2 className="text-xl font-bold mb-4">{t.title || "Person Details"}</h2>
|
||||||
|
<h2 className="text-lg font-semibold mb-4">
|
||||||
|
{readOnly ? t.view : initialData?.uu_id ? t.update : t.createNew}
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<Form {...form}>
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* Identification Information Section */}
|
||||||
|
{hasIdentificationFields && (
|
||||||
|
<div className="bg-gray-50 p-4 rounded-md">
|
||||||
|
<h3 className="text-lg font-medium mb-3">Identification</h3>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
{fieldGroups.identificationInfo.map((field) => (
|
||||||
|
<FormField
|
||||||
|
key={field.name}
|
||||||
|
control={form.control}
|
||||||
|
name={field.name as any}
|
||||||
|
render={({ field: formField }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>
|
||||||
|
{field.label}
|
||||||
|
{field.required && (
|
||||||
|
<span className="text-red-500 ml-1">*</span>
|
||||||
|
)}
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
{field.type === "checkbox" ? (
|
||||||
|
<Checkbox
|
||||||
|
checked={formField.value as boolean}
|
||||||
|
onCheckedChange={formField.onChange}
|
||||||
|
disabled={field.readOnly}
|
||||||
|
/>
|
||||||
|
) : field.type === "date" ? (
|
||||||
|
<Input
|
||||||
|
type="date"
|
||||||
|
{...formField}
|
||||||
|
value={formField.value || ""}
|
||||||
|
disabled={field.readOnly}
|
||||||
|
className={cn(
|
||||||
|
"w-full",
|
||||||
|
field.readOnly && "bg-gray-100"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Input
|
||||||
|
{...formField}
|
||||||
|
value={formField.value || ""}
|
||||||
|
disabled={field.readOnly}
|
||||||
|
className={cn(
|
||||||
|
"w-full",
|
||||||
|
field.readOnly && "bg-gray-100"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
{field.name
|
||||||
|
? (t as any)[`form.descriptions.${field.name}`] ||
|
||||||
|
""
|
||||||
|
: ""}
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Personal Information Section */}
|
||||||
|
{hasPersonalFields && (
|
||||||
|
<div className="bg-blue-50 p-4 rounded-md">
|
||||||
|
<h3 className="text-lg font-medium mb-3">Personal Information</h3>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
{fieldGroups.personalInfo.map((field) => (
|
||||||
|
<FormField
|
||||||
|
key={field.name}
|
||||||
|
control={form.control}
|
||||||
|
name={field.name as any}
|
||||||
|
render={({ field: formField }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>
|
||||||
|
{field.label}
|
||||||
|
{field.required && (
|
||||||
|
<span className="text-red-500 ml-1">*</span>
|
||||||
|
)}
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
{field.type === "checkbox" ? (
|
||||||
|
<Checkbox
|
||||||
|
checked={formField.value as boolean}
|
||||||
|
onCheckedChange={formField.onChange}
|
||||||
|
disabled={field.readOnly}
|
||||||
|
/>
|
||||||
|
) : field.type === "date" ? (
|
||||||
|
<Input
|
||||||
|
type="date"
|
||||||
|
{...formField}
|
||||||
|
value={formField.value || ""}
|
||||||
|
disabled={field.readOnly}
|
||||||
|
className={cn(
|
||||||
|
"w-full",
|
||||||
|
field.readOnly && "bg-gray-100"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Input
|
||||||
|
{...formField}
|
||||||
|
value={formField.value || ""}
|
||||||
|
disabled={field.readOnly}
|
||||||
|
className={cn(
|
||||||
|
"w-full",
|
||||||
|
field.readOnly && "bg-gray-100"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
{field.name
|
||||||
|
? (t as any)[`form.descriptions.${field.name}`] ||
|
||||||
|
""
|
||||||
|
: ""}
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Location Information Section */}
|
||||||
|
{hasLocationFields && (
|
||||||
|
<div className="bg-green-50 p-4 rounded-md">
|
||||||
|
<h3 className="text-lg font-medium mb-3">Location Information</h3>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
{fieldGroups.locationInfo.map((field) => (
|
||||||
|
<FormField
|
||||||
|
key={field.name}
|
||||||
|
control={form.control}
|
||||||
|
name={field.name as any}
|
||||||
|
render={({ field: formField }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>{field.label}</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
{...formField}
|
||||||
|
value={formField.value || ""}
|
||||||
|
disabled={true} // System fields are always read-only
|
||||||
|
className="w-full bg-gray-100"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
{field.name
|
||||||
|
? (t as any)[`form.descriptions.${field.name}`] ||
|
||||||
|
""
|
||||||
|
: ""}
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Expiry Information Section */}
|
||||||
|
{hasExpiryFields && (
|
||||||
|
<div className="bg-yellow-50 p-4 rounded-md">
|
||||||
|
<h3 className="text-lg font-medium mb-3">Expiry Information</h3>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
{fieldGroups.expiryInfo.map((field) => (
|
||||||
|
<FormField
|
||||||
|
key={field.name}
|
||||||
|
control={form.control}
|
||||||
|
name={field.name as any}
|
||||||
|
render={({ field: formField }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>
|
||||||
|
{field.label}
|
||||||
|
{field.required && (
|
||||||
|
<span className="text-red-500 ml-1">*</span>
|
||||||
|
)}
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
type="date"
|
||||||
|
{...formField}
|
||||||
|
value={formField.value || ""}
|
||||||
|
disabled={field.readOnly}
|
||||||
|
className={cn(
|
||||||
|
"w-full",
|
||||||
|
field.readOnly && "bg-gray-100"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
{field.name
|
||||||
|
? (t as any)[`form.descriptions.${field.name}`] ||
|
||||||
|
""
|
||||||
|
: ""}
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Status Information Section */}
|
||||||
|
{hasStatusFields && (
|
||||||
|
<div className="bg-purple-50 p-4 rounded-md">
|
||||||
|
<h3 className="text-lg font-medium mb-3">Status Information</h3>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
{fieldGroups.statusInfo.map((field) => (
|
||||||
|
<FormField
|
||||||
|
key={field.name}
|
||||||
|
control={form.control}
|
||||||
|
name={field.name as any}
|
||||||
|
render={({ field: formField }) => (
|
||||||
|
<FormItem className="flex flex-row items-start space-x-3 space-y-0 rounded-md border p-4">
|
||||||
|
<FormControl>
|
||||||
|
<Checkbox
|
||||||
|
checked={formField.value as boolean}
|
||||||
|
onCheckedChange={formField.onChange}
|
||||||
|
disabled={field.readOnly}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<div className="space-y-1 leading-none">
|
||||||
|
<FormLabel className="text-sm font-medium">
|
||||||
|
{field.label}
|
||||||
|
{field.required && (
|
||||||
|
<span className="text-red-500 ml-1">*</span>
|
||||||
|
)}
|
||||||
|
</FormLabel>
|
||||||
|
<FormDescription>
|
||||||
|
{field.name
|
||||||
|
? (t as any)[`form.descriptions.${field.name}`] ||
|
||||||
|
""
|
||||||
|
: ""}
|
||||||
|
</FormDescription>
|
||||||
|
</div>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex justify-end space-x-3 mt-6">
|
||||||
|
<Button type="button" variant="outline" onClick={handleCancel}>
|
||||||
|
{readOnly ? t.back : t.cancel}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{!readOnly && (
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="default"
|
||||||
|
disabled={formSubmitting || readOnly}
|
||||||
|
onClick={form.handleSubmit(onSubmitHandler)}
|
||||||
|
>
|
||||||
|
{mode === "update"
|
||||||
|
? t.update || "Update"
|
||||||
|
: t.createNew || "Create"}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,141 @@
|
||||||
|
import React from "react";
|
||||||
|
import { PeopleFormData } from "./schema";
|
||||||
|
import { getTranslation, LanguageKey } from "./language";
|
||||||
|
import { ActionButtonsComponent } from "./ActionButtonsComponent";
|
||||||
|
import { SortingComponent } from "./SortingComponent";
|
||||||
|
import { PaginationToolsComponent } from "./PaginationToolsComponent";
|
||||||
|
import { PagePagination } from "@/components/validations/list/paginations";
|
||||||
|
|
||||||
|
interface DataCardProps {
|
||||||
|
item: PeopleFormData;
|
||||||
|
onView: (item: PeopleFormData) => void;
|
||||||
|
onUpdate: (item: PeopleFormData) => 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.person_tag}</h3>
|
||||||
|
<p className="text-gray-600 mt-1">
|
||||||
|
{item.birth_date ? new Date(item.birth_date).toLocaleString() : ""}
|
||||||
|
</p>
|
||||||
|
<div className="mt-2 flex items-center">
|
||||||
|
<span
|
||||||
|
className={`px-2 py-1 rounded text-xs ${
|
||||||
|
item.active
|
||||||
|
? "bg-green-100 text-green-800"
|
||||||
|
: "bg-gray-100 text-gray-800"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{item.is_confirmed}
|
||||||
|
</span>
|
||||||
|
<span className="text-xs text-gray-500 ml-2">
|
||||||
|
{t.formLabels.createdAt}:{" "}
|
||||||
|
{item.created_at ? new Date(item.created_at).toLocaleString() : ""}
|
||||||
|
</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: PeopleFormData[];
|
||||||
|
pagination: PagePagination;
|
||||||
|
loading: boolean;
|
||||||
|
error: Error | null;
|
||||||
|
updatePagination: (updates: Partial<PagePagination>) => void;
|
||||||
|
onCreateClick: () => void;
|
||||||
|
onViewClick: (item: PeopleFormData) => void;
|
||||||
|
onUpdateClick: (item: PeopleFormData) => 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.uu_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,86 @@
|
||||||
|
import React from "react";
|
||||||
|
import { getTranslation, LanguageKey } from "./language";
|
||||||
|
import { PagePagination } from "@/components/validations/list/paginations";
|
||||||
|
|
||||||
|
interface PaginationToolsComponentProps {
|
||||||
|
pagination: PagePagination;
|
||||||
|
updatePagination: (updates: Partial<PagePagination>) => 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,143 @@
|
||||||
|
import React, { useState, useEffect } from "react";
|
||||||
|
import { PeopleSchema } 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(PeopleSchema.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,81 @@
|
||||||
|
import React from "react";
|
||||||
|
import { PeopleSchema } from "./schema";
|
||||||
|
import { getTranslation, LanguageKey } from "./language";
|
||||||
|
import { PagePagination } from "@/components/validations/list/paginations";
|
||||||
|
|
||||||
|
interface SortingComponentProps {
|
||||||
|
pagination: PagePagination;
|
||||||
|
updatePagination: (updates: Partial<PagePagination>) => 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(PeopleSchema.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,112 @@
|
||||||
|
"use client";
|
||||||
|
import React, { useState } from "react";
|
||||||
|
import { usePaginatedData } from "./hooks";
|
||||||
|
import { FormComponent } from "./FormComponent";
|
||||||
|
import { SearchComponent } from "./SearchComponent";
|
||||||
|
import { LanguageKey } from "./language";
|
||||||
|
import { ActionButtonsComponent } from "./ActionButtonsComponent";
|
||||||
|
import { SortingComponent } from "./SortingComponent";
|
||||||
|
import { PaginationToolsComponent } from "./PaginationToolsComponent";
|
||||||
|
import { DataDisplayComponent } from "./DataDisplayComponent";
|
||||||
|
import { PeopleFormData } from "./schema";
|
||||||
|
|
||||||
|
interface TemplateProps {
|
||||||
|
lang?: LanguageKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Main template component
|
||||||
|
function DashboardSuperUserApp({ lang = "en" }: TemplateProps) {
|
||||||
|
const { data, pagination, loading, error, updatePagination, refetch } =
|
||||||
|
usePaginatedData();
|
||||||
|
|
||||||
|
const [mode, setMode] = useState<"list" | "create" | "view" | "update">(
|
||||||
|
"list"
|
||||||
|
);
|
||||||
|
const [selectedItem, setSelectedItem] = useState<PeopleFormData | null>(null);
|
||||||
|
|
||||||
|
// These functions are used by the DataDisplayComponent to handle item actions
|
||||||
|
const handleViewClick = (item: PeopleFormData) => {
|
||||||
|
setSelectedItem(item);
|
||||||
|
setMode("view");
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUpdateClick = (item: PeopleFormData) => {
|
||||||
|
setSelectedItem(item);
|
||||||
|
setMode("update");
|
||||||
|
};
|
||||||
|
|
||||||
|
// Function to handle the create button click
|
||||||
|
const handleCreateClick = () => {
|
||||||
|
setSelectedItem(null);
|
||||||
|
setMode("create");
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container mx-auto px-4 py-6">
|
||||||
|
{/* 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 !== "list" && (
|
||||||
|
<div>
|
||||||
|
<FormComponent
|
||||||
|
initialData={selectedItem || undefined}
|
||||||
|
lang={lang}
|
||||||
|
mode={mode}
|
||||||
|
refetch={refetch}
|
||||||
|
setMode={setMode}
|
||||||
|
setSelectedItem={setSelectedItem}
|
||||||
|
onCancel={() => {}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DashboardSuperUserApp;
|
||||||
|
|
@ -0,0 +1,136 @@
|
||||||
|
import { useState, useEffect, useCallback } from "react";
|
||||||
|
import { PeopleFormData, PeopleSchema, fetchData } from "./schema";
|
||||||
|
import {
|
||||||
|
PagePagination,
|
||||||
|
RequestParams,
|
||||||
|
ResponseMetadata,
|
||||||
|
} from "@/components/validations/list/paginations";
|
||||||
|
|
||||||
|
// Custom hook for pagination and data fetching
|
||||||
|
export function usePaginatedData() {
|
||||||
|
const [data, setData] = useState<PeopleFormData[]>([]);
|
||||||
|
|
||||||
|
// 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: any) => {
|
||||||
|
try {
|
||||||
|
return PeopleSchema.parse(item);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Validation error for item:", item, err);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.filter(Boolean) as PeopleFormData[];
|
||||||
|
|
||||||
|
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>) => {
|
||||||
|
// Transform query parameters to use __ilike with %value% format
|
||||||
|
if (updates.query) {
|
||||||
|
const transformedQuery: Record<string, any> = {};
|
||||||
|
|
||||||
|
Object.entries(updates.query).forEach(([key, value]) => {
|
||||||
|
// Only transform string values that aren't already using a special operator
|
||||||
|
if (typeof value === 'string' && !key.includes('__')) {
|
||||||
|
transformedQuery[`${key}__ilike`] = `%${value}%`;
|
||||||
|
} else {
|
||||||
|
transformedQuery[key] = value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
updates.query = transformedQuery;
|
||||||
|
}
|
||||||
|
|
||||||
|
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: PagePagination = {
|
||||||
|
...requestParams,
|
||||||
|
...responseMetadata,
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
data,
|
||||||
|
pagination,
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
updatePagination,
|
||||||
|
setQuery,
|
||||||
|
refetch,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,140 @@
|
||||||
|
// 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",
|
||||||
|
creating: "Creating",
|
||||||
|
updating: "Updating",
|
||||||
|
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: "Search",
|
||||||
|
searchPlaceholder: "Enter search term...",
|
||||||
|
searchFields: "Search in fields",
|
||||||
|
activeSearch: "Active search",
|
||||||
|
clearSearch: "Clear",
|
||||||
|
formLabels: {
|
||||||
|
title: "Title",
|
||||||
|
description: "Description",
|
||||||
|
status: "Status",
|
||||||
|
createdAt: "Created",
|
||||||
|
uu_id: "ID",
|
||||||
|
created_at: "Created At",
|
||||||
|
updated_at: "Updated At",
|
||||||
|
person_tag: "Person Tag",
|
||||||
|
expiry_starts: "Expiry Starts",
|
||||||
|
expiry_ends: "Expiry Ends",
|
||||||
|
firstname: "First Name",
|
||||||
|
middle_name: "Middle Name",
|
||||||
|
surname: "Surname",
|
||||||
|
birth_date: "Birth Date",
|
||||||
|
birth_place: "Birth Place",
|
||||||
|
sex_code: "Sex Code",
|
||||||
|
country_code: "Country Code",
|
||||||
|
tax_no: "Tax Number",
|
||||||
|
active: "Active",
|
||||||
|
deleted: "Deleted",
|
||||||
|
is_confirmed: "Confirmed",
|
||||||
|
is_notification_send: "Notification Sent",
|
||||||
|
},
|
||||||
|
status: {
|
||||||
|
active: "Active",
|
||||||
|
inactive: "Inactive",
|
||||||
|
},
|
||||||
|
buttons: {
|
||||||
|
view: "View",
|
||||||
|
update: "Update",
|
||||||
|
create: "Create",
|
||||||
|
save: "Save",
|
||||||
|
},
|
||||||
|
actions: "Actions",
|
||||||
|
},
|
||||||
|
tr: {
|
||||||
|
title: "Veri Yönetimi",
|
||||||
|
create: "Yeni Oluştur",
|
||||||
|
view: "Görüntüle",
|
||||||
|
update: "Güncelle",
|
||||||
|
createNew: "Yeni Oluştur",
|
||||||
|
back: "Geri",
|
||||||
|
cancel: "İptal",
|
||||||
|
submit: "Gönder",
|
||||||
|
creating: "Oluşturuluyor",
|
||||||
|
updating: "Güncelleniyor",
|
||||||
|
noItemsFound: "Hiçbir kayıt bulunamadı",
|
||||||
|
previous: "Önceki",
|
||||||
|
next: "Sonraki",
|
||||||
|
page: "Sayfa",
|
||||||
|
of: "of",
|
||||||
|
itemsPerPage: "Sayfa başına kayıt:",
|
||||||
|
sortBy: "Sırala:",
|
||||||
|
loading: "Yükleniyor...",
|
||||||
|
error: "Veri yüklenirken hata:",
|
||||||
|
showing: "Gösteriliyor",
|
||||||
|
items: "kayıtlar",
|
||||||
|
total: "Toplam",
|
||||||
|
search: "Ara",
|
||||||
|
searchPlaceholder: "Ara...",
|
||||||
|
searchFields: "Ara alanları",
|
||||||
|
activeSearch: "Aktif arama",
|
||||||
|
clearSearch: "Temizle",
|
||||||
|
formLabels: {
|
||||||
|
title: "Başlık",
|
||||||
|
description: "Açıklama",
|
||||||
|
status: "Durum",
|
||||||
|
createdAt: "Oluşturulma",
|
||||||
|
uu_id: "Kimlik",
|
||||||
|
created_at: "Oluşturulma Tarihi",
|
||||||
|
updated_at: "Güncelleme Tarihi",
|
||||||
|
person_tag: "Kişi Etiketi",
|
||||||
|
expiry_starts: "Geçerlilik Başlangıcı",
|
||||||
|
expiry_ends: "Geçerlilik Bitişi",
|
||||||
|
firstname: "Ad",
|
||||||
|
middle_name: "İkinci Ad",
|
||||||
|
surname: "Soyad",
|
||||||
|
birth_date: "Doğum Tarihi",
|
||||||
|
birth_place: "Doğum Yeri",
|
||||||
|
sex_code: "Cinsiyet Kodu",
|
||||||
|
country_code: "Ülke Kodu",
|
||||||
|
tax_no: "Vergi Numarası",
|
||||||
|
active: "Aktif",
|
||||||
|
deleted: "Silinmiş",
|
||||||
|
is_confirmed: "Onaylanmış",
|
||||||
|
is_notification_send: "Bildirim Gönderildi",
|
||||||
|
},
|
||||||
|
status: {
|
||||||
|
active: "Aktif",
|
||||||
|
inactive: "Pasif",
|
||||||
|
},
|
||||||
|
buttons: {
|
||||||
|
view: "Görüntüle",
|
||||||
|
update: "Güncelle",
|
||||||
|
create: "Oluştur",
|
||||||
|
save: "Kaydet",
|
||||||
|
},
|
||||||
|
actions: "Eylemler",
|
||||||
|
},
|
||||||
|
// 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,219 @@
|
||||||
|
import { z } from "zod";
|
||||||
|
import { PagePagination } from "@/components/validations/list/paginations";
|
||||||
|
import { peopleList } from "@/apicalls/people/people";
|
||||||
|
|
||||||
|
// Base schema with all possible fields
|
||||||
|
const PeopleBaseSchema = z.object({
|
||||||
|
// Identification fields
|
||||||
|
uu_id: z.string().optional(),
|
||||||
|
person_tag: z.string().min(1, "Person tag is required"),
|
||||||
|
|
||||||
|
// Personal information fields
|
||||||
|
firstname: z.string().min(1, "First name is required"),
|
||||||
|
middle_name: z.string().optional(),
|
||||||
|
surname: z.string().min(1, "Surname is required"),
|
||||||
|
birth_date: z.string().optional(),
|
||||||
|
birth_place: z.string().optional(),
|
||||||
|
sex_code: z.string().optional(),
|
||||||
|
|
||||||
|
// Location fields
|
||||||
|
country_code: z.string().optional(),
|
||||||
|
tax_no: z.string().optional(),
|
||||||
|
|
||||||
|
// Status fields
|
||||||
|
active: z.boolean().default(true),
|
||||||
|
is_confirmed: z.boolean().default(false),
|
||||||
|
is_notification_send: z.boolean().default(false),
|
||||||
|
deleted: z.boolean().default(false),
|
||||||
|
|
||||||
|
// System fields
|
||||||
|
created_at: z.string().optional(),
|
||||||
|
updated_at: z.string().optional(),
|
||||||
|
|
||||||
|
// Expiry fields
|
||||||
|
expiry_starts: z.string().optional(),
|
||||||
|
expiry_ends: z.string().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Schema for creating a new person
|
||||||
|
export const CreatePeopleSchema = PeopleBaseSchema.omit({
|
||||||
|
uu_id: true,
|
||||||
|
created_at: true,
|
||||||
|
updated_at: true,
|
||||||
|
is_notification_send: true,
|
||||||
|
deleted: true
|
||||||
|
});
|
||||||
|
|
||||||
|
// Schema for updating an existing person
|
||||||
|
export const UpdatePeopleSchema = PeopleBaseSchema.omit({
|
||||||
|
created_at: true,
|
||||||
|
updated_at: true,
|
||||||
|
is_notification_send: true,
|
||||||
|
deleted: true
|
||||||
|
}).required({
|
||||||
|
uu_id: true
|
||||||
|
});
|
||||||
|
|
||||||
|
// Schema for viewing a person (all fields)
|
||||||
|
export const ViewPeopleSchema = PeopleBaseSchema;
|
||||||
|
|
||||||
|
// Default schema (used for validation)
|
||||||
|
export const PeopleSchema = PeopleBaseSchema;
|
||||||
|
|
||||||
|
export type PeopleFormData = z.infer<typeof PeopleSchema>;
|
||||||
|
export type CreatePeopleFormData = z.infer<typeof CreatePeopleSchema>;
|
||||||
|
export type UpdatePeopleFormData = z.infer<typeof UpdatePeopleSchema>;
|
||||||
|
export type ViewPeopleFormData = z.infer<typeof ViewPeopleSchema>;
|
||||||
|
|
||||||
|
// Base field definitions with common properties
|
||||||
|
const baseFieldDefinitions = {
|
||||||
|
// Identification fields
|
||||||
|
uu_id: { type: "text", group: "identificationInfo", label: "UUID" },
|
||||||
|
person_tag: { type: "text", group: "identificationInfo", label: "Person Tag" },
|
||||||
|
|
||||||
|
// Personal information fields
|
||||||
|
firstname: { type: "text", group: "personalInfo", label: "First Name" },
|
||||||
|
middle_name: { type: "text", group: "personalInfo", label: "Middle Name" },
|
||||||
|
surname: { type: "text", group: "personalInfo", label: "Surname" },
|
||||||
|
birth_date: { type: "date", group: "personalInfo", label: "Birth Date" },
|
||||||
|
birth_place: { type: "text", group: "personalInfo", label: "Birth Place" },
|
||||||
|
sex_code: { type: "text", group: "personalInfo", label: "Sex Code" },
|
||||||
|
|
||||||
|
// Location fields
|
||||||
|
country_code: { type: "text", group: "locationInfo", label: "Country Code" },
|
||||||
|
tax_no: { type: "text", group: "locationInfo", label: "Tax Number" },
|
||||||
|
|
||||||
|
// Status fields
|
||||||
|
active: { type: "checkbox", group: "statusInfo", label: "Active" },
|
||||||
|
is_confirmed: { type: "checkbox", group: "statusInfo", label: "Confirmed" },
|
||||||
|
is_notification_send: { type: "checkbox", group: "statusInfo", label: "Notification Sent" },
|
||||||
|
deleted: { type: "checkbox", group: "statusInfo", label: "Deleted" },
|
||||||
|
|
||||||
|
// System fields
|
||||||
|
created_at: { type: "date", group: "systemInfo", label: "Created At" },
|
||||||
|
updated_at: { type: "date", group: "systemInfo", label: "Updated At" },
|
||||||
|
|
||||||
|
// Expiry fields
|
||||||
|
expiry_starts: { type: "date", group: "expiryInfo", label: "Expiry Start Date" },
|
||||||
|
expiry_ends: { type: "date", group: "expiryInfo", label: "Expiry End Date" },
|
||||||
|
};
|
||||||
|
|
||||||
|
// Field definitions for create mode
|
||||||
|
export const createFieldDefinitions = {
|
||||||
|
person_tag: { ...baseFieldDefinitions.person_tag, readOnly: false, required: true, defaultValue: "" },
|
||||||
|
firstname: { ...baseFieldDefinitions.firstname, readOnly: false, required: true, defaultValue: "" },
|
||||||
|
middle_name: { ...baseFieldDefinitions.middle_name, readOnly: false, required: false, defaultValue: "" },
|
||||||
|
surname: { ...baseFieldDefinitions.surname, readOnly: false, required: true, defaultValue: "" },
|
||||||
|
birth_date: { ...baseFieldDefinitions.birth_date, readOnly: false, required: false, defaultValue: "" },
|
||||||
|
birth_place: { ...baseFieldDefinitions.birth_place, readOnly: false, required: false, defaultValue: "" },
|
||||||
|
sex_code: { ...baseFieldDefinitions.sex_code, readOnly: false, required: false, defaultValue: "" },
|
||||||
|
country_code: { ...baseFieldDefinitions.country_code, readOnly: false, required: false, defaultValue: "" },
|
||||||
|
tax_no: { ...baseFieldDefinitions.tax_no, readOnly: false, required: false, defaultValue: "" },
|
||||||
|
is_confirmed: { ...baseFieldDefinitions.is_confirmed, readOnly: false, required: false, defaultValue: false },
|
||||||
|
};
|
||||||
|
|
||||||
|
// Field definitions for update mode
|
||||||
|
export const updateFieldDefinitions = {
|
||||||
|
person_tag: { ...baseFieldDefinitions.person_tag, readOnly: false, required: true, defaultValue: "" },
|
||||||
|
firstname: { ...baseFieldDefinitions.firstname, readOnly: false, required: true, defaultValue: "" },
|
||||||
|
middle_name: { ...baseFieldDefinitions.middle_name, readOnly: false, required: false, defaultValue: "" },
|
||||||
|
surname: { ...baseFieldDefinitions.surname, readOnly: false, required: true, defaultValue: "" },
|
||||||
|
birth_date: { ...baseFieldDefinitions.birth_date, readOnly: false, required: false, defaultValue: "" },
|
||||||
|
birth_place: { ...baseFieldDefinitions.birth_place, readOnly: false, required: false, defaultValue: "" },
|
||||||
|
sex_code: { ...baseFieldDefinitions.sex_code, readOnly: false, required: false, defaultValue: "" },
|
||||||
|
country_code: { ...baseFieldDefinitions.country_code, readOnly: false, required: false, defaultValue: "" },
|
||||||
|
tax_no: { ...baseFieldDefinitions.tax_no, readOnly: false, required: false, defaultValue: "" },
|
||||||
|
active: { ...baseFieldDefinitions.active, readOnly: false, required: false, defaultValue: false },
|
||||||
|
is_confirmed: { ...baseFieldDefinitions.is_confirmed, readOnly: false, required: false, defaultValue: false },
|
||||||
|
};
|
||||||
|
|
||||||
|
// Field definitions for view mode
|
||||||
|
export const viewFieldDefinitions = {
|
||||||
|
uu_id: { ...baseFieldDefinitions.uu_id, readOnly: true, required: false, defaultValue: "" },
|
||||||
|
person_tag: { ...baseFieldDefinitions.person_tag, readOnly: true, required: false, defaultValue: "" },
|
||||||
|
firstname: { ...baseFieldDefinitions.firstname, readOnly: true, required: false, defaultValue: "" },
|
||||||
|
middle_name: { ...baseFieldDefinitions.middle_name, readOnly: true, required: false, defaultValue: "" },
|
||||||
|
surname: { ...baseFieldDefinitions.surname, readOnly: true, required: false, defaultValue: "" },
|
||||||
|
birth_date: { ...baseFieldDefinitions.birth_date, readOnly: true, required: false, defaultValue: "" },
|
||||||
|
birth_place: { ...baseFieldDefinitions.birth_place, readOnly: true, required: false, defaultValue: "" },
|
||||||
|
sex_code: { ...baseFieldDefinitions.sex_code, readOnly: true, required: false, defaultValue: "" },
|
||||||
|
country_code: { ...baseFieldDefinitions.country_code, readOnly: true, required: false, defaultValue: "" },
|
||||||
|
tax_no: { ...baseFieldDefinitions.tax_no, readOnly: true, required: false, defaultValue: "" },
|
||||||
|
active: { ...baseFieldDefinitions.active, readOnly: true, required: false, defaultValue: false },
|
||||||
|
is_confirmed: { ...baseFieldDefinitions.is_confirmed, readOnly: true, required: false, defaultValue: false },
|
||||||
|
is_notification_send: { ...baseFieldDefinitions.is_notification_send, readOnly: true, required: false, defaultValue: false },
|
||||||
|
deleted: { ...baseFieldDefinitions.deleted, readOnly: true, required: false, defaultValue: false },
|
||||||
|
created_at: { ...baseFieldDefinitions.created_at, readOnly: true, required: false, defaultValue: "" },
|
||||||
|
updated_at: { ...baseFieldDefinitions.updated_at, readOnly: true, required: false, defaultValue: "" },
|
||||||
|
expiry_starts: { ...baseFieldDefinitions.expiry_starts, readOnly: true, required: false, defaultValue: "" },
|
||||||
|
expiry_ends: { ...baseFieldDefinitions.expiry_ends, readOnly: true, required: false, defaultValue: "" },
|
||||||
|
};
|
||||||
|
|
||||||
|
// Combined field definitions for all modes
|
||||||
|
export const fieldDefinitions = {
|
||||||
|
...baseFieldDefinitions,
|
||||||
|
getDefinitionsByMode: (mode: "create" | "update" | "view") => {
|
||||||
|
switch (mode) {
|
||||||
|
case "create":
|
||||||
|
return createFieldDefinitions;
|
||||||
|
case "update":
|
||||||
|
return updateFieldDefinitions;
|
||||||
|
case "view":
|
||||||
|
return viewFieldDefinitions;
|
||||||
|
default:
|
||||||
|
return baseFieldDefinitions;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Fields to show based on mode - dynamically generated from field definitions
|
||||||
|
export const fieldsByMode = {
|
||||||
|
create: Object.keys(createFieldDefinitions),
|
||||||
|
update: Object.keys(updateFieldDefinitions),
|
||||||
|
view: Object.keys(viewFieldDefinitions),
|
||||||
|
};
|
||||||
|
|
||||||
|
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>;
|
||||||
|
}) => {
|
||||||
|
// Call the actual API function
|
||||||
|
try {
|
||||||
|
const response = await peopleList({
|
||||||
|
page,
|
||||||
|
size,
|
||||||
|
orderField: orderFields,
|
||||||
|
orderType: orderTypes,
|
||||||
|
query,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: response.data,
|
||||||
|
pagination: response.pagination,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching data:", error);
|
||||||
|
return {
|
||||||
|
data: [],
|
||||||
|
pagination: {
|
||||||
|
page,
|
||||||
|
size,
|
||||||
|
totalCount: 0,
|
||||||
|
allCount: 0,
|
||||||
|
totalPages: 0,
|
||||||
|
orderFields,
|
||||||
|
orderTypes,
|
||||||
|
pageCount: 0,
|
||||||
|
} as PagePagination,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
import { PageComponent } from "@/components/validations/translations/translation";
|
||||||
|
import { peopleApplications } from "./people";
|
||||||
|
import { dashboardApplications } from "./dashboard";
|
||||||
|
|
||||||
|
export const PageNavigator: Record<string, Record<string, PageComponent>> = {
|
||||||
|
"/individual": peopleApplications,
|
||||||
|
"/dashboard": dashboardApplications,
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
import { PageComponent } from "@/components/validations/translations/translation";
|
||||||
|
import PeopleSuperUserApp from "./superusers/app";
|
||||||
|
|
||||||
|
export const peopleApplications: Record<string, PageComponent> = {
|
||||||
|
app000003: PeopleSuperUserApp,
|
||||||
|
};
|
||||||
|
|
@ -7,6 +7,10 @@ import EmployeeProfileSection from "./EmployeeProfileSection";
|
||||||
import OccupantProfileSection from "./OccupantProfileSection";
|
import OccupantProfileSection from "./OccupantProfileSection";
|
||||||
import ProfileLoadingState from "./ProfileLoadingState";
|
import ProfileLoadingState from "./ProfileLoadingState";
|
||||||
import NavigationMenu from "./NavigationMenu";
|
import NavigationMenu from "./NavigationMenu";
|
||||||
|
import {
|
||||||
|
ClientMenuProps,
|
||||||
|
UserSelection,
|
||||||
|
} from "@/components/validations/menu/menu";
|
||||||
|
|
||||||
// Language definitions for dashboard title
|
// Language definitions for dashboard title
|
||||||
const dashboardLanguage = {
|
const dashboardLanguage = {
|
||||||
|
|
@ -20,16 +24,6 @@ const dashboardLanguage = {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
interface ClientMenuProps {
|
|
||||||
siteUrls: string[];
|
|
||||||
lang?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface UserSelection {
|
|
||||||
userType: string;
|
|
||||||
selected: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ClientMenu: React.FC<ClientMenuProps> = ({ siteUrls, lang = "en" }) => {
|
const ClientMenu: React.FC<ClientMenuProps> = ({ siteUrls, lang = "en" }) => {
|
||||||
const transformedMenu = transformMenu(siteUrls);
|
const transformedMenu = transformMenu(siteUrls);
|
||||||
const t =
|
const t =
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
import { PageComponent } from "@/components/validations/translations/translation";
|
||||||
|
import { UnAuthorizedPage } from "@/components/navigator/unauthorizedpage";
|
||||||
|
import { PageNavigator } from "../Pages";
|
||||||
|
|
||||||
|
export function retrievePageByUrlAndPageId(
|
||||||
|
pageId: string,
|
||||||
|
pageUrl: string
|
||||||
|
): PageComponent | typeof UnAuthorizedPage {
|
||||||
|
try {
|
||||||
|
const PageComponent =
|
||||||
|
PageNavigator[pageUrl as keyof typeof PageNavigator][
|
||||||
|
pageId as keyof (typeof PageNavigator)[typeof pageUrl]
|
||||||
|
];
|
||||||
|
if (!PageComponent) {
|
||||||
|
console.log(`Page component not found for pageId: ${pageId}`);
|
||||||
|
return UnAuthorizedPage;
|
||||||
|
}
|
||||||
|
return PageComponent;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(
|
||||||
|
`Error retrieving page component for pageId: ${pageId}`,
|
||||||
|
error
|
||||||
|
);
|
||||||
|
return UnAuthorizedPage;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default retrievePageByUrlAndPageId;
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
import { PageProps } from "../validations/translations/translation";
|
||||||
|
|
||||||
|
// Language dictionary for internationalization
|
||||||
|
const languageDictionary = {
|
||||||
|
en: {
|
||||||
|
title: "Unauthorized Access",
|
||||||
|
message1: "You do not have permission to access this page.",
|
||||||
|
message2: "Please contact the administrator.",
|
||||||
|
footer: `© ${new Date().getFullYear()} My Application`,
|
||||||
|
},
|
||||||
|
tr: {
|
||||||
|
title: "Yetkisiz Erişim",
|
||||||
|
message1: "Bu sayfaya erişim izniniz yok.",
|
||||||
|
message2: "Lütfen yönetici ile iletişime geçin.",
|
||||||
|
footer: `© ${new Date().getFullYear()} Uygulamam`,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const UnAuthorizedPage: React.FC<PageProps> = ({
|
||||||
|
lang = "en",
|
||||||
|
queryParams,
|
||||||
|
}) => {
|
||||||
|
// Use the language dictionary based on the lang prop, defaulting to English
|
||||||
|
const t =
|
||||||
|
languageDictionary[lang as keyof typeof languageDictionary] ||
|
||||||
|
languageDictionary.en;
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="flex flex-col h-screen">
|
||||||
|
<header className="bg-gray-800 text-white p-4 text-center">
|
||||||
|
<h1 className="text-2xl font-bold">{t.title}</h1>
|
||||||
|
</header>
|
||||||
|
<main className="flex-grow p-4 bg-gray-100">
|
||||||
|
<p className="text-gray-700">{t.message1}</p>
|
||||||
|
<p className="text-gray-700">{t.message2}</p>
|
||||||
|
</main>
|
||||||
|
<footer className="bg-gray-800 text-white p-4 text-center">
|
||||||
|
<p>{t.footer}</p>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
interface ClientMenuProps {
|
||||||
|
siteUrls: string[];
|
||||||
|
lang?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UserSelection {
|
||||||
|
userType: string;
|
||||||
|
selected: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type { ClientMenuProps, UserSelection };
|
||||||
|
|
@ -0,0 +1,61 @@
|
||||||
|
// Define TypeScript interfaces for menu structure
|
||||||
|
interface LanguageTranslation {
|
||||||
|
tr: string;
|
||||||
|
en: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MenuThirdLevel {
|
||||||
|
name: string;
|
||||||
|
lg: LanguageTranslation;
|
||||||
|
siteUrl: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MenuSecondLevel {
|
||||||
|
name: string;
|
||||||
|
lg: LanguageTranslation;
|
||||||
|
subList: MenuThirdLevel[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MenuFirstLevel {
|
||||||
|
name: string;
|
||||||
|
lg: LanguageTranslation;
|
||||||
|
subList: MenuSecondLevel[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Define interfaces for the filtered menu structure
|
||||||
|
interface FilteredMenuThirdLevel {
|
||||||
|
name: string;
|
||||||
|
lg: LanguageTranslation;
|
||||||
|
siteUrl: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FilteredMenuSecondLevel {
|
||||||
|
name: string;
|
||||||
|
lg: LanguageTranslation;
|
||||||
|
subList: FilteredMenuThirdLevel[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FilteredMenuFirstLevel {
|
||||||
|
name: string;
|
||||||
|
lg: LanguageTranslation;
|
||||||
|
subList: FilteredMenuSecondLevel[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PageProps {
|
||||||
|
lang: keyof LanguageTranslation;
|
||||||
|
queryParams: { [key: string]: string | undefined };
|
||||||
|
}
|
||||||
|
|
||||||
|
type PageComponent = React.ComponentType<PageProps>;
|
||||||
|
|
||||||
|
export type {
|
||||||
|
PageComponent,
|
||||||
|
PageProps,
|
||||||
|
MenuFirstLevel,
|
||||||
|
MenuSecondLevel,
|
||||||
|
MenuThirdLevel,
|
||||||
|
FilteredMenuFirstLevel,
|
||||||
|
FilteredMenuSecondLevel,
|
||||||
|
FilteredMenuThirdLevel,
|
||||||
|
LanguageTranslation,
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
import { PageComponent } from "@/components/validations/translations/translation";
|
||||||
|
import PeopleSuperUserApp from "@/components/Pages/people/superusers/app";
|
||||||
|
|
||||||
|
export const PageNavigator: Record<string, Record<string, PageComponent>> = {
|
||||||
|
"/individual": {
|
||||||
|
app000003: PeopleSuperUserApp,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
@ -5,51 +5,15 @@
|
||||||
* @returns {Array} - Filtered menu structure with only matching items
|
* @returns {Array} - Filtered menu structure with only matching items
|
||||||
*/
|
*/
|
||||||
import Menu from "@/components/menu/store"; // Assuming you have a menu structure imported
|
import Menu from "@/components/menu/store"; // Assuming you have a menu structure imported
|
||||||
|
import {
|
||||||
|
MenuFirstLevel,
|
||||||
|
MenuSecondLevel,
|
||||||
|
MenuThirdLevel,
|
||||||
|
FilteredMenuFirstLevel,
|
||||||
|
FilteredMenuSecondLevel,
|
||||||
|
FilteredMenuThirdLevel,
|
||||||
|
} from "@/components/validations/translations/tr";
|
||||||
|
|
||||||
// Define TypeScript interfaces for menu structure
|
|
||||||
interface LanguageTranslation {
|
|
||||||
tr: string;
|
|
||||||
en: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface MenuThirdLevel {
|
|
||||||
name: string;
|
|
||||||
lg: LanguageTranslation;
|
|
||||||
siteUrl: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface MenuSecondLevel {
|
|
||||||
name: string;
|
|
||||||
lg: LanguageTranslation;
|
|
||||||
subList: MenuThirdLevel[];
|
|
||||||
}
|
|
||||||
|
|
||||||
interface MenuFirstLevel {
|
|
||||||
name: string;
|
|
||||||
lg: LanguageTranslation;
|
|
||||||
subList: MenuSecondLevel[];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Define interfaces for the filtered menu structure
|
|
||||||
interface FilteredMenuThirdLevel {
|
|
||||||
name: string;
|
|
||||||
lg: LanguageTranslation;
|
|
||||||
siteUrl: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface FilteredMenuSecondLevel {
|
|
||||||
name: string;
|
|
||||||
lg: LanguageTranslation;
|
|
||||||
subList: FilteredMenuThirdLevel[];
|
|
||||||
}
|
|
||||||
|
|
||||||
interface FilteredMenuFirstLevel {
|
|
||||||
name: string;
|
|
||||||
lg: LanguageTranslation;
|
|
||||||
subList: FilteredMenuSecondLevel[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export type { LanguageTranslation };
|
|
||||||
|
|
||||||
function transformMenu(siteUrls: string[]) {
|
function transformMenu(siteUrls: string[]) {
|
||||||
// Process the menu structure
|
// Process the menu structure
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,6 @@ services:
|
||||||
dockerfile: WebServices/client-frontend/Dockerfile
|
dockerfile: WebServices/client-frontend/Dockerfile
|
||||||
networks:
|
networks:
|
||||||
- wag-services
|
- wag-services
|
||||||
depends_on:
|
|
||||||
- postgres-service
|
|
||||||
ports:
|
ports:
|
||||||
- "3000:3000"
|
- "3000:3000"
|
||||||
environment:
|
environment:
|
||||||
|
|
@ -15,34 +13,32 @@ services:
|
||||||
# volumes:
|
# volumes:
|
||||||
# - client-frontend:/WebServices/client-frontend
|
# - client-frontend:/WebServices/client-frontend
|
||||||
|
|
||||||
# identity_service:
|
identity_service:
|
||||||
# container_name: identity_service
|
container_name: identity_service
|
||||||
# build:
|
build:
|
||||||
# context: .
|
context: .
|
||||||
# dockerfile: ApiServices/IdentityService/Dockerfile
|
dockerfile: ApiServices/IdentityService/Dockerfile
|
||||||
# networks:
|
networks:
|
||||||
# - wag-services
|
- wag-services
|
||||||
# env_file:
|
depends_on:
|
||||||
# - api_env.env
|
- initializer_service
|
||||||
# environment:
|
env_file:
|
||||||
# - API_PATH=app:app
|
- api_env.env
|
||||||
# - API_HOST=0.0.0.0
|
environment:
|
||||||
# - API_PORT=8002
|
- API_PATH=app:app
|
||||||
# - API_LOG_LEVEL=info
|
- API_HOST=0.0.0.0
|
||||||
# - API_RELOAD=1
|
- API_PORT=8002
|
||||||
# - API_APP_NAME=evyos-identity-api-gateway
|
- API_LOG_LEVEL=info
|
||||||
# - API_TITLE=WAG API Identity Api Gateway
|
- API_RELOAD=1
|
||||||
# - API_FORGOT_LINK=https://identity_service/forgot-password
|
- API_APP_NAME=evyos-identity-api-gateway
|
||||||
# - API_DESCRIPTION=This api is serves as web identity api gateway only to evyos web services.
|
- API_TITLE=WAG API Identity Api Gateway
|
||||||
# - API_APP_URL=https://identity_service
|
- API_FORGOT_LINK=https://identity_service/forgot-password
|
||||||
# ports:
|
- API_DESCRIPTION=This api is serves as web identity api gateway only to evyos web services.
|
||||||
# - "8002:8002"
|
- API_APP_URL=https://identity_service
|
||||||
# depends_on:
|
ports:
|
||||||
# - postgres-service
|
- "8002:8002"
|
||||||
# - mongo_service
|
mem_limit: 4096m
|
||||||
# - redis_service
|
cpus: 3
|
||||||
# mem_limit: 512M
|
|
||||||
# cpus: 0.5
|
|
||||||
|
|
||||||
auth_service:
|
auth_service:
|
||||||
container_name: auth_service
|
container_name: auth_service
|
||||||
|
|
@ -53,6 +49,8 @@ services:
|
||||||
- wag-services
|
- wag-services
|
||||||
env_file:
|
env_file:
|
||||||
- api_env.env
|
- api_env.env
|
||||||
|
depends_on:
|
||||||
|
- initializer_service
|
||||||
environment:
|
environment:
|
||||||
- API_PATH=app:app
|
- API_PATH=app:app
|
||||||
- API_HOST=0.0.0.0
|
- API_HOST=0.0.0.0
|
||||||
|
|
@ -66,40 +64,18 @@ services:
|
||||||
- API_APP_URL=https://auth_service
|
- API_APP_URL=https://auth_service
|
||||||
ports:
|
ports:
|
||||||
- "8001:8001"
|
- "8001:8001"
|
||||||
depends_on:
|
|
||||||
- postgres-service
|
|
||||||
- mongo_service
|
|
||||||
- redis_service
|
|
||||||
mem_limit: 512M
|
|
||||||
cpus: 0.5
|
|
||||||
|
|
||||||
# management_frontend:
|
initializer_service:
|
||||||
# container_name: management_frontend
|
container_name: initializer_service
|
||||||
# build:
|
build:
|
||||||
# context: .
|
context: .
|
||||||
# dockerfile: WebServices/management-frontend/Dockerfile
|
dockerfile: ApiServices/InitialService/Dockerfile
|
||||||
# networks:
|
environment:
|
||||||
# - wag-services
|
- set_alembic=0
|
||||||
# ports:
|
networks:
|
||||||
# - "3001:3000" # Using different host port to avoid conflicts
|
- wag-services
|
||||||
# # volumes:
|
env_file:
|
||||||
# # - management-frontend:/WebServices/management-frontend
|
- api_env.env
|
||||||
# environment:
|
|
||||||
# - NODE_ENV=development
|
|
||||||
|
|
||||||
# initializer_service:
|
|
||||||
# container_name: initializer_service
|
|
||||||
# build:
|
|
||||||
# context: .
|
|
||||||
# dockerfile: ApiServices/InitialService/Dockerfile
|
|
||||||
# networks:
|
|
||||||
# - wag-services
|
|
||||||
# env_file:
|
|
||||||
# - api_env.env
|
|
||||||
# depends_on:
|
|
||||||
# - postgres-service
|
|
||||||
# - mongo_service
|
|
||||||
# - redis_service
|
|
||||||
|
|
||||||
dealer_service:
|
dealer_service:
|
||||||
container_name: dealer_service
|
container_name: dealer_service
|
||||||
|
|
@ -110,10 +86,25 @@ services:
|
||||||
- wag-services
|
- wag-services
|
||||||
env_file:
|
env_file:
|
||||||
- api_env.env
|
- api_env.env
|
||||||
depends_on:
|
|
||||||
- postgres-service
|
networks:
|
||||||
- mongo_service
|
wag-services:
|
||||||
- redis_service
|
# client-frontend:
|
||||||
|
# management-frontend:
|
||||||
|
|
||||||
|
# management_frontend:
|
||||||
|
# container_name: management_frontend
|
||||||
|
# build:
|
||||||
|
# context: .
|
||||||
|
# dockerfile: WebServices/management-frontend/Dockerfile
|
||||||
|
# networks:
|
||||||
|
# - wag-services
|
||||||
|
# ports:
|
||||||
|
# - "3001:3000" # Using different host port to avoid conflicts
|
||||||
|
# # volumes:
|
||||||
|
# # - management-frontend:/WebServices/management-frontend
|
||||||
|
# environment:
|
||||||
|
# - NODE_ENV=development
|
||||||
|
|
||||||
# template_service:
|
# template_service:
|
||||||
# container_name: template_service
|
# container_name: template_service
|
||||||
|
|
@ -141,7 +132,6 @@ services:
|
||||||
# - postgres-service
|
# - postgres-service
|
||||||
# - mongo_service
|
# - mongo_service
|
||||||
# - redis_service
|
# - redis_service
|
||||||
|
|
||||||
# test_server:
|
# test_server:
|
||||||
# container_name: test_server
|
# container_name: test_server
|
||||||
# build:
|
# build:
|
||||||
|
|
@ -151,12 +141,3 @@ services:
|
||||||
# - api_env.env
|
# - api_env.env
|
||||||
# networks:
|
# networks:
|
||||||
# - wag-services
|
# - wag-services
|
||||||
|
|
||||||
networks:
|
|
||||||
wag-services:
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
postgres-data:
|
|
||||||
mongodb-data:
|
|
||||||
# client-frontend:
|
|
||||||
# management-frontend:
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue