updated postgres and mongo updated
This commit is contained in:
@@ -23,9 +23,7 @@ async function retrievePageList() {
|
||||
: null;
|
||||
}
|
||||
|
||||
|
||||
async function retrievePagebyUrl(pageUrl: string) {
|
||||
|
||||
const response = await fetchDataWithToken(
|
||||
pageValid,
|
||||
{
|
||||
@@ -66,50 +64,56 @@ async function retrieveAccessObjects() {
|
||||
async function retrieveUserSelection() {
|
||||
const cookieStore = await cookies();
|
||||
const encrpytUserSelection = cookieStore.get("userSelection")?.value || "";
|
||||
|
||||
let objectUserSelection = {};
|
||||
let decrpytUserSelection: any = await nextCrypto.decrypt(
|
||||
encrpytUserSelection
|
||||
);
|
||||
decrpytUserSelection = decrpytUserSelection
|
||||
? JSON.parse(decrpytUserSelection)
|
||||
: null;
|
||||
|
||||
const userSelection = decrpytUserSelection?.company_uu_id;
|
||||
|
||||
let objectUserSelection = {};
|
||||
console.log("decrpytUserSelection", decrpytUserSelection);
|
||||
const userSelection = decrpytUserSelection?.selected;
|
||||
const accessObjects = (await retrieveAccessObjects()) || {};
|
||||
console.log("accessObjects", accessObjects);
|
||||
|
||||
if (decrpytUserSelection?.user_type === "employee") {
|
||||
const accessObjects = (await retrieveAccessObjects()) || {};
|
||||
const companyList = accessObjects?.companies_list;
|
||||
const companyList = accessObjects?.selectionList;
|
||||
const selectedCompany = companyList.find(
|
||||
(company: any) => company.uu_id === userSelection
|
||||
);
|
||||
if (selectedCompany) {
|
||||
objectUserSelection = {
|
||||
occupantName: `${selectedCompany?.public_name}`,
|
||||
};
|
||||
objectUserSelection = { userType: "employee", selected: selectedCompany };
|
||||
}
|
||||
} else if (decrpytUserSelection?.user_type === "occupant") {
|
||||
const buildPartUUID = userSelection?.build_part_uu_id;
|
||||
const occupantUUID = userSelection?.occupant_uu_id;
|
||||
const build_id = userSelection?.build_id;
|
||||
const accessObjects = (await retrieveAccessObjects()) || {};
|
||||
const availableOccupants = accessObjects?.available_occupants[build_id];
|
||||
const buildName = availableOccupants?.build_name;
|
||||
const buildNo = availableOccupants?.build_no;
|
||||
let selectedOccupant: any = null;
|
||||
const occupants = availableOccupants?.occupants;
|
||||
if (occupants) {
|
||||
selectedOccupant = occupants.find(
|
||||
(occupant: any) =>
|
||||
occupant.part_uu_id === buildPartUUID &&
|
||||
occupant.uu_id === occupantUUID
|
||||
);
|
||||
}
|
||||
if (selectedOccupant) {
|
||||
objectUserSelection = {
|
||||
buildName: `${buildName} - No:${buildNo}`,
|
||||
occupantName: `${selectedOccupant?.description} ${selectedOccupant?.part_name}`,
|
||||
};
|
||||
const buildingsList = accessObjects?.selectionList;
|
||||
|
||||
// Iterate through all buildings
|
||||
if (buildingsList) {
|
||||
// Loop through each building
|
||||
for (const buildKey in buildingsList) {
|
||||
const building = buildingsList[buildKey];
|
||||
|
||||
// Check if the building has occupants
|
||||
if (building.occupants && building.occupants.length > 0) {
|
||||
// Find the occupant with the matching build_living_space_uu_id
|
||||
const occupant = building.occupants.find(
|
||||
(occ: any) => occ.build_living_space_uu_id === userSelection
|
||||
);
|
||||
|
||||
if (occupant) {
|
||||
objectUserSelection = {
|
||||
userType: "occupant",
|
||||
selected: {
|
||||
...occupant,
|
||||
buildName: building.build_name,
|
||||
buildNo: building.build_no,
|
||||
},
|
||||
};
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
|
||||
@@ -7,6 +7,7 @@ import { cookies } from "next/headers";
|
||||
|
||||
const loginEndpoint = `${baseUrlAuth}/authentication/login`;
|
||||
const loginSelectEndpoint = `${baseUrlAuth}/authentication/select`;
|
||||
const logoutEndpoint = `${baseUrlAuth}/authentication/logout`;
|
||||
|
||||
console.log("loginEndpoint", loginEndpoint);
|
||||
console.log("loginSelectEndpoint", loginSelectEndpoint);
|
||||
@@ -25,6 +26,16 @@ interface LoginSelectOccupant {
|
||||
build_living_space_uu_id: any;
|
||||
}
|
||||
|
||||
async function logoutActiveSession() {
|
||||
const cookieStore = await cookies();
|
||||
const response = await fetchDataWithToken(logoutEndpoint, {}, "POST", false);
|
||||
cookieStore.delete("accessToken");
|
||||
cookieStore.delete("accessObject");
|
||||
cookieStore.delete("userProfile");
|
||||
cookieStore.delete("userSelection");
|
||||
return response;
|
||||
}
|
||||
|
||||
async function loginViaAccessKeys(payload: LoginViaAccessKeys) {
|
||||
try {
|
||||
const cookieStore = await cookies();
|
||||
@@ -59,8 +70,6 @@ async function loginViaAccessKeys(payload: LoginViaAccessKeys) {
|
||||
value: accessToken,
|
||||
...cookieObject,
|
||||
});
|
||||
console.log("accessObject", accessObject);
|
||||
|
||||
cookieStore.set({
|
||||
name: "accessObject",
|
||||
value: accessObject,
|
||||
@@ -109,19 +118,21 @@ async function loginViaAccessKeys(payload: LoginViaAccessKeys) {
|
||||
async function loginSelectEmployee(payload: LoginSelectEmployee) {
|
||||
const cookieStore = await cookies();
|
||||
const nextCrypto = new NextCrypto(tokenSecret);
|
||||
|
||||
const companyUUID = payload.company_uu_id;
|
||||
const selectResponse: any = await fetchDataWithToken(
|
||||
loginSelectEndpoint,
|
||||
{
|
||||
company_uu_id: payload.company_uu_id,
|
||||
company_uu_id: companyUUID,
|
||||
},
|
||||
"POST",
|
||||
false
|
||||
);
|
||||
cookieStore.delete("userSelection");
|
||||
|
||||
if (selectResponse.status === 200 || selectResponse.status === 202) {
|
||||
const usersSelection = await nextCrypto.encrypt(
|
||||
JSON.stringify({
|
||||
company_uu_id: payload.company_uu_id,
|
||||
selected: companyUUID,
|
||||
user_type: "employee",
|
||||
})
|
||||
);
|
||||
@@ -135,27 +146,23 @@ async function loginSelectEmployee(payload: LoginSelectEmployee) {
|
||||
}
|
||||
|
||||
async function loginSelectOccupant(payload: LoginSelectOccupant) {
|
||||
const build_living_space_uu_id = payload.build_living_space_uu_id;
|
||||
const livingSpaceUUID = payload.build_living_space_uu_id;
|
||||
const cookieStore = await cookies();
|
||||
const nextCrypto = new NextCrypto(tokenSecret);
|
||||
const selectResponse: any = await fetchDataWithToken(
|
||||
loginSelectEndpoint,
|
||||
{
|
||||
build_living_space_uu_id: build_living_space_uu_id,
|
||||
build_living_space_uu_id: livingSpaceUUID,
|
||||
},
|
||||
"POST",
|
||||
false
|
||||
);
|
||||
cookieStore.delete("userSelection");
|
||||
|
||||
if (selectResponse.status === 200) {
|
||||
if (selectResponse.status === 200 || selectResponse.status === 202) {
|
||||
const usersSelection = await nextCrypto.encrypt(
|
||||
JSON.stringify({
|
||||
// company_uu_id: {
|
||||
// build_part_uu_id: payload.build_part_uu_id,
|
||||
// occupant_uu_id: payload.occupant_uu_id,
|
||||
// build_id: selectedBuilding,
|
||||
// },
|
||||
build_living_space_uu_id: build_living_space_uu_id,
|
||||
selected: livingSpaceUUID,
|
||||
user_type: "occupant",
|
||||
})
|
||||
);
|
||||
@@ -164,9 +171,13 @@ async function loginSelectOccupant(payload: LoginSelectOccupant) {
|
||||
value: usersSelection,
|
||||
...cookieObject,
|
||||
});
|
||||
// await setAvailableEvents();
|
||||
}
|
||||
return selectResponse;
|
||||
}
|
||||
|
||||
export { loginViaAccessKeys, loginSelectEmployee, loginSelectOccupant };
|
||||
export {
|
||||
loginViaAccessKeys,
|
||||
loginSelectEmployee,
|
||||
loginSelectOccupant,
|
||||
logoutActiveSession,
|
||||
};
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
"use server";
|
||||
import React from "react";
|
||||
import {
|
||||
checkAccessTokenIsValid,
|
||||
retrieveUserType,
|
||||
} from "@/apicalls/cookies/token";
|
||||
import { redirect } from "next/navigation";
|
||||
import SelectList from "@/components/auth/select";
|
||||
import LoginEmployee from "@/components/auth/LoginEmployee";
|
||||
import LoginOccupant from "@/components/auth/LoginOccupant";
|
||||
|
||||
async function SelectPage() {
|
||||
const token_is_valid = await checkAccessTokenIsValid();
|
||||
const selection = await retrieveUserType();
|
||||
console.log("selection", selection);
|
||||
|
||||
const isEmployee = selection?.userType == "employee";
|
||||
const isOccupant = selection?.userType == "occupant";
|
||||
@@ -18,28 +21,19 @@ async function SelectPage() {
|
||||
if (!selectionList || !token_is_valid) {
|
||||
redirect("/auth/login");
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col items-center justify-center h-screen">
|
||||
<div className="text-2xl font-bold">Select your company</div>
|
||||
<div className="flex flex-col items-center justify-center">
|
||||
{isEmployee && (
|
||||
<div className="text-sm text-gray-500 mt-4">
|
||||
You are logged in as an employee
|
||||
</div>
|
||||
{isEmployee && Array.isArray(selectionList) && (
|
||||
<LoginEmployee selectionList={selectionList} />
|
||||
)}
|
||||
{isOccupant && (
|
||||
<div className="text-sm text-gray-500 mt-4">
|
||||
You are logged in as an occupant
|
||||
</div>
|
||||
|
||||
{isOccupant && !Array.isArray(selectionList) && (
|
||||
<LoginOccupant selectionList={selectionList} />
|
||||
)}
|
||||
<SelectList
|
||||
isEmployee={isEmployee}
|
||||
isOccupant={isOccupant}
|
||||
selectionList={selectionList}
|
||||
/>
|
||||
</div>
|
||||
<div className="text-sm text-gray-500 mt-4"></div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,45 +1,35 @@
|
||||
"use server";
|
||||
import React from "react";
|
||||
import LeftMenu from "@/components/menu/leftMenu";
|
||||
import { retrievePageList } from "@/apicalls/cookies/token";
|
||||
import ClientMenu from "@/components/menu/menu";
|
||||
import { retrievePageList, retrievePagebyUrl } from "@/apicalls/cookies/token";
|
||||
import { retrievePage } from "@/components/NavigatePages";
|
||||
import Header from "@/components/header/Header";
|
||||
|
||||
export default async function DashboardLayout({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: Promise<{ [key: string]: string | undefined }>;
|
||||
}) {
|
||||
const siteUrlsList = (await retrievePageList()) || [];
|
||||
const lang = "tr";
|
||||
const searchParamsInstance = await searchParams;
|
||||
const activePage = "/dashboard";
|
||||
const siteUrlsList = (await retrievePageList()) || [];
|
||||
const pageToDirect = await retrievePagebyUrl(activePage);
|
||||
const PageComponent = retrievePage(pageToDirect);
|
||||
const searchParamsInstance = await searchParams;
|
||||
const lang = (searchParamsInstance?.lang as "en" | "tr") || "en";
|
||||
|
||||
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 siteUrls={siteUrlsList} lang={lang} />
|
||||
</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">Dashboard</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 Component */}
|
||||
<Header lang={lang} />
|
||||
<PageComponent lang={lang} queryParams={searchParamsInstance} />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -1,49 +1,52 @@
|
||||
"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 LeftMenu from "@/components/menu/leftMenu";
|
||||
import { searchPlaceholder } from "@/app/commons/pageDefaults";
|
||||
|
||||
const pageInfo = {
|
||||
tr: "Birey Sayfası",
|
||||
en: "Individual Page",
|
||||
};
|
||||
|
||||
export default async function Dashboard({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: Promise<{ [key: string]: string | undefined }>;
|
||||
}) {
|
||||
const siteUrlsList = (await retrievePageList()) || [];
|
||||
const lang = "tr";
|
||||
const searchParamsInstance = await searchParams;
|
||||
const activePage = "/individual";
|
||||
const siteUrlsList = (await retrievePageList()) || [];
|
||||
const pageToDirect = await retrievePagebyUrl(activePage);
|
||||
const PageComponent = retrievePage(pageToDirect);
|
||||
const searchParamsInstance = await searchParams;
|
||||
const lang = (searchParamsInstance?.lang as "en" | "tr") || "en";
|
||||
|
||||
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 siteUrls={siteUrlsList} lang={lang} />
|
||||
</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>
|
||||
<h1 className="text-2xl font-semibold">{pageInfo[lang]}</h1>
|
||||
<div className="flex items-center space-x-4">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search..."
|
||||
placeholder={searchPlaceholder[lang]}
|
||||
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} />
|
||||
<div className="p-4 overflow-y-auto">
|
||||
<PageComponent lang={lang} queryParams={searchParamsInstance} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
"use server";
|
||||
import React from "react";
|
||||
import Template from "@/components/Pages/template/app";
|
||||
import ClientMenu from "@/components/menuCleint/menu";
|
||||
import {
|
||||
getTranslation,
|
||||
LanguageKey,
|
||||
} from "@/components/Pages/template/language";
|
||||
import { retrievePageList } from "@/apicalls/cookies/token";
|
||||
import ClientMenu from "@/components/menu/menu";
|
||||
import { retrievePage } from "@/components/NavigatePages";
|
||||
import { retrievePageList, retrievePagebyUrl } from "@/apicalls/cookies/token";
|
||||
import { searchPlaceholder } from "@/app/commons/pageDefaults";
|
||||
|
||||
const pageInfo = {
|
||||
tr: "Tamamlayıcı Sayfası",
|
||||
en: "Template Page",
|
||||
};
|
||||
|
||||
interface TemplatePageProps {
|
||||
searchParams: { [key: string]: string | string[] | undefined };
|
||||
@@ -14,10 +17,12 @@ interface TemplatePageProps {
|
||||
|
||||
async function TemplatePage({ searchParams }: TemplatePageProps) {
|
||||
// Get language from query params or default to 'en'
|
||||
const activePage = "/template";
|
||||
const siteUrlsList = (await retrievePageList()) || [];
|
||||
const pageToDirect = await retrievePagebyUrl(activePage);
|
||||
const PageComponent = retrievePage(pageToDirect);
|
||||
const searchParamsInstance = await searchParams;
|
||||
const lang = (searchParamsInstance?.lang as LanguageKey) || "en";
|
||||
const t = getTranslation(lang);
|
||||
const lang = (searchParamsInstance?.lang as "en" | "tr") || "en";
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -33,11 +38,11 @@ async function TemplatePage({ searchParams }: TemplatePageProps) {
|
||||
<div className="flex flex-col w-3/4 overflow-y-auto">
|
||||
{/* Sticky Header */}
|
||||
<header className="sticky top-0 bg-white shadow-md z-10 p-4 flex justify-between items-center">
|
||||
<h1 className="text-2xl font-semibold">{t.title}</h1>
|
||||
<h1 className="text-2xl font-semibold">{pageInfo[lang]}</h1>
|
||||
<div className="flex items-center space-x-4">
|
||||
<input
|
||||
type="text"
|
||||
placeholder={t.search}
|
||||
placeholder={searchPlaceholder[lang]}
|
||||
className="border px-3 py-2 rounded-lg"
|
||||
/>
|
||||
<div className="w-10 h-10 bg-gray-300 rounded-full"></div>
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
export const searchPlaceholder = {
|
||||
tr: "Ara...",
|
||||
en: "Search...",
|
||||
};
|
||||
|
||||
export const menuLanguage = {
|
||||
tr: "Menü",
|
||||
en: "Menu",
|
||||
};
|
||||
@@ -1,26 +0,0 @@
|
||||
import React from "react";
|
||||
import { PageProps } from "./interFaces";
|
||||
|
||||
function App000001({ lang, queryParams }: PageProps) {
|
||||
return (
|
||||
<>
|
||||
<div className="flex">
|
||||
{/* 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">Dashboard</h1>
|
||||
<div className="flex items-center space-x-4">
|
||||
{JSON.stringify({ lang, queryParams })}
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search..."
|
||||
className="border px-3 py-2 rounded-lg"
|
||||
/>
|
||||
<div className="w-10 h-10 bg-gray-300 rounded-full"></div>
|
||||
</div>
|
||||
</header>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default App000001;
|
||||
@@ -1,130 +0,0 @@
|
||||
"use client";
|
||||
import React from "react";
|
||||
import { Pencil, Plus, ScanSearch } from "lucide-react";
|
||||
|
||||
// Define types
|
||||
interface CardData {
|
||||
id: number;
|
||||
title: string;
|
||||
description: string;
|
||||
status: string;
|
||||
lastUpdated: string;
|
||||
}
|
||||
|
||||
interface CardProps {
|
||||
data: CardData;
|
||||
onUpdate: (id: number) => void;
|
||||
}
|
||||
|
||||
// Mock data
|
||||
const mockData: CardData[] = [
|
||||
{
|
||||
id: 1,
|
||||
title: "Project Alpha",
|
||||
description: "A cutting-edge project for automation",
|
||||
status: "In Progress",
|
||||
lastUpdated: "2024-03-15",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: "Project Beta",
|
||||
description: "Machine learning integration project",
|
||||
status: "Completed",
|
||||
lastUpdated: "2024-03-10",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: "Project Gamma",
|
||||
description: "Cloud infrastructure optimization",
|
||||
status: "Planning",
|
||||
lastUpdated: "2024-03-05",
|
||||
},
|
||||
];
|
||||
|
||||
// Card component
|
||||
const Card: React.FC<CardProps> = ({ data, onUpdate }) => (
|
||||
<div className="bg-white text-black rounded-lg shadow-md p-6 mb-4 hover:shadow-lg transition-shadow">
|
||||
<div className="flex justify-between items-start">
|
||||
<div>
|
||||
<h3 className="text-xl font-semibold mb-2">{data.title}</h3>
|
||||
<p className="mb-2">{data.description}</p>
|
||||
<div className="flex items-center gap-4">
|
||||
<span className="text-sm text-gray-500">Status: {data.status}</span>
|
||||
<span className="text-sm text-gray-500">
|
||||
Last Updated: {data.lastUpdated}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => onUpdate(data.id)}
|
||||
className="text-blue-500 hover:text-blue-700 p-2"
|
||||
aria-label="Update"
|
||||
>
|
||||
<div className="flex flex-col">
|
||||
<div>
|
||||
<Pencil />
|
||||
</div>
|
||||
<div className="mt-5">
|
||||
<ScanSearch />
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
function app000002() {
|
||||
const [modifyEnable, setModifyEnable] = React.useState(false);
|
||||
const [selectedId, setSelectedId] = React.useState<number | null>(null);
|
||||
|
||||
const handleUpdate = (id: number) => {
|
||||
console.log(`Update clicked for item ${id}`);
|
||||
// Add your update logic here
|
||||
};
|
||||
|
||||
const handleCreate = () => {
|
||||
console.log("Create clicked");
|
||||
// Add your create logic here
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="container mx-auto p-6">
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<h1 className="text-2xl font-bold">Projects Dashboard</h1>
|
||||
<button
|
||||
onClick={handleCreate}
|
||||
className="bg-blue-500 text-white px-4 py-2 rounded-lg flex items-center gap-2 hover:bg-blue-600 transition-colors"
|
||||
>
|
||||
<Plus />
|
||||
Create New
|
||||
</button>
|
||||
</div>
|
||||
{!selectedId ? (
|
||||
<div className="grid gap-4">
|
||||
{mockData.map((item) => (
|
||||
<Card
|
||||
key={item.id}
|
||||
data={item}
|
||||
onUpdate={() => setSelectedId(item.id)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div
|
||||
key={selectedId}
|
||||
className="flex min-h-full justify-between items-center mb-6"
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
className="border border-gray-300 rounded-lg p-2 w-full"
|
||||
placeholder="Enter new title"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default app000002;
|
||||
@@ -1,156 +0,0 @@
|
||||
"use client";
|
||||
import React, { useEffect } from "react";
|
||||
|
||||
import {
|
||||
PeopleFormData,
|
||||
PeopleSchema,
|
||||
} from "../Pages/people/superusers/peopleschema1";
|
||||
|
||||
import PeoplePageForm1 from "../Pages/people/superusers/peopleform1";
|
||||
import PeoplePage1 from "../Pages/people/superusers/peoplepage1";
|
||||
import PeopleInfo1 from "../Pages/people/superusers/peopleinfo1";
|
||||
import { peopleList } from "@/apicalls/people/people";
|
||||
|
||||
interface Pagination {
|
||||
page: number;
|
||||
size: number;
|
||||
totalCount: number;
|
||||
allCount: number;
|
||||
totalPages: number;
|
||||
orderField: string[];
|
||||
orderType: string[];
|
||||
pageCount: number;
|
||||
}
|
||||
|
||||
const defaultPagination: Pagination = {
|
||||
page: 1,
|
||||
size: 1,
|
||||
totalCount: 0,
|
||||
allCount: 0,
|
||||
totalPages: 0,
|
||||
orderField: ["uu_id"],
|
||||
orderType: ["asc"],
|
||||
pageCount: 0,
|
||||
};
|
||||
|
||||
function app000003() {
|
||||
const [modifyEnable, setModifyEnable] = React.useState<boolean | null>(false);
|
||||
const [isCreate, setIsCreate] = React.useState<boolean | null>(false);
|
||||
const [selectedId, setSelectedId] = React.useState<string | null>(null);
|
||||
const [tableData, setTableData] = React.useState<PeopleFormData[]>([]);
|
||||
const [pagination, setPagination] =
|
||||
React.useState<Pagination>(defaultPagination);
|
||||
|
||||
const fecthData = async ({
|
||||
// Add any parameters if needed
|
||||
page = 1,
|
||||
pageSize = 10,
|
||||
orderBy = ["asc"],
|
||||
orderType = ["uu_id"],
|
||||
query = {},
|
||||
}) => {
|
||||
// Simulate an API call
|
||||
const result = await peopleList({
|
||||
page,
|
||||
size: pageSize,
|
||||
orderField: orderType,
|
||||
orderType: orderBy,
|
||||
query: query,
|
||||
});
|
||||
setTableData(result?.data || []);
|
||||
setPagination(result?.pagination || {});
|
||||
};
|
||||
|
||||
const fetchDataRef = React.useCallback(({
|
||||
page = 1,
|
||||
pageSize = 10,
|
||||
orderBy = ["asc"],
|
||||
orderType = ["uu_id"],
|
||||
query = {},
|
||||
}) => {
|
||||
peopleList({
|
||||
page,
|
||||
size: pageSize,
|
||||
orderField: orderType,
|
||||
orderType: orderBy,
|
||||
query: query,
|
||||
}).then(result => {
|
||||
setTableData(result?.data || []);
|
||||
setPagination(result?.pagination || {});
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
fetchDataRef({
|
||||
page: pagination.page,
|
||||
pageSize: pagination.size,
|
||||
orderBy: pagination.orderField,
|
||||
orderType: pagination.orderType,
|
||||
query: {},
|
||||
});
|
||||
}, 300); // 300ms debounce
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, [pagination.page, pagination.size, fetchDataRef]);
|
||||
|
||||
const onSubmit = (data: PeopleFormData) => {
|
||||
console.log("Form data:", data);
|
||||
// Submit to API or do other operations
|
||||
};
|
||||
|
||||
const handleUpdateModify = (uuid: string) => {
|
||||
setSelectedId(uuid);
|
||||
setModifyEnable(false);
|
||||
};
|
||||
|
||||
const handleView = (uuid: string) => {
|
||||
setSelectedId(uuid);
|
||||
setModifyEnable(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="h-screen overflow-y-auto">
|
||||
<PeopleInfo1
|
||||
pagination={pagination}
|
||||
setPagination={setPagination}
|
||||
selectedId={selectedId}
|
||||
setIsCreate={() => setIsCreate(true)}
|
||||
/>
|
||||
{!isCreate ? (
|
||||
<div className="min-w-full mx-4 p-6 rounded-lg shadow-md ">
|
||||
{!selectedId ? (
|
||||
<PeoplePage1
|
||||
data={tableData}
|
||||
handleUpdateModify={handleUpdateModify}
|
||||
handleView={handleView}
|
||||
/>
|
||||
) : (
|
||||
<PeoplePageForm1
|
||||
data={tableData.find((item) => item.uu_id === selectedId) || {}}
|
||||
onSubmit={onSubmit}
|
||||
modifyEnable={modifyEnable}
|
||||
setSelectedId={() => setSelectedId(null)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<PeoplePageForm1
|
||||
data={{
|
||||
build_date: new Date(),
|
||||
decision_period_date: new Date(),
|
||||
}}
|
||||
onSubmit={onSubmit}
|
||||
modifyEnable={modifyEnable}
|
||||
setSelectedId={() => setIsCreate(null)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default app000003;
|
||||
@@ -1,7 +0,0 @@
|
||||
import React from "react";
|
||||
|
||||
function app000004() {
|
||||
return <div>app000004</div>;
|
||||
}
|
||||
|
||||
export default app000004;
|
||||
@@ -1,7 +0,0 @@
|
||||
import React from "react";
|
||||
|
||||
function app000005() {
|
||||
return <div>app000005</div>;
|
||||
}
|
||||
|
||||
export default app000005;
|
||||
@@ -1,7 +0,0 @@
|
||||
import React from "react";
|
||||
|
||||
function app000006() {
|
||||
return <div>app000006</div>;
|
||||
}
|
||||
|
||||
export default app000006;
|
||||
@@ -1,7 +0,0 @@
|
||||
import React from "react";
|
||||
|
||||
function app000007() {
|
||||
return <div>app000007</div>;
|
||||
}
|
||||
|
||||
export default app000007;
|
||||
@@ -1,7 +0,0 @@
|
||||
import React from "react";
|
||||
|
||||
function app000008() {
|
||||
return <div>app000008</div>;
|
||||
}
|
||||
|
||||
export default app000008;
|
||||
@@ -1,7 +0,0 @@
|
||||
import React from "react";
|
||||
|
||||
function app000009() {
|
||||
return <div>app000009</div>;
|
||||
}
|
||||
|
||||
export default app000009;
|
||||
@@ -1,7 +0,0 @@
|
||||
import React from "react";
|
||||
|
||||
function app000010() {
|
||||
return <div>app000010</div>;
|
||||
}
|
||||
|
||||
export default app000010;
|
||||
@@ -1,7 +0,0 @@
|
||||
import React from "react";
|
||||
|
||||
function app000011() {
|
||||
return <div>app000011</div>;
|
||||
}
|
||||
|
||||
export default app000011;
|
||||
@@ -1,9 +0,0 @@
|
||||
import React from 'react'
|
||||
|
||||
function app000012() {
|
||||
return (
|
||||
<div>app000012</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default app000012
|
||||
@@ -1,7 +0,0 @@
|
||||
import React from "react";
|
||||
|
||||
function app000013() {
|
||||
return <div>app000013</div>;
|
||||
}
|
||||
|
||||
export default app000013;
|
||||
@@ -1,9 +0,0 @@
|
||||
import React from 'react'
|
||||
|
||||
function app000014() {
|
||||
return (
|
||||
<div>app000014</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default app000014
|
||||
@@ -1,7 +0,0 @@
|
||||
import React from "react";
|
||||
|
||||
function app000015() {
|
||||
return <div>app000015</div>;
|
||||
}
|
||||
|
||||
export default app000015;
|
||||
@@ -1,7 +0,0 @@
|
||||
import React from "react";
|
||||
|
||||
function app000016() {
|
||||
return <div>app000016</div>;
|
||||
}
|
||||
|
||||
export default app000016;
|
||||
@@ -1,7 +0,0 @@
|
||||
import React from "react";
|
||||
|
||||
function app000017() {
|
||||
return <div>app000017</div>;
|
||||
}
|
||||
|
||||
export default app000017;
|
||||
@@ -1,9 +0,0 @@
|
||||
import React from 'react'
|
||||
|
||||
function app000018() {
|
||||
return (
|
||||
<div>app000018</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default app000018
|
||||
@@ -1,9 +0,0 @@
|
||||
import React from 'react'
|
||||
|
||||
function app000019() {
|
||||
return (
|
||||
<div>app000019</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default app000019
|
||||
@@ -1,43 +1,66 @@
|
||||
import React from "react";
|
||||
import { PageProps } from "./interFaces";
|
||||
|
||||
import App000001 from "./app000001";
|
||||
import App000002 from "./app000002";
|
||||
import app000003 from "./app000003";
|
||||
import PeopleSuperUserApp from "../Pages/people/superusers/app";
|
||||
|
||||
export const PageIndex = {
|
||||
app000001: App000001,
|
||||
app000002: App000002,
|
||||
app000003: app000003,
|
||||
// Ensure all components in PageIndex accept PageProps
|
||||
type PageComponent = React.ComponentType<PageProps>;
|
||||
|
||||
export const PageIndex: Record<string, PageComponent> = {
|
||||
app000003: PeopleSuperUserApp as unknown as PageComponent,
|
||||
};
|
||||
|
||||
function UnAuthorizedPage({ lang, queryParams }: PageProps) {
|
||||
// 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">Unauthorized Access</h1>
|
||||
<h1 className="text-2xl font-bold">{t.title}</h1>
|
||||
</header>
|
||||
<main className="flex-grow p-4 bg-gray-100">
|
||||
<p className="text-gray-700">
|
||||
You do not have permission to access this page.
|
||||
{t.message1}
|
||||
</p>
|
||||
<p className="text-gray-700">Please contact the administrator.</p>
|
||||
<p className="text-gray-700">{t.message2}</p>
|
||||
</main>
|
||||
<footer className="bg-gray-800 text-white p-4 text-center">
|
||||
<p>© 2023 My Application</p>
|
||||
<p>{t.footer}</p>
|
||||
</footer>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export function retrievePage(pageId: string): React.ComponentType<PageProps> {
|
||||
const PageComponent = PageIndex[pageId as keyof typeof PageIndex];
|
||||
if (!PageComponent) {
|
||||
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;
|
||||
}
|
||||
return PageComponent;
|
||||
}
|
||||
|
||||
export default retrievePage;
|
||||
|
||||
@@ -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 PeopleSuperUserApp({ 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 PeopleSuperUserApp;
|
||||
@@ -1,44 +0,0 @@
|
||||
import React from "react";
|
||||
import { Pencil, ScanSearch } from "lucide-react";
|
||||
|
||||
interface CardProps {
|
||||
data: any;
|
||||
onUpdate: (uu_id: string) => void;
|
||||
onView: (uu_id: string) => void;
|
||||
}
|
||||
|
||||
// Card component
|
||||
const Card: React.FC<CardProps> = ({ data, onUpdate, onView }) => (
|
||||
|
||||
<div className="bg-white text-black rounded-lg shadow-md p-6 mb-4 hover:shadow-lg transition-shadow">
|
||||
<div className="flex justify-between items-start">
|
||||
<div>
|
||||
<h3 className="text-xl font-semibold mb-2">{data.person_tag}</h3>
|
||||
<p className="mb-2">
|
||||
Building Number: {data.firstname} {data.surname}
|
||||
</p>
|
||||
<div className="flex items-center gap-4">
|
||||
<span className="text-sm text-gray-500">UUID: {data.uu_id}</span>
|
||||
<span className="text-sm text-gray-500">
|
||||
Built: {new Date(data.created_at).toDateString()}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
className="text-blue-500 hover:text-blue-700 p-2"
|
||||
aria-label="Update"
|
||||
>
|
||||
<div className="flex flex-col">
|
||||
<div onClick={() => onUpdate(data.uu_id)}>
|
||||
<Pencil />
|
||||
</div>
|
||||
<div className="mt-5" onClick={() => onView(data.uu_id)}>
|
||||
<ScanSearch />
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default Card;
|
||||
@@ -0,0 +1,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;
|
||||
@@ -1,431 +0,0 @@
|
||||
import React from "react";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useForm } from "react-hook-form";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { ArrowLeft } from "lucide-react";
|
||||
import { DatePicker } from "@/components/ui/datepicker";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { PeopleFormData, PeopleSchema } from "./peopleschema1";
|
||||
|
||||
interface FormProps {
|
||||
data: any;
|
||||
modifyEnable: boolean | null;
|
||||
setSelectedId: () => void;
|
||||
onSubmit: (data: any) => void;
|
||||
}
|
||||
|
||||
const PeoplePageForm1: React.FC<FormProps> = ({
|
||||
data,
|
||||
modifyEnable,
|
||||
setSelectedId,
|
||||
onSubmit,
|
||||
}) => {
|
||||
const form = useForm<PeopleFormData>({
|
||||
resolver: zodResolver(PeopleSchema),
|
||||
defaultValues: {
|
||||
...data,
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<div onClick={() => setSelectedId()} className="flex items-center">
|
||||
<ArrowLeft /> Back
|
||||
</div>
|
||||
<div className="mx-auto min-w-full p-6 h-screen overflow-y-auto">
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
|
||||
<div className="flex">
|
||||
{/* Government Address Code */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="gov_address_code"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Government Address Code</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} disabled={Boolean(modifyEnable)} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
This is your public display name.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{/* Building Name */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="build_name"
|
||||
render={({ field }) => (
|
||||
<FormItem className="w-full">
|
||||
<FormLabel>Building Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
className="min-w-full ml-5"
|
||||
disabled={Boolean(modifyEnable)}
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
This is your public display name.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
{/* Building Number */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="build_no"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Building Number</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} disabled={Boolean(modifyEnable)} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
This is your public display name.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{/* Building Max Floor */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="max_floor"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Max Floor</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
type="number"
|
||||
disabled={Boolean(modifyEnable)}
|
||||
min={1}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
This is your public display name.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{/* Lift Count */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="lift_count"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Lift Count</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
type="number"
|
||||
disabled={Boolean(modifyEnable)}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
This is your public display name.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{/* Underground Floor */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="underground_floor"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Underground Floor</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
type="number"
|
||||
disabled={Boolean(modifyEnable)}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
This is your public display name.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{/* Block Service Man Count */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="block_service_man_count"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Block Service Man Count</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
type="number"
|
||||
disabled={Boolean(modifyEnable)}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
This is your public display name.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
{/* Build Date */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="build_date"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Build Date</FormLabel>
|
||||
<FormControl>
|
||||
<DatePicker
|
||||
control={form.control}
|
||||
name="date"
|
||||
initialDate={new Date(data?.build_date)}
|
||||
disabled={Boolean(modifyEnable)}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
This is your public display name.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{/* Decision Period Date */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="decision_period_date"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Decision Period Date</FormLabel>
|
||||
<FormControl>
|
||||
<DatePicker
|
||||
control={form.control}
|
||||
name="date"
|
||||
initialDate={new Date(data?.decision_period_date)}
|
||||
disabled={Boolean(modifyEnable)}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
This is your public display name.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{/* Tax Number */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="tax_no"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Tax Number</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} disabled={Boolean(modifyEnable)} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
This is your public display name.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{/* Garage Count */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="garage_count"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Garage Count</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
type="number"
|
||||
disabled={Boolean(modifyEnable)}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
This is your public display name.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{/* Security Service Man Count */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="security_service_man_count"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Security Service Man Count</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
type="number"
|
||||
disabled={Boolean(modifyEnable)}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
This is your public display name.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
{/* Systems */}
|
||||
<fieldset className="flex justify-evenly items-center border p-3 rounded space-x-4">
|
||||
<legend className="text-sm font-medium px-2">
|
||||
Building Systems
|
||||
</legend>
|
||||
{/* Heating System */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="heating_system"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Heating System</FormLabel>
|
||||
<FormControl>
|
||||
<Checkbox
|
||||
defaultChecked={data?.heating_system}
|
||||
disabled={Boolean(modifyEnable)}
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{/* Cooling System */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="cooling_system"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Cooling System</FormLabel>
|
||||
<FormControl>
|
||||
<Checkbox
|
||||
defaultChecked={data?.cooling_system}
|
||||
disabled={Boolean(modifyEnable)}
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{/* Hot Water System */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="hot_water_system"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Hot Water System</FormLabel>
|
||||
<FormControl>
|
||||
<Checkbox
|
||||
defaultChecked={data?.hot_water_system}
|
||||
disabled={Boolean(modifyEnable)}
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</fieldset>
|
||||
<div className="flex justify-evenly">
|
||||
{/* Site UUID */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="site_uu_id"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Site UUID</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} disabled={Boolean(modifyEnable)} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
This is your public display name.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* Address UUID */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="address_uu_id"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Address UUID</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} disabled={Boolean(modifyEnable)} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
This is your public display name.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{/* Building Type UUID */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="build_types_uu_id"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Building Type UUID</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} disabled={Boolean(modifyEnable)} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
This is your public display name.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{!modifyEnable && (
|
||||
<div className="flex justify-center items-center space-x-4 pt-4">
|
||||
<button
|
||||
type="submit"
|
||||
className="px-4 py-2 bg-indigo-600 text-white rounded w-1/3 hover:bg-white hover:text-indigo-700 transition-colors duration-300"
|
||||
>
|
||||
Submit
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
</Form>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default PeoplePageForm1;
|
||||
@@ -1,74 +0,0 @@
|
||||
import React from "react";
|
||||
import { ChevronFirst, ChevronLast, Plus } from "lucide-react";
|
||||
|
||||
interface InfoData {
|
||||
pagination: any;
|
||||
selectedId: string | null;
|
||||
setPagination: () => void;
|
||||
setIsCreate: () => void;
|
||||
}
|
||||
|
||||
|
||||
|
||||
const PeopleInfo1: React.FC<InfoData> = ({
|
||||
pagination,
|
||||
selectedId,
|
||||
setPagination,
|
||||
setIsCreate,
|
||||
}) => {
|
||||
console.log("PeopleInfo1", pagination, selectedId);
|
||||
return (
|
||||
<div>
|
||||
<div className="min-w-full mx-4 p-6 rounded-lg shadow-md m-6">
|
||||
<div className="flex justify-evenly items-center my-1">
|
||||
<h1 className="text-2xl font-bold ml-2">Individual Dashboard</h1>
|
||||
<h3 className="text-lg font-semibold text-gray-700 ml-2">
|
||||
Selected ID: {selectedId}
|
||||
</h3>
|
||||
<button
|
||||
onClick={() => setIsCreate()}
|
||||
className="bg-blue-500 text-white px-4 py-2 rounded-lg flex items-center gap-2 hover:bg-blue-600 transition-colors"
|
||||
>
|
||||
<Plus />
|
||||
Create New
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex justify-evenly items-center my-1">
|
||||
<h3 className="text-lg font-semibold text-gray-700 ml-2">
|
||||
Active Page: {pagination.page}
|
||||
</h3>
|
||||
<h3 className="text-lg font-semibold text-gray-700 ml-2">
|
||||
Size: {pagination.size}
|
||||
</h3>
|
||||
<h3 className="text-lg font-semibold text-gray-700 ml-2">
|
||||
Total Pages: {pagination.totalPages}
|
||||
</h3>
|
||||
<h3 className="text-lg font-semibold text-gray-700 ml-2">
|
||||
Order By: {JSON.stringify(pagination.orderField)}
|
||||
</h3>
|
||||
<h3 className="text-lg font-semibold text-gray-700 ml-2">
|
||||
Order Type: {JSON.stringify(pagination.orderType)}
|
||||
</h3>
|
||||
<h3 className="text-lg font-semibold text-gray-700 ml-2">
|
||||
All Count: {pagination.allCount}
|
||||
</h3>
|
||||
<h3 className="text-lg font-semibold text-gray-700 ml-2">
|
||||
Total Count: {pagination.totalCount}
|
||||
</h3>
|
||||
|
||||
<h3 className="text-lg font-semibold text-gray-700 ml-2">Page: 1</h3>
|
||||
<button className="bg-black text-white px-4 py-2 w-24 rounded-lg flex items-center gap-2 hover:bg-white hover:text-black transition-colors">
|
||||
Previous
|
||||
<ChevronFirst />
|
||||
</button>
|
||||
<button className="bg-black text-white px-4 py-2 w-24 rounded-lg flex items-center gap-2 hover:bg-white hover:text-black transition-colors">
|
||||
Next
|
||||
<ChevronLast />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PeopleInfo1;
|
||||
@@ -1,37 +0,0 @@
|
||||
"use client";
|
||||
import React from "react";
|
||||
import Card from "./card1";
|
||||
import { PeopleFormData } from "./peopleschema1";
|
||||
|
||||
interface PageData {
|
||||
data: PeopleFormData[];
|
||||
handleUpdateModify: (uu_id: string) => void;
|
||||
handleView: (uu_id: string) => void;
|
||||
}
|
||||
|
||||
const PeoplePage1: React.FC<PageData> = ({
|
||||
data,
|
||||
handleUpdateModify,
|
||||
handleView,
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<div className="grid gap-4">
|
||||
{data.map((item) => (
|
||||
<div key={`CardDiv-${item.uu_id}`}>
|
||||
<Card
|
||||
key={`Card-${item.uu_id}`}
|
||||
data={item}
|
||||
onUpdate={handleUpdateModify}
|
||||
onView={handleView}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default PeoplePage1;
|
||||
@@ -1,24 +0,0 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const PeopleSchema = z.object({
|
||||
uu_id: z.string(),
|
||||
created_at: z.string(),
|
||||
updated_at: z.string(),
|
||||
person_tag: z.string(),
|
||||
expiry_starts: z.string(),
|
||||
expiry_ends: z.string(),
|
||||
firstname: z.string(),
|
||||
middle_name: z.string(),
|
||||
surname: z.string(),
|
||||
birth_date: z.string(),
|
||||
birth_place: z.string(),
|
||||
sex_code: z.string(),
|
||||
country_code: z.string(),
|
||||
tax_no: z.string(),
|
||||
active: z.boolean(),
|
||||
deleted: z.boolean(),
|
||||
is_confirmed: z.boolean(),
|
||||
is_notification_send: z.boolean(),
|
||||
});
|
||||
|
||||
export type PeopleFormData = z.infer<typeof PeopleSchema>;
|
||||
@@ -0,0 +1,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,
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -1,127 +1,432 @@
|
||||
import React, { useState } from 'react';
|
||||
import { z } from 'zod';
|
||||
import { DataType, DataSchema } from './schema';
|
||||
import { getTranslation, LanguageKey } from './language';
|
||||
"use client";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { DataType, DataSchema } 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 {
|
||||
initialData?: Partial<DataType>;
|
||||
onSubmit: (data: DataType) => void;
|
||||
lang: LanguageKey;
|
||||
mode: "create" | "update" | "view";
|
||||
onCancel: () => void;
|
||||
readOnly?: boolean;
|
||||
lang?: LanguageKey;
|
||||
refetch: () => void;
|
||||
setMode: React.Dispatch<
|
||||
React.SetStateAction<"list" | "create" | "view" | "update">
|
||||
>;
|
||||
setSelectedItem: React.Dispatch<React.SetStateAction<DataType | null>>;
|
||||
initialData?: Partial<DataType>;
|
||||
}
|
||||
|
||||
export function FormComponent({
|
||||
initialData,
|
||||
onSubmit,
|
||||
onCancel,
|
||||
readOnly = false,
|
||||
lang = 'en'
|
||||
export function FormComponent({
|
||||
lang,
|
||||
mode,
|
||||
onCancel,
|
||||
refetch,
|
||||
setMode,
|
||||
setSelectedItem,
|
||||
initialData,
|
||||
}: FormComponentProps) {
|
||||
// Derive readOnly from mode
|
||||
const readOnly = mode === "view";
|
||||
const t = getTranslation(lang);
|
||||
const [formData, setFormData] = useState<Partial<DataType>>(initialData || {
|
||||
title: '',
|
||||
description: '',
|
||||
status: 'active',
|
||||
});
|
||||
const [errors, setErrors] = useState<Record<string, string>>({});
|
||||
const [formSubmitting, setFormSubmitting] = useState<boolean>(false);
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => {
|
||||
const { name, value } = e.target;
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
[name]: value,
|
||||
}));
|
||||
// Select the appropriate schema based on the mode
|
||||
const getSchemaForMode = () => {
|
||||
// You can implement different schemas for different modes
|
||||
return DataSchema;
|
||||
};
|
||||
|
||||
// Get default values directly from schema or initialData
|
||||
const getDefaultValues = (): Record<string, any> => {
|
||||
// For view and update modes, use initialData if available
|
||||
if ((mode === 'view' || mode === 'update') && initialData) {
|
||||
return initialData as Record<string, any>;
|
||||
}
|
||||
|
||||
// For create mode or when initialData is not available, use default values
|
||||
return {
|
||||
title: "",
|
||||
description: "",
|
||||
status: "active",
|
||||
};
|
||||
};
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
// Define form with react-hook-form and zod validation
|
||||
const form = useForm<DataType>({
|
||||
resolver: zodResolver(getSchemaForMode()),
|
||||
defaultValues: getDefaultValues(),
|
||||
mode: "onChange",
|
||||
});
|
||||
|
||||
// Update form values when initialData changes
|
||||
useEffect(() => {
|
||||
if (initialData) {
|
||||
Object.keys(initialData).forEach((key) => {
|
||||
form.setValue(
|
||||
key as keyof DataType,
|
||||
initialData[key as keyof DataType]
|
||||
);
|
||||
});
|
||||
}
|
||||
}, [initialData, form]);
|
||||
|
||||
// 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();
|
||||
};
|
||||
|
||||
// Define the submission handler function
|
||||
const onSubmitHandler = async (data: DataType) => {
|
||||
console.log("Submitting form data:", data, "Mode:", mode);
|
||||
setFormSubmitting(true);
|
||||
|
||||
try {
|
||||
// Validate with Zod
|
||||
const validData = DataSchema.parse(formData);
|
||||
onSubmit(validData);
|
||||
setErrors({});
|
||||
} catch (err) {
|
||||
if (err instanceof z.ZodError) {
|
||||
const fieldErrors: Record<string, string> = {};
|
||||
err.errors.forEach(error => {
|
||||
const field = error.path[0];
|
||||
fieldErrors[field as string] = error.message;
|
||||
});
|
||||
setErrors(fieldErrors);
|
||||
// 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 createData(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 updateData(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);
|
||||
}
|
||||
};
|
||||
|
||||
// Define field groups with their field types
|
||||
const fieldGroups = {
|
||||
basicInfo: [
|
||||
{ name: "id", type: "text" },
|
||||
{ name: "title", type: "text" },
|
||||
],
|
||||
detailsInfo: [{ name: "description", type: "textarea" }],
|
||||
statusInfo: [
|
||||
{ name: "status", type: "select" },
|
||||
{ name: "createdAt", type: "date" },
|
||||
],
|
||||
};
|
||||
|
||||
// Filter out fields based on readOnly mode
|
||||
if (!readOnly) {
|
||||
// Remove fields that shouldn't be editable
|
||||
fieldGroups.basicInfo = fieldGroups.basicInfo.filter(
|
||||
(field) => field.name !== "id"
|
||||
);
|
||||
fieldGroups.statusInfo = fieldGroups.statusInfo.filter(
|
||||
(field) => field.name !== "createdAt"
|
||||
);
|
||||
}
|
||||
|
||||
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-semibold mb-4">
|
||||
{readOnly ? t.view : initialData?.id ? t.update : t.createNew}
|
||||
</h2>
|
||||
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="mb-4">
|
||||
<label htmlFor="title" className="block text-sm font-medium mb-1">{t.formLabels.title}</label>
|
||||
<input
|
||||
type="text"
|
||||
id="title"
|
||||
name="title"
|
||||
value={formData.title || ''}
|
||||
onChange={handleChange}
|
||||
disabled={readOnly}
|
||||
className="w-full p-2 border rounded focus:ring-blue-500 focus:border-blue-500 disabled:bg-gray-100"
|
||||
/>
|
||||
{errors.title && <p className="text-red-500 text-sm mt-1">{errors.title}</p>}
|
||||
</div>
|
||||
|
||||
<div className="mb-4">
|
||||
<label htmlFor="description" className="block text-sm font-medium mb-1">{t.formLabels.description}</label>
|
||||
<textarea
|
||||
id="description"
|
||||
name="description"
|
||||
value={formData.description || ''}
|
||||
onChange={handleChange}
|
||||
disabled={readOnly}
|
||||
rows={3}
|
||||
className="w-full p-2 border rounded focus:ring-blue-500 focus:border-blue-500 disabled:bg-gray-100"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mb-4">
|
||||
<label htmlFor="status" className="block text-sm font-medium mb-1">{t.formLabels.status}</label>
|
||||
<select
|
||||
id="status"
|
||||
name="status"
|
||||
value={formData.status || 'active'}
|
||||
onChange={handleChange}
|
||||
disabled={readOnly}
|
||||
className="w-full p-2 border rounded focus:ring-blue-500 focus:border-blue-500 disabled:bg-gray-100"
|
||||
>
|
||||
<option value="active">{t.status.active}</option>
|
||||
<option value="inactive">{t.status.inactive}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end space-x-3 mt-6">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onCancel}
|
||||
className="px-4 py-2 bg-gray-200 rounded hover:bg-gray-300"
|
||||
>
|
||||
{readOnly ? t.back : t.cancel}
|
||||
</button>
|
||||
|
||||
{!readOnly && (
|
||||
<button
|
||||
type="submit"
|
||||
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
|
||||
|
||||
<Form {...form}>
|
||||
<div className="space-y-6">
|
||||
{/* Basic Information Section */}
|
||||
<div className="bg-gray-50 p-4 rounded-md">
|
||||
<h3 className="text-lg font-medium mb-3">Basic Information</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{fieldGroups.basicInfo.map((field) => {
|
||||
const label =
|
||||
t.formLabels[field.name as keyof typeof t.formLabels] ||
|
||||
field.name;
|
||||
|
||||
return (
|
||||
<FormField
|
||||
key={field.name}
|
||||
control={form.control}
|
||||
name={field.name as any}
|
||||
render={({ field: formField }) => (
|
||||
<FormItem
|
||||
className={
|
||||
field.type === "textarea" ? "col-span-2" : ""
|
||||
}
|
||||
>
|
||||
{field.type !== "checkbox" && (
|
||||
<FormLabel>{label}</FormLabel>
|
||||
)}
|
||||
<FormControl>
|
||||
{field.type === "checkbox" ? (
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
checked={formField.value as boolean}
|
||||
onCheckedChange={formField.onChange}
|
||||
disabled={readOnly}
|
||||
id={field.name}
|
||||
/>
|
||||
<label
|
||||
htmlFor={field.name}
|
||||
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||||
>
|
||||
{label}
|
||||
</label>
|
||||
</div>
|
||||
) : field.type === "textarea" ? (
|
||||
<textarea
|
||||
{...formField}
|
||||
value={formField.value || ""}
|
||||
disabled={readOnly}
|
||||
rows={3}
|
||||
className={cn(
|
||||
"w-full p-2 border rounded focus:ring-blue-500 focus:border-blue-500",
|
||||
readOnly && "bg-gray-100"
|
||||
)}
|
||||
/>
|
||||
) : field.type === "select" ? (
|
||||
<select
|
||||
{...formField}
|
||||
disabled={readOnly}
|
||||
className={cn(
|
||||
"w-full p-2 border rounded focus:ring-blue-500 focus:border-blue-500",
|
||||
readOnly && "bg-gray-100"
|
||||
)}
|
||||
>
|
||||
<option value="active">{t.status.active}</option>
|
||||
<option value="inactive">
|
||||
{t.status.inactive}
|
||||
</option>
|
||||
</select>
|
||||
) : field.type === "date" ? (
|
||||
<Input
|
||||
type="date"
|
||||
{...formField}
|
||||
value={
|
||||
typeof formField.value === "string"
|
||||
? formField.value
|
||||
: ""
|
||||
}
|
||||
disabled={readOnly}
|
||||
className={cn(
|
||||
"w-full",
|
||||
readOnly && "bg-gray-100"
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
<Input
|
||||
{...formField}
|
||||
value={formField.value || ""}
|
||||
disabled={readOnly}
|
||||
className={cn(
|
||||
"w-full",
|
||||
readOnly && "bg-gray-100"
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Details Information Section */}
|
||||
<div className="bg-blue-50 p-4 rounded-md">
|
||||
<h3 className="text-lg font-medium mb-3">Details</h3>
|
||||
<div className="grid grid-cols-1 gap-4">
|
||||
{fieldGroups.detailsInfo.map((field) => {
|
||||
const label =
|
||||
t.formLabels[field.name as keyof typeof t.formLabels] ||
|
||||
field.name;
|
||||
|
||||
return (
|
||||
<FormField
|
||||
key={field.name}
|
||||
control={form.control}
|
||||
name={field.name as any}
|
||||
render={({ field: formField }) => (
|
||||
<FormItem className="col-span-2">
|
||||
<FormLabel>{label}</FormLabel>
|
||||
<FormControl>
|
||||
<textarea
|
||||
{...formField}
|
||||
value={formField.value || ""}
|
||||
disabled={readOnly}
|
||||
rows={3}
|
||||
className={cn(
|
||||
"w-full p-2 border rounded focus:ring-blue-500 focus:border-blue-500",
|
||||
readOnly && "bg-gray-100"
|
||||
)}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
{t.formDescriptions?.[
|
||||
field.name as keyof typeof t.formLabels
|
||||
] || ""}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Status Information Section */}
|
||||
<div className="bg-green-50 p-4 rounded-md">
|
||||
<h3 className="text-lg font-medium mb-3">Status</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{fieldGroups.statusInfo.map((field) => {
|
||||
const label =
|
||||
t.formLabels[field.name as keyof typeof t.formLabels] ||
|
||||
field.name;
|
||||
|
||||
return (
|
||||
<FormField
|
||||
key={field.name}
|
||||
control={form.control}
|
||||
name={field.name as any}
|
||||
render={({ field: formField }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{label}</FormLabel>
|
||||
<FormControl>
|
||||
{field.type === "select" ? (
|
||||
<select
|
||||
{...formField}
|
||||
disabled={readOnly}
|
||||
className={cn(
|
||||
"w-full p-2 border rounded focus:ring-blue-500 focus:border-blue-500",
|
||||
readOnly && "bg-gray-100"
|
||||
)}
|
||||
>
|
||||
<option value="active">{t.status.active}</option>
|
||||
<option value="inactive">
|
||||
{t.status.inactive}
|
||||
</option>
|
||||
</select>
|
||||
) : (
|
||||
<Input
|
||||
type="date"
|
||||
{...formField}
|
||||
value={
|
||||
typeof formField.value === "string"
|
||||
? formField.value
|
||||
: ""
|
||||
}
|
||||
disabled={readOnly}
|
||||
className={cn(
|
||||
"w-full",
|
||||
readOnly && "bg-gray-100"
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
{t.formDescriptions?.[
|
||||
field.name as keyof typeof t.formLabels
|
||||
] || ""}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Buttons Section */}
|
||||
<div className="flex justify-end space-x-3 mt-6">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={handleCancel}
|
||||
>
|
||||
{initialData?.id ? t.buttons.update : t.buttons.create}
|
||||
</button>
|
||||
)}
|
||||
{readOnly ? t.back : t.cancel}
|
||||
</Button>
|
||||
|
||||
{!readOnly && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="default"
|
||||
disabled={formSubmitting || readOnly}
|
||||
onClick={form.handleSubmit(onSubmitHandler)}
|
||||
>
|
||||
{mode === "update" ? t.buttons.update : t.buttons.create}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { z } from "zod";
|
||||
import { DataSchema } from "./schema";
|
||||
import { getTranslation, LanguageKey } from "./language";
|
||||
|
||||
@@ -99,19 +100,24 @@ export function SearchComponent({
|
||||
{t.searchFields || "Search in fields"}:
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{Object.keys(DataSchema.shape).map((field) => (
|
||||
<button
|
||||
key={field}
|
||||
onClick={() => toggleField(field)}
|
||||
className={`px-3 py-1 text-sm rounded ${
|
||||
activeFields.includes(field)
|
||||
? "bg-blue-500 text-white"
|
||||
: "bg-gray-200 text-gray-700"
|
||||
}`}
|
||||
>
|
||||
{field}
|
||||
</button>
|
||||
))}
|
||||
{Object.entries(DataSchema.shape).map(([field, fieldSchema]) => {
|
||||
// Skip boolean fields as they're not suitable for text search
|
||||
if (fieldSchema instanceof z.ZodBoolean) return null;
|
||||
|
||||
return (
|
||||
<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>
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React from "react";
|
||||
import { z } from "zod";
|
||||
import { DataSchema } from "./schema";
|
||||
import { getTranslation, LanguageKey } from "./language";
|
||||
import { PagePagination } from "@/components/validations/list/paginations";
|
||||
@@ -49,7 +50,10 @@ export function SortingComponent({
|
||||
<div className="flex items-center space-x-2">
|
||||
<label className="text-sm font-medium">{t.sortBy}</label>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{Object.keys(DataSchema.shape).map((field) => {
|
||||
{Object.entries(DataSchema.shape).map(([field, fieldSchema]) => {
|
||||
// Skip boolean fields as they're not suitable for sorting
|
||||
if (fieldSchema instanceof z.ZodBoolean) return null;
|
||||
|
||||
// Find if this field is in the orderFields array
|
||||
const fieldIndex = pagination.orderFields.indexOf(field);
|
||||
const isActive = fieldIndex !== -1;
|
||||
|
||||
@@ -25,11 +25,7 @@ function TemplateApp({ lang = "en" }: TemplateProps) {
|
||||
);
|
||||
const [selectedItem, setSelectedItem] = useState<DataType | null>(null);
|
||||
|
||||
const handleCreateClick = () => {
|
||||
setSelectedItem(null);
|
||||
setMode("create");
|
||||
};
|
||||
|
||||
// These functions are used by the DataDisplayComponent to handle item actions
|
||||
const handleViewClick = (item: DataType) => {
|
||||
setSelectedItem(item);
|
||||
setMode("view");
|
||||
@@ -39,21 +35,11 @@ function TemplateApp({ lang = "en" }: TemplateProps) {
|
||||
setSelectedItem(item);
|
||||
setMode("update");
|
||||
};
|
||||
|
||||
const handleFormSubmit = async (data: DataType) => {
|
||||
console.log("Submitting form data:", data);
|
||||
// Here you would call your API to save the data
|
||||
// await saveData(data);
|
||||
|
||||
// After saving, refresh the list and go back to list view
|
||||
setMode("list");
|
||||
setSelectedItem(null);
|
||||
refetch();
|
||||
};
|
||||
|
||||
const handleFormCancel = () => {
|
||||
setMode("list");
|
||||
|
||||
// Function to handle the create button click
|
||||
const handleCreateClick = () => {
|
||||
setSelectedItem(null);
|
||||
setMode("create");
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -107,14 +93,18 @@ function TemplateApp({ lang = "en" }: TemplateProps) {
|
||||
/>
|
||||
)}
|
||||
|
||||
{(mode === "create" || mode === "update" || mode === "view") && (
|
||||
<FormComponent
|
||||
initialData={selectedItem || undefined}
|
||||
onSubmit={handleFormSubmit}
|
||||
onCancel={handleFormCancel}
|
||||
readOnly={mode === "view"}
|
||||
lang={lang}
|
||||
/>
|
||||
{mode !== "list" && (
|
||||
<div>
|
||||
<FormComponent
|
||||
initialData={selectedItem || undefined}
|
||||
lang={lang}
|
||||
mode={mode}
|
||||
refetch={refetch}
|
||||
setMode={setMode}
|
||||
setSelectedItem={setSelectedItem}
|
||||
onCancel={() => {}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -17,6 +17,8 @@ const language = {
|
||||
itemsPerPage: "Items per page:",
|
||||
sortBy: "Sort by:",
|
||||
loading: "Loading...",
|
||||
creating: "Creating",
|
||||
updating: "Updating",
|
||||
error: "Error loading data:",
|
||||
showing: "Showing",
|
||||
items: "items",
|
||||
@@ -32,6 +34,12 @@ const language = {
|
||||
status: "Status",
|
||||
createdAt: "Created",
|
||||
},
|
||||
formDescriptions: {
|
||||
title: "Enter a descriptive title",
|
||||
description: "Provide detailed information",
|
||||
status: "Select the current status",
|
||||
createdAt: "Date when the item was created",
|
||||
},
|
||||
status: {
|
||||
active: "Active",
|
||||
inactive: "Inactive",
|
||||
@@ -61,6 +69,8 @@ const language = {
|
||||
itemsPerPage: "Sayfa başına kayıt:",
|
||||
sortBy: "Sırala:",
|
||||
loading: "Yükleniyor...",
|
||||
creating: "Oluşturuluyor",
|
||||
updating: "Güncelleniyor",
|
||||
error: "Veri yüklenirken hata:",
|
||||
showing: "Gösteriliyor",
|
||||
items: "kayıtlar",
|
||||
@@ -76,6 +86,12 @@ const language = {
|
||||
status: "Durum",
|
||||
createdAt: "Oluşturulma",
|
||||
},
|
||||
formDescriptions: {
|
||||
title: "Açıklayıcı bir başlık girin",
|
||||
description: "Detaylı bilgi sağlayın",
|
||||
status: "Mevcut durumu seçin",
|
||||
createdAt: "Öğenin oluşturulduğu tarih",
|
||||
},
|
||||
status: {
|
||||
active: "Aktif",
|
||||
inactive: "Pasif",
|
||||
|
||||
@@ -1,18 +1,98 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { PagePagination } from "@/components/validations/list/paginations";
|
||||
|
||||
// Define the data schema using Zod
|
||||
// Base schema with all possible fields
|
||||
export const DataSchema = z.object({
|
||||
id: z.string(),
|
||||
id: z.string().optional(),
|
||||
title: z.string(),
|
||||
description: z.string().optional(),
|
||||
status: z.string(),
|
||||
createdAt: z.string().or(z.date()),
|
||||
createdAt: z.string().or(z.date()).optional(),
|
||||
updatedAt: z.string().or(z.date()).optional(),
|
||||
// Add more fields as needed
|
||||
});
|
||||
|
||||
// Schema for creating new records
|
||||
export const CreateDataSchema = DataSchema.omit({
|
||||
id: true,
|
||||
createdAt: true,
|
||||
updatedAt: true
|
||||
});
|
||||
|
||||
// Schema for updating existing records
|
||||
export const UpdateDataSchema = DataSchema.omit({
|
||||
createdAt: true,
|
||||
updatedAt: true
|
||||
});
|
||||
|
||||
// Schema for viewing records
|
||||
export const ViewDataSchema = DataSchema;
|
||||
|
||||
// Type definitions
|
||||
export type DataType = z.infer<typeof DataSchema>;
|
||||
export type CreateDataType = z.infer<typeof CreateDataSchema>;
|
||||
export type UpdateDataType = z.infer<typeof UpdateDataSchema>;
|
||||
export type ViewDataType = z.infer<typeof ViewDataSchema>;
|
||||
|
||||
// Base field definitions with common properties
|
||||
const baseFieldDefinitions = {
|
||||
id: { type: "text", group: "identificationInfo", label: "ID" },
|
||||
title: { type: "text", group: "basicInfo", label: "Title" },
|
||||
description: { type: "text", group: "basicInfo", label: "Description" },
|
||||
status: { type: "text", group: "statusInfo", label: "Status" },
|
||||
createdAt: { type: "date", group: "systemInfo", label: "Created At" },
|
||||
updatedAt: { type: "date", group: "systemInfo", label: "Updated At" },
|
||||
};
|
||||
|
||||
// Field definitions for create mode
|
||||
export const createFieldDefinitions = {
|
||||
title: { ...baseFieldDefinitions.title, readOnly: false, required: true, defaultValue: "" },
|
||||
description: { ...baseFieldDefinitions.description, readOnly: false, required: false, defaultValue: "" },
|
||||
status: { ...baseFieldDefinitions.status, readOnly: false, required: true, defaultValue: "active" },
|
||||
};
|
||||
|
||||
// Field definitions for update mode
|
||||
export const updateFieldDefinitions = {
|
||||
id: { ...baseFieldDefinitions.id, readOnly: true, required: true, defaultValue: "" },
|
||||
title: { ...baseFieldDefinitions.title, readOnly: false, required: true, defaultValue: "" },
|
||||
description: { ...baseFieldDefinitions.description, readOnly: false, required: false, defaultValue: "" },
|
||||
status: { ...baseFieldDefinitions.status, readOnly: false, required: true, defaultValue: "active" },
|
||||
createdAt: { ...baseFieldDefinitions.createdAt, readOnly: true, required: false, defaultValue: "" },
|
||||
updatedAt: { ...baseFieldDefinitions.updatedAt, readOnly: true, required: false, defaultValue: "" },
|
||||
};
|
||||
|
||||
// Field definitions for view mode
|
||||
export const viewFieldDefinitions = {
|
||||
id: { ...baseFieldDefinitions.id, readOnly: true, required: false, defaultValue: "" },
|
||||
title: { ...baseFieldDefinitions.title, readOnly: true, required: false, defaultValue: "" },
|
||||
description: { ...baseFieldDefinitions.description, readOnly: true, required: false, defaultValue: "" },
|
||||
status: { ...baseFieldDefinitions.status, readOnly: true, required: false, defaultValue: "active" },
|
||||
createdAt: { ...baseFieldDefinitions.createdAt, readOnly: true, required: false, defaultValue: "" },
|
||||
updatedAt: { ...baseFieldDefinitions.updatedAt, readOnly: true, required: false, defaultValue: "" },
|
||||
};
|
||||
|
||||
// Field definitions manager
|
||||
export const fieldDefinitions = {
|
||||
getDefinitionsByMode: (mode: "create" | "update" | "view") => {
|
||||
switch (mode) {
|
||||
case "create":
|
||||
return createFieldDefinitions;
|
||||
case "update":
|
||||
return updateFieldDefinitions;
|
||||
case "view":
|
||||
return viewFieldDefinitions;
|
||||
default:
|
||||
return createFieldDefinitions;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 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),
|
||||
};
|
||||
|
||||
// Mock API function (replace with your actual API call)
|
||||
export const fetchData = async ({
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
"use client";
|
||||
import React from "react";
|
||||
import { loginSelectEmployee } from "@/apicalls/login/login";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { Company } from "./types";
|
||||
|
||||
interface LoginEmployeeProps {
|
||||
selectionList: Company[];
|
||||
lang?: "en" | "tr";
|
||||
onSelect?: (uu_id: string) => void;
|
||||
}
|
||||
|
||||
// Language dictionary for internationalization
|
||||
const languageDictionary = {
|
||||
tr: {
|
||||
companySelection: "Şirket Seçimi",
|
||||
loggedInAs: "Çalışan olarak giriş yaptınız",
|
||||
duty: "Görev",
|
||||
id: "Kimlik",
|
||||
noSelections: "Seçenek bulunamadı",
|
||||
},
|
||||
en: {
|
||||
companySelection: "Select your company",
|
||||
loggedInAs: "You are logged in as an employee",
|
||||
duty: "Duty",
|
||||
id: "ID",
|
||||
noSelections: "No selections available",
|
||||
},
|
||||
};
|
||||
|
||||
function LoginEmployee({
|
||||
selectionList,
|
||||
lang = "en",
|
||||
onSelect,
|
||||
}: LoginEmployeeProps) {
|
||||
const t = languageDictionary[lang] || languageDictionary.en;
|
||||
const router = useRouter();
|
||||
|
||||
const handleSelect = (uu_id: string) => {
|
||||
console.log("Selected employee uu_id:", uu_id);
|
||||
|
||||
// If an external onSelect handler is provided, use it
|
||||
if (onSelect) {
|
||||
onSelect(uu_id);
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise use the internal handler
|
||||
loginSelectEmployee({ company_uu_id: uu_id })
|
||||
.then((responseData: any) => {
|
||||
if (responseData?.status === 200 || responseData?.status === 202) {
|
||||
router.push("/dashboard");
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="text-2xl font-bold">{t.companySelection}</div>
|
||||
<div className="text-sm text-gray-500 mt-4">{t.loggedInAs}</div>
|
||||
|
||||
{Array.isArray(selectionList) && selectionList.length === 0 && (
|
||||
<div className="text-center p-4">{t.noSelections}</div>
|
||||
)}
|
||||
|
||||
{Array.isArray(selectionList) && selectionList.length === 1 && (
|
||||
<div className="w-full p-4 m-2 bg-emerald-300 rounded-lg">
|
||||
<div className="flex flex-col items-center md:items-start">
|
||||
<div>
|
||||
<span className="text-2xl font-medium">
|
||||
{selectionList[0].public_name}
|
||||
</span>
|
||||
{selectionList[0].company_type && (
|
||||
<span className="ml-2 font-medium text-sky-500">
|
||||
{selectionList[0].company_type}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{selectionList[0].duty && (
|
||||
<div className="mt-1">
|
||||
<span className="text-md font-medium text-gray-700">
|
||||
{t.duty}: {selectionList[0].duty}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="mt-2">
|
||||
<span className="flex gap-2 font-medium text-gray-600 dark:text-gray-400 text-sm">
|
||||
<span>
|
||||
{t.id}: {selectionList[0].uu_id}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-3">
|
||||
<button
|
||||
className="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"
|
||||
onClick={() => handleSelect(selectionList[0].uu_id)}
|
||||
>
|
||||
Continue
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{Array.isArray(selectionList) &&
|
||||
selectionList.length > 1 &&
|
||||
selectionList.map((item: Company, index: number) => (
|
||||
<div
|
||||
key={index}
|
||||
className="w-full p-4 m-2 bg-emerald-300 hover:bg-emerald-500 rounded-lg transition-colors duration-200 cursor-pointer"
|
||||
onClick={() => handleSelect(item.uu_id)}
|
||||
>
|
||||
<div className="flex flex-col items-center md:items-start">
|
||||
<div>
|
||||
<span className="text-2xl font-medium">{item.public_name}</span>
|
||||
{item.company_type && (
|
||||
<span className="ml-2 font-medium text-sky-500">
|
||||
{item.company_type}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{item.duty && (
|
||||
<div className="mt-1">
|
||||
<span className="text-md font-medium text-gray-700">
|
||||
{t.duty}: {item.duty}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="mt-2">
|
||||
<span className="flex gap-2 font-medium text-gray-600 dark:text-gray-400 text-sm">
|
||||
<span>
|
||||
{t.id}: {item.uu_id}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default LoginEmployee;
|
||||
@@ -0,0 +1,111 @@
|
||||
"use client";
|
||||
import React from "react";
|
||||
import { loginSelectOccupant } from "@/apicalls/login/login";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { BuildingMap } from "./types";
|
||||
|
||||
interface LoginOccupantProps {
|
||||
selectionList: BuildingMap;
|
||||
lang?: "en" | "tr";
|
||||
}
|
||||
|
||||
// Language dictionary for internationalization
|
||||
const languageDictionary = {
|
||||
tr: {
|
||||
occupantSelection: "Daire Seçimi",
|
||||
loggedInAs: "Kiracı olarak giriş yaptınız",
|
||||
buildingInfo: "Bina Bilgisi",
|
||||
level: "Kat",
|
||||
noSelections: "Seçenek bulunamadı",
|
||||
},
|
||||
en: {
|
||||
occupantSelection: "Select your occupant type",
|
||||
loggedInAs: "You are logged in as an occupant",
|
||||
buildingInfo: "Building Info",
|
||||
level: "Level",
|
||||
noSelections: "No selections available",
|
||||
},
|
||||
};
|
||||
|
||||
function LoginOccupant({
|
||||
selectionList,
|
||||
lang = "en"
|
||||
}: LoginOccupantProps) {
|
||||
const t = languageDictionary[lang] || languageDictionary.en;
|
||||
const router = useRouter();
|
||||
|
||||
const handleSelect = (uu_id: string) => {
|
||||
console.log("Selected occupant uu_id:", uu_id);
|
||||
|
||||
loginSelectOccupant({
|
||||
build_living_space_uu_id: uu_id,
|
||||
})
|
||||
.then((responseData: any) => {
|
||||
if (responseData?.status === 200 || responseData?.status === 202) {
|
||||
router.push("/dashboard");
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="text-2xl font-bold">{t.occupantSelection}</div>
|
||||
<div className="text-sm text-gray-500 mt-4">
|
||||
{t.loggedInAs}
|
||||
</div>
|
||||
{selectionList && Object.keys(selectionList).length > 0 ? (
|
||||
Object.keys(selectionList).map((buildKey: string) => {
|
||||
const building = selectionList[buildKey];
|
||||
return (
|
||||
<div key={buildKey} className="mb-6">
|
||||
<div className="w-full p-3 bg-blue-100 rounded-t-lg">
|
||||
<h3 className="text-lg font-medium text-blue-800">
|
||||
<span className="mr-1">{t.buildingInfo}:</span>
|
||||
{building.build_name} - No: {building.build_no}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2 mt-2">
|
||||
{building.occupants.map((occupant: any, idx: number) => (
|
||||
<div
|
||||
key={`${buildKey}-${idx}`}
|
||||
className="w-full p-4 bg-emerald-300 hover:bg-emerald-500 rounded-lg transition-colors duration-200 cursor-pointer"
|
||||
onClick={() => handleSelect(occupant.build_living_space_uu_id)}
|
||||
>
|
||||
<div className="flex flex-col">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xl font-medium">
|
||||
{occupant.description}
|
||||
</span>
|
||||
<span className="text-sm font-medium bg-blue-500 text-white px-2 py-1 rounded">
|
||||
{occupant.code}
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-2">
|
||||
<span className="text-md font-medium">
|
||||
{occupant.part_name}
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-1">
|
||||
<span className="text-sm text-gray-700">
|
||||
{t.level}: {occupant.part_level}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<div className="text-center p-4">{t.noSelections}</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default LoginOccupant;
|
||||
@@ -1,28 +1,39 @@
|
||||
"use client";
|
||||
import React from "react";
|
||||
|
||||
import {
|
||||
loginSelectEmployee,
|
||||
loginSelectOccupant,
|
||||
} from "@/apicalls/login/login";
|
||||
import { useRouter } from "next/navigation";
|
||||
import LoginEmployee from "./LoginEmployee";
|
||||
import LoginOccupant from "./LoginOccupant";
|
||||
import { SelectListProps, Company, BuildingMap } from "./types";
|
||||
|
||||
function SelectList({
|
||||
selectionList,
|
||||
isEmployee,
|
||||
isOccupant,
|
||||
}: {
|
||||
selectionList: {
|
||||
uu_id: string;
|
||||
public_name: string;
|
||||
company_type: string;
|
||||
company_address: string;
|
||||
}[];
|
||||
isEmployee: boolean;
|
||||
isOccupant: boolean;
|
||||
}) {
|
||||
lang = "en",
|
||||
}: SelectListProps) {
|
||||
const router = useRouter();
|
||||
|
||||
// Log the complete selectionList object and its structure
|
||||
console.log("selectionList (complete):", selectionList);
|
||||
console.log(
|
||||
"selectionList (type):",
|
||||
Array.isArray(selectionList) ? "Array" : "Object"
|
||||
);
|
||||
|
||||
if (isEmployee && Array.isArray(selectionList)) {
|
||||
console.log("Employee companies:", selectionList);
|
||||
} else if (isOccupant && !Array.isArray(selectionList)) {
|
||||
// Log each building and its occupants
|
||||
Object.entries(selectionList).forEach(([buildingKey, building]) => {
|
||||
console.log(`Building ${buildingKey}:`, building);
|
||||
console.log(`Occupants for building ${buildingKey}:`, building.occupants);
|
||||
});
|
||||
}
|
||||
|
||||
const setSelectionHandler = (uu_id: string) => {
|
||||
if (isEmployee) {
|
||||
console.log("Selected isEmployee uu_id:", uu_id);
|
||||
@@ -37,7 +48,10 @@ function SelectList({
|
||||
});
|
||||
} else if (isOccupant) {
|
||||
console.log("Selected isOccupant uu_id:", uu_id);
|
||||
loginSelectOccupant({ build_living_space_uu_id: uu_id })
|
||||
// For occupants, the uu_id is a composite of buildKey|partUuid
|
||||
loginSelectOccupant({
|
||||
build_living_space_uu_id: uu_id,
|
||||
})
|
||||
.then((responseData: any) => {
|
||||
if (responseData?.status === 200 || responseData?.status === 202) {
|
||||
router.push("/dashboard");
|
||||
@@ -51,33 +65,22 @@ function SelectList({
|
||||
|
||||
return (
|
||||
<>
|
||||
{selectionList.map((item: any, index: number) => (
|
||||
<div
|
||||
key={index}
|
||||
className="w-full p-4 m-2 bg-emerald-300 hover:bg-emerald-500 rounded-lg transition-colors duration-200 cursor-pointer"
|
||||
onClick={() => setSelectionHandler(item.uu_id)}
|
||||
>
|
||||
<div className="flex flex-col items-center md:items-start">
|
||||
<div>
|
||||
<span className="text-2xl font-medium">{item.public_name}</span>
|
||||
<span className="font-medium text-sky-500">
|
||||
{item.company_type}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="flex gap-2 font-medium text-gray-600 dark:text-gray-400">
|
||||
<span>{item.uu_id}</span>
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="flex gap-2 font-medium text-gray-600 dark:text-gray-400">
|
||||
<span>{item.company_address}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
{isEmployee && Array.isArray(selectionList) && (
|
||||
<LoginEmployee
|
||||
selectionList={selectionList as Company[]}
|
||||
lang={lang as "en" | "tr"}
|
||||
onSelect={setSelectionHandler}
|
||||
/>
|
||||
)}
|
||||
|
||||
{isOccupant && !Array.isArray(selectionList) && (
|
||||
<LoginOccupant
|
||||
selectionList={selectionList as BuildingMap}
|
||||
lang={lang as "en" | "tr"}
|
||||
onSelect={setSelectionHandler}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
36
WebServices/client-frontend/src/components/auth/types.ts
Normal file
36
WebServices/client-frontend/src/components/auth/types.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
// TypeScript interfaces for proper type checking
|
||||
export interface Company {
|
||||
uu_id: string;
|
||||
public_name: string;
|
||||
company_type?: string;
|
||||
company_address?: any;
|
||||
duty?: string;
|
||||
}
|
||||
|
||||
export interface Occupant {
|
||||
build_living_space_uu_id: string;
|
||||
part_uu_id: string;
|
||||
part_name: string;
|
||||
part_level: number;
|
||||
occupant_uu_id: string;
|
||||
description: string;
|
||||
code: string;
|
||||
}
|
||||
|
||||
export interface Building {
|
||||
build_uu_id: string;
|
||||
build_name: string;
|
||||
build_no: string;
|
||||
occupants: Occupant[];
|
||||
}
|
||||
|
||||
export interface BuildingMap {
|
||||
[key: string]: Building;
|
||||
}
|
||||
|
||||
export interface SelectListProps {
|
||||
selectionList: Company[] | BuildingMap;
|
||||
isEmployee: boolean;
|
||||
isOccupant: boolean;
|
||||
lang?: "en" | "tr";
|
||||
}
|
||||
407
WebServices/client-frontend/src/components/header/Header.tsx
Normal file
407
WebServices/client-frontend/src/components/header/Header.tsx
Normal file
@@ -0,0 +1,407 @@
|
||||
"use client";
|
||||
import React, { useState, useRef, useEffect } from "react";
|
||||
import {
|
||||
ChevronDown,
|
||||
LogOut,
|
||||
Settings,
|
||||
User,
|
||||
Bell,
|
||||
MessageSquare,
|
||||
X,
|
||||
} from "lucide-react";
|
||||
import { searchPlaceholder, menuLanguage } from "@/app/commons/pageDefaults";
|
||||
import { logoutActiveSession } from "@/apicalls/login/login";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
interface HeaderProps {
|
||||
lang: "en" | "tr";
|
||||
}
|
||||
|
||||
// Language dictionary for the dropdown menu
|
||||
const dropdownLanguage = {
|
||||
en: {
|
||||
profile: "Profile",
|
||||
settings: "Settings",
|
||||
logout: "Logout",
|
||||
notifications: "Notifications",
|
||||
messages: "Messages",
|
||||
viewAll: "View all",
|
||||
noNotifications: "No notifications",
|
||||
noMessages: "No messages",
|
||||
markAllAsRead: "Mark all as read",
|
||||
},
|
||||
tr: {
|
||||
profile: "Profil",
|
||||
settings: "Ayarlar",
|
||||
logout: "Çıkış",
|
||||
notifications: "Bildirimler",
|
||||
messages: "Mesajlar",
|
||||
viewAll: "Tümünü gör",
|
||||
noNotifications: "Bildirim yok",
|
||||
noMessages: "Mesaj yok",
|
||||
markAllAsRead: "Tümünü okundu olarak işaretle",
|
||||
},
|
||||
};
|
||||
|
||||
// Mock data for notifications
|
||||
const mockNotifications = [
|
||||
{
|
||||
id: 1,
|
||||
title: "New update available",
|
||||
description: "System update v2.4.1 is now available",
|
||||
time: new Date(2025, 3, 19, 14, 30),
|
||||
read: false,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: "Meeting reminder",
|
||||
description: "Team meeting in 30 minutes",
|
||||
time: new Date(2025, 3, 19, 13, 45),
|
||||
read: false,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: "Task completed",
|
||||
description: "Project X has been completed successfully",
|
||||
time: new Date(2025, 3, 18, 16, 20),
|
||||
read: true,
|
||||
},
|
||||
];
|
||||
|
||||
// Mock data for messages
|
||||
const mockMessages = [
|
||||
{
|
||||
id: 1,
|
||||
sender: "John Doe",
|
||||
message: "Hi there! Can we discuss the project details?",
|
||||
time: new Date(2025, 3, 19, 15, 10),
|
||||
read: false,
|
||||
avatar: "JD",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
sender: "Jane Smith",
|
||||
message: "Please review the latest documents I sent",
|
||||
time: new Date(2025, 3, 19, 12, 5),
|
||||
read: false,
|
||||
avatar: "JS",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
sender: "Mike Johnson",
|
||||
message: "Thanks for your help yesterday!",
|
||||
time: new Date(2025, 3, 18, 9, 45),
|
||||
read: true,
|
||||
avatar: "MJ",
|
||||
},
|
||||
];
|
||||
|
||||
const Header: React.FC<HeaderProps> = ({ lang }) => {
|
||||
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
|
||||
const [isNotificationsOpen, setIsNotificationsOpen] = useState(false);
|
||||
const [isMessagesOpen, setIsMessagesOpen] = useState(false);
|
||||
const [notifications, setNotifications] = useState(mockNotifications);
|
||||
const [messages, setMessages] = useState(mockMessages);
|
||||
|
||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||
const notificationsRef = useRef<HTMLDivElement>(null);
|
||||
const messagesRef = useRef<HTMLDivElement>(null);
|
||||
const t = dropdownLanguage[lang] || dropdownLanguage.en;
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
// Close dropdowns when clicking outside
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (
|
||||
dropdownRef.current &&
|
||||
!dropdownRef.current.contains(event.target as Node)
|
||||
) {
|
||||
setIsDropdownOpen(false);
|
||||
}
|
||||
if (
|
||||
notificationsRef.current &&
|
||||
!notificationsRef.current.contains(event.target as Node)
|
||||
) {
|
||||
setIsNotificationsOpen(false);
|
||||
}
|
||||
if (
|
||||
messagesRef.current &&
|
||||
!messagesRef.current.contains(event.target as Node)
|
||||
) {
|
||||
setIsMessagesOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener("mousedown", handleClickOutside);
|
||||
return () => {
|
||||
document.removeEventListener("mousedown", handleClickOutside);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleLogout = () => {
|
||||
// Implement logout functionality
|
||||
console.log("Logging out...");
|
||||
logoutActiveSession()
|
||||
.then(() => {
|
||||
console.log("Logout successful");
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Logout error:", error);
|
||||
})
|
||||
.finally(() => {
|
||||
setIsDropdownOpen(false);
|
||||
router.replace("/auth/login");
|
||||
});
|
||||
};
|
||||
|
||||
const markAllNotificationsAsRead = () => {
|
||||
setNotifications(
|
||||
notifications.map((notification) => ({
|
||||
...notification,
|
||||
read: true,
|
||||
}))
|
||||
);
|
||||
};
|
||||
|
||||
const markAllMessagesAsRead = () => {
|
||||
setMessages(
|
||||
messages.map((message) => ({
|
||||
...message,
|
||||
read: true,
|
||||
}))
|
||||
);
|
||||
};
|
||||
|
||||
// Format date to display in a user-friendly way
|
||||
const formatDate = (date: Date) => {
|
||||
return date.toLocaleString(lang === "tr" ? "tr-TR" : "en-US", {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
day: "2-digit",
|
||||
month: "2-digit",
|
||||
year: "numeric",
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<header className="sticky top-0 bg-white shadow-md z-10 p-4 flex justify-between items-center">
|
||||
<h1 className="text-2xl font-semibold">{menuLanguage[lang]}</h1>
|
||||
<div className="flex items-center space-x-4">
|
||||
<input
|
||||
type="text"
|
||||
placeholder={searchPlaceholder[lang]}
|
||||
className="border px-3 py-2 rounded-lg"
|
||||
/>
|
||||
|
||||
{/* Notifications dropdown */}
|
||||
<div className="relative" ref={notificationsRef}>
|
||||
<div
|
||||
className="flex items-center cursor-pointer relative"
|
||||
onClick={() => {
|
||||
setIsNotificationsOpen(!isNotificationsOpen);
|
||||
setIsMessagesOpen(false);
|
||||
setIsDropdownOpen(false);
|
||||
}}
|
||||
>
|
||||
<Bell size={20} className="text-gray-600" />
|
||||
{notifications.some((n) => !n.read) && (
|
||||
<span className="absolute -top-1 -right-1 bg-red-500 text-white text-xs rounded-full h-4 w-4 flex items-center justify-center">
|
||||
{notifications.filter((n) => !n.read).length}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Notifications dropdown menu */}
|
||||
{isNotificationsOpen && (
|
||||
<div className="absolute right-0 mt-2 w-80 bg-white rounded-md shadow-lg py-1 z-20 border">
|
||||
<div className="px-4 py-2 border-b flex justify-between items-center">
|
||||
<h3 className="font-semibold">{t.notifications}</h3>
|
||||
{notifications.some((n) => !n.read) && (
|
||||
<button
|
||||
onClick={markAllNotificationsAsRead}
|
||||
className="text-xs text-blue-600 hover:text-blue-800"
|
||||
>
|
||||
{t.markAllAsRead}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="max-h-80 overflow-y-auto">
|
||||
{notifications.length === 0 ? (
|
||||
<div className="px-4 py-2 text-sm text-gray-500">
|
||||
{t.noNotifications}
|
||||
</div>
|
||||
) : (
|
||||
notifications.map((notification) => (
|
||||
<div
|
||||
key={notification.id}
|
||||
className={`px-4 py-2 border-b last:border-b-0 ${
|
||||
!notification.read ? "bg-blue-50" : ""
|
||||
}`}
|
||||
>
|
||||
<div className="flex justify-between">
|
||||
<h4 className="text-sm font-semibold">
|
||||
{notification.title}
|
||||
</h4>
|
||||
{!notification.read && (
|
||||
<button className="text-gray-400 hover:text-gray-600">
|
||||
<X size={14} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-xs text-gray-600 mt-1">
|
||||
{notification.description}
|
||||
</p>
|
||||
<p className="text-xs text-gray-400 mt-1">
|
||||
{formatDate(notification.time)}
|
||||
</p>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="px-4 py-2 border-t text-center">
|
||||
<a
|
||||
href="/notifications"
|
||||
className="text-sm text-blue-600 hover:text-blue-800"
|
||||
>
|
||||
{t.viewAll}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Messages dropdown */}
|
||||
<div className="relative" ref={messagesRef}>
|
||||
<div
|
||||
className="flex items-center cursor-pointer relative"
|
||||
onClick={() => {
|
||||
setIsMessagesOpen(!isMessagesOpen);
|
||||
setIsNotificationsOpen(false);
|
||||
setIsDropdownOpen(false);
|
||||
}}
|
||||
>
|
||||
<MessageSquare size={20} className="text-gray-600" />
|
||||
{messages.some((m) => !m.read) && (
|
||||
<span className="absolute -top-1 -right-1 bg-red-500 text-white text-xs rounded-full h-4 w-4 flex items-center justify-center">
|
||||
{messages.filter((m) => !m.read).length}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Messages dropdown menu */}
|
||||
{isMessagesOpen && (
|
||||
<div className="absolute right-0 mt-2 w-80 bg-white rounded-md shadow-lg py-1 z-20 border">
|
||||
<div className="px-4 py-2 border-b flex justify-between items-center">
|
||||
<h3 className="font-semibold">{t.messages}</h3>
|
||||
{messages.some((m) => !m.read) && (
|
||||
<button
|
||||
onClick={markAllMessagesAsRead}
|
||||
className="text-xs text-blue-600 hover:text-blue-800"
|
||||
>
|
||||
{t.markAllAsRead}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="max-h-80 overflow-y-auto">
|
||||
{messages.length === 0 ? (
|
||||
<div className="px-4 py-2 text-sm text-gray-500">
|
||||
{t.noMessages}
|
||||
</div>
|
||||
) : (
|
||||
messages.map((message) => (
|
||||
<div
|
||||
key={message.id}
|
||||
className={`px-4 py-2 border-b last:border-b-0 ${
|
||||
!message.read ? "bg-blue-50" : ""
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-start">
|
||||
<div className="w-8 h-8 bg-gray-300 rounded-full flex items-center justify-center mr-2 flex-shrink-0">
|
||||
<span className="text-xs font-semibold">
|
||||
{message.avatar}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="flex justify-between">
|
||||
<h4 className="text-sm font-semibold">
|
||||
{message.sender}
|
||||
</h4>
|
||||
<p className="text-xs text-gray-400">
|
||||
{formatDate(message.time)}
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-xs text-gray-600 mt-1">
|
||||
{message.message}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="px-4 py-2 border-t text-center">
|
||||
<a
|
||||
href="/messages"
|
||||
className="text-sm text-blue-600 hover:text-blue-800"
|
||||
>
|
||||
{t.viewAll}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Profile dropdown */}
|
||||
<div className="relative" ref={dropdownRef}>
|
||||
<div
|
||||
className="flex items-center cursor-pointer"
|
||||
onClick={() => {
|
||||
setIsDropdownOpen(!isDropdownOpen);
|
||||
setIsNotificationsOpen(false);
|
||||
setIsMessagesOpen(false);
|
||||
}}
|
||||
>
|
||||
<div className="w-10 h-10 bg-gray-300 rounded-full flex items-center justify-center">
|
||||
<User size={20} className="text-gray-600" />
|
||||
</div>
|
||||
<ChevronDown size={16} className="ml-1 text-gray-600" />
|
||||
</div>
|
||||
|
||||
{/* Dropdown menu */}
|
||||
{isDropdownOpen && (
|
||||
<div className="absolute right-0 mt-2 w-48 bg-white rounded-md shadow-lg py-1 z-20 border">
|
||||
<a
|
||||
href="/profile"
|
||||
className="flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"
|
||||
>
|
||||
<User size={16} className="mr-2" />
|
||||
{t.profile}
|
||||
</a>
|
||||
<a
|
||||
href="/settings"
|
||||
className="flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"
|
||||
>
|
||||
<Settings size={16} className="mr-2" />
|
||||
{t.settings}
|
||||
</a>
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="flex items-center w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"
|
||||
>
|
||||
<LogOut size={16} className="mr-2" />
|
||||
{t.logout}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
};
|
||||
|
||||
export default Header;
|
||||
@@ -0,0 +1,223 @@
|
||||
"use client";
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { User, Briefcase, ChevronDown } from "lucide-react";
|
||||
import {
|
||||
retrieveAccessObjects,
|
||||
retrieveUserSelection,
|
||||
} from "@/apicalls/cookies/token";
|
||||
import { loginSelectEmployee } from "@/apicalls/login/login";
|
||||
|
||||
// Language definitions for employee profile section
|
||||
const profileLanguage = {
|
||||
tr: {
|
||||
userType: "Kullanıcı Tipi",
|
||||
employee: "Çalışan",
|
||||
loading: "Yükleniyor...",
|
||||
changeSelection: "Seçimi Değiştir",
|
||||
selectCompany: "Şirket Seçin",
|
||||
noCompanies: "Kullanılabilir şirket bulunamadı",
|
||||
duty: "Görev",
|
||||
},
|
||||
en: {
|
||||
userType: "User Type",
|
||||
employee: "Employee",
|
||||
loading: "Loading...",
|
||||
changeSelection: "Change Selection",
|
||||
selectCompany: "Select Company",
|
||||
noCompanies: "No companies available",
|
||||
duty: "Duty",
|
||||
},
|
||||
};
|
||||
|
||||
interface CompanyInfo {
|
||||
uu_id: string;
|
||||
public_name: string;
|
||||
company_type: string;
|
||||
company_address: string | null;
|
||||
duty: string;
|
||||
}
|
||||
|
||||
interface UserSelection {
|
||||
userType: string;
|
||||
selected: CompanyInfo;
|
||||
}
|
||||
|
||||
interface EmployeeProfileSectionProps {
|
||||
userSelectionData: UserSelection;
|
||||
lang?: "en" | "tr";
|
||||
}
|
||||
|
||||
const EmployeeProfileSection: React.FC<EmployeeProfileSectionProps> = ({
|
||||
userSelectionData,
|
||||
lang = "en",
|
||||
}) => {
|
||||
const t =
|
||||
profileLanguage[lang as keyof typeof profileLanguage] || profileLanguage.en;
|
||||
|
||||
const [showSelectionList, setShowSelectionList] = useState<boolean>(false);
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
// Initialize state with data from props
|
||||
const [userSelection, setUserSelection] = useState<CompanyInfo | null>(
|
||||
userSelectionData?.selected || null
|
||||
);
|
||||
const [selectionList, setSelectionList] = useState<CompanyInfo[] | null>(
|
||||
null
|
||||
);
|
||||
const [availableCompanies, setAvailableCompanies] = useState<CompanyInfo[]>(
|
||||
[]
|
||||
);
|
||||
const [hasMultipleOptions, setHasMultipleOptions] = useState<boolean>(false);
|
||||
|
||||
// Fetch access objects for selection list when needed
|
||||
useEffect(() => {
|
||||
if (showSelectionList && !selectionList) {
|
||||
setLoading(true);
|
||||
retrieveAccessObjects()
|
||||
.then((accessObjectsData) => {
|
||||
console.log("Access Objects:", accessObjectsData);
|
||||
|
||||
if (accessObjectsData && "selectionList" in accessObjectsData) {
|
||||
const companies = (accessObjectsData as any)
|
||||
.selectionList as CompanyInfo[];
|
||||
setSelectionList(companies);
|
||||
|
||||
// Filter out the currently selected company
|
||||
if (userSelection) {
|
||||
const filteredCompanies = companies.filter(
|
||||
(company) => company.uu_id !== userSelection.uu_id
|
||||
);
|
||||
setAvailableCompanies(filteredCompanies);
|
||||
setHasMultipleOptions(filteredCompanies.length > 0);
|
||||
} else {
|
||||
setAvailableCompanies(companies);
|
||||
setHasMultipleOptions(companies.length > 1);
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error("Error fetching access objects:", err);
|
||||
})
|
||||
.finally(() => setLoading(false));
|
||||
}
|
||||
}, [showSelectionList, selectionList, userSelection]);
|
||||
|
||||
// Update user selection when props change
|
||||
useEffect(() => {
|
||||
if (userSelectionData?.selected) {
|
||||
setUserSelection(userSelectionData.selected as CompanyInfo);
|
||||
|
||||
// Check if we need to fetch selection list to determine if multiple options exist
|
||||
if (!selectionList) {
|
||||
retrieveAccessObjects()
|
||||
.then((accessObjectsData) => {
|
||||
if (accessObjectsData && "selectionList" in accessObjectsData) {
|
||||
const companies = (accessObjectsData as any)
|
||||
.selectionList as CompanyInfo[];
|
||||
setSelectionList(companies);
|
||||
|
||||
// Filter out the currently selected company
|
||||
const filteredCompanies = companies.filter(
|
||||
(company) => company.uu_id !== userSelectionData.selected.uu_id
|
||||
);
|
||||
setHasMultipleOptions(filteredCompanies.length > 0);
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error("Error fetching access objects:", err);
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [userSelectionData, selectionList]);
|
||||
|
||||
const handleSelectCompany = (company: any) => {
|
||||
loginSelectEmployee({ company_uu_id: company.uu_id })
|
||||
.then((responseData: any) => {
|
||||
if (responseData?.status === 200 || responseData?.status === 202) {
|
||||
// Refresh the page to update the selection
|
||||
window.location.reload();
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Error selecting company:", error);
|
||||
});
|
||||
};
|
||||
|
||||
if (!userSelection) {
|
||||
return <div className="text-center text-gray-500">{t.loading}</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col space-y-3">
|
||||
{/* <div className="flex items-center space-x-3">
|
||||
<div className="bg-blue-100 p-2 rounded-full">
|
||||
<User className="h-6 w-6 text-blue-600" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-medium text-gray-900">{t.userType}</h3>
|
||||
<p className="text-sm text-gray-600">{t.employee}</p>
|
||||
</div>
|
||||
</div> */}
|
||||
|
||||
<div
|
||||
className={`flex items-center justify-between p-2 ${
|
||||
hasMultipleOptions ? "hover:bg-gray-100 cursor-pointer" : ""
|
||||
} rounded-lg transition-colors`}
|
||||
onClick={() =>
|
||||
hasMultipleOptions && setShowSelectionList(!showSelectionList)
|
||||
}
|
||||
>
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="bg-green-100 p-2 rounded-full">
|
||||
<Briefcase className="h-5 w-5 text-green-600" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-medium text-gray-900">
|
||||
{userSelection.public_name} {userSelection.company_type}
|
||||
</h3>
|
||||
<p className="text-sm text-gray-500">{userSelection.duty}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Selection dropdown */}
|
||||
{showSelectionList && hasMultipleOptions && (
|
||||
<div className="mt-2 border rounded-lg overflow-hidden shadow-md">
|
||||
<div className="bg-gray-50 p-2 border-b">
|
||||
<h4 className="font-medium text-gray-700">{t.selectCompany}</h4>
|
||||
</div>
|
||||
<div className="max-h-60 overflow-y-auto">
|
||||
{loading ? (
|
||||
<div className="p-4 text-center text-gray-500">{t.loading}</div>
|
||||
) : availableCompanies.length > 0 ? (
|
||||
availableCompanies.map((company, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="p-3 hover:bg-gray-50 cursor-pointer border-b last:border-b-0"
|
||||
onClick={() => handleSelectCompany(company)}
|
||||
>
|
||||
<div className="font-medium">{company.public_name}</div>
|
||||
{company.company_type && (
|
||||
<div className="text-sm text-blue-600">
|
||||
{company.company_type}
|
||||
</div>
|
||||
)}
|
||||
{company.duty && (
|
||||
<div className="text-xs text-gray-500 mt-1">
|
||||
{t.duty}: {company.duty}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className="p-4 text-center text-gray-500">
|
||||
{t.noCompanies}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default EmployeeProfileSection;
|
||||
@@ -0,0 +1,102 @@
|
||||
"use client";
|
||||
import React, { useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { Home, ChevronDown, ChevronRight } from "lucide-react";
|
||||
import type { LanguageTranslation } from "./handler";
|
||||
|
||||
interface NavigationMenuProps {
|
||||
transformedMenu: any[];
|
||||
lang: string;
|
||||
}
|
||||
|
||||
const NavigationMenu: React.FC<NavigationMenuProps> = ({ transformedMenu, lang }) => {
|
||||
// State to track which menu items are expanded
|
||||
const [firstLayerIndex, setFirstLayerIndex] = useState<number>(-1);
|
||||
const [secondLayerIndex, setSecondLayerIndex] = useState<number>(-1);
|
||||
|
||||
// Handle first level menu click
|
||||
const handleFirstLevelClick = (index: number) => {
|
||||
setFirstLayerIndex(index === firstLayerIndex ? -1 : index);
|
||||
setSecondLayerIndex(-1); // Reset second layer selection when first layer changes
|
||||
};
|
||||
|
||||
// Handle second level menu click
|
||||
const handleSecondLevelClick = (index: number) => {
|
||||
setSecondLayerIndex(index === secondLayerIndex ? -1 : index);
|
||||
};
|
||||
|
||||
return (
|
||||
<nav className="flex flex-col">
|
||||
{transformedMenu &&
|
||||
transformedMenu.map((item, firstIndex) => (
|
||||
<div
|
||||
key={item.name}
|
||||
className="border-b border-gray-100 last:border-b-0"
|
||||
>
|
||||
<button
|
||||
onClick={() => handleFirstLevelClick(firstIndex)}
|
||||
className={`flex items-center justify-between w-full p-4 text-left transition-colors ${
|
||||
firstIndex === firstLayerIndex
|
||||
? "bg-emerald-50 text-emerald-700"
|
||||
: "text-gray-700 hover:bg-gray-50"
|
||||
}`}
|
||||
>
|
||||
<span className="font-medium">{item.lg[lang]}</span>
|
||||
{firstIndex === firstLayerIndex ? (
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
) : (
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
)}
|
||||
</button>
|
||||
|
||||
{/* First level separator and second layer */}
|
||||
{firstIndex === firstLayerIndex && (
|
||||
<div className="bg-gray-50 border-t border-gray-100">
|
||||
{item.subList.map((subItem: any, secondIndex: number) => (
|
||||
<div
|
||||
key={subItem.name}
|
||||
className="border-b border-gray-100 last:border-b-0"
|
||||
>
|
||||
<button
|
||||
onClick={() => handleSecondLevelClick(secondIndex)}
|
||||
className={`flex items-center justify-between w-full p-3 pl-8 text-left transition-colors ${
|
||||
secondIndex === secondLayerIndex
|
||||
? "bg-emerald-100 text-emerald-800"
|
||||
: "text-gray-600 hover:bg-gray-100"
|
||||
}`}
|
||||
>
|
||||
<span className="font-medium">{subItem.lg[lang]}</span>
|
||||
{secondIndex === secondLayerIndex ? (
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
) : (
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
)}
|
||||
</button>
|
||||
|
||||
{/* Second level separator and third layer */}
|
||||
{firstIndex === firstLayerIndex &&
|
||||
secondIndex === secondLayerIndex && (
|
||||
<div className="bg-gray-100 border-t border-gray-200">
|
||||
{subItem.subList.map((subSubItem: any) => (
|
||||
<Link
|
||||
key={subSubItem.name}
|
||||
href={subSubItem.siteUrl}
|
||||
className="flex items-center w-full p-3 pl-12 text-left text-gray-700 hover:bg-gray-200 transition-colors"
|
||||
>
|
||||
<Home className="h-4 w-4 mr-2 text-gray-500" />
|
||||
<span>{subSubItem.lg[lang]}</span>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</nav>
|
||||
);
|
||||
};
|
||||
|
||||
export default NavigationMenu;
|
||||
@@ -0,0 +1,455 @@
|
||||
"use client";
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { User, Building, Home, ChevronDown } from "lucide-react";
|
||||
import { retrieveAccessObjects } from "@/apicalls/cookies/token";
|
||||
import { loginSelectOccupant } from "@/apicalls/login/login";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
// Language definitions for occupant profile section
|
||||
const profileLanguage = {
|
||||
tr: {
|
||||
userType: "Kullanıcı Tipi",
|
||||
occupant: "Sakin",
|
||||
building: "Bina",
|
||||
apartment: "Daire",
|
||||
loading: "Yükleniyor...",
|
||||
changeSelection: "Seçimi Değiştir",
|
||||
selectOccupant: "Daire Seçin",
|
||||
noOccupants: "Kullanılabilir daire bulunamadı",
|
||||
},
|
||||
en: {
|
||||
userType: "User Type",
|
||||
occupant: "Occupant",
|
||||
building: "Building",
|
||||
apartment: "Apartment",
|
||||
loading: "Loading...",
|
||||
changeSelection: "Change Selection",
|
||||
selectOccupant: "Select Apartment",
|
||||
noOccupants: "No apartments available",
|
||||
},
|
||||
};
|
||||
|
||||
// {
|
||||
// "userType": "occupant",
|
||||
// "selectionList": {
|
||||
// "3fe72194-dad6-4ddc-8679-70acdbe7f619": {
|
||||
// "build_uu_id": "3fe72194-dad6-4ddc-8679-70acdbe7f619",
|
||||
// "build_name": "Build Example",
|
||||
// "build_no": "B001",
|
||||
// "occupants": [
|
||||
// {
|
||||
// "build_living_space_uu_id": "b67e5a37-ac04-45ab-8bca-5a3427358015",
|
||||
// "part_uu_id": "441ef61b-1cc5-465b-90b2-4835d0e16540",
|
||||
// "part_name": "APARTMAN DAIRESI : 1",
|
||||
// "part_level": 1,
|
||||
// "occupant_uu_id": "6bde6bf9-0d13-4b6f-a612-28878cd7324f",
|
||||
// "description": "Daire Kiracısı",
|
||||
// "code": "FL-TEN"
|
||||
// }
|
||||
// ]
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// Define interfaces for occupant data structures based on the access object structure
|
||||
interface OccupantDetails {
|
||||
build_living_space_uu_id: string;
|
||||
part_uu_id: string;
|
||||
part_name: string;
|
||||
part_level: number;
|
||||
occupant_uu_id: string;
|
||||
description: string;
|
||||
code: string;
|
||||
[key: string]: any; // Allow other properties
|
||||
}
|
||||
|
||||
interface BuildingInfo {
|
||||
build_uu_id: string;
|
||||
build_name: string;
|
||||
build_no: string;
|
||||
occupants: OccupantDetails[];
|
||||
[key: string]: any; // Allow other properties
|
||||
}
|
||||
|
||||
interface OccupantSelectionList {
|
||||
userType: string;
|
||||
selectionList: {
|
||||
[key: string]: BuildingInfo;
|
||||
};
|
||||
}
|
||||
|
||||
// Interface for the selected occupant data
|
||||
interface OccupantInfo {
|
||||
buildName?: string;
|
||||
buildNo?: string;
|
||||
occupantName?: string;
|
||||
description?: string;
|
||||
code?: string;
|
||||
part_name?: string;
|
||||
build_living_space_uu_id?: string;
|
||||
[key: string]: any; // Allow other properties
|
||||
}
|
||||
|
||||
interface UserSelection {
|
||||
userType: string;
|
||||
selected: OccupantInfo;
|
||||
}
|
||||
|
||||
interface OccupantProfileSectionProps {
|
||||
userSelectionData: UserSelection;
|
||||
lang?: "en" | "tr";
|
||||
}
|
||||
|
||||
const OccupantProfileSection: React.FC<OccupantProfileSectionProps> = ({
|
||||
userSelectionData,
|
||||
lang = "en",
|
||||
}) => {
|
||||
const t =
|
||||
profileLanguage[lang as keyof typeof profileLanguage] || profileLanguage.en;
|
||||
const router = useRouter();
|
||||
|
||||
const [showSelectionList, setShowSelectionList] = useState<boolean>(false);
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
// Initialize state with data from props
|
||||
const [userSelection, setUserSelection] = useState<OccupantInfo | null>(
|
||||
userSelectionData?.selected || null
|
||||
);
|
||||
const [selectionList, setSelectionList] =
|
||||
useState<OccupantSelectionList | null>(null);
|
||||
const [availableOccupants, setAvailableOccupants] = useState<any[]>([]);
|
||||
const [hasMultipleOptions, setHasMultipleOptions] = useState<boolean>(false);
|
||||
const [selectedBuildingKey, setSelectedBuildingKey] = useState<string | null>(
|
||||
null
|
||||
);
|
||||
const [buildings, setBuildings] = useState<{ [key: string]: BuildingInfo }>(
|
||||
{}
|
||||
);
|
||||
|
||||
// Fetch access objects for selection list when needed
|
||||
useEffect(() => {
|
||||
if (showSelectionList && !selectionList) {
|
||||
setLoading(true);
|
||||
retrieveAccessObjects()
|
||||
.then((accessObjectsData) => {
|
||||
console.log("Access Objects:", accessObjectsData);
|
||||
|
||||
if (accessObjectsData && accessObjectsData.selectionList) {
|
||||
const data = accessObjectsData as OccupantSelectionList;
|
||||
setSelectionList(data);
|
||||
setBuildings(data.selectionList);
|
||||
|
||||
// Check if there are multiple buildings or multiple occupants across all buildings
|
||||
const buildingKeys = Object.keys(data.selectionList);
|
||||
let totalOccupants = 0;
|
||||
let currentBuildingKey = null;
|
||||
let currentOccupantId = null;
|
||||
|
||||
// Count total occupants and find current building/occupant
|
||||
buildingKeys.forEach((key) => {
|
||||
const building = data.selectionList[key];
|
||||
if (building.occupants && building.occupants.length > 0) {
|
||||
totalOccupants += building.occupants.length;
|
||||
|
||||
// Try to find the current user's building and occupant
|
||||
if (userSelection) {
|
||||
building.occupants.forEach((occupant) => {
|
||||
if (
|
||||
occupant.build_living_space_uu_id ===
|
||||
userSelection.build_living_space_uu_id
|
||||
) {
|
||||
currentBuildingKey = key;
|
||||
currentOccupantId = occupant.build_living_space_uu_id;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Set whether there are multiple options
|
||||
setHasMultipleOptions(totalOccupants > 1);
|
||||
|
||||
// If we found the current building, set it as selected
|
||||
if (currentBuildingKey) {
|
||||
setSelectedBuildingKey(currentBuildingKey);
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error("Error fetching access objects:", err);
|
||||
})
|
||||
.finally(() => setLoading(false));
|
||||
}
|
||||
}, [showSelectionList, userSelection]);
|
||||
|
||||
// Update user selection when props change
|
||||
useEffect(() => {
|
||||
if (userSelectionData?.selected) {
|
||||
setUserSelection(userSelectionData.selected as OccupantInfo);
|
||||
|
||||
// Check if we need to fetch selection list to determine if multiple options exist
|
||||
if (!selectionList) {
|
||||
retrieveAccessObjects()
|
||||
.then((accessObjectsData) => {
|
||||
if (accessObjectsData && accessObjectsData.selectionList) {
|
||||
const data = accessObjectsData as OccupantSelectionList;
|
||||
setSelectionList(data);
|
||||
setBuildings(data.selectionList);
|
||||
|
||||
// Count total occupants across all buildings
|
||||
let totalOccupants = 0;
|
||||
let currentBuildingKey = null;
|
||||
|
||||
Object.keys(data.selectionList).forEach((key) => {
|
||||
const building = data.selectionList[key];
|
||||
if (building.occupants && building.occupants.length > 0) {
|
||||
totalOccupants += building.occupants.length;
|
||||
|
||||
// Try to find the current user's building
|
||||
building.occupants.forEach((occupant) => {
|
||||
if (
|
||||
userSelectionData.selected.build_living_space_uu_id ===
|
||||
occupant.build_living_space_uu_id
|
||||
) {
|
||||
currentBuildingKey = key;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
setHasMultipleOptions(totalOccupants > 1);
|
||||
if (currentBuildingKey) {
|
||||
setSelectedBuildingKey(currentBuildingKey);
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error("Error fetching access objects:", err);
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [userSelectionData, selectionList]);
|
||||
|
||||
// Helper function to process occupant data
|
||||
const processOccupantData = (data: OccupantSelectionList) => {
|
||||
if (!data.selectionList) return;
|
||||
|
||||
const occupantList: any[] = [];
|
||||
|
||||
// Process the building/occupant structure
|
||||
Object.keys(data.selectionList).forEach((buildKey) => {
|
||||
const building = data.selectionList[buildKey];
|
||||
if (building.occupants && building.occupants.length > 0) {
|
||||
building.occupants.forEach((occupant: OccupantDetails) => {
|
||||
occupantList.push({
|
||||
buildKey,
|
||||
buildName: building.build_name,
|
||||
buildNo: building.build_no,
|
||||
...occupant,
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
setAvailableOccupants(occupantList);
|
||||
};
|
||||
|
||||
// Process occupant data when selection menu is opened or when selectionList changes
|
||||
useEffect(() => {
|
||||
if (showSelectionList && selectionList && selectionList.selectionList) {
|
||||
setLoading(true);
|
||||
processOccupantData(selectionList);
|
||||
setLoading(false);
|
||||
}
|
||||
}, [showSelectionList, selectionList]);
|
||||
|
||||
const handleSelectOccupant = (occupant: any) => {
|
||||
loginSelectOccupant({
|
||||
build_living_space_uu_id: occupant.build_living_space_uu_id,
|
||||
})
|
||||
.then((responseData: any) => {
|
||||
if (responseData?.status === 200 || responseData?.status === 202) {
|
||||
// Refresh the page to update the selection
|
||||
window.location.reload();
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Error selecting occupant:", error);
|
||||
});
|
||||
};
|
||||
|
||||
if (!userSelection) {
|
||||
return <div className="text-center text-gray-500">{t.loading}</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col space-y-3">
|
||||
{/* <div className="flex items-center space-x-3">
|
||||
<div className="bg-blue-100 p-2 rounded-full">
|
||||
<User className="h-6 w-6 text-blue-600" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-medium text-gray-900">{t.userType}</h3>
|
||||
<p className="text-sm text-gray-600">{t.occupant}</p>
|
||||
</div>
|
||||
</div> */}
|
||||
|
||||
{userSelection?.buildName && (
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="bg-amber-100 p-2 rounded-full">
|
||||
<Building className="h-5 w-5 text-amber-600" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-medium text-gray-900">{t.building}</h3>
|
||||
<p className="text-sm text-gray-600">{userSelection.buildName}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{userSelection?.part_name && (
|
||||
<div
|
||||
className={`flex items-center justify-between space-x-3 p-2 ${
|
||||
hasMultipleOptions ? "hover:bg-gray-100 cursor-pointer" : ""
|
||||
} rounded-lg transition-colors`}
|
||||
onClick={() =>
|
||||
hasMultipleOptions && setShowSelectionList(!showSelectionList)
|
||||
}
|
||||
>
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="bg-purple-100 p-2 rounded-full">
|
||||
<Home className="h-5 w-5 text-purple-600" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-medium text-gray-900">{t.apartment}</h3>
|
||||
<p className="text-sm text-gray-600">{userSelection.part_name}</p>
|
||||
{userSelection.description && (
|
||||
<p className="text-xs text-gray-500">
|
||||
{userSelection.description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{hasMultipleOptions && (
|
||||
<div className="text-xs text-blue-600 flex items-center">
|
||||
{t.changeSelection} <ChevronDown className="h-3 w-3 ml-1" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Selection dropdown - First layer: Buildings */}
|
||||
{showSelectionList && hasMultipleOptions && (
|
||||
<div className="mt-2 border rounded-lg overflow-hidden shadow-md">
|
||||
<div className="bg-gray-50 p-2 border-b">
|
||||
<h4 className="font-medium text-gray-700">{t.selectOccupant}</h4>
|
||||
</div>
|
||||
<div className="max-h-60 overflow-y-auto">
|
||||
{loading ? (
|
||||
<div className="p-4 text-center text-gray-500">{t.loading}</div>
|
||||
) : buildings && Object.keys(buildings).length > 0 ? (
|
||||
selectedBuildingKey ? (
|
||||
// Second layer: Occupants in the selected building
|
||||
<div>
|
||||
<div className="bg-gray-100 p-2 border-b flex justify-between items-center">
|
||||
<h5 className="font-medium text-gray-700">
|
||||
{buildings[selectedBuildingKey].build_name}
|
||||
</h5>
|
||||
<button
|
||||
className="text-xs text-blue-600"
|
||||
onClick={() => setSelectedBuildingKey(null)}
|
||||
>
|
||||
Back to buildings
|
||||
</button>
|
||||
</div>
|
||||
{buildings[selectedBuildingKey].occupants.length > 0 ? (
|
||||
buildings[selectedBuildingKey].occupants.map(
|
||||
(occupant, index) => {
|
||||
// Skip the currently selected occupant
|
||||
if (
|
||||
userSelection &&
|
||||
occupant.build_living_space_uu_id ===
|
||||
userSelection.build_living_space_uu_id
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className="p-3 hover:bg-gray-50 cursor-pointer border-b last:border-b-0"
|
||||
onClick={() => handleSelectOccupant(occupant)}
|
||||
>
|
||||
<div className="font-medium">
|
||||
{occupant.description || "Apartment"}
|
||||
</div>
|
||||
<div className="text-sm text-gray-600">
|
||||
{occupant.part_name}
|
||||
</div>
|
||||
{occupant.code && (
|
||||
<div className="text-xs text-gray-500 mt-1">
|
||||
{t.apartment} {occupant.code}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
)
|
||||
) : (
|
||||
<div className="p-4 text-center text-gray-500">
|
||||
{t.noOccupants}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
// First layer: Buildings list
|
||||
Object.keys(buildings).map((buildingKey, index) => {
|
||||
const building = buildings[buildingKey];
|
||||
// Skip buildings with no occupants or only the current occupant
|
||||
if (!building.occupants || building.occupants.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check if this building has any occupants other than the current one
|
||||
if (userSelection) {
|
||||
const hasOtherOccupants = building.occupants.some(
|
||||
(occupant) =>
|
||||
occupant.build_living_space_uu_id !==
|
||||
userSelection.build_living_space_uu_id
|
||||
);
|
||||
if (!hasOtherOccupants) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className="p-3 hover:bg-gray-50 cursor-pointer border-b last:border-b-0"
|
||||
onClick={() => setSelectedBuildingKey(buildingKey)}
|
||||
>
|
||||
<div className="font-medium">{building.build_name}</div>
|
||||
<div className="text-sm text-gray-600">
|
||||
No: {building.build_no}
|
||||
</div>
|
||||
<div className="text-xs text-gray-500 mt-1">
|
||||
{building.occupants.length}{" "}
|
||||
{building.occupants.length === 1
|
||||
? t.apartment.toLowerCase()
|
||||
: t.apartment.toLowerCase() + "s"}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
)
|
||||
) : (
|
||||
<div className="p-4 text-center text-gray-500">
|
||||
{t.noOccupants}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default OccupantProfileSection;
|
||||
@@ -0,0 +1,22 @@
|
||||
"use client";
|
||||
import React from "react";
|
||||
|
||||
interface ProfileLoadingStateProps {
|
||||
loadingText: string;
|
||||
}
|
||||
|
||||
const ProfileLoadingState: React.FC<ProfileLoadingStateProps> = ({ loadingText }) => {
|
||||
return (
|
||||
<div className="flex items-center justify-center py-2">
|
||||
<div className="animate-pulse flex space-x-4 w-full">
|
||||
<div className="rounded-full bg-gray-300 h-12 w-12"></div>
|
||||
<div className="flex-1 space-y-2 py-1">
|
||||
<div className="h-4 bg-gray-300 rounded w-3/4"></div>
|
||||
<div className="h-4 bg-gray-300 rounded w-1/2"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProfileLoadingState;
|
||||
@@ -1,113 +0,0 @@
|
||||
"use server";
|
||||
import React from "react";
|
||||
import Link from "next/link";
|
||||
|
||||
import { Home } from "lucide-react";
|
||||
import { transformMenu, LanguageTranslation } from "@/components/menu/runner";
|
||||
|
||||
async function LeftMenu({
|
||||
searchParams,
|
||||
pageUuidList,
|
||||
lang,
|
||||
pageSelected,
|
||||
}: {
|
||||
pageUuidList: string[];
|
||||
lang: keyof LanguageTranslation;
|
||||
searchParams: { [key: string]: string | string[] | undefined };
|
||||
pageSelected: string;
|
||||
}) {
|
||||
const transformedMenu = transformMenu(pageUuidList) || [];
|
||||
|
||||
// Get the menuContext from searchParams without setting a default value
|
||||
const menuContext = searchParams?.menu;
|
||||
|
||||
// Only parse the indices if menuContext exists
|
||||
let firstLayerIndex = -1;
|
||||
let secondLayerIndex = -1;
|
||||
|
||||
if (menuContext) {
|
||||
const indices = menuContext.toString().split("*").map(Number);
|
||||
firstLayerIndex = indices[0] || 0;
|
||||
secondLayerIndex = indices[1] || 0;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<nav className="flex flex-col space-y-2">
|
||||
<div className="text-xl font-bold mb-6 text-center">Dashboard</div>
|
||||
{transformedMenu &&
|
||||
transformedMenu.map((item, firstIndex) => (
|
||||
<div key={item.name} className="mb-4">
|
||||
<Link
|
||||
href={`${pageSelected}?menu=${firstIndex}*0`}
|
||||
className={`text-xl font-semibold pl-5 my-2 py-2 block ${
|
||||
firstIndex === firstLayerIndex
|
||||
? "text-emerald-600"
|
||||
: "text-emerald-400"
|
||||
} hover:text-emerald-600`}
|
||||
>
|
||||
{item.lg[lang]}
|
||||
</Link>
|
||||
|
||||
{/* Only render the second layer if menuContext exists and this first layer item is selected */}
|
||||
{menuContext && firstIndex === firstLayerIndex && (
|
||||
<ul className="space-y-2">
|
||||
{item.subList.map((subItem, secondIndex) => (
|
||||
<div key={subItem.name}>
|
||||
<Link
|
||||
href={`${pageSelected}?menu=${firstIndex}*${secondIndex}`}
|
||||
className={`ml-5 my-4 pl-4 text-xl font-semibold block ${
|
||||
secondIndex === secondLayerIndex
|
||||
? "text-emerald-700"
|
||||
: "text-emerald-500"
|
||||
} hover:text-emerald-700`}
|
||||
>
|
||||
{subItem.lg[lang]}
|
||||
</Link>
|
||||
{/* Only render the third layer if this second layer item is selected */}
|
||||
{firstIndex === firstLayerIndex &&
|
||||
secondIndex === secondLayerIndex && (
|
||||
<div className="ml-5">
|
||||
{subItem.subList.map((subSubItem) =>
|
||||
`${pageSelected}` !== subSubItem.siteUrl ? (
|
||||
<Link
|
||||
key={subSubItem.name}
|
||||
href={`${subSubItem?.siteUrl}?menu=${firstIndex}*${secondIndex}`}
|
||||
className={`flex flex-row text-xl py-4 my-4 w-full space-x-2 p-2 rounded hover:bg-gray-200`}
|
||||
>
|
||||
<span className="text-gray-400">
|
||||
<Home />
|
||||
</span>
|
||||
<span className="ml-5 text-gray-700">
|
||||
{subSubItem.lg[lang]}
|
||||
</span>
|
||||
</Link>
|
||||
) : (
|
||||
<a
|
||||
key={subSubItem.name}
|
||||
href={`${subSubItem?.siteUrl}?menu=${firstIndex}*${secondIndex}`}
|
||||
className={`flex flex-row text-xl py-4 my-4 w-full space-x-2 p-2 rounded bg-gray-100 cursor-not-allowed"`}
|
||||
>
|
||||
<span className="text-gray-400">
|
||||
<Home />
|
||||
</span>
|
||||
<span className="ml-5 text-gray-700">
|
||||
{subSubItem.lg[lang]}
|
||||
</span>
|
||||
</a>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</nav>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default LeftMenu;
|
||||
100
WebServices/client-frontend/src/components/menu/menu.tsx
Normal file
100
WebServices/client-frontend/src/components/menu/menu.tsx
Normal file
@@ -0,0 +1,100 @@
|
||||
"use client";
|
||||
|
||||
import React, { useEffect, useState, Suspense } from "react";
|
||||
import { transformMenu } from "./handler";
|
||||
import { retrieveUserSelection } from "@/apicalls/cookies/token";
|
||||
import EmployeeProfileSection from "./EmployeeProfileSection";
|
||||
import OccupantProfileSection from "./OccupantProfileSection";
|
||||
import ProfileLoadingState from "./ProfileLoadingState";
|
||||
import NavigationMenu from "./NavigationMenu";
|
||||
|
||||
// Language definitions for dashboard title
|
||||
const dashboardLanguage = {
|
||||
tr: {
|
||||
dashboard: "Kontrol Paneli",
|
||||
loading: "Yükleniyor...",
|
||||
},
|
||||
en: {
|
||||
dashboard: "Control Panel",
|
||||
loading: "Loading...",
|
||||
},
|
||||
};
|
||||
|
||||
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 =
|
||||
dashboardLanguage[lang as keyof typeof dashboardLanguage] ||
|
||||
dashboardLanguage.en;
|
||||
|
||||
// State for loading indicator, user type, and user selection data
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
const [userType, setUserType] = useState<string | null>(null);
|
||||
const [userSelectionData, setUserSelectionData] =
|
||||
useState<UserSelection | null>(null);
|
||||
|
||||
// Fetch user selection data
|
||||
useEffect(() => {
|
||||
setLoading(true);
|
||||
|
||||
retrieveUserSelection()
|
||||
.then((data) => {
|
||||
console.log("User Selection:", data);
|
||||
|
||||
if (data && "userType" in data) {
|
||||
setUserType((data as UserSelection).userType);
|
||||
setUserSelectionData(data as UserSelection);
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error("Error fetching user selection data:", err);
|
||||
})
|
||||
.finally(() => setLoading(false));
|
||||
}, []);
|
||||
return (
|
||||
<div className="w-full bg-white shadow-sm rounded-lg overflow-hidden">
|
||||
<div className="p-4 border-b border-gray-200">
|
||||
<h2 className="text-xl font-bold text-center text-gray-800">
|
||||
{t.dashboard}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
{/* Profile Section with Suspense */}
|
||||
<div className="p-4 border-b border-gray-200 bg-gray-50">
|
||||
<Suspense
|
||||
fallback={<div className="text-center py-4">{t.loading}</div>}
|
||||
>
|
||||
{loading ? (
|
||||
<ProfileLoadingState loadingText={t.loading} />
|
||||
) : userType === "employee" && userSelectionData ? (
|
||||
<EmployeeProfileSection
|
||||
userSelectionData={userSelectionData}
|
||||
lang={lang as "en" | "tr"}
|
||||
/>
|
||||
) : userType === "occupant" && userSelectionData ? (
|
||||
<OccupantProfileSection
|
||||
userSelectionData={userSelectionData}
|
||||
lang={lang as "en" | "tr"}
|
||||
/>
|
||||
) : (
|
||||
<div className="text-center text-gray-500">{t.loading}</div>
|
||||
)}
|
||||
</Suspense>
|
||||
</div>
|
||||
|
||||
{/* Navigation Menu */}
|
||||
<NavigationMenu transformedMenu={transformedMenu} lang={lang} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ClientMenu;
|
||||
@@ -1,112 +0,0 @@
|
||||
/**
|
||||
* Filters the menu structure based on intersections with provided UUIDs
|
||||
* @param {string[]} uuids - Array of UUIDs to check for intersection
|
||||
* @param {Array} menu - The original menu structure
|
||||
* @returns {Array} - Filtered menu structure with only matching items
|
||||
*/
|
||||
import Menu from "@/components/menu/store"; // Assuming you have a menu structure imported
|
||||
|
||||
// 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[]) {
|
||||
// Process the menu structure
|
||||
const filteredMenu: FilteredMenuFirstLevel[] = Menu.reduce(
|
||||
(acc: FilteredMenuFirstLevel[], firstLevel: MenuFirstLevel) => {
|
||||
// Create a new first level item with empty subList
|
||||
const newFirstLevel: FilteredMenuFirstLevel = {
|
||||
name: firstLevel.name,
|
||||
lg: { ...firstLevel.lg },
|
||||
subList: [],
|
||||
};
|
||||
|
||||
// Process second level items
|
||||
firstLevel.subList.forEach((secondLevel: MenuSecondLevel) => {
|
||||
// Create a new second level item with empty subList
|
||||
const newSecondLevel: FilteredMenuSecondLevel = {
|
||||
name: secondLevel.name,
|
||||
lg: { ...secondLevel.lg },
|
||||
subList: [],
|
||||
};
|
||||
|
||||
// Process third level items
|
||||
secondLevel.subList.forEach((thirdLevel: MenuThirdLevel) => {
|
||||
// Check if the third level's siteUrl matches exactly
|
||||
if (
|
||||
thirdLevel.siteUrl &&
|
||||
siteUrls.some((url) => url === thirdLevel.siteUrl)
|
||||
) {
|
||||
// Create a modified third level item
|
||||
const newThirdLevel: FilteredMenuThirdLevel = {
|
||||
name: thirdLevel.name,
|
||||
lg: { ...thirdLevel.lg },
|
||||
siteUrl: thirdLevel.siteUrl,
|
||||
};
|
||||
|
||||
// Add the modified third level to the second level's subList
|
||||
newSecondLevel.subList.push(newThirdLevel);
|
||||
}
|
||||
});
|
||||
|
||||
// Only add the second level to the first level if it has any matching third level items
|
||||
if (newSecondLevel.subList.length > 0) {
|
||||
newFirstLevel.subList.push(newSecondLevel);
|
||||
}
|
||||
});
|
||||
|
||||
// Only add the first level to the result if it has any matching second level items
|
||||
if (newFirstLevel.subList.length > 0) {
|
||||
acc.push(newFirstLevel);
|
||||
}
|
||||
|
||||
return acc;
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
return filteredMenu;
|
||||
}
|
||||
|
||||
export { transformMenu };
|
||||
@@ -124,6 +124,33 @@ const MeetingParticipations = {
|
||||
siteUrl: "/meeting/participation",
|
||||
};
|
||||
|
||||
const TenantSendMessageToBuildManager = {
|
||||
name: "TenantSendMessageToBuildManager",
|
||||
lg: {
|
||||
tr: "Bina Yöneticisine Mesaj Gönder",
|
||||
en: "Send Message to Build Manager",
|
||||
},
|
||||
siteUrl: "/tenant/messageToBM",
|
||||
};
|
||||
|
||||
const TenantSendMessageToOwner = {
|
||||
name: "TenantSendMessageToOwner",
|
||||
lg: {
|
||||
tr: "Sahibine Mesaj Gönder",
|
||||
en: "Send Message to Owner",
|
||||
},
|
||||
siteUrl: "/tenant/messageToOwner",
|
||||
};
|
||||
|
||||
const TenantAccountView = {
|
||||
name: "TenantAccountView",
|
||||
lg: {
|
||||
tr: "Kiracı Cari Hareketleri",
|
||||
en: "Tenant Accountings",
|
||||
},
|
||||
siteUrl: "/tenant/accounting",
|
||||
};
|
||||
|
||||
const Menu = [
|
||||
{
|
||||
name: "Dashboard",
|
||||
@@ -198,6 +225,31 @@ const Menu = [
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Tenants",
|
||||
lg: {
|
||||
tr: "Kiracı İşlemleri",
|
||||
en: "Tenant Actions",
|
||||
},
|
||||
subList: [
|
||||
{
|
||||
name: "Accountings",
|
||||
lg: {
|
||||
tr: "Kiracı Cari Hareketler",
|
||||
en: "Tenant Accountings",
|
||||
},
|
||||
subList: [TenantAccountView],
|
||||
},
|
||||
{
|
||||
name: "Messages",
|
||||
lg: {
|
||||
tr: "Mesaj Gönder",
|
||||
en: "Send Messages",
|
||||
},
|
||||
subList: [TenantSendMessageToBuildManager, TenantSendMessageToOwner],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export default Menu;
|
||||
|
||||
@@ -1,116 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { Home, ChevronDown, ChevronRight } from "lucide-react";
|
||||
import { transformMenu } from "./handler";
|
||||
import type { LanguageTranslation } from "./handler";
|
||||
|
||||
interface ClientMenuProps {
|
||||
siteUrls: string[];
|
||||
lang: keyof LanguageTranslation;
|
||||
}
|
||||
|
||||
const ClientMenu: React.FC<ClientMenuProps> = ({ siteUrls, lang }) => {
|
||||
const transformedMenu = transformMenu(siteUrls) || [];
|
||||
|
||||
// State to track which menu items are expanded
|
||||
const [firstLayerIndex, setFirstLayerIndex] = useState<number>(-1);
|
||||
const [secondLayerIndex, setSecondLayerIndex] = useState<number>(-1);
|
||||
|
||||
// Handle first level menu click
|
||||
const handleFirstLevelClick = (index: number) => {
|
||||
setFirstLayerIndex(index === firstLayerIndex ? -1 : index);
|
||||
setSecondLayerIndex(-1); // Reset second layer selection when first layer changes
|
||||
};
|
||||
|
||||
// Handle second level menu click
|
||||
const handleSecondLevelClick = (index: number) => {
|
||||
setSecondLayerIndex(index === secondLayerIndex ? -1 : index);
|
||||
};
|
||||
|
||||
// No need for a navigation handler since we'll use Link components
|
||||
|
||||
return (
|
||||
<div className="w-full bg-white shadow-sm rounded-lg overflow-hidden">
|
||||
<div className="p-4 border-b border-gray-200">
|
||||
<h2 className="text-xl font-bold text-center text-gray-800">
|
||||
Dashboard
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<nav className="flex flex-col">
|
||||
{transformedMenu &&
|
||||
transformedMenu.map((item, firstIndex) => (
|
||||
<div
|
||||
key={item.name}
|
||||
className="border-b border-gray-100 last:border-b-0"
|
||||
>
|
||||
<button
|
||||
onClick={() => handleFirstLevelClick(firstIndex)}
|
||||
className={`flex items-center justify-between w-full p-4 text-left transition-colors ${
|
||||
firstIndex === firstLayerIndex
|
||||
? "bg-emerald-50 text-emerald-700"
|
||||
: "text-gray-700 hover:bg-gray-50"
|
||||
}`}
|
||||
>
|
||||
<span className="font-medium">{item.lg[lang]}</span>
|
||||
{firstIndex === firstLayerIndex ? (
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
) : (
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
)}
|
||||
</button>
|
||||
|
||||
{/* First level separator and second layer */}
|
||||
{firstIndex === firstLayerIndex && (
|
||||
<div className="bg-gray-50 border-t border-gray-100">
|
||||
{item.subList.map((subItem, secondIndex) => (
|
||||
<div
|
||||
key={subItem.name}
|
||||
className="border-b border-gray-100 last:border-b-0"
|
||||
>
|
||||
<button
|
||||
onClick={() => handleSecondLevelClick(secondIndex)}
|
||||
className={`flex items-center justify-between w-full p-3 pl-8 text-left transition-colors ${
|
||||
secondIndex === secondLayerIndex
|
||||
? "bg-emerald-100 text-emerald-800"
|
||||
: "text-gray-600 hover:bg-gray-100"
|
||||
}`}
|
||||
>
|
||||
<span className="font-medium">{subItem.lg[lang]}</span>
|
||||
{secondIndex === secondLayerIndex ? (
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
) : (
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
)}
|
||||
</button>
|
||||
|
||||
{/* Second level separator and third layer */}
|
||||
{firstIndex === firstLayerIndex &&
|
||||
secondIndex === secondLayerIndex && (
|
||||
<div className="bg-gray-100 border-t border-gray-200">
|
||||
{subItem.subList.map((subSubItem) => (
|
||||
<Link
|
||||
key={subSubItem.name}
|
||||
href={subSubItem.siteUrl}
|
||||
className="flex items-center w-full p-3 pl-12 text-left text-gray-700 hover:bg-gray-200 transition-colors"
|
||||
>
|
||||
<Home className="h-4 w-4 mr-2 text-gray-500" />
|
||||
<span>{subSubItem.lg[lang]}</span>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</nav>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ClientMenu;
|
||||
@@ -1,203 +0,0 @@
|
||||
const Individual = {
|
||||
name: "Individual",
|
||||
lg: {
|
||||
tr: "Birey",
|
||||
en: "Individual",
|
||||
},
|
||||
siteUrl: "/individual",
|
||||
};
|
||||
|
||||
const User = {
|
||||
name: "User",
|
||||
lg: {
|
||||
tr: "Kullanıcı",
|
||||
en: "User",
|
||||
},
|
||||
siteUrl: "/user",
|
||||
};
|
||||
|
||||
const Build = {
|
||||
name: "Build",
|
||||
lg: {
|
||||
tr: "Apartman",
|
||||
en: "Build",
|
||||
},
|
||||
siteUrl: "/build",
|
||||
};
|
||||
|
||||
const Dashboard = {
|
||||
name: "Dashboard",
|
||||
lg: {
|
||||
tr: "Pano",
|
||||
en: "Dashboard",
|
||||
},
|
||||
siteUrl: "/dashboard",
|
||||
};
|
||||
|
||||
const BuildParts = {
|
||||
name: "BuildParts",
|
||||
lg: {
|
||||
tr: "Daireler",
|
||||
en: "Build Parts",
|
||||
},
|
||||
siteUrl: "/build/parts",
|
||||
};
|
||||
|
||||
const BuildArea = {
|
||||
name: "BuildArea",
|
||||
lg: {
|
||||
tr: "Daire Alanları",
|
||||
en: "Build Area",
|
||||
},
|
||||
siteUrl: "/build/area",
|
||||
};
|
||||
|
||||
const ManagementAccounting = {
|
||||
name: "ManagementAccounting",
|
||||
lg: {
|
||||
tr: "Yönetim Cari Hareketler",
|
||||
en: "ManagementAccounting",
|
||||
},
|
||||
siteUrl: "/management/accounting",
|
||||
};
|
||||
|
||||
const ManagementBudget = {
|
||||
name: "ManagementBudget",
|
||||
lg: {
|
||||
tr: "Yönetim Bütçe İşlemleri",
|
||||
en: "Management Budget",
|
||||
},
|
||||
siteUrl: "/management/budget",
|
||||
};
|
||||
|
||||
const BuildPartsAccounting = {
|
||||
name: "BuildPartsAccounting",
|
||||
lg: {
|
||||
tr: "Daire Cari Hareketler",
|
||||
en: "Build Parts Accounting",
|
||||
},
|
||||
siteUrl: "/build/parts/accounting",
|
||||
};
|
||||
|
||||
const AnnualMeeting = {
|
||||
name: "AnnualMeeting",
|
||||
lg: {
|
||||
tr: "Yıllık Olağan Toplantı Tanımlama ve Davet",
|
||||
en: "Annual Meetings and Invitations",
|
||||
},
|
||||
siteUrl: "/annual/meeting",
|
||||
};
|
||||
|
||||
const AnnualMeetingClose = {
|
||||
name: "AnnualMeetingClose",
|
||||
lg: {
|
||||
tr: "Yıllık Olağan Toplantı kapatma ve Cari Yaratma",
|
||||
en: "Annual Meeting Close and Accountings",
|
||||
},
|
||||
siteUrl: "/annual/meeting/close",
|
||||
};
|
||||
|
||||
const EmergencyMeeting = {
|
||||
name: "EmergencyMeeting",
|
||||
lg: {
|
||||
tr: "Acil Toplantı Tanımlama ve Davet",
|
||||
en: "Emergency Meeting and Invitations",
|
||||
},
|
||||
siteUrl: "/emergency/meeting",
|
||||
};
|
||||
|
||||
const EmergencyMeetingClose = {
|
||||
name: "EmergencyMeetingClose",
|
||||
lg: {
|
||||
tr: "Acil Olağan Toplantı kapatma ve Cari Yaratma",
|
||||
en: "Emergency Meeting Close and Accountings",
|
||||
},
|
||||
siteUrl: "/emergency/meeting/close",
|
||||
};
|
||||
|
||||
const MeetingParticipations = {
|
||||
name: "MeetingParticipations",
|
||||
lg: {
|
||||
tr: "Toplantı Katılım İşlemleri",
|
||||
en: "Meeting Participations",
|
||||
},
|
||||
siteUrl: "/meeting/participation",
|
||||
};
|
||||
|
||||
const Menu = [
|
||||
{
|
||||
name: "Dashboard",
|
||||
lg: {
|
||||
tr: "Pano",
|
||||
en: "Dashboard",
|
||||
},
|
||||
subList: [
|
||||
{
|
||||
name: "Dashboard",
|
||||
lg: {
|
||||
tr: "Pano",
|
||||
en: "Dashboard",
|
||||
},
|
||||
subList: [Dashboard],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Definitions",
|
||||
lg: {
|
||||
tr: "Tanımlar",
|
||||
en: "Definitions",
|
||||
},
|
||||
subList: [
|
||||
{
|
||||
name: "People",
|
||||
lg: {
|
||||
tr: "Kişiler",
|
||||
en: "People",
|
||||
},
|
||||
subList: [Individual, User],
|
||||
},
|
||||
{
|
||||
name: "Building",
|
||||
lg: {
|
||||
tr: "Binalar",
|
||||
en: "Building",
|
||||
},
|
||||
subList: [Build, BuildParts, BuildArea],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Building Management",
|
||||
lg: {
|
||||
tr: "Bina Yönetimi",
|
||||
en: "Building Management",
|
||||
},
|
||||
subList: [
|
||||
{
|
||||
name: "Management Accounting",
|
||||
lg: {
|
||||
tr: "Cari işlemler",
|
||||
en: "Management Accounting",
|
||||
},
|
||||
subList: [ManagementAccounting, ManagementBudget, BuildPartsAccounting],
|
||||
},
|
||||
{
|
||||
name: "Meetings",
|
||||
lg: {
|
||||
tr: "Toplantılar",
|
||||
en: "Meetings",
|
||||
},
|
||||
subList: [
|
||||
AnnualMeeting,
|
||||
AnnualMeetingClose,
|
||||
EmergencyMeeting,
|
||||
EmergencyMeetingClose,
|
||||
MeetingParticipations,
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export default Menu;
|
||||
Reference in New Issue
Block a user