client frontend tested

This commit is contained in:
Berkay 2025-05-21 16:50:46 +03:00
parent fdf9d2edb8
commit 82d16ed3c9
53 changed files with 2669 additions and 486 deletions

View File

@ -20,7 +20,7 @@ from endpoints.index import endpoints_index
from api_validations.defaults.validations import CommonHeaders from api_validations.defaults.validations import CommonHeaders
from api_middlewares.token_provider import TokenProvider from api_middlewares.token_provider import TokenProvider
from events.auth.events import LoginHandler
auth_route = APIRouter(prefix="/authentication", tags=["Authentication Cluster"]) auth_route = APIRouter(prefix="/authentication", tags=["Authentication Cluster"])
@ -124,8 +124,12 @@ auth_route_check_token = "AuthCheckToken"
) )
def check_token(headers: CommonHeaders = Depends(CommonHeaders.as_dependency)): def check_token(headers: CommonHeaders = Depends(CommonHeaders.as_dependency)):
"""Check if token is valid""" """Check if token is valid"""
token_object = TokenProvider.get_dict_from_redis(token=headers.token) try:
return None if token_object := LoginHandler.authentication_check_token_valid(access_token=headers.token, domain=headers.domain):
return JSONResponse(status_code=status.HTTP_200_OK, content={"success": True})
except Exception as e:
print(e)
return JSONResponse(status_code=status.HTTP_401_UNAUTHORIZED, content={"success": False})
auth_route_refresh_token = "AuthRefreshToken" auth_route_refresh_token = "AuthRefreshToken"

View File

@ -402,13 +402,6 @@ class LoginHandler:
def authentication_check_token_valid(cls, domain, access_token: str) -> bool: def authentication_check_token_valid(cls, domain, access_token: str) -> bool:
redis_handler = RedisHandlers() redis_handler = RedisHandlers()
if auth_token := redis_handler.get_object_from_redis(access_token=access_token): if auth_token := redis_handler.get_object_from_redis(access_token=access_token):
if auth_token.is_employee:
if domain not in auth_token.domain_list:
raise ValueError("EYS_00112")
return True
elif auth_token.is_occupant:
if domain not in auth_token.domain_list:
raise ValueError("EYS_00113")
return True return True
return False return False

View File

@ -26,6 +26,28 @@ class TokenProvider:
return OccupantTokenObject(**redis_object) return OccupantTokenObject(**redis_object)
raise ValueError("Invalid user type") raise ValueError("Invalid user type")
@classmethod
def get_login_token_from_redis(
cls, token: Optional[str] = None, user_uu_id: Optional[str] = None
) -> Union[TokenDictType, List[TokenDictType]]:
"""
Retrieve token object from Redis using token and user_uu_id
"""
token_to_use, user_uu_id_to_use = token or "*", user_uu_id or "*"
list_of_token_dict, auth_key_list = [], [cls.AUTH_TOKEN, token_to_use, user_uu_id_to_use]
if token:
result = RedisActions.get_json(list_keys=auth_key_list, limit=1)
if first_record := result.first:
return cls.convert_redis_object_to_token(first_record)
elif user_uu_id:
result = RedisActions.get_json(list_keys=auth_key_list)
if all_records := result.all:
for all_record in all_records:
list_of_token_dict.append(cls.convert_redis_object_to_token(all_record))
return list_of_token_dict
raise ValueError("Token not found in Redis. Please check the token or user_uu_id.")
@classmethod @classmethod
def get_dict_from_redis( def get_dict_from_redis(
cls, token: Optional[str] = None, user_uu_id: Optional[str] = None cls, token: Optional[str] = None, user_uu_id: Optional[str] = None

View File

@ -1,5 +1,4 @@
"use server"; "use server";
import { retrieveAccessToken } from "@/apifetchers/mutual/cookies/token";
import { import {
DEFAULT_RESPONSE, DEFAULT_RESPONSE,
defaultHeaders, defaultHeaders,
@ -8,6 +7,7 @@ import {
ApiResponse, ApiResponse,
DEFAULT_TIMEOUT, DEFAULT_TIMEOUT,
} from "./basics"; } from "./basics";
import { retrieveAccessToken } from "@/apifetchers/mutual/cookies/token";
/** /**
* Creates a promise that rejects after a specified timeout * Creates a promise that rejects after a specified timeout
@ -116,7 +116,7 @@ async function fetchDataWithToken<T>(
cache: boolean = false, cache: boolean = false,
timeout: number = DEFAULT_TIMEOUT timeout: number = DEFAULT_TIMEOUT
): Promise<ApiResponse<T>> { ): Promise<ApiResponse<T>> {
const accessToken = (await retrieveAccessToken()) || ""; const accessToken = (await retrieveAccessToken());
const headers = { const headers = {
...defaultHeaders, ...defaultHeaders,
"eys-acs-tkn": accessToken, "eys-acs-tkn": accessToken,

View File

@ -12,6 +12,7 @@ import {
defaultLinkList defaultLinkList
} from "@/types/mutual/context/validations"; } from "@/types/mutual/context/validations";
import { retrievePageList } from "@/apifetchers/mutual/cookies/token"; import { retrievePageList } from "@/apifetchers/mutual/cookies/token";
import { deleteAllCookies } from "@/apifetchers/mutual/cookies/cookie-actions";
import { setMenuToRedis } from "@/apifetchers/mutual/context/page/menu/fetch"; import { setMenuToRedis } from "@/apifetchers/mutual/context/page/menu/fetch";
const loginEndpoint = `${baseUrlAuth}/authentication/login`; const loginEndpoint = `${baseUrlAuth}/authentication/login`;
@ -33,11 +34,8 @@ interface LoginSelectOccupant {
} }
async function logoutActiveSession() { async function logoutActiveSession() {
const cookieStore = await cookies();
const response = await fetchDataWithToken(logoutEndpoint, {}, "GET", false); const response = await fetchDataWithToken(logoutEndpoint, {}, "GET", false);
cookieStore.delete("eys-zzz"); await deleteAllCookies();
cookieStore.delete("eys-yyy");
cookieStore.delete("eys-sel");
return response; return response;
} }
@ -82,6 +80,7 @@ async function loginViaAccessKeys(payload: LoginViaAccessKeys) {
"POST", "POST",
false false
); );
await deleteAllCookies()
if (response.status === 200 || response.status === 202) { if (response.status === 200 || response.status === 202) {
const loginRespone: any = response?.data; const loginRespone: any = response?.data;
@ -157,8 +156,7 @@ async function loginSelectEmployee(payload: LoginSelect) {
const employeeUUID = payload.uuid; const employeeUUID = payload.uuid;
const redisKey = `CLIENT:EMPLOYEE:${employeeUUID}`; const redisKey = `CLIENT:EMPLOYEE:${employeeUUID}`;
const selectResponse: any = await fetchDataWithToken(loginSelectEndpoint, { uuid: employeeUUID }, "POST", false); const selectResponse: any = await fetchDataWithToken(loginSelectEndpoint, { uuid: employeeUUID }, "POST", false);
cookieStore.delete({ name: "eys-sel", ...cookieObject });
cookieStore.delete("eys-sel");
if (selectResponse.status === 200 || selectResponse.status === 202) { if (selectResponse.status === 200 || selectResponse.status === 202) {
const usersSelection = await nextCrypto.encrypt( const usersSelection = await nextCrypto.encrypt(

View File

@ -3,30 +3,201 @@ import { redis } from "@/lib/redis";
import { functionRetrieveUserSelection } from "@/apifetchers/utils"; import { functionRetrieveUserSelection } from "@/apifetchers/utils";
import { ClientRedisToken } from "@/types/mutual/context/validations"; import { ClientRedisToken } from "@/types/mutual/context/validations";
// Redis operation timeout (5 seconds)
const REDIS_TIMEOUT = 5000;
// Default values for Redis data
const defaultValues: ClientRedisToken = {
online: {
lastLogin: new Date(),
lastLogout: new Date(),
lastAction: new Date(),
lastPage: "/dashboard",
userType: "employee",
lang: "tr",
timezone: "GMT+3"
},
pageConfig: {
mode: "light",
textFont: 14,
theme: "default"
},
menu: {
selectionList: ["/dashboard"],
activeSelection: "/dashboard"
},
header: {
header: [],
activeDomain: "",
listOfDomains: [],
connections: []
},
selection: {
selectionList: [],
activeSelection: {}
},
user: {
uuid: "",
avatar: "",
email: "",
phone_number: "",
user_tag: "",
password_expiry_begins: new Date().toISOString(),
person: {
uuid: "",
firstname: "",
surname: "",
middle_name: "",
sex_code: "",
person_tag: "",
country_code: "",
birth_date: ""
}
},
settings: {
lastOnline: new Date(),
token: ""
},
chatRoom: {
linkList: []
},
notifications: {
linkList: []
},
messages: {
linkList: []
}
};
/**
* Gets the complete data from Redis with improved error handling and timeouts
* @returns The complete Redis data or default values if there's an error
*/
const getCompleteFromRedis = async (): Promise<ClientRedisToken> => { const getCompleteFromRedis = async (): Promise<ClientRedisToken> => {
const decrpytUserSelection = await functionRetrieveUserSelection() try {
let decrpytUserSelection;
try {
decrpytUserSelection = await functionRetrieveUserSelection();
} catch (error) {
console.error('Error retrieving user selection:', error);
return defaultValues;
}
const redisKey = decrpytUserSelection?.redisKey; const redisKey = decrpytUserSelection?.redisKey;
if (!redisKey) throw new Error("No redis key found");
const result = await redis.get(`${redisKey}`); if (!redisKey) {
if (!result) throw new Error("No data found in redis"); console.error("No redis key found in user selection");
return JSON.parse(result); return defaultValues;
}
if (redisKey === "default") {
return defaultValues;
}
try {
const timeoutPromise = new Promise<string | null>((_, reject) => {
setTimeout(() => reject(new Error('Redis operation timed out')), REDIS_TIMEOUT);
});
// Race the Redis operation against the timeout
const result = await Promise.race([
redis.get(`${redisKey}`),
timeoutPromise
]);
if (!result) {
return defaultValues;
}
try {
const parsedResult = JSON.parse(result);
return parsedResult;
} catch (parseError) {
return defaultValues;
}
} catch (redisError) {
return defaultValues;
}
} catch (error) {
return defaultValues;
}
} }
const setCompleteToRedis = async (completeObject: ClientRedisToken) => { /**
const decrpytUserSelection = await functionRetrieveUserSelection() * Sets the complete data in Redis with improved error handling and timeouts
if (!decrpytUserSelection) throw new Error("No user selection found"); * @param completeObject The complete data to set in Redis
* @returns True if successful, false otherwise
*/
const setCompleteToRedis = async (completeObject: ClientRedisToken): Promise<boolean> => {
try {
if (!completeObject) {
return false;
}
let decrpytUserSelection;
try {
decrpytUserSelection = await functionRetrieveUserSelection();
} catch (error) {
return false;
}
if (!decrpytUserSelection) {
return false;
}
const redisKey = decrpytUserSelection?.redisKey; const redisKey = decrpytUserSelection?.redisKey;
if (!redisKey) throw new Error("No redis key found"); if (!redisKey) {
if (!completeObject) throw new Error("No complete object provided"); return false;
await redis.set(redisKey, JSON.stringify(completeObject)); }
try {
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => reject(new Error('Redis operation timed out')), REDIS_TIMEOUT);
});
await Promise.race([
redis.set(redisKey, JSON.stringify(completeObject)),
timeoutPromise
]);
return true; return true;
} catch (redisError) {
return false;
}
} catch (error) {
return false;
}
} }
const setNewCompleteToRedis = async (completeObject: ClientRedisToken, redisKey: string) => { /**
if (!redisKey) throw new Error("No redis key found"); * Sets new complete data in Redis with a specific key with improved error handling and timeouts
if (!completeObject) throw new Error("No complete object provided"); * @param completeObject The complete data to set in Redis
await redis.set(redisKey, JSON.stringify(completeObject)); * @param redisKey The specific Redis key to use
* @returns True if successful, false otherwise
*/
const setNewCompleteToRedis = async (completeObject: ClientRedisToken, redisKey: string): Promise<boolean> => {
try {
if (!redisKey) {
return false;
}
if (!completeObject) {
return false;
}
try {
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => reject(new Error('Redis operation timed out')), REDIS_TIMEOUT);
});
await Promise.race([
redis.set(redisKey, JSON.stringify(completeObject)),
timeoutPromise
]);
return true; return true;
} catch (redisError) {
return false;
}
} catch (error) {
return false;
}
} }
export { getCompleteFromRedis, setCompleteToRedis, setNewCompleteToRedis }; export { getCompleteFromRedis, setCompleteToRedis, setNewCompleteToRedis };

View File

@ -4,79 +4,170 @@ import { functionRetrieveUserSelection } from "@/apifetchers/utils";
import { ClientOnline } from "@/types/mutual/context/validations"; import { ClientOnline } from "@/types/mutual/context/validations";
import { getCompleteFromRedis, setCompleteToRedis } from "@/apifetchers/mutual/context/complete/fetch"; import { getCompleteFromRedis, setCompleteToRedis } from "@/apifetchers/mutual/context/complete/fetch";
// Default online object to use as fallback
const defaultOnlineObject: ClientOnline = {
lang: "en",
userType: "occupant",
lastLogin: new Date(),
lastLogout: new Date(),
lastAction: new Date(),
lastPage: "/auth/login",
timezone: "GMT+3"
};
// Redis operation timeout (5 seconds)
const REDIS_TIMEOUT = 5000;
/**
* Gets the online state from Redis
* @returns The online state object
*/
const getOnlineFromRedis = async (): Promise<ClientOnline> => { const getOnlineFromRedis = async (): Promise<ClientOnline> => {
try { try {
// Get user selection with default fallback console.log('Getting online state from Redis...');
const decrpytUserSelection = await functionRetrieveUserSelection();
const redisKey = decrpytUserSelection?.redisKey;
// If we have a default redisKey, return a default online object // Get user selection with default fallback
if (redisKey === "default") { let decrpytUserSelection;
return { try {
lang: "en", decrpytUserSelection = await functionRetrieveUserSelection();
userType: "occupant", console.log('User selection retrieved successfully');
lastLogin: new Date(), } catch (error) {
lastLogout: new Date(), console.error('Error retrieving user selection:', error);
lastAction: new Date(), return defaultOnlineObject;
lastPage: "/auth/login", }
timezone: "GMT+3"
}; const redisKey = decrpytUserSelection?.redisKey;
console.log('Redis key:', redisKey);
// If we have a default redisKey, return a default online object
if (!redisKey || redisKey === "default") {
console.log('Using default online object due to default/missing redisKey');
return defaultOnlineObject;
}
// Try to get data from Redis with timeout
let result;
try {
// Create a promise that rejects after the timeout
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => reject(new Error('Redis operation timed out')), REDIS_TIMEOUT);
});
// Race the Redis operation against the timeout
result = await Promise.race([
redis.get(`${redisKey}`),
timeoutPromise
]) as string | null;
console.log('Redis get operation completed');
} catch (redisError) {
console.error('Error accessing Redis:', redisError);
return defaultOnlineObject;
} }
// Try to get data from Redis
const result = await redis.get(`${redisKey}`);
if (!result) { if (!result) {
return { console.log('No data found in Redis for key:', redisKey);
lang: "en", return defaultOnlineObject;
userType: "occupant",
lastLogin: new Date(),
lastLogout: new Date(),
lastAction: new Date(),
lastPage: "/auth/login",
timezone: "GMT+3"
};
} }
// Parse the result // Parse the result
try {
const parsedResult = JSON.parse(result); const parsedResult = JSON.parse(result);
console.log('Successfully parsed Redis result');
if (!parsedResult.online) { if (!parsedResult.online) {
return { console.warn('No online object in parsed result');
lang: "en", return defaultOnlineObject;
userType: "occupant",
lastLogin: new Date(),
lastLogout: new Date(),
lastAction: new Date(),
lastPage: "/auth/login",
timezone: "GMT+3"
};
} }
console.log('Returning online object from Redis');
return parsedResult.online; return parsedResult.online;
} catch (parseError) {
console.error('Error parsing Redis result:', parseError);
return defaultOnlineObject;
}
} catch (error) { } catch (error) {
console.error("Error getting online from Redis:", error); console.error('Unexpected error in getOnlineFromRedis:', error);
// Return default online object in case of any error return defaultOnlineObject;
return {
lang: "en",
userType: "occupant",
lastLogin: new Date(),
lastLogout: new Date(),
lastAction: new Date(),
lastPage: "/auth/login",
timezone: "GMT+3"
};
} }
} }
const setOnlineToRedis = async (onlineObject: ClientOnline) => { /**
const decrpytUserSelection = await functionRetrieveUserSelection() * Sets the online state in Redis
if (!decrpytUserSelection) throw new Error("No user selection found"); * @param onlineObject The online state to set
* @returns True if successful, false otherwise
*/
const setOnlineToRedis = async (onlineObject: ClientOnline): Promise<boolean> => {
try {
console.log('Setting online state in Redis:', onlineObject);
// Validate input
if (!onlineObject) {
console.error('No online object provided');
return false;
}
// Get user selection
let decrpytUserSelection;
try {
decrpytUserSelection = await functionRetrieveUserSelection();
console.log('User selection retrieved successfully');
} catch (error) {
console.error('Error retrieving user selection:', error);
return false;
}
if (!decrpytUserSelection) {
console.error('No user selection found');
return false;
}
const redisKey = decrpytUserSelection?.redisKey; const redisKey = decrpytUserSelection?.redisKey;
if (!redisKey) throw new Error("No redis key found"); if (!redisKey) {
if (!onlineObject) throw new Error("No online object provided"); console.error('No redis key found in user selection');
const oldData = await getCompleteFromRedis(); return false;
if (!oldData) throw new Error("No old data found in redis"); }
await setCompleteToRedis({ ...oldData, online: onlineObject });
console.log('Using Redis key:', redisKey);
// Get existing data from Redis
let oldData;
try {
oldData = await getCompleteFromRedis();
console.log('Retrieved existing data from Redis');
} catch (error) {
console.error('Error getting complete data from Redis:', error);
return false;
}
if (!oldData) {
console.error('No existing data found in Redis');
return false;
}
// Update Redis with timeout
try {
// Create a promise that rejects after the timeout
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => reject(new Error('Redis operation timed out')), REDIS_TIMEOUT);
});
// Race the Redis operation against the timeout
await Promise.race([
setCompleteToRedis({ ...oldData, online: onlineObject }),
timeoutPromise
]);
console.log('Successfully updated online state in Redis');
return true; return true;
} catch (redisError) {
console.error('Error updating Redis:', redisError);
return false;
}
} catch (error) {
console.error('Unexpected error in setOnlineToRedis:', error);
return false;
}
} }
export { getOnlineFromRedis, setOnlineToRedis }; export { getOnlineFromRedis, setOnlineToRedis };

View File

@ -0,0 +1,18 @@
'use server';
import { cookies } from "next/headers";
import { cookieObject } from "@/apifetchers/basics";
/**
* Server action to delete all access cookies at once
* This is a direct server action that can be called from server components
*/
export async function deleteAllCookies() {
try {
const cookieStore = await cookies();
if (cookieStore.has("eys-zzz")) { cookieStore.delete({ name: "eys-zzz", ...cookieObject }); }
if (cookieStore.has("eys-yyy")) { cookieStore.delete({ name: "eys-yyy", ...cookieObject }); }
if (cookieStore.has("eys-sel")) { cookieStore.delete({ name: "eys-sel", ...cookieObject }); }
return true;
} catch (error) { console.error("Error in deleteAllCookies:", error); return false }
}

View File

@ -1,4 +1,3 @@
"use server";
import NextCrypto from "next-crypto"; import NextCrypto from "next-crypto";
import { fetchDataWithToken } from "@/apifetchers/api-fetcher"; import { fetchDataWithToken } from "@/apifetchers/api-fetcher";
@ -18,7 +17,7 @@ function fetchResponseStatus(response: any) {
async function checkAccessTokenIsValid() { async function checkAccessTokenIsValid() {
try { try {
const response = await fetchDataWithToken(checkToken, {}, "GET", false); const response = await fetchDataWithToken(checkToken, {}, "GET", false);
return fetchResponseStatus(response) ? true : false; return fetchResponseStatus(response);
} catch (error) { } catch (error) {
console.error("Error checking token validity:", error); console.error("Error checking token validity:", error);
return false; return false;
@ -37,15 +36,22 @@ async function retrieveApplicationbyUrl(pageUrl: string) {
async function retrieveAccessToken() { async function retrieveAccessToken() {
const cookieStore = await cookies(); const cookieStore = await cookies();
try {
const encrpytAccessToken = cookieStore.get("eys-zzz")?.value || ""; const encrpytAccessToken = cookieStore.get("eys-zzz")?.value || "";
return encrpytAccessToken ? await nextCrypto.decrypt(encrpytAccessToken) : null; return await nextCrypto.decrypt(encrpytAccessToken) || "";
}
catch (error) { console.error("Error retrieving access token:", error) }
return "";
} }
async function retrieveAccessObjects() { async function retrieveAccessObjects() {
const cookieStore = await cookies(); const cookieStore = await cookies();
try {
const encrpytAccessObject = cookieStore.get("eys-yyy")?.value || ""; const encrpytAccessObject = cookieStore.get("eys-yyy")?.value || "";
const decrpytAccessObject = await nextCrypto.decrypt(encrpytAccessObject); return await nextCrypto.decrypt(encrpytAccessObject) || "";
return decrpytAccessObject ? JSON.parse(decrpytAccessObject) : null; }
catch (error) { console.error("Error retrieving access objects:", error) }
return "";
} }
export { export {

View File

@ -5,13 +5,48 @@ import { cookies } from "next/headers";
const nextCrypto = new NextCrypto(tokenSecret); const nextCrypto = new NextCrypto(tokenSecret);
/**
* Retrieves user selection from cookies with graceful fallback
* @returns User selection object or default selection if not found
*/
const functionRetrieveUserSelection = async () => { const functionRetrieveUserSelection = async () => {
try {
const cookieStore = await cookies(); const cookieStore = await cookies();
const encrpytUserSelection = cookieStore.get("eys-sel")?.value || ""; const encrpytUserSelection = cookieStore.get("eys-sel")?.value || "";
if (!encrpytUserSelection) throw new Error("No user selection found");
if (!encrpytUserSelection) {
return {
redisKey: "default",
uuid: "",
timestamp: new Date().toISOString()
};
}
try {
const decrpytUserSelection = await nextCrypto.decrypt(encrpytUserSelection); const decrpytUserSelection = await nextCrypto.decrypt(encrpytUserSelection);
if (!decrpytUserSelection) throw new Error("No user selection found"); if (!decrpytUserSelection) {
return {
redisKey: "default",
uuid: "",
timestamp: new Date().toISOString()
};
}
return JSON.parse(decrpytUserSelection); return JSON.parse(decrpytUserSelection);
} catch (decryptError) {
return {
redisKey: "default",
uuid: "",
timestamp: new Date().toISOString()
};
}
} catch (error) {
return {
redisKey: "default",
uuid: "",
timestamp: new Date().toISOString()
};
}
} }
const functionSetUserSelection = async (userSelection: any) => { const functionSetUserSelection = async (userSelection: any) => {

View File

@ -1,17 +1,21 @@
'use server'; 'use server';
import { AuthLayout } from "@/layouts/auth/layout"; import { AuthLayout } from "@/layouts/auth/layout";
import { AuthServerProps } from "@/validations/mutual/pages/props"; import { AuthServerProps } from "@/validations/mutual/pages/props";
import { checkContextPageOnline } from "@/components/mutual/context/online/context"; import { LanguageTypes } from "@/validations/mutual/language/validations";
import getPage from "@/webPages/getPage"; import { checkAccessTokenIsValid } from "@/apifetchers/mutual/cookies/token";
import Login from "@/webPages/auth/login/page";
import Select from "@/webPages/auth/select/page";
import { getOnlineFromRedis } from "@/apifetchers/mutual/context/page/online/fetch";
const AuthPageEn = async ({ params, searchParams }: AuthServerProps) => { const AuthPageSSR = async ({ params, searchParams }: AuthServerProps) => {
const online = await checkContextPageOnline();
const lang = online?.lang || "en";
const awaitedParams = await params; const awaitedParams = await params;
const awaitedSearchParams = await searchParams; const awaitedSearchParams = await searchParams;
const pageUrlFromParams = `/${awaitedParams.page?.join("/")}` || "/login"; const pageUrlFromParams = `/${awaitedParams.page?.join("/")}` || "/login";
const FoundPage = getPage(pageUrlFromParams, { language: lang, query: awaitedSearchParams }); const tokenValid = await checkAccessTokenIsValid();
return <AuthLayout lang={lang} page={FoundPage} activePageUrl={pageUrlFromParams} /> let FoundPage = <Login language={"en"} query={awaitedSearchParams} />
const online = await getOnlineFromRedis();
if (tokenValid && online) { FoundPage = <Select language={online?.lang as LanguageTypes} type={online?.userType} /> }
return <div className="flex flex-col items-center justify-center"><AuthLayout lang={online?.lang as LanguageTypes} page={FoundPage} activePageUrl={pageUrlFromParams} /></div>
} }
export default AuthPageEn; export default AuthPageSSR;

View File

@ -1,10 +1,27 @@
'use server'; 'use server';
import { MaindasboardPageProps } from "@/validations/mutual/dashboard/props"; import { MaindasboardPageProps } from "@/validations/mutual/dashboard/props";
import { DashboardLayout } from "@/layouts/dashboard/layout"; import { DashboardLayout } from "@/layouts/dashboard/layout";
import { redirect } from "next/navigation";
import { fetchDataWithToken } from "@/apifetchers/api-fetcher";
import { baseUrlAuth } from "@/apifetchers/basics";
// Function to check token validity without trying to delete cookies
async function isTokenValid() {
try {
const checkToken = `${baseUrlAuth}/authentication/token/check`;
const response = await fetchDataWithToken(checkToken, {}, "GET", false);
return response?.status >= 200 && response?.status < 300;
} catch (error) {
console.error("Error checking token validity:", error);
return false;
}
}
const MainEnPage: React.FC<MaindasboardPageProps> = async ({ params, searchParams }) => { const MainEnPage: React.FC<MaindasboardPageProps> = async ({ params, searchParams }) => {
const parameters = await params; const parameters = await params;
const searchParameters = await searchParams; const searchParameters = await searchParams;
const tokenValid = await isTokenValid()
if (!tokenValid) { redirect("/auth/login") }
return ( return (
<div className="flex flex-col items-center justify-center"> <div className="flex flex-col items-center justify-center">
<DashboardLayout params={parameters} searchParams={searchParameters} lang="en" /> <DashboardLayout params={parameters} searchParams={searchParameters} lang="en" />

View File

@ -1,33 +0,0 @@
import { NextResponse } from "next/server";
import { retrieveAccessToken } from "@/apifetchers/mutual/cookies/token";
export async function GET() {
try {
// Check if token exists
const token = await retrieveAccessToken();
if (!token) {
return NextResponse.json({
status: 401,
data: null,
error: "No token found",
});
}
// In a real implementation, you would validate the token
// For now, just return success if a token exists
return NextResponse.json({
status: 200,
data: {
valid: true,
},
});
} catch (error) {
console.error("Error checking token:", error);
return NextResponse.json({
status: 500,
data: null,
error: "Error validating token",
});
}
}

View File

@ -0,0 +1,14 @@
import { NextResponse } from "next/server";
import { checkAccessTokenIsValid } from "@/apifetchers/mutual/cookies/token";
export async function GET() {
try {
const token = await checkAccessTokenIsValid();
if (token) {
return NextResponse.json({ status: 200 });
}
} catch (error) {
console.error("Error checking token:", error);
}
return NextResponse.json({ status: 401 });
}

View File

@ -0,0 +1,12 @@
import { NextResponse } from "next/server";
import { removeAllAccessCookies } from "@/apifetchers/mutual/cookies/token";
export async function GET() {
try {
await removeAllAccessCookies();
return NextResponse.json({ status: 200 });
} catch (error) {
console.error("Error checking token:", error);
return NextResponse.json({ status: 401 });
}
}

View File

@ -10,7 +10,6 @@ export async function POST() {
const data = await result.json(); const data = await result.json();
return NextResponse.json({ status: 200, data: data }); return NextResponse.json({ status: 200, data: data });
} catch (error) { } catch (error) {
console.log(error);
return NextResponse.json({ status: 500, message: "No data is found" }); return NextResponse.json({ status: 500, message: "No data is found" });
} }
} }

View File

@ -9,7 +9,6 @@ const loginSchemaEmployee = z.object({
export async function POST(req: Request): Promise<NextResponse> { export async function POST(req: Request): Promise<NextResponse> {
try { try {
const headers = req.headers; const headers = req.headers;
console.log("headers", Object.entries(headers));
const body = await req.json(); const body = await req.json();
const dataValidated = { const dataValidated = {
uuid: body.uuid, uuid: body.uuid,

View File

@ -1,12 +1,36 @@
import { ContentProps } from "@/validations/mutual/dashboard/props"; import { ContentProps } from "@/validations/mutual/dashboard/props";
import { resolveWhichPageToRenderMulti } from "@/pages/resolver/resolver";
import ContentToRenderNoPage from "@/pages/mutual/noContent/page"; import ContentToRenderNoPage from "@/pages/mutual/noContent/page";
import pageIndexMulti from "@/pages/multi/index";
const PageToBeChildrendMulti: React.FC<ContentProps> = ({
const PageToBeChildrendMulti: React.FC<ContentProps> = async ({ lang, activePageUrl, mode }) => { lang,
const ApplicationToRender = await resolveWhichPageToRenderMulti({ activePageUrl }) activePageUrl,
if (!ApplicationToRender) return <ContentToRenderNoPage lang={lang} /> mode,
return <ApplicationToRender lang={lang} activePageUrl={activePageUrl} mode={mode} /> userData,
userLoading,
userError,
selectionData,
selectionLoading,
selectionError,
useReloadWindow
}) => {
const pageComponents = pageIndexMulti[activePageUrl];
if (!pageComponents) { return <ContentToRenderNoPage lang={lang} /> }
const ComponentKey = Object.keys(pageComponents)[0];
const PageComponent = pageComponents[ComponentKey];
if (!PageComponent) { return <ContentToRenderNoPage lang={lang} /> }
return <PageComponent
lang={lang}
activePageUrl={activePageUrl}
mode={mode}
userData={userData}
userLoading={userLoading}
userError={userError}
selectionData={selectionData}
selectionLoading={selectionLoading}
selectionError={selectionError}
useReloadWindow={useReloadWindow}
/>;
} }
export default PageToBeChildrendMulti export default PageToBeChildrendMulti

View File

@ -1,8 +1,12 @@
'use server'; 'use client';
import { FC, Suspense } from "react";
import { FC, Suspense, useMemo, memo } from "react";
import { ContentProps, ModeTypes, ModeTypesList } from "@/validations/mutual/dashboard/props"; import { ContentProps, ModeTypes, ModeTypesList } from "@/validations/mutual/dashboard/props";
import LoadingContent from "@/components/mutual/loader/component";
import PageToBeChildrendMulti from "./PageToBeChildrendMulti"; import PageToBeChildrendMulti from "./PageToBeChildrendMulti";
import LoadingContent from "@/components/mutual/loader/component";
// Create a memoized version of PageToBeChildrendMulti to prevent unnecessary re-renders
const MemoizedMultiPage = memo(PageToBeChildrendMulti);
// const ContentComponent: FC<ContentProps> = async ({ lang, translations, activePageUrl, isMulti, mode }) => { // const ContentComponent: FC<ContentProps> = async ({ lang, translations, activePageUrl, isMulti, mode }) => {
// const modeFromQuery = ModeTypesList.includes(mode || '') ? mode : 'shortList' // const modeFromQuery = ModeTypesList.includes(mode || '') ? mode : 'shortList'
@ -13,12 +17,91 @@ import PageToBeChildrendMulti from "./PageToBeChildrendMulti";
// return <div className={classNameDiv}><Suspense fallback={loadingContent}><PageToBeChildrend {...renderProps} /></Suspense></div> // return <div className={classNameDiv}><Suspense fallback={loadingContent}><PageToBeChildrend {...renderProps} /></Suspense></div>
// }; // };
const ContentComponent: FC<ContentProps> = async ({ lang, activePageUrl, mode }) => { // Static fallback component to avoid state updates during render
const modeFromQuery = ModeTypesList.includes(mode || '') ? mode : 'shortList' const FallbackContent: FC<{ lang: string; activePageUrl: string; mode: string }> = memo(({ lang, activePageUrl, mode }) => (
const renderProps = { lang, activePageUrl, mode: modeFromQuery as ModeTypes } <div className="p-6 bg-white rounded-lg shadow-md">
const loadingContent = <LoadingContent height="h-16" size="w-36 h-48" plane="h-full w-full" /> <h2 className="text-2xl font-bold mb-4">Content Loading</h2>
const classNameDiv = "fixed top-24 left-80 right-0 py-10 px-15 border-emerald-150 border-l-2 overflow-y-auto h-[calc(100vh-64px)]" <p className="text-gray-600 mb-4">The requested page is currently unavailable or still loading.</p>
return <div className={classNameDiv}><Suspense fallback={loadingContent}><PageToBeChildrendMulti {...renderProps} /></Suspense></div> <div className="p-4 bg-blue-50 border border-blue-200 rounded-md">
<p className="text-sm text-blue-700">Page URL: {activePageUrl}</p>
<p className="text-sm text-blue-700">Language: {lang}</p>
<p className="text-sm text-blue-700">Mode: {mode}</p>
</div>
</div>
));
const ContentComponent: FC<ContentProps> = ({
lang, activePageUrl, mode,
userData, userLoading, userError,
selectionData, selectionLoading, selectionError,
}) => {
const page = useMemo(() => { const extractedPage = activePageUrl.split('/').pop(); return extractedPage }, [activePageUrl]);
const modeFromQuery: string = ModeTypesList.includes(mode || '') ? (mode || 'shortList') : 'shortList';
const loadingContent = <LoadingContent height="h-16" size="w-36 h-48" plane="h-full w-full" />;
const classNameDiv = "fixed top-24 left-80 right-0 py-10 px-15 border-emerald-150 border-l-2 overflow-y-auto h-[calc(100vh-64px)]";
if (selectionLoading || userLoading) { return <div className={classNameDiv}>{loadingContent}</div> }
if (selectionError || userError) {
return <div className={classNameDiv}>
<div className="p-6 bg-white rounded-lg shadow-md">
<h2 className="text-2xl font-bold mb-4 text-red-600">Error Loading Content</h2>
<p className="text-gray-600 mb-4">{selectionError || userError}</p>
</div>
</div>
}
return (
<div className={classNameDiv}>
<Suspense fallback={loadingContent}>
<div className="p-6 bg-white rounded-lg shadow-md">
<h2 className="text-2xl font-bold mb-4">Content Area</h2>
{/* Fallback Content */}
{(!userData || !selectionData) && (
<FallbackContent
lang={lang || ''}
activePageUrl={activePageUrl || ''}
mode={modeFromQuery}
/>
)}
{/* Wrap component in memo to prevent unnecessary re-renders */}
<MemoizedMultiPage
lang={lang || ''}
activePageUrl={activePageUrl || ''}
mode={modeFromQuery as ModeTypes}
userData={userData}
userLoading={userLoading}
userError={userError}
selectionData={selectionData}
selectionLoading={selectionLoading}
selectionError={selectionError}
/>
</div>
</Suspense>
</div>
);
}; };
export default ContentComponent; export default ContentComponent;
// {userData && (
// <div className="mb-6 p-4 bg-blue-50 rounded-lg">
// {/* User Information */}
// <h3 className="text-lg font-semibold mb-2">User Information</h3>
// <p>User Type: {userData.user_tag || 'N/A'}</p>
// {userData.person && (
// <p>Name: {userData.person.firstname} {userData.person.surname}</p>
// )}
// </div>
// )}
// {selectionData && (
// <div className="mb-6 p-4 bg-green-50 rounded-lg">
// {/* Selection Information */}
// <h3 className="text-lg font-semibold mb-2">Selection Information</h3>
// <p>Current Page: {activePageUrl || 'Home'}</p>
// <p>Mode: {modeFromQuery}</p>
// </div>
// )}

View File

@ -1,7 +1,7 @@
'use server'; 'use client';
import { FC } from "react"; import { FC } from "react";
import { langGetKey } from "@/lib/langGet"; import { langGetKey } from "@/lib/langGet";
import { AllProps } from "@/validations/mutual/dashboard/props"; import { FooterProps } from "@/validations/mutual/dashboard/props";
const translations = { const translations = {
en: { en: {
@ -14,11 +14,29 @@ const translations = {
} }
} }
const FooterComponent: FC<AllProps> = ({ lang, activePageUrl, prefix, mode }) => { const FooterComponent: FC<FooterProps> = ({
lang, activePageUrl, useReloadWindow, configData, configLoading, configError
}) => {
// Use the config context hook
return ( return (
<div className="fixed text-center bottom-0 left-0 right-0 h-16 p-4 border-t border-emerald-150 border-t-2 shadow-sm backdrop-blur-sm bg-emerald-50"> <div className="fixed text-center bottom-0 left-0 right-0 h-16 p-4 border-t border-emerald-150 border-t-2 shadow-sm backdrop-blur-sm bg-emerald-50">
<div className="flex justify-between items-center">
<div className="text-sm text-gray-500">
{!configLoading && configData && (
<span>Theme: {configData.theme || 'Default'}</span>
)}
</div>
<div>
<h1>{langGetKey(translations[lang], "footer")}: {langGetKey(translations[lang], "page")}</h1> <h1>{langGetKey(translations[lang], "footer")}: {langGetKey(translations[lang], "page")}</h1>
</div> </div>
<div className="text-sm text-gray-500">
{!configLoading && configData && (
<span>Text Size: {configData.textFont || 'Default'}</span>
)}
</div>
</div>
</div>
); );
}; };

View File

@ -1,9 +1,8 @@
'use client'; 'use client';
import { FC, useState, useEffect } from "react"; import { FC } from "react";
import { AllProps } from "@/validations/mutual/dashboard/props"; import { HeaderProps } from "@/validations/mutual/dashboard/props";
import LanguageSelectionComponent from "@/components/mutual/languageSelection/component";
import { langGetKey } from "@/lib/langGet"; import { langGetKey } from "@/lib/langGet";
import { checkContextPageOnline } from "@/components/mutual/context/online/context"; import LanguageSelectionComponent from "@/components/mutual/languageSelection/component";
const translations = { const translations = {
en: { en: {
@ -16,21 +15,25 @@ const translations = {
} }
} }
const HeaderComponent: FC<AllProps> = ({ lang, activePageUrl, prefix, mode }) => { const HeaderComponent: FC<HeaderProps> = ({
const [online, setOnline] = useState(false); lang, activePageUrl, prefix, mode,
useEffect(() => { onlineData, onlineLoading, onlineError,
checkContextPageOnline().then((online) => { userData, userLoading, userError,
setOnline(online); }) => {
});
}, []);
return ( return (
<div className="flex justify-between h-24 items-center p-4 border-emerald-150 border-b-2 shadow-sm backdrop-blur-sm sticky top-0 z-50 bg-emerald-50"> <div className="flex justify-between h-24 items-center p-4 border-emerald-150 border-b-2 shadow-sm backdrop-blur-sm sticky top-0 z-50 bg-emerald-50">
<div className="flex flex-row justify-center items-center"> <div className="flex flex-row justify-center items-center">
<p className="text-2xl font-bold mx-3">{langGetKey(translations[lang], 'selectedPage')} :</p> <p className="text-2xl font-bold mx-3">{langGetKey(translations[lang], 'selectedPage')} :</p>
<p className="text-lg font-bold mx-3"> {langGetKey(translations[lang], 'page')}</p> <p className="text-lg font-bold mx-3"> {activePageUrl || langGetKey(translations[lang], 'page')}</p>
</div>
<div className="flex items-center">
{!onlineLoading && onlineData && onlineData.userType && (
<div className="mr-4 text-sm">
<span className="font-semibold">{onlineData.lang || lang}</span>
<span className="ml-2 text-xs bg-green-100 text-green-800 px-2 py-1 rounded-full">{onlineData.userType}</span>
</div>
)}<LanguageSelectionComponent lang={lang} activePage={activePageUrl} prefix={prefix} />
</div> </div>
<div>{JSON.stringify(online)}</div>
<LanguageSelectionComponent lang={lang} activePage={activePageUrl} prefix={prefix} />
</div> </div>
); );
}; };

View File

@ -0,0 +1,41 @@
'use client';
import { FC, useState } from "react";
import renderOneClientSelection from "./renderOneClientSelection";
interface ClientSelectionSectionProps {
selectionData: any;
initialSelectedClient?: any;
onClientSelect?: (client: any) => void;
}
const ClientSelectionSection: FC<ClientSelectionSectionProps> = ({
selectionData,
initialSelectedClient = null,
onClientSelect
}) => {
const [selectedClient, setSelectedClient] = useState<any>(initialSelectedClient);
if (!selectionData || !selectionData.selectionList || selectionData.selectionList.length === 0) { return null }
const handleClientSelection = (client: any) => { setSelectedClient(client); if (onClientSelect) { onClientSelect(client) } };
return (
<div className="mb-3">
{selectionData.selectionList.map((client: any, index: number) => {
return (
<div
key={client.uu_id || client.id || `client-${index}`}
onClick={() => handleClientSelection(client)}
>
{client && renderOneClientSelection({
item: client,
isSelected: selectedClient?.uu_id === client.uu_id,
onClickHandler: handleClientSelection
})}
</div>
);
})}
</div>
);
};
export default ClientSelectionSection;

View File

@ -1,136 +1,45 @@
'use client'; 'use client';
import { FC, useState, useEffect } from "react"; import { FC, Suspense } from "react";
import { AllProps } from "@/validations/mutual/dashboard/props"; import { MenuProps } from "@/validations/mutual/dashboard/props";
import { parseURlFormString } from "@/lib/menuGet";
import { menuTranslation } from "@/languages/mutual/menu"
import FirstLayerDropdown from "./firstLayerComponent";
import SecondLayerDropdown from "./secondLayerComponent";
import ThirdLayerDropdown from "./thirdLayerComponent";
import { checkContextPageMenu } from "@/components/mutual/context/menu/context";
// Define types for menu structure import UserProfileSection from "./userProfileSection";
type ThirdLayerItem = Record<string, any>; import ClientSelectionSection from "./clientSelectionSection";
type SecondLayerItems = Record<string, ThirdLayerItem>; import MenuItemsSection from "./menuItemsSection";
type FirstLayerItems = Record<string, SecondLayerItems>; import MenuLoadingState from "./menuLoadingState";
type MenuStructure = FirstLayerItems; import MenuErrorState from "./menuErrorState";
import MenuEmptyState from "./menuEmptyState";
// Helper function to get translation for a URL path import LoadingContent from "@/components/mutual/loader/component";
const getMenuTranslation = (translations: any, urlPath: string) => {
if (translations[urlPath]) {
return translations[urlPath];
}
const cleanPath = urlPath.startsWith('/') ? urlPath.substring(1) : urlPath;
if (translations[cleanPath]) {
return translations[cleanPath];
}
const pathWithSlash = urlPath.startsWith('/') ? urlPath : `/${urlPath}`;
if (translations[pathWithSlash]) {
return translations[pathWithSlash];
}
const keys = Object.keys(translations);
for (const key of keys) {
const cleanKey = key.startsWith('/') ? key.substring(1) : key;
const cleanUrlPath = urlPath.startsWith('/') ? urlPath.substring(1) : urlPath;
if (cleanUrlPath.includes(cleanKey) || cleanKey.includes(cleanUrlPath)) {
return translations[key];
}
}
const parts = urlPath.split('/');
return parts[parts.length - 1] || urlPath;
};
const MenuComponent: FC<AllProps> = ({ lang, activePageUrl, prefix, mode }) => {
const [availableApplications, setAvailableApplications] = useState<string[]>([]);
useEffect(() => {
checkContextPageMenu().then((apps) => {
console.log('apps', apps);
setAvailableApplications(apps?.selectionList || []);
});
}, []);
const [expandedFirstLayer, setExpandedFirstLayer] = useState<string | null>(null);
const [expandedSecondLayer, setExpandedSecondLayer] = useState<string | null>(null);
const [menuStructure, setMenuStructure] = useState<MenuStructure>({});
const menuTranslationWLang = menuTranslation[lang];
const activePathLayers = parseURlFormString(activePageUrl).data;
const activeFirstLayer = activePathLayers[0] || null;
const activeSecondLayer = activePathLayers[1] || null;
const activeThirdLayer = activePathLayers.slice(2, activePathLayers.length).join("/");
useEffect(() => {
const newMenuStructure: MenuStructure = {};
availableApplications.forEach((appPath: string) => {
const cleanPath = appPath.startsWith('/') ? appPath.substring(1) : appPath;
const pathParts = cleanPath.split('/');
if (pathParts.length >= 3) {
const firstLayer = pathParts[0];
const secondLayer = pathParts[1];
const thirdLayer = pathParts.slice(2).join('/');
if (!newMenuStructure[firstLayer]) { newMenuStructure[firstLayer] = {} }
if (!newMenuStructure[firstLayer][secondLayer]) { newMenuStructure[firstLayer][secondLayer] = {} }
newMenuStructure[firstLayer][secondLayer][thirdLayer] = true;
}
});
setMenuStructure(newMenuStructure);
}, [availableApplications]);
useEffect(() => { if (activeFirstLayer) { setExpandedFirstLayer(activeFirstLayer); if (activeSecondLayer) { setExpandedSecondLayer(activeSecondLayer) } } }, [activeFirstLayer, activeSecondLayer]);
const handleFirstLayerClick = (key: string) => { if (expandedFirstLayer === key) { setExpandedFirstLayer(null); setExpandedSecondLayer(null) } else { setExpandedFirstLayer(key); setExpandedSecondLayer(null) } };
const handleSecondLayerClick = (key: string) => { if (expandedSecondLayer === key) { setExpandedSecondLayer(null) } else { setExpandedSecondLayer(key) } };
const renderThirdLayerItems = (firstLayerKey: string, secondLayerKey: string, thirdLayerItems: ThirdLayerItem) => {
const baseUrl = `/${firstLayerKey}/${secondLayerKey}`;
return Object.keys(thirdLayerItems).map(thirdLayerKey => {
const isActive = activeFirstLayer === firstLayerKey && activeSecondLayer === secondLayerKey && activeThirdLayer === thirdLayerKey;
const mergeUrl = `${baseUrl}/${thirdLayerKey}`;
const url = `/${lang}${baseUrl}/${thirdLayerKey}`;
console.log('mergeUrl', mergeUrl);
return <div key={`${thirdLayerKey}-item`} className="ml-2 my-1"><ThirdLayerDropdown isActive={isActive} innerText={getMenuTranslation(menuTranslationWLang, mergeUrl)} url={url} /></div>
});
};
const renderSecondLayerItems = (firstLayerKey: string, secondLayerItems: SecondLayerItems) => {
return Object.entries(secondLayerItems).map(([secondLayerKey, thirdLayerItems]) => {
const isActive = activeFirstLayer === firstLayerKey && activeSecondLayer === secondLayerKey;
const isExpanded = expandedSecondLayer === secondLayerKey;
const mergeUrl = `/${firstLayerKey}/${secondLayerKey}`;
console.log('mergeUrl', mergeUrl);
return (
<div key={`${secondLayerKey}-item`} className="ml-2 my-1">
<SecondLayerDropdown isActive={isActive} isExpanded={isExpanded} innerText={getMenuTranslation(menuTranslationWLang, mergeUrl)} onClick={() => handleSecondLayerClick(secondLayerKey)} />
{isExpanded && (<div className="ml-2 mt-1">{renderThirdLayerItems(firstLayerKey, secondLayerKey, thirdLayerItems)}</div>)}
</div>
);
});
};
const renderFirstLayerItems = () => {
return Object.entries(menuStructure).map(([firstLayerKey, secondLayerItems]) => {
const isActive = activeFirstLayer === firstLayerKey;
const isExpanded = expandedFirstLayer === firstLayerKey;
const mergeUrl = `/${firstLayerKey}`;
console.log('mergeUrl', mergeUrl);
return (
<div key={`${firstLayerKey}-item`} className="mb-2">
<FirstLayerDropdown isActive={isActive} isExpanded={isExpanded} innerText={getMenuTranslation(menuTranslationWLang, mergeUrl)} onClick={() => handleFirstLayerClick(firstLayerKey)} />
{isExpanded && (<div className="mt-1">{renderSecondLayerItems(firstLayerKey, secondLayerItems)}</div>)}
</div>
);
});
};
const MenuComponent: FC<MenuProps> = ({
lang, activePageUrl, useReloadWindow, availableApplications,
onlineData, onlineLoading, onlineError,
userData, userLoading, userError,
selectionData, selectionLoading, selectionError,
menuData, menuLoading, menuError
}) => {
if (menuLoading) { return <MenuLoadingState /> } // Render loading state
if (menuError) { return <MenuErrorState error={menuError} />; } // Render error state
if (availableApplications.length === 0) { return <MenuEmptyState />; } // Render empty state
function handleClientSelection(client: any) { console.log('Client selected:', client) }
return ( return (
<div className="fixed top-24 p-5 left-0 right-0 w-80 border-emerald-150 border-r-2 overflow-y-auto h-[calc(100vh-6rem)]"> <div className="fixed top-24 p-5 left-0 right-0 w-80 border-emerald-150 border-r-2 overflow-y-auto h-[calc(100vh-6rem)]">
<div className="flex flex-col"> <div className="flex flex-col">
{renderFirstLayerItems()} {/* User Profile Section */}
<Suspense fallback={<div><LoadingContent height="h-16" size="w-36 h-48" key={"loading-conent"} plane="h-full w-full" /></div>}>
<UserProfileSection userData={userData} onlineData={onlineData} />
</Suspense>
{/* Client Selection Section */}
<Suspense fallback={<div><LoadingContent height="h-16" size="w-36 h-48" key={"loading-conent"} plane="h-full w-full" /></div>}>
<ClientSelectionSection selectionData={selectionData} initialSelectedClient={selectionData} onClientSelect={handleClientSelection} />
</Suspense>
{/* Menu Items Section */}
<Suspense fallback={<div><LoadingContent height="h-16" size="w-36 h-48" key={"loading-conent"} plane="h-full w-full" /></div>}>
<MenuItemsSection availableApplications={availableApplications} activePageUrl={activePageUrl} lang={lang} />
</Suspense>
</div> </div>
</div> </div>
); );
}; };
export default MenuComponent; export default MenuComponent;

View File

@ -0,0 +1,18 @@
'use client';
import { FC } from "react";
const MenuEmptyState: FC = () => {
return (
<div className="fixed top-24 p-5 left-0 right-0 w-80 border-emerald-150 border-r-2 overflow-y-auto h-[calc(100vh-6rem)]">
<div className="flex justify-center items-center h-full">
<div className="text-center text-gray-500 dark:text-gray-400">
<p className="text-sm">No menu items available</p>
<p className="text-xs mt-1">Please check your permissions or contact an administrator</p>
</div>
</div>
</div>
);
};
export default MenuEmptyState;

View File

@ -0,0 +1,28 @@
'use client';
import { FC } from "react";
interface MenuErrorStateProps {
error: string | null;
}
const MenuErrorState: FC<MenuErrorStateProps> = ({ error }) => {
return (
<div className="fixed top-24 p-5 left-0 right-0 w-80 border-emerald-150 border-r-2 overflow-y-auto h-[calc(100vh-6rem)]">
<div className="flex justify-center items-center h-full">
<div className="bg-red-100 dark:bg-red-900 border border-red-400 text-red-700 dark:text-red-200 px-3 py-2 rounded relative text-xs" role="alert">
<strong className="font-bold">Error loading menu: </strong>
<span className="block sm:inline">{error}</span>
<button
className="mt-2 bg-red-500 hover:bg-red-700 text-white font-bold py-1 px-3 rounded text-xs"
onClick={() => window.location.reload()}
>
Retry
</button>
</div>
</div>
</div>
);
};
export default MenuErrorState;

View File

@ -0,0 +1,175 @@
'use client';
import { FC, useState, useEffect } from "react";
import FirstLayerDropdown from "./firstLayerComponent";
import SecondLayerDropdown from "./secondLayerComponent";
import ThirdLayerDropdown from "./thirdLayerComponent";
import { parseURlFormString } from "@/lib/menuGet";
import { menuTranslation } from "@/languages/mutual/menu";
// Define types for menu structure
type ThirdLayerItem = Record<string, any>;
type SecondLayerItems = Record<string, ThirdLayerItem>;
type FirstLayerItems = Record<string, SecondLayerItems>;
type MenuStructure = FirstLayerItems;
interface MenuItemsSectionProps {
availableApplications: string[];
activePageUrl: string;
lang: string;
}
// Helper function to get translation for a URL path
const getMenuTranslation = (translations: any, urlPath: string) => {
if (translations[urlPath]) {
return translations[urlPath];
}
const cleanPath = urlPath.startsWith('/') ? urlPath.substring(1) : urlPath;
if (translations[cleanPath]) {
return translations[cleanPath];
}
const pathWithSlash = urlPath.startsWith('/') ? urlPath : `/${urlPath}`;
if (translations[pathWithSlash]) {
return translations[pathWithSlash];
}
const keys = Object.keys(translations);
for (const key of keys) {
const cleanKey = key.startsWith('/') ? key.substring(1) : key;
const cleanUrlPath = urlPath.startsWith('/') ? urlPath.substring(1) : urlPath;
if (cleanUrlPath.includes(cleanKey) || cleanKey.includes(cleanUrlPath)) {
return translations[key];
}
}
const parts = urlPath.split('/');
return parts[parts.length - 1] || urlPath;
};
const MenuItemsSection: FC<MenuItemsSectionProps> = ({ availableApplications, activePageUrl, lang }) => {
const [expandedFirstLayer, setExpandedFirstLayer] = useState<string | null>(null);
const [expandedSecondLayer, setExpandedSecondLayer] = useState<string | null>(null);
const [menuStructure, setMenuStructure] = useState<MenuStructure>({});
const menuTranslationWLang = menuTranslation[lang as keyof typeof menuTranslation];
const activePathLayers = parseURlFormString(activePageUrl).data;
const activeFirstLayer = activePathLayers[0] || null;
const activeSecondLayer = activePathLayers[1] || null;
const activeThirdLayer = activePathLayers.slice(2, activePathLayers.length).join("/");
useEffect(() => {
const newMenuStructure: MenuStructure = {};
availableApplications.forEach((appPath: string) => {
const cleanPath = appPath.startsWith('/') ? appPath.substring(1) : appPath;
const pathParts = cleanPath.split('/');
if (pathParts.length >= 3) {
const firstLayer = pathParts[0];
const secondLayer = pathParts[1];
const thirdLayer = pathParts.slice(2).join('/');
if (!newMenuStructure[firstLayer]) { newMenuStructure[firstLayer] = {} }
if (!newMenuStructure[firstLayer][secondLayer]) { newMenuStructure[firstLayer][secondLayer] = {} }
newMenuStructure[firstLayer][secondLayer][thirdLayer] = true;
}
});
setMenuStructure(newMenuStructure);
}, [availableApplications]);
useEffect(() => {
if (activeFirstLayer) {
setExpandedFirstLayer(activeFirstLayer);
if (activeSecondLayer) {
setExpandedSecondLayer(activeSecondLayer)
}
}
}, [activeFirstLayer, activeSecondLayer]);
const handleFirstLayerClick = (key: string) => {
if (expandedFirstLayer === key) {
setExpandedFirstLayer(null);
setExpandedSecondLayer(null)
} else {
setExpandedFirstLayer(key);
setExpandedSecondLayer(null)
}
};
const handleSecondLayerClick = (key: string) => {
if (expandedSecondLayer === key) {
setExpandedSecondLayer(null)
} else {
setExpandedSecondLayer(key)
}
};
const renderThirdLayerItems = (firstLayerKey: string, secondLayerKey: string, thirdLayerItems: ThirdLayerItem) => {
const baseUrl = `/${firstLayerKey}/${secondLayerKey}`;
return Object.keys(thirdLayerItems).map(thirdLayerKey => {
const isActive = activeFirstLayer === firstLayerKey && activeSecondLayer === secondLayerKey && activeThirdLayer === thirdLayerKey;
const mergeUrl = `${baseUrl}/${thirdLayerKey}`;
const url = `/${lang}${baseUrl}/${thirdLayerKey}`;
return (
<div key={`${thirdLayerKey}-item`} className="ml-2 my-1">
<ThirdLayerDropdown
isActive={isActive}
innerText={getMenuTranslation(menuTranslationWLang, mergeUrl)}
url={url}
/>
</div>
);
});
};
const renderSecondLayerItems = (firstLayerKey: string, secondLayerItems: SecondLayerItems) => {
return Object.entries(secondLayerItems).map(([secondLayerKey, thirdLayerItems]) => {
const isActive = activeFirstLayer === firstLayerKey && activeSecondLayer === secondLayerKey;
const isExpanded = expandedSecondLayer === secondLayerKey;
const mergeUrl = `/${firstLayerKey}/${secondLayerKey}`;
return (
<div key={`${secondLayerKey}-item`} className="ml-2 my-1">
<SecondLayerDropdown
isActive={isActive}
isExpanded={isExpanded}
innerText={getMenuTranslation(menuTranslationWLang, mergeUrl)}
onClick={() => handleSecondLayerClick(secondLayerKey)}
/>
{isExpanded && (
<div className="ml-2 mt-1">
{renderThirdLayerItems(firstLayerKey, secondLayerKey, thirdLayerItems)}
</div>
)}
</div>
);
});
};
const renderFirstLayerItems = () => {
return Object.entries(menuStructure).map(([firstLayerKey, secondLayerItems]) => {
const isActive = activeFirstLayer === firstLayerKey;
const isExpanded = expandedFirstLayer === firstLayerKey;
const mergeUrl = `/${firstLayerKey}`;
return (
<div key={`${firstLayerKey}-item`} className="mb-2">
<FirstLayerDropdown
isActive={isActive}
isExpanded={isExpanded}
innerText={getMenuTranslation(menuTranslationWLang, mergeUrl)}
onClick={() => handleFirstLayerClick(firstLayerKey)}
/>
{isExpanded && (
<div className="mt-1">
{renderSecondLayerItems(firstLayerKey, secondLayerItems)}
</div>
)}
</div>
);
});
};
return (
<div className="mt-1">
<h3 className="text-sm font-semibold mb-1">Menu</h3>
{renderFirstLayerItems()}
</div>
);
};
export default MenuItemsSection;

View File

@ -0,0 +1,20 @@
'use client';
import { FC } from "react";
const MenuLoadingState: FC = () => {
return (
<div className="fixed top-24 p-5 left-0 right-0 w-80 border-emerald-150 border-r-2 overflow-y-auto h-[calc(100vh-6rem)]">
<div className="flex justify-center items-center h-full">
<div className="animate-pulse flex flex-col space-y-2 w-full">
<div className="h-6 bg-gray-300 dark:bg-gray-600 rounded w-3/4"></div>
<div className="h-6 bg-gray-300 dark:bg-gray-600 rounded w-2/3"></div>
<div className="h-6 bg-gray-300 dark:bg-gray-600 rounded w-1/2"></div>
<div className="text-center text-xs text-gray-500 dark:text-gray-400 mt-2">Loading menu...</div>
</div>
</div>
</div>
);
};
export default MenuLoadingState;

View File

@ -0,0 +1,60 @@
'use client';
import { FC } from "react";
import { Briefcase } from "lucide-react";
interface Props {
item: any;
isSelected: boolean;
onClickHandler: (item: any) => void;
}
const RenderOneClientSelection: FC<Props> = ({ item, isSelected, onClickHandler }) => {
if (isSelected) {
return (
<div key={item.uu_id} onClick={() => { onClickHandler(item) }}
className="w-full text-xs bg-white shadow rounded-lg overflow-hidden transition-all hover:shadow-md mb-2 cursor-pointer">
<div className="bg-amber-300 p-2 hover:bg-amber-400 transition-all">
<div className="flex items-center">
<div className="mr-2 relative">
<div className="w-8 h-8 rounded-full bg-amber-400 flex items-center justify-center overflow-hidden border border-white">
{item.avatar ? (<img src={item.avatar} alt="Company" className="w-full h-full object-cover" />) :
(<div className="text-white text-xs font-bold">{(item.public_name || "No Name").slice(0, 2)}</div>)}
</div>
<div className="absolute -bottom-0.5 -right-0.5 bg-white p-0.5 rounded-full border border-amber-400">
<Briefcase size={8} className="text-amber-600" />
</div>
</div>
<div className="overflow-hidden">
<h2 className="text-xs font-bold text-black truncate">{item.public_name} {item.company_type}</h2>
<p className="text-xs text-amber-800 truncate">{item.duty}</p>
</div>
</div>
</div>
</div>
)
}
return (
<div key={item.uu_id} className="w-full text-xs bg-white shadow rounded-lg overflow-hidden transition-all mb-2 cursor-pointer">
<div className="bg-gray-100 p-2">
<div className="flex items-center">
<div className="mr-2 relative">
<div className="w-8 h-8 rounded-full bg-gray-300 flex items-center justify-center overflow-hidden border border-white">
{item.avatar ? (<img src={item.avatar} alt="Company" className="w-full h-full object-cover" />) :
(<div className="text-white text-xs font-bold">{(item.duty || "No Duty").slice(0, 2)}</div>)}
</div>
<div className="absolute -bottom-0.5 -right-0.5 bg-white p-0.5 rounded-full border border-gray-300">
<Briefcase size={8} className="text-gray-600" />
</div>
</div>
<div className="overflow-hidden">
<h2 className="text-xs font-bold text-gray-700 truncate">{item.public_name} {item.company_type}</h2>
<p className="text-xs text-gray-600 truncate">{item.duty}</p>
</div>
</div>
</div>
</div>
)
}
export default RenderOneClientSelection;

View File

@ -0,0 +1,36 @@
import { ClientUser } from "@/types/mutual/context/validations";
import { FC } from "react";
interface Props {
userProfile: ClientUser;
}
const UserProfile: FC<Props> = ({ userProfile }) => {
if (!userProfile || !userProfile.person) return;
const profileuser: ClientUser = JSON.parse(JSON.stringify(userProfile));
return (
<div className="max-w-md w-full text-md bg-white shadow-lg rounded-lg overflow-hidden transition-all hover:shadow-xl">
<div className="bg-amber-300 p-4 hover:bg-amber-400 transition-all">
<div className="flex items-center">
<div className="mr-4">
<img className="rounded-full border-2 border-white" src={profileuser.avatar} alt="Avatar" width={80} height={80} />
</div>
<div><h2 className="text-md font-bold text-black">Profile Info</h2></div>
</div>
</div>
<div className="p-4">
<div className="mb-2 flex items-center">
<span className="font-semibold w-28 text-gray-700">Email:</span>
<span className="text-gray-800">{profileuser.email}</span>
</div>
<div className="mb-2 flex items-center">
<span className="font-semibold w-28 text-gray-700">Full Name:</span>
<span className="text-gray-800">{profileuser.person.firstname} {profileuser.person.surname}</span>
</div>
</div>
</div>
)
}
export default UserProfile;

View File

@ -0,0 +1,40 @@
'use client';
import { FC } from "react";
import { ClientUser } from "@/types/mutual/context/validations";
import { ClientOnline } from "@/types/mutual/context/validations";
interface UserProfileSectionProps {
userData: ClientUser | null;
onlineData: ClientOnline | null;
}
const UserProfileSection: FC<UserProfileSectionProps> = ({ userData, onlineData }) => {
if (!userData) return null;
return (
<div className="mb-2">
<div className="w-full text-xs bg-white shadow rounded-lg overflow-hidden transition-all hover:shadow-md">
<div className="bg-amber-300 p-2 hover:bg-amber-400 transition-all">
<div className="flex items-center">
<div className="mr-2">
{userData && userData.avatar ? (
<img className="rounded-full border border-white" src={userData.avatar} alt="Avatar" width={40} height={40} />
) : (
<div className="w-10 h-10 rounded-full bg-amber-400 flex items-center justify-center border border-white">
<div className="text-white text-sm font-bold">{userData?.email ? userData.email.slice(0, 2).toUpperCase() : 'U'}</div>
</div>
)}
</div>
<div className="overflow-hidden">
<h2 className="text-sm font-bold text-black truncate">{userData?.person ? `${userData.person.firstname} ${userData.person.surname}` : 'User'}</h2>
<p className="text-xs text-amber-800 truncate">{userData?.email || 'No email'}</p>
<p className="text-xs font-medium capitalize">{onlineData?.userType || 'guest'}</p>
</div>
</div>
</div>
</div>
</div>
);
};
export default UserProfileSection;

View File

@ -1,16 +1,22 @@
'use client';
import { ClientPageConfig } from "@/types/mutual/context/validations"; import { ClientPageConfig } from "@/types/mutual/context/validations";
import { API_BASE_URL } from "@/config/config"; import { API_BASE_URL } from "@/config/config";
import { createContextHook } from '../hookFactory';
async function checkContextPageConfig(): Promise<ClientPageConfig> { // Original fetch functions for backward compatibility
async function checkContextPageConfig(): Promise<ClientPageConfig | null> {
try {
const result = await fetch(`${API_BASE_URL}/context/page/config`, { const result = await fetch(`${API_BASE_URL}/context/page/config`, {
method: "GET", method: "GET",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
}); });
try {
const data = await result.json(); const data = await result.json();
if (data.status === 200) return data.data;
return data; return data;
} catch (error) { } catch (error) {
throw new Error("No data is found"); console.error("Error checking page config:", error);
return null;
} }
} }
@ -19,17 +25,47 @@ async function setContextPageConfig({
}: { }: {
pageConfig: ClientPageConfig; pageConfig: ClientPageConfig;
}) { }) {
try {
const result = await fetch(`${API_BASE_URL}/context/page/config`, { const result = await fetch(`${API_BASE_URL}/context/page/config`, {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify(pageConfig), body: JSON.stringify(pageConfig),
}); });
try {
const data = await result.json(); const data = await result.json();
return data; return data;
} catch (error) { } catch (error) {
console.error("Error setting page config:", error);
throw new Error("No data is set"); throw new Error("No data is set");
} }
} }
// Create the config hook using the factory
const useContextConfig = createContextHook<ClientPageConfig>({
endpoint: '/context/page/config',
contextName: 'config',
enablePeriodicRefresh: false
});
// Custom hook for config data with the expected interface
interface UseConfigResult {
configData: ClientPageConfig | null;
isLoading: boolean;
error: string | null;
refreshConfig: () => Promise<void>;
updateConfig: (newConfig: ClientPageConfig) => Promise<boolean>;
}
// Wrapper hook that adapts the generic hook to the expected interface
export function useConfig(): UseConfigResult {
const { data, isLoading, error, refresh, update } = useContextConfig();
return {
configData: data,
isLoading,
error,
refreshConfig: refresh,
updateConfig: update
};
}
export { checkContextPageConfig, setContextPageConfig }; export { checkContextPageConfig, setContextPageConfig };

View File

@ -0,0 +1,317 @@
"use client";
import { useState, useEffect, useCallback } from "react";
import { API_BASE_URL } from "@/config/config";
// Default timeout for fetch requests
const FETCH_TIMEOUT = 5000; // 5 seconds
interface UseContextHookOptions<T> {
// The endpoint path (without API_BASE_URL)
endpoint: string;
// The name of the context for logging
contextName: string;
// Function to extract available items (e.g., selectionList for menu)
extractAvailableItems?: (data: T) => string[];
// Whether to enable periodic refresh
enablePeriodicRefresh?: boolean;
// Refresh interval in milliseconds (default: 5 minutes)
refreshInterval?: number;
// Custom fetch function for getting data
customFetch?: () => Promise<T | null>;
// Custom update function for setting data
customUpdate?: (newData: T) => Promise<boolean>;
// Default value to use when data is not available
defaultValue?: T;
}
interface UseContextHookResult<T> {
data: T | null;
availableItems: string[];
isLoading: boolean;
error: string | null;
refresh: () => Promise<void>;
update: (newData: T) => Promise<boolean>;
}
/**
* Factory function to create a custom hook for any context type
*/
export function createContextHook<T>(options: UseContextHookOptions<T>) {
const {
endpoint,
contextName,
extractAvailableItems = () => [],
enablePeriodicRefresh = false,
refreshInterval = 5 * 60 * 1000, // 5 minutes
customFetch,
customUpdate,
defaultValue,
} = options;
// The API endpoint paths
const apiEndpoint = `/api${endpoint}`;
const directEndpoint = `${API_BASE_URL}${endpoint}`;
/**
* Fetches data from the context API
*/
async function fetchData(): Promise<T | null> {
try {
// Create an AbortController to handle timeouts
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT);
const result = await fetch(directEndpoint, {
method: "GET",
headers: {
"Content-Type": "application/json",
"Cache-Control": "no-cache",
},
signal: controller.signal,
});
clearTimeout(timeoutId);
if (!result.ok) {
throw new Error(`HTTP error! Status: ${result.status}`);
}
const data = await result.json();
if (data.status === 200 && data.data) {
return data.data;
} else {
return null;
}
} catch (error) {
return null;
}
}
/**
* Updates data in the context API
*/
async function updateData(newData: T): Promise<boolean> {
try {
// Create an AbortController to handle timeouts
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT);
const result = await fetch(directEndpoint, {
method: "POST",
headers: {
"Content-Type": "application/json",
"Cache-Control": "no-cache",
},
body: JSON.stringify(newData),
signal: controller.signal,
});
clearTimeout(timeoutId);
if (!result.ok) {
throw new Error(`HTTP error! Status: ${result.status}`);
}
const data = await result.json();
return data.status === 200;
} catch (error) {
return false;
}
}
// Return the custom hook
return function useContextHook(): UseContextHookResult<T> {
const [data, setData] = useState<T | null>(defaultValue || null);
const [availableItems, setAvailableItems] = useState<string[]>([]);
const [isLoading, setIsLoading] = useState<boolean>(true);
const [error, setError] = useState<string | null>(null);
const [selectedClient, setSelectedClient] = useState<any>(null);
const refreshData = useCallback(async () => {
try {
setIsLoading(true);
setError(null);
if (customFetch) {
try {
const customData = await customFetch();
if (customData) {
setData(customData);
if (extractAvailableItems) {
const items = extractAvailableItems(customData);
setAvailableItems(items);
}
setIsLoading(false);
return;
}
} catch (customError) {}
}
try {
const response = await fetch(apiEndpoint);
if (!response.ok) {
throw new Error(
`Failed to fetch ${contextName} data: ${response.status}`
);
}
const result = await response.json();
if (result.status === 200 && result.data) {
setData(result.data);
if (extractAvailableItems) {
const items = extractAvailableItems(result.data);
setAvailableItems(items);
}
setIsLoading(false);
return;
}
} catch (apiError) {
console.warn(
`API endpoint failed, falling back to direct fetch:`,
apiError
);
}
const directData = await fetchData();
if (directData) {
setData(directData);
if (extractAvailableItems) {
const items = extractAvailableItems(directData);
setAvailableItems(items);
}
} else if (defaultValue) {
setData(defaultValue);
if (extractAvailableItems) {
const items = extractAvailableItems(defaultValue);
setAvailableItems(items);
}
} else {
setData(null);
setAvailableItems([]);
}
} catch (err) {
setError(err instanceof Error ? err.message : "Unknown error");
if (defaultValue) {
setData(defaultValue);
if (extractAvailableItems) {
const items = extractAvailableItems(defaultValue);
setAvailableItems(items);
}
} else {
setData(null);
setAvailableItems([]);
}
} finally {
setIsLoading(false);
}
}, []);
// Function to update data
const update = useCallback(
async (newData: T): Promise<boolean> => {
try {
setIsLoading(true);
setError(null);
if (customUpdate) {
try {
const success = await customUpdate(newData);
if (success) {
await refreshData();
return true;
} else {
setError("Failed to update data with custom update function");
return false;
}
} catch (customError) {}
}
try {
const response = await fetch(apiEndpoint, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(newData),
});
if (!response.ok) {
throw new Error(
`Failed to update ${contextName} data: ${response.status}`
);
}
const result = await response.json();
if (result.status === 200) {
await refreshData(); // Refresh data after update
return true;
}
} catch (apiError) {
console.warn(
`API update failed, falling back to direct update:`,
apiError
);
// Fall back to direct update if API update fails
}
// Fallback to direct update
const success = await updateData(newData);
if (success) {
// Refresh data to get the updated state
await refreshData();
return true;
} else {
setError("Failed to update data");
return false;
}
} catch (err) {
console.error(`Error updating ${contextName} data:`, err);
setError(err instanceof Error ? err.message : "Unknown error");
return false;
} finally {
setIsLoading(false);
}
},
[refreshData]
);
// Fetch data on component mount and set up periodic refresh if enabled
useEffect(() => {
refreshData();
// Set up periodic refresh if enabled
if (enablePeriodicRefresh) {
const interval = setInterval(() => {
refreshData();
}, refreshInterval);
return () => clearInterval(interval);
}
}, [refreshData, enablePeriodicRefresh, refreshInterval]);
return {
data,
availableItems,
isLoading,
error,
refresh: refreshData,
update: update,
};
};
}

View File

@ -1,6 +1,10 @@
"use client";
import { ClientMenu } from "@/types/mutual/context/validations"; import { ClientMenu } from "@/types/mutual/context/validations";
import { API_BASE_URL } from "@/config/config"; import { API_BASE_URL } from "@/config/config";
import { createContextHook } from "../hookFactory";
// Original fetch functions for backward compatibility
async function checkContextPageMenu() { async function checkContextPageMenu() {
const result = await fetch(`${API_BASE_URL}/context/page/menu`, { const result = await fetch(`${API_BASE_URL}/context/page/menu`, {
method: "GET", method: "GET",
@ -8,10 +12,8 @@ async function checkContextPageMenu() {
}); });
try { try {
const data = await result.json(); const data = await result.json();
console.log("checkContextPageMenu ", data);
if (data.status == 200) return data.data; if (data.status == 200) return data.data;
} catch (error) { } catch (error) {
console.log(error);
throw new Error("No data is found"); throw new Error("No data is found");
} }
} }
@ -26,9 +28,41 @@ async function setContextPageMenu(setMenu: ClientMenu) {
const data = await result.json(); const data = await result.json();
return data; return data;
} catch (error) { } catch (error) {
console.log(error);
throw new Error("No data is set"); throw new Error("No data is set");
} }
} }
// Create the menu hook using the factory
const useContextMenu = createContextHook<ClientMenu>({
endpoint: "/context/page/menu",
contextName: "menu",
extractAvailableItems: (data) => data.selectionList || [],
enablePeriodicRefresh: false,
});
// Custom hook for menu data with the expected interface
interface UseMenuResult {
menuData: ClientMenu | null;
availableApplications: string[];
isLoading: boolean;
error: string | null;
refreshMenu: () => Promise<void>;
updateMenu: (newMenu: ClientMenu) => Promise<boolean>;
}
// Wrapper hook that adapts the generic hook to the expected interface
export function useMenu(): UseMenuResult {
const { data, availableItems, isLoading, error, refresh, update } =
useContextMenu();
return {
menuData: data,
availableApplications: availableItems,
isLoading,
error,
refreshMenu: refresh,
updateMenu: update,
};
}
export { checkContextPageMenu, setContextPageMenu }; export { checkContextPageMenu, setContextPageMenu };

View File

@ -1,29 +1,152 @@
"use client";
import { ClientOnline } from "@/types/mutual/context/validations"; import { ClientOnline } from "@/types/mutual/context/validations";
import { API_BASE_URL } from "@/config/config"; import { API_BASE_URL } from "@/config/config";
import { createContextHook } from "../hookFactory";
async function checkContextPageOnline() { // Constants
const FETCH_TIMEOUT = 5000; // 5 seconds timeout
// Default online state to use when API calls fail
const DEFAULT_ONLINE_STATE: ClientOnline = {
lastLogin: new Date(),
lastLogout: new Date(),
lastAction: new Date(),
lastPage: "/",
userType: "guest",
lang: "en",
timezone: "UTC",
};
/**
* Fetches the current online state from the context API
* @returns The online state or null if there was an error
*/
async function checkContextPageOnline(): Promise<ClientOnline | null> {
try {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT);
try {
const result = await fetch(`${API_BASE_URL}/context/page/online`, { const result = await fetch(`${API_BASE_URL}/context/page/online`, {
method: "GET", method: "GET",
headers: { "Content-Type": "application/json" }, headers: {
"Content-Type": "application/json",
"Cache-Control": "no-cache",
},
signal: controller.signal,
}); });
try {
// Clear the timeout
clearTimeout(timeoutId);
if (!result.ok) {
throw new Error(`HTTP error! Status: ${result.status}`);
}
const data = await result.json(); const data = await result.json();
if (data.status == 200) return data.data;
} catch (error) {} if (data.status === 200 && data.data) {
return data.data;
} else {
return null; return null;
}
} catch (fetchError) {
// Clear the timeout if it hasn't fired yet
clearTimeout(timeoutId);
if (
fetchError instanceof DOMException &&
fetchError.name === "AbortError"
) {
return DEFAULT_ONLINE_STATE;
}
throw fetchError;
}
} catch (error) {
if (error instanceof DOMException && error.name === "AbortError") {
return DEFAULT_ONLINE_STATE;
} else {
console.error(
"Error fetching online state:",
error instanceof Error ? error.message : "Unknown error"
);
// Return default state instead of null for better user experience
return DEFAULT_ONLINE_STATE;
}
}
} }
async function setContextPageOnline(setOnline: ClientOnline) { /**
* Updates the online state in the context API
* @param setOnline The new online state to set
* @returns The updated online state or null if there was an error
*/
async function setContextPageOnline(
setOnline: ClientOnline
): Promise<ClientOnline | null> {
try {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT);
const result = await fetch(`${API_BASE_URL}/context/page/online`, { const result = await fetch(`${API_BASE_URL}/context/page/online`, {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: {
"Content-Type": "application/json",
"Cache-Control": "no-cache",
},
body: JSON.stringify(setOnline), body: JSON.stringify(setOnline),
signal: controller.signal,
}); });
try {
// Clear the timeout
clearTimeout(timeoutId);
if (!result.ok) {
throw new Error(`HTTP error! Status: ${result.status}`);
}
const data = await result.json(); const data = await result.json();
if (data.status == 200) return data.data;
} catch (error) {} if (data.status === 200 && data.data) {
return data.data;
} else {
return null; return null;
}
} catch (error) {
return null;
}
}
// Create the online hook using the factory
const useContextOnline = createContextHook<ClientOnline>({
endpoint: "/context/page/online",
contextName: "online",
enablePeriodicRefresh: true,
refreshInterval: 5 * 60 * 1000, // 5 minutes
});
// Custom hook for online data with the expected interface
interface UseOnlineResult {
onlineData: ClientOnline | null;
isLoading: boolean;
error: string | null;
refreshOnline: () => Promise<void>;
updateOnline: (newOnline: ClientOnline) => Promise<boolean>;
}
// Wrapper hook that adapts the generic hook to the expected interface
export function useOnline(): UseOnlineResult {
const { data, isLoading, error, refresh, update } = useContextOnline();
return {
onlineData: data,
isLoading,
error,
refreshOnline: refresh,
updateOnline: update,
};
} }
export { checkContextPageOnline, setContextPageOnline }; export { checkContextPageOnline, setContextPageOnline };

View File

@ -0,0 +1,188 @@
'use client';
import React, { FC, ReactNode, createContext, useContext, useEffect, useState, useCallback } from 'react';
import { ClientOnline } from '@/types/mutual/context/validations';
import { checkContextPageOnline, setContextPageOnline } from './context';
import { setOnlineToRedis } from '@/apifetchers/mutual/context/page/online/fetch';
// Default online state to use as fallback
const DEFAULT_ONLINE_STATE: ClientOnline = {
lang: "en",
userType: "occupant",
lastLogin: new Date(),
lastLogout: new Date(),
lastAction: new Date(),
lastPage: "/auth/login",
timezone: "GMT+3"
};
// Create context with default values
interface OnlineContextType {
online: ClientOnline | null;
updateOnline: (newOnline: ClientOnline) => Promise<boolean>;
isLoading: boolean;
error: string | null;
retryFetch: () => Promise<void>;
}
const OnlineContext = createContext<OnlineContextType>({
online: null,
updateOnline: async () => false,
isLoading: false,
error: null,
retryFetch: async () => {}
});
// Custom hook to use the context
export const useOnline = () => useContext(OnlineContext);
// Provider component
interface OnlineProviderProps {
children: ReactNode;
}
export const OnlineProvider: FC<OnlineProviderProps> = ({ children }) => {
const [online, setOnline] = useState<ClientOnline | null>(null);
const [isLoading, setIsLoading] = useState<boolean>(true);
const [error, setError] = useState<string | null>(null);
const [retryCount, setRetryCount] = useState<number>(0);
const [lastRetryTime, setLastRetryTime] = useState<number>(0);
// Maximum number of automatic retries
const MAX_AUTO_RETRIES = 3;
// Minimum time between retries in milliseconds (5 seconds)
const MIN_RETRY_INTERVAL = 5000;
// Function to fetch online state
const fetchOnline = useCallback(async (force = false) => {
// Don't fetch if we already have data and it's not forced
if (online && !force && !error) {
console.log("Using existing online state:", online);
return;
}
// Don't retry too frequently
const now = Date.now();
if (!force && now - lastRetryTime < MIN_RETRY_INTERVAL) {
console.log("Retry attempted too soon, skipping");
return;
}
setIsLoading(true);
setError(null);
setLastRetryTime(now);
try {
console.log("Fetching online state...");
const data = await checkContextPageOnline();
if (data) {
console.log("Successfully fetched online state:", data);
setOnline(data);
setRetryCount(0); // Reset retry count on success
} else {
console.warn("No online state returned, using default");
setOnline(DEFAULT_ONLINE_STATE);
setError("Could not retrieve online state, using default values");
// Auto-retry if under the limit
if (retryCount < MAX_AUTO_RETRIES) {
setRetryCount(prev => prev + 1);
console.log(`Scheduling retry ${retryCount + 1}/${MAX_AUTO_RETRIES}...`);
setTimeout(() => fetchOnline(true), MIN_RETRY_INTERVAL);
}
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
console.error("Error fetching online state:", errorMessage);
setError(`Failed to fetch online state: ${errorMessage}`);
setOnline(DEFAULT_ONLINE_STATE); // Use default as fallback
// Auto-retry if under the limit
if (retryCount < MAX_AUTO_RETRIES) {
setRetryCount(prev => prev + 1);
console.log(`Scheduling retry ${retryCount + 1}/${MAX_AUTO_RETRIES}...`);
setTimeout(() => fetchOnline(true), MIN_RETRY_INTERVAL);
}
} finally {
setIsLoading(false);
}
}, [online, error, retryCount, lastRetryTime]);
// Manual retry function that can be called from components
const retryFetch = useCallback(async () => {
console.log("Manual retry requested");
setRetryCount(0); // Reset retry count for manual retry
await fetchOnline(true);
}, [fetchOnline]);
// Fetch online state on component mount
useEffect(() => {
console.log("OnlineProvider mounted, fetching initial data");
// Always fetch data on mount
fetchOnline();
// Set up periodic refresh (every 5 minutes)
const refreshInterval = setInterval(() => {
console.log("Performing periodic refresh of online state");
fetchOnline(true);
}, 5 * 60 * 1000);
return () => {
console.log("OnlineProvider unmounted, clearing interval");
clearInterval(refreshInterval);
};
}, [fetchOnline]);
// Function to update online state
const updateOnline = async (newOnline: ClientOnline): Promise<boolean> => {
setIsLoading(true);
setError(null);
try {
console.log("Updating online state:", newOnline);
// Update Redis first
console.log('Updating Redis...');
await setOnlineToRedis(newOnline);
// Then update context API
console.log('Updating context API...');
await setContextPageOnline(newOnline);
// Finally update local state to trigger re-renders
console.log('Updating local state...');
setOnline(newOnline);
console.log('Online state updated successfully');
return true;
} catch (error) {
console.error('Error updating online state:', error);
// Still update local state to maintain UI consistency
// even if the backend updates failed
setOnline(newOnline);
return false;
} finally {
setIsLoading(false);
}
};
// Add debug logging for provider state
useEffect(() => {
console.log('OnlineProvider state updated:', {
online: online ? 'present' : 'not present',
isLoading
});
}, [online, isLoading]);
return (
<OnlineContext.Provider value={{ online, updateOnline, isLoading, error, retryFetch }}>
{children}
</OnlineContext.Provider>
);
};
// Export as default for backward compatibility
export default OnlineProvider;

View File

@ -1,6 +1,10 @@
'use client';
import { ClientSelection } from "@/types/mutual/context/validations"; import { ClientSelection } from "@/types/mutual/context/validations";
import { API_BASE_URL } from "@/config/config"; import { API_BASE_URL } from "@/config/config";
import { createContextHook } from '../hookFactory';
// Original fetch functions for backward compatibility
async function checkContextDashSelection() { async function checkContextDashSelection() {
try { try {
const result = await fetch(`${API_BASE_URL}/context/dash/selection`, { const result = await fetch(`${API_BASE_URL}/context/dash/selection`, {
@ -10,7 +14,9 @@ async function checkContextDashSelection() {
const data = await result.json(); const data = await result.json();
if (data.status === 200) return data.data; if (data.status === 200) return data.data;
} catch (error) {} } catch (error) {
console.error("Error checking dash selection:", error);
}
return null; return null;
} }
@ -34,4 +40,33 @@ async function setContextDashUserSelection({
return null; return null;
} }
// Create the selection hook using the factory
const useContextSelection = createContextHook<ClientSelection>({
endpoint: '/context/dash/selection',
contextName: 'selection',
enablePeriodicRefresh: false
});
// Custom hook for selection data with the expected interface
interface UseSelectionResult {
selectionData: ClientSelection | null;
isLoading: boolean;
error: string | null;
refreshSelection: () => Promise<void>;
updateSelection: (newSelection: ClientSelection) => Promise<boolean>;
}
// Wrapper hook that adapts the generic hook to the expected interface
export function useSelection(): UseSelectionResult {
const { data, isLoading, error, refresh, update } = useContextSelection();
return {
selectionData: data,
isLoading,
error,
refreshSelection: refresh,
updateSelection: update
};
}
export { checkContextDashSelection, setContextDashUserSelection }; export { checkContextDashSelection, setContextDashUserSelection };

View File

@ -1,33 +1,225 @@
'use client';
import { ClientUser } from "@/types/mutual/context/validations"; import { ClientUser } from "@/types/mutual/context/validations";
import { API_BASE_URL } from "@/config/config"; import { API_BASE_URL } from "@/config/config";
import { createContextHook } from '../hookFactory';
import { getUserFromServer, setUserFromServer } from '@/server/context/user';
// Constants
const FETCH_TIMEOUT = 5000; // 5 seconds timeout
// Default user state to use when API calls fail
const DEFAULT_USER_STATE: ClientUser = {
uuid: 'default-user-id',
avatar: '',
email: '',
phone_number: '',
user_tag: 'guest',
password_expiry_begins: new Date().toISOString(),
person: {
uuid: 'default-person-id',
firstname: 'Guest',
surname: 'User',
middle_name: '',
sex_code: '',
person_tag: 'guest',
country_code: '',
birth_date: ''
}
};
// Client-side fetch function that uses the server-side implementation
async function checkContextDashUserInfo(): Promise<ClientUser> { async function checkContextDashUserInfo(): Promise<ClientUser> {
try {
console.log('Fetching user data using server-side function');
// First try to use the server-side implementation
try {
const serverData = await getUserFromServer();
console.log('User data from server:', JSON.stringify(serverData, null, 2));
// If we got valid data from the server, return it
if (serverData && serverData.uuid) {
// Check if we have a real user (not the default)
if (serverData.uuid !== 'default-user-id' ||
serverData.email ||
(serverData.person &&
(serverData.person.firstname !== 'Guest' ||
serverData.person.surname !== 'User'))) {
console.log('Valid user data found from server');
return serverData;
} else {
console.log('Default user data returned from server, falling back to client-side');
}
} else {
console.warn('Invalid user data structure from server');
}
} catch (serverError) {
console.warn('Error using server-side user data fetch, falling back to client-side:', serverError);
// Continue to client-side implementation
}
// Fall back to client-side implementation
console.log(`Falling back to client-side fetch: ${API_BASE_URL}/context/dash/user`);
// Create an AbortController to handle timeouts
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT);
try {
const result = await fetch(`${API_BASE_URL}/context/dash/user`, { const result = await fetch(`${API_BASE_URL}/context/dash/user`, {
method: "GET", method: "GET",
headers: { "Content-Type": "application/json" }, headers: {
"Content-Type": "application/json",
"Cache-Control": "no-cache"
},
signal: controller.signal
}); });
try {
// Clear the timeout
clearTimeout(timeoutId);
if (!result.ok) {
throw new Error(`HTTP error! Status: ${result.status}`);
}
const data = await result.json(); const data = await result.json();
console.log('User data API response:', data);
// Handle different response formats
if (data.status === 200 && data.data) {
// Standard API response format
return data.data;
} else if (data.user) {
// Direct Redis object format
console.log('Found user data in Redis format');
return data.user;
} else if (data.uuid) {
// Direct user object format
console.log('Found direct user data format');
return data; return data;
} else {
console.warn('Invalid response format from user API');
return DEFAULT_USER_STATE;
}
} catch (fetchError) {
// Clear the timeout if it hasn't fired yet
clearTimeout(timeoutId);
// Check if this is an abort error (timeout)
if (fetchError instanceof DOMException && fetchError.name === 'AbortError') {
console.warn('Request timed out or was aborted');
return DEFAULT_USER_STATE;
}
// Re-throw other errors to be caught by the outer catch
throw fetchError;
}
} catch (error) { } catch (error) {
console.log(error); // Handle all other errors
throw new Error("No data is found"); console.error('Error fetching user data:', error instanceof Error ? error.message : 'Unknown error');
return DEFAULT_USER_STATE;
} }
} }
async function setContextDashUserInfo({ userSet }: { userSet: ClientUser }) { async function setContextDashUserInfo({ userSet }: { userSet: ClientUser }): Promise<boolean> {
try {
console.log('Setting user data using server-side function');
// First try to use the server-side implementation
try {
const success = await setUserFromServer(userSet);
if (success) {
console.log('Successfully updated user data using server-side function');
return true;
}
} catch (serverError) {
console.warn('Error using server-side user data update, falling back to client-side:', serverError);
// Continue to client-side implementation
}
// Fall back to client-side implementation
console.log(`Falling back to client-side update: ${API_BASE_URL}/context/dash/user`);
// Create an AbortController to handle timeouts
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT);
try {
const result = await fetch(`${API_BASE_URL}/context/dash/user`, { const result = await fetch(`${API_BASE_URL}/context/dash/user`, {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: {
"Content-Type": "application/json",
"Cache-Control": "no-cache"
},
body: JSON.stringify(userSet), body: JSON.stringify(userSet),
signal: controller.signal
}); });
try {
const data = await result.json(); // Clear the timeout
return data; clearTimeout(timeoutId);
} catch (error) {
console.log(error); if (!result.ok) {
throw new Error("No data is set"); throw new Error(`HTTP error! Status: ${result.status}`);
} }
const data = await result.json();
console.log('Update user data API response:', data);
return data.status === 200;
} catch (fetchError) {
// Clear the timeout if it hasn't fired yet
clearTimeout(timeoutId);
// Check if this is an abort error (timeout)
if (fetchError instanceof DOMException && fetchError.name === 'AbortError') {
console.warn('Request timed out or was aborted');
return false;
}
// Re-throw other errors to be caught by the outer catch
throw fetchError;
}
} catch (error) {
console.error('Error setting user data:', error instanceof Error ? error.message : 'Unknown error');
return false;
}
}
// Create the user hook using the factory with custom fetch functions
const useContextUser = createContextHook<ClientUser>({
endpoint: '/context/dash/user',
contextName: 'user',
enablePeriodicRefresh: false,
// Use our improved fetch functions
customFetch: checkContextDashUserInfo,
customUpdate: async (newData: ClientUser) => {
return await setContextDashUserInfo({ userSet: newData });
},
// Provide default value
defaultValue: DEFAULT_USER_STATE
});
// Custom hook for user data with the expected interface
interface UseUserResult {
userData: ClientUser | null;
isLoading: boolean;
error: string | null;
refreshUser: () => Promise<void>;
updateUser: (newUser: ClientUser) => Promise<boolean>;
}
// Wrapper hook that adapts the generic hook to the expected interface
export function useUser(): UseUserResult {
const { data, isLoading, error, refresh, update } = useContextUser();
return {
userData: data,
isLoading,
error,
refreshUser: refresh,
updateUser: update
};
} }
export { checkContextDashUserInfo, setContextDashUserInfo }; export { checkContextDashUserInfo, setContextDashUserInfo };

View File

@ -1,30 +1,35 @@
'use client'; 'use client';
import { useState, useEffect } from "react"; import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent } from "@/components/mutual/shadcnui/dropdown-menu";
import { DropdownMenu, DropdownMenuTrigger } from "@/components/mutual/shadcnui/dropdown-menu";
import { Button } from "@/components/mutual/shadcnui/button"; import { Button } from "@/components/mutual/shadcnui/button";
import { languageSelectionTranslation } from "@/languages/mutual/languageSelection"; import { languageSelectionTranslation } from "@/languages/mutual/languageSelection";
import { langGetKey, langGet } from "@/lib/langGet"; import { langGetKey, langGet } from "@/lib/langGet";
import { LanguageTypes } from "@/validations/mutual/language/validations"; import { LanguageTypes } from "@/validations/mutual/language/validations";
import { checkContextPageOnline, setContextPageOnline } from "@/components/mutual/context/online/context";
import LanguageSelectionItem from "./languageItem"; import LanguageSelectionItem from "./languageItem";
const LanguageSelectionComponent: React.FC<{ lang: LanguageTypes, activePage: string, prefix: string }> = ({ lang, activePage, prefix }) => { interface LanguageSelectionComponentProps {
lang: LanguageTypes;
activePage: string;
prefix: string;
}
const LanguageSelectionComponent: React.FC<LanguageSelectionComponentProps> = ({ lang, activePage, prefix }) => {
const translations = langGet(lang, languageSelectionTranslation); const translations = langGet(lang, languageSelectionTranslation);
const getPageWithLocale = (locale: LanguageTypes): string => { return `${prefix}/${activePage}` } const getPageWithLocale = (locale: LanguageTypes): string => { return `${prefix}/${activePage}` }
const [online, setOnline] = useState<any>({}); const languageButtons = [
useEffect(() => { const online = checkContextPageOnline(); setOnline({ ...online, lang: lang }) }, []); { activeLang: lang, buttonsLang: "en", refUrl: getPageWithLocale("en"), innerText: langGetKey(translations, "english") },
const englishButtonProps = { activeLang: lang, buttonsLang: "en", refUrl: getPageWithLocale("en"), innerText: langGetKey(translations, "english") } { activeLang: lang, buttonsLang: "tr", refUrl: getPageWithLocale("tr"), innerText: langGetKey(translations, "turkish") }
const turkishButtonProps = { activeLang: lang, buttonsLang: "tr", refUrl: getPageWithLocale("tr"), innerText: langGetKey(translations, "turkish") } ]
return ( return (
<div className="flex items-end justify-end"> <div className="flex items-end justify-end">
<div>{JSON.stringify(online)}</div>
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<Button className="w-48 h-12 text-center text-md">{langGetKey(translations, "title")}</Button> <Button className="w-48 h-12 text-center text-md">{langGetKey(translations, "title")}</Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<LanguageSelectionItem {...englishButtonProps} /><LanguageSelectionItem {...turkishButtonProps} /> <DropdownMenuContent>
{languageButtons.map((props, index) => (
<LanguageSelectionItem key={props.buttonsLang} {...props} />
))}
</DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
</div> </div>
); );

View File

@ -1,49 +1,91 @@
'use client'; 'use client';
import { useState, FC } from "react"; import { FC } from "react";
import { DropdownMenuContent, DropdownMenuLabel } from "@/components/mutual/shadcnui/dropdown-menu"; import { DropdownMenuItem } from "@/components/mutual/shadcnui/dropdown-menu";
import { useRouter, usePathname, useSearchParams } from "next/navigation";
// Import the useOnline hook from the context file instead of the provider
import { useOnline } from "@/components/mutual/context/online/context";
import { ClientOnline } from "@/types/mutual/context/validations";
import Link from "next/link"; import Link from "next/link";
import LoadingContent from "@/components/mutual/loader/component";
import { setContextPageOnline, checkContextPageOnline } from "@/components/mutual/context/online/context";
const RenderLinkComponent: FC<{ refUrl: string, innerText: string, setisL: (isLoading: boolean) => void, buttonsLang: string }> = ( const RenderButtonComponent: FC<{ refUrl: string, innerText: string, buttonsLang: string }> = (
{ refUrl, innerText, setisL, buttonsLang }) => { { refUrl, innerText, buttonsLang }) => {
const setOnline = async () => { const router = useRouter();
setisL(true); // Use the new hook properties: onlineData instead of online, refreshOnline and updateOnline
const oldOnline = await checkContextPageOnline(); const { onlineData, isLoading, error, refreshOnline, updateOnline } = useOnline();
await setContextPageOnline({
...oldOnline, const setOnlineObject = async () => {
if (!onlineData || isLoading) return;
try {
console.log("Updating language to:", buttonsLang);
// Use the context hook to update online state
// This will update Redis, context API, and trigger re-renders
const success = await updateOnline({
...onlineData,
lang: buttonsLang, lang: buttonsLang,
lastAction: new Date()
}); });
setisL(false);
if (success) {
console.log("Language updated successfully");
// Refresh the online data to get the latest state
await refreshOnline();
// Navigate to the new URL
router.push(refUrl);
} else {
console.error("Failed to update language");
} }
} catch (error) {
console.error("Error updating language:", error);
}
}
return ( return (
<Link replace href={refUrl} onClick={() => { setOnline() }}> <DropdownMenuItem
<DropdownMenuContent className="flex w-48 h-12 align-center justify-center text-center text-md overflow-y-hidden"> onClick={setOnlineObject}
<DropdownMenuLabel className="flex items-center justify-center">{innerText}</DropdownMenuLabel> className="flex w-full h-12 items-center justify-center text-center text-md cursor-pointer">
</DropdownMenuContent> {innerText}
</Link> </DropdownMenuItem>
) )
} }
const RenderLoadingComponent: FC<{ setisL: (isLoading: boolean) => void }> = ({ setisL }) => {
return (
<DropdownMenuContent className="flex w-48 h-12 align-center justify-center text-center text-md overflow-y-hidden">
<DropdownMenuLabel className="flex items-center justify-center">
<LoadingContent height="h-8" size="w-8 h-8" plane="" />
</DropdownMenuLabel>
</DropdownMenuContent>
)
}
const LanguageSelectionItem: React.FC<{ const LanguageSelectionItem: React.FC<{
activeLang: string, buttonsLang: string, refUrl: string, innerText: string activeLang: string, buttonsLang: string, refUrl: string, innerText: string
}> = ({ activeLang, buttonsLang, refUrl, innerText }) => { }> = ({ activeLang, buttonsLang, refUrl, innerText }) => {
const [isL, setisL] = useState<boolean>(false); // Get the current online state to determine the actual current language
const isC = buttonsLang !== activeLang const { onlineData } = useOnline();
const RenderLinkProp = { refUrl, innerText, setisL, buttonsLang } const pathname = usePathname();
const searchParams = useSearchParams();
const currentUrl = pathname + (searchParams.toString() ? `?${searchParams.toString()}` : "");
// Use the language from the online state if available, otherwise use the prop
const currentLang = onlineData?.lang || activeLang;
// Determine if this button should be active (not the current language)
const isActive = buttonsLang !== currentLang;
const RenderButtonProp = {
refUrl,
innerText,
buttonsLang
}
// Only render the button if it's not the current language
return ( return (
<>{isC && <>{isL ? <RenderLoadingComponent setisL={setisL} /> : <RenderLinkComponent {...RenderLinkProp} />}</>}</> <>
{isActive ? (
<div><RenderButtonComponent {...RenderButtonProp} /></div>
) : (
<DropdownMenuItem
disabled={!isActive}
className="flex w-full h-12 items-center justify-center text-center text-md opacity-50 cursor-not-allowed">
{innerText}
</DropdownMenuItem>
)}
</>
) )
} }
export default LanguageSelectionItem export default LanguageSelectionItem;

View File

@ -0,0 +1,23 @@
'use client';
import React, { ReactNode } from 'react';
import { OnlineProvider } from '@/components/mutual/context/online/provider';
interface ClientProvidersProps {
children: ReactNode;
}
export function ClientProviders({ children }: ClientProvidersProps) {
// Log provider initialization for debugging
React.useEffect(() => {
console.log('ClientProviders initialized');
}, []);
return (
<OnlineProvider>
{children}
</OnlineProvider>
);
}
export default ClientProviders;

View File

@ -0,0 +1,127 @@
'use client';
import HeaderComponent from "@/components/custom/header/component";
import MenuComponent from "@/components/custom/menu/component";
import ContentComponent from "@/components/custom/content/component";
import FooterComponent from "@/components/custom/footer/component";
import React, { FC, useEffect } from 'react';
import { ClientProviders } from "@/components/mutual/providers/client-providers";
import { ClientOnline, ClientMenu, ClientSelection, ClientUser, ClientSettings } from "@/types/mutual/context/validations";
import { ModeTypes } from "@/validations/mutual/dashboard/props";
import type { LanguageTypes } from "@/validations/mutual/language/validations";
// Import all context hooks
import { useMenu } from "@/components/mutual/context/menu/context";
import { useOnline } from "@/components/mutual/context/online/context";
import { useSelection } from "@/components/mutual/context/selection/context";
import { useUser } from "@/components/mutual/context/user/context";
import { useConfig } from "@/components/mutual/context/config/context";
interface ClientLayoutProps {
allProps: {
lang: LanguageTypes;
activePageUrl: string;
mode: ModeTypes;
prefix: string;
};
packs?: {
menu?: ClientMenu;
selection?: ClientSelection;
user?: ClientUser;
settings?: ClientSettings;
online?: ClientOnline;
};
useReloadWindow?: () => void;
}
const ClientLayout: FC<ClientLayoutProps> = ({ allProps }) => {
const { onlineData, isLoading: onlineLoading, error: onlineError } = useOnline();
const { userData, isLoading: userLoading, error: userError } = useUser();
const { availableApplications, isLoading: menuLoading, error: menuError, menuData, refreshMenu } = useMenu();
const { selectionData, isLoading: selectionLoading, error: selectionError } = useSelection();
const { configData, isLoading: configLoading, error: configError } = useConfig();
console.log("RedisData", {
onlineData: {
isLoading: onlineLoading,
error: onlineError,
data: onlineData
},
userData: {
isLoading: userLoading,
error: userError,
data: userData
},
menuData: {
isLoading: menuLoading,
error: menuError,
data: menuData
},
selectionData: {
isLoading: selectionLoading,
error: selectionError,
data: selectionData
},
configData: {
isLoading: configLoading,
error: configError,
data: configData
}
})
useEffect(() => {
console.log('ClientLayout rendered with context providers');
return () => { console.log('ClientLayout unmounted'); };
}, []);
useEffect(() => {
console.log('OnlineData changed with context providers');
return () => { console.log('ClientLayout unmounted'); };
}, [onlineData]);
useEffect(() => {
console.log('UserData changed with context providers');
return () => { console.log('ClientLayout unmounted'); };
}, [userData]);
useEffect(() => {
console.log('MenuData changed with context providers');
return () => { console.log('ClientLayout unmounted'); };
}, [menuData]);
useEffect(() => {
console.log('SelectionData changed with context providers');
return () => { console.log('ClientLayout unmounted'); };
}, [selectionData]);
useEffect(() => {
console.log('ConfigData changed with context providers');
return () => { console.log('ClientLayout unmounted'); };
}, [configData]);
const useReloadWindow = () => { window.location.reload() }
return (
<ClientProviders>
<div className="flex flex-col min-w-screen">
<HeaderComponent {...allProps}
useReloadWindow={useReloadWindow}
onlineData={onlineData} onlineLoading={onlineLoading} onlineError={onlineError}
userData={userData} userLoading={userLoading} userError={userError}
/>
<MenuComponent {...allProps}
useReloadWindow={useReloadWindow}
availableApplications={availableApplications}
onlineData={onlineData} onlineLoading={onlineLoading} onlineError={onlineError}
userData={userData} userLoading={userLoading} userError={userError}
selectionData={selectionData} selectionLoading={selectionLoading} selectionError={selectionError}
menuData={menuData} menuLoading={menuLoading} menuError={menuError} />
<ContentComponent {...allProps}
useReloadWindow={useReloadWindow}
userData={userData} userLoading={userLoading} userError={userError}
selectionData={selectionData} selectionLoading={selectionLoading} selectionError={selectionError}
/>
<FooterComponent {...allProps}
useReloadWindow={useReloadWindow}
availableApplications={availableApplications}
configData={configData} configLoading={configLoading} configError={configError}
/>
</div>
</ClientProviders>
);
};
export { ClientLayout }

View File

@ -1,25 +1,13 @@
'use server'; 'use server';
import { FC } from "react"; import { FC } from "react";
import { DashboardLayoutProps, ModeTypes } from "@/validations/mutual/dashboard/props"; import { DashboardLayoutProps, ModeTypes } from "@/validations/mutual/dashboard/props";
import { ClientLayout } from "./client";
import HeaderComponent from "@/components/custom/header/component";
import MenuComponent from "@/components/custom/menu/component";
import ContentComponent from "@/components/custom/content/component";
import FooterComponent from "@/components/custom/footer/component";
const DashboardLayout: FC<DashboardLayoutProps> = async ({ params, searchParams, lang }) => { const DashboardLayout: FC<DashboardLayoutProps> = async ({ params, searchParams, lang }) => {
const mode = (searchParams?.mode as ModeTypes) || 'shortList'; const mode = (searchParams?.mode as ModeTypes) || 'shortList';
const activePageUrl = `/${params.page?.join('/')}`; const activePageUrl = `/${params.page?.join('/')}`;
const allProps = { lang, activePageUrl, mode, prefix: "/panel" } const allProps = { lang: lang || '', activePageUrl, mode, prefix: "/panel" };
return <ClientLayout allProps={allProps} />
return (
<div className="flex flex-col min-w-screen">
<HeaderComponent {...allProps} />
<MenuComponent {...allProps} />
<ContentComponent {...allProps} />
<FooterComponent {...allProps} />
</div>
);
} }
export { DashboardLayout }; export { DashboardLayout };

View File

@ -1,21 +1,41 @@
import { ContentProps } from "@/validations/mutual/dashboard/props"; import { ContentProps } from "@/validations/mutual/dashboard/props";
import superUserTenantSomething from "./management/account/tenantSomething/page"; import superUserBuildingPartsTenantSomething from "./building/parts/tenantSomething/page";
const pageIndexMulti: Record<string, Record<string, React.FC<ContentProps>>> = { const pageIndexMulti: Record<string, Record<string, React.FC<ContentProps>>> = {
"/main/pages/user/dashboard": { superUserTenantSomething }, "/main/pages/user/dashboard": { superUserBuildingPartsTenantSomething },
"/definitions/identifications/people": { superUserTenantSomething }, "/definitions/identifications/people": {
"/definitions/identifications/users": { superUserTenantSomething }, superUserBuildingPartsTenantSomething,
"/definitions/building/parts": { superUserTenantSomething }, },
"/definitions/building/areas": { superUserTenantSomething }, "/definitions/identifications/users": {
"/building/accounts/managment/accounts": { superUserTenantSomething }, superUserBuildingPartsTenantSomething,
"/building/accounts/managment/budgets": { superUserTenantSomething }, },
"/building/accounts/parts/accounts": { superUserTenantSomething }, "/definitions/building/parts": { superUserBuildingPartsTenantSomething },
"/building/accounts/parts/budgets": { superUserTenantSomething }, "/definitions/building/areas": { superUserBuildingPartsTenantSomething },
"/building/meetings/regular/actions": { superUserTenantSomething }, "/building/accounts/managment/accounts": {
"/building/meetings/regular/accounts": { superUserTenantSomething }, superUserBuildingPartsTenantSomething,
"/building/meetings/ergunt/actions": { superUserTenantSomething }, },
"/building/meetings/ergunt/accounts": { superUserTenantSomething }, "/building/accounts/managment/budgets": {
"/building/meetings/invited/attendance": { superUserTenantSomething }, superUserBuildingPartsTenantSomething,
},
"/building/accounts/parts/accounts": {
superUserBuildingPartsTenantSomething,
},
"/building/accounts/parts/budgets": { superUserBuildingPartsTenantSomething },
"/building/meetings/regular/actions": {
superUserBuildingPartsTenantSomething,
},
"/building/meetings/regular/accounts": {
superUserBuildingPartsTenantSomething,
},
"/building/meetings/ergunt/actions": {
superUserBuildingPartsTenantSomething,
},
"/building/meetings/ergunt/accounts": {
superUserBuildingPartsTenantSomething,
},
"/building/meetings/invited/attendance": {
superUserBuildingPartsTenantSomething,
},
}; };
export default pageIndexMulti; export default pageIndexMulti;

View File

@ -12,7 +12,7 @@ import { getSchemaByLanguage } from "@/schemas/custom/building/parts/tenantSomet
// This is a mock page dont use it // This is a mock page dont use it
const superUserTenantSomething: React.FC<ContentProps> = ({ lang, translations, activePageUrl, mode }) => { const superUserTenantSomething: React.FC<ContentProps> = ({ lang, activePageUrl, mode }) => {
const [selectedRow, setSelectedRow] = useState<any>(null); const [selectedRow, setSelectedRow] = useState<any>(null);
const getSchema = getSchemaByLanguage(lang) const getSchema = getSchemaByLanguage(lang)

View File

@ -0,0 +1,113 @@
'use server';
import { ClientOnline } from "@/types/mutual/context/validations";
import { API_BASE_URL } from "@/config/config";
// Default online state to use when API calls fail
const DEFAULT_ONLINE_STATE: ClientOnline = {
lastLogin: new Date(),
lastLogout: new Date(),
lastAction: new Date(),
lastPage: "/",
userType: "guest",
lang: "en",
timezone: "UTC"
};
/**
* Server-side function to fetch online state
* This can be safely called from server components
*/
export async function getOnlineFromServer(): Promise<ClientOnline> {
try {
console.log(`[Server] Fetching online state from ${API_BASE_URL}/context/page/online`);
// Create an AbortController with a timeout
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 5000); // 5 second timeout
try {
const result = await fetch(`${API_BASE_URL}/context/page/online`, {
method: "GET",
headers: {
"Content-Type": "application/json",
"Cache-Control": "no-cache"
},
signal: controller.signal,
// Add next.js cache options
next: {
revalidate: 60 // Revalidate every 60 seconds
}
});
// Clear the timeout
clearTimeout(timeoutId);
if (!result.ok) {
throw new Error(`HTTP error! Status: ${result.status}`);
}
const data = await result.json();
console.log('[Server] Online state API response:', data);
if (data.status === 200 && data.data) {
return data.data;
} else {
console.warn('[Server] Invalid response format from online state API');
return DEFAULT_ONLINE_STATE;
}
} catch (fetchError) {
// Clear the timeout if it hasn't fired yet
clearTimeout(timeoutId);
// Check if this is an abort error (timeout)
if (fetchError instanceof DOMException && fetchError.name === 'AbortError') {
console.warn('[Server] Request timed out or was aborted');
return DEFAULT_ONLINE_STATE;
}
// Re-throw other errors to be caught by the outer catch
throw fetchError;
}
} catch (error) {
// Handle all other errors
if (error instanceof DOMException && error.name === 'AbortError') {
console.warn('[Server] Request for online state was aborted');
return DEFAULT_ONLINE_STATE;
} else {
console.error('[Server] Error fetching online state:', error instanceof Error ? error.message : 'Unknown error');
return DEFAULT_ONLINE_STATE;
}
}
}
/**
* Server-side function to update online state
* This can be safely called from server components
*/
export async function setOnlineFromServer(newOnline: ClientOnline): Promise<boolean> {
try {
console.log(`[Server] Updating online state at ${API_BASE_URL}/context/page/online`);
const result = await fetch(`${API_BASE_URL}/context/page/online`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"Cache-Control": "no-cache"
},
body: JSON.stringify(newOnline)
});
if (!result.ok) {
throw new Error(`HTTP error! Status: ${result.status}`);
}
const data = await result.json();
console.log('[Server] Update online state API response:', data);
return data.status === 200;
} catch (error) {
console.error('[Server] Error updating online state:', error instanceof Error ? error.message : 'Unknown error');
return false;
}
}

View File

@ -0,0 +1,141 @@
'use server';
import { ClientUser } from "@/types/mutual/context/validations";
import { API_BASE_URL } from "@/config/config";
// Default user state to use when API calls fail
const DEFAULT_USER_STATE: ClientUser = {
uuid: 'default-user-id',
avatar: '',
email: '',
phone_number: '',
user_tag: 'guest',
password_expiry_begins: new Date().toISOString(),
person: {
uuid: 'default-person-id',
firstname: 'Guest',
surname: 'User',
middle_name: '',
sex_code: '',
person_tag: 'guest',
country_code: '',
birth_date: ''
}
};
/**
* Server-side function to fetch user data
* This can be safely called from server components
*/
export async function getUserFromServer(): Promise<ClientUser> {
try {
console.log(`[Server] Fetching user data from ${API_BASE_URL}/context/dash/user`);
// Create an AbortController with a timeout
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 5000); // 5 second timeout
try {
const result = await fetch(`${API_BASE_URL}/context/dash/user`, {
method: "GET",
headers: {
"Content-Type": "application/json",
"Cache-Control": "no-cache"
},
signal: controller.signal,
// Add next.js cache options
next: {
revalidate: 60 // Revalidate every 60 seconds
}
});
// Clear the timeout
clearTimeout(timeoutId);
if (!result.ok) {
throw new Error(`HTTP error! Status: ${result.status}`);
}
const data = await result.json();
console.log('[Server] User data API response:', JSON.stringify(data, null, 2));
// Handle different response formats
if (data.status === 200 && data.data) {
// Standard API response format
console.log('[Server] Found standard API response format with data');
return data.data;
} else if (data.user) {
// Direct Redis object format
console.log('[Server] Found user data in Redis format:', JSON.stringify(data.user, null, 2));
return data.user;
} else if (data.uuid) {
// Direct user object format
console.log('[Server] Found direct user data format');
return data;
} else if (data.email || (data.person && data.person.firstname)) {
// Partial user object format
console.log('[Server] Found partial user data format');
return {
...DEFAULT_USER_STATE,
...data,
uuid: data.uuid || DEFAULT_USER_STATE.uuid
};
} else {
console.warn('[Server] Invalid response format from user API');
return DEFAULT_USER_STATE;
}
} catch (fetchError) {
// Clear the timeout if it hasn't fired yet
clearTimeout(timeoutId);
// Check if this is an abort error (timeout)
if (fetchError instanceof DOMException && fetchError.name === 'AbortError') {
console.warn('[Server] Request timed out or was aborted');
return DEFAULT_USER_STATE;
}
// Re-throw other errors to be caught by the outer catch
throw fetchError;
}
} catch (error) {
// Handle all other errors
if (error instanceof DOMException && error.name === 'AbortError') {
console.warn('[Server] Request for user data was aborted');
return DEFAULT_USER_STATE;
} else {
console.error('[Server] Error fetching user data:', error instanceof Error ? error.message : 'Unknown error');
return DEFAULT_USER_STATE;
}
}
}
/**
* Server-side function to update user data
* This can be safely called from server components
*/
export async function setUserFromServer(newUser: ClientUser): Promise<boolean> {
try {
console.log(`[Server] Updating user data at ${API_BASE_URL}/context/dash/user`);
const result = await fetch(`${API_BASE_URL}/context/dash/user`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"Cache-Control": "no-cache"
},
body: JSON.stringify(newUser)
});
if (!result.ok) {
throw new Error(`HTTP error! Status: ${result.status}`);
}
const data = await result.json();
console.log('[Server] Update user data API response:', data);
return data.status === 200;
} catch (error) {
console.error('[Server] Error updating user data:', error instanceof Error ? error.message : 'Unknown error');
return false;
}
}

View File

@ -19,22 +19,56 @@ interface ContentProps {
lang: LanguageTypes; lang: LanguageTypes;
activePageUrl: string; activePageUrl: string;
mode?: ModeTypes; mode?: ModeTypes;
useReloadWindow?: () => void;
userData: any;
userLoading: boolean;
userError: any;
selectionData: any;
selectionLoading: boolean;
selectionError: any;
} }
interface MenuProps { interface MenuProps {
lang: LanguageTypes; lang: LanguageTypes;
availableApplications: string[]; availableApplications: string[];
activePageUrl: string; activePageUrl: string;
useReloadWindow?: () => void;
onlineData: any;
onlineLoading: boolean;
onlineError: any;
userData: any;
userLoading: boolean;
userError: any;
selectionData: any;
selectionLoading: boolean;
selectionError: any;
menuData: any;
menuLoading: boolean;
menuError: any;
} }
interface FooterProps {} interface FooterProps {
lang: LanguageTypes;
availableApplications: string[];
activePageUrl: string;
useReloadWindow?: () => void;
configData: any;
configLoading: boolean;
configError: any;
}
interface AllProps { interface HeaderProps {
lang: LanguageTypes; lang: LanguageTypes;
activePageUrl: string; activePageUrl: string;
prefix: string; prefix: string;
mode?: ModeTypes; mode?: ModeTypes;
useReloadWindow?: () => void;
onlineData: any;
onlineLoading: boolean;
onlineError: any;
userData: any;
userLoading: boolean;
userError: any;
} }
export type { export type {
@ -43,7 +77,7 @@ export type {
ContentProps, ContentProps,
MenuProps, MenuProps,
FooterProps, FooterProps,
AllProps, HeaderProps,
ModeTypes, ModeTypes,
}; };

View File

@ -1,8 +0,0 @@
'use server';
import Login from "./page";
import { FC } from "react";
import { AuthPageProps } from "@/validations/mutual/auth/props";
const LoginPage: FC<AuthPageProps> = async ({ query, language }) => { return <Login language={language} query={query} /> };
export default LoginPage;

View File

@ -4,9 +4,9 @@ import LoginEmployee from "./LoginEmployee";
import { SelectListProps } from "./types"; import { SelectListProps } from "./types";
const Select: React.FC<SelectListProps> = ({ language, query }) => { const Select: React.FC<SelectListProps> = ({ language, type }) => {
const isEmployeee = query?.type == "employee"; const isEmployeee = type.toLowerCase() == "employee";
const isOccupante = query?.type == "occupant"; const isOccupante = type.toLowerCase() == "occupant";
return ( return (
<> <>
<div className="flex h-full min-h-[inherit] flex-col items-center justify-center gap-4"> <div className="flex h-full min-h-[inherit] flex-col items-center justify-center gap-4">

View File

@ -1,17 +0,0 @@
"use server";
import React, { FC } from "react";
import Select from "./page";
import { redirect } from "next/navigation";
import { AuthPageProps } from "@/validations/mutual/auth/props";
const SelectPage: FC<AuthPageProps> = async ({ query, language }) => {
try {
return <Select language={language} query={query} />;
} catch (error) {
redirect("/auth/login");
return null;
}
}
export default SelectPage;

View File

@ -32,7 +32,7 @@ interface BuildingMap {
interface SelectListProps { interface SelectListProps {
language: LanguageTypes; language: LanguageTypes;
query?: { [key: string]: string | string[] | undefined }; type: string;
} }
interface LoginOccupantProps { interface LoginOccupantProps {

View File

@ -1,15 +0,0 @@
import LoginPage from "./auth/login/serverPage";
import SelectPage from "./auth/select/serverPage";
export default function getPage(pageName: string, props: any) {
switch (pageName) {
case "/login":
return <LoginPage {...props} />;
case "/select":
return <SelectPage {...props} />;
default:
return <LoginPage {...props} />;
}
return <></>
}