updated components and page navigator

This commit is contained in:
2025-04-20 18:25:00 +03:00
parent 6ab4410a82
commit a0daf45530
36 changed files with 1939 additions and 252 deletions

View File

@@ -28,7 +28,7 @@ interface LoginSelectOccupant {
async function logoutActiveSession() {
const cookieStore = await cookies();
const response = await fetchDataWithToken(logoutEndpoint, {}, "POST", false);
const response = await fetchDataWithToken(logoutEndpoint, {}, "GET", false);
cookieStore.delete("accessToken");
cookieStore.delete("accessObject");
cookieStore.delete("userProfile");

View File

@@ -1,9 +1,10 @@
"use server";
import React from "react";
import ClientMenu from "@/components/menu/menu";
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 retrievePageByUrlAndPageId from "@/components/navigator/retriever";
export default async function DashboardLayout({
searchParams,
@@ -13,7 +14,7 @@ export default async function DashboardLayout({
const activePage = "/dashboard";
const siteUrlsList = (await retrievePageList()) || [];
const pageToDirect = await retrievePagebyUrl(activePage);
const PageComponent = retrievePage(pageToDirect);
const PageComponent = retrievePageByUrlAndPageId(pageToDirect, activePage);
const searchParamsInstance = await searchParams;
const lang = (searchParamsInstance?.lang as "en" | "tr") || "en";

View File

@@ -2,7 +2,7 @@
import React from "react";
import ClientMenu from "@/components/menu/menu";
import { retrievePageList, retrievePagebyUrl } from "@/apicalls/cookies/token";
import { retrievePage } from "@/components/NavigatePages";
import { retrievePageByUrlAndPageId } from "@/components/navigator/retriever";
import { searchPlaceholder } from "@/app/commons/pageDefaults";
const pageInfo = {
@@ -18,7 +18,7 @@ export default async function Dashboard({
const activePage = "/individual";
const siteUrlsList = (await retrievePageList()) || [];
const pageToDirect = await retrievePagebyUrl(activePage);
const PageComponent = retrievePage(pageToDirect);
const PageComponent = retrievePageByUrlAndPageId(pageToDirect, activePage);
const searchParamsInstance = await searchParams;
const lang = (searchParamsInstance?.lang as "en" | "tr") || "en";

View File

@@ -1,8 +1,9 @@
"use server";
import React from "react";
import { retrievePageList, retrievePagebyUrl } from "@/apicalls/cookies/token";
import { retrievePage } from "@/components/NavigatePages";
import LeftMenu from "@/components/menu/leftMenu";
import retrievePageByUrlAndPageId from "@/components/navigator/retriever";
import ClientMenu from "@/components/menu/menu";
import Header from "@/components/header/Header";
export default async function Dashboard({
searchParams,
@@ -14,35 +15,20 @@ export default async function Dashboard({
const searchParamsInstance = await searchParams;
const activePage = "/management/accounting";
const pageToDirect = await retrievePagebyUrl(activePage);
const PageComponent = retrievePage(pageToDirect);
const PageComponent = retrievePageByUrlAndPageId(activePage, pageToDirect);
return (
<>
<div className="min-h-screen min-w-screen flex h-screen w-screen overflow-hidden">
{/* Sidebar */}
<aside className="w-1/4 border-r p-4 overflow-y-auto">
<LeftMenu
pageUuidList={siteUrlsList}
lang={lang}
searchParams={searchParamsInstance}
pageSelected={activePage}
/>
<ClientMenu lang={lang} siteUrls={siteUrlsList} />
</aside>
{/* Main Content Area */}
<div className="flex flex-col w-3/4">
{/* Sticky Header */}
<header className="sticky top-0 bg-white shadow-md z-10 p-4 flex justify-between items-center">
<h1 className="text-2xl font-semibold">{activePage}</h1>
<div className="flex items-center space-x-4">
<input
type="text"
placeholder="Search..."
className="border px-3 py-2 rounded-lg"
/>
<div className="w-10 h-10 bg-gray-300 rounded-full"></div>
</div>
</header>
<Header lang={lang} />
<PageComponent lang={lang} queryParams={searchParamsInstance} />
</div>
</div>

View File

@@ -1,8 +1,9 @@
"use server";
import React from "react";
import { retrievePageList, retrievePagebyUrl } from "@/apicalls/cookies/token";
import { retrievePage } from "@/components/NavigatePages";
import LeftMenu from "@/components/menu/leftMenu";
import retrievePageByUrlAndPageId from "@/components/navigator/retriever";
import ClientMenu from "@/components/menu/menu";
import Header from "@/components/header/Header";
export default async function Dashboard({
searchParams,
@@ -14,35 +15,20 @@ export default async function Dashboard({
const searchParamsInstance = await searchParams;
const activePage = "/management/budget";
const pageToDirect = await retrievePagebyUrl(activePage);
const PageComponent = retrievePage(pageToDirect);
const PageComponent = retrievePageByUrlAndPageId(activePage, pageToDirect);
return (
<>
<div className="min-h-screen min-w-screen flex h-screen w-screen overflow-hidden">
{/* Sidebar */}
<aside className="w-1/4 border-r p-4 overflow-y-auto">
<LeftMenu
pageUuidList={siteUrlsList}
lang={lang}
searchParams={searchParamsInstance}
pageSelected={activePage}
/>
<ClientMenu lang={lang} siteUrls={siteUrlsList} />
</aside>
{/* Main Content Area */}
<div className="flex flex-col w-3/4">
{/* Sticky Header */}
<header className="sticky top-0 bg-white shadow-md z-10 p-4 flex justify-between items-center">
<h1 className="text-2xl font-semibold">{activePage}</h1>
<div className="flex items-center space-x-4">
<input
type="text"
placeholder="Search..."
className="border px-3 py-2 rounded-lg"
/>
<div className="w-10 h-10 bg-gray-300 rounded-full"></div>
</div>
</header>
<Header lang={lang} />
<PageComponent lang={lang} queryParams={searchParamsInstance} />
</div>
</div>

View File

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

View File

@@ -1,6 +0,0 @@
import { LanguageTranslation } from "@/components/menu/runner";
export interface PageProps {
lang: keyof LanguageTranslation;
queryParams: { [key: string]: string | undefined };
}

View File

@@ -1,215 +0,0 @@
// Mock data for buildings table
const buildingsMockData = [
{
uu_id: "63192f8a-0b36-49b5-a058-423eb375ab1b",
gov_address_code: "GAC12345678",
build_name: "Sunset Towers",
build_no: "A123",
max_floor: 15,
underground_floor: 2,
build_date: "2010-05-12T00:00:00Z",
decision_period_date: "2024-03-15T00:00:00Z",
tax_no: "TX123456789012",
lift_count: 3,
heating_system: true,
cooling_system: true,
hot_water_system: true,
block_service_man_count: 2,
security_service_man_count: 1,
garage_count: 25,
site_uu_id: "site-uuid-6789abcd-1234",
address_uu_id: "addr-uuid-1234-5678-abcd",
build_types_uu_id: "type-uuid-residential-apt",
},
{
uu_id: "8149fcac-3ac8-4107-acce-ef52f378a874",
gov_address_code: "GAC23456789",
build_name: "Ocean View Plaza",
build_no: "B456",
max_floor: 22,
underground_floor: 3,
build_date: "2015-07-23T00:00:00Z",
decision_period_date: "2024-05-10T00:00:00Z",
tax_no: "TX234567890123",
lift_count: 5,
heating_system: true,
cooling_system: true,
hot_water_system: true,
block_service_man_count: 3,
security_service_man_count: 2,
garage_count: 50,
site_uu_id: "site-uuid-7890bcde-2345",
address_uu_id: "addr-uuid-2345-6789-bcde",
build_types_uu_id: "type-uuid-residential-condo",
},
{
uu_id: "10fb6ffe-610b-4e7e-bb5b-b46e0946cff7",
gov_address_code: "GAC34567890",
build_name: "Parkside Heights",
build_no: "C789",
max_floor: 8,
underground_floor: 1,
build_date: "2005-11-30T00:00:00Z",
decision_period_date: "2024-04-22T00:00:00Z",
tax_no: "TX345678901234",
lift_count: 2,
heating_system: true,
cooling_system: false,
hot_water_system: true,
block_service_man_count: 1,
security_service_man_count: 1,
garage_count: 16,
site_uu_id: "site-uuid-8901cdef-3456",
address_uu_id: "addr-uuid-3456-7890-cdef",
build_types_uu_id: "type-uuid-commercial-office",
},
{
uu_id: "0447123a-8992-4e22-ba86-2f0feaa763d2",
gov_address_code: "GAC45678901",
build_name: "Riverside Apartments",
build_no: "D012",
max_floor: 12,
underground_floor: 2,
build_date: "2018-03-17T00:00:00Z",
decision_period_date: "2024-02-28T00:00:00Z",
tax_no: "TX456789012345",
lift_count: 3,
heating_system: true,
cooling_system: true,
hot_water_system: false,
block_service_man_count: 2,
security_service_man_count: 1,
garage_count: 30,
site_uu_id: "site-uuid-9012defg-4567",
address_uu_id: "addr-uuid-4567-8901-defg",
build_types_uu_id: "type-uuid-residential-apt",
},
{
uu_id: "6682a927-abb7-4d33-b877-3df170c3679c",
gov_address_code: "GAC56789012",
build_name: "Mountain View Plaza",
build_no: "E345",
max_floor: 5,
underground_floor: 0,
build_date: "2000-09-05T00:00:00Z",
decision_period_date: "2024-01-15T00:00:00Z",
tax_no: "TX567890123456",
lift_count: 1,
heating_system: true,
cooling_system: false,
hot_water_system: false,
block_service_man_count: 1,
security_service_man_count: 0,
garage_count: 8,
site_uu_id: "site-uuid-0123efgh-5678",
address_uu_id: "addr-uuid-5678-9012-efgh",
build_types_uu_id: "type-uuid-mixed-use",
},
{
uu_id: "a06fef1b-3eb7-4aed-b901-47a171a12a93",
gov_address_code: "GAC67890123",
build_name: "City Center Tower",
build_no: "F678",
max_floor: 30,
underground_floor: 4,
build_date: "2020-01-10T00:00:00Z",
decision_period_date: "2024-06-30T00:00:00Z",
tax_no: "TX678901234567",
lift_count: 8,
heating_system: true,
cooling_system: true,
hot_water_system: true,
block_service_man_count: 4,
security_service_man_count: 3,
garage_count: 100,
site_uu_id: "site-uuid-1234fghi-6789",
address_uu_id: "addr-uuid-6789-0123-fghi",
build_types_uu_id: "type-uuid-commercial-skyscraper",
},
{
uu_id: "22be0407-f6a4-456e-a183-6641d2714d73",
gov_address_code: "GAC78901234",
build_name: "Garden Villas",
build_no: "G901",
max_floor: 3,
underground_floor: 0,
build_date: "2012-06-22T00:00:00Z",
decision_period_date: "2024-03-01T00:00:00Z",
tax_no: "TX789012345678",
lift_count: 0,
heating_system: true,
cooling_system: false,
hot_water_system: true,
block_service_man_count: 1,
security_service_man_count: 0,
garage_count: 6,
site_uu_id: "site-uuid-2345ghij-7890",
address_uu_id: "addr-uuid-7890-1234-ghij",
build_types_uu_id: "type-uuid-residential-townhouse",
},
{
uu_id: "7792645f-350c-4567-8a78-190014674e6b",
gov_address_code: "GAC89012345",
build_name: "Industrial Complex",
build_no: "H234",
max_floor: 2,
underground_floor: 1,
build_date: "2008-12-05T00:00:00Z",
decision_period_date: "2024-05-15T00:00:00Z",
tax_no: "TX890123456789",
lift_count: 1,
heating_system: true,
cooling_system: false,
hot_water_system: false,
block_service_man_count: 0,
security_service_man_count: 1,
garage_count: 12,
site_uu_id: "site-uuid-3456hijk-8901",
address_uu_id: "addr-uuid-8901-2345-hijk",
build_types_uu_id: "type-uuid-industrial",
},
{
uu_id: "8de7a620-3c1e-4925-8147-3eb33a2059cc",
gov_address_code: "GAC90123456",
build_name: "Hillside Residences",
build_no: "I567",
max_floor: 10,
underground_floor: 2,
build_date: "2017-04-18T00:00:00Z",
decision_period_date: "2024-02-10T00:00:00Z",
tax_no: "TX901234567890",
lift_count: 2,
heating_system: true,
cooling_system: true,
hot_water_system: true,
block_service_man_count: 2,
security_service_man_count: 1,
garage_count: 20,
site_uu_id: "site-uuid-4567ijkl-9012",
address_uu_id: "addr-uuid-9012-3456-ijkl",
build_types_uu_id: "type-uuid-residential-apt",
},
{
uu_id: "1a680003-d005-414c-86ab-f16e090aba25",
gov_address_code: "GACA0123456",
build_name: "Tech Hub Center",
build_no: "J890",
max_floor: 18,
underground_floor: 3,
build_date: "2019-08-30T00:00:00Z",
decision_period_date: "2024-04-01T00:00:00Z",
tax_no: "TXA01234567890",
lift_count: 6,
heating_system: true,
cooling_system: true,
hot_water_system: true,
block_service_man_count: 3,
security_service_man_count: 2,
garage_count: 45,
site_uu_id: "site-uuid-5678jklm-0123",
address_uu_id: "addr-uuid-0123-4567-jklm",
build_types_uu_id: "type-uuid-commercial-office",
},
];
export default buildingsMockData;

View File

@@ -1,36 +0,0 @@
import React from "react";
import { PageProps } from "./interFaces";
const pageContext = {
tr: {
pageTitle: "Sayfa 0001",
pageDescription: "Bu, Sayfa 0001'in içeriğidir.",
},
en: {
pageTitle: "Page 0001",
pageDescription: "This is the content of Page 0001.",
},
};
function Page0001({ lang }: PageProps) {
const { pageTitle, pageDescription } =
pageContext[lang as keyof typeof pageContext];
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">{pageTitle}</h1>
</header>
<main className="flex-grow p-4 bg-gray-100">
<p className="text-gray-700">{pageDescription}</p>
</main>
<footer className="bg-gray-800 text-white p-4 text-center">
<p>&copy; 2023 My Application</p>
</footer>
</div>
</>
);
}
export default Page0001;

View File

@@ -0,0 +1,6 @@
import { PageComponent } from "@/components/validations/translations/translation";
import DashboardSuperUserApp from "./superusers/app";
export const dashboardApplications: Record<string, PageComponent> = {
app000001: DashboardSuperUserApp,
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,6 @@
import { PageComponent } from "@/components/validations/translations/translation";
import PeopleSuperUserApp from "./superusers/app";
export const peopleApplications: Record<string, PageComponent> = {
app000003: PeopleSuperUserApp,
};

View File

@@ -7,6 +7,10 @@ import EmployeeProfileSection from "./EmployeeProfileSection";
import OccupantProfileSection from "./OccupantProfileSection";
import ProfileLoadingState from "./ProfileLoadingState";
import NavigationMenu from "./NavigationMenu";
import {
ClientMenuProps,
UserSelection,
} from "@/components/validations/menu/menu";
// Language definitions for dashboard title
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 transformedMenu = transformMenu(siteUrls);
const t =

View File

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

View File

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

View File

@@ -0,0 +1,11 @@
interface ClientMenuProps {
siteUrls: string[];
lang?: string;
}
interface UserSelection {
userType: string;
selected: any;
}
export type { ClientMenuProps, UserSelection };

View File

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