updated client-frontend additions
This commit is contained in:
@@ -17,14 +17,14 @@ async function checkAccessTokenIsValid() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function retrievePageList() {
|
async function retrievePageList() {
|
||||||
const response = await fetchDataWithToken(siteUrls, {}, "GET", false);
|
const response: any = await fetchDataWithToken(siteUrls, {}, "GET", false);
|
||||||
return response?.status === 200 || response?.status === 202
|
return response?.status === 200 || response?.status === 202
|
||||||
? response.data?.sites
|
? response.data?.sites
|
||||||
: null;
|
: null;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function retrievePagebyUrl(pageUrl: string) {
|
async function retrieveApplicationbyUrl(pageUrl: string) {
|
||||||
const response = await fetchDataWithToken(
|
const response: any = await fetchDataWithToken(
|
||||||
pageValid,
|
pageValid,
|
||||||
{
|
{
|
||||||
page_url: pageUrl,
|
page_url: pageUrl,
|
||||||
@@ -142,7 +142,7 @@ export {
|
|||||||
retrieveUserType,
|
retrieveUserType,
|
||||||
retrieveAccessObjects,
|
retrieveAccessObjects,
|
||||||
retrieveUserSelection,
|
retrieveUserSelection,
|
||||||
retrievePagebyUrl,
|
retrieveApplicationbyUrl,
|
||||||
retrievePageList,
|
retrievePageList,
|
||||||
// retrieveavailablePages,
|
// retrieveavailablePages,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,16 +1,6 @@
|
|||||||
import React from "react";
|
'use server';
|
||||||
import Login from "@/components/auth/login";
|
import Login from "@/webPages/auth/Login/page";
|
||||||
import { Metadata } from "next";
|
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
const LoginPage = async () => { return <Login language="en" /> };
|
||||||
title: "WAG Login",
|
|
||||||
description: "Login to WAG system",
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function LoginPage() {
|
export default LoginPage;
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Login />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -5,38 +5,17 @@ import {
|
|||||||
retrieveUserType,
|
retrieveUserType,
|
||||||
} from "@/apicalls/cookies/token";
|
} from "@/apicalls/cookies/token";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import LoginEmployee from "@/components/auth/LoginEmployee";
|
import Select from "@/webPages/auth/Select/page";
|
||||||
import LoginOccupant from "@/components/auth/LoginOccupant";
|
|
||||||
|
|
||||||
async function SelectPage() {
|
const SelectPage = async () => {
|
||||||
const token_is_valid = await checkAccessTokenIsValid();
|
const token_is_valid = await checkAccessTokenIsValid();
|
||||||
const selection = await retrieveUserType();
|
const selection = await retrieveUserType();
|
||||||
console.log("selection", selection);
|
|
||||||
|
|
||||||
const isEmployee = selection?.userType == "employee";
|
const isEmployee = selection?.userType == "employee";
|
||||||
const isOccupant = selection?.userType == "occupant";
|
const isOccupant = selection?.userType == "occupant";
|
||||||
|
|
||||||
const selectionList = selection?.selectionList;
|
const selectionList = selection?.selectionList;
|
||||||
|
|
||||||
if (!selectionList || !token_is_valid) {
|
if (!selectionList || !token_is_valid) { redirect("/auth/login") }
|
||||||
redirect("/auth/login");
|
return <Select selectionList={selectionList} isEmployee={isEmployee} isOccupant={isOccupant} language={"en"} />
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className="flex flex-col items-center justify-center h-screen">
|
|
||||||
<div className="flex flex-col items-center justify-center">
|
|
||||||
{isEmployee && Array.isArray(selectionList) && (
|
|
||||||
<LoginEmployee selectionList={selectionList} />
|
|
||||||
)}
|
|
||||||
|
|
||||||
{isOccupant && !Array.isArray(selectionList) && (
|
|
||||||
<LoginOccupant selectionList={selectionList} />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default SelectPage;
|
export default SelectPage;
|
||||||
|
|||||||
@@ -1,51 +1,27 @@
|
|||||||
"use server";
|
"use server";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import LeftMenu from "@/components/menu/leftMenu";
|
import DashboardLayout from "@/components/layouts/DashboardLayout";
|
||||||
import { retrievePageList, retrievePagebyUrl } from "@/apicalls/cookies/token";
|
import { useDashboardPage } from "@/components/common/hooks/useDashboardPage";
|
||||||
import { retrievePage } from "@/components/NavigatePages";
|
|
||||||
|
|
||||||
export default async function Dashboard({
|
export default async function Dashboard({
|
||||||
searchParams,
|
searchParams,
|
||||||
}: {
|
}: {
|
||||||
searchParams: Promise<{ [key: string]: string | undefined }>;
|
searchParams: Promise<{ [key: string]: string | undefined }>;
|
||||||
}) {
|
}) {
|
||||||
const siteUrlsList = (await retrievePageList()) || [];
|
const {
|
||||||
const lang = "tr";
|
activePage,
|
||||||
const searchParamsInstance = await searchParams;
|
searchParamsInstance,
|
||||||
const activePage = "/annual/meeting/close";
|
lang,
|
||||||
const pageToDirect = await retrievePagebyUrl(activePage);
|
PageComponent,
|
||||||
const PageComponent = retrievePage(pageToDirect);
|
siteUrlsList,
|
||||||
|
} = await useDashboardPage({
|
||||||
|
pageUrl: "/annual/meeting/close",
|
||||||
|
searchParams
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<DashboardLayout lang={lang} activePage={activePage} siteUrls={siteUrlsList} >
|
||||||
<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}
|
|
||||||
/>
|
|
||||||
</aside>
|
|
||||||
|
|
||||||
{/* Main Content Area */}
|
|
||||||
<div className="flex flex-col w-3/4">
|
|
||||||
{/* Sticky Header */}
|
|
||||||
<header className="sticky top-0 bg-white shadow-md z-10 p-4 flex justify-between items-center">
|
|
||||||
<h1 className="text-2xl font-semibold">{activePage}</h1>
|
|
||||||
<div className="flex items-center space-x-4">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder="Search..."
|
|
||||||
className="border px-3 py-2 rounded-lg"
|
|
||||||
/>
|
|
||||||
<div className="w-10 h-10 bg-gray-300 rounded-full"></div>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
<PageComponent lang={lang} queryParams={searchParamsInstance} />
|
<PageComponent lang={lang} queryParams={searchParamsInstance} />
|
||||||
</div>
|
</DashboardLayout>
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,51 +1,27 @@
|
|||||||
"use server";
|
"use server";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import LeftMenu from "@/components/menu/leftMenu";
|
import DashboardLayout from "@/components/layouts/DashboardLayout";
|
||||||
import { retrievePageList, retrievePagebyUrl } from "@/apicalls/cookies/token";
|
import { useDashboardPage } from "@/components/common/hooks/useDashboardPage";
|
||||||
import { retrievePage } from "@/components/NavigatePages";
|
|
||||||
|
|
||||||
export default async function Dashboard({
|
export default async function Dashboard({
|
||||||
searchParams,
|
searchParams,
|
||||||
}: {
|
}: {
|
||||||
searchParams: Promise<{ [key: string]: string | undefined }>;
|
searchParams: Promise<{ [key: string]: string | undefined }>;
|
||||||
}) {
|
}) {
|
||||||
const siteUrlsList = (await retrievePageList()) || [];
|
const {
|
||||||
const lang = "tr";
|
activePage,
|
||||||
const searchParamsInstance = await searchParams;
|
searchParamsInstance,
|
||||||
const activePage = "/annual/meeting";
|
lang,
|
||||||
const pageToDirect = await retrievePagebyUrl(activePage);
|
PageComponent,
|
||||||
const PageComponent = retrievePage(pageToDirect);
|
siteUrlsList,
|
||||||
|
} = await useDashboardPage({
|
||||||
|
pageUrl: "/annual/meeting",
|
||||||
|
searchParams
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<DashboardLayout lang={lang} activePage={activePage} siteUrls={siteUrlsList} >
|
||||||
<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}
|
|
||||||
/>
|
|
||||||
</aside>
|
|
||||||
|
|
||||||
{/* Main Content Area */}
|
|
||||||
<div className="flex flex-col w-3/4">
|
|
||||||
{/* Sticky Header */}
|
|
||||||
<header className="sticky top-0 bg-white shadow-md z-10 p-4 flex justify-between items-center">
|
|
||||||
<h1 className="text-2xl font-semibold">{activePage}</h1>
|
|
||||||
<div className="flex items-center space-x-4">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder="Search..."
|
|
||||||
className="border px-3 py-2 rounded-lg"
|
|
||||||
/>
|
|
||||||
<div className="w-10 h-10 bg-gray-300 rounded-full"></div>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
<PageComponent lang={lang} queryParams={searchParamsInstance} />
|
<PageComponent lang={lang} queryParams={searchParamsInstance} />
|
||||||
</div>
|
</DashboardLayout>
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
"use server";
|
||||||
|
import React from "react";
|
||||||
|
import DashboardLayout from "@/components/layouts/DashboardLayout";
|
||||||
|
import { useDashboardPage } from "@/components/common/hooks/useDashboardPage";
|
||||||
|
|
||||||
|
export default async function Dashboard({
|
||||||
|
searchParams,
|
||||||
|
}: {
|
||||||
|
searchParams: Promise<{ [key: string]: string | undefined }>;
|
||||||
|
}) {
|
||||||
|
const {
|
||||||
|
activePage,
|
||||||
|
searchParamsInstance,
|
||||||
|
lang,
|
||||||
|
PageComponent,
|
||||||
|
siteUrlsList,
|
||||||
|
} = await useDashboardPage({
|
||||||
|
pageUrl: "/build/area",
|
||||||
|
searchParams
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DashboardLayout lang={lang} activePage={activePage} siteUrls={siteUrlsList} >
|
||||||
|
<PageComponent lang={lang} queryParams={searchParamsInstance} />
|
||||||
|
</DashboardLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
"use server";
|
||||||
|
import React from "react";
|
||||||
|
import DashboardLayout from "@/components/layouts/DashboardLayout";
|
||||||
|
import { useDashboardPage } from "@/components/common/hooks/useDashboardPage";
|
||||||
|
|
||||||
|
export default async function Dashboard({
|
||||||
|
searchParams,
|
||||||
|
}: {
|
||||||
|
searchParams: Promise<{ [key: string]: string | undefined }>;
|
||||||
|
}) {
|
||||||
|
const {
|
||||||
|
activePage,
|
||||||
|
searchParamsInstance,
|
||||||
|
lang,
|
||||||
|
PageComponent,
|
||||||
|
siteUrlsList,
|
||||||
|
} = await useDashboardPage({
|
||||||
|
pageUrl: "/build",
|
||||||
|
searchParams
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DashboardLayout lang={lang} activePage={activePage} siteUrls={siteUrlsList} >
|
||||||
|
<PageComponent lang={lang} queryParams={searchParamsInstance} />
|
||||||
|
</DashboardLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,38 +1,27 @@
|
|||||||
"use server";
|
"use server";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { retrievePageList, retrievePagebyUrl } from "@/apicalls/cookies/token";
|
import DashboardLayout from "@/components/layouts/DashboardLayout";
|
||||||
|
import { useDashboardPage } from "@/components/common/hooks/useDashboardPage";
|
||||||
|
|
||||||
import ClientMenu from "@/components/menu/menu";
|
export default async function Dashboard({
|
||||||
import Header from "@/components/header/Header";
|
|
||||||
import retrievePageByUrlAndPageId from "@/components/navigator/retriever";
|
|
||||||
|
|
||||||
export default async function DashboardLayout({
|
|
||||||
searchParams,
|
searchParams,
|
||||||
}: {
|
}: {
|
||||||
searchParams: Promise<{ [key: string]: string | undefined }>;
|
searchParams: Promise<{ [key: string]: string | undefined }>;
|
||||||
}) {
|
}) {
|
||||||
const activePage = "/dashboard";
|
const {
|
||||||
const siteUrlsList = (await retrievePageList()) || [];
|
activePage,
|
||||||
const pageToDirect = await retrievePagebyUrl(activePage);
|
searchParamsInstance,
|
||||||
const PageComponent = retrievePageByUrlAndPageId(pageToDirect, activePage);
|
lang,
|
||||||
const searchParamsInstance = await searchParams;
|
PageComponent,
|
||||||
const lang = (searchParamsInstance?.lang as "en" | "tr") || "en";
|
siteUrlsList,
|
||||||
|
} = await useDashboardPage({
|
||||||
|
pageUrl: "/dashboard",
|
||||||
|
searchParams
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<DashboardLayout lang={lang} activePage={activePage} siteUrls={siteUrlsList} >
|
||||||
<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">
|
|
||||||
<ClientMenu siteUrls={siteUrlsList} lang={lang} />
|
|
||||||
</aside>
|
|
||||||
|
|
||||||
{/* Main Content Area */}
|
|
||||||
<div className="flex flex-col w-3/4">
|
|
||||||
{/* Header Component */}
|
|
||||||
<Header lang={lang} />
|
|
||||||
<PageComponent lang={lang} queryParams={searchParamsInstance} />
|
<PageComponent lang={lang} queryParams={searchParamsInstance} />
|
||||||
</div>
|
</DashboardLayout>
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,51 +1,27 @@
|
|||||||
"use server";
|
"use server";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import LeftMenu from "@/components/menu/leftMenu";
|
import DashboardLayout from "@/components/layouts/DashboardLayout";
|
||||||
import { retrievePageList, retrievePagebyUrl } from "@/apicalls/cookies/token";
|
import { useDashboardPage } from "@/components/common/hooks/useDashboardPage";
|
||||||
import { retrievePage } from "@/components/NavigatePages";
|
|
||||||
|
|
||||||
export default async function Dashboard({
|
export default async function Dashboard({
|
||||||
searchParams,
|
searchParams,
|
||||||
}: {
|
}: {
|
||||||
searchParams: Promise<{ [key: string]: string | undefined }>;
|
searchParams: Promise<{ [key: string]: string | undefined }>;
|
||||||
}) {
|
}) {
|
||||||
const siteUrlsList = (await retrievePageList()) || [];
|
const {
|
||||||
const lang = "tr";
|
activePage,
|
||||||
const searchParamsInstance = await searchParams;
|
searchParamsInstance,
|
||||||
const activePage = "/emergency/meeting/close";
|
lang,
|
||||||
const pageToDirect = await retrievePagebyUrl(activePage);
|
PageComponent,
|
||||||
const PageComponent = retrievePage(pageToDirect);
|
siteUrlsList,
|
||||||
|
} = await useDashboardPage({
|
||||||
|
pageUrl: "/emergency/meeting/close",
|
||||||
|
searchParams
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<DashboardLayout lang={lang} activePage={activePage} siteUrls={siteUrlsList} >
|
||||||
<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}
|
|
||||||
/>
|
|
||||||
</aside>
|
|
||||||
|
|
||||||
{/* Main Content Area */}
|
|
||||||
<div className="flex flex-col w-3/4">
|
|
||||||
{/* Sticky Header */}
|
|
||||||
<header className="sticky top-0 bg-white shadow-md z-10 p-4 flex justify-between items-center">
|
|
||||||
<h1 className="text-2xl font-semibold">{activePage}</h1>
|
|
||||||
<div className="flex items-center space-x-4">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder="Search..."
|
|
||||||
className="border px-3 py-2 rounded-lg"
|
|
||||||
/>
|
|
||||||
<div className="w-10 h-10 bg-gray-300 rounded-full"></div>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
<PageComponent lang={lang} queryParams={searchParamsInstance} />
|
<PageComponent lang={lang} queryParams={searchParamsInstance} />
|
||||||
</div>
|
</DashboardLayout>
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,51 +1,27 @@
|
|||||||
"use server";
|
"use server";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import LeftMenu from "@/components/menu/leftMenu";
|
import DashboardLayout from "@/components/layouts/DashboardLayout";
|
||||||
import { retrievePageList, retrievePagebyUrl } from "@/apicalls/cookies/token";
|
import { useDashboardPage } from "@/components/common/hooks/useDashboardPage";
|
||||||
import { retrievePage } from "@/components/NavigatePages";
|
|
||||||
|
|
||||||
export default async function Dashboard({
|
export default async function Dashboard({
|
||||||
searchParams,
|
searchParams,
|
||||||
}: {
|
}: {
|
||||||
searchParams: Promise<{ [key: string]: string | undefined }>;
|
searchParams: Promise<{ [key: string]: string | undefined }>;
|
||||||
}) {
|
}) {
|
||||||
const siteUrlsList = (await retrievePageList()) || [];
|
const {
|
||||||
const lang = "tr";
|
activePage,
|
||||||
const searchParamsInstance = await searchParams;
|
searchParamsInstance,
|
||||||
const activePage = "/emergency/meeting";
|
lang,
|
||||||
const pageToDirect = await retrievePagebyUrl(activePage);
|
PageComponent,
|
||||||
const PageComponent = retrievePage(pageToDirect);
|
siteUrlsList,
|
||||||
|
} = await useDashboardPage({
|
||||||
|
pageUrl: "/emergency/meeting",
|
||||||
|
searchParams
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<DashboardLayout lang={lang} activePage={activePage} siteUrls={siteUrlsList} >
|
||||||
<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}
|
|
||||||
/>
|
|
||||||
</aside>
|
|
||||||
|
|
||||||
{/* Main Content Area */}
|
|
||||||
<div className="flex flex-col w-3/4">
|
|
||||||
{/* Sticky Header */}
|
|
||||||
<header className="sticky top-0 bg-white shadow-md z-10 p-4 flex justify-between items-center">
|
|
||||||
<h1 className="text-2xl font-semibold">{activePage}</h1>
|
|
||||||
<div className="flex items-center space-x-4">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder="Search..."
|
|
||||||
className="border px-3 py-2 rounded-lg"
|
|
||||||
/>
|
|
||||||
<div className="w-10 h-10 bg-gray-300 rounded-full"></div>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
<PageComponent lang={lang} queryParams={searchParamsInstance} />
|
<PageComponent lang={lang} queryParams={searchParamsInstance} />
|
||||||
</div>
|
</DashboardLayout>
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,13 +8,12 @@ export default async function Dashboard({
|
|||||||
}: {
|
}: {
|
||||||
searchParams: Promise<{ [key: string]: string | undefined }>;
|
searchParams: Promise<{ [key: string]: string | undefined }>;
|
||||||
}) {
|
}) {
|
||||||
// Use the enhanced dashboard hook to get all necessary data
|
|
||||||
const {
|
const {
|
||||||
activePage,
|
activePage,
|
||||||
searchParamsInstance,
|
searchParamsInstance,
|
||||||
lang,
|
lang,
|
||||||
PageComponent,
|
PageComponent,
|
||||||
siteUrlsList
|
siteUrlsList,
|
||||||
} = await useDashboardPage({
|
} = await useDashboardPage({
|
||||||
pageUrl: "/individual",
|
pageUrl: "/individual",
|
||||||
searchParams
|
searchParams
|
||||||
|
|||||||
@@ -1,37 +1,27 @@
|
|||||||
"use server";
|
"use server";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { retrievePageList, retrievePagebyUrl } from "@/apicalls/cookies/token";
|
import DashboardLayout from "@/components/layouts/DashboardLayout";
|
||||||
import retrievePageByUrlAndPageId from "@/components/navigator/retriever";
|
import { useDashboardPage } from "@/components/common/hooks/useDashboardPage";
|
||||||
import ClientMenu from "@/components/menu/menu";
|
|
||||||
import Header from "@/components/header/Header";
|
|
||||||
|
|
||||||
export default async function Dashboard({
|
export default async function Dashboard({
|
||||||
searchParams,
|
searchParams,
|
||||||
}: {
|
}: {
|
||||||
searchParams: Promise<{ [key: string]: string | undefined }>;
|
searchParams: Promise<{ [key: string]: string | undefined }>;
|
||||||
}) {
|
}) {
|
||||||
const siteUrlsList = (await retrievePageList()) || [];
|
const {
|
||||||
const lang = "tr";
|
activePage,
|
||||||
const searchParamsInstance = await searchParams;
|
searchParamsInstance,
|
||||||
const activePage = "/management/accounting";
|
lang,
|
||||||
const pageToDirect = await retrievePagebyUrl(activePage);
|
PageComponent,
|
||||||
const PageComponent = retrievePageByUrlAndPageId(activePage, pageToDirect);
|
siteUrlsList,
|
||||||
|
} = await useDashboardPage({
|
||||||
|
pageUrl: "/management/accounting",
|
||||||
|
searchParams
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<DashboardLayout lang={lang} activePage={activePage} siteUrls={siteUrlsList} >
|
||||||
<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">
|
|
||||||
<ClientMenu lang={lang} siteUrls={siteUrlsList} />
|
|
||||||
</aside>
|
|
||||||
|
|
||||||
{/* Main Content Area */}
|
|
||||||
<div className="flex flex-col w-3/4">
|
|
||||||
{/* Sticky Header */}
|
|
||||||
<Header lang={lang} />
|
|
||||||
<PageComponent lang={lang} queryParams={searchParamsInstance} />
|
<PageComponent lang={lang} queryParams={searchParamsInstance} />
|
||||||
</div>
|
</DashboardLayout>
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,37 +1,27 @@
|
|||||||
"use server";
|
"use server";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { retrievePageList, retrievePagebyUrl } from "@/apicalls/cookies/token";
|
import DashboardLayout from "@/components/layouts/DashboardLayout";
|
||||||
import retrievePageByUrlAndPageId from "@/components/navigator/retriever";
|
import { useDashboardPage } from "@/components/common/hooks/useDashboardPage";
|
||||||
import ClientMenu from "@/components/menu/menu";
|
|
||||||
import Header from "@/components/header/Header";
|
|
||||||
|
|
||||||
export default async function Dashboard({
|
export default async function Dashboard({
|
||||||
searchParams,
|
searchParams,
|
||||||
}: {
|
}: {
|
||||||
searchParams: Promise<{ [key: string]: string | undefined }>;
|
searchParams: Promise<{ [key: string]: string | undefined }>;
|
||||||
}) {
|
}) {
|
||||||
const siteUrlsList = (await retrievePageList()) || [];
|
const {
|
||||||
const lang = "tr";
|
activePage,
|
||||||
const searchParamsInstance = await searchParams;
|
searchParamsInstance,
|
||||||
const activePage = "/management/budget";
|
lang,
|
||||||
const pageToDirect = await retrievePagebyUrl(activePage);
|
PageComponent,
|
||||||
const PageComponent = retrievePageByUrlAndPageId(activePage, pageToDirect);
|
siteUrlsList,
|
||||||
|
} = await useDashboardPage({
|
||||||
|
pageUrl: "/management/budget",
|
||||||
|
searchParams
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<DashboardLayout lang={lang} activePage={activePage} siteUrls={siteUrlsList} >
|
||||||
<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">
|
|
||||||
<ClientMenu lang={lang} siteUrls={siteUrlsList} />
|
|
||||||
</aside>
|
|
||||||
|
|
||||||
{/* Main Content Area */}
|
|
||||||
<div className="flex flex-col w-3/4">
|
|
||||||
{/* Sticky Header */}
|
|
||||||
<Header lang={lang} />
|
|
||||||
<PageComponent lang={lang} queryParams={searchParamsInstance} />
|
<PageComponent lang={lang} queryParams={searchParamsInstance} />
|
||||||
</div>
|
</DashboardLayout>
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,52 +1,27 @@
|
|||||||
"use server";
|
"use server";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import LeftMenu from "@/components/menu/leftMenu";
|
import DashboardLayout from "@/components/layouts/DashboardLayout";
|
||||||
|
import { useDashboardPage } from "@/components/common/hooks/useDashboardPage";
|
||||||
import { retrievePageList, retrievePagebyUrl } from "@/apicalls/cookies/token";
|
|
||||||
import { retrievePage } from "@/components/NavigatePages";
|
|
||||||
|
|
||||||
export default async function Dashboard({
|
export default async function Dashboard({
|
||||||
searchParams,
|
searchParams,
|
||||||
}: {
|
}: {
|
||||||
searchParams: Promise<{ [key: string]: string | undefined }>;
|
searchParams: Promise<{ [key: string]: string | undefined }>;
|
||||||
}) {
|
}) {
|
||||||
const siteUrlsList = (await retrievePageList()) || [];
|
const {
|
||||||
const lang = "tr";
|
activePage,
|
||||||
const searchParamsInstance = await searchParams;
|
searchParamsInstance,
|
||||||
const activePage = "/meeting/participation";
|
lang,
|
||||||
const pageToDirect = await retrievePagebyUrl(activePage);
|
PageComponent,
|
||||||
const PageComponent = retrievePage(pageToDirect);
|
siteUrlsList,
|
||||||
|
} = await useDashboardPage({
|
||||||
|
pageUrl: "/meeting/participation",
|
||||||
|
searchParams
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<DashboardLayout lang={lang} activePage={activePage} siteUrls={siteUrlsList} >
|
||||||
<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}
|
|
||||||
/>
|
|
||||||
</aside>
|
|
||||||
|
|
||||||
{/* Main Content Area */}
|
|
||||||
<div className="flex flex-col w-3/4">
|
|
||||||
{/* Sticky Header */}
|
|
||||||
<header className="sticky top-0 bg-white shadow-md z-10 p-4 flex justify-between items-center">
|
|
||||||
<h1 className="text-2xl font-semibold">{activePage}</h1>
|
|
||||||
<div className="flex items-center space-x-4">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder="Search..."
|
|
||||||
className="border px-3 py-2 rounded-lg"
|
|
||||||
/>
|
|
||||||
<div className="w-10 h-10 bg-gray-300 rounded-full"></div>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
<PageComponent lang={lang} queryParams={searchParamsInstance} />
|
<PageComponent lang={lang} queryParams={searchParamsInstance} />
|
||||||
</div>
|
</DashboardLayout>
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"use server";
|
"use server";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import Template from "@/components/Pages/template/app";
|
import Template from "@/eventRouters/Pages/template/app";
|
||||||
import ClientMenu from "@/components/menu/menu";
|
import ClientMenu from "@/components/menu/menu";
|
||||||
import { retrievePage } from "@/components/NavigatePages";
|
import { retrievePage } from "@/components/NavigatePages";
|
||||||
import { retrievePageList, retrievePagebyUrl } from "@/apicalls/cookies/token";
|
import { retrievePageList, retrievePagebyUrl } from "@/apicalls/cookies/token";
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
"use server";
|
||||||
|
import React from "react";
|
||||||
|
import DashboardLayout from "@/components/layouts/DashboardLayout";
|
||||||
|
import { useDashboardPage } from "@/components/common/hooks/useDashboardPage";
|
||||||
|
|
||||||
|
export default async function Dashboard({
|
||||||
|
searchParams,
|
||||||
|
}: {
|
||||||
|
searchParams: Promise<{ [key: string]: string | undefined }>;
|
||||||
|
}) {
|
||||||
|
const {
|
||||||
|
activePage,
|
||||||
|
searchParamsInstance,
|
||||||
|
lang,
|
||||||
|
PageComponent,
|
||||||
|
siteUrlsList,
|
||||||
|
} = await useDashboardPage({
|
||||||
|
pageUrl: "/tenant/accounting",
|
||||||
|
searchParams
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DashboardLayout lang={lang} activePage={activePage} siteUrls={siteUrlsList} >
|
||||||
|
<PageComponent lang={lang} queryParams={searchParamsInstance} />
|
||||||
|
</DashboardLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
"use server";
|
||||||
|
import React from "react";
|
||||||
|
import DashboardLayout from "@/components/layouts/DashboardLayout";
|
||||||
|
import { useDashboardPage } from "@/components/common/hooks/useDashboardPage";
|
||||||
|
|
||||||
|
export default async function Dashboard({
|
||||||
|
searchParams,
|
||||||
|
}: {
|
||||||
|
searchParams: Promise<{ [key: string]: string | undefined }>;
|
||||||
|
}) {
|
||||||
|
const {
|
||||||
|
activePage,
|
||||||
|
searchParamsInstance,
|
||||||
|
lang,
|
||||||
|
PageComponent,
|
||||||
|
siteUrlsList,
|
||||||
|
} = await useDashboardPage({
|
||||||
|
pageUrl: "/tenant/messageToBM",
|
||||||
|
searchParams
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DashboardLayout lang={lang} activePage={activePage} siteUrls={siteUrlsList} >
|
||||||
|
<PageComponent lang={lang} queryParams={searchParamsInstance} />
|
||||||
|
</DashboardLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
"use server";
|
||||||
|
import React from "react";
|
||||||
|
import DashboardLayout from "@/components/layouts/DashboardLayout";
|
||||||
|
import { useDashboardPage } from "@/components/common/hooks/useDashboardPage";
|
||||||
|
|
||||||
|
export default async function Dashboard({
|
||||||
|
searchParams,
|
||||||
|
}: {
|
||||||
|
searchParams: Promise<{ [key: string]: string | undefined }>;
|
||||||
|
}) {
|
||||||
|
const {
|
||||||
|
activePage,
|
||||||
|
searchParamsInstance,
|
||||||
|
lang,
|
||||||
|
PageComponent,
|
||||||
|
siteUrlsList,
|
||||||
|
} = await useDashboardPage({
|
||||||
|
pageUrl: "/tenant/messageToOwner",
|
||||||
|
searchParams
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DashboardLayout lang={lang} activePage={activePage} siteUrls={siteUrlsList} >
|
||||||
|
<PageComponent lang={lang} queryParams={searchParamsInstance} />
|
||||||
|
</DashboardLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,51 +1,27 @@
|
|||||||
"use server";
|
"use server";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import LeftMenu from "@/components/menu/leftMenu";
|
import DashboardLayout from "@/components/layouts/DashboardLayout";
|
||||||
import { retrievePageList, retrievePagebyUrl } from "@/apicalls/cookies/token";
|
import { useDashboardPage } from "@/components/common/hooks/useDashboardPage";
|
||||||
import { retrievePage } from "@/components/NavigatePages";
|
|
||||||
|
|
||||||
export default async function Dashboard({
|
export default async function Dashboard({
|
||||||
searchParams,
|
searchParams,
|
||||||
}: {
|
}: {
|
||||||
searchParams: Promise<{ [key: string]: string | undefined }>;
|
searchParams: Promise<{ [key: string]: string | undefined }>;
|
||||||
}) {
|
}) {
|
||||||
const siteUrlsList = (await retrievePageList()) || [];
|
const {
|
||||||
const lang = "tr";
|
activePage,
|
||||||
const searchParamsInstance = await searchParams;
|
searchParamsInstance,
|
||||||
const activePage = "/user";
|
lang,
|
||||||
const pageToDirect = await retrievePagebyUrl(activePage);
|
PageComponent,
|
||||||
const PageComponent = retrievePage(pageToDirect);
|
siteUrlsList,
|
||||||
|
} = await useDashboardPage({
|
||||||
|
pageUrl: "/user",
|
||||||
|
searchParams
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<DashboardLayout lang={lang} activePage={activePage} siteUrls={siteUrlsList} >
|
||||||
<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}
|
|
||||||
/>
|
|
||||||
</aside>
|
|
||||||
|
|
||||||
{/* Main Content Area */}
|
|
||||||
<div className="flex flex-col w-3/4">
|
|
||||||
{/* Sticky Header */}
|
|
||||||
<header className="sticky top-0 bg-white shadow-md z-10 p-4 flex justify-between items-center">
|
|
||||||
<h1 className="text-2xl font-semibold">{activePage}</h1>
|
|
||||||
<div className="flex items-center space-x-4">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder="Search..."
|
|
||||||
className="border px-3 py-2 rounded-lg"
|
|
||||||
/>
|
|
||||||
<div className="w-10 h-10 bg-gray-300 rounded-full"></div>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
<PageComponent lang={lang} queryParams={searchParamsInstance} />
|
<PageComponent lang={lang} queryParams={searchParamsInstance} />
|
||||||
</div>
|
</DashboardLayout>
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { retrieveUserSelection } from "@/apicalls/cookies/token";
|
||||||
|
import { NextResponse } from "next/server";
|
||||||
|
|
||||||
|
export async function POST(): Promise<NextResponse> {
|
||||||
|
try {
|
||||||
|
const userSelection = await retrieveUserSelection();
|
||||||
|
console.log("userSelection", userSelection);
|
||||||
|
if (userSelection) {
|
||||||
|
return NextResponse.json({
|
||||||
|
status: 200,
|
||||||
|
message: "User selection found",
|
||||||
|
data: userSelection,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {}
|
||||||
|
return NextResponse.json({
|
||||||
|
status: 500,
|
||||||
|
message: "User selection not found",
|
||||||
|
});
|
||||||
|
}
|
||||||
39
WebServices/client-frontend/src/app/api/login/email/route.ts
Normal file
39
WebServices/client-frontend/src/app/api/login/email/route.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import { loginViaAccessKeys } from "@/apicalls/login/login";
|
||||||
|
import { NextResponse } from "next/server";
|
||||||
|
import { loginSchemaEmail } from "@/webPages/auth/Login/schemas";
|
||||||
|
|
||||||
|
export async function POST(req: Request): Promise<NextResponse> {
|
||||||
|
try {
|
||||||
|
const headers = req.headers;
|
||||||
|
console.log("headers", Object.entries(headers));
|
||||||
|
const body = await req.json();
|
||||||
|
const dataValidated = {
|
||||||
|
accessKey: body.email,
|
||||||
|
password: body.password,
|
||||||
|
rememberMe: body.rememberMe,
|
||||||
|
};
|
||||||
|
const validatedLoginBody = loginSchemaEmail.safeParse(body);
|
||||||
|
if (!validatedLoginBody.success) {
|
||||||
|
return NextResponse.json({
|
||||||
|
status: 422,
|
||||||
|
message: validatedLoginBody.error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const userLogin = await loginViaAccessKeys(dataValidated);
|
||||||
|
if (userLogin.status === 200 || userLogin.status === 202) {
|
||||||
|
return NextResponse.json({
|
||||||
|
status: 200,
|
||||||
|
message: "Login successfully completed",
|
||||||
|
data: userLogin.data,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return NextResponse.json({
|
||||||
|
status: userLogin.status,
|
||||||
|
message: userLogin.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
return NextResponse.json({ status: 401, message: "Invalid credentials" });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
import { loginSelectEmployee } from "@/apicalls/login/login";
|
||||||
|
import { NextResponse } from "next/server";
|
||||||
|
|
||||||
|
const loginSchemaEmployee = z.object({
|
||||||
|
company_uu_id: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export async function POST(req: Request): Promise<NextResponse> {
|
||||||
|
try {
|
||||||
|
const headers = req.headers;
|
||||||
|
console.log("headers", Object.entries(headers));
|
||||||
|
const body = await req.json();
|
||||||
|
const dataValidated = {
|
||||||
|
company_uu_id: body.company_uu_id,
|
||||||
|
};
|
||||||
|
const validatedLoginBody = loginSchemaEmployee.safeParse(body);
|
||||||
|
if (!validatedLoginBody.success) {
|
||||||
|
return NextResponse.json({
|
||||||
|
status: 422,
|
||||||
|
message: validatedLoginBody.error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const userLogin = await loginSelectEmployee(dataValidated);
|
||||||
|
if (userLogin.status === 200 || userLogin.status === 202) {
|
||||||
|
return NextResponse.json({
|
||||||
|
status: 200,
|
||||||
|
message: "Selection successfully completed",
|
||||||
|
data: userLogin.data,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return NextResponse.json({
|
||||||
|
status: userLogin.status,
|
||||||
|
message: userLogin.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
return NextResponse.json({ status: 401, message: "Invalid credentials" });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
import { loginSelectOccupant } from "@/apicalls/login/login";
|
||||||
|
import { NextResponse } from "next/server";
|
||||||
|
|
||||||
|
const loginSchemaOccupant = z.object({
|
||||||
|
build_living_space_uu_id: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export async function POST(req: Request): Promise<NextResponse> {
|
||||||
|
try {
|
||||||
|
const headers = req.headers;
|
||||||
|
console.log("headers", Object.entries(headers));
|
||||||
|
const body = await req.json();
|
||||||
|
const dataValidated = {
|
||||||
|
build_living_space_uu_id: body.build_living_space_uu_id,
|
||||||
|
};
|
||||||
|
const validatedLoginBody = loginSchemaOccupant.safeParse(body);
|
||||||
|
if (!validatedLoginBody.success) {
|
||||||
|
return NextResponse.json({
|
||||||
|
status: 422,
|
||||||
|
message: validatedLoginBody.error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const userLogin = await loginSelectOccupant(dataValidated);
|
||||||
|
if (userLogin.status === 200 || userLogin.status === 202) {
|
||||||
|
return NextResponse.json({
|
||||||
|
status: 200,
|
||||||
|
message: "Selection successfully completed",
|
||||||
|
data: userLogin.data,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return NextResponse.json({
|
||||||
|
status: userLogin.status,
|
||||||
|
message: userLogin.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
return NextResponse.json({ status: 401, message: "Invalid credentials" });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
"use server";
|
"use server";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
export default async function Home() {
|
export default async function Home() {
|
||||||
// Server-side rendering
|
|
||||||
const currentDate = new Date().toLocaleString("tr-TR", {
|
const currentDate = new Date().toLocaleString("tr-TR", {
|
||||||
timeZone: "Europe/Istanbul",
|
timeZone: "Europe/Istanbul",
|
||||||
});
|
});
|
||||||
@@ -19,70 +19,12 @@ export default async function Home() {
|
|||||||
{/* Login Section */}
|
{/* Login Section */}
|
||||||
<div className="p-8">
|
<div className="p-8">
|
||||||
<div className="bg-white rounded-lg p-6 border border-gray-200">
|
<div className="bg-white rounded-lg p-6 border border-gray-200">
|
||||||
<h2 className="text-2xl font-semibold text-center mb-6 text-gray-800">
|
<Link
|
||||||
Login to Your Account
|
href="/auth/login"
|
||||||
</h2>
|
className="w-full bg-blue-600 text-white py-2 px-4 rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition-colors"
|
||||||
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div>
|
|
||||||
<label
|
|
||||||
className="block text-gray-700 text-sm font-medium mb-2"
|
|
||||||
htmlFor="email"
|
|
||||||
>
|
>
|
||||||
Email Address
|
Go to Sign In
|
||||||
</label>
|
</Link>
|
||||||
<input
|
|
||||||
className="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
||||||
id="email"
|
|
||||||
type="email"
|
|
||||||
placeholder="Enter your email"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label
|
|
||||||
className="block text-gray-700 text-sm font-medium mb-2"
|
|
||||||
htmlFor="password"
|
|
||||||
>
|
|
||||||
Password
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
className="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
||||||
id="password"
|
|
||||||
type="password"
|
|
||||||
placeholder="Enter your password"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div className="flex items-center">
|
|
||||||
<input
|
|
||||||
id="remember_me"
|
|
||||||
type="checkbox"
|
|
||||||
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
|
|
||||||
/>
|
|
||||||
<label
|
|
||||||
htmlFor="remember_me"
|
|
||||||
className="ml-2 block text-sm text-gray-700"
|
|
||||||
>
|
|
||||||
Remember me
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="text-sm">
|
|
||||||
<a
|
|
||||||
href="#"
|
|
||||||
className="font-medium text-blue-600 hover:text-blue-500"
|
|
||||||
>
|
|
||||||
Forgot your password?
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button className="w-full bg-blue-600 text-white py-2 px-4 rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition-colors">
|
|
||||||
Sign In
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-6 text-center text-sm text-gray-600">
|
<div className="mt-6 text-center text-sm text-gray-600">
|
||||||
|
|||||||
@@ -1,146 +0,0 @@
|
|||||||
"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;
|
|
||||||
@@ -1,111 +0,0 @@
|
|||||||
"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,152 +0,0 @@
|
|||||||
"use client";
|
|
||||||
import { useState, useTransition } from "react";
|
|
||||||
import { useRouter } from "next/navigation";
|
|
||||||
import { useForm } from "react-hook-form";
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
|
||||||
import { loginViaAccessKeys } from "@/apicalls/login/login";
|
|
||||||
import { z } from "zod";
|
|
||||||
|
|
||||||
const loginSchema = z.object({
|
|
||||||
email: z.string().email("Invalid email address"),
|
|
||||||
password: z.string().min(5, "Password must be at least 5 characters"),
|
|
||||||
remember_me: z.boolean().optional().default(false),
|
|
||||||
});
|
|
||||||
|
|
||||||
type LoginFormData = {
|
|
||||||
email: string;
|
|
||||||
password: string;
|
|
||||||
remember_me?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
function Login() {
|
|
||||||
// Open transition for form login
|
|
||||||
const [isPending, startTransition] = useTransition();
|
|
||||||
const [error, setError] = useState<string | null>(null);
|
|
||||||
const [jsonText, setJsonText] = useState<string | null>(null);
|
|
||||||
|
|
||||||
const Router = useRouter();
|
|
||||||
|
|
||||||
const {
|
|
||||||
register,
|
|
||||||
formState: { errors },
|
|
||||||
handleSubmit,
|
|
||||||
} = useForm<LoginFormData>({
|
|
||||||
resolver: zodResolver(loginSchema),
|
|
||||||
});
|
|
||||||
|
|
||||||
const onSubmit = async (data: LoginFormData) => {
|
|
||||||
try {
|
|
||||||
startTransition(() => {
|
|
||||||
try {
|
|
||||||
loginViaAccessKeys({
|
|
||||||
accessKey: data.email,
|
|
||||||
password: data.password,
|
|
||||||
rememberMe: false,
|
|
||||||
})
|
|
||||||
.then((result: any) => {
|
|
||||||
const dataResponse = result?.data;
|
|
||||||
if (dataResponse?.access_token) {
|
|
||||||
setJsonText(JSON.stringify(dataResponse));
|
|
||||||
setTimeout(() => {
|
|
||||||
Router.push("/auth/select");
|
|
||||||
}, 2000);
|
|
||||||
}
|
|
||||||
return dataResponse;
|
|
||||||
})
|
|
||||||
.catch(() => {});
|
|
||||||
} catch (error) {}
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Login error:", error);
|
|
||||||
setError("An error occurred during login");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className="flex h-full min-h-[inherit] flex-col items-center justify-center gap-4">
|
|
||||||
<div className="w-full max-w-md rounded-lg bg-white p-8 shadow-md">
|
|
||||||
<h2 className="mb-6 text-center text-2xl font-bold text-gray-900">
|
|
||||||
Login
|
|
||||||
</h2>
|
|
||||||
<form onSubmit={handleSubmit(onSubmit)} className="space-y-6">
|
|
||||||
<div>
|
|
||||||
<label
|
|
||||||
htmlFor="email"
|
|
||||||
className="block text-sm font-medium text-gray-700"
|
|
||||||
>
|
|
||||||
Email
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
{...register("email")}
|
|
||||||
type="email"
|
|
||||||
id="email"
|
|
||||||
className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-indigo-500"
|
|
||||||
/>
|
|
||||||
{errors.email && (
|
|
||||||
<p className="mt-1 text-sm text-red-600">
|
|
||||||
{errors.email.message}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label
|
|
||||||
htmlFor="password"
|
|
||||||
className="block text-sm font-medium text-gray-700"
|
|
||||||
>
|
|
||||||
Password
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
{...register("password")}
|
|
||||||
type="password"
|
|
||||||
id="password"
|
|
||||||
className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-indigo-500"
|
|
||||||
/>
|
|
||||||
{errors.password && (
|
|
||||||
<p className="mt-1 text-sm text-red-600">
|
|
||||||
{errors.password.message}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{error && (
|
|
||||||
<div className="rounded-md bg-red-50 p-4">
|
|
||||||
<p className="text-sm text-red-700">{error}</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
disabled={isPending}
|
|
||||||
className="w-full rounded-md bg-indigo-600 px-4 py-2 text-white hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 disabled:opacity-50"
|
|
||||||
>
|
|
||||||
{isPending ? "Logging in..." : "Login"}
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{jsonText && (
|
|
||||||
<div className="w-full max-w-md rounded-lg bg-white p-8 shadow-md">
|
|
||||||
<h2 className="mb-4 text-center text-xl font-bold text-gray-900">
|
|
||||||
Response Data
|
|
||||||
</h2>
|
|
||||||
<div className="space-y-2">
|
|
||||||
{Object.entries(JSON.parse(jsonText)).map(([key, value]) => (
|
|
||||||
<div key={key} className="flex items-start gap-2">
|
|
||||||
<strong className="text-gray-700">{key}:</strong>
|
|
||||||
<span className="text-gray-600">
|
|
||||||
{typeof value === "object"
|
|
||||||
? JSON.stringify(value)
|
|
||||||
: value?.toString() || "N/A"}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Login;
|
|
||||||
@@ -1,87 +0,0 @@
|
|||||||
"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,
|
|
||||||
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);
|
|
||||||
loginSelectEmployee({ company_uu_id: uu_id })
|
|
||||||
.then((responseData: any) => {
|
|
||||||
if (responseData?.status === 200 || responseData?.status === 202) {
|
|
||||||
router.push("/dashboard");
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.error(error);
|
|
||||||
});
|
|
||||||
} else if (isOccupant) {
|
|
||||||
console.log("Selected isOccupant 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");
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.error(error);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{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}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default SelectList;
|
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
"use client";
|
||||||
|
import React from "react";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Plus } from "lucide-react";
|
||||||
|
|
||||||
|
interface CreateButtonProps {
|
||||||
|
onClick: () => void;
|
||||||
|
translations: Record<string, any>;
|
||||||
|
lang: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CreateButton: React.FC<CreateButtonProps> = ({
|
||||||
|
onClick,
|
||||||
|
translations,
|
||||||
|
lang,
|
||||||
|
}) => {
|
||||||
|
const t = translations[lang] || {};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button onClick={onClick} className="flex items-center">
|
||||||
|
<Plus className="mr-2 h-4 w-4" />
|
||||||
|
{t.create || "Create"}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
"use client";
|
||||||
|
import React from "react";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { CustomButton } from "./types";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
interface CustomButtonComponentProps {
|
||||||
|
button: CustomButton;
|
||||||
|
isSelected: boolean;
|
||||||
|
onClick: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CustomButtonComponent: React.FC<CustomButtonComponentProps> = ({
|
||||||
|
button,
|
||||||
|
isSelected,
|
||||||
|
onClick,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
onClick={onClick}
|
||||||
|
variant={button.variant || "default"}
|
||||||
|
className={cn(
|
||||||
|
"flex items-center",
|
||||||
|
isSelected && "ring-2 ring-primary ring-offset-2"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{button.icon && <span className="mr-2">{button.icon}</span>}
|
||||||
|
{button.label}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export * from './CreateButton';
|
||||||
|
export * from './CustomButtonComponent';
|
||||||
|
export * from './types';
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { ReactNode } from "react";
|
||||||
|
|
||||||
|
export interface CustomButton {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
onClick: () => void;
|
||||||
|
variant?: "default" | "destructive" | "outline" | "secondary" | "ghost" | "link";
|
||||||
|
icon?: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ActionButtonsProps {
|
||||||
|
onCreateClick: () => void;
|
||||||
|
translations: Record<string, any>;
|
||||||
|
lang: string;
|
||||||
|
customButtons?: CustomButton[];
|
||||||
|
defaultSelectedButtonId?: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
"use client";
|
||||||
|
import React from "react";
|
||||||
|
import { CardItem } from "./CardItem";
|
||||||
|
import { CardSkeleton } from "./CardSkeleton";
|
||||||
|
import { getFieldValue, getGridClasses } from "./utils";
|
||||||
|
import { CardDisplayProps } from "./schema";
|
||||||
|
import { GridSize } from "../HeaderSelections/GridSelectionComponent";
|
||||||
|
|
||||||
|
export function CardDisplay<T>({
|
||||||
|
showFields,
|
||||||
|
data,
|
||||||
|
lang,
|
||||||
|
translations,
|
||||||
|
error,
|
||||||
|
loading,
|
||||||
|
titleField,
|
||||||
|
onCardClick,
|
||||||
|
renderCustomField,
|
||||||
|
gridCols = 4,
|
||||||
|
showViewIcon = false,
|
||||||
|
showUpdateIcon = false,
|
||||||
|
onViewClick,
|
||||||
|
onUpdateClick,
|
||||||
|
size = "lg",
|
||||||
|
}: CardDisplayProps<T>) {
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<div className="p-6 text-center text-red-500">
|
||||||
|
{error.message || "An error occurred while fetching data."}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={getGridClasses(gridCols as GridSize)}>
|
||||||
|
{loading ? (
|
||||||
|
// Loading skeletons
|
||||||
|
Array.from({ length: 10 }).map((_, index) => (
|
||||||
|
<CardSkeleton
|
||||||
|
key={`loading-${index}`}
|
||||||
|
index={index}
|
||||||
|
showFields={showFields}
|
||||||
|
showViewIcon={showViewIcon}
|
||||||
|
showUpdateIcon={showUpdateIcon}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
) : data.length === 0 ? (
|
||||||
|
<div className="col-span-full text-center py-1">
|
||||||
|
{translations[lang].noData || "No data found"}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
data.map((item: T, index: number) => (
|
||||||
|
<CardItem
|
||||||
|
key={index}
|
||||||
|
item={item}
|
||||||
|
index={index}
|
||||||
|
showFields={showFields}
|
||||||
|
titleField={titleField}
|
||||||
|
lang={lang}
|
||||||
|
translations={translations}
|
||||||
|
showViewIcon={showViewIcon}
|
||||||
|
showUpdateIcon={showUpdateIcon}
|
||||||
|
onCardClick={onCardClick}
|
||||||
|
renderCustomField={renderCustomField}
|
||||||
|
onViewClick={onViewClick}
|
||||||
|
onUpdateClick={onUpdateClick}
|
||||||
|
getFieldValue={getFieldValue}
|
||||||
|
size={size}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,245 @@
|
|||||||
|
"use client";
|
||||||
|
import React from "react";
|
||||||
|
import { Card, CardContent, CardHeader } from "@/components/ui/card";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Eye, Edit } from "lucide-react";
|
||||||
|
import { CardItemProps, CardActionsProps, CardFieldProps } from "./schema";
|
||||||
|
|
||||||
|
export function CardItem<T>({
|
||||||
|
item,
|
||||||
|
index,
|
||||||
|
showFields,
|
||||||
|
titleField,
|
||||||
|
lang,
|
||||||
|
translations,
|
||||||
|
onCardClick,
|
||||||
|
renderCustomField,
|
||||||
|
showViewIcon,
|
||||||
|
showUpdateIcon,
|
||||||
|
onViewClick,
|
||||||
|
onUpdateClick,
|
||||||
|
getFieldValue,
|
||||||
|
size = "lg",
|
||||||
|
}: CardItemProps<T>) {
|
||||||
|
|
||||||
|
const getCardHeight = () => {
|
||||||
|
switch (size) {
|
||||||
|
case "xs": return "h-16 max-h-16";
|
||||||
|
case "sm": return "h-20 max-h-20";
|
||||||
|
case "md": return "h-24 max-h-24";
|
||||||
|
case "lg":
|
||||||
|
default: return "h-full";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getCardStyle = () => {
|
||||||
|
switch (size) {
|
||||||
|
case "xs": return "!py-0 !gap-0 !flex !flex-col";
|
||||||
|
case "sm": return "!py-1 !gap-1 !flex !flex-col";
|
||||||
|
case "md": return "!py-2 !gap-2 !flex !flex-col";
|
||||||
|
case "lg":
|
||||||
|
default: return "";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getTitleSize = () => {
|
||||||
|
switch (size) {
|
||||||
|
case "xs": return "text-xs";
|
||||||
|
case "sm": return "text-sm";
|
||||||
|
case "md": return "text-base";
|
||||||
|
case "lg":
|
||||||
|
default: return "text-lg";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getContentPadding = () => {
|
||||||
|
switch (size) {
|
||||||
|
case "xs": return "p-1 py-1";
|
||||||
|
case "sm": return "p-1 py-1";
|
||||||
|
case "md": return "p-2 py-1";
|
||||||
|
case "lg":
|
||||||
|
default: return "p-3";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (size === "xs" || size === "sm") {
|
||||||
|
return (
|
||||||
|
<div key={index} className="w-full p-1">
|
||||||
|
<div
|
||||||
|
className={`border rounded-lg shadow-sm ${getCardHeight()} ${onCardClick ? 'cursor-pointer hover:shadow-md transition-shadow' : ''} overflow-hidden flex flex-col bg-card text-card-foreground relative`}
|
||||||
|
onClick={onCardClick ? () => onCardClick(item) : undefined}
|
||||||
|
>
|
||||||
|
{showViewIcon && (
|
||||||
|
<button
|
||||||
|
className="absolute top-1 right-1 h-5 w-5 inline-flex items-center justify-center rounded-full bg-white/80 hover:bg-white z-10"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
if (onViewClick) onViewClick(item);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Eye className="h-3 w-3" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{showUpdateIcon && (
|
||||||
|
<button
|
||||||
|
className="absolute top-1 right-7 h-5 w-5 inline-flex items-center justify-center rounded-full bg-white/80 hover:bg-white z-10"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
if (onUpdateClick) onUpdateClick(item);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Edit className="h-3 w-3" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<div className="px-2 pt-1 pb-1">
|
||||||
|
<h3 className={`${getTitleSize()} font-semibold truncate pr-6`}>{getFieldValue(item, titleField)}</h3>
|
||||||
|
<div className="flex flex-col justify-start mt-1">
|
||||||
|
{showFields.map((field) => (
|
||||||
|
<CardField
|
||||||
|
key={`${index}-${field}`}
|
||||||
|
item={item}
|
||||||
|
field={field}
|
||||||
|
lang={lang}
|
||||||
|
translations={translations}
|
||||||
|
renderCustomField={renderCustomField}
|
||||||
|
getFieldValue={getFieldValue}
|
||||||
|
size={size}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={index} className="w-full p-1">
|
||||||
|
<Card
|
||||||
|
className={`${getCardHeight()} ${onCardClick ? 'cursor-pointer hover:shadow-md transition-shadow' : ''} overflow-hidden flex flex-col`}
|
||||||
|
onClick={onCardClick ? () => onCardClick(item) : undefined}
|
||||||
|
>
|
||||||
|
<CardHeader className={`${getContentPadding()} pb-0 flex justify-between items-start`}>
|
||||||
|
<h3 className={`${getTitleSize()} font-semibold`}>{getFieldValue(item, titleField)}</h3>
|
||||||
|
<CardActions
|
||||||
|
item={item}
|
||||||
|
showViewIcon={showViewIcon}
|
||||||
|
showUpdateIcon={showUpdateIcon}
|
||||||
|
onViewClick={onViewClick}
|
||||||
|
onUpdateClick={onUpdateClick}
|
||||||
|
/>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className={`${getContentPadding()} flex-1 overflow-hidden`}>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{showFields.map((field) => (
|
||||||
|
<CardField
|
||||||
|
key={`${index}-${field}`}
|
||||||
|
item={item}
|
||||||
|
field={field}
|
||||||
|
lang={lang}
|
||||||
|
translations={translations}
|
||||||
|
renderCustomField={renderCustomField}
|
||||||
|
getFieldValue={getFieldValue}
|
||||||
|
size={size}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardActions<T>({
|
||||||
|
item,
|
||||||
|
showViewIcon,
|
||||||
|
showUpdateIcon,
|
||||||
|
onViewClick,
|
||||||
|
onUpdateClick,
|
||||||
|
}: CardActionsProps<T>) {
|
||||||
|
if (!showViewIcon && !showUpdateIcon) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex space-x-1">
|
||||||
|
{showViewIcon && (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="h-8 w-8"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
if (onViewClick) onViewClick(item);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Eye className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{showUpdateIcon && (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="h-8 w-8"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
if (onUpdateClick) onUpdateClick(item);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Edit className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardField<T>({
|
||||||
|
item,
|
||||||
|
field,
|
||||||
|
lang,
|
||||||
|
translations,
|
||||||
|
renderCustomField,
|
||||||
|
getFieldValue,
|
||||||
|
size = "lg",
|
||||||
|
}: CardFieldProps<T>) {
|
||||||
|
const getTextSize = () => {
|
||||||
|
switch (size) {
|
||||||
|
case "xs": return "text-xs";
|
||||||
|
case "sm": return "text-xs";
|
||||||
|
case "md": return "text-sm";
|
||||||
|
case "lg":
|
||||||
|
default: return "text-base";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getLabelWidth = () => {
|
||||||
|
switch (size) {
|
||||||
|
case "xs": return "w-16";
|
||||||
|
case "sm": return "w-20";
|
||||||
|
case "md": return "w-24";
|
||||||
|
case "lg":
|
||||||
|
default: return "w-32";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (renderCustomField) {
|
||||||
|
return renderCustomField(item, field);
|
||||||
|
}
|
||||||
|
|
||||||
|
const label = translations?.[field]?.[lang] || field;
|
||||||
|
const value = getFieldValue(item, field);
|
||||||
|
|
||||||
|
if (size === "xs" || size === "sm") {
|
||||||
|
return (
|
||||||
|
<div className={`flex items-center ${getTextSize()} py-0 my-0 leading-tight`}>
|
||||||
|
<span className={`${getLabelWidth()} font-medium truncate mr-1`}>{label}:</span>
|
||||||
|
<span className="truncate max-w-[60%]">{value}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`flex items-center ${getTextSize()}`}>
|
||||||
|
<span className={`${getLabelWidth()} font-medium truncate`}>{label}:</span>
|
||||||
|
<span className="truncate">{value}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
"use client";
|
||||||
|
import React from "react";
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardHeader,
|
||||||
|
} from "@/components/ui/card";
|
||||||
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
|
import { CardSkeletonProps } from "./schema";
|
||||||
|
|
||||||
|
// Interface moved to schema.ts
|
||||||
|
|
||||||
|
export function CardSkeleton({
|
||||||
|
index,
|
||||||
|
showFields,
|
||||||
|
showViewIcon,
|
||||||
|
showUpdateIcon,
|
||||||
|
}: CardSkeletonProps) {
|
||||||
|
return (
|
||||||
|
<div key={`loading-${index}`} className="w-full p-1">
|
||||||
|
<Card className="h-full">
|
||||||
|
<CardHeader className="p-3 pb-0 flex justify-between items-start">
|
||||||
|
<Skeleton className="h-5 w-3/4" />
|
||||||
|
<div className="flex space-x-1">
|
||||||
|
{showViewIcon && (
|
||||||
|
<Skeleton className="h-8 w-8 rounded-full" />
|
||||||
|
)}
|
||||||
|
{showUpdateIcon && (
|
||||||
|
<Skeleton className="h-8 w-8 rounded-full" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="p-3">
|
||||||
|
<div className="space-y-2">
|
||||||
|
{showFields.map((field, fieldIndex) => (
|
||||||
|
<div key={`loading-${index}-${field}`} className="flex">
|
||||||
|
<Skeleton className="h-4 w-10 mr-2" />
|
||||||
|
<Skeleton className="h-4 w-full" />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { CardDisplay } from './CardDisplay';
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
import { GridSize } from "../HeaderSelections/GridSelectionComponent";
|
||||||
|
|
||||||
|
export type CardSize = "xs" | "sm" | "md" | "lg";
|
||||||
|
|
||||||
|
export interface CardDisplayProps<T> {
|
||||||
|
showFields: string[];
|
||||||
|
data: T[];
|
||||||
|
lang: string;
|
||||||
|
translations: Record<string, any>;
|
||||||
|
error: Error | null;
|
||||||
|
loading: boolean;
|
||||||
|
titleField: string;
|
||||||
|
onCardClick?: (item: T) => void;
|
||||||
|
renderCustomField?: (item: T, field: string) => React.ReactNode;
|
||||||
|
gridCols?: number | GridSize;
|
||||||
|
showViewIcon?: boolean;
|
||||||
|
showUpdateIcon?: boolean;
|
||||||
|
onViewClick?: (item: T) => void;
|
||||||
|
onUpdateClick?: (item: T) => void;
|
||||||
|
size?: CardSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CardItemProps<T> {
|
||||||
|
item: T;
|
||||||
|
index: number;
|
||||||
|
showFields: string[];
|
||||||
|
titleField: string;
|
||||||
|
lang: string;
|
||||||
|
translations: Record<string, any>;
|
||||||
|
onCardClick?: (item: T) => void;
|
||||||
|
renderCustomField?: (item: T, field: string) => React.ReactNode;
|
||||||
|
showViewIcon: boolean;
|
||||||
|
showUpdateIcon: boolean;
|
||||||
|
onViewClick?: (item: T) => void;
|
||||||
|
onUpdateClick?: (item: T) => void;
|
||||||
|
getFieldValue: (item: any, field: string) => any;
|
||||||
|
size?: CardSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CardActionsProps<T> {
|
||||||
|
item: T;
|
||||||
|
showViewIcon: boolean;
|
||||||
|
showUpdateIcon: boolean;
|
||||||
|
onViewClick?: (item: T) => void;
|
||||||
|
onUpdateClick?: (item: T) => void;
|
||||||
|
}
|
||||||
|
export interface CardFieldProps<T> {
|
||||||
|
item: T;
|
||||||
|
field: string;
|
||||||
|
lang: string;
|
||||||
|
translations: Record<string, any>;
|
||||||
|
renderCustomField?: (item: T, field: string) => React.ReactNode;
|
||||||
|
getFieldValue: (item: any, field: string) => any;
|
||||||
|
size?: CardSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CardSkeletonProps {
|
||||||
|
index: number;
|
||||||
|
showFields: string[];
|
||||||
|
showViewIcon: boolean;
|
||||||
|
showUpdateIcon: boolean;
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
export function getFieldValue(item: any, field: string): any {
|
||||||
|
if (!item) return "";
|
||||||
|
if (field.includes(".")) {
|
||||||
|
const parts = field.split(".");
|
||||||
|
let value = item;
|
||||||
|
for (const part of parts) {
|
||||||
|
if (value === null || value === undefined) return "";
|
||||||
|
value = value[part];
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
return item[field];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getFieldLabel(
|
||||||
|
field: string,
|
||||||
|
translations: Record<string, any>,
|
||||||
|
lang: string
|
||||||
|
): string {
|
||||||
|
const t = translations[lang] || {};
|
||||||
|
return (
|
||||||
|
t[field] ||
|
||||||
|
field.charAt(0).toUpperCase() + field.slice(1).replace(/_/g, " ")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getGridClasses(gridCols: 1 | 2 | 3 | 4 | 5 | 6): string {
|
||||||
|
const baseClass = "grid grid-cols-1 gap-4";
|
||||||
|
const colClasses: Record<number, string> = {
|
||||||
|
1: "",
|
||||||
|
2: "sm:grid-cols-2",
|
||||||
|
3: "sm:grid-cols-2 md:grid-cols-3",
|
||||||
|
4: "sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4",
|
||||||
|
5: "sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5",
|
||||||
|
6: "sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-6",
|
||||||
|
};
|
||||||
|
|
||||||
|
return `${baseClass} ${colClasses[gridCols]}`;
|
||||||
|
}
|
||||||
@@ -0,0 +1,303 @@
|
|||||||
|
"use client";
|
||||||
|
import React, { useState, useEffect } from "react";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
|
||||||
|
import { CreateComponentProps, FieldDefinition } from "./types";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||||
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { useForm, SubmitHandler } from "react-hook-form";
|
||||||
|
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||||
|
import { AlertCircle } from "lucide-react";
|
||||||
|
|
||||||
|
export function CreateComponent<T>({
|
||||||
|
refetch,
|
||||||
|
setMode,
|
||||||
|
setSelectedItem,
|
||||||
|
onCancel,
|
||||||
|
lang,
|
||||||
|
translations,
|
||||||
|
formProps = {},
|
||||||
|
apiUrl,
|
||||||
|
}: CreateComponentProps<T>) {
|
||||||
|
const t = translations[lang as keyof typeof translations] || {};
|
||||||
|
|
||||||
|
// Get field definitions from formProps if available
|
||||||
|
const fieldDefinitions = formProps.fieldDefinitions || {};
|
||||||
|
const validationSchema = formProps.validationSchema;
|
||||||
|
|
||||||
|
// Group fields by their group property
|
||||||
|
const [groupedFields, setGroupedFields] = useState<Record<string, FieldDefinition[]>>({});
|
||||||
|
|
||||||
|
// Process field definitions to group them
|
||||||
|
useEffect(() => {
|
||||||
|
if (Object.keys(fieldDefinitions).length > 0) {
|
||||||
|
const groups: Record<string, FieldDefinition[]> = {};
|
||||||
|
|
||||||
|
// Group fields by their group property
|
||||||
|
Object.entries(fieldDefinitions).forEach(([fieldName, definition]) => {
|
||||||
|
const def = definition as FieldDefinition;
|
||||||
|
if (!groups[def.group]) {
|
||||||
|
groups[def.group] = [];
|
||||||
|
}
|
||||||
|
groups[def.group].push({ ...def, name: fieldName });
|
||||||
|
});
|
||||||
|
|
||||||
|
setGroupedFields(groups);
|
||||||
|
}
|
||||||
|
}, [fieldDefinitions]);
|
||||||
|
|
||||||
|
// Initialize form with default values from field definitions
|
||||||
|
const defaultValues: Record<string, any> = {};
|
||||||
|
Object.entries(fieldDefinitions).forEach(([key, def]) => {
|
||||||
|
const fieldDef = def as FieldDefinition;
|
||||||
|
defaultValues[key] = fieldDef.defaultValue !== undefined ? fieldDef.defaultValue : "";
|
||||||
|
});
|
||||||
|
|
||||||
|
// Setup form with validation schema if available
|
||||||
|
const {
|
||||||
|
register,
|
||||||
|
handleSubmit,
|
||||||
|
formState: { errors },
|
||||||
|
setValue,
|
||||||
|
watch,
|
||||||
|
reset,
|
||||||
|
} = useForm<Record<string, any>>({
|
||||||
|
defaultValues,
|
||||||
|
resolver: validationSchema ? zodResolver(validationSchema) : undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
const formValues = watch();
|
||||||
|
|
||||||
|
// Get language-specific validation schema if available
|
||||||
|
useEffect(() => {
|
||||||
|
if (formProps.schemaPath) {
|
||||||
|
const loadLanguageValidationSchema = async () => {
|
||||||
|
try {
|
||||||
|
// Dynamic import of the schema module
|
||||||
|
const schemaModule = await import(formProps.schemaPath);
|
||||||
|
|
||||||
|
// Check if language-specific schema functions are available
|
||||||
|
if (schemaModule.getCreateApplicationSchema) {
|
||||||
|
const langValidationSchema = schemaModule.getCreateApplicationSchema(lang as "en" | "tr");
|
||||||
|
|
||||||
|
// Reset the form with the current values
|
||||||
|
reset(defaultValues);
|
||||||
|
|
||||||
|
// Update the validation schema in formProps for future validations
|
||||||
|
formProps.validationSchema = langValidationSchema;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error loading language-specific validation schema:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
loadLanguageValidationSchema();
|
||||||
|
}
|
||||||
|
}, [lang, formProps.schemaPath, reset, defaultValues]);
|
||||||
|
|
||||||
|
// Handle form submission
|
||||||
|
const onSubmit: SubmitHandler<Record<string, any>> = async (data) => {
|
||||||
|
try {
|
||||||
|
if (apiUrl) {
|
||||||
|
const createUrl = `${apiUrl}/create`;
|
||||||
|
const response = await fetch(createUrl, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
});
|
||||||
|
console.log("Response:", response.ok);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`API error: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const createdItem = await response.json();
|
||||||
|
console.log("Created item:", createdItem);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (refetch) refetch();
|
||||||
|
setMode("list");
|
||||||
|
setSelectedItem(null);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error saving form:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle select changes
|
||||||
|
const handleSelectChange = (name: string, value: string) => {
|
||||||
|
setValue(name, value);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle checkbox changes
|
||||||
|
const handleCheckboxChange = (name: string, checked: boolean) => {
|
||||||
|
setValue(name, checked);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Translate group names for display dynamically
|
||||||
|
const getGroupTitle = (groupName: string) => {
|
||||||
|
// Check if we have a translation for this group name
|
||||||
|
if (t[groupName]) {
|
||||||
|
return t[groupName];
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no translation is found, just format the name as a fallback
|
||||||
|
const formattedName = groupName
|
||||||
|
.replace(/([A-Z])/g, ' $1')
|
||||||
|
.replace(/_/g, ' ')
|
||||||
|
.replace(/^./, (str) => str.toUpperCase())
|
||||||
|
.replace(/\b\w/g, (c) => c.toUpperCase());
|
||||||
|
return formattedName;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Render a field based on its type
|
||||||
|
const renderField = (fieldName: string, field: FieldDefinition) => {
|
||||||
|
const errorMessage = errors[fieldName]?.message as string;
|
||||||
|
|
||||||
|
switch (field.type) {
|
||||||
|
case "text":
|
||||||
|
return (
|
||||||
|
<div className="space-y-2" key={fieldName}>
|
||||||
|
<Label htmlFor={fieldName}>
|
||||||
|
{t[fieldName] || field.label[lang as "en" | "tr"]}
|
||||||
|
{field.required && <span className="text-red-500 ml-1">*</span>}
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id={fieldName}
|
||||||
|
{...register(fieldName)}
|
||||||
|
placeholder={t[fieldName] || field.label[lang as "en" | "tr"]}
|
||||||
|
disabled={field.readOnly}
|
||||||
|
/>
|
||||||
|
{errorMessage && (
|
||||||
|
<p className="text-sm text-red-500">{errorMessage}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
case "textarea":
|
||||||
|
return (
|
||||||
|
<div className="space-y-2" key={fieldName}>
|
||||||
|
<Label htmlFor={fieldName}>
|
||||||
|
{t[fieldName] || field.label[lang as "en" | "tr"]}
|
||||||
|
{field.required && <span className="text-red-500 ml-1">*</span>}
|
||||||
|
</Label>
|
||||||
|
<Textarea
|
||||||
|
id={fieldName}
|
||||||
|
{...register(fieldName)}
|
||||||
|
placeholder={t[fieldName] || field.label[lang as "en" | "tr"]}
|
||||||
|
rows={3}
|
||||||
|
disabled={field.readOnly}
|
||||||
|
/>
|
||||||
|
{errorMessage && (
|
||||||
|
<p className="text-sm text-red-500">{errorMessage}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
case "select":
|
||||||
|
return (
|
||||||
|
<div className="space-y-2" key={fieldName}>
|
||||||
|
<Label htmlFor={fieldName}>
|
||||||
|
{t[fieldName] || field.label[lang as "en" | "tr"]}
|
||||||
|
{field.required && <span className="text-red-500 ml-1">*</span>}
|
||||||
|
</Label>
|
||||||
|
<Select
|
||||||
|
value={formValues[fieldName]}
|
||||||
|
onValueChange={(value) => handleSelectChange(fieldName, value)}
|
||||||
|
disabled={field.readOnly}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder={t[fieldName] || field.label[lang as "en" | "tr"]} />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{field.options?.map((option) => (
|
||||||
|
<SelectItem key={option} value={option}>
|
||||||
|
{t[option] || option}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
{errorMessage && (
|
||||||
|
<p className="text-sm text-red-500">{errorMessage}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
case "checkbox":
|
||||||
|
return (
|
||||||
|
<div className="flex items-center space-x-2" key={fieldName}>
|
||||||
|
<Checkbox
|
||||||
|
id={fieldName}
|
||||||
|
checked={formValues[fieldName]}
|
||||||
|
onCheckedChange={(checked) =>
|
||||||
|
handleCheckboxChange(fieldName, checked as boolean)
|
||||||
|
}
|
||||||
|
disabled={field.readOnly}
|
||||||
|
/>
|
||||||
|
<Label htmlFor={fieldName}>
|
||||||
|
{t[fieldName] || field.label[lang as "en" | "tr"]}
|
||||||
|
{field.required && <span className="text-red-500 ml-1">*</span>}
|
||||||
|
</Label>
|
||||||
|
{errorMessage && (
|
||||||
|
<p className="text-sm text-red-500">{errorMessage}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={handleSubmit(onSubmit)}>
|
||||||
|
<div className="grid grid-cols-2 gap-4 pt-6 my-4">
|
||||||
|
<Button type="button" variant="outline" onClick={onCancel} className="w-full">
|
||||||
|
{t.cancel || "Cancel"}
|
||||||
|
</Button>
|
||||||
|
<Button type="submit" className="w-full">
|
||||||
|
{t.save || "Save"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<Card className="w-full mb-6">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>{t.create || "Create"}</CardTitle>
|
||||||
|
<CardDescription>{t.createDescription || "Create a new item"}</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
{/* Display validation errors summary if any */}
|
||||||
|
{Object.keys(errors).length > 0 && (
|
||||||
|
<Alert variant="destructive" className="mb-4">
|
||||||
|
<AlertCircle className="h-4 w-4" />
|
||||||
|
<AlertDescription>
|
||||||
|
{t.formErrors || "Please correct the errors in the form"}
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Render fields grouped by their group property */}
|
||||||
|
<div className="space-y-6">
|
||||||
|
{Object.entries(groupedFields).map(([groupName, fields]) => (
|
||||||
|
<Card key={groupName} className="shadow-sm">
|
||||||
|
<CardHeader className="py-3">
|
||||||
|
<CardTitle className="text-lg">{getGroupTitle(groupName)}</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="space-y-4">
|
||||||
|
{fields.map((field: any) => renderField(field.name, field))}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
"use client";
|
||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
import { CreateComponent } from "./CreateComponent";
|
||||||
|
import { UpdateComponent } from "./UpdateComponent";
|
||||||
|
import { ViewComponent } from "./ViewComponent";
|
||||||
|
import { FormDisplayProps } from "./types";
|
||||||
|
|
||||||
|
export function FormDisplay<T>({
|
||||||
|
initialData,
|
||||||
|
mode,
|
||||||
|
refetch,
|
||||||
|
setMode,
|
||||||
|
setSelectedItem,
|
||||||
|
onCancel,
|
||||||
|
lang,
|
||||||
|
translations,
|
||||||
|
formProps = {},
|
||||||
|
apiUrl,
|
||||||
|
}: FormDisplayProps<T>) {
|
||||||
|
const [enhancedFormProps, setEnhancedFormProps] = useState(formProps);
|
||||||
|
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const updateFormProps = async () => {
|
||||||
|
try {
|
||||||
|
if (formProps.schemaPath) {
|
||||||
|
const schemaModule = await import(formProps.schemaPath);
|
||||||
|
|
||||||
|
let fieldDefs;
|
||||||
|
if (schemaModule.fieldDefinitions?.getDefinitionsByMode) {
|
||||||
|
fieldDefs = schemaModule.fieldDefinitions.getDefinitionsByMode(mode);
|
||||||
|
} else if (mode === "create" && schemaModule.createFieldDefinitions) {
|
||||||
|
fieldDefs = schemaModule.createFieldDefinitions;
|
||||||
|
} else if (mode === "update" && schemaModule.updateFieldDefinitions) {
|
||||||
|
fieldDefs = schemaModule.updateFieldDefinitions;
|
||||||
|
} else if (mode === "view" && schemaModule.viewFieldDefinitions) {
|
||||||
|
fieldDefs = schemaModule.viewFieldDefinitions;
|
||||||
|
}
|
||||||
|
|
||||||
|
let validationSchema;
|
||||||
|
if (mode === "create" && schemaModule.getCreateApplicationSchema) {
|
||||||
|
validationSchema = schemaModule.getCreateApplicationSchema(lang as "en" | "tr");
|
||||||
|
} else if (mode === "update" && schemaModule.getUpdateApplicationSchema) {
|
||||||
|
validationSchema = schemaModule.getUpdateApplicationSchema(lang as "en" | "tr");
|
||||||
|
} else if (mode === "view" && schemaModule.ViewApplicationSchema) {
|
||||||
|
validationSchema = schemaModule.ViewApplicationSchema;
|
||||||
|
} else if (schemaModule.ApplicationSchema) {
|
||||||
|
validationSchema = schemaModule.ApplicationSchema;
|
||||||
|
}
|
||||||
|
|
||||||
|
const groupedFieldDefs = schemaModule.baseFieldDefinitions || {};
|
||||||
|
setEnhancedFormProps({
|
||||||
|
...formProps,
|
||||||
|
fieldDefinitions: fieldDefs || {},
|
||||||
|
validationSchema,
|
||||||
|
fieldsByMode: schemaModule.fieldsByMode || {},
|
||||||
|
groupedFieldDefinitions: groupedFieldDefs,
|
||||||
|
currentLang: lang,
|
||||||
|
schemaPath: formProps.schemaPath
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setEnhancedFormProps({
|
||||||
|
...formProps,
|
||||||
|
currentLang: lang
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error loading schema definitions:", error);
|
||||||
|
setEnhancedFormProps({
|
||||||
|
...formProps,
|
||||||
|
currentLang: lang
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
updateFormProps();
|
||||||
|
}, [formProps, mode, lang]);
|
||||||
|
|
||||||
|
switch (mode) {
|
||||||
|
case "create":
|
||||||
|
return (
|
||||||
|
<CreateComponent<T>
|
||||||
|
key={`create-${lang}`}
|
||||||
|
refetch={refetch}
|
||||||
|
setMode={setMode}
|
||||||
|
setSelectedItem={setSelectedItem}
|
||||||
|
onCancel={onCancel}
|
||||||
|
lang={lang}
|
||||||
|
translations={translations}
|
||||||
|
formProps={enhancedFormProps}
|
||||||
|
apiUrl={apiUrl}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
case "update":
|
||||||
|
const updateKey = `update-${lang}-${(initialData as any)?.uu_id || 'new'}`;
|
||||||
|
return initialData ? (
|
||||||
|
<UpdateComponent<T>
|
||||||
|
key={updateKey}
|
||||||
|
initialData={initialData}
|
||||||
|
refetch={refetch}
|
||||||
|
setMode={setMode}
|
||||||
|
setSelectedItem={setSelectedItem}
|
||||||
|
onCancel={onCancel}
|
||||||
|
lang={lang}
|
||||||
|
translations={translations}
|
||||||
|
formProps={enhancedFormProps}
|
||||||
|
apiUrl={apiUrl}
|
||||||
|
/>
|
||||||
|
) : null;
|
||||||
|
case "view":
|
||||||
|
return initialData ? (
|
||||||
|
<ViewComponent<T>
|
||||||
|
key={`view-${lang}`}
|
||||||
|
initialData={initialData}
|
||||||
|
refetch={refetch}
|
||||||
|
setMode={setMode}
|
||||||
|
setSelectedItem={setSelectedItem}
|
||||||
|
onCancel={onCancel}
|
||||||
|
lang={lang}
|
||||||
|
translations={translations}
|
||||||
|
formProps={enhancedFormProps}
|
||||||
|
/>
|
||||||
|
) : null;
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,412 @@
|
|||||||
|
"use client";
|
||||||
|
import React, { useState, useEffect, useMemo } from "react";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
|
||||||
|
import { UpdateComponentProps, FieldDefinition } from "./types";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||||
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||||
|
import { AlertCircle } from "lucide-react";
|
||||||
|
|
||||||
|
export function UpdateComponent<T>({
|
||||||
|
initialData,
|
||||||
|
refetch,
|
||||||
|
setMode,
|
||||||
|
setSelectedItem,
|
||||||
|
onCancel,
|
||||||
|
lang,
|
||||||
|
translations,
|
||||||
|
apiUrl,
|
||||||
|
formProps = {},
|
||||||
|
}: UpdateComponentProps<T>) {
|
||||||
|
const t = translations[lang as keyof typeof translations] || {};
|
||||||
|
|
||||||
|
// Get field definitions from formProps if available
|
||||||
|
const fieldDefinitions = formProps.fieldDefinitions || {};
|
||||||
|
const validationSchema = formProps.validationSchema;
|
||||||
|
|
||||||
|
// Ensure field definitions are processed only once
|
||||||
|
const processedFieldDefinitions = useMemo(() => {
|
||||||
|
const processed = { ...fieldDefinitions };
|
||||||
|
// Make all fields editable except system fields
|
||||||
|
Object.entries(processed).forEach(([fieldName, definition]) => {
|
||||||
|
if (fieldName !== 'uu_id' && fieldName !== 'created_at' && fieldName !== 'updated_at') {
|
||||||
|
(processed[fieldName] as FieldDefinition).readOnly = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return processed;
|
||||||
|
}, [fieldDefinitions]);
|
||||||
|
|
||||||
|
const [groupedFields, setGroupedFields] = useState<Record<string, FieldDefinition[]>>({});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (Object.keys(processedFieldDefinitions).length > 0) {
|
||||||
|
const groups: Record<string, FieldDefinition[]> = {};
|
||||||
|
|
||||||
|
// Group the processed field definitions
|
||||||
|
Object.entries(processedFieldDefinitions).forEach(([fieldName, definition]) => {
|
||||||
|
// Convert to FieldDefinition type
|
||||||
|
const def = definition as FieldDefinition;
|
||||||
|
|
||||||
|
// Add the field name to the definition
|
||||||
|
const fieldDef = { ...def, name: fieldName };
|
||||||
|
|
||||||
|
// Add to the appropriate group
|
||||||
|
if (!groups[def.group]) {
|
||||||
|
groups[def.group] = [];
|
||||||
|
}
|
||||||
|
groups[def.group].push(fieldDef);
|
||||||
|
});
|
||||||
|
setGroupedFields(groups);
|
||||||
|
}
|
||||||
|
}, [processedFieldDefinitions]);
|
||||||
|
|
||||||
|
const defaultValues: Record<string, any> = {};
|
||||||
|
Object.entries(processedFieldDefinitions).forEach(([key, def]) => {
|
||||||
|
const fieldDef = def as FieldDefinition;
|
||||||
|
defaultValues[key] = fieldDef.defaultValue !== undefined ? fieldDef.defaultValue : "";
|
||||||
|
});
|
||||||
|
|
||||||
|
if (initialData) {
|
||||||
|
Object.assign(defaultValues, initialData as Record<string, any>);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Track the current language to detect changes
|
||||||
|
const [currentLang, setCurrentLang] = useState(lang);
|
||||||
|
|
||||||
|
const {
|
||||||
|
register,
|
||||||
|
handleSubmit,
|
||||||
|
formState: { errors, isSubmitting, isValid },
|
||||||
|
setValue,
|
||||||
|
watch,
|
||||||
|
reset,
|
||||||
|
trigger,
|
||||||
|
} = useForm({
|
||||||
|
defaultValues,
|
||||||
|
resolver: validationSchema ? zodResolver(validationSchema) : undefined,
|
||||||
|
mode: "onChange",
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (Object.keys(errors).length > 0) {
|
||||||
|
console.log("Form errors:", errors);
|
||||||
|
}
|
||||||
|
}, [errors]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (initialData) {
|
||||||
|
reset({ ...initialData as Record<string, any> });
|
||||||
|
}
|
||||||
|
}, [initialData, reset]);
|
||||||
|
|
||||||
|
// Detect language changes and update validation schema
|
||||||
|
useEffect(() => {
|
||||||
|
// If language has changed, update the form
|
||||||
|
if (currentLang !== lang || formProps.currentLang !== lang) {
|
||||||
|
const updateValidationForLanguage = async () => {
|
||||||
|
try {
|
||||||
|
// If we have a schema path, dynamically load the schema for the current language
|
||||||
|
if (formProps.schemaPath) {
|
||||||
|
// Dynamic import of the schema module
|
||||||
|
const schemaModule = await import(formProps.schemaPath);
|
||||||
|
|
||||||
|
// Check if language-specific schema functions are available
|
||||||
|
if (schemaModule.getUpdateApplicationSchema) {
|
||||||
|
// Get the schema for the current language
|
||||||
|
const langValidationSchema = schemaModule.getUpdateApplicationSchema(lang as "en" | "tr");
|
||||||
|
|
||||||
|
// Save current form values
|
||||||
|
const formValues = watch();
|
||||||
|
|
||||||
|
// Reset the form with current values but clear errors
|
||||||
|
reset(formValues, {
|
||||||
|
keepDirty: true,
|
||||||
|
keepValues: true,
|
||||||
|
keepErrors: false
|
||||||
|
});
|
||||||
|
|
||||||
|
// Manually trigger validation after reset
|
||||||
|
setTimeout(() => {
|
||||||
|
// Trigger validation for all fields to show updated error messages
|
||||||
|
Object.keys(formValues).forEach(fieldName => {
|
||||||
|
trigger(fieldName);
|
||||||
|
});
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
// Update our tracked language
|
||||||
|
setCurrentLang(lang);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error updating validation schema for language:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
updateValidationForLanguage();
|
||||||
|
}
|
||||||
|
}, [lang, formProps.currentLang, currentLang, formProps.schemaPath, reset, watch, trigger]);
|
||||||
|
|
||||||
|
const formValues = watch();
|
||||||
|
|
||||||
|
// Handle form submission
|
||||||
|
const onSubmit = async (data: any) => {
|
||||||
|
try {
|
||||||
|
|
||||||
|
const isFormValid = await trigger();
|
||||||
|
if (!isFormValid) {
|
||||||
|
console.error("Form validation failed - stopping submission");
|
||||||
|
return; // Stop submission if validation fails
|
||||||
|
}
|
||||||
|
if (!apiUrl) {
|
||||||
|
console.error("API URL is missing or undefined");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const uuid = initialData ? (initialData as any).uu_id : null;
|
||||||
|
if (!uuid) {
|
||||||
|
console.error("UUID not found in initialData");
|
||||||
|
throw new Error("UUID is required for update operations");
|
||||||
|
}
|
||||||
|
const dataToSend = { ...data };
|
||||||
|
Object.entries(fieldDefinitions).forEach(([key, def]) => {
|
||||||
|
const fieldDef = def as FieldDefinition;
|
||||||
|
if (fieldDef.readOnly) {
|
||||||
|
delete dataToSend[key];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const updateUrl = `${apiUrl}/update?uuid=${uuid}`;
|
||||||
|
console.log("Updating application with payload:", dataToSend, 'uuId:', uuid);
|
||||||
|
try {
|
||||||
|
let response = await fetch(updateUrl, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify(dataToSend),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorText = await response.text();
|
||||||
|
console.error("API error response:", errorText);
|
||||||
|
throw new Error(`API error: ${response.status}`);
|
||||||
|
}
|
||||||
|
if (refetch) refetch();
|
||||||
|
setMode("list");
|
||||||
|
setSelectedItem(null);
|
||||||
|
} catch (fetchError) {
|
||||||
|
console.error("Error during fetch:", fetchError);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error details:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle select changes
|
||||||
|
const handleSelectChange = (name: string, value: string) => {
|
||||||
|
setValue(name, value);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle checkbox changes
|
||||||
|
const handleCheckboxChange = (name: string, checked: boolean) => {
|
||||||
|
setValue(name, checked);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Translate group names for display dynamically
|
||||||
|
const getGroupTitle = (groupName: string) => {
|
||||||
|
// Check if we have a translation for this group name
|
||||||
|
if (t[groupName]) {
|
||||||
|
return t[groupName];
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no translation is found, just format the name as a fallback
|
||||||
|
const formattedName = groupName
|
||||||
|
.replace(/([A-Z])/g, ' $1')
|
||||||
|
.replace(/_/g, ' ')
|
||||||
|
.replace(/^./, (str) => str.toUpperCase())
|
||||||
|
.replace(/\b\w/g, (c) => c.toUpperCase());
|
||||||
|
|
||||||
|
return formattedName;
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderField = (fieldName: string, field: FieldDefinition) => {
|
||||||
|
const errorMessage = errors[fieldName]?.message as string;
|
||||||
|
const fieldValue = formValues[fieldName];
|
||||||
|
|
||||||
|
const renderLabel = () => (
|
||||||
|
<Label htmlFor={fieldName}>
|
||||||
|
{t[fieldName] || field.label[lang as "en" | "tr"]}
|
||||||
|
{field.required && <span className="text-red-500 ml-1">*</span>}
|
||||||
|
</Label>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (field.readOnly) {
|
||||||
|
return (
|
||||||
|
<div className="space-y-2" key={fieldName}>
|
||||||
|
{renderLabel()}
|
||||||
|
<div className="p-2 bg-gray-50 rounded border border-gray-200">
|
||||||
|
{field.type === "checkbox" ?
|
||||||
|
(fieldValue ? "Yes" : "No") :
|
||||||
|
(fieldValue || "-")}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// For editable fields, render the appropriate input type
|
||||||
|
switch (field.type) {
|
||||||
|
case "text":
|
||||||
|
return (
|
||||||
|
<div className="space-y-2" key={fieldName}>
|
||||||
|
{renderLabel()}
|
||||||
|
<Input
|
||||||
|
id={fieldName}
|
||||||
|
{...register(fieldName)}
|
||||||
|
placeholder={t[fieldName] || field.label[lang as "en" | "tr"]}
|
||||||
|
/>
|
||||||
|
{errorMessage && (
|
||||||
|
<p className="text-sm text-red-500">{errorMessage}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
case "textarea":
|
||||||
|
return (
|
||||||
|
<div className="space-y-2" key={fieldName}>
|
||||||
|
{renderLabel()}
|
||||||
|
<Textarea
|
||||||
|
id={fieldName}
|
||||||
|
{...register(fieldName)}
|
||||||
|
placeholder={t[fieldName] || field.label[lang as "en" | "tr"]}
|
||||||
|
rows={3}
|
||||||
|
/>
|
||||||
|
{errorMessage && (
|
||||||
|
<p className="text-sm text-red-500">{errorMessage}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
case "select":
|
||||||
|
return (
|
||||||
|
<div className="space-y-2" key={fieldName}>
|
||||||
|
{renderLabel()}
|
||||||
|
<Select
|
||||||
|
value={fieldValue}
|
||||||
|
onValueChange={(value) => handleSelectChange(fieldName, value)}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder={t[fieldName] || field.label[lang as "en" | "tr"]} />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{field.options?.map((option) => (
|
||||||
|
<SelectItem key={option} value={option}>
|
||||||
|
{t[option] || option}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
{errorMessage && (
|
||||||
|
<p className="text-sm text-red-500">{errorMessage}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
case "checkbox":
|
||||||
|
return (
|
||||||
|
<div className="flex items-center space-x-2" key={fieldName}>
|
||||||
|
<Checkbox
|
||||||
|
id={fieldName}
|
||||||
|
checked={fieldValue}
|
||||||
|
onCheckedChange={(checked) =>
|
||||||
|
handleCheckboxChange(fieldName, checked as boolean)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
{renderLabel()}
|
||||||
|
{errorMessage && (
|
||||||
|
<p className="text-sm text-red-500">{errorMessage}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
case "date":
|
||||||
|
return (
|
||||||
|
<div className="space-y-2" key={fieldName}>
|
||||||
|
{renderLabel()}
|
||||||
|
<Input
|
||||||
|
id={fieldName}
|
||||||
|
type="date"
|
||||||
|
{...register(fieldName)}
|
||||||
|
/>
|
||||||
|
{errorMessage && (
|
||||||
|
<p className="text-sm text-red-500">{errorMessage}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={handleSubmit(onSubmit)}>
|
||||||
|
<Card className="w-full mb-6">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>{t.update || "Update"}</CardTitle>
|
||||||
|
<CardDescription>{t.updateDescription || "Update existing item"}</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
|
||||||
|
{/* Display validation errors summary if any */}
|
||||||
|
{Object.keys(errors).length > 0 && (
|
||||||
|
<Alert variant="destructive" className="mb-4">
|
||||||
|
<AlertCircle className="h-4 w-4" />
|
||||||
|
<AlertDescription>
|
||||||
|
{t.formErrors || "Please correct the errors in the form"}
|
||||||
|
<ul className="mt-2 list-disc pl-5">
|
||||||
|
{Object.entries(errors).map(([field, error]) => (
|
||||||
|
<li key={field}>
|
||||||
|
{t[field] || field}: {(error as any)?.message || 'Invalid value'}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-4 pt-6 my-4">
|
||||||
|
<Button type="button" variant="outline" onClick={onCancel} className="w-full">
|
||||||
|
{t.cancel || "Cancel"}
|
||||||
|
</Button>
|
||||||
|
<Button type="submit" className="w-full">
|
||||||
|
{t.save || "Save"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Render fields grouped by their group property */}
|
||||||
|
<div className="space-y-6">
|
||||||
|
{Object.entries(groupedFields).map(([groupName, fields]) => (
|
||||||
|
<Card key={groupName} className="shadow-sm">
|
||||||
|
<CardHeader className="py-3">
|
||||||
|
<CardTitle className="text-lg">{getGroupTitle(groupName)}</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="space-y-4">
|
||||||
|
{fields.map((field: any) => renderField(field.name, field))}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,219 @@
|
|||||||
|
"use client";
|
||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
|
||||||
|
import { ViewComponentProps, FieldDefinition } from "./types";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
|
||||||
|
// Utility function to format field label
|
||||||
|
const formatFieldLabel = (fieldName: string) => fieldName.replace(/_/g, ' ').replace(/^./, (str) => str.toUpperCase()).replace(/\b\w/g, (c) => c.toUpperCase());
|
||||||
|
|
||||||
|
// Component for rendering a single field
|
||||||
|
const ViewField: React.FC<{
|
||||||
|
fieldName: string;
|
||||||
|
value: any;
|
||||||
|
label: string;
|
||||||
|
lang: string;
|
||||||
|
translations: any;
|
||||||
|
hasError?: string;
|
||||||
|
}> = ({ fieldName, value, label, lang, translations: t, hasError }) => {
|
||||||
|
const formatFieldValue = () => {
|
||||||
|
if (value === undefined || value === null) return "-";
|
||||||
|
|
||||||
|
switch (true) {
|
||||||
|
case typeof value === 'string' && !isNaN(Date.parse(value)):
|
||||||
|
return new Date(value).toLocaleString(lang === "tr" ? "tr-TR" : "en-US");
|
||||||
|
case typeof value === 'boolean':
|
||||||
|
return value ? (
|
||||||
|
<span className="inline-flex items-center rounded-md bg-green-50 px-2 py-1 text-xs font-medium text-green-700">
|
||||||
|
{t.yes || "Yes"}
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span className="inline-flex items-center rounded-md bg-red-50 px-2 py-1 text-xs font-medium text-red-700">
|
||||||
|
{t.no || "No"}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
case fieldName === "application_type" && (value === "employee" || value === "occupant"):
|
||||||
|
return t[value] || value;
|
||||||
|
default:
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-1">
|
||||||
|
<Label className="text-sm font-medium">
|
||||||
|
{t[fieldName] || label}
|
||||||
|
</Label>
|
||||||
|
<div className={`rounded-md border ${hasError ? 'border-red-500' : 'border-input'} bg-background px-3 py-2 text-sm min-h-[2.5rem] flex items-center`}>
|
||||||
|
{formatFieldValue()}
|
||||||
|
</div>
|
||||||
|
{hasError && <p className="text-sm text-red-500">{hasError}</p>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Component for rendering a group of fields
|
||||||
|
const ViewFieldGroup: React.FC<{
|
||||||
|
groupName: string;
|
||||||
|
fields: FieldDefinition[];
|
||||||
|
initialData: any;
|
||||||
|
lang: string;
|
||||||
|
translations: any;
|
||||||
|
validationErrors: Record<string, string>;
|
||||||
|
}> = ({ groupName, fields, initialData, lang, translations, validationErrors }) => {
|
||||||
|
const getGroupTitle = (name: string) => { return translations[name] || formatFieldLabel(name); };
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="shadow-sm">
|
||||||
|
<CardHeader className="py-3">
|
||||||
|
<CardTitle className="text-lg">{getGroupTitle(groupName)}</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="space-y-4">
|
||||||
|
{fields.map((field) => {
|
||||||
|
const fieldName = field.name || "";
|
||||||
|
const value = initialData ? (initialData as any)[fieldName] : undefined;
|
||||||
|
const hasError = validationErrors[fieldName];
|
||||||
|
return (
|
||||||
|
<ViewField
|
||||||
|
key={fieldName}
|
||||||
|
fieldName={fieldName}
|
||||||
|
value={value}
|
||||||
|
label={field.label[lang as "en" | "tr"]}
|
||||||
|
lang={lang}
|
||||||
|
translations={translations}
|
||||||
|
hasError={hasError}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export function ViewComponent<T>({
|
||||||
|
initialData,
|
||||||
|
setMode,
|
||||||
|
setSelectedItem,
|
||||||
|
onCancel,
|
||||||
|
lang,
|
||||||
|
translations,
|
||||||
|
formProps = {},
|
||||||
|
}: ViewComponentProps<T>) {
|
||||||
|
const t = translations[lang as keyof typeof translations] || {};
|
||||||
|
|
||||||
|
const fieldDefinitions = formProps.fieldDefinitions || {};
|
||||||
|
const validationSchema = formProps.validationSchema as z.ZodObject<any> | undefined;
|
||||||
|
|
||||||
|
const [groupedFields, setGroupedFields] = useState<Record<string, FieldDefinition[]>>({});
|
||||||
|
const [validationErrors, setValidationErrors] = useState<Record<string, string>>({});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (Object.keys(fieldDefinitions).length > 0) {
|
||||||
|
const groups: Record<string, FieldDefinition[]> = {};
|
||||||
|
|
||||||
|
Object.entries(fieldDefinitions).forEach(([fieldName, definition]) => {
|
||||||
|
const def = definition as FieldDefinition;
|
||||||
|
if (!groups[def.group]) { groups[def.group] = []; }
|
||||||
|
groups[def.group].push({ ...def, name: fieldName });
|
||||||
|
});
|
||||||
|
|
||||||
|
setGroupedFields(groups);
|
||||||
|
}
|
||||||
|
}, [fieldDefinitions]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (validationSchema && initialData) {
|
||||||
|
try {
|
||||||
|
validationSchema.parse(initialData);
|
||||||
|
setValidationErrors({});
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof z.ZodError) {
|
||||||
|
console.warn('View data validation issues (not shown to user):', error.errors);
|
||||||
|
setValidationErrors({});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [initialData, validationSchema]);
|
||||||
|
|
||||||
|
const handleEdit = () => { setMode("update") };
|
||||||
|
|
||||||
|
console.log("Grouped Fields", groupedFields);
|
||||||
|
console.log("Validation Errors", validationErrors);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="w-full mb-6">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>{t.view || "View"}</CardTitle>
|
||||||
|
<CardDescription>{t.viewDescription || "View item details"}</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="flex justify-end space-x-2 pt-4 my-4">
|
||||||
|
<Button variant="outline" onClick={onCancel}>
|
||||||
|
{t.back || "Back"}
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleEdit}>
|
||||||
|
{t.edit || "Edit"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-6">
|
||||||
|
{
|
||||||
|
Object.keys(groupedFields).length > 0 ? (
|
||||||
|
Object.entries(groupedFields).map(([groupName, fields]) => (
|
||||||
|
<ViewFieldGroup
|
||||||
|
key={groupName}
|
||||||
|
groupName={groupName}
|
||||||
|
fields={fields}
|
||||||
|
initialData={initialData}
|
||||||
|
lang={lang}
|
||||||
|
translations={t}
|
||||||
|
validationErrors={validationErrors}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
initialData && (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{validationSchema ? (
|
||||||
|
Object.entries(validationSchema.shape || {}).map(([fieldName, _]) => {
|
||||||
|
const value = (initialData as any)[fieldName];
|
||||||
|
if (value === undefined || value === null) return null;
|
||||||
|
return (
|
||||||
|
<ViewField
|
||||||
|
key={fieldName}
|
||||||
|
fieldName={fieldName}
|
||||||
|
value={value}
|
||||||
|
label={formatFieldLabel(fieldName)}
|
||||||
|
lang={lang}
|
||||||
|
translations={t}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
) : (
|
||||||
|
Object.entries(initialData as Record<string, any>).map(([fieldName, value]) => {
|
||||||
|
if (value === undefined || value === null) return null;
|
||||||
|
return (
|
||||||
|
<ViewField
|
||||||
|
key={fieldName}
|
||||||
|
fieldName={fieldName}
|
||||||
|
value={value}
|
||||||
|
label={formatFieldLabel(fieldName)}
|
||||||
|
lang={lang}
|
||||||
|
translations={t}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
// Export the main components
|
||||||
|
export { FormDisplay } from "./FormDisplay";
|
||||||
|
|
||||||
|
// Export types
|
||||||
|
export type { FormMode } from "./types";
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
export interface FieldDefinition {
|
||||||
|
type: string;
|
||||||
|
group: string;
|
||||||
|
label: { tr: string; en: string };
|
||||||
|
options?: string[];
|
||||||
|
readOnly?: boolean;
|
||||||
|
required?: boolean;
|
||||||
|
defaultValue?: any;
|
||||||
|
name?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type FormMode = "list" | "create" | "update" | "view";
|
||||||
|
export type FormModeView = "list" | "view";
|
||||||
|
|
||||||
|
export interface BaseFormProps<T> {
|
||||||
|
initialData?: T;
|
||||||
|
refetch?: () => void;
|
||||||
|
setMode: React.Dispatch<React.SetStateAction<FormMode | FormModeView>>;
|
||||||
|
setSelectedItem: React.Dispatch<React.SetStateAction<T | null>>;
|
||||||
|
onCancel: () => void;
|
||||||
|
lang: string;
|
||||||
|
translations: Record<string, Record<string, string>>;
|
||||||
|
formProps?: Record<string, any>;
|
||||||
|
apiUrl?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateComponentProps<T> extends BaseFormProps<T> {}
|
||||||
|
|
||||||
|
export interface UpdateComponentProps<T> extends BaseFormProps<T> {
|
||||||
|
initialData: T;
|
||||||
|
apiUrl: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ViewComponentProps<T> extends BaseFormProps<T> {
|
||||||
|
initialData: T;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FormDisplayProps<T> {
|
||||||
|
mode: FormMode | FormModeView;
|
||||||
|
initialData?: T;
|
||||||
|
refetch?: (additionalParams?: Record<string, any>) => void;
|
||||||
|
setMode: React.Dispatch<React.SetStateAction<FormModeView | FormMode>>;
|
||||||
|
setSelectedItem: React.Dispatch<React.SetStateAction<T | null>>;
|
||||||
|
onCancel: () => void;
|
||||||
|
lang: string;
|
||||||
|
translations: Record<string, Record<string, string>>;
|
||||||
|
formProps?: Record<string, any>;
|
||||||
|
apiUrl: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
"use client";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
export type GridSize = 1 | 2 | 3 | 4 | 5 | 6;
|
||||||
|
|
||||||
|
interface GridSelectionComponentProps {
|
||||||
|
gridCols: GridSize;
|
||||||
|
setGridCols: (size: GridSize) => void;
|
||||||
|
translations?: Record<string, any>;
|
||||||
|
lang?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const GridSelectionComponent: React.FC<GridSelectionComponentProps> = ({
|
||||||
|
gridCols,
|
||||||
|
setGridCols,
|
||||||
|
translations,
|
||||||
|
lang = "en",
|
||||||
|
}) => {
|
||||||
|
const t = translations?.[lang] || {};
|
||||||
|
|
||||||
|
const handleChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||||
|
setGridCols(Number(e.target.value) as GridSize);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center">
|
||||||
|
<span className="mr-2 text-sm">{t.gridSize || "Grid Size:"}:</span>
|
||||||
|
<select
|
||||||
|
value={gridCols}
|
||||||
|
onChange={handleChange}
|
||||||
|
className="p-2 border rounded"
|
||||||
|
>
|
||||||
|
<option value="1">{t.oneColumn || "1 Column"}</option>
|
||||||
|
<option value="2">{t.twoColumns || "2 Columns"}</option>
|
||||||
|
<option value="3">{t.threeColumns || "3 Columns"}</option>
|
||||||
|
<option value="4">{t.fourColumns || "4 Columns"}</option>
|
||||||
|
<option value="5">{t.fiveColumns || "5 Columns"}</option>
|
||||||
|
<option value="6">{t.sixColumns || "6 Columns"}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
"use client";
|
||||||
|
import React from "react";
|
||||||
|
import { Language, LanguageSelectionComponentProps } from "@/components/common/schemas";
|
||||||
|
|
||||||
|
export const LanguageSelectionComponent: React.FC<LanguageSelectionComponentProps> = ({
|
||||||
|
lang,
|
||||||
|
setLang,
|
||||||
|
className = "p-2 border rounded",
|
||||||
|
}) => {
|
||||||
|
|
||||||
|
const handleChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||||
|
setLang(e.target.value as Language);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center">
|
||||||
|
<select
|
||||||
|
value={lang}
|
||||||
|
onChange={handleChange}
|
||||||
|
className={className}
|
||||||
|
>
|
||||||
|
<option value="en">English</option>
|
||||||
|
<option value="tr">Turkish</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
const Loader = () => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="flex items-center justify-center h-40 bg-gray-100 rounded-md">
|
||||||
|
<div className="animate-spin rounded-full h-10 w-10 border-b-2 border-primary"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Loader
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
"use client";
|
||||||
|
import React from "react";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { PaginationBaseProps } from "./types";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate the page numbers to display in pagination
|
||||||
|
* @param currentPage Current active page
|
||||||
|
* @param totalPages Total number of pages
|
||||||
|
* @param maxButtons Maximum number of page buttons to show (default: 5)
|
||||||
|
* @returns Array of page numbers to display
|
||||||
|
*/
|
||||||
|
const getPageNumbers = (currentPage: number, totalPages: number, maxButtons: number = 5): number[] => {
|
||||||
|
const pageNumbers: number[] = [];
|
||||||
|
|
||||||
|
// If we have fewer pages than the maximum buttons, show all pages
|
||||||
|
if (totalPages <= maxButtons) {
|
||||||
|
for (let i = 1; i <= totalPages; i++) {
|
||||||
|
pageNumbers.push(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If we're near the beginning, show first maxButtons pages
|
||||||
|
else if (currentPage <= 3) {
|
||||||
|
for (let i = 1; i <= maxButtons; i++) {
|
||||||
|
pageNumbers.push(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If we're near the end, show last maxButtons pages
|
||||||
|
else if (currentPage >= totalPages - 2) {
|
||||||
|
for (let i = totalPages - maxButtons + 1; i <= totalPages; i++) {
|
||||||
|
pageNumbers.push(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Otherwise, show pages centered around current page
|
||||||
|
else {
|
||||||
|
for (let i = currentPage - 2; i <= currentPage + 2; i++) {
|
||||||
|
pageNumbers.push(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return pageNumbers;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PageNavigation: React.FC<PaginationBaseProps> = ({
|
||||||
|
pagination,
|
||||||
|
updatePagination,
|
||||||
|
loading = false,
|
||||||
|
lang,
|
||||||
|
translations,
|
||||||
|
}) => {
|
||||||
|
const t = translations[lang] || {};
|
||||||
|
|
||||||
|
const handlePageChange = (newPage: number) => {
|
||||||
|
if (newPage >= 1 && newPage <= pagination.totalPages) {
|
||||||
|
updatePagination({ page: newPage });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get the page numbers to display
|
||||||
|
const pageNumbers = getPageNumbers(pagination.page, pagination.totalPages);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
{pagination.back ? (
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handlePageChange(pagination.page - 1)}
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
{t.previous || "Previous"}
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button variant="ghost" size="sm" disabled>
|
||||||
|
{t.previous || "Previous"}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Page number buttons */}
|
||||||
|
<div className="flex items-center space-x-1">
|
||||||
|
{pageNumbers.map((pageNum) => (
|
||||||
|
<Button
|
||||||
|
key={pageNum}
|
||||||
|
variant={pagination.page === pageNum ? "default" : "outline"}
|
||||||
|
size="sm"
|
||||||
|
className="w-9 h-9 p-0"
|
||||||
|
onClick={() => handlePageChange(pageNum)}
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
{pageNum}
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{pagination.page < pagination.totalPages ? (
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handlePageChange(pagination.page + 1)}
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
{t.next || "Next"}
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button variant="ghost" size="sm" disabled>
|
||||||
|
{t.next || "Next"}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Page text display */}
|
||||||
|
<span className="px-4 py-1 text-sm text-muted-foreground">
|
||||||
|
{t.page || "Page"} {pagination.page} {t.of || "of"} {pagination.totalPages}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
"use client";
|
||||||
|
import React from "react";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
import { PaginationBaseProps } from "./types";
|
||||||
|
|
||||||
|
interface PageSizeSelectorProps extends PaginationBaseProps {
|
||||||
|
pageSizeOptions?: number[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PageSizeSelector: React.FC<PageSizeSelectorProps> = ({
|
||||||
|
pagination,
|
||||||
|
updatePagination,
|
||||||
|
lang,
|
||||||
|
translations,
|
||||||
|
pageSizeOptions = [5, 10, 20, 50],
|
||||||
|
}) => {
|
||||||
|
const t = translations[lang] || {};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<span className="text-sm text-muted-foreground">
|
||||||
|
{t.itemsPerPage || "Items per page"}
|
||||||
|
</span>
|
||||||
|
<Select
|
||||||
|
value={pagination.size.toString()}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
updatePagination({
|
||||||
|
size: Number(value),
|
||||||
|
page: 1, // Reset to first page when changing page size
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-16">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{pageSizeOptions.map((size) => (
|
||||||
|
<SelectItem key={size} value={size.toString()}>
|
||||||
|
{size}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
"use client";
|
||||||
|
import React from "react";
|
||||||
|
import { PaginationBaseProps } from "./types";
|
||||||
|
|
||||||
|
export const PaginationStats: React.FC<PaginationBaseProps> = ({
|
||||||
|
pagination,
|
||||||
|
lang,
|
||||||
|
translations,
|
||||||
|
}) => {
|
||||||
|
const t = translations[lang] || {};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="text-sm text-muted-foreground">
|
||||||
|
<div>
|
||||||
|
{t.showing || "Showing"}{" "}
|
||||||
|
{/* Show the range based on filtered count when available */}
|
||||||
|
{(pagination.totalCount || pagination.allCount || 0) > 0
|
||||||
|
? (pagination.page - 1) * pagination.size + 1
|
||||||
|
: 0}{" "}
|
||||||
|
-{" "}
|
||||||
|
{Math.min(
|
||||||
|
pagination.page * pagination.size,
|
||||||
|
pagination.totalCount || pagination.allCount || 0
|
||||||
|
)}{" "}
|
||||||
|
{t.of || "of"} {pagination.totalCount || pagination.allCount || 0} {t.items || "items"}
|
||||||
|
</div>
|
||||||
|
{pagination.totalCount &&
|
||||||
|
pagination.totalCount !== (pagination.allCount || 0) && (
|
||||||
|
<div>
|
||||||
|
{t.total || "Total"}: {pagination.allCount || 0} {t.items || "items"} ({t.filtered || "Filtered"}:{" "}
|
||||||
|
{pagination.totalCount} {t.items || "items"})
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
"use client";
|
||||||
|
import React from "react";
|
||||||
|
import { PaginationBaseProps } from "./types";
|
||||||
|
import { PaginationStats } from "./PaginationStats";
|
||||||
|
import { PageNavigation } from "./PageNavigation";
|
||||||
|
import { PageSizeSelector } from "./PageSizeSelector";
|
||||||
|
|
||||||
|
export const PaginationToolsComponent: React.FC<PaginationBaseProps> = ({
|
||||||
|
pagination,
|
||||||
|
updatePagination,
|
||||||
|
loading = false,
|
||||||
|
lang,
|
||||||
|
translations,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-wrap justify-between items-center mt-6 gap-4">
|
||||||
|
{/* Pagination stats - left side */}
|
||||||
|
<PaginationStats
|
||||||
|
pagination={pagination}
|
||||||
|
updatePagination={updatePagination}
|
||||||
|
lang={lang}
|
||||||
|
translations={translations}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Navigation buttons - center */}
|
||||||
|
<PageNavigation
|
||||||
|
pagination={pagination}
|
||||||
|
updatePagination={updatePagination}
|
||||||
|
loading={loading}
|
||||||
|
lang={lang}
|
||||||
|
translations={translations}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Items per page selector - right side */}
|
||||||
|
<PageSizeSelector
|
||||||
|
pagination={pagination}
|
||||||
|
updatePagination={updatePagination}
|
||||||
|
lang={lang}
|
||||||
|
translations={translations}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
export * from './PaginationToolsComponent';
|
||||||
|
export * from './PaginationStats';
|
||||||
|
export * from './PageNavigation';
|
||||||
|
export * from './PageSizeSelector';
|
||||||
|
export * from './types';
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { PagePagination } from "../hooks/useDataFetching";
|
||||||
|
|
||||||
|
export interface ResponseMetadata {
|
||||||
|
totalCount: number;
|
||||||
|
totalItems: number;
|
||||||
|
totalPages: number;
|
||||||
|
pageCount: number;
|
||||||
|
allCount?: number;
|
||||||
|
next: boolean;
|
||||||
|
back: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PaginationBaseProps {
|
||||||
|
pagination: PagePagination;
|
||||||
|
updatePagination: (updates: Partial<PagePagination>) => void;
|
||||||
|
loading?: boolean;
|
||||||
|
lang: string;
|
||||||
|
translations: Record<string, any>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
"use client";
|
||||||
|
import React, { useCallback } from "react";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||||
|
import { SelectQueryModifierProps } from "./types";
|
||||||
|
|
||||||
|
export const SelectQueryModifier: React.FC<SelectQueryModifierProps> = ({
|
||||||
|
fieldKey,
|
||||||
|
value,
|
||||||
|
label,
|
||||||
|
options,
|
||||||
|
placeholder,
|
||||||
|
onQueryChange,
|
||||||
|
translations,
|
||||||
|
lang,
|
||||||
|
}) => {
|
||||||
|
const t = translations[lang] || {};
|
||||||
|
|
||||||
|
const handleChange = useCallback((newValue: string) => {
|
||||||
|
const formattedValue = newValue.trim() ? `%${newValue.trim()}%` : null;
|
||||||
|
onQueryChange(`${fieldKey}__ilike`, formattedValue);
|
||||||
|
}, [fieldKey, onQueryChange]);
|
||||||
|
|
||||||
|
const handleClear = useCallback(() => {
|
||||||
|
onQueryChange(fieldKey, null);
|
||||||
|
onQueryChange(`${fieldKey}__ilike`, null);
|
||||||
|
}, [fieldKey, onQueryChange]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full">
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<label className="block text-xs font-medium">
|
||||||
|
{label || t[fieldKey] || fieldKey}
|
||||||
|
</label>
|
||||||
|
{value && (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="h-5 px-2 text-xs"
|
||||||
|
onClick={handleClear}
|
||||||
|
>
|
||||||
|
{t.clear || "Clear"}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="w-full mt-1">
|
||||||
|
<Select
|
||||||
|
value={value}
|
||||||
|
onValueChange={handleChange}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-full h-10">
|
||||||
|
<SelectValue
|
||||||
|
placeholder={placeholder || `${label || t[fieldKey] || fieldKey}...`}
|
||||||
|
/>
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{options.map((option) => (
|
||||||
|
<SelectItem key={option.value} value={option.value}>
|
||||||
|
{option.label}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
"use client";
|
||||||
|
import React, { useCallback } from "react";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Search, X } from "lucide-react";
|
||||||
|
import { TextQueryModifierProps } from "./types";
|
||||||
|
|
||||||
|
export const TextQueryModifier: React.FC<TextQueryModifierProps> = ({
|
||||||
|
fieldKey,
|
||||||
|
value,
|
||||||
|
label,
|
||||||
|
placeholder,
|
||||||
|
translations,
|
||||||
|
lang,
|
||||||
|
onQueryChange,
|
||||||
|
}) => {
|
||||||
|
const t = translations[lang] || {};
|
||||||
|
const handleChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const newValue = e.target.value; onQueryChange(fieldKey, newValue);
|
||||||
|
}, [fieldKey, onQueryChange]);
|
||||||
|
|
||||||
|
const handleClear = useCallback(() => {
|
||||||
|
onQueryChange(fieldKey, null); onQueryChange(`${fieldKey}__ilike`, null);
|
||||||
|
}, [fieldKey, onQueryChange]);
|
||||||
|
|
||||||
|
const handleKeyUp = useCallback((e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
const formattedValue = value.trim() ? `%${value.trim()}%` : null;
|
||||||
|
onQueryChange(`${fieldKey}__ilike`, formattedValue);
|
||||||
|
}
|
||||||
|
}, [fieldKey, value, onQueryChange]);
|
||||||
|
|
||||||
|
const handleSearch = useCallback(() => {
|
||||||
|
const formattedValue = value.trim() ? `%${value.trim()}%` : null;
|
||||||
|
onQueryChange(`${fieldKey}__ilike`, formattedValue);
|
||||||
|
}, [fieldKey, value, onQueryChange]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full">
|
||||||
|
<label className="block text-xs font-medium mb-1">{label || t[fieldKey] || fieldKey}</label>
|
||||||
|
<div className="relative w-full flex">
|
||||||
|
<div className="relative w-full">
|
||||||
|
<Input
|
||||||
|
placeholder={placeholder || `${label || t[fieldKey] || fieldKey}...`}
|
||||||
|
value={value || ""}
|
||||||
|
onChange={handleChange}
|
||||||
|
onKeyUp={handleKeyUp}
|
||||||
|
className="pl-8 pr-8 w-full h-10"
|
||||||
|
/>
|
||||||
|
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
|
||||||
|
{value && (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="absolute right-1 top-1 h-8 w-8"
|
||||||
|
onClick={handleClear}
|
||||||
|
><X className="h-4 w-4" /></Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="default"
|
||||||
|
size="lg"
|
||||||
|
className="ml-2"
|
||||||
|
onClick={handleSearch}
|
||||||
|
><Search className="h-4 w-4" /></Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
"use client";
|
||||||
|
import React, { useCallback, useEffect } from "react";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { User } from "lucide-react";
|
||||||
|
import { TypeQueryModifierProps } from "./types";
|
||||||
|
|
||||||
|
export const TypeQueryModifier: React.FC<TypeQueryModifierProps & { defaultValue?: string }> = ({
|
||||||
|
fieldKey,
|
||||||
|
value,
|
||||||
|
options,
|
||||||
|
onQueryChange,
|
||||||
|
translations,
|
||||||
|
lang,
|
||||||
|
defaultValue,
|
||||||
|
}) => {
|
||||||
|
const t = translations[lang] || {};
|
||||||
|
|
||||||
|
const handleTypeSelect = useCallback((selectedValue: string) => {
|
||||||
|
const formattedValue = selectedValue.trim() ? `%${selectedValue.trim()}%` : null;
|
||||||
|
onQueryChange(`${fieldKey}__ilike`, formattedValue);
|
||||||
|
}, [fieldKey, onQueryChange]);
|
||||||
|
|
||||||
|
// Apply default value on initial render if no value is set
|
||||||
|
useEffect(() => {
|
||||||
|
if (defaultValue && !value && options.some(opt => opt.value === defaultValue)) {
|
||||||
|
handleTypeSelect(defaultValue);
|
||||||
|
}
|
||||||
|
}, [defaultValue, value, options, handleTypeSelect]);
|
||||||
|
|
||||||
|
if (!options || options.length === 0) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full flex flex-col space-y-4">
|
||||||
|
<div className="font-medium text-sm flex items-center">
|
||||||
|
<User className="mr-2 h-4 w-4" />
|
||||||
|
{t.typeSelection || "Type Selection"}
|
||||||
|
</div>
|
||||||
|
{options?.map((option) => (
|
||||||
|
<Button
|
||||||
|
key={option.value}
|
||||||
|
variant={value === option.value ? "default" : "outline"}
|
||||||
|
className="justify-start"
|
||||||
|
onClick={() => handleTypeSelect(option.value)}
|
||||||
|
>
|
||||||
|
{option.icon}
|
||||||
|
{option.label}
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export * from './types';
|
||||||
|
export * from './TextQueryModifier';
|
||||||
|
export * from './SelectQueryModifier';
|
||||||
|
export * from './TypeQueryModifier';
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import { ReactNode } from 'react';
|
||||||
|
|
||||||
|
export interface QueryModifierProps {
|
||||||
|
onQueryChange: (key: string, value: string | null) => void;
|
||||||
|
translations: Record<string, Record<string, string>>;
|
||||||
|
lang: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TextQueryModifierProps extends QueryModifierProps {
|
||||||
|
fieldKey: string;
|
||||||
|
value: string;
|
||||||
|
label: string;
|
||||||
|
placeholder?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SelectQueryModifierProps extends QueryModifierProps {
|
||||||
|
fieldKey: string;
|
||||||
|
value: string;
|
||||||
|
label: string;
|
||||||
|
options: { value: string; label: string }[];
|
||||||
|
placeholder?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TypeQueryModifierProps extends QueryModifierProps {
|
||||||
|
fieldKey: string;
|
||||||
|
value: string;
|
||||||
|
options: { value: string; label: string; icon?: React.ReactNode }[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface QueryModifierValue {
|
||||||
|
key: string;
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type QueryModifierResult = Record<string, string>;
|
||||||
128
WebServices/client-frontend/src/components/common/ReadMe.md
Normal file
128
WebServices/client-frontend/src/components/common/ReadMe.md
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
# EVYOS Management Frontend - Common Components
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This directory contains modular, reusable components for building consistent UIs across the EVYOS Management Frontend. These components follow a modular design pattern where complex functionality is broken down into smaller, focused components with clear responsibilities.
|
||||||
|
|
||||||
|
## Component Structure
|
||||||
|
|
||||||
|
### ActionButtonsDisplay
|
||||||
|
- **CreateButton**: A button component for triggering create actions with translation support
|
||||||
|
- **CustomButtonComponent**: Configurable button component with selection state support
|
||||||
|
- **types.ts**: Shared type definitions for button components
|
||||||
|
|
||||||
|
### CardDisplay
|
||||||
|
- **CardDisplay**: Main component for displaying data in a responsive grid layout
|
||||||
|
- **CardItem**: Individual card component with customizable fields and actions
|
||||||
|
- **CardSkeleton**: Loading state placeholder for cards
|
||||||
|
- **schema.ts**: API response and data schemas
|
||||||
|
- **utils.ts**: Helper functions for card operations
|
||||||
|
|
||||||
|
### FormDisplay
|
||||||
|
- **FormDisplay**: Container component that handles form mode switching
|
||||||
|
- **CreateComponent**: Form implementation for creating new records
|
||||||
|
- **UpdateComponent**: Form implementation for updating existing records
|
||||||
|
- **ViewComponent**: Read-only view of record details
|
||||||
|
- **types.ts**: Type definitions for form components and modes
|
||||||
|
|
||||||
|
### HeaderSelections
|
||||||
|
- **GridSelectionComponent**: Controls the number of columns in card grid layouts
|
||||||
|
- **LanguageSelectionComponent**: Language switcher with translation support
|
||||||
|
|
||||||
|
### PaginationModifiers
|
||||||
|
- **PaginationToolsComponent**: Main container for pagination controls
|
||||||
|
- **PaginationStats**: Displays record count information
|
||||||
|
- **PageNavigation**: Handles page navigation buttons with smart page number calculation
|
||||||
|
- **PageSizeSelector**: Controls items per page selection
|
||||||
|
- **types.ts**: Type definitions including ResponseMetadata interface
|
||||||
|
|
||||||
|
### QueryModifiers
|
||||||
|
- **TextQueryModifier**: Text search input with clear functionality
|
||||||
|
- **SelectQueryModifier**: Dropdown selection for filtering
|
||||||
|
- **TypeQueryModifier**: Button-based type selection
|
||||||
|
- **types.ts**: Shared interfaces for query components
|
||||||
|
|
||||||
|
### Hooks
|
||||||
|
- **useApiData**: Custom hook for fetching and managing API data with pagination
|
||||||
|
- **useDataFetching**: Base hook for data fetching with pagination, sorting, and filtering
|
||||||
|
|
||||||
|
## Usage Example
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// Import components
|
||||||
|
import { CardDisplay } from "@/components/common/CardDisplay";
|
||||||
|
import { TextQueryModifier, SelectQueryModifier, TypeQueryModifier } from "@/components/common/QueryModifiers";
|
||||||
|
import { CreateButton } from "@/components/common/ActionButtonsDisplay/CreateButton";
|
||||||
|
import { PaginationToolsComponent } from "@/components/common/PaginationModifiers/PaginationToolsComponent";
|
||||||
|
import { GridSelectionComponent, GridSize } from "@/components/common/HeaderSelections/GridSelectionComponent";
|
||||||
|
import { LanguageSelectionComponent, Language } from "@/components/common/HeaderSelections/LanguageSelectionComponent";
|
||||||
|
import { FormDisplay } from "@/components/common/FormDisplay/FormDisplay";
|
||||||
|
import { useApiData } from "@/components/common/hooks/useApiData";
|
||||||
|
|
||||||
|
// Use the API data hook
|
||||||
|
const {
|
||||||
|
data,
|
||||||
|
pagination,
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
updatePagination,
|
||||||
|
refetch
|
||||||
|
} = useApiData<YourDataType>("/api/your-endpoint");
|
||||||
|
|
||||||
|
// Define fields to display
|
||||||
|
const showFields = ["field1", "field2", "field3"];
|
||||||
|
|
||||||
|
// Example component usage
|
||||||
|
<Card>
|
||||||
|
<CardContent>
|
||||||
|
<TextQueryModifier
|
||||||
|
fieldKey="name"
|
||||||
|
value={pagination.query["name__ilike"] ? pagination.query["name__ilike"].replace(/%/g, "") : ""}
|
||||||
|
label="Search"
|
||||||
|
onQueryChange={handleQueryChange}
|
||||||
|
translations={translations}
|
||||||
|
lang={lang}
|
||||||
|
/>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<CardDisplay
|
||||||
|
showFields={showFields}
|
||||||
|
data={data}
|
||||||
|
lang={lang}
|
||||||
|
translations={translations}
|
||||||
|
error={error}
|
||||||
|
loading={loading}
|
||||||
|
titleField="name"
|
||||||
|
onCardClick={handleCardClick}
|
||||||
|
gridCols={gridCols}
|
||||||
|
showViewIcon={true}
|
||||||
|
showUpdateIcon={true}
|
||||||
|
onViewClick={handleViewClick}
|
||||||
|
onUpdateClick={handleUpdateClick}
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
## API Response Structure
|
||||||
|
|
||||||
|
Components expect API responses in this format:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface ApiResponse<T> {
|
||||||
|
data: T[];
|
||||||
|
pagination: {
|
||||||
|
page: number;
|
||||||
|
size: number;
|
||||||
|
totalCount: number;
|
||||||
|
totalItems: number;
|
||||||
|
totalPages: number;
|
||||||
|
pageCount: number;
|
||||||
|
allCount?: number;
|
||||||
|
orderField: string[];
|
||||||
|
orderType: string[];
|
||||||
|
query: Record<string, any>;
|
||||||
|
next: boolean;
|
||||||
|
back: boolean;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 67 KiB |
@@ -0,0 +1,49 @@
|
|||||||
|
"use client";
|
||||||
|
import React from "react";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { ArrowDown, ArrowUp } from "lucide-react";
|
||||||
|
import { SortingComponentProps, SortField } from "./types";
|
||||||
|
|
||||||
|
export const SortingComponent: React.FC<SortingComponentProps> = ({
|
||||||
|
sortField,
|
||||||
|
sortDirection,
|
||||||
|
onSort,
|
||||||
|
translations,
|
||||||
|
lang,
|
||||||
|
sortFields = [
|
||||||
|
{ key: "name", label: "Name" },
|
||||||
|
{ key: "code", label: "Code" },
|
||||||
|
{ key: "type", label: "Type" },
|
||||||
|
{ key: "created_at", label: "Created" },
|
||||||
|
],
|
||||||
|
}) => {
|
||||||
|
const t = translations?.[lang] || {};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-wrap gap-2 my-4">
|
||||||
|
<div className="text-sm font-medium mr-2 flex items-center">
|
||||||
|
{t.sortBy || "Sort by:"}
|
||||||
|
</div>
|
||||||
|
{sortFields.map((field) => (
|
||||||
|
<Button
|
||||||
|
key={field.key}
|
||||||
|
variant={sortField === field.key ? "default" : "outline"}
|
||||||
|
size="sm"
|
||||||
|
onClick={() => onSort(field.key)}
|
||||||
|
className="flex items-center"
|
||||||
|
>
|
||||||
|
{t[field.key] || field.label}
|
||||||
|
{sortField === field.key && (
|
||||||
|
<>
|
||||||
|
{sortDirection === "asc" ? (
|
||||||
|
<ArrowUp className="ml-1 h-3 w-3" />
|
||||||
|
) : (
|
||||||
|
<ArrowDown className="ml-1 h-3 w-3" />
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export * from './SortingComponent';
|
||||||
|
export * from './types';
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
export interface SortField {
|
||||||
|
key: string;
|
||||||
|
label: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SortingComponentProps {
|
||||||
|
sortField: string | null;
|
||||||
|
sortDirection: "asc" | "desc" | null;
|
||||||
|
onSort: (field: string) => void;
|
||||||
|
translations?: Record<string, any>;
|
||||||
|
lang: string;
|
||||||
|
sortFields?: SortField[];
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import { useDataFetching, ApiResponse } from "./useDataFetching";
|
||||||
|
import { RequestParams } from "../schemas";
|
||||||
|
import { defaultPaginationResponse } from "@/app/api/utils/types";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook for fetching data from Next.js API routes
|
||||||
|
* @param endpoint The API endpoint to fetch data from (e.g., '/api/applications')
|
||||||
|
* @param initialParams Initial request parameters
|
||||||
|
* @returns Object containing data, pagination, loading, error, updatePagination, and refetch
|
||||||
|
*/
|
||||||
|
export function useApiData<T>(
|
||||||
|
endpoint: string,
|
||||||
|
initialParams: Partial<RequestParams> = {}
|
||||||
|
) {
|
||||||
|
const fetchFromApi = async (
|
||||||
|
params: RequestParams
|
||||||
|
): Promise<ApiResponse<T>> => {
|
||||||
|
try {
|
||||||
|
const requestBody = {
|
||||||
|
page: params.page,
|
||||||
|
size: params.size,
|
||||||
|
orderField: params.orderField,
|
||||||
|
orderType: params.orderType,
|
||||||
|
query: params.query,
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await fetch(endpoint, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify(requestBody),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`API request failed with status ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return await response.json();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching data from API:", error);
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: [],
|
||||||
|
pagination: defaultPaginationResponse,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return useDataFetching<T>(fetchFromApi, initialParams);
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
import {
|
||||||
|
retrieveApplicationbyUrl,
|
||||||
|
retrievePageList,
|
||||||
|
} from "@/apicalls/cookies/token";
|
||||||
|
import { retrievePageByUrl } from "@/eventRouters/pageRetriever";
|
||||||
|
import { PageProps } from "@/validations/translations/translation";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
export interface DashboardPageParams {
|
||||||
|
pageUrl: string;
|
||||||
|
searchParams: Promise<{ [key: string]: string | undefined }>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DashboardPageResult {
|
||||||
|
activePage: string;
|
||||||
|
searchParamsInstance: { [key: string]: string | undefined };
|
||||||
|
lang: "en" | "tr";
|
||||||
|
PageComponent: React.FC<PageProps>;
|
||||||
|
siteUrlsList: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook to retrieve and prepare dashboard page data
|
||||||
|
* Throws errors for Next.js error boundary to catch
|
||||||
|
*
|
||||||
|
* @param params The dashboard page parameters
|
||||||
|
* @returns The processed dashboard page data
|
||||||
|
* @throws Error if page URL is invalid or page component is not found
|
||||||
|
*/
|
||||||
|
export async function useDashboardPage({
|
||||||
|
pageUrl,
|
||||||
|
searchParams,
|
||||||
|
}: DashboardPageParams): Promise<DashboardPageResult> {
|
||||||
|
let searchParamsInstance: { [key: string]: string | undefined } = {};
|
||||||
|
const defaultLang = "en";
|
||||||
|
|
||||||
|
if (!pageUrl || typeof pageUrl !== "string") {
|
||||||
|
throw new Error(`Invalid page URL: ${pageUrl}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
searchParamsInstance = await searchParams;
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error resolving search parameters:", err);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
|
const applicationName = (await retrieveApplicationbyUrl(pageUrl)) || "";
|
||||||
|
const siteUrlsList = (await retrievePageList()) || [];
|
||||||
|
const lang = (searchParamsInstance?.lang as "en" | "tr") || defaultLang;
|
||||||
|
if (lang !== "en" && lang !== "tr") {
|
||||||
|
console.warn(
|
||||||
|
`Invalid language "${lang}" specified, falling back to "${defaultLang}"`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const PageComponent = retrievePageByUrl(pageUrl, applicationName);
|
||||||
|
if (!PageComponent) {
|
||||||
|
throw new Error(`Page component not found for URL: ${pageUrl}`);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
activePage: pageUrl,
|
||||||
|
searchParamsInstance,
|
||||||
|
lang,
|
||||||
|
PageComponent,
|
||||||
|
siteUrlsList,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useDashboardPage;
|
||||||
@@ -0,0 +1,167 @@
|
|||||||
|
import { useState, useEffect, useCallback, useRef } from "react";
|
||||||
|
import { RequestParams, ResponseMetadata } from "../schemas";
|
||||||
|
|
||||||
|
export interface PagePagination extends RequestParams, ResponseMetadata {}
|
||||||
|
|
||||||
|
export interface ApiResponse<T> {
|
||||||
|
data: T[];
|
||||||
|
pagination: PagePagination;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generic data fetching hook that can be used with any API endpoint
|
||||||
|
* @param fetchFunction - The API function to call for fetching data
|
||||||
|
* @param initialParams - Initial request parameters
|
||||||
|
* @returns Object containing data, pagination, loading, error, updatePagination, and refetch
|
||||||
|
*/
|
||||||
|
export function useDataFetching<T>(
|
||||||
|
fetchFunction: (params: RequestParams) => Promise<ApiResponse<T>>,
|
||||||
|
initialParams: Partial<RequestParams> = {}
|
||||||
|
) {
|
||||||
|
const [data, setData] = useState<T[]>([]);
|
||||||
|
|
||||||
|
// Request parameters - these are controlled by the user
|
||||||
|
const [requestParams, setRequestParams] = useState<RequestParams>({
|
||||||
|
page: initialParams.page || 1,
|
||||||
|
size: initialParams.size || 10,
|
||||||
|
orderField: initialParams.orderField || ["uu_id"],
|
||||||
|
orderType: initialParams.orderType || ["asc"],
|
||||||
|
query: initialParams.query || {},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Response metadata - these come from the API
|
||||||
|
const [responseMetadata, setResponseMetadata] = useState<ResponseMetadata>({
|
||||||
|
totalCount: 0,
|
||||||
|
totalItems: 0,
|
||||||
|
totalPages: 0,
|
||||||
|
pageCount: 0,
|
||||||
|
next: true,
|
||||||
|
back: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<Error | null>(null);
|
||||||
|
|
||||||
|
const fetchDataFromApi = useCallback(async () => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const result = await fetchFunction({
|
||||||
|
page: requestParams.page,
|
||||||
|
size: requestParams.size,
|
||||||
|
orderField: requestParams.orderField,
|
||||||
|
orderType: requestParams.orderType,
|
||||||
|
query: requestParams.query,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result && result.data) {
|
||||||
|
setData(result.data);
|
||||||
|
|
||||||
|
// Update response metadata from API response
|
||||||
|
if (result.pagination) {
|
||||||
|
setResponseMetadata({
|
||||||
|
totalCount: result.pagination.totalCount || 0,
|
||||||
|
totalItems: result.pagination.totalCount || 0,
|
||||||
|
totalPages: result.pagination.totalPages || 1,
|
||||||
|
pageCount: result.pagination.pageCount || 0,
|
||||||
|
allCount: result.pagination.allCount || 0,
|
||||||
|
next: result.pagination.next || false,
|
||||||
|
back: result.pagination.back || false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setError(null);
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err : new Error("Unknown error"));
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
fetchFunction,
|
||||||
|
requestParams.page,
|
||||||
|
requestParams.size,
|
||||||
|
requestParams.orderField,
|
||||||
|
requestParams.orderType,
|
||||||
|
requestParams.query,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const initialMountRef = useRef(true);
|
||||||
|
const prevRequestParamsRef = useRef<RequestParams>(requestParams);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const paramsChanged =
|
||||||
|
JSON.stringify(prevRequestParamsRef.current) !==
|
||||||
|
JSON.stringify(requestParams);
|
||||||
|
|
||||||
|
if (initialMountRef.current || paramsChanged) {
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
fetchDataFromApi();
|
||||||
|
initialMountRef.current = false;
|
||||||
|
prevRequestParamsRef.current = { ...requestParams };
|
||||||
|
}, 300);
|
||||||
|
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}
|
||||||
|
}, [fetchDataFromApi, requestParams]);
|
||||||
|
|
||||||
|
const updatePagination = useCallback((updates: Partial<RequestParams>) => {
|
||||||
|
if (updates.query) {
|
||||||
|
const transformedQuery: Record<string, any> = {};
|
||||||
|
|
||||||
|
Object.entries(updates.query).forEach(([key, value]) => {
|
||||||
|
if (
|
||||||
|
typeof value === "string" &&
|
||||||
|
!key.includes("__") &&
|
||||||
|
value.trim() !== ""
|
||||||
|
) {
|
||||||
|
transformedQuery[`${key}__ilike`] = `%${value}%`;
|
||||||
|
} else {
|
||||||
|
transformedQuery[key] = value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
updates.query = transformedQuery;
|
||||||
|
if (!updates.hasOwnProperty("page")) {
|
||||||
|
updates.page = 1;
|
||||||
|
}
|
||||||
|
setResponseMetadata({
|
||||||
|
totalCount: 0,
|
||||||
|
totalItems: 0,
|
||||||
|
totalPages: 0,
|
||||||
|
pageCount: 0,
|
||||||
|
allCount: 0,
|
||||||
|
next: true,
|
||||||
|
back: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setRequestParams((prev) => ({
|
||||||
|
...prev,
|
||||||
|
...updates,
|
||||||
|
}));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Create a combined refetch function
|
||||||
|
const refetch = useCallback(() => {
|
||||||
|
setRequestParams((prev) => ({
|
||||||
|
...prev,
|
||||||
|
page: 1,
|
||||||
|
}));
|
||||||
|
fetchDataFromApi();
|
||||||
|
}, [fetchDataFromApi]);
|
||||||
|
|
||||||
|
// Combine request params and response metadata
|
||||||
|
const pagination: PagePagination = {
|
||||||
|
...requestParams,
|
||||||
|
...responseMetadata,
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
data,
|
||||||
|
pagination,
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
updatePagination,
|
||||||
|
refetch,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,181 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cache options for the fetch request
|
||||||
|
*/
|
||||||
|
export type CacheOptions = {
|
||||||
|
/** Whether to cache the request (default: true) */
|
||||||
|
cache?: boolean;
|
||||||
|
/** Revalidate time in seconds (if not provided, uses Next.js defaults) */
|
||||||
|
revalidate?: number;
|
||||||
|
/** Force cache to be revalidated (equivalent to cache: 'no-store' in fetch) */
|
||||||
|
noStore?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request options for the fetch
|
||||||
|
*/
|
||||||
|
export type FetchOptions = {
|
||||||
|
/** HTTP method (default: 'GET') */
|
||||||
|
method?: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
|
||||||
|
/** Request headers */
|
||||||
|
headers?: HeadersInit;
|
||||||
|
/** Request body (for POST, PUT, PATCH) */
|
||||||
|
body?: any;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A hook for fetching data from an API endpoint without pagination using Next.js fetch
|
||||||
|
* @param url The API endpoint URL
|
||||||
|
* @param initialParams Initial query parameters
|
||||||
|
* @param options Additional fetch options
|
||||||
|
* @param cacheOptions Cache control options
|
||||||
|
* @returns Object containing data, loading state, error state, and refetch function
|
||||||
|
*/
|
||||||
|
export function useStandardApiFetch<T>(
|
||||||
|
url: string,
|
||||||
|
initialParams: Record<string, any> = {},
|
||||||
|
options: FetchOptions = {},
|
||||||
|
cacheOptions: CacheOptions = { cache: true }
|
||||||
|
) {
|
||||||
|
const [data, setData] = useState<T | null>(null);
|
||||||
|
const [params, setParams] = useState<Record<string, any>>(initialParams);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState<Error | null>(null);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds the URL with query parameters
|
||||||
|
*/
|
||||||
|
const buildUrl = () => {
|
||||||
|
const queryParams = new URLSearchParams();
|
||||||
|
|
||||||
|
// Add all non-null and non-empty params
|
||||||
|
Object.entries(params).forEach(([key, value]) => {
|
||||||
|
if (value !== null && value !== '') {
|
||||||
|
queryParams.append(key, String(value));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const queryString = queryParams.toString();
|
||||||
|
return queryString ? `${url}?${queryString}` : url;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configure fetch options including cache settings
|
||||||
|
*/
|
||||||
|
const getFetchOptions = (): RequestInit => {
|
||||||
|
const { method = 'GET', headers = {}, body } = options;
|
||||||
|
|
||||||
|
const fetchOptions: RequestInit = {
|
||||||
|
method,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...headers,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add body for non-GET requests if provided
|
||||||
|
if (method !== 'GET' && body) {
|
||||||
|
fetchOptions.body = typeof body === 'string' ? body : JSON.stringify(body);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Configure cache options
|
||||||
|
if (!cacheOptions.cache) {
|
||||||
|
fetchOptions.cache = 'no-store';
|
||||||
|
} else if (cacheOptions.noStore) {
|
||||||
|
fetchOptions.cache = 'no-store';
|
||||||
|
} else if (cacheOptions.revalidate !== undefined) {
|
||||||
|
fetchOptions.next = { revalidate: cacheOptions.revalidate };
|
||||||
|
}
|
||||||
|
|
||||||
|
return fetchOptions;
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchData = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const fullUrl = buildUrl();
|
||||||
|
const fetchOptions = getFetchOptions();
|
||||||
|
|
||||||
|
const response = await fetch(fullUrl, fetchOptions);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! Status: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const responseData = await response.json();
|
||||||
|
setData(responseData);
|
||||||
|
setError(null);
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err : new Error('An unknown error occurred'));
|
||||||
|
setData(null);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchData();
|
||||||
|
}, [url, JSON.stringify(params), JSON.stringify(options), JSON.stringify(cacheOptions)]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the query parameters and trigger a refetch
|
||||||
|
* @param newParams New parameters to merge with existing ones
|
||||||
|
*/
|
||||||
|
const updateParams = (newParams: Record<string, any>) => {
|
||||||
|
// Filter out null or empty string values
|
||||||
|
const filteredParams = Object.entries(newParams).reduce((acc, [key, value]) => {
|
||||||
|
if (value !== null && value !== '') {
|
||||||
|
acc[key] = value;
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}, {} as Record<string, any>);
|
||||||
|
|
||||||
|
setParams(prev => ({
|
||||||
|
...prev,
|
||||||
|
...filteredParams
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset all parameters to initial values
|
||||||
|
*/
|
||||||
|
const resetParams = () => {
|
||||||
|
setParams(initialParams);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manually trigger a refetch of the data
|
||||||
|
*/
|
||||||
|
const refetch = () => {
|
||||||
|
fetchData();
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
data,
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
updateParams,
|
||||||
|
resetParams,
|
||||||
|
refetch
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// // Basic usage (with default caching)
|
||||||
|
// const { data, loading, error, refetch } = useStandardApiFetch<schema.YourDataType>('/api/your-endpoint');
|
||||||
|
|
||||||
|
// // With no caching (for data that changes frequently)
|
||||||
|
// const { data, loading, error, refetch } = useStandardApiFetch<schema.YourDataType>(
|
||||||
|
// '/api/your-endpoint',
|
||||||
|
// {},
|
||||||
|
// {},
|
||||||
|
// { cache: false }
|
||||||
|
// );
|
||||||
|
|
||||||
|
// // With specific revalidation time
|
||||||
|
// const { data, loading, error, refetch } = useStandardApiFetch<schema.YourDataType>(
|
||||||
|
// '/api/your-endpoint',
|
||||||
|
// {},
|
||||||
|
// {},
|
||||||
|
// { revalidate: 60 } // Revalidate every 60 seconds
|
||||||
|
// );
|
||||||
26
WebServices/client-frontend/src/components/common/index.ts
Normal file
26
WebServices/client-frontend/src/components/common/index.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
// Export all components from the common directory
|
||||||
|
export { CardDisplay } from "./CardDisplay";
|
||||||
|
export { SortingComponent, type SortingComponentProps, type SortField } from "./SortingComponent";
|
||||||
|
|
||||||
|
// Export QueryModifiers
|
||||||
|
export {
|
||||||
|
TextQueryModifier,
|
||||||
|
SelectQueryModifier,
|
||||||
|
TypeQueryModifier,
|
||||||
|
type QueryModifierProps,
|
||||||
|
type TextQueryModifierProps,
|
||||||
|
type SelectQueryModifierProps,
|
||||||
|
type TypeQueryModifierProps,
|
||||||
|
type QueryModifierValue,
|
||||||
|
type QueryModifierResult
|
||||||
|
} from "./QueryModifiers";
|
||||||
|
|
||||||
|
// Export hooks
|
||||||
|
export {
|
||||||
|
useDataFetching,
|
||||||
|
type RequestParams,
|
||||||
|
type ResponseMetadata,
|
||||||
|
type PagePagination,
|
||||||
|
type ApiResponse,
|
||||||
|
} from "./hooks/useDataFetching";
|
||||||
|
export { useApiData } from "./hooks/useApiData";
|
||||||
45
WebServices/client-frontend/src/components/common/schemas.ts
Normal file
45
WebServices/client-frontend/src/components/common/schemas.ts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
// Carried schemas from any request and response
|
||||||
|
|
||||||
|
// Common request parameters interface
|
||||||
|
export interface RequestParams {
|
||||||
|
page: number;
|
||||||
|
size: number;
|
||||||
|
orderField: string[];
|
||||||
|
orderType: string[];
|
||||||
|
query: Record<string, any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Common response metadata interface
|
||||||
|
export interface ResponseMetadata {
|
||||||
|
totalCount: number;
|
||||||
|
totalItems: number;
|
||||||
|
totalPages: number;
|
||||||
|
pageCount: number;
|
||||||
|
allCount?: number;
|
||||||
|
next: boolean;
|
||||||
|
back: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generic API response interface
|
||||||
|
export interface ApiResponse<T> {
|
||||||
|
data: T[];
|
||||||
|
metadata: ResponseMetadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pagination state interface
|
||||||
|
export interface PagePagination {
|
||||||
|
page: number;
|
||||||
|
size: number;
|
||||||
|
orderField: string[];
|
||||||
|
orderType: string[];
|
||||||
|
query: Record<string, any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Language = "en" | "tr";
|
||||||
|
|
||||||
|
export interface LanguageSelectionComponentProps {
|
||||||
|
lang: Language;
|
||||||
|
setLang: (lang: Language) => void;
|
||||||
|
translations?: Record<string, any>;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
@@ -112,7 +112,6 @@ const Header: React.FC<HeaderProps> = ({ lang, setLang }) => {
|
|||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
// Close dropdowns when clicking outside
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleClickOutside = (event: MouseEvent) => {
|
const handleClickOutside = (event: MouseEvent) => {
|
||||||
if (
|
if (
|
||||||
@@ -142,7 +141,6 @@ const Header: React.FC<HeaderProps> = ({ lang, setLang }) => {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleLogout = () => {
|
const handleLogout = () => {
|
||||||
// Implement logout functionality
|
|
||||||
console.log("Logging out...");
|
console.log("Logging out...");
|
||||||
logoutActiveSession()
|
logoutActiveSession()
|
||||||
.then(() => {
|
.then(() => {
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ export const DashboardLayout: React.FC<DashboardLayoutProps> = ({
|
|||||||
children,
|
children,
|
||||||
lang,
|
lang,
|
||||||
activePage,
|
activePage,
|
||||||
|
siteUrls,
|
||||||
}) => {
|
}) => {
|
||||||
const [language, setLanguage] = useState<Language>(lang as Language);
|
const [language, setLanguage] = useState<Language>(lang as Language);
|
||||||
|
|
||||||
@@ -30,7 +31,7 @@ export const DashboardLayout: React.FC<DashboardLayoutProps> = ({
|
|||||||
<div className="min-h-screen min-w-screen flex h-screen w-screen">
|
<div className="min-h-screen min-w-screen flex h-screen w-screen">
|
||||||
{/* Sidebar */}
|
{/* Sidebar */}
|
||||||
<aside className="w-1/4 border-r p-4 overflow-y-auto">
|
<aside className="w-1/4 border-r p-4 overflow-y-auto">
|
||||||
<ClientMenu lang={language} activePage={activePage} />
|
<ClientMenu lang={language} activePage={activePage} siteUrls={siteUrls} />
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
{/* Main Content Area */}
|
{/* Main Content Area */}
|
||||||
|
|||||||
@@ -4,4 +4,5 @@ export interface DashboardLayoutProps {
|
|||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
lang: "en" | "tr";
|
lang: "en" | "tr";
|
||||||
activePage: string;
|
activePage: string;
|
||||||
|
siteUrls: string[];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ const Build = {
|
|||||||
tr: "Apartman",
|
tr: "Apartman",
|
||||||
en: "Build",
|
en: "Build",
|
||||||
},
|
},
|
||||||
siteUrl: "/build",
|
siteUrl: "/buildings",
|
||||||
};
|
};
|
||||||
|
|
||||||
const Dashboard = {
|
const Dashboard = {
|
||||||
@@ -40,7 +40,7 @@ const BuildParts = {
|
|||||||
tr: "Daireler",
|
tr: "Daireler",
|
||||||
en: "Build Parts",
|
en: "Build Parts",
|
||||||
},
|
},
|
||||||
siteUrl: "/build/parts",
|
siteUrl: "/buildings/parts",
|
||||||
};
|
};
|
||||||
|
|
||||||
const BuildArea = {
|
const BuildArea = {
|
||||||
@@ -49,7 +49,7 @@ const BuildArea = {
|
|||||||
tr: "Daire Alanları",
|
tr: "Daire Alanları",
|
||||||
en: "Build Area",
|
en: "Build Area",
|
||||||
},
|
},
|
||||||
siteUrl: "/build/area",
|
siteUrl: "/buildings/area",
|
||||||
};
|
};
|
||||||
|
|
||||||
const ManagementAccounting = {
|
const ManagementAccounting = {
|
||||||
@@ -76,7 +76,7 @@ const BuildPartsAccounting = {
|
|||||||
tr: "Daire Cari Hareketler",
|
tr: "Daire Cari Hareketler",
|
||||||
en: "Build Parts Accounting",
|
en: "Build Parts Accounting",
|
||||||
},
|
},
|
||||||
siteUrl: "/build/parts/accounting",
|
siteUrl: "/buildings/parts/accounting",
|
||||||
};
|
};
|
||||||
|
|
||||||
const AnnualMeeting = {
|
const AnnualMeeting = {
|
||||||
@@ -1,110 +0,0 @@
|
|||||||
"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;
|
|
||||||
activePage?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const NavigationMenu: React.FC<NavigationMenuProps> = ({ transformedMenu, lang, activePage }) => {
|
|
||||||
// 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) => {
|
|
||||||
// Check if this is the active page
|
|
||||||
const isActive = activePage === subSubItem.siteUrl;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Link
|
|
||||||
key={subSubItem.name}
|
|
||||||
href={subSubItem.siteUrl}
|
|
||||||
className={`flex items-center w-full p-3 pl-12 text-left transition-colors ${isActive
|
|
||||||
? "bg-emerald-200 text-emerald-900 font-medium"
|
|
||||||
: "text-gray-700 hover:bg-gray-200"}`}
|
|
||||||
>
|
|
||||||
<Home className={`h-4 w-4 mr-2 ${isActive ? "text-emerald-700" : "text-gray-500"}`} />
|
|
||||||
<span>{subSubItem.lg[lang]}</span>
|
|
||||||
</Link>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</nav>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default NavigationMenu;
|
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
"use client";
|
||||||
|
import React from "react";
|
||||||
|
import { ChevronDown, ChevronRight } from "lucide-react";
|
||||||
|
import { MenuItemProps } from "./types";
|
||||||
|
import SubMenuItem from "./SubMenuItem";
|
||||||
|
|
||||||
|
const MenuItem: React.FC<MenuItemProps> = ({
|
||||||
|
item,
|
||||||
|
firstIndex,
|
||||||
|
firstLayerIndex,
|
||||||
|
secondLayerIndex,
|
||||||
|
activeMenuPath,
|
||||||
|
lang,
|
||||||
|
activePage,
|
||||||
|
handleFirstLevelClick,
|
||||||
|
handleSecondLevelClick,
|
||||||
|
}) => {
|
||||||
|
const isFirstActive = activeMenuPath?.first === firstIndex;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<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 || isFirstActive
|
||||||
|
? "bg-emerald-50 text-emerald-700"
|
||||||
|
: "text-gray-700 hover:bg-gray-50"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span className="font-medium">{item.lg[lang as keyof typeof item.lg]}</span>
|
||||||
|
{firstIndex === firstLayerIndex || isFirstActive ? (
|
||||||
|
<ChevronDown className="h-4 w-4" />
|
||||||
|
) : (
|
||||||
|
<ChevronRight className="h-4 w-4" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* First level separator and second layer */}
|
||||||
|
{(firstIndex === firstLayerIndex || isFirstActive) && (
|
||||||
|
<div className="bg-gray-50 border-t border-gray-100">
|
||||||
|
{item.subList.map((subItem, secondIndex) => (
|
||||||
|
<SubMenuItem
|
||||||
|
key={subItem.name}
|
||||||
|
subItem={subItem}
|
||||||
|
secondIndex={secondIndex}
|
||||||
|
firstIndex={firstIndex}
|
||||||
|
firstLayerIndex={firstLayerIndex}
|
||||||
|
secondLayerIndex={secondLayerIndex}
|
||||||
|
isFirstActive={isFirstActive}
|
||||||
|
activeMenuPath={activeMenuPath}
|
||||||
|
lang={lang}
|
||||||
|
activePage={activePage}
|
||||||
|
handleSecondLevelClick={handleSecondLevelClick}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MenuItem;
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
"use client";
|
||||||
|
import React from "react";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { Home } from "lucide-react";
|
||||||
|
import { MenuLinkProps } from "./types";
|
||||||
|
|
||||||
|
const MenuLink: React.FC<MenuLinkProps> = ({ subSubItem, activePage, lang }) => {
|
||||||
|
const isActive = activePage === subSubItem.siteUrl;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
href={subSubItem.siteUrl}
|
||||||
|
className={`flex items-center w-full p-3 pl-12 text-left transition-colors ${
|
||||||
|
isActive
|
||||||
|
? "bg-emerald-200 text-emerald-900 font-medium"
|
||||||
|
: "text-gray-700 hover:bg-gray-200"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Home className={`h-4 w-4 mr-2 ${isActive ? "text-emerald-700" : "text-gray-500"}`} />
|
||||||
|
<span>{subSubItem.lg[lang as keyof typeof subSubItem.lg]}</span>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MenuLink;
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
"use client";
|
||||||
|
import React from "react";
|
||||||
|
import { ChevronDown, ChevronRight } from "lucide-react";
|
||||||
|
import { SubMenuItemProps } from "./types";
|
||||||
|
import MenuLink from "./MenuLink";
|
||||||
|
|
||||||
|
const SubMenuItem: React.FC<SubMenuItemProps> = ({
|
||||||
|
subItem,
|
||||||
|
secondIndex,
|
||||||
|
firstIndex,
|
||||||
|
firstLayerIndex,
|
||||||
|
secondLayerIndex,
|
||||||
|
isFirstActive,
|
||||||
|
activeMenuPath,
|
||||||
|
lang,
|
||||||
|
activePage,
|
||||||
|
handleSecondLevelClick,
|
||||||
|
}) => {
|
||||||
|
const isSecondActive = isFirstActive && activeMenuPath?.second === secondIndex;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<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 || isSecondActive
|
||||||
|
? "bg-emerald-100 text-emerald-800"
|
||||||
|
: "text-gray-600 hover:bg-gray-100"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span className="font-medium">{subItem.lg[lang as keyof typeof subItem.lg]}</span>
|
||||||
|
{secondIndex === secondLayerIndex || isSecondActive ? (
|
||||||
|
<ChevronDown className="h-4 w-4" />
|
||||||
|
) : (
|
||||||
|
<ChevronRight className="h-4 w-4" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Second level separator and third layer */}
|
||||||
|
{(firstIndex === firstLayerIndex && secondIndex === secondLayerIndex) || isSecondActive ? (
|
||||||
|
<div className="bg-gray-100 border-t border-gray-200">
|
||||||
|
{subItem.subList.map((subSubItem) => (
|
||||||
|
<MenuLink
|
||||||
|
key={subSubItem.name}
|
||||||
|
subSubItem={subSubItem}
|
||||||
|
activePage={activePage}
|
||||||
|
lang={lang}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SubMenuItem;
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
"use client";
|
||||||
|
import React, { useState, useEffect } from "react";
|
||||||
|
import { NavigationMenuProps, ActiveMenuPath } from "./types";
|
||||||
|
import MenuItem from "./MenuItem";
|
||||||
|
|
||||||
|
const NavigationMenu: React.FC<NavigationMenuProps> = ({ transformedMenu, lang, activePage }) => {
|
||||||
|
// State to track which menu items are expanded
|
||||||
|
const [firstLayerIndex, setFirstLayerIndex] = useState<number>(-1);
|
||||||
|
const [secondLayerIndex, setSecondLayerIndex] = useState<number>(-1);
|
||||||
|
const [activeMenuPath, setActiveMenuPath] = useState<ActiveMenuPath | null>(null);
|
||||||
|
|
||||||
|
// Find the active menu path when component mounts or activePage changes
|
||||||
|
useEffect(() => {
|
||||||
|
if (activePage && activePage !== "/dashboard") {
|
||||||
|
// Find which menu item contains the active page
|
||||||
|
transformedMenu.forEach((firstItem, firstIdx) => {
|
||||||
|
firstItem.subList.forEach((secondItem, secondIdx) => {
|
||||||
|
secondItem.subList.forEach((thirdItem) => {
|
||||||
|
if (thirdItem.siteUrl === activePage) {
|
||||||
|
setFirstLayerIndex(firstIdx);
|
||||||
|
setSecondLayerIndex(secondIdx);
|
||||||
|
setActiveMenuPath({ first: firstIdx, second: secondIdx, third: thirdItem.siteUrl });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [activePage, transformedMenu]);
|
||||||
|
|
||||||
|
// Handle first level menu click
|
||||||
|
const handleFirstLevelClick = (index: number) => {
|
||||||
|
// Only allow collapsing if we're not on an active page or if it's dashboard
|
||||||
|
if (activePage === "/dashboard" || !activeMenuPath) {
|
||||||
|
setFirstLayerIndex(index === firstLayerIndex ? -1 : index);
|
||||||
|
setSecondLayerIndex(-1); // Reset second layer selection when first layer changes
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle second level menu click
|
||||||
|
const handleSecondLevelClick = (index: number) => {
|
||||||
|
// Only allow collapsing if we're not on an active page or if it's dashboard
|
||||||
|
if (activePage === "/dashboard" || !activeMenuPath) {
|
||||||
|
setSecondLayerIndex(index === secondLayerIndex ? -1 : index);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<nav className="flex flex-col">
|
||||||
|
{transformedMenu &&
|
||||||
|
transformedMenu.map((item, firstIndex) => (
|
||||||
|
<MenuItem
|
||||||
|
key={item.name}
|
||||||
|
item={item}
|
||||||
|
firstIndex={firstIndex}
|
||||||
|
firstLayerIndex={firstLayerIndex}
|
||||||
|
secondLayerIndex={secondLayerIndex}
|
||||||
|
activeMenuPath={activeMenuPath}
|
||||||
|
lang={lang}
|
||||||
|
activePage={activePage}
|
||||||
|
handleFirstLevelClick={handleFirstLevelClick}
|
||||||
|
handleSecondLevelClick={handleSecondLevelClick}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</nav>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NavigationMenu;
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
// Import types from the handler
|
||||||
|
import {
|
||||||
|
LanguageTranslation,
|
||||||
|
FilteredMenuFirstLevel,
|
||||||
|
FilteredMenuSecondLevel,
|
||||||
|
FilteredMenuThirdLevel
|
||||||
|
} from "../handler";
|
||||||
|
|
||||||
|
// Navigation menu props and state types
|
||||||
|
export interface NavigationMenuProps {
|
||||||
|
transformedMenu: FilteredMenuFirstLevel[];
|
||||||
|
lang: string;
|
||||||
|
activePage?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ActiveMenuPath {
|
||||||
|
first: number;
|
||||||
|
second: number;
|
||||||
|
third: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Component props interfaces
|
||||||
|
export interface MenuItemProps {
|
||||||
|
item: FilteredMenuFirstLevel;
|
||||||
|
firstIndex: number;
|
||||||
|
firstLayerIndex: number;
|
||||||
|
secondLayerIndex: number;
|
||||||
|
activeMenuPath: ActiveMenuPath | null;
|
||||||
|
lang: string;
|
||||||
|
activePage?: string;
|
||||||
|
handleFirstLevelClick: (index: number) => void;
|
||||||
|
handleSecondLevelClick: (index: number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SubMenuItemProps {
|
||||||
|
subItem: FilteredMenuSecondLevel;
|
||||||
|
secondIndex: number;
|
||||||
|
firstIndex: number;
|
||||||
|
firstLayerIndex: number;
|
||||||
|
secondLayerIndex: number;
|
||||||
|
isFirstActive: boolean;
|
||||||
|
activeMenuPath: ActiveMenuPath | null;
|
||||||
|
lang: string;
|
||||||
|
activePage?: string;
|
||||||
|
handleSecondLevelClick: (index: number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MenuLinkProps {
|
||||||
|
subSubItem: FilteredMenuThirdLevel;
|
||||||
|
activePage?: string;
|
||||||
|
lang: string;
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import Menu from "./store";
|
import Menu from "./IndexStore/store";
|
||||||
|
|
||||||
// Define TypeScript interfaces for menu structure
|
// Define TypeScript interfaces for menu structure
|
||||||
export interface LanguageTranslation {
|
export interface LanguageTranslation {
|
||||||
|
|||||||
18
WebServices/client-frontend/src/components/menu/hook.ts
Normal file
18
WebServices/client-frontend/src/components/menu/hook.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { UserSelection } from "../validations/menu/menu";
|
||||||
|
|
||||||
|
export async function getUserSelectionHook(
|
||||||
|
setError: any
|
||||||
|
): Promise<UserSelection | null> {
|
||||||
|
try {
|
||||||
|
const response = await fetch("/api/cookies/selection", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
});
|
||||||
|
const data = await response.json();
|
||||||
|
// setJsonText(JSON.stringify(data));
|
||||||
|
return data.data;
|
||||||
|
} catch (error) {
|
||||||
|
setError("An error occurred during login");
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
12
WebServices/client-frontend/src/components/menu/language.ts
Normal file
12
WebServices/client-frontend/src/components/menu/language.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
const dashboardLanguage = {
|
||||||
|
tr: {
|
||||||
|
dashboard: "Kontrol Paneli",
|
||||||
|
loading: "Yükleniyor...",
|
||||||
|
},
|
||||||
|
en: {
|
||||||
|
dashboard: "Control Panel",
|
||||||
|
loading: "Loading...",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default dashboardLanguage;
|
||||||
@@ -1,92 +1,59 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React, { useEffect, useState, Suspense } from "react";
|
import React, { useEffect, useState, Suspense } from "react";
|
||||||
import { transformMenu } from "./handler";
|
|
||||||
import { retrieveUserSelection } from "@/apicalls/cookies/token";
|
import EmployeeProfileSection from "./ProfileSections/EmployeeProfileSection";
|
||||||
import EmployeeProfileSection from "./EmployeeProfileSection";
|
import OccupantProfileSection from "./ProfileSections/OccupantProfileSection";
|
||||||
import OccupantProfileSection from "./OccupantProfileSection";
|
import dashboardLanguage from "./language";
|
||||||
import ProfileLoadingState from "./ProfileLoadingState";
|
import ProfileLoadingState from "./ProfileSections/ProfileLoadingState";
|
||||||
import NavigationMenu from "./NavigationMenu";
|
import NavigationMenu from "./NavigationMenu";
|
||||||
import {
|
|
||||||
ClientMenuProps,
|
|
||||||
UserSelection,
|
|
||||||
} from "@/components/validations/menu/menu";
|
|
||||||
|
|
||||||
// Language definitions for dashboard title
|
import { transformMenu } from "./handler";
|
||||||
const dashboardLanguage = {
|
import { ClientMenuProps, UserSelection } from "@/components/validations/menu/menu";
|
||||||
tr: {
|
import { getUserSelectionHook } from "./hook";
|
||||||
dashboard: "Kontrol Paneli",
|
|
||||||
loading: "Yükleniyor...",
|
|
||||||
},
|
|
||||||
en: {
|
|
||||||
dashboard: "Control Panel",
|
|
||||||
loading: "Loading...",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const ClientMenu: React.FC<ClientMenuProps> = ({ siteUrls, lang = "en", activePage }) => {
|
const ClientMenu: React.FC<ClientMenuProps> = ({ siteUrls, lang = "en" as keyof typeof dashboardLanguage, activePage }) => {
|
||||||
|
const lng = lang as keyof typeof dashboardLanguage;
|
||||||
const transformedMenu = transformMenu(siteUrls);
|
const transformedMenu = transformMenu(siteUrls);
|
||||||
const t =
|
const [translation, setTranslation] = useState<Record<string, string>>(dashboardLanguage[lng] || dashboardLanguage.en);
|
||||||
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 [loading, setLoading] = useState<boolean>(true);
|
||||||
const [userType, setUserType] = useState<string | null>(null);
|
const [userType, setUserType] = useState<string | null>(null);
|
||||||
const [userSelectionData, setUserSelectionData] =
|
const [userSelectionData, setUserSelectionData] = useState<UserSelection | null>(null);
|
||||||
useState<UserSelection | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setTranslation(dashboardLanguage[lng] || dashboardLanguage.en);
|
||||||
|
}, [lng]);
|
||||||
|
|
||||||
// Fetch user selection data
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
getUserSelectionHook(setError).then((data) => {
|
||||||
retrieveUserSelection()
|
if (data) { setUserType(data.userType); setUserSelectionData({ userType: data.userType, selected: data.selected }) }
|
||||||
.then((data) => {
|
setLoading(false);
|
||||||
console.log("User Selection:", data);
|
}).catch((err) => { setError(err?.message); setLoading(false) }).finally(() => { setLoading(false) });
|
||||||
|
|
||||||
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 (
|
return (
|
||||||
<div className="w-full bg-white shadow-sm rounded-lg overflow-hidden">
|
<div className="w-full bg-white shadow-sm rounded-lg overflow-hidden">
|
||||||
<div className="p-4 border-b border-gray-200">
|
<div className="p-4 border-b border-gray-200">
|
||||||
<h2 className="text-xl font-bold text-center text-gray-800">
|
<h2 className="text-xl font-bold text-center text-gray-800">{translation.dashboard}</h2>
|
||||||
{t.dashboard}
|
|
||||||
</h2>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Error Section */}
|
||||||
|
{error && <p className="text-red-500">{error}</p>}
|
||||||
|
|
||||||
{/* Profile Section with Suspense */}
|
{/* Profile Section with Suspense */}
|
||||||
<div className="p-4 border-b border-gray-200 bg-gray-50">
|
<div className="p-4 border-b border-gray-200 bg-gray-50">
|
||||||
<Suspense
|
<Suspense fallback={<div className="text-center py-4">{translation.loading}</div>}>
|
||||||
fallback={<div className="text-center py-4">{t.loading}</div>}
|
{loading ? (<ProfileLoadingState loadingText={translation.loading} />) : userType === "employee" && userSelectionData ? (
|
||||||
>
|
<EmployeeProfileSection userSelectionData={userSelectionData} lang={lng} />
|
||||||
{loading ? (
|
|
||||||
<ProfileLoadingState loadingText={t.loading} />
|
|
||||||
) : userType === "employee" && userSelectionData ? (
|
|
||||||
<EmployeeProfileSection
|
|
||||||
userSelectionData={userSelectionData}
|
|
||||||
lang={lang as "en" | "tr"}
|
|
||||||
/>
|
|
||||||
) : userType === "occupant" && userSelectionData ? (
|
) : userType === "occupant" && userSelectionData ? (
|
||||||
<OccupantProfileSection
|
<OccupantProfileSection userSelectionData={userSelectionData} lang={lng} />
|
||||||
userSelectionData={userSelectionData}
|
) : (<div className="text-center text-gray-500">{translation.loading}</div>)}
|
||||||
lang={lang as "en" | "tr"}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<div className="text-center text-gray-500">{t.loading}</div>
|
|
||||||
)}
|
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Navigation Menu */}
|
{/* Navigation Menu */}
|
||||||
<NavigationMenu transformedMenu={transformedMenu} lang={lang} activePage={activePage} />
|
<NavigationMenu transformedMenu={transformedMenu} lang={lng} activePage={activePage} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { PageComponent } from "@/components/validations/translations/translation";
|
import { PageComponent } from "@/components/validations/translations/translation";
|
||||||
import { UnAuthorizedPage } from "@/components/navigator/unauthorizedpage";
|
import { UnAuthorizedPage } from "@/components/navigator/unauthorizedpage";
|
||||||
import { PageNavigator } from "../Pages";
|
import { PageNavigator } from "../../eventRouters/Pages";
|
||||||
|
|
||||||
export function retrievePageByUrlAndPageId(
|
export function retrievePageByUrlAndPageId(
|
||||||
pageId: string,
|
pageId: string,
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user