client frontend tested
This commit is contained in:
parent
fdf9d2edb8
commit
82d16ed3c9
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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 };
|
||||||
|
|
|
||||||
|
|
@ -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 };
|
||||||
|
|
|
||||||
|
|
@ -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 }
|
||||||
|
}
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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) => {
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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" />
|
||||||
|
|
|
||||||
|
|
@ -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",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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 });
|
||||||
|
}
|
||||||
|
|
@ -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 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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" });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
// )}
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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 };
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -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 };
|
||||||
|
|
|
||||||
|
|
@ -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 };
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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 };
|
||||||
|
|
|
||||||
|
|
@ -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 };
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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 }
|
||||||
|
|
@ -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 };
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
|
||||||
|
|
@ -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">
|
||||||
|
|
|
||||||
|
|
@ -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;
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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 <></>
|
|
||||||
}
|
|
||||||
|
|
||||||
Loading…
Reference in New Issue