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_middlewares.token_provider import TokenProvider
|
||||
|
||||
from events.auth.events import LoginHandler
|
||||
|
||||
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)):
|
||||
"""Check if token is valid"""
|
||||
token_object = TokenProvider.get_dict_from_redis(token=headers.token)
|
||||
return None
|
||||
try:
|
||||
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"
|
||||
|
|
|
|||
|
|
@ -402,13 +402,6 @@ class LoginHandler:
|
|||
def authentication_check_token_valid(cls, domain, access_token: str) -> bool:
|
||||
redis_handler = RedisHandlers()
|
||||
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 False
|
||||
|
||||
|
|
|
|||
|
|
@ -26,6 +26,28 @@ class TokenProvider:
|
|||
return OccupantTokenObject(**redis_object)
|
||||
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
|
||||
def get_dict_from_redis(
|
||||
cls, token: Optional[str] = None, user_uu_id: Optional[str] = None
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
"use server";
|
||||
import { retrieveAccessToken } from "@/apifetchers/mutual/cookies/token";
|
||||
import {
|
||||
DEFAULT_RESPONSE,
|
||||
defaultHeaders,
|
||||
|
|
@ -8,6 +7,7 @@ import {
|
|||
ApiResponse,
|
||||
DEFAULT_TIMEOUT,
|
||||
} from "./basics";
|
||||
import { retrieveAccessToken } from "@/apifetchers/mutual/cookies/token";
|
||||
|
||||
/**
|
||||
* Creates a promise that rejects after a specified timeout
|
||||
|
|
@ -116,7 +116,7 @@ async function fetchDataWithToken<T>(
|
|||
cache: boolean = false,
|
||||
timeout: number = DEFAULT_TIMEOUT
|
||||
): Promise<ApiResponse<T>> {
|
||||
const accessToken = (await retrieveAccessToken()) || "";
|
||||
const accessToken = (await retrieveAccessToken());
|
||||
const headers = {
|
||||
...defaultHeaders,
|
||||
"eys-acs-tkn": accessToken,
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import {
|
|||
defaultLinkList
|
||||
} from "@/types/mutual/context/validations";
|
||||
import { retrievePageList } from "@/apifetchers/mutual/cookies/token";
|
||||
import { deleteAllCookies } from "@/apifetchers/mutual/cookies/cookie-actions";
|
||||
import { setMenuToRedis } from "@/apifetchers/mutual/context/page/menu/fetch";
|
||||
|
||||
const loginEndpoint = `${baseUrlAuth}/authentication/login`;
|
||||
|
|
@ -33,11 +34,8 @@ interface LoginSelectOccupant {
|
|||
}
|
||||
|
||||
async function logoutActiveSession() {
|
||||
const cookieStore = await cookies();
|
||||
const response = await fetchDataWithToken(logoutEndpoint, {}, "GET", false);
|
||||
cookieStore.delete("eys-zzz");
|
||||
cookieStore.delete("eys-yyy");
|
||||
cookieStore.delete("eys-sel");
|
||||
await deleteAllCookies();
|
||||
return response;
|
||||
}
|
||||
|
||||
|
|
@ -82,6 +80,7 @@ async function loginViaAccessKeys(payload: LoginViaAccessKeys) {
|
|||
"POST",
|
||||
false
|
||||
);
|
||||
await deleteAllCookies()
|
||||
if (response.status === 200 || response.status === 202) {
|
||||
|
||||
const loginRespone: any = response?.data;
|
||||
|
|
@ -157,8 +156,7 @@ async function loginSelectEmployee(payload: LoginSelect) {
|
|||
const employeeUUID = payload.uuid;
|
||||
const redisKey = `CLIENT:EMPLOYEE:${employeeUUID}`;
|
||||
const selectResponse: any = await fetchDataWithToken(loginSelectEndpoint, { uuid: employeeUUID }, "POST", false);
|
||||
|
||||
cookieStore.delete("eys-sel");
|
||||
cookieStore.delete({ name: "eys-sel", ...cookieObject });
|
||||
|
||||
if (selectResponse.status === 200 || selectResponse.status === 202) {
|
||||
const usersSelection = await nextCrypto.encrypt(
|
||||
|
|
|
|||
|
|
@ -3,30 +3,201 @@ import { redis } from "@/lib/redis";
|
|||
import { functionRetrieveUserSelection } from "@/apifetchers/utils";
|
||||
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 decrpytUserSelection = await functionRetrieveUserSelection()
|
||||
const redisKey = decrpytUserSelection?.redisKey;
|
||||
if (!redisKey) throw new Error("No redis key found");
|
||||
const result = await redis.get(`${redisKey}`);
|
||||
if (!result) throw new Error("No data found in redis");
|
||||
return JSON.parse(result);
|
||||
try {
|
||||
let decrpytUserSelection;
|
||||
try {
|
||||
decrpytUserSelection = await functionRetrieveUserSelection();
|
||||
} catch (error) {
|
||||
console.error('Error retrieving user selection:', error);
|
||||
return defaultValues;
|
||||
}
|
||||
|
||||
const setCompleteToRedis = async (completeObject: ClientRedisToken) => {
|
||||
const decrpytUserSelection = await functionRetrieveUserSelection()
|
||||
if (!decrpytUserSelection) throw new Error("No user selection found");
|
||||
const redisKey = decrpytUserSelection?.redisKey;
|
||||
if (!redisKey) throw new Error("No redis key found");
|
||||
if (!completeObject) throw new Error("No complete object provided");
|
||||
await redis.set(redisKey, JSON.stringify(completeObject));
|
||||
return true;
|
||||
|
||||
if (!redisKey) {
|
||||
console.error("No redis key found in user selection");
|
||||
return defaultValues;
|
||||
}
|
||||
|
||||
const setNewCompleteToRedis = async (completeObject: ClientRedisToken, redisKey: string) => {
|
||||
if (!redisKey) throw new Error("No redis key found");
|
||||
if (!completeObject) throw new Error("No complete object provided");
|
||||
await redis.set(redisKey, JSON.stringify(completeObject));
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the complete data in Redis with improved error handling and timeouts
|
||||
* @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;
|
||||
if (!redisKey) {
|
||||
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;
|
||||
} catch (redisError) {
|
||||
return false;
|
||||
}
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets new complete data in Redis with a specific key with improved error handling and timeouts
|
||||
* @param completeObject The complete data to set in Redis
|
||||
* @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;
|
||||
} catch (redisError) {
|
||||
return false;
|
||||
}
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export { getCompleteFromRedis, setCompleteToRedis, setNewCompleteToRedis };
|
||||
|
|
|
|||
|
|
@ -4,79 +4,170 @@ import { functionRetrieveUserSelection } from "@/apifetchers/utils";
|
|||
import { ClientOnline } from "@/types/mutual/context/validations";
|
||||
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> => {
|
||||
try {
|
||||
// Get user selection with default fallback
|
||||
const decrpytUserSelection = await functionRetrieveUserSelection();
|
||||
const redisKey = decrpytUserSelection?.redisKey;
|
||||
console.log('Getting online state from Redis...');
|
||||
|
||||
// If we have a default redisKey, return a default online object
|
||||
if (redisKey === "default") {
|
||||
return {
|
||||
lang: "en",
|
||||
userType: "occupant",
|
||||
lastLogin: new Date(),
|
||||
lastLogout: new Date(),
|
||||
lastAction: new Date(),
|
||||
lastPage: "/auth/login",
|
||||
timezone: "GMT+3"
|
||||
};
|
||||
// Get user selection with default fallback
|
||||
let decrpytUserSelection;
|
||||
try {
|
||||
decrpytUserSelection = await functionRetrieveUserSelection();
|
||||
console.log('User selection retrieved successfully');
|
||||
} catch (error) {
|
||||
console.error('Error retrieving user selection:', error);
|
||||
return defaultOnlineObject;
|
||||
}
|
||||
|
||||
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) {
|
||||
return {
|
||||
lang: "en",
|
||||
userType: "occupant",
|
||||
lastLogin: new Date(),
|
||||
lastLogout: new Date(),
|
||||
lastAction: new Date(),
|
||||
lastPage: "/auth/login",
|
||||
timezone: "GMT+3"
|
||||
};
|
||||
console.log('No data found in Redis for key:', redisKey);
|
||||
return defaultOnlineObject;
|
||||
}
|
||||
|
||||
// Parse the result
|
||||
try {
|
||||
const parsedResult = JSON.parse(result);
|
||||
console.log('Successfully parsed Redis result');
|
||||
|
||||
if (!parsedResult.online) {
|
||||
return {
|
||||
lang: "en",
|
||||
userType: "occupant",
|
||||
lastLogin: new Date(),
|
||||
lastLogout: new Date(),
|
||||
lastAction: new Date(),
|
||||
lastPage: "/auth/login",
|
||||
timezone: "GMT+3"
|
||||
};
|
||||
console.warn('No online object in parsed result');
|
||||
return defaultOnlineObject;
|
||||
}
|
||||
|
||||
console.log('Returning online object from Redis');
|
||||
return parsedResult.online;
|
||||
} catch (parseError) {
|
||||
console.error('Error parsing Redis result:', parseError);
|
||||
return defaultOnlineObject;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error getting online from Redis:", error);
|
||||
// Return default online object in case of any error
|
||||
return {
|
||||
lang: "en",
|
||||
userType: "occupant",
|
||||
lastLogin: new Date(),
|
||||
lastLogout: new Date(),
|
||||
lastAction: new Date(),
|
||||
lastPage: "/auth/login",
|
||||
timezone: "GMT+3"
|
||||
};
|
||||
console.error('Unexpected error in getOnlineFromRedis:', error);
|
||||
return defaultOnlineObject;
|
||||
}
|
||||
}
|
||||
|
||||
const setOnlineToRedis = async (onlineObject: ClientOnline) => {
|
||||
const decrpytUserSelection = await functionRetrieveUserSelection()
|
||||
if (!decrpytUserSelection) throw new Error("No user selection found");
|
||||
/**
|
||||
* Sets the online state in Redis
|
||||
* @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;
|
||||
if (!redisKey) throw new Error("No redis key found");
|
||||
if (!onlineObject) throw new Error("No online object provided");
|
||||
const oldData = await getCompleteFromRedis();
|
||||
if (!oldData) throw new Error("No old data found in redis");
|
||||
await setCompleteToRedis({ ...oldData, online: onlineObject });
|
||||
if (!redisKey) {
|
||||
console.error('No redis key found in user selection');
|
||||
return false;
|
||||
}
|
||||
|
||||
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;
|
||||
} catch (redisError) {
|
||||
console.error('Error updating Redis:', redisError);
|
||||
return false;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Unexpected error in setOnlineToRedis:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
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 { fetchDataWithToken } from "@/apifetchers/api-fetcher";
|
||||
|
|
@ -18,7 +17,7 @@ function fetchResponseStatus(response: any) {
|
|||
async function checkAccessTokenIsValid() {
|
||||
try {
|
||||
const response = await fetchDataWithToken(checkToken, {}, "GET", false);
|
||||
return fetchResponseStatus(response) ? true : false;
|
||||
return fetchResponseStatus(response);
|
||||
} catch (error) {
|
||||
console.error("Error checking token validity:", error);
|
||||
return false;
|
||||
|
|
@ -37,15 +36,22 @@ async function retrieveApplicationbyUrl(pageUrl: string) {
|
|||
|
||||
async function retrieveAccessToken() {
|
||||
const cookieStore = await cookies();
|
||||
try {
|
||||
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() {
|
||||
const cookieStore = await cookies();
|
||||
try {
|
||||
const encrpytAccessObject = cookieStore.get("eys-yyy")?.value || "";
|
||||
const decrpytAccessObject = await nextCrypto.decrypt(encrpytAccessObject);
|
||||
return decrpytAccessObject ? JSON.parse(decrpytAccessObject) : null;
|
||||
return await nextCrypto.decrypt(encrpytAccessObject) || "";
|
||||
}
|
||||
catch (error) { console.error("Error retrieving access objects:", error) }
|
||||
return "";
|
||||
}
|
||||
|
||||
export {
|
||||
|
|
|
|||
|
|
@ -5,13 +5,48 @@ import { cookies } from "next/headers";
|
|||
|
||||
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 () => {
|
||||
try {
|
||||
const cookieStore = await cookies();
|
||||
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);
|
||||
if (!decrpytUserSelection) throw new Error("No user selection found");
|
||||
if (!decrpytUserSelection) {
|
||||
return {
|
||||
redisKey: "default",
|
||||
uuid: "",
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
}
|
||||
|
||||
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) => {
|
||||
|
|
|
|||
|
|
@ -1,17 +1,21 @@
|
|||
'use server';
|
||||
import { AuthLayout } from "@/layouts/auth/layout";
|
||||
import { AuthServerProps } from "@/validations/mutual/pages/props";
|
||||
import { checkContextPageOnline } from "@/components/mutual/context/online/context";
|
||||
import getPage from "@/webPages/getPage";
|
||||
import { LanguageTypes } from "@/validations/mutual/language/validations";
|
||||
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 online = await checkContextPageOnline();
|
||||
const lang = online?.lang || "en";
|
||||
const AuthPageSSR = async ({ params, searchParams }: AuthServerProps) => {
|
||||
const awaitedParams = await params;
|
||||
const awaitedSearchParams = await searchParams;
|
||||
const pageUrlFromParams = `/${awaitedParams.page?.join("/")}` || "/login";
|
||||
const FoundPage = getPage(pageUrlFromParams, { language: lang, query: awaitedSearchParams });
|
||||
return <AuthLayout lang={lang} page={FoundPage} activePageUrl={pageUrlFromParams} />
|
||||
const tokenValid = await checkAccessTokenIsValid();
|
||||
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';
|
||||
import { MaindasboardPageProps } from "@/validations/mutual/dashboard/props";
|
||||
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 parameters = await params;
|
||||
const searchParameters = await searchParams;
|
||||
const tokenValid = await isTokenValid()
|
||||
if (!tokenValid) { redirect("/auth/login") }
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center">
|
||||
<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();
|
||||
return NextResponse.json({ status: 200, data: data });
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
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> {
|
||||
try {
|
||||
const headers = req.headers;
|
||||
console.log("headers", Object.entries(headers));
|
||||
const body = await req.json();
|
||||
const dataValidated = {
|
||||
uuid: body.uuid,
|
||||
|
|
|
|||
|
|
@ -1,12 +1,36 @@
|
|||
import { ContentProps } from "@/validations/mutual/dashboard/props";
|
||||
import { resolveWhichPageToRenderMulti } from "@/pages/resolver/resolver";
|
||||
import ContentToRenderNoPage from "@/pages/mutual/noContent/page";
|
||||
import pageIndexMulti from "@/pages/multi/index";
|
||||
|
||||
|
||||
const PageToBeChildrendMulti: React.FC<ContentProps> = async ({ lang, activePageUrl, mode }) => {
|
||||
const ApplicationToRender = await resolveWhichPageToRenderMulti({ activePageUrl })
|
||||
if (!ApplicationToRender) return <ContentToRenderNoPage lang={lang} />
|
||||
return <ApplicationToRender lang={lang} activePageUrl={activePageUrl} mode={mode} />
|
||||
const PageToBeChildrendMulti: React.FC<ContentProps> = ({
|
||||
lang,
|
||||
activePageUrl,
|
||||
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
|
||||
|
|
|
|||
|
|
@ -1,8 +1,12 @@
|
|||
'use server';
|
||||
import { FC, Suspense } from "react";
|
||||
'use client';
|
||||
|
||||
import { FC, Suspense, useMemo, memo } from "react";
|
||||
import { ContentProps, ModeTypes, ModeTypesList } from "@/validations/mutual/dashboard/props";
|
||||
import LoadingContent from "@/components/mutual/loader/component";
|
||||
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 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>
|
||||
// };
|
||||
|
||||
const ContentComponent: FC<ContentProps> = async ({ lang, activePageUrl, mode }) => {
|
||||
const modeFromQuery = ModeTypesList.includes(mode || '') ? mode : 'shortList'
|
||||
const renderProps = { lang, activePageUrl, mode: modeFromQuery as ModeTypes }
|
||||
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)]"
|
||||
return <div className={classNameDiv}><Suspense fallback={loadingContent}><PageToBeChildrendMulti {...renderProps} /></Suspense></div>
|
||||
// Static fallback component to avoid state updates during render
|
||||
const FallbackContent: FC<{ lang: string; activePageUrl: string; mode: string }> = memo(({ lang, activePageUrl, mode }) => (
|
||||
<div className="p-6 bg-white rounded-lg shadow-md">
|
||||
<h2 className="text-2xl font-bold mb-4">Content Loading</h2>
|
||||
<p className="text-gray-600 mb-4">The requested page is currently unavailable or still loading.</p>
|
||||
<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;
|
||||
|
||||
|
||||
// {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 { langGetKey } from "@/lib/langGet";
|
||||
import { AllProps } from "@/validations/mutual/dashboard/props";
|
||||
import { FooterProps } from "@/validations/mutual/dashboard/props";
|
||||
|
||||
const translations = {
|
||||
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 (
|
||||
<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>
|
||||
</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';
|
||||
import { FC, useState, useEffect } from "react";
|
||||
import { AllProps } from "@/validations/mutual/dashboard/props";
|
||||
import LanguageSelectionComponent from "@/components/mutual/languageSelection/component";
|
||||
import { FC } from "react";
|
||||
import { HeaderProps } from "@/validations/mutual/dashboard/props";
|
||||
import { langGetKey } from "@/lib/langGet";
|
||||
import { checkContextPageOnline } from "@/components/mutual/context/online/context";
|
||||
import LanguageSelectionComponent from "@/components/mutual/languageSelection/component";
|
||||
|
||||
const translations = {
|
||||
en: {
|
||||
|
|
@ -16,21 +15,25 @@ const translations = {
|
|||
}
|
||||
}
|
||||
|
||||
const HeaderComponent: FC<AllProps> = ({ lang, activePageUrl, prefix, mode }) => {
|
||||
const [online, setOnline] = useState(false);
|
||||
useEffect(() => {
|
||||
checkContextPageOnline().then((online) => {
|
||||
setOnline(online);
|
||||
});
|
||||
}, []);
|
||||
const HeaderComponent: FC<HeaderProps> = ({
|
||||
lang, activePageUrl, prefix, mode,
|
||||
onlineData, onlineLoading, onlineError,
|
||||
userData, userLoading, userError,
|
||||
}) => {
|
||||
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 flex-row justify-center items-center">
|
||||
<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>{JSON.stringify(online)}</div>
|
||||
<LanguageSelectionComponent lang={lang} activePage={activePageUrl} prefix={prefix} />
|
||||
</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';
|
||||
import { FC, useState, useEffect } from "react";
|
||||
import { AllProps } 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";
|
||||
import { FC, Suspense } from "react";
|
||||
import { MenuProps } from "@/validations/mutual/dashboard/props";
|
||||
|
||||
// Define types for menu structure
|
||||
type ThirdLayerItem = Record<string, any>;
|
||||
type SecondLayerItems = Record<string, ThirdLayerItem>;
|
||||
type FirstLayerItems = Record<string, SecondLayerItems>;
|
||||
type MenuStructure = FirstLayerItems;
|
||||
|
||||
// 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 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>
|
||||
);
|
||||
});
|
||||
};
|
||||
import UserProfileSection from "./userProfileSection";
|
||||
import ClientSelectionSection from "./clientSelectionSection";
|
||||
import MenuItemsSection from "./menuItemsSection";
|
||||
import MenuLoadingState from "./menuLoadingState";
|
||||
import MenuErrorState from "./menuErrorState";
|
||||
import MenuEmptyState from "./menuEmptyState";
|
||||
import LoadingContent from "@/components/mutual/loader/component";
|
||||
|
||||
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 (
|
||||
<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">
|
||||
{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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
|
||||
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 { 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`, {
|
||||
method: "GET",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
try {
|
||||
const data = await result.json();
|
||||
if (data.status === 200) return data.data;
|
||||
return data;
|
||||
} 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;
|
||||
}) {
|
||||
try {
|
||||
const result = await fetch(`${API_BASE_URL}/context/page/config`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(pageConfig),
|
||||
});
|
||||
try {
|
||||
const data = await result.json();
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error("Error setting page config:", error);
|
||||
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 };
|
||||
|
|
|
|||
|
|
@ -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 { API_BASE_URL } from "@/config/config";
|
||||
import { createContextHook } from "../hookFactory";
|
||||
|
||||
// Original fetch functions for backward compatibility
|
||||
async function checkContextPageMenu() {
|
||||
const result = await fetch(`${API_BASE_URL}/context/page/menu`, {
|
||||
method: "GET",
|
||||
|
|
@ -8,10 +12,8 @@ async function checkContextPageMenu() {
|
|||
});
|
||||
try {
|
||||
const data = await result.json();
|
||||
console.log("checkContextPageMenu ", data);
|
||||
if (data.status == 200) return data.data;
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
throw new Error("No data is found");
|
||||
}
|
||||
}
|
||||
|
|
@ -26,9 +28,41 @@ async function setContextPageMenu(setMenu: ClientMenu) {
|
|||
const data = await result.json();
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
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 };
|
||||
|
|
|
|||
|
|
@ -1,29 +1,152 @@
|
|||
"use client";
|
||||
|
||||
import { ClientOnline } from "@/types/mutual/context/validations";
|
||||
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`, {
|
||||
method: "GET",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Cache-Control": "no-cache",
|
||||
},
|
||||
signal: controller.signal,
|
||||
});
|
||||
try {
|
||||
const data = await result.json();
|
||||
if (data.status == 200) return data.data;
|
||||
} catch (error) {}
|
||||
return null;
|
||||
|
||||
// Clear the timeout
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
if (!result.ok) {
|
||||
throw new Error(`HTTP error! Status: ${result.status}`);
|
||||
}
|
||||
|
||||
async function setContextPageOnline(setOnline: ClientOnline) {
|
||||
const data = await result.json();
|
||||
|
||||
if (data.status === 200 && data.data) {
|
||||
return data.data;
|
||||
} else {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Cache-Control": "no-cache",
|
||||
},
|
||||
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();
|
||||
if (data.status == 200) return data.data;
|
||||
} catch (error) {}
|
||||
|
||||
if (data.status === 200 && data.data) {
|
||||
return data.data;
|
||||
} else {
|
||||
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 };
|
||||
|
|
|
|||
|
|
@ -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 { API_BASE_URL } from "@/config/config";
|
||||
import { createContextHook } from '../hookFactory';
|
||||
|
||||
// Original fetch functions for backward compatibility
|
||||
async function checkContextDashSelection() {
|
||||
try {
|
||||
const result = await fetch(`${API_BASE_URL}/context/dash/selection`, {
|
||||
|
|
@ -10,7 +14,9 @@ async function checkContextDashSelection() {
|
|||
|
||||
const data = await result.json();
|
||||
if (data.status === 200) return data.data;
|
||||
} catch (error) {}
|
||||
} catch (error) {
|
||||
console.error("Error checking dash selection:", error);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
@ -34,4 +40,33 @@ async function setContextDashUserSelection({
|
|||
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 };
|
||||
|
|
|
|||
|
|
@ -1,33 +1,225 @@
|
|||
'use client';
|
||||
|
||||
import { ClientUser } from "@/types/mutual/context/validations";
|
||||
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> {
|
||||
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`, {
|
||||
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();
|
||||
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;
|
||||
} 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) {
|
||||
console.log(error);
|
||||
throw new Error("No data is found");
|
||||
// Handle all other errors
|
||||
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`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Cache-Control": "no-cache"
|
||||
},
|
||||
body: JSON.stringify(userSet),
|
||||
signal: controller.signal
|
||||
});
|
||||
try {
|
||||
const data = await result.json();
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
throw new Error("No data is set");
|
||||
|
||||
// Clear the timeout
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
if (!result.ok) {
|
||||
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 };
|
||||
|
|
|
|||
|
|
@ -1,30 +1,35 @@
|
|||
'use client';
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
import { DropdownMenu, DropdownMenuTrigger } from "@/components/mutual/shadcnui/dropdown-menu";
|
||||
import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent } from "@/components/mutual/shadcnui/dropdown-menu";
|
||||
import { Button } from "@/components/mutual/shadcnui/button";
|
||||
import { languageSelectionTranslation } from "@/languages/mutual/languageSelection";
|
||||
import { langGetKey, langGet } from "@/lib/langGet";
|
||||
import { LanguageTypes } from "@/validations/mutual/language/validations";
|
||||
import { checkContextPageOnline, setContextPageOnline } from "@/components/mutual/context/online/context";
|
||||
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 getPageWithLocale = (locale: LanguageTypes): string => { return `${prefix}/${activePage}` }
|
||||
const [online, setOnline] = useState<any>({});
|
||||
useEffect(() => { const online = checkContextPageOnline(); setOnline({ ...online, lang: lang }) }, []);
|
||||
const englishButtonProps = { activeLang: lang, buttonsLang: "en", refUrl: getPageWithLocale("en"), innerText: langGetKey(translations, "english") }
|
||||
const turkishButtonProps = { activeLang: lang, buttonsLang: "tr", refUrl: getPageWithLocale("tr"), innerText: langGetKey(translations, "turkish") }
|
||||
|
||||
const languageButtons = [
|
||||
{ activeLang: lang, buttonsLang: "en", refUrl: getPageWithLocale("en"), innerText: langGetKey(translations, "english") },
|
||||
{ activeLang: lang, buttonsLang: "tr", refUrl: getPageWithLocale("tr"), innerText: langGetKey(translations, "turkish") }
|
||||
]
|
||||
return (
|
||||
<div className="flex items-end justify-end">
|
||||
<div>{JSON.stringify(online)}</div>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button className="w-48 h-12 text-center text-md">{langGetKey(translations, "title")}</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<LanguageSelectionItem {...englishButtonProps} /><LanguageSelectionItem {...turkishButtonProps} />
|
||||
<DropdownMenuContent>
|
||||
{languageButtons.map((props, index) => (
|
||||
<LanguageSelectionItem key={props.buttonsLang} {...props} />
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,49 +1,91 @@
|
|||
'use client';
|
||||
import { useState, FC } from "react";
|
||||
import { DropdownMenuContent, DropdownMenuLabel } from "@/components/mutual/shadcnui/dropdown-menu";
|
||||
import { FC } from "react";
|
||||
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 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 }> = (
|
||||
{ refUrl, innerText, setisL, buttonsLang }) => {
|
||||
const setOnline = async () => {
|
||||
setisL(true);
|
||||
const oldOnline = await checkContextPageOnline();
|
||||
await setContextPageOnline({
|
||||
...oldOnline,
|
||||
const RenderButtonComponent: FC<{ refUrl: string, innerText: string, buttonsLang: string }> = (
|
||||
{ refUrl, innerText, buttonsLang }) => {
|
||||
const router = useRouter();
|
||||
// Use the new hook properties: onlineData instead of online, refreshOnline and updateOnline
|
||||
const { onlineData, isLoading, error, refreshOnline, updateOnline } = useOnline();
|
||||
|
||||
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,
|
||||
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 (
|
||||
<Link replace href={refUrl} onClick={() => { setOnline() }}>
|
||||
<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">{innerText}</DropdownMenuLabel>
|
||||
</DropdownMenuContent>
|
||||
</Link>
|
||||
<DropdownMenuItem
|
||||
onClick={setOnlineObject}
|
||||
className="flex w-full h-12 items-center justify-center text-center text-md cursor-pointer">
|
||||
{innerText}
|
||||
</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<{
|
||||
activeLang: string, buttonsLang: string, refUrl: string, innerText: string
|
||||
}> = ({ activeLang, buttonsLang, refUrl, innerText }) => {
|
||||
const [isL, setisL] = useState<boolean>(false);
|
||||
const isC = buttonsLang !== activeLang
|
||||
const RenderLinkProp = { refUrl, innerText, setisL, buttonsLang }
|
||||
// Get the current online state to determine the actual current language
|
||||
const { onlineData } = useOnline();
|
||||
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 (
|
||||
<>{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';
|
||||
import { FC } from "react";
|
||||
import { DashboardLayoutProps, ModeTypes } from "@/validations/mutual/dashboard/props";
|
||||
|
||||
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 { ClientLayout } from "./client";
|
||||
|
||||
const DashboardLayout: FC<DashboardLayoutProps> = async ({ params, searchParams, lang }) => {
|
||||
const mode = (searchParams?.mode as ModeTypes) || 'shortList';
|
||||
const activePageUrl = `/${params.page?.join('/')}`;
|
||||
const allProps = { lang, activePageUrl, mode, prefix: "/panel" }
|
||||
|
||||
return (
|
||||
<div className="flex flex-col min-w-screen">
|
||||
<HeaderComponent {...allProps} />
|
||||
<MenuComponent {...allProps} />
|
||||
<ContentComponent {...allProps} />
|
||||
<FooterComponent {...allProps} />
|
||||
</div>
|
||||
);
|
||||
const allProps = { lang: lang || '', activePageUrl, mode, prefix: "/panel" };
|
||||
return <ClientLayout allProps={allProps} />
|
||||
}
|
||||
|
||||
export { DashboardLayout };
|
||||
|
|
|
|||
|
|
@ -1,21 +1,41 @@
|
|||
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>>> = {
|
||||
"/main/pages/user/dashboard": { superUserTenantSomething },
|
||||
"/definitions/identifications/people": { superUserTenantSomething },
|
||||
"/definitions/identifications/users": { superUserTenantSomething },
|
||||
"/definitions/building/parts": { superUserTenantSomething },
|
||||
"/definitions/building/areas": { superUserTenantSomething },
|
||||
"/building/accounts/managment/accounts": { superUserTenantSomething },
|
||||
"/building/accounts/managment/budgets": { superUserTenantSomething },
|
||||
"/building/accounts/parts/accounts": { superUserTenantSomething },
|
||||
"/building/accounts/parts/budgets": { superUserTenantSomething },
|
||||
"/building/meetings/regular/actions": { superUserTenantSomething },
|
||||
"/building/meetings/regular/accounts": { superUserTenantSomething },
|
||||
"/building/meetings/ergunt/actions": { superUserTenantSomething },
|
||||
"/building/meetings/ergunt/accounts": { superUserTenantSomething },
|
||||
"/building/meetings/invited/attendance": { superUserTenantSomething },
|
||||
"/main/pages/user/dashboard": { superUserBuildingPartsTenantSomething },
|
||||
"/definitions/identifications/people": {
|
||||
superUserBuildingPartsTenantSomething,
|
||||
},
|
||||
"/definitions/identifications/users": {
|
||||
superUserBuildingPartsTenantSomething,
|
||||
},
|
||||
"/definitions/building/parts": { superUserBuildingPartsTenantSomething },
|
||||
"/definitions/building/areas": { superUserBuildingPartsTenantSomething },
|
||||
"/building/accounts/managment/accounts": {
|
||||
superUserBuildingPartsTenantSomething,
|
||||
},
|
||||
"/building/accounts/managment/budgets": {
|
||||
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;
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import { getSchemaByLanguage } from "@/schemas/custom/building/parts/tenantSomet
|
|||
|
||||
|
||||
// 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 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;
|
||||
activePageUrl: string;
|
||||
mode?: ModeTypes;
|
||||
useReloadWindow?: () => void;
|
||||
userData: any;
|
||||
userLoading: boolean;
|
||||
userError: any;
|
||||
selectionData: any;
|
||||
selectionLoading: boolean;
|
||||
selectionError: any;
|
||||
}
|
||||
|
||||
interface MenuProps {
|
||||
lang: LanguageTypes;
|
||||
availableApplications: 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;
|
||||
activePageUrl: string;
|
||||
prefix: string;
|
||||
mode?: ModeTypes;
|
||||
|
||||
useReloadWindow?: () => void;
|
||||
onlineData: any;
|
||||
onlineLoading: boolean;
|
||||
onlineError: any;
|
||||
userData: any;
|
||||
userLoading: boolean;
|
||||
userError: any;
|
||||
}
|
||||
|
||||
export type {
|
||||
|
|
@ -43,7 +77,7 @@ export type {
|
|||
ContentProps,
|
||||
MenuProps,
|
||||
FooterProps,
|
||||
AllProps,
|
||||
HeaderProps,
|
||||
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";
|
||||
|
||||
const Select: React.FC<SelectListProps> = ({ language, query }) => {
|
||||
const isEmployeee = query?.type == "employee";
|
||||
const isOccupante = query?.type == "occupant";
|
||||
const Select: React.FC<SelectListProps> = ({ language, type }) => {
|
||||
const isEmployeee = type.toLowerCase() == "employee";
|
||||
const isOccupante = type.toLowerCase() == "occupant";
|
||||
return (
|
||||
<>
|
||||
<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 {
|
||||
language: LanguageTypes;
|
||||
query?: { [key: string]: string | string[] | undefined };
|
||||
type: string;
|
||||
}
|
||||
|
||||
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