updated service web customer
This commit is contained in:
parent
1c0bb741a0
commit
3a35752b46
|
|
@ -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 });
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
import {
|
||||
getSelectionFromRedis,
|
||||
setSelectionToRedis,
|
||||
} from "@/fetchers/custom/context/dash/selection/fetch";
|
||||
import { AuthError } from "@/fetchers/types/context";
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const selection = await getSelectionFromRedis();
|
||||
return NextResponse.json({
|
||||
status: 200,
|
||||
data: selection || null,
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof AuthError) {
|
||||
// Return 401 Unauthorized for authentication errors
|
||||
return new NextResponse(
|
||||
JSON.stringify({
|
||||
status: 401,
|
||||
error: error.message,
|
||||
}),
|
||||
{
|
||||
status: 401,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}
|
||||
);
|
||||
}
|
||||
// For other errors, return 500 Internal Server Error
|
||||
return new NextResponse(
|
||||
JSON.stringify({
|
||||
status: 500,
|
||||
error: "Internal server error",
|
||||
}),
|
||||
{
|
||||
status: 500,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const selection = await request.json();
|
||||
await setSelectionToRedis(selection);
|
||||
return NextResponse.json({
|
||||
status: 200,
|
||||
data: selection || null,
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof AuthError) {
|
||||
return new NextResponse(
|
||||
JSON.stringify({
|
||||
status: 401,
|
||||
error: error.message,
|
||||
}),
|
||||
{
|
||||
status: 401,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}
|
||||
);
|
||||
}
|
||||
return new NextResponse(
|
||||
JSON.stringify({
|
||||
status: 500,
|
||||
error: "Internal server error",
|
||||
}),
|
||||
{
|
||||
status: 500,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
import {
|
||||
getSettingsFromRedis,
|
||||
setSettingsToRedis,
|
||||
} from "@/fetchers/custom/context/dash/settings/fetch";
|
||||
import { AuthError } from "@/fetchers/types/context";
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const settings = await getSettingsFromRedis();
|
||||
return NextResponse.json(settings);
|
||||
} catch (error) {
|
||||
if (error instanceof AuthError) {
|
||||
// Return 401 Unauthorized for authentication errors
|
||||
return new NextResponse(JSON.stringify({ error: error.message }), {
|
||||
status: 401,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
// For other errors, return 500 Internal Server Error
|
||||
return new NextResponse(
|
||||
JSON.stringify({ error: "Internal server error" }),
|
||||
{
|
||||
status: 500,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const settings = await request.json();
|
||||
await setSettingsToRedis(settings);
|
||||
return NextResponse.json(settings);
|
||||
} catch (error) {
|
||||
if (error instanceof AuthError) {
|
||||
return new NextResponse(JSON.stringify({ error: error.message }), {
|
||||
status: 401,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
return new NextResponse(
|
||||
JSON.stringify({ error: "Internal server error" }),
|
||||
{
|
||||
status: 500,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
import {
|
||||
getUserFromRedis,
|
||||
setUserToRedis,
|
||||
} from "@/fetchers/custom/context/dash/user/fetch";
|
||||
import { AuthError } from "@/fetchers/types/context";
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const user = await getUserFromRedis();
|
||||
return NextResponse.json(user);
|
||||
} catch (error) {
|
||||
if (error instanceof AuthError) {
|
||||
// Return 401 Unauthorized for authentication errors
|
||||
return new NextResponse(JSON.stringify({ error: error.message }), {
|
||||
status: 401,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
// For other errors, return 500 Internal Server Error
|
||||
return new NextResponse(
|
||||
JSON.stringify({ error: "Internal server error" }),
|
||||
{
|
||||
status: 500,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const user = await request.json();
|
||||
await setUserToRedis(user);
|
||||
return NextResponse.json(user);
|
||||
} catch (error) {
|
||||
if (error instanceof AuthError) {
|
||||
return new NextResponse(JSON.stringify({ error: error.message }), {
|
||||
status: 401,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
return new NextResponse(
|
||||
JSON.stringify({ error: "Internal server error" }),
|
||||
{
|
||||
status: 500,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
import {
|
||||
getConfigFromRedis,
|
||||
setConfigToRedis,
|
||||
} from "@/fetchers/custom/context/page/config/fetch";
|
||||
import { AuthError } from "@/fetchers/types/context";
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const config = await getConfigFromRedis();
|
||||
return NextResponse.json(config);
|
||||
} catch (error) {
|
||||
if (error instanceof AuthError) {
|
||||
// Return 401 Unauthorized for authentication errors
|
||||
return new NextResponse(JSON.stringify({ error: error.message }), {
|
||||
status: 401,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
// For other errors, return 500 Internal Server Error
|
||||
return new NextResponse(
|
||||
JSON.stringify({ error: "Internal server error" }),
|
||||
{
|
||||
status: 500,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const config = await request.json();
|
||||
await setConfigToRedis(config);
|
||||
return NextResponse.json(config);
|
||||
} catch (error) {
|
||||
if (error instanceof AuthError) {
|
||||
return new NextResponse(JSON.stringify({ error: error.message }), {
|
||||
status: 401,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
return new NextResponse(
|
||||
JSON.stringify({ error: "Internal server error" }),
|
||||
{
|
||||
status: 500,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
import {
|
||||
getMenuFromRedis,
|
||||
setMenuToRedis,
|
||||
} from "@/fetchers/custom/context/page/menu/fetch";
|
||||
import { AuthError } from "@/fetchers/types/context";
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const menu = await getMenuFromRedis();
|
||||
return NextResponse.json({
|
||||
status: 200,
|
||||
data: menu,
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof AuthError) {
|
||||
// Return 401 Unauthorized for authentication errors
|
||||
return new NextResponse(
|
||||
JSON.stringify({
|
||||
status: 401,
|
||||
error: error.message,
|
||||
}),
|
||||
{
|
||||
status: 401,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}
|
||||
);
|
||||
}
|
||||
// For other errors, return 500 Internal Server Error
|
||||
return new NextResponse(
|
||||
JSON.stringify({
|
||||
status: 500,
|
||||
error: "Internal server error",
|
||||
}),
|
||||
{
|
||||
status: 500,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const menu = await request.json();
|
||||
await setMenuToRedis(menu);
|
||||
return NextResponse.json({
|
||||
status: 200,
|
||||
data: menu,
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof AuthError) {
|
||||
return new NextResponse(
|
||||
JSON.stringify({
|
||||
status: 401,
|
||||
error: error.message,
|
||||
}),
|
||||
{
|
||||
status: 401,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}
|
||||
);
|
||||
}
|
||||
return new NextResponse(
|
||||
JSON.stringify({
|
||||
status: 500,
|
||||
error: "Internal server error",
|
||||
}),
|
||||
{
|
||||
status: 500,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
import {
|
||||
getOnlineFromRedis,
|
||||
setOnlineToRedis,
|
||||
} from "@/fetchers/custom/context/page/online/fetch";
|
||||
import { AuthError } from "@/fetchers/types/context";
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const online = await getOnlineFromRedis();
|
||||
return NextResponse.json({
|
||||
status: 200,
|
||||
data: online,
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof AuthError) {
|
||||
// Return 401 Unauthorized for authentication errors
|
||||
return new NextResponse(
|
||||
JSON.stringify({
|
||||
status: 401,
|
||||
error: error.message,
|
||||
}),
|
||||
{
|
||||
status: 401,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}
|
||||
);
|
||||
}
|
||||
// For other errors, return 500 Internal Server Error
|
||||
return new NextResponse(
|
||||
JSON.stringify({
|
||||
status: 500,
|
||||
error: "Internal server error",
|
||||
}),
|
||||
{
|
||||
status: 500,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const online = await request.json();
|
||||
await setOnlineToRedis(online);
|
||||
return NextResponse.json({
|
||||
status: 200,
|
||||
data: online,
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof AuthError) {
|
||||
return new NextResponse(
|
||||
JSON.stringify({
|
||||
status: 401,
|
||||
error: error.message,
|
||||
}),
|
||||
{
|
||||
status: 401,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}
|
||||
);
|
||||
}
|
||||
return new NextResponse(
|
||||
JSON.stringify({
|
||||
status: 500,
|
||||
error: "Internal server error",
|
||||
}),
|
||||
{
|
||||
status: 500,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
import { NextResponse } from "next/server";
|
||||
import { loginViaAccessKeys } from "@/apifetchers/custom/login/login";
|
||||
import { loginSchemaEmail } from "@/webPages/auth/login/schemas";
|
||||
|
||||
export async function POST(req: Request): Promise<NextResponse> {
|
||||
try {
|
||||
const headers = req.headers;
|
||||
const body = await req.json();
|
||||
const dataValidated = {
|
||||
accessKey: body.email,
|
||||
password: body.password,
|
||||
rememberMe: body.rememberMe,
|
||||
};
|
||||
const validatedLoginBody = loginSchemaEmail.safeParse(body);
|
||||
if (!validatedLoginBody.success) {
|
||||
return NextResponse.json({
|
||||
status: 422,
|
||||
message: validatedLoginBody.error.message,
|
||||
});
|
||||
}
|
||||
|
||||
const userLogin = await loginViaAccessKeys(dataValidated);
|
||||
if (userLogin.status === 200 || userLogin.status === 202) {
|
||||
return NextResponse.json({
|
||||
status: 200,
|
||||
message: "Login successfully completed",
|
||||
data: userLogin.data,
|
||||
});
|
||||
} else {
|
||||
return NextResponse.json({
|
||||
status: userLogin.status,
|
||||
message: userLogin.message,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
return NextResponse.json({ status: 401, message: "Invalid credentials" });
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
import { API_BASE_URL } from "@/config/config";
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
export async function POST() {
|
||||
const result = await fetch(`${API_BASE_URL}/context/page/menu`, {
|
||||
method: "GET",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
try {
|
||||
const data = await result.json();
|
||||
return NextResponse.json({ status: 200, data: data });
|
||||
} catch (error) {
|
||||
return NextResponse.json({ status: 500, message: "No data is found" });
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
import { NextResponse } from "next/server";
|
||||
|
||||
export async function POST(): Promise<NextResponse> {
|
||||
async function retrievePageToRender(): Promise<string> {
|
||||
return new Promise((resolve) => {
|
||||
resolve("superUserTenantSomething");
|
||||
});
|
||||
}
|
||||
|
||||
const pageToRender = await retrievePageToRender();
|
||||
return NextResponse.json({
|
||||
status: 200,
|
||||
data: pageToRender,
|
||||
});
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
import { z } from "zod";
|
||||
import { loginSelectEmployee } from "@/apifetchers/custom/login/login";
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
const loginSchemaEmployee = z.object({
|
||||
uuid: z.string(),
|
||||
});
|
||||
|
||||
export async function POST(req: Request): Promise<NextResponse> {
|
||||
try {
|
||||
const headers = req.headers;
|
||||
const body = await req.json();
|
||||
const dataValidated = {
|
||||
uuid: body.uuid,
|
||||
};
|
||||
const validatedLoginBody = loginSchemaEmployee.safeParse(body);
|
||||
if (!validatedLoginBody.success) {
|
||||
return NextResponse.json({
|
||||
status: 422,
|
||||
message: validatedLoginBody.error.message,
|
||||
});
|
||||
}
|
||||
|
||||
const userLogin = await loginSelectEmployee(dataValidated);
|
||||
if (userLogin.status === 200 || userLogin.status === 202) {
|
||||
return NextResponse.json({
|
||||
status: 200,
|
||||
message: "Selection successfully completed",
|
||||
data: userLogin.data,
|
||||
});
|
||||
} else {
|
||||
return NextResponse.json({
|
||||
status: userLogin.status,
|
||||
message: userLogin.message,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
return NextResponse.json({ status: 401, message: "Invalid credentials" });
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
import { z } from "zod";
|
||||
import { loginSelectOccupant } from "@/apifetchers/custom/login/login";
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
const loginSchemaOccupant = z.object({
|
||||
uuid: z.string(),
|
||||
});
|
||||
|
||||
export async function POST(req: Request): Promise<NextResponse> {
|
||||
try {
|
||||
const headers = req.headers;
|
||||
const body = await req.json();
|
||||
const dataValidated = {
|
||||
uuid: body.uuid,
|
||||
};
|
||||
const validatedLoginBody = loginSchemaOccupant.safeParse(body);
|
||||
if (!validatedLoginBody.success) {
|
||||
return NextResponse.json({
|
||||
status: 422,
|
||||
message: validatedLoginBody.error.message,
|
||||
});
|
||||
}
|
||||
|
||||
const userLogin = await loginSelectOccupant(dataValidated);
|
||||
if (userLogin.status === 200 || userLogin.status === 202) {
|
||||
return NextResponse.json({
|
||||
status: 200,
|
||||
message: "Selection successfully completed",
|
||||
data: userLogin.data,
|
||||
});
|
||||
} else {
|
||||
return NextResponse.json({
|
||||
status: userLogin.status,
|
||||
message: userLogin.message,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
return NextResponse.json({ status: 401, message: "Invalid credentials" });
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,165 @@
|
|||
import { NextRequest } from "next/server";
|
||||
import {
|
||||
successResponse,
|
||||
errorResponse,
|
||||
paginationResponse,
|
||||
createResponse,
|
||||
updateResponse,
|
||||
deleteResponse,
|
||||
} from "./responseHandlers";
|
||||
import { withErrorHandling, validateRequiredFields } from "./requestHandlers";
|
||||
import {
|
||||
ApiHandler,
|
||||
PaginationParams,
|
||||
ListFunction,
|
||||
CreateFunction,
|
||||
UpdateFunction,
|
||||
DeleteFunction,
|
||||
} from "./types";
|
||||
|
||||
/**
|
||||
* Generic list operation handler
|
||||
* @param request NextRequest object
|
||||
* @param body Request body
|
||||
* @param listFunction The function to call to get the list data
|
||||
*/
|
||||
export async function handleListOperation(
|
||||
request: NextRequest,
|
||||
body: any,
|
||||
listFunction: ListFunction
|
||||
) {
|
||||
const page = body.page || 1;
|
||||
const size = body.size || 10;
|
||||
const orderField = body.orderField || ["uu_id"];
|
||||
const orderType = body.orderType || ["asc"];
|
||||
const query = body.query || {};
|
||||
const response = await listFunction({
|
||||
page,
|
||||
size,
|
||||
orderField,
|
||||
orderType,
|
||||
query,
|
||||
} as PaginationParams);
|
||||
return paginationResponse(response.data, response.pagination);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic create operation handler
|
||||
* @param request NextRequest object
|
||||
* @param body Request body
|
||||
* @param createFunction The function to call to create the item
|
||||
* @param requiredFields Array of required field names
|
||||
*/
|
||||
export async function handleCreateOperation(
|
||||
body: any,
|
||||
createFunction?: CreateFunction,
|
||||
requiredFields: string[] = []
|
||||
) {
|
||||
if (requiredFields.length > 0) {
|
||||
const validation = validateRequiredFields(body, requiredFields);
|
||||
if (!validation.valid) {
|
||||
return errorResponse(validation.error as string, 400);
|
||||
}
|
||||
}
|
||||
|
||||
if (createFunction) {
|
||||
const result = await createFunction(body);
|
||||
return createResponse(result);
|
||||
}
|
||||
|
||||
return createResponse({
|
||||
uuid: Math.floor(Math.random() * 1000),
|
||||
...body,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic update operation handler
|
||||
* @param request NextRequest object
|
||||
* @param body Request body
|
||||
* @param updateFunction The function to call to update the item
|
||||
*/
|
||||
export async function handleUpdateOperation(
|
||||
request: NextRequest,
|
||||
body: any,
|
||||
updateFunction?: UpdateFunction
|
||||
) {
|
||||
const uuid = request.nextUrl.searchParams.get("uuid");
|
||||
if (!uuid) {
|
||||
return errorResponse("UUID not found", 400);
|
||||
}
|
||||
if (updateFunction) {
|
||||
const result = await updateFunction(body, uuid);
|
||||
return updateResponse(result);
|
||||
}
|
||||
return updateResponse(body);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic delete operation handler
|
||||
* @param request NextRequest object
|
||||
* @param deleteFunction The function to call to delete the item
|
||||
*/
|
||||
export async function handleDeleteOperation(
|
||||
request: NextRequest,
|
||||
deleteFunction?: DeleteFunction
|
||||
) {
|
||||
const uuid = request.nextUrl.searchParams.get("uuid");
|
||||
if (!uuid) {
|
||||
return errorResponse("UUID not found", 400);
|
||||
}
|
||||
|
||||
if (deleteFunction) {
|
||||
await deleteFunction(uuid);
|
||||
}
|
||||
return deleteResponse();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a wrapped list handler with error handling
|
||||
* @param listFunction The function to call to get the list data
|
||||
*/
|
||||
export function createListHandler(listFunction: ListFunction) {
|
||||
return withErrorHandling((request: NextRequest, body: any) =>
|
||||
handleListOperation(request, body, listFunction)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a wrapped create handler with error handling
|
||||
* @param createFunction The function to call to create the item
|
||||
* @param requiredFields Array of required field names
|
||||
*/
|
||||
export function createCreateHandler(
|
||||
createFunction?: CreateFunction,
|
||||
requiredFields: string[] = []
|
||||
) {
|
||||
// This handler only takes the body parameter, not the request
|
||||
return withErrorHandling((body: any) => {
|
||||
// Ensure we're only passing the actual body data to the create function
|
||||
if (body && typeof body === 'object' && body.body) {
|
||||
return handleCreateOperation(body.body, createFunction, requiredFields);
|
||||
}
|
||||
return handleCreateOperation(body, createFunction, requiredFields);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a wrapped update handler with error handling
|
||||
* @param updateFunction The function to call to update the item
|
||||
*/
|
||||
export function createUpdateHandler(updateFunction?: UpdateFunction) {
|
||||
return withErrorHandling((request: NextRequest, body: any) =>
|
||||
handleUpdateOperation(request, body, updateFunction)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a wrapped delete handler with error handling
|
||||
* @param deleteFunction The function to call to delete the item
|
||||
*/
|
||||
export function createDeleteHandler(deleteFunction?: DeleteFunction) {
|
||||
return withErrorHandling((request: NextRequest) =>
|
||||
handleDeleteOperation(request, deleteFunction)
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
// Export all utility functions from a single entry point
|
||||
export * from './responseHandlers';
|
||||
export * from './requestHandlers';
|
||||
export * from './apiOperations';
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
import { NextRequest } from "next/server";
|
||||
import { errorResponse } from "./responseHandlers";
|
||||
import { ValidationResult, ApiHandler, ApiHandlerBodyOnly, ApiHandlerWithRequest } from "./types";
|
||||
|
||||
/**
|
||||
* Safely parse JSON request body with error handling
|
||||
* @param request NextRequest object
|
||||
* @returns Parsed request body or null if parsing fails
|
||||
*/
|
||||
export async function parseRequestBody(request: NextRequest) {
|
||||
try {
|
||||
return await request.json();
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper for API route handlers with built-in error handling
|
||||
* @param handler The handler function to wrap
|
||||
*/
|
||||
export function withErrorHandling(
|
||||
handler: ApiHandler
|
||||
) {
|
||||
return async (request: NextRequest) => {
|
||||
try {
|
||||
const body = await parseRequestBody(request);
|
||||
|
||||
if (body === null) {
|
||||
return errorResponse("Invalid request body", 400);
|
||||
}
|
||||
|
||||
// Check handler parameter count to determine if it needs request object
|
||||
// If handler has only 1 parameter, it's likely a create operation that only needs body
|
||||
if (handler.length === 1) {
|
||||
// Cast to the appropriate handler type
|
||||
return await (handler as ApiHandlerBodyOnly)(body);
|
||||
} else {
|
||||
// Otherwise pass both request and body (for list, update, delete operations)
|
||||
return await (handler as ApiHandlerWithRequest)(request, body);
|
||||
}
|
||||
} catch (error: any) {
|
||||
return errorResponse(
|
||||
error.message || "Internal Server Error",
|
||||
error.status || 500
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate that required fields are present in the request body
|
||||
* @param body Request body
|
||||
* @param requiredFields Array of required field names
|
||||
* @returns Object with validation result and error message if validation fails
|
||||
*/
|
||||
export function validateRequiredFields(body: any, requiredFields: string[]): ValidationResult {
|
||||
const missingFields = requiredFields.filter(field =>
|
||||
body[field] === undefined || body[field] === null || body[field] === ''
|
||||
);
|
||||
|
||||
if (missingFields.length > 0) {
|
||||
return {
|
||||
valid: false,
|
||||
error: `Missing required fields: ${missingFields.join(', ')}`
|
||||
};
|
||||
}
|
||||
|
||||
return { valid: true };
|
||||
}
|
||||
|
|
@ -0,0 +1,91 @@
|
|||
import { NextResponse } from "next/server";
|
||||
import { ApiResponse, PaginationResponse, PaginatedApiResponse } from "./types";
|
||||
|
||||
/**
|
||||
* Standard success response handler
|
||||
* @param data The data to return in the response
|
||||
* @param status HTTP status code (default: 200)
|
||||
*/
|
||||
export function successResponse<T>(data: T, status: number = 200) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: true,
|
||||
data,
|
||||
} as ApiResponse<T>,
|
||||
{ status }
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Standard error response handler
|
||||
* @param message Error message
|
||||
* @param status HTTP status code (default: 500)
|
||||
*/
|
||||
export function errorResponse(message: string, status: number = 500) {
|
||||
console.error(`API error: ${message}`);
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: message
|
||||
} as ApiResponse<never>,
|
||||
{ status }
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Standard pagination response format
|
||||
* @param data Array of items to return
|
||||
* @param pagination Pagination information
|
||||
*/
|
||||
export function paginationResponse<T>(data: T[], pagination: PaginationResponse | null) {
|
||||
return NextResponse.json({
|
||||
data: data || [],
|
||||
pagination: pagination || {
|
||||
page: 1,
|
||||
size: 10,
|
||||
totalCount: 0,
|
||||
totalItems: 0,
|
||||
totalPages: 0,
|
||||
pageCount: 0,
|
||||
orderField: ["name"],
|
||||
orderType: ["asc"],
|
||||
query: {},
|
||||
next: false,
|
||||
back: false,
|
||||
},
|
||||
} as PaginatedApiResponse<T>);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create response handler
|
||||
* @param data The created item data
|
||||
*/
|
||||
export function createResponse<T>(data: T) {
|
||||
return successResponse(
|
||||
{
|
||||
...data as any,
|
||||
createdAt: new Date().toISOString(),
|
||||
} as T,
|
||||
201
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update response handler
|
||||
* @param data The updated item data
|
||||
*/
|
||||
export function updateResponse<T>(data: T) {
|
||||
return successResponse(
|
||||
{
|
||||
...data as any,
|
||||
updatedAt: new Date().toISOString(),
|
||||
} as T
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete response handler
|
||||
*/
|
||||
export function deleteResponse() {
|
||||
return successResponse({ message: "Item deleted successfully" }, 204);
|
||||
}
|
||||
|
|
@ -0,0 +1,119 @@
|
|||
/**
|
||||
* Type definitions for API utilities
|
||||
*/
|
||||
|
||||
import { NextRequest } from "next/server";
|
||||
|
||||
/**
|
||||
* Validation result interface
|
||||
*/
|
||||
export interface ValidationResult {
|
||||
valid: boolean;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pagination parameters interface
|
||||
*/
|
||||
export interface PaginationParams {
|
||||
page: number;
|
||||
size: number;
|
||||
orderField: string[];
|
||||
orderType: string[];
|
||||
query: Record<string, any>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pagination response interface
|
||||
*/
|
||||
export interface PaginationResponse {
|
||||
page: number;
|
||||
size: number;
|
||||
totalCount: number;
|
||||
totalItems: number;
|
||||
totalPages: number;
|
||||
pageCount: number;
|
||||
orderField: string[];
|
||||
orderType: string[];
|
||||
query: Record<string, any>;
|
||||
next: boolean;
|
||||
back: boolean;
|
||||
}
|
||||
|
||||
export const defaultPaginationResponse: PaginationResponse = {
|
||||
page: 1,
|
||||
size: 10,
|
||||
totalCount: 0,
|
||||
totalItems: 0,
|
||||
totalPages: 0,
|
||||
pageCount: 0,
|
||||
orderField: ["uu_id"],
|
||||
orderType: ["asc"],
|
||||
query: {},
|
||||
next: false,
|
||||
back: false,
|
||||
};
|
||||
|
||||
/**
|
||||
* API response interface
|
||||
*/
|
||||
export interface ApiResponse<T> {
|
||||
success: boolean;
|
||||
data?: T;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Paginated API response interface
|
||||
*/
|
||||
export interface PaginatedApiResponse<T> {
|
||||
data: T[];
|
||||
pagination: PaginationResponse;
|
||||
}
|
||||
|
||||
export const collectPaginationFromApiResponse = (
|
||||
response: PaginatedApiResponse<any>
|
||||
): PaginationResponse => {
|
||||
return {
|
||||
page: response.pagination?.page || 1,
|
||||
size: response.pagination?.size || 10,
|
||||
totalCount: response.pagination?.totalCount || 0,
|
||||
totalItems: response.pagination?.totalItems || 0,
|
||||
totalPages: response.pagination?.totalPages || 0,
|
||||
pageCount: response.pagination?.pageCount || 0,
|
||||
orderField: response.pagination?.orderField || ["uu_id"],
|
||||
orderType: response.pagination?.orderType || ["asc"],
|
||||
query: response.pagination?.query || {},
|
||||
next: response.pagination?.next || false,
|
||||
back: response.pagination?.back || false,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* API handler function types
|
||||
*/
|
||||
export type ApiHandlerWithRequest = (request: NextRequest, body: any) => Promise<Response>;
|
||||
export type ApiHandlerBodyOnly = (body: any) => Promise<Response>;
|
||||
export type ApiHandler = ApiHandlerWithRequest | ApiHandlerBodyOnly;
|
||||
|
||||
/**
|
||||
* List function type
|
||||
*/
|
||||
export type ListFunction = (
|
||||
params: PaginationParams
|
||||
) => Promise<PaginatedApiResponse<any>>;
|
||||
|
||||
/**
|
||||
* Create function type
|
||||
*/
|
||||
export type CreateFunction = (data: any) => Promise<any>;
|
||||
|
||||
/**
|
||||
* Update function type
|
||||
*/
|
||||
export type UpdateFunction = (id: any, data: any) => Promise<any>;
|
||||
|
||||
/**
|
||||
* Delete function type
|
||||
*/
|
||||
export type DeleteFunction = (id: any) => Promise<any>;
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
import { DashboardLayout } from '@/layouts/dashboard/layout';
|
||||
import { LanguageTypes } from '@/validations/mutual/language/validations';
|
||||
|
||||
export default function Page({ params, searchParams }: { params: { page?: string[] }, searchParams: Record<string, any> }) {
|
||||
const lang: LanguageTypes = 'en';
|
||||
return <DashboardLayout params={params} searchParams={searchParams} lang={lang} />;
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
@import "tailwindcss";
|
||||
@import "tw-animate-css";
|
||||
@import "../styles/custom-scrollbar.css";
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,83 @@
|
|||
import { FC, useState, useRef, useEffect } 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);
|
||||
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
|
||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// If there's only one client or no clients, don't show dropdown functionality
|
||||
const shouldShowAsDropdown = selectionData?.selectionList?.length > 1;
|
||||
|
||||
if (!selectionData || !selectionData.selectionList || selectionData.selectionList.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const handleClientSelection = (client: any) => {
|
||||
setSelectedClient(client);
|
||||
setIsDropdownOpen(false);
|
||||
if (onClientSelect) {
|
||||
onClientSelect(client);
|
||||
}
|
||||
};
|
||||
|
||||
// Close dropdown when clicking outside
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
|
||||
setIsDropdownOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="mb-3 relative" ref={dropdownRef}>
|
||||
{/* Current selection that acts as dropdown trigger */}
|
||||
<div onClick={() => shouldShowAsDropdown && setIsDropdownOpen(!isDropdownOpen)}>
|
||||
{selectedClient && renderOneClientSelection({
|
||||
item: selectedClient,
|
||||
isSelected: true,
|
||||
onClickHandler: () => shouldShowAsDropdown && setIsDropdownOpen(!isDropdownOpen)
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Dropdown menu */}
|
||||
{shouldShowAsDropdown && isDropdownOpen && (
|
||||
<div className="absolute left-0 right-0 top-full mt-1 bg-white rounded-lg shadow-lg z-10 max-h-60 overflow-y-auto">
|
||||
{selectionData.selectionList
|
||||
.filter((client: any) => client.uu_id !== selectedClient?.uu_id)
|
||||
.map((client: any, index: number) => (
|
||||
<div
|
||||
key={client.uu_id || client.id || `client-${index}`}
|
||||
onClick={() => handleClientSelection(client)}
|
||||
>
|
||||
{renderOneClientSelection({
|
||||
item: client,
|
||||
isSelected: false,
|
||||
onClickHandler: handleClientSelection
|
||||
})}
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ClientSelectionSection;
|
||||
|
|
@ -0,0 +1,96 @@
|
|||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
|
||||
interface TableColumn {
|
||||
header: string;
|
||||
accessor: string;
|
||||
cell?: (value: any, row: any) => React.ReactNode;
|
||||
}
|
||||
|
||||
interface DataTableProps {
|
||||
title: string;
|
||||
columns: TableColumn[];
|
||||
data: any[];
|
||||
}
|
||||
|
||||
export default function DataTable({ title, columns, data }: DataTableProps) {
|
||||
const [activeDropdown, setActiveDropdown] = useState<number | null>(null);
|
||||
|
||||
const toggleDropdown = (index: number) => {
|
||||
setActiveDropdown(prev => prev === index ? null : index);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-white border border-gray-100 shadow-md shadow-black/5 p-6 rounded-md">
|
||||
<div className="flex justify-between mb-4 items-start">
|
||||
<div className="font-medium">{title}</div>
|
||||
<div className="dropdown relative">
|
||||
<button type="button" className="dropdown-toggle text-gray-400 hover:text-gray-600">
|
||||
<i className="ri-more-fill"></i>
|
||||
</button>
|
||||
<ul className="dropdown-menu shadow-md shadow-black/5 z-30 hidden py-1.5 rounded-md bg-white border border-gray-100 w-full max-w-[140px]">
|
||||
<li>
|
||||
<a href="#" className="flex items-center text-[13px] py-1.5 px-4 text-gray-600 hover:text-blue-500 hover:bg-gray-50">Profile</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#" className="flex items-center text-[13px] py-1.5 px-4 text-gray-600 hover:text-blue-500 hover:bg-gray-50">Settings</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#" className="flex items-center text-[13px] py-1.5 px-4 text-gray-600 hover:text-blue-500 hover:bg-gray-50">Logout</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full min-w-[540px]">
|
||||
<thead>
|
||||
<tr>
|
||||
{columns.map((column, index) => (
|
||||
<th key={index} className="text-[12px] uppercase tracking-wide font-medium text-gray-400 py-2 px-4 bg-gray-50 text-left">
|
||||
{column.header}
|
||||
</th>
|
||||
))}
|
||||
<th className="text-[12px] uppercase tracking-wide font-medium text-gray-400 py-2 px-4 bg-gray-50 text-left"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{data.map((row, rowIndex) => (
|
||||
<tr key={rowIndex}>
|
||||
{columns.map((column, colIndex) => (
|
||||
<td key={colIndex} className="py-2 px-4 border-b border-b-gray-50">
|
||||
{column.cell ? column.cell(row[column.accessor], row) : row[column.accessor]}
|
||||
</td>
|
||||
))}
|
||||
<td className="py-2 px-4 border-b border-b-gray-50">
|
||||
<div className="dropdown relative">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => toggleDropdown(rowIndex)}
|
||||
className="dropdown-toggle text-gray-400 hover:text-gray-600 text-sm w-6 h-6 rounded flex items-center justify-center bg-gray-50"
|
||||
>
|
||||
<i className="ri-more-2-fill"></i>
|
||||
</button>
|
||||
{activeDropdown === rowIndex && (
|
||||
<ul className="dropdown-menu shadow-md shadow-black/5 z-30 py-1.5 rounded-md bg-white border border-gray-100 w-full max-w-[140px] absolute right-0">
|
||||
<li>
|
||||
<a href="#" className="flex items-center text-[13px] py-1.5 px-4 text-gray-600 hover:text-blue-500 hover:bg-gray-50">Profile</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#" className="flex items-center text-[13px] py-1.5 px-4 text-gray-600 hover:text-blue-500 hover:bg-gray-50">Settings</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#" className="flex items-center text-[13px] py-1.5 px-4 text-gray-600 hover:text-blue-500 hover:bg-gray-50">Logout</a>
|
||||
</li>
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
|
||||
interface EarningItem {
|
||||
service: string;
|
||||
imageUrl: string;
|
||||
amount: string;
|
||||
isPositive: boolean;
|
||||
status: string;
|
||||
}
|
||||
|
||||
interface EarningsTableProps {
|
||||
items: EarningItem[];
|
||||
}
|
||||
|
||||
export default function EarningsTable({ items }: EarningsTableProps) {
|
||||
const [activeDropdown, setActiveDropdown] = useState<boolean>(false);
|
||||
|
||||
// Toggle dropdown
|
||||
const toggleDropdown = () => {
|
||||
setActiveDropdown(!activeDropdown);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-white border border-gray-100 shadow-md shadow-black/5 p-6 rounded-md">
|
||||
<div className="flex justify-between mb-4 items-start">
|
||||
<div className="font-medium">Earnings</div>
|
||||
<div className="dropdown relative">
|
||||
<button
|
||||
type="button"
|
||||
onClick={toggleDropdown}
|
||||
className="dropdown-toggle text-gray-400 hover:text-gray-600"
|
||||
>
|
||||
<i className="ri-more-fill"></i>
|
||||
</button>
|
||||
{activeDropdown && (
|
||||
<ul className="dropdown-menu shadow-md shadow-black/5 z-30 py-1.5 rounded-md bg-white border border-gray-100 w-full max-w-[140px] absolute right-0">
|
||||
<li>
|
||||
<a href="#" className="flex items-center text-[13px] py-1.5 px-4 text-gray-600 hover:text-blue-500 hover:bg-gray-50">Profile</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#" className="flex items-center text-[13px] py-1.5 px-4 text-gray-600 hover:text-blue-500 hover:bg-gray-50">Settings</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#" className="flex items-center text-[13px] py-1.5 px-4 text-gray-600 hover:text-blue-500 hover:bg-gray-50">Logout</a>
|
||||
</li>
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full min-w-[460px]">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="text-[12px] uppercase tracking-wide font-medium text-gray-400 py-2 px-4 bg-gray-50 text-left rounded-tl-md rounded-bl-md">Service</th>
|
||||
<th className="text-[12px] uppercase tracking-wide font-medium text-gray-400 py-2 px-4 bg-gray-50 text-left">Earning</th>
|
||||
<th className="text-[12px] uppercase tracking-wide font-medium text-gray-400 py-2 px-4 bg-gray-50 text-left rounded-tr-md rounded-br-md">Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{items.map((item, index) => (
|
||||
<tr key={index}>
|
||||
<td className="py-2 px-4 border-b border-b-gray-50">
|
||||
<div className="flex items-center">
|
||||
<img src={item.imageUrl} alt="" className="w-8 h-8 rounded object-cover block" />
|
||||
<a href="#" className="text-gray-600 text-sm font-medium hover:text-blue-500 ml-2 truncate">{item.service}</a>
|
||||
</div>
|
||||
</td>
|
||||
<td className="py-2 px-4 border-b border-b-gray-50">
|
||||
<span className={`text-[13px] font-medium ${item.isPositive ? 'text-emerald-500' : 'text-rose-500'}`}>
|
||||
{item.isPositive ? '+' : '-'}{item.amount}
|
||||
</span>
|
||||
</td>
|
||||
<td className="py-2 px-4 border-b border-b-gray-50">
|
||||
<span className={`inline-block p-1 rounded ${item.isPositive ? 'bg-emerald-500/10 text-emerald-500' : 'bg-rose-500/10 text-rose-500'} font-medium text-[12px] leading-none`}>
|
||||
{item.status}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,102 @@
|
|||
'use client';
|
||||
|
||||
import { useRef, useEffect } from 'react';
|
||||
|
||||
interface OrderStatsProps {
|
||||
activeOrders: number;
|
||||
activeAmount: number;
|
||||
completedOrders: number;
|
||||
completedAmount: number;
|
||||
canceledOrders: number;
|
||||
canceledAmount: number;
|
||||
}
|
||||
|
||||
export default function OrderStats({
|
||||
activeOrders,
|
||||
activeAmount,
|
||||
completedOrders,
|
||||
completedAmount,
|
||||
canceledOrders,
|
||||
canceledAmount
|
||||
}: OrderStatsProps) {
|
||||
const [activeDropdown, setActiveDropdown] = useState<boolean>(false);
|
||||
const chartRef = useRef<HTMLCanvasElement>(null);
|
||||
|
||||
// Toggle dropdown
|
||||
const toggleDropdown = () => {
|
||||
setActiveDropdown(!activeDropdown);
|
||||
};
|
||||
|
||||
// Chart initialization would go here
|
||||
useEffect(() => {
|
||||
if (chartRef.current) {
|
||||
const ctx = chartRef.current.getContext('2d');
|
||||
if (ctx) {
|
||||
// This is where you would initialize a chart library like Chart.js
|
||||
// For now, we'll just draw some placeholder text
|
||||
ctx.font = '14px Arial';
|
||||
ctx.fillStyle = '#333';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText('Order Statistics Chart', chartRef.current.width / 2, chartRef.current.height / 2);
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="bg-white border border-gray-100 shadow-md shadow-black/5 p-6 rounded-md lg:col-span-2">
|
||||
<div className="flex justify-between mb-4 items-start">
|
||||
<div className="font-medium">Order Statistics</div>
|
||||
<div className="dropdown relative">
|
||||
<button
|
||||
type="button"
|
||||
onClick={toggleDropdown}
|
||||
className="dropdown-toggle text-gray-400 hover:text-gray-600"
|
||||
>
|
||||
<i className="ri-more-fill"></i>
|
||||
</button>
|
||||
{activeDropdown && (
|
||||
<ul className="dropdown-menu shadow-md shadow-black/5 z-30 py-1.5 rounded-md bg-white border border-gray-100 w-full max-w-[140px] absolute right-0">
|
||||
<li>
|
||||
<a href="#" className="flex items-center text-[13px] py-1.5 px-4 text-gray-600 hover:text-blue-500 hover:bg-gray-50">Profile</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#" className="flex items-center text-[13px] py-1.5 px-4 text-gray-600 hover:text-blue-500 hover:bg-gray-50">Settings</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#" className="flex items-center text-[13px] py-1.5 px-4 text-gray-600 hover:text-blue-500 hover:bg-gray-50">Logout</a>
|
||||
</li>
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 mb-4">
|
||||
<div className="rounded-md border border-dashed border-gray-200 p-4">
|
||||
<div className="flex items-center mb-0.5">
|
||||
<div className="text-xl font-semibold">{activeOrders}</div>
|
||||
<span className="p-1 rounded text-[12px] font-semibold bg-blue-500/10 text-blue-500 leading-none ml-1">${activeAmount}</span>
|
||||
</div>
|
||||
<span className="text-gray-400 text-sm">Active</span>
|
||||
</div>
|
||||
<div className="rounded-md border border-dashed border-gray-200 p-4">
|
||||
<div className="flex items-center mb-0.5">
|
||||
<div className="text-xl font-semibold">{completedOrders}</div>
|
||||
<span className="p-1 rounded text-[12px] font-semibold bg-emerald-500/10 text-emerald-500 leading-none ml-1">+${completedAmount}</span>
|
||||
</div>
|
||||
<span className="text-gray-400 text-sm">Completed</span>
|
||||
</div>
|
||||
<div className="rounded-md border border-dashed border-gray-200 p-4">
|
||||
<div className="flex items-center mb-0.5">
|
||||
<div className="text-xl font-semibold">{canceledOrders}</div>
|
||||
<span className="p-1 rounded text-[12px] font-semibold bg-rose-500/10 text-rose-500 leading-none ml-1">-${canceledAmount}</span>
|
||||
</div>
|
||||
<span className="text-gray-400 text-sm">Canceled</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<canvas ref={chartRef} id="order-chart" width="100%" height="200"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
import { useState } from 'react';
|
||||
|
|
@ -0,0 +1,153 @@
|
|||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import ClientSelectionSection from './ClientSelectionSection';
|
||||
import UserProfileSection from './UserProfileSection';
|
||||
|
||||
interface SidebarProps {
|
||||
isSidebarOpen: boolean;
|
||||
toggleSidebar: () => void;
|
||||
}
|
||||
|
||||
export default function Sidebar({ isSidebarOpen, toggleSidebar }: SidebarProps) {
|
||||
const [openSidebarSubmenus, setOpenSidebarSubmenus] = useState<Record<string, boolean>>({});
|
||||
|
||||
// Sample user data for demonstration
|
||||
const sampleUserData = {
|
||||
email: 'user@example.com',
|
||||
person: {
|
||||
firstname: 'John',
|
||||
surname: 'Doe'
|
||||
}
|
||||
};
|
||||
|
||||
const sampleOnlineData = {
|
||||
userType: 'admin'
|
||||
};
|
||||
|
||||
// Sample client selection data
|
||||
const sampleClientData = {
|
||||
selectionList: [
|
||||
{
|
||||
uu_id: '1',
|
||||
name: 'Acme Corp',
|
||||
description: 'Technology solutions'
|
||||
},
|
||||
{
|
||||
uu_id: '2',
|
||||
name: 'Globex',
|
||||
description: 'Manufacturing'
|
||||
},
|
||||
{
|
||||
uu_id: '3',
|
||||
name: 'Initech',
|
||||
description: 'Software development'
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
// Toggle submenu
|
||||
const toggleSubmenu = (itemName: string) => {
|
||||
setOpenSidebarSubmenus(prev => ({ ...prev, [itemName]: !prev[itemName] }));
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={`fixed left-0 top-0 w-72 h-full bg-[#f8f4f3] z-50 sidebar-menu transition-transform ${isSidebarOpen ? '' : '-translate-x-full'} md:translate-x-0 flex flex-col`}>
|
||||
{/* Fixed header section */}
|
||||
<div className="p-4 border-b border-b-gray-800 flex-shrink-0">
|
||||
<UserProfileSection userData={sampleUserData} onlineData={sampleOnlineData} />
|
||||
</div>
|
||||
|
||||
{/* Scrollable content section */}
|
||||
<div className="flex-grow overflow-y-auto custom-scrollbar p-4">
|
||||
{/* Client selection menu */}
|
||||
<ClientSelectionSection
|
||||
selectionData={sampleClientData}
|
||||
initialSelectedClient={sampleClientData.selectionList[0]}
|
||||
onClientSelect={(client) => console.log('Selected client:', client)}
|
||||
/>
|
||||
|
||||
{/* Add more scrollable content here if needed */}
|
||||
<ul className="mt-4">
|
||||
<span className="text-gray-400 font-bold">ADMIN</span>
|
||||
<li className="mb-1 group active">
|
||||
<a href="#" className="flex font-semibold items-center py-2 px-4 text-gray-900 hover:bg-gray-950 hover:text-gray-100 rounded-md group-[.active]:bg-gray-800 group-[.active]:text-white group-[.selected]:bg-gray-950 group-[.selected]:text-gray-100">
|
||||
<i className="ri-home-2-line mr-3 text-lg"></i>
|
||||
<span className="text-sm">Dashboard</span>
|
||||
</a>
|
||||
</li>
|
||||
<li className={`mb-1 group ${openSidebarSubmenus['users'] ? 'selected' : ''}`}>
|
||||
<a href="#" onClick={(e) => { e.preventDefault(); toggleSubmenu('users'); }} className="flex font-semibold items-center py-2 px-4 text-gray-900 hover:bg-gray-950 hover:text-gray-100 rounded-md group-[.active]:bg-gray-800 group-[.active]:text-white group-[.selected]:bg-gray-950 group-[.selected]:text-gray-100 sidebar-dropdown-toggle">
|
||||
<i className='bx bx-user mr-3 text-lg'></i>
|
||||
<span className="text-sm">Users</span>
|
||||
<i className={`ri-arrow-right-s-line ml-auto ${openSidebarSubmenus['users'] ? 'rotate-90' : ''}`}></i>
|
||||
</a>
|
||||
<ul className={`pl-7 mt-2 ${openSidebarSubmenus['users'] ? 'block' : 'hidden'}`}>
|
||||
<li className="mb-4">
|
||||
<a href="#" className="text-gray-900 text-sm flex items-center hover:text-[#f84525] before:content-[''] before:w-1 before:h-1 before:rounded-full before:bg-gray-300 before:mr-3">All</a>
|
||||
</li>
|
||||
<li className="mb-4">
|
||||
<a href="#" className="text-gray-900 text-sm flex items-center hover:text-[#f84525] before:content-[''] before:w-1 before:h-1 before:rounded-full before:bg-gray-300 before:mr-3">Roles</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li className="mb-1 group">
|
||||
<a href="#" className="flex font-semibold items-center py-2 px-4 text-gray-900 hover:bg-gray-950 hover:text-gray-100 rounded-md group-[.active]:bg-gray-800 group-[.active]:text-white group-[.selected]:bg-gray-950 group-[.selected]:text-gray-100">
|
||||
<i className='bx bx-list-ul mr-3 text-lg'></i>
|
||||
<span className="text-sm">Activities</span>
|
||||
</a>
|
||||
</li>
|
||||
<span className="text-gray-400 font-bold">BLOG</span>
|
||||
<li className={`mb-1 group ${openSidebarSubmenus['post'] ? 'selected' : ''}`}>
|
||||
<a href="#" onClick={(e) => { e.preventDefault(); toggleSubmenu('post'); }} className="flex font-semibold items-center py-2 px-4 text-gray-900 hover:bg-gray-950 hover:text-gray-100 rounded-md group-[.active]:bg-gray-800 group-[.active]:text-white group-[.selected]:bg-gray-950 group-[.selected]:text-gray-100 sidebar-dropdown-toggle">
|
||||
<i className='bx bxl-blogger mr-3 text-lg'></i>
|
||||
<span className="text-sm">Post</span>
|
||||
<i className={`ri-arrow-right-s-line ml-auto ${openSidebarSubmenus['post'] ? 'rotate-90' : ''}`}></i>
|
||||
</a>
|
||||
<ul className={`pl-7 mt-2 ${openSidebarSubmenus['post'] ? 'block' : 'hidden'}`}>
|
||||
<li className="mb-4">
|
||||
<a href="#" className="text-gray-900 text-sm flex items-center hover:text-[#f84525] before:content-[''] before:w-1 before:h-1 before:rounded-full before:bg-gray-300 before:mr-3">All</a>
|
||||
</li>
|
||||
<li className="mb-4">
|
||||
<a href="#" className="text-gray-900 text-sm flex items-center hover:text-[#f84525] before:content-[''] before:w-1 before:h-1 before:rounded-full before:bg-gray-300 before:mr-3">Categories</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
<li className="mb-1 group">
|
||||
<a href="#" className="flex font-semibold items-center py-2 px-4 text-gray-900 hover:bg-gray-950 hover:text-gray-100 rounded-md group-[.active]:bg-gray-800 group-[.active]:text-white group-[.selected]:bg-gray-950 group-[.selected]:text-gray-100">
|
||||
<i className='bx bx-archive mr-3 text-lg'></i>
|
||||
<span className="text-sm">Archive</span>
|
||||
</a>
|
||||
</li>
|
||||
<span className="text-gray-400 font-bold">PERSONAL</span>
|
||||
<li className="mb-1 group">
|
||||
<a href="#" className="flex font-semibold items-center py-2 px-4 text-gray-900 hover:bg-gray-950 hover:text-gray-100 rounded-md group-[.active]:bg-gray-800 group-[.active]:text-white group-[.selected]:bg-gray-950 group-[.selected]:text-gray-100">
|
||||
<i className='bx bx-bell mr-3 text-lg'></i>
|
||||
<span className="text-sm">Notifications</span>
|
||||
<span className="md:block px-2 py-0.5 ml-auto text-xs font-medium tracking-wide text-red-600 bg-red-200 rounded-full">5</span>
|
||||
</a>
|
||||
</li>
|
||||
<li className="mb-1 group">
|
||||
<a href="#" className="flex font-semibold items-center py-2 px-4 text-gray-900 hover:bg-gray-950 hover:text-gray-100 rounded-md group-[.active]:bg-gray-800 group-[.active]:text-white group-[.selected]:bg-gray-950 group-[.selected]:text-gray-100">
|
||||
<i className='bx bx-envelope mr-3 text-lg'></i>
|
||||
<span className="text-sm">Messages</span>
|
||||
<span className="md:block px-2 py-0.5 ml-auto text-xs font-medium tracking-wide text-green-600 bg-green-200 rounded-full">2 New</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
{/* Overlay for mobile */}
|
||||
{isSidebarOpen && (
|
||||
<div className="fixed top-0 left-0 w-full h-full bg-black/50 z-40 md:hidden sidebar-overlay" onClick={toggleSidebar}></div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
'use client';
|
||||
|
||||
interface StatCardProps {
|
||||
title: string;
|
||||
count: string;
|
||||
icon: string;
|
||||
iconColor: string;
|
||||
percentage: string;
|
||||
isPositive: boolean;
|
||||
}
|
||||
|
||||
export default function StatCard({ title, count, icon, iconColor, percentage, isPositive }: StatCardProps) {
|
||||
return (
|
||||
<div className="bg-white p-6 rounded-md border border-gray-100 shadow-md shadow-black/5">
|
||||
<div className="flex justify-between mb-4">
|
||||
<div>
|
||||
<div className="flex items-center mb-1">
|
||||
<div className="text-2xl font-semibold">{count}</div>
|
||||
<div className={`text-xs font-medium ${isPositive ? 'text-green-500' : 'text-red-500'} ml-2`}>
|
||||
{isPositive ? '+' : '-'}{percentage}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-sm font-medium text-gray-400">{title}</div>
|
||||
</div>
|
||||
<div className={`${iconColor} bg-opacity-10 w-12 h-12 rounded-full flex items-center justify-center`}>
|
||||
<i className={`${icon} text-2xl`}></i>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<div className={`text-xs font-medium ${isPositive ? 'text-green-500' : 'text-red-500'}`}>
|
||||
{percentage} {isPositive ? 'increase' : 'decrease'}
|
||||
</div>
|
||||
<div className="text-xs text-gray-400 ml-2">from last week</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
import { FC } from 'react';
|
||||
|
||||
interface UserProfileSectionProps {
|
||||
userData: {
|
||||
avatar?: string;
|
||||
email?: string;
|
||||
person?: {
|
||||
firstname: string;
|
||||
surname: string;
|
||||
};
|
||||
} | null;
|
||||
onlineData?: {
|
||||
userType?: string;
|
||||
} | 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;
|
||||
|
|
@ -0,0 +1,167 @@
|
|||
'use client';
|
||||
|
||||
import { FC } from 'react';
|
||||
import { ContentProps } from '@/validations/mutual/dashboard/props';
|
||||
import StatCard from '@/components/custom/StatCard';
|
||||
import DataTable from '@/components/custom/DataTable';
|
||||
import OrderStats from '@/components/custom/OrderStats';
|
||||
import EarningsTable from '@/components/custom/EarningsTable';
|
||||
|
||||
interface DashboardPageProps {
|
||||
searchParams: Record<string, any>;
|
||||
userData?: any;
|
||||
userLoading?: boolean;
|
||||
userError?: any;
|
||||
refreshUser?: () => void;
|
||||
updateUser?: (data: any) => void;
|
||||
onlineData?: any;
|
||||
onlineLoading?: boolean;
|
||||
onlineError?: any;
|
||||
refreshOnline?: () => void;
|
||||
updateOnline?: (data: any) => void;
|
||||
activePageUrl?: string;
|
||||
}
|
||||
|
||||
export const DashboardPage: FC<DashboardPageProps> = ({
|
||||
searchParams,
|
||||
userData,
|
||||
userLoading,
|
||||
onlineData,
|
||||
activePageUrl
|
||||
}) => {
|
||||
// Sample data for the dashboard
|
||||
const statCardsData = [
|
||||
{
|
||||
title: 'Users',
|
||||
count: '36.5k',
|
||||
icon: 'bx bx-user',
|
||||
iconColor: 'text-blue-500',
|
||||
percentage: '4.65%',
|
||||
isPositive: true
|
||||
},
|
||||
{
|
||||
title: 'Companies',
|
||||
count: '4.5k',
|
||||
icon: 'bx bx-building',
|
||||
iconColor: 'text-yellow-500',
|
||||
percentage: '1.25%',
|
||||
isPositive: false
|
||||
},
|
||||
{
|
||||
title: 'Blogs',
|
||||
count: '12.5k',
|
||||
icon: 'bx bxl-blogger',
|
||||
iconColor: 'text-green-500',
|
||||
percentage: '2.15%',
|
||||
isPositive: true
|
||||
},
|
||||
{
|
||||
title: 'Revenue',
|
||||
count: '$35.5k',
|
||||
icon: 'bx bx-dollar',
|
||||
iconColor: 'text-pink-500',
|
||||
percentage: '3.75%',
|
||||
isPositive: true
|
||||
}
|
||||
];
|
||||
|
||||
const userRolesColumns = [
|
||||
{ header: 'Name', accessor: 'name' },
|
||||
{ header: 'Email', accessor: 'email' },
|
||||
{
|
||||
header: 'Role', accessor: 'role',
|
||||
cell: (value: string) => (
|
||||
<span className={`py-1 px-2 rounded-md text-xs ${value === 'Admin' ? 'bg-blue-500/10 text-blue-500' : value === 'Editor' ? 'bg-yellow-500/10 text-yellow-500' : 'bg-emerald-500/10 text-emerald-500'}`}>
|
||||
{value}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
];
|
||||
|
||||
const userRolesData = [
|
||||
{ name: 'John Doe', email: 'john@example.com', role: 'Admin' },
|
||||
{ name: 'Jane Smith', email: 'jane@example.com', role: 'Editor' },
|
||||
{ name: 'Robert Johnson', email: 'robert@example.com', role: 'Customer' },
|
||||
{ name: 'Emily Davis', email: 'emily@example.com', role: 'Customer' },
|
||||
{ name: 'Michael Brown', email: 'michael@example.com', role: 'Editor' }
|
||||
];
|
||||
|
||||
const activitiesColumns = [
|
||||
{ header: 'Name', accessor: 'name' },
|
||||
{ header: 'Date', accessor: 'date' },
|
||||
{ header: 'Time', accessor: 'time' }
|
||||
];
|
||||
|
||||
const activitiesData = [
|
||||
{ name: 'Lorem Ipsum', date: '02-02-2024', time: '17.45' },
|
||||
{ name: 'Lorem Ipsum', date: '02-02-2024', time: '17.45' },
|
||||
{ name: 'Lorem Ipsum', date: '02-02-2024', time: '17.45' },
|
||||
{ name: 'Lorem Ipsum', date: '02-02-2024', time: '17.45' },
|
||||
{ name: 'Lorem Ipsum', date: '02-02-2024', time: '17.45' }
|
||||
];
|
||||
|
||||
const earningsData = [
|
||||
{ service: 'Create landing page', imageUrl: 'https://placehold.co/32x32', amount: '$235', isPositive: true, status: 'Pending' },
|
||||
{ service: 'Create landing page', imageUrl: 'https://placehold.co/32x32', amount: '$235', isPositive: false, status: 'Withdrawn' },
|
||||
{ service: 'Create landing page', imageUrl: 'https://placehold.co/32x32', amount: '$235', isPositive: true, status: 'Pending' },
|
||||
{ service: 'Create landing page', imageUrl: 'https://placehold.co/32x32', amount: '$235', isPositive: false, status: 'Withdrawn' },
|
||||
{ service: 'Create landing page', imageUrl: 'https://placehold.co/32x32', amount: '$235', isPositive: true, status: 'Pending' }
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="p-6">
|
||||
{/* User info section */}
|
||||
{userData && (
|
||||
<div className="mb-6 p-4 bg-blue-50 rounded-lg">
|
||||
<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>
|
||||
)}
|
||||
|
||||
{/* Stat cards */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-6">
|
||||
{statCardsData.map((card, index) => (
|
||||
<StatCard
|
||||
key={index}
|
||||
title={card.title}
|
||||
count={card.count}
|
||||
icon={card.icon}
|
||||
iconColor={card.iconColor}
|
||||
percentage={card.percentage}
|
||||
isPositive={card.isPositive}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Tables */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
|
||||
<DataTable
|
||||
title="User Roles"
|
||||
columns={userRolesColumns}
|
||||
data={userRolesData}
|
||||
/>
|
||||
<DataTable
|
||||
title="Activities"
|
||||
columns={activitiesColumns}
|
||||
data={activitiesData}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Order Stats and Earnings */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-6">
|
||||
<OrderStats
|
||||
activeOrders={10}
|
||||
activeAmount={80}
|
||||
completedOrders={50}
|
||||
completedAmount={469}
|
||||
canceledOrders={4}
|
||||
canceledAmount={130}
|
||||
/>
|
||||
<EarningsTable items={earningsData} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
import { ContentProps } from "@/validations/mutual/dashboard/props";
|
||||
import ContentToRenderNoPage from "@/pages/mutual/noContent/page";
|
||||
import pageIndexMulti from "@/pages/multi/index";
|
||||
|
||||
const PageToBeChildrendMulti: React.FC<ContentProps> = ({
|
||||
activePageUrl,
|
||||
userData,
|
||||
userLoading,
|
||||
userError,
|
||||
onlineData,
|
||||
onlineLoading,
|
||||
onlineError,
|
||||
searchParams,
|
||||
refreshOnline,
|
||||
updateOnline,
|
||||
refreshUser,
|
||||
updateUser,
|
||||
}) => {
|
||||
const pageComponents = pageIndexMulti[activePageUrl];
|
||||
if (!pageComponents) { return <ContentToRenderNoPage lang={onlineData.lang} /> }
|
||||
const ComponentKey = Object.keys(pageComponents)[0];
|
||||
const PageComponent = pageComponents[ComponentKey];
|
||||
if (!PageComponent) { return <ContentToRenderNoPage lang={onlineData.lang} /> }
|
||||
return <PageComponent
|
||||
activePageUrl={activePageUrl} searchParams={searchParams}
|
||||
userData={userData} userLoading={userLoading} userError={userError} refreshUser={refreshUser} updateUser={updateUser}
|
||||
onlineData={onlineData} onlineLoading={onlineLoading} onlineError={onlineError} refreshOnline={refreshOnline} updateOnline={updateOnline}
|
||||
/>;
|
||||
}
|
||||
|
||||
export default PageToBeChildrendMulti
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
import { ContentProps } from "@/validations/mutual/dashboard/props";
|
||||
import ContentToRenderNoPage from "@/pages/mutual/noContent/page";
|
||||
import { resolveWhichPageToRenderSingle } from "@/pages/resolver/resolver";
|
||||
|
||||
const PageToBeChildrendSingle: React.FC<ContentProps> = async ({ lang, translations, activePageUrl, mode }) => {
|
||||
const ApplicationToRender = await resolveWhichPageToRenderSingle({ activePageUrl })
|
||||
if (ApplicationToRender) {
|
||||
return <ApplicationToRender lang={lang} translations={translations} activePageUrl={activePageUrl} mode={mode} />
|
||||
}
|
||||
return <ContentToRenderNoPage lang={lang} />
|
||||
}
|
||||
|
||||
export default PageToBeChildrendSingle
|
||||
|
|
@ -0,0 +1,93 @@
|
|||
'use client';
|
||||
import { FC, Suspense, memo } from "react";
|
||||
import { ContentProps, ModeTypes, ModeTypesList } from "@/validations/mutual/dashboard/props";
|
||||
import { LanguageTypes } from "@/validations/mutual/language/validations";
|
||||
import PageToBeChildrendMulti from "./PageToBeChildrendMulti";
|
||||
import LoadingContent from "@/components/mutual/loader/component";
|
||||
|
||||
const MemoizedMultiPage = memo(PageToBeChildrendMulti);
|
||||
|
||||
const translations = {
|
||||
en: {
|
||||
errorLoadingContent: "Error Loading Content",
|
||||
contentArea: "Content Area",
|
||||
contentLoading: "Content Loading",
|
||||
contentLoadingDescription: "The requested page is currently unavailable or still loading.",
|
||||
pageUrl: "Page URL",
|
||||
language: "Language",
|
||||
},
|
||||
tr: {
|
||||
errorLoadingContent: "İçerik Yüklenirken Hata",
|
||||
contentArea: "İçerik Alanı",
|
||||
contentLoading: "İçerik Yükleniyor",
|
||||
contentLoadingDescription: "İçerik Yüklenirken Hata",
|
||||
pageUrl: "Sayfa URL",
|
||||
language: "Dil",
|
||||
}
|
||||
}
|
||||
|
||||
const FallbackContent: FC<{ lang: LanguageTypes; activePageUrl: string }> = memo(({ lang, activePageUrl }) => (
|
||||
<div className="p-6 bg-white rounded-lg shadow-md">
|
||||
<h2 className="text-2xl font-bold mb-4">{translations[lang].contentLoading}</h2>
|
||||
<p className="text-gray-600 mb-4">{translations[lang].contentLoadingDescription}</p>
|
||||
<div className="p-4 bg-blue-50 border border-blue-200 rounded-md">
|
||||
<p className="text-sm text-blue-700">{translations[lang].pageUrl}: {activePageUrl}</p>
|
||||
<p className="text-sm text-blue-700">{translations[lang].language}: {lang}</p>
|
||||
</div>
|
||||
</div>
|
||||
));
|
||||
|
||||
const ContentComponent: FC<ContentProps> = ({
|
||||
searchParams, activePageUrl, mode, userData, userLoading, userError, refreshUser, updateUser,
|
||||
onlineData, onlineLoading, onlineError, refreshOnline, updateOnline,
|
||||
}) => {
|
||||
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)]";
|
||||
const lang = onlineData?.lang as LanguageTypes || 'en';
|
||||
|
||||
if (userLoading) { return <div className={classNameDiv}>{loadingContent}</div> }
|
||||
if (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">{translations[lang].errorLoadingContent}</h2><p className="text-gray-600 mb-4">{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">{translations[lang].contentArea}</h2>
|
||||
{(!userData) && (<FallbackContent lang={lang} activePageUrl={activePageUrl || ''} />)}
|
||||
<MemoizedMultiPage
|
||||
activePageUrl={activePageUrl || ''} searchParams={searchParams}
|
||||
userData={userData} userLoading={userLoading} userError={userError} refreshUser={refreshUser} updateUser={updateUser}
|
||||
onlineData={onlineData} onlineLoading={onlineLoading} onlineError={onlineError} refreshOnline={refreshOnline} updateOnline={updateOnline} />
|
||||
</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>
|
||||
// )}
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
'use client';
|
||||
import { FC } from "react";
|
||||
import { langGetKey } from "@/lib/langGet";
|
||||
import { FooterProps } from "@/validations/mutual/dashboard/props";
|
||||
import { LanguageTypes } from "@/validations/mutual/language/validations";
|
||||
|
||||
const translations = {
|
||||
en: {
|
||||
footer: "footer",
|
||||
page: "page"
|
||||
},
|
||||
tr: {
|
||||
footer: "footer",
|
||||
page: "sayfa"
|
||||
}
|
||||
}
|
||||
|
||||
const FooterComponent: FC<FooterProps> = ({
|
||||
activePageUrl, configData, configLoading, configError,
|
||||
onlineData, onlineLoading, onlineError
|
||||
}) => {
|
||||
// Use the config context hook
|
||||
const lang = onlineData?.lang as LanguageTypes || 'en';
|
||||
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
||||
export default FooterComponent;
|
||||
|
|
@ -0,0 +1,162 @@
|
|||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
} from '@/components/mutual/ui/dropdown-menu';
|
||||
|
||||
interface NavbarProps {
|
||||
toggleSidebar: () => void;
|
||||
}
|
||||
|
||||
export default function Navbar({ toggleSidebar }: NavbarProps) {
|
||||
const [activeNotificationTab, setActiveNotificationTab] = useState('notifications');
|
||||
|
||||
// Toggle fullscreen
|
||||
const toggleFullscreen = () => {
|
||||
if (!document.fullscreenElement) {
|
||||
document.documentElement.requestFullscreen().catch(err => {
|
||||
console.error(`Error attempting to enable full-screen mode: ${err.message}`);
|
||||
});
|
||||
} else if (document.exitFullscreen) {
|
||||
document.exitFullscreen();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="py-2 px-6 bg-[#f8f4f3] flex items-center shadow-md shadow-black/5 sticky top-0 left-0 z-30">
|
||||
<button type="button" className="text-lg text-gray-900 font-semibold sidebar-toggle md:hidden" onClick={toggleSidebar}>
|
||||
<i className="ri-menu-line"></i>
|
||||
</button>
|
||||
|
||||
<ul className="ml-auto flex items-center">
|
||||
{/* Search dropdown */}
|
||||
<li className="mr-1">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
className="text-gray-400 mr-4 w-8 h-8 rounded flex items-center justify-center hover:text-gray-600"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" className="hover:bg-gray-100 rounded-full" viewBox="0 0 24 24" style={{ fill: 'gray' }}>
|
||||
<path d="M19.023 16.977a35.13 35.13 0 0 1-1.367-1.384c-.372-.378-.596-.653-.596-.653l-2.8-1.337A6.962 6.962 0 0 0 16 9c0-3.859-3.14-7-7-7S2 5.141 2 9s3.14 7 7 7c1.763 0 3.37-.66 4.603-1.739l1.337 2.8s.275.224.653.596c.387.363.896.854 1.384 1.367l1.358 1.392.604.646 2.121-2.121-.646-.604c-.379-.372-.885-.866-1.391-1.36zM9 14c-2.757 0-5-2.243-5-5s2.243-5 5-5 5 2.243 5 5-2.243 5-5 5z"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="w-64">
|
||||
<div className="p-4 border-b border-b-gray-100">
|
||||
<div className="relative w-full">
|
||||
<input type="text" className="py-2 pr-4 pl-10 bg-gray-50 w-full outline-none border border-gray-100 rounded-md text-sm focus:border-blue-500" placeholder="Search..." />
|
||||
<i className="ri-search-line absolute top-1/2 left-4 -translate-y-1/2 text-gray-900"></i>
|
||||
</div>
|
||||
</div>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</li>
|
||||
|
||||
{/* Notifications dropdown */}
|
||||
<li>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
className="text-gray-400 mr-4 w-8 h-8 rounded flex items-center justify-center hover:text-gray-600"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" className="hover:bg-gray-100 rounded-full" viewBox="0 0 24 24" style={{ fill: 'gray' }}>
|
||||
<path d="M19 13.586V10c0-3.217-2.185-5.927-5.145-6.742C13.562 2.52 12.846 2 12 2s-1.562.52-1.855 1.258C7.185 4.074 5 6.783 5 10v3.586l-1.707 1.707A.996.996 0 0 0 3 16v2a1 1 0 0 0 1 1h16a1 1 0 0 0 1-1v-2a.996.996 0 0 0-.293-.707L19 13.586zM19 17H5v-.586l1.707-1.707A.996.996 0 0 0 7 14v-4c0-2.757 2.243-5 5-5s5 2.243 5 5v4c0 .266.105.52.293.707L19 16.414V17zm-7 5a2.98 2.98 0 0 0 2.818-2H9.182A2.98 2.98 0 0 0 12 22z"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="w-64">
|
||||
<div className="flex items-center px-4 pt-4 border-b border-b-gray-100 notification-tab">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setActiveNotificationTab('notifications')}
|
||||
className={`text-gray-400 font-medium text-[13px] hover:text-gray-600 border-b-2 ${activeNotificationTab === 'notifications' ? 'border-b-blue-500 text-blue-500' : 'border-b-transparent'} mr-4 pb-1`}
|
||||
>
|
||||
Notifications
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setActiveNotificationTab('messages')}
|
||||
className={`text-gray-400 font-medium text-[13px] hover:text-gray-600 border-b-2 ${activeNotificationTab === 'messages' ? 'border-b-blue-500 text-blue-500' : 'border-b-transparent'} mr-4 pb-1`}
|
||||
>
|
||||
Messages
|
||||
</button>
|
||||
</div>
|
||||
<div className="max-h-64 overflow-y-auto py-2">
|
||||
{activeNotificationTab === 'notifications' ? (
|
||||
<div>
|
||||
{[...Array(5)].map((_, i) => (
|
||||
<DropdownMenuItem key={`notif-${i}`} className="px-4 py-2 hover:bg-gray-50 cursor-pointer">
|
||||
<div className="flex items-center w-full">
|
||||
<div className="w-8 h-8 rounded bg-blue-500 flex items-center justify-center text-white font-medium">N</div>
|
||||
<div className="ml-2 flex-grow">
|
||||
<div className="text-[13px] text-gray-600 font-medium truncate">New order</div>
|
||||
<div className="text-[11px] text-gray-400">from John Doe</div>
|
||||
</div>
|
||||
<div className="text-[11px] text-gray-400">5 min ago</div>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
{[...Array(5)].map((_, i) => (
|
||||
<DropdownMenuItem key={`msg-${i}`} className="px-4 py-2 hover:bg-gray-50 cursor-pointer">
|
||||
<div className="flex items-center w-full">
|
||||
<div className="w-8 h-8 rounded bg-green-500 flex items-center justify-center text-white font-medium">J</div>
|
||||
<div className="ml-2 flex-grow">
|
||||
<div className="text-[13px] text-gray-600 font-medium truncate">John Doe</div>
|
||||
<div className="text-[11px] text-gray-400">Hello there!</div>
|
||||
</div>
|
||||
<div className="text-[11px] text-gray-400">5 min ago</div>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</li>
|
||||
|
||||
{/* Fullscreen button */}
|
||||
<li>
|
||||
<button
|
||||
type="button"
|
||||
onClick={toggleFullscreen}
|
||||
className="text-gray-400 mr-4 w-8 h-8 rounded flex items-center justify-center hover:text-gray-600"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" className="hover:bg-gray-100 rounded-full" viewBox="0 0 24 24" style={{ fill: 'gray' }}>
|
||||
<path d="M5 5h5V3H3v7h2zm5 14H5v-5H3v7h7zm11-5h-2v5h-5v2h7zm-2-4h2V3h-7v2h5z"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</li>
|
||||
|
||||
{/* Profile dropdown */}
|
||||
<li>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<button className="flex items-center">
|
||||
<div className="w-8 h-8 rounded-full bg-blue-600 flex items-center justify-center text-white font-medium">
|
||||
B
|
||||
</div>
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem className="cursor-pointer">Profile</DropdownMenuItem>
|
||||
<DropdownMenuItem className="cursor-pointer">Settings</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem className="cursor-pointer">Logout</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
'use client';
|
||||
import { FC } from "react";
|
||||
import { HeaderProps } from "@/validations/mutual/dashboard/props";
|
||||
import { langGetKey } from "@/lib/langGet";
|
||||
import { LanguageTypes } from "@/validations/mutual/language/validations";
|
||||
import LanguageSelectionComponent from "@/components/mutual/languageSelection/component";
|
||||
|
||||
const translations = {
|
||||
en: {
|
||||
selectedPage: "selectedPage",
|
||||
page: "page"
|
||||
},
|
||||
tr: {
|
||||
selectedPage: "seçiliSayfa",
|
||||
page: "sayfa"
|
||||
}
|
||||
}
|
||||
|
||||
const HeaderComponent: FC<HeaderProps> = ({
|
||||
activePageUrl, onlineData, onlineLoading, onlineError, refreshOnline, updateOnline,
|
||||
userData, userLoading, userError, refreshUser, updateUser
|
||||
}) => {
|
||||
const lang = onlineData?.lang as LanguageTypes || 'en';
|
||||
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"> {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">{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
|
||||
activePage={activePageUrl} onlineData={onlineData} onlineLoading={onlineLoading} onlineError={onlineError} refreshOnline={refreshOnline} updateOnline={updateOnline}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default HeaderComponent;
|
||||
|
|
@ -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;
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
'use client';
|
||||
import { FC, Suspense } from "react";
|
||||
import { MenuProps } from "@/validations/mutual/dashboard/props";
|
||||
import { LanguageTypes } from "@/validations/mutual/language/validations";
|
||||
|
||||
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> = ({
|
||||
activePageUrl, availableApplications, prefix,
|
||||
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) }
|
||||
const lang = onlineData?.lang as LanguageTypes || 'en';
|
||||
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">
|
||||
{/* User Profile Section */}
|
||||
<Suspense fallback={<div><LoadingContent height="h-16" size="w-36 h-48" key={"loading-content"} 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-content"} 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-content"} plane="h-full w-full" /></div>}>
|
||||
<MenuItemsSection availableApplications={availableApplications} activePageUrl={activePageUrl} lang={lang} prefix={prefix} />
|
||||
</Suspense>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MenuComponent;
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
'use client';
|
||||
import { FC } from "react";
|
||||
|
||||
interface FirstLayerDropdownProps {
|
||||
isActive: boolean;
|
||||
isExpanded: boolean;
|
||||
innerText: string;
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
const FirstLayerDropdown: FC<FirstLayerDropdownProps> = ({ isActive, isExpanded, innerText, onClick }) => {
|
||||
// Base styles
|
||||
const baseClassName = "py-3 px-4 text-sm rounded-xl cursor-pointer transition-colors duration-200 flex justify-between items-center w-full";
|
||||
|
||||
// Determine the appropriate class based on active and expanded states
|
||||
let className = baseClassName;
|
||||
if (isActive) {
|
||||
className += " bg-emerald-700 text-white font-medium";
|
||||
} else if (isExpanded) {
|
||||
className += " bg-emerald-600 text-white";
|
||||
} else {
|
||||
className += " bg-emerald-800 text-white hover:bg-emerald-700";
|
||||
}
|
||||
|
||||
return (
|
||||
<div onClick={onClick} className={className}>
|
||||
<span>{innerText}</span>
|
||||
{isExpanded ? (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 15l7-7 7 7" />
|
||||
</svg>
|
||||
) : (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FirstLayerDropdown;
|
||||
|
|
@ -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,101 @@
|
|||
'use client';
|
||||
import React, { FC, useEffect, useState } from "react";
|
||||
import { menuTranslation } from "@/languages/mutual/menu";
|
||||
import FirstLayerDropdown from "./firstLayerComponent";
|
||||
import SecondLayerDropdown from "./secondLayerComponent";
|
||||
import ThirdLayerDropdown from "./thirdLayerComponent";
|
||||
|
||||
type TranslationItem = { value: string; key: string };
|
||||
type ThirdLayerItemData = { path: string; translation: TranslationItem[] };
|
||||
type ThirdLayerItem = Record<string, ThirdLayerItemData>;
|
||||
type SecondLayerItems = Record<string, ThirdLayerItem>;
|
||||
type FirstLayerItems = Record<string, SecondLayerItems>;
|
||||
type MenuStructure = FirstLayerItems;
|
||||
|
||||
interface MenuItemsSectionProps {
|
||||
availableApplications: string[];
|
||||
activePageUrl: string;
|
||||
lang: string;
|
||||
prefix?: string;
|
||||
}
|
||||
|
||||
const menuStaticTranslation = {
|
||||
tr: { menu: "Menü" },
|
||||
en: { menu: "Menu" }
|
||||
}
|
||||
|
||||
const MenuItemsSection: FC<MenuItemsSectionProps> = ({ availableApplications, activePageUrl, lang, prefix }) => {
|
||||
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 activeParsedLayer = (menuTranslationWLang[activePageUrl as keyof typeof menuTranslationWLang] as unknown as TranslationItem[]) || [];
|
||||
const activeFirstLayer = activeParsedLayer[0] ? activeParsedLayer[0].key : null;
|
||||
const activeSecondLayer = activeParsedLayer[1] ? activeParsedLayer[1].key : null;
|
||||
const activeThirdLayer = activeParsedLayer[2] ? activeParsedLayer[2].key : null;
|
||||
|
||||
useEffect(() => {
|
||||
const newMenuStructure: MenuStructure = {};
|
||||
availableApplications.forEach((appPath: string) => {
|
||||
const pathTranslation = menuTranslationWLang[appPath as keyof typeof menuTranslationWLang] as unknown as TranslationItem[] | undefined;
|
||||
if (pathTranslation && pathTranslation.length >= 3) {
|
||||
const firstLayer = pathTranslation[0] ? pathTranslation[0].key : '';
|
||||
const secondLayer = pathTranslation[1] ? pathTranslation[1].key : '';
|
||||
const thirdLayer = pathTranslation[2] ? pathTranslation[2].key : '';
|
||||
if (!newMenuStructure[firstLayer]) { newMenuStructure[firstLayer] = {} }
|
||||
if (!newMenuStructure[firstLayer][secondLayer]) { newMenuStructure[firstLayer][secondLayer] = {} }
|
||||
newMenuStructure[firstLayer][secondLayer][thirdLayer] = { path: appPath, translation: pathTranslation };
|
||||
}
|
||||
});
|
||||
setMenuStructure(newMenuStructure);
|
||||
}, [availableApplications, menuTranslationWLang]);
|
||||
|
||||
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) => {
|
||||
return Object.entries(thirdLayerItems).map(([thirdLayerKey, itemData]) => {
|
||||
const isActive = activeFirstLayer === firstLayerKey && activeSecondLayer === secondLayerKey && activeThirdLayer === thirdLayerKey;
|
||||
const url = itemData ? itemData.path || '' : '';
|
||||
const translation = itemData ? itemData.translation || [] : [];
|
||||
const displayText = translation[2]?.value || thirdLayerKey;
|
||||
return <div key={`${thirdLayerKey}-item`} className="ml-2 my-1"><ThirdLayerDropdown isActive={isActive} innerText={displayText} url={`${prefix}${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 anyThirdLayerItem = Object.values(thirdLayerItems)[0];
|
||||
const translation = anyThirdLayerItem ? anyThirdLayerItem.translation : [];
|
||||
const displayText = translation[1]?.value || secondLayerKey;
|
||||
return (
|
||||
<div key={`${secondLayerKey}-item`} className="ml-2 my-1">
|
||||
<SecondLayerDropdown isActive={isActive} isExpanded={isExpanded} innerText={displayText} 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 anySecondLayer = Object.values(secondLayerItems)[0];
|
||||
const anyThirdLayerItem = anySecondLayer ? Object.values(anySecondLayer)[0] : null;
|
||||
const translation = anyThirdLayerItem ? anyThirdLayerItem.translation : [];
|
||||
const displayText = translation[0]?.value || firstLayerKey;
|
||||
return (
|
||||
<div key={`${firstLayerKey}-item`} className="mb-2">
|
||||
<FirstLayerDropdown isActive={isActive} isExpanded={isExpanded} innerText={displayText} 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">{menuStaticTranslation[lang as keyof typeof menuStaticTranslation].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,41 @@
|
|||
'use client';
|
||||
import { FC } from "react";
|
||||
|
||||
interface SecondLayerDropdownProps {
|
||||
isActive: boolean;
|
||||
isExpanded: boolean;
|
||||
innerText: string;
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
const SecondLayerDropdown: FC<SecondLayerDropdownProps> = ({ isActive, isExpanded, innerText, onClick }) => {
|
||||
// Base styles
|
||||
const baseClassName = "py-2 my-1 px-3 text-sm rounded-lg cursor-pointer transition-colors duration-200 flex justify-between items-center w-full";
|
||||
|
||||
// Determine the appropriate class based on active and expanded states
|
||||
let className = baseClassName;
|
||||
if (isActive) {
|
||||
className += " bg-emerald-600 text-white font-medium";
|
||||
} else if (isExpanded) {
|
||||
className += " bg-emerald-500 text-white";
|
||||
} else {
|
||||
className += " bg-emerald-700 text-white hover:bg-emerald-600";
|
||||
}
|
||||
|
||||
return (
|
||||
<div onClick={onClick} className={className}>
|
||||
<span>{innerText}</span>
|
||||
{isExpanded ? (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 15l7-7 7 7" />
|
||||
</svg>
|
||||
) : (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SecondLayerDropdown;
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
'use client'
|
||||
import { FC, useState } from "react";
|
||||
import Link from "next/link";
|
||||
import LoadingContent from "@/components/mutual/loader/component";
|
||||
|
||||
interface ThirdLayerDropdownProps {
|
||||
isActive: boolean,
|
||||
innerText: string,
|
||||
url: string,
|
||||
}
|
||||
|
||||
const ThirdLayerDropdown: FC<ThirdLayerDropdownProps> = ({ isActive, innerText, url }) => {
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
|
||||
// Base styles
|
||||
const baseClassName = "py-2 my-1 px-3 text-sm rounded-lg bg-black transition-colors duration-200 flex items-center w-full";
|
||||
|
||||
// Determine the appropriate class based on active state
|
||||
let className = baseClassName;
|
||||
if (isActive) {
|
||||
className += " bg-emerald-500 text-white font-medium";
|
||||
} else {
|
||||
className += " bg-emerald-600 text-white hover:bg-emerald-500";
|
||||
}
|
||||
|
||||
if (isActive) {
|
||||
return (
|
||||
<div className={`${className} cursor-not-allowed`}>
|
||||
<span className="ml-2">{innerText}</span>
|
||||
</div>
|
||||
);
|
||||
} else if (isLoading) {
|
||||
return (
|
||||
<div className={className}>
|
||||
<LoadingContent height="h-5" size="w-5 h-5" plane="" />
|
||||
<span className="ml-2">{innerText}</span>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<Link href={url} onClick={() => setIsLoading(true)} className="block">
|
||||
<div className={className}>
|
||||
<span className="ml-2">{innerText}</span>
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default ThirdLayerDropdown;
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
interface IntrerfaceLayerDropdown {
|
||||
isActive: boolean;
|
||||
innerText: string;
|
||||
onClick: () => void;
|
||||
}
|
||||
|
|
@ -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,38 @@
|
|||
'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;
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
import React from 'react';
|
||||
|
||||
interface RenderOneClientSelectionProps {
|
||||
item: any;
|
||||
isSelected: boolean;
|
||||
onClickHandler: (client: any) => void;
|
||||
}
|
||||
|
||||
const renderOneClientSelection = ({
|
||||
item,
|
||||
isSelected,
|
||||
onClickHandler
|
||||
}: RenderOneClientSelectionProps) => {
|
||||
return (
|
||||
<div
|
||||
className={`w-full text-xs bg-white shadow rounded-lg overflow-hidden mb-2 transition-all ${isSelected ? 'border-2 border-amber-500' : ''} hover:shadow-md`}
|
||||
onClick={() => onClickHandler(item)}
|
||||
>
|
||||
<div className={`${isSelected ? 'bg-amber-100' : 'bg-gray-50'} p-2 hover:bg-amber-50 transition-all`}>
|
||||
<div className="flex items-center">
|
||||
<div className="mr-2">
|
||||
{item.logo ? (
|
||||
<img className="rounded border border-gray-200" src={item.logo} alt="Logo" width={40} height={40} />
|
||||
) : (
|
||||
<div className="w-10 h-10 rounded bg-gray-200 flex items-center justify-center border border-gray-300">
|
||||
<div className="text-gray-500 text-sm font-bold">{item.name ? item.name.slice(0, 2).toUpperCase() : 'C'}</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="overflow-hidden">
|
||||
<h2 className="text-sm font-bold text-gray-800 truncate">{item.name || 'Client'}</h2>
|
||||
<p className="text-xs text-gray-500 truncate">{item.description || 'No description'}</p>
|
||||
</div>
|
||||
{isSelected && (
|
||||
<div className="ml-auto">
|
||||
<div className="bg-amber-500 rounded-full w-4 h-4 flex items-center justify-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-3 w-3 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default renderOneClientSelection;
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
"use client";
|
||||
|
||||
import { ClientPageConfig } from "@/types/mutual/context";
|
||||
import { API_BASE_URL } from "@/config/config";
|
||||
import { createContextHook } from "../hookFactory";
|
||||
|
||||
// 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" },
|
||||
});
|
||||
const data = await result.json();
|
||||
if (data.status === 200) return data.data;
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error("Error checking page config:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function setContextPageConfig({
|
||||
pageConfig,
|
||||
}: {
|
||||
pageConfig: ClientPageConfig;
|
||||
}) {
|
||||
try {
|
||||
const result = await fetch(`${API_BASE_URL}/context/page/config`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(pageConfig),
|
||||
});
|
||||
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,
|
||||
};
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
"use client";
|
||||
|
||||
import { ClientMenu } from "@/types/mutual/context";
|
||||
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",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
try {
|
||||
const data = await result.json();
|
||||
if (data.status == 200) return data.data;
|
||||
} catch (error) {
|
||||
throw new Error("No data is found");
|
||||
}
|
||||
}
|
||||
|
||||
async function setContextPageMenu(setMenu: ClientMenu) {
|
||||
const result = await fetch(`${API_BASE_URL}/context/page/menu`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(setMenu),
|
||||
});
|
||||
try {
|
||||
const data = await result.json();
|
||||
return data;
|
||||
} catch (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 };
|
||||
|
|
@ -0,0 +1,152 @@
|
|||
"use client";
|
||||
|
||||
import { ClientOnline } from "@/types/mutual/context";
|
||||
import { API_BASE_URL } from "@/config/config";
|
||||
import { createContextHook } from "../hookFactory";
|
||||
|
||||
// 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",
|
||||
"Cache-Control": "no-cache",
|
||||
},
|
||||
signal: controller.signal,
|
||||
});
|
||||
|
||||
// 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 && 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",
|
||||
"Cache-Control": "no-cache",
|
||||
},
|
||||
body: JSON.stringify(setOnline),
|
||||
signal: controller.signal,
|
||||
});
|
||||
console.log("result", await result.json());
|
||||
|
||||
// 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 && 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,197 @@
|
|||
'use client';
|
||||
|
||||
import React, { FC, ReactNode, createContext, useContext, useEffect, useState, useCallback } from 'react';
|
||||
import { checkContextPageOnline, setContextPageOnline } from './context';
|
||||
import { setOnlineToRedis } from '@/fetchers/custom/context/page/online/fetch';
|
||||
|
||||
interface ClientOnline {
|
||||
lang: string;
|
||||
userType: string;
|
||||
lastLogin: Date;
|
||||
lastLogout: Date;
|
||||
lastAction: Date;
|
||||
lastPage: string;
|
||||
timezone: string;
|
||||
}
|
||||
|
||||
// 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;
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
'use client';
|
||||
|
||||
import { ClientSelection } from "@/types/mutual/context";
|
||||
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`, {
|
||||
method: "GET",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
|
||||
const data = await result.json();
|
||||
if (data.status === 200) return data.data;
|
||||
} catch (error) {
|
||||
console.error("Error checking dash selection:", error);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async function setContextDashUserSelection({
|
||||
userSet,
|
||||
}: {
|
||||
userSet: ClientSelection;
|
||||
}) {
|
||||
try {
|
||||
const result = await fetch(`${API_BASE_URL}/context/dash/selection`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(userSet),
|
||||
});
|
||||
|
||||
const data = await result.json();
|
||||
if (data.status === 200) return data.data;
|
||||
} catch (error) {
|
||||
console.error("Error setting dash selection:", error);
|
||||
}
|
||||
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 };
|
||||
|
|
@ -0,0 +1,263 @@
|
|||
"use client";
|
||||
|
||||
import { ClientUser } from "@/types/mutual/context";
|
||||
import { API_BASE_URL } from "@/config/config";
|
||||
import { createContextHook } from "../hookFactory";
|
||||
import {
|
||||
getUserFromRedis as getUserFromServer,
|
||||
setUserToRedis as setUserFromServer,
|
||||
} from "@/fetchers/custom/context/dash/user/fetch";
|
||||
|
||||
// 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",
|
||||
"Cache-Control": "no-cache",
|
||||
},
|
||||
signal: controller.signal,
|
||||
});
|
||||
|
||||
// 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) {
|
||||
// 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;
|
||||
}): 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",
|
||||
"Cache-Control": "no-cache",
|
||||
},
|
||||
body: JSON.stringify(userSet),
|
||||
signal: controller.signal,
|
||||
});
|
||||
|
||||
// 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 };
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
'use client';
|
||||
import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent } from "@/components/mutual/ui/dropdown-menu";
|
||||
import { Button } from "@/components/mutual/ui/button";
|
||||
import { languageSelectionTranslation } from "@/languages/mutual/languageSelection";
|
||||
import { langGetKey, langGet } from "@/lib/langGet";
|
||||
import { LanguageTypes } from "@/validations/mutual/language/validations";
|
||||
import LanguageSelectionItem from "./languageItem";
|
||||
|
||||
interface LanguageSelectionComponentProps {
|
||||
activePage: string;
|
||||
onlineData: any;
|
||||
onlineLoading: boolean;
|
||||
onlineError: any;
|
||||
refreshOnline: () => Promise<void>;
|
||||
updateOnline: (newOnline: any) => Promise<boolean>;
|
||||
}
|
||||
|
||||
const LanguageSelectionComponent: React.FC<LanguageSelectionComponentProps> = ({ activePage, onlineData, onlineLoading, onlineError, refreshOnline, updateOnline }) => {
|
||||
const lang = onlineData?.lang as LanguageTypes || 'en';
|
||||
const translations = langGet(lang, languageSelectionTranslation);
|
||||
const languageButtons = [
|
||||
{ activeLang: lang, buttonsLang: "en", refUrl: "en", innerText: langGetKey(translations, "english") },
|
||||
{ activeLang: lang, buttonsLang: "tr", refUrl: "tr", innerText: langGetKey(translations, "turkish") }
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="flex items-end justify-end">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button className="w-48 h-12 text-center text-md">{langGetKey(translations, "title")}</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
{languageButtons.map((props, index) => (
|
||||
<LanguageSelectionItem key={props.buttonsLang} {...props}
|
||||
onlineData={onlineData} onlineLoading={onlineLoading} onlineError={onlineError} refreshOnline={refreshOnline} updateOnline={updateOnline} />
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LanguageSelectionComponent;
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
'use client';
|
||||
import { FC } from "react";
|
||||
import { DropdownMenuItem } from "@/components/mutual/ui/dropdown-menu";
|
||||
|
||||
interface LanguageSelectionItemProps {
|
||||
activeLang: string,
|
||||
buttonsLang: string,
|
||||
refUrl: string,
|
||||
innerText: string,
|
||||
onlineData: any,
|
||||
onlineLoading: boolean,
|
||||
onlineError: any,
|
||||
refreshOnline: () => Promise<void>,
|
||||
updateOnline: (newOnline: any) => Promise<boolean>
|
||||
}
|
||||
|
||||
const RenderButtonComponent: FC<LanguageSelectionItemProps> = (
|
||||
{ activeLang, buttonsLang, refUrl, innerText, onlineData, onlineLoading, onlineError, refreshOnline, updateOnline }) => {
|
||||
const setOnlineObject = async () => {
|
||||
if (!onlineData || onlineLoading) return;
|
||||
|
||||
try {
|
||||
console.log("Updating language to:", buttonsLang);
|
||||
const success = await updateOnline({
|
||||
...onlineData,
|
||||
lang: buttonsLang,
|
||||
lastAction: new Date()
|
||||
});
|
||||
|
||||
if (success) {
|
||||
console.log("Language updated successfully");
|
||||
await refreshOnline();
|
||||
} else {
|
||||
console.error("Failed to update language");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error updating language:", error);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
onClick={setOnlineObject}
|
||||
className="flex w-full h-12 items-center justify-center text-center text-md cursor-pointer">
|
||||
{innerText}
|
||||
</DropdownMenuItem>
|
||||
)
|
||||
}
|
||||
|
||||
const LanguageSelectionItem: React.FC<LanguageSelectionItemProps> = ({ activeLang, buttonsLang, refUrl, innerText, onlineData, onlineLoading, onlineError, refreshOnline, updateOnline }) => {
|
||||
|
||||
const currentLang = onlineData?.lang || activeLang;
|
||||
const isActive = buttonsLang !== currentLang;
|
||||
const RenderButtonProp = {
|
||||
refUrl,
|
||||
innerText,
|
||||
activeLang,
|
||||
buttonsLang,
|
||||
onlineData,
|
||||
onlineLoading,
|
||||
onlineError,
|
||||
refreshOnline,
|
||||
updateOnline
|
||||
}
|
||||
|
||||
// Only render the button if it's not the current language
|
||||
return (
|
||||
<>
|
||||
{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;
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
import { Loader2 } from "lucide-react";
|
||||
|
||||
const LoadingContent: React.FC<{ height: string, size: string, plane: string }> = ({ height = "h-16", size = "w-36 h-48", plane = "h-full w-full" }) => {
|
||||
return <>
|
||||
<div className={`flex items-center justify-center ${plane}`}>
|
||||
<div className={height}><Loader2 className={`animate-spin ${size}`} /></div>
|
||||
</div></>
|
||||
}
|
||||
|
||||
export default LoadingContent
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
'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;
|
||||
|
|
@ -1,10 +1,11 @@
|
|||
import { ClientRedisOnline } from "@/fetchers/types/context/online/validations";
|
||||
import { ClientPageConfig } from "@/fetchers/types/context/pageConfig/validations";
|
||||
import { ClientMenu } from "@/fetchers/types/context/menu/validations";
|
||||
import { ClientHeader } from "@/fetchers/types/context/header/validations";
|
||||
import { ClientSelection } from "@/fetchers/types/context/selection/validations";
|
||||
import { ClientUser } from "@/fetchers/types/context/user/validations";
|
||||
import { ClientSettings } from "@/fetchers/types/context/settings/validations";
|
||||
import { ClientOnline } from "@/fetchers/types/context/online/validations";
|
||||
|
||||
import { defaultValuesOnline } from "@/fetchers/types/context/online/validations";
|
||||
import { defaultValuesPageConfig } from "@/fetchers/types/context/pageConfig/validations";
|
||||
import { defaultValuesMenu } from "@/fetchers/types/context/menu/validations";
|
||||
|
|
@ -14,7 +15,7 @@ import { defaultValuesUser } from "@/fetchers/types/context/user/validations";
|
|||
import { defaultValuesSettings } from "@/fetchers/types/context/settings/validations";
|
||||
|
||||
interface ClientRedisToken {
|
||||
online: ClientRedisOnline;
|
||||
online: ClientOnline;
|
||||
pageConfig: ClientPageConfig;
|
||||
menu: ClientMenu;
|
||||
header: ClientHeader;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,29 @@
|
|||
export async function retrieveAvailableApplications(data: any): Promise<string[]> {
|
||||
try {
|
||||
const response = await fetch("http://localhost:3000/api/menu", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json", "no-cache": "true" },
|
||||
body: JSON.stringify({ ...data }),
|
||||
})
|
||||
if (response.status !== 200) {
|
||||
throw new Error("Failed to retrieve available applications");
|
||||
}
|
||||
const result = await response.json();
|
||||
return result.data
|
||||
} catch (error) { throw error }
|
||||
}
|
||||
|
||||
export async function retrievePageToRender(data: any): Promise<string> {
|
||||
try {
|
||||
const response = await fetch("http://localhost:3000/api/pages", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json", "no-cache": "true" },
|
||||
body: JSON.stringify({ ...data }),
|
||||
})
|
||||
if (response.status !== 200) {
|
||||
throw new Error("Failed to retrieve page to render");
|
||||
}
|
||||
const result = await response.json();
|
||||
return result.data
|
||||
} catch (error) { throw error }
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
const buildingEn = {
|
||||
building: "Building First Layer Label",
|
||||
};
|
||||
|
||||
const buildingPartsEn = {
|
||||
...buildingEn,
|
||||
parts: "Parts Second Layer Label",
|
||||
};
|
||||
|
||||
const buildingPartsFieldsEn = {
|
||||
"Users.uuid": "UUID",
|
||||
"Users.firstName": "First Name",
|
||||
"Users.lastName": "Last Name",
|
||||
"Users.email": "Email",
|
||||
"Users.phoneNumber": "Phone Number",
|
||||
"Users.country": "Country",
|
||||
"Users.description": "Description",
|
||||
"Users.isDeleted": "Is Deleted",
|
||||
"Users.isConfirmed": "Is Confirmed",
|
||||
"Users.createdAt": "Created At",
|
||||
"Users.updatedAt": "Updated At",
|
||||
};
|
||||
|
||||
export { buildingEn, buildingPartsEn, buildingPartsFieldsEn };
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
const buildingTr = {
|
||||
building: "Bina Birinci Seviye",
|
||||
};
|
||||
const buildingPartsTr = {
|
||||
...buildingTr,
|
||||
parts: "Parçalar İkinci Seviye",
|
||||
};
|
||||
|
||||
const buildingPartsFieldsTr = {
|
||||
"Users.uuid": "UUID",
|
||||
"Users.firstName": "Ad",
|
||||
"Users.lastName": "Soyad",
|
||||
"Users.email": "Email",
|
||||
"Users.phoneNumber": "Telefon Numarası",
|
||||
"Users.country": "Ülke",
|
||||
"Users.description": "Açıklama",
|
||||
"Users.isDeleted": "Silindi",
|
||||
"Users.isConfirmed": "Onaylandı",
|
||||
"Users.createdAt": "Oluşturulma Tarihi",
|
||||
"Users.updatedAt": "Güncellenme Tarihi",
|
||||
};
|
||||
|
||||
export { buildingTr, buildingPartsTr, buildingPartsFieldsTr };
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
import { LanguageTypes } from "@/validations/mutual/language/validations";
|
||||
import { DynamicPage } from "@/validations/mutual/menu/menu";
|
||||
import { managementAccountTenantMain } from "./management/account/tenantSomething/index";
|
||||
// import { managementAccountTenantMainSecond } from "./management/account/tenantSomethingSecond/index";
|
||||
// import { buildingPartsTenantSomething } from "./building/parts/tenantSomething/index";
|
||||
|
||||
const dynamicPagesIndex: Record<string, Record<LanguageTypes, DynamicPage>> = {
|
||||
"/main/pages/user/dashboard": managementAccountTenantMain,
|
||||
"/definitions/identifications/people": managementAccountTenantMain,
|
||||
"/definitions/identifications/users": managementAccountTenantMain,
|
||||
"/definitions/building/parts": managementAccountTenantMain,
|
||||
"/definitions/building/areas": managementAccountTenantMain,
|
||||
"/building/accounts/managment/accounts": managementAccountTenantMain,
|
||||
"/building/accounts/managment/budgets": managementAccountTenantMain,
|
||||
"/building/accounts/parts/accounts": managementAccountTenantMain,
|
||||
"/building/accounts/parts/budgets": managementAccountTenantMain,
|
||||
"/building/meetings/regular/actions": managementAccountTenantMain,
|
||||
"/building/meetings/regular/accounts": managementAccountTenantMain,
|
||||
"/building/meetings/ergunt/actions": managementAccountTenantMain,
|
||||
"/building/meetings/ergunt/accounts": managementAccountTenantMain,
|
||||
"/building/meetings/invited/attendance": managementAccountTenantMain,
|
||||
};
|
||||
|
||||
export { dynamicPagesIndex };
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
import { footerDefaultEn } from "@/languages/mutual/footer/english";
|
||||
import { headerDefaultEn } from "@/languages/mutual/header/english";
|
||||
import { managementAccountEn, managementAccountFieldsEn } from "../../english";
|
||||
import { contentDefaultEn } from "@/languages/mutual/content/english";
|
||||
|
||||
const contentManagementAccountTenantSomethingEn = {
|
||||
...managementAccountFieldsEn,
|
||||
title: "Management Account Tenant Something",
|
||||
content: "Management Account Tenant Something Content",
|
||||
button: "Management Account Tenant Something Button",
|
||||
};
|
||||
const footerManagementAccountTenantSomethingEn = {
|
||||
...footerDefaultEn,
|
||||
page: "Management Account Tenant Something Footer",
|
||||
};
|
||||
const headerManagementAccountTenantSomethingEn = {
|
||||
...headerDefaultEn,
|
||||
page: "Management Account Tenant Something Header",
|
||||
};
|
||||
|
||||
const menuManagementAccountTenantSomethingEn = {
|
||||
...managementAccountEn,
|
||||
"tenant/something": "Tenant Info",
|
||||
};
|
||||
|
||||
const managementAccountTenantMainEn = {
|
||||
header: headerManagementAccountTenantSomethingEn,
|
||||
menu: menuManagementAccountTenantSomethingEn,
|
||||
content: contentManagementAccountTenantSomethingEn,
|
||||
footer: footerManagementAccountTenantSomethingEn,
|
||||
};
|
||||
|
||||
export {
|
||||
contentManagementAccountTenantSomethingEn,
|
||||
footerManagementAccountTenantSomethingEn,
|
||||
headerManagementAccountTenantSomethingEn,
|
||||
menuManagementAccountTenantSomethingEn,
|
||||
managementAccountTenantMainEn,
|
||||
};
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
import { managementAccountTenantMainTr } from "./turkish";
|
||||
import { managementAccountTenantMainEn } from "./english";
|
||||
|
||||
const managementAccountTenantMain = {
|
||||
tr: managementAccountTenantMainTr,
|
||||
en: managementAccountTenantMainEn,
|
||||
}
|
||||
|
||||
export { managementAccountTenantMain }
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
import { footerDefaultTr } from "@/languages/mutual/footer/turkish";
|
||||
import { headerDefaultTr } from "@/languages/mutual/header/turkish";
|
||||
import { managementAccountTr } from "../../turkish";
|
||||
|
||||
const contentManagementAccountTenantSomethingTr = {
|
||||
title: "Yönetim Hesap Kiracı Bilgileri",
|
||||
description: "Yönetim Hesap Kiracı Bilgileri",
|
||||
button: "Yönetim Hesap Kiracı Bilgileri Buton",
|
||||
};
|
||||
const footerManagementAccountTenantSomethingTr = {
|
||||
...footerDefaultTr,
|
||||
page: "Yönetim Hesap Kiracı Bilgileri Footer",
|
||||
};
|
||||
const headerManagementAccountTenantSomethingTr = {
|
||||
...headerDefaultTr,
|
||||
page: "Yönetim Hesap Kiracı Bilgileri Header",
|
||||
};
|
||||
|
||||
const menuManagementAccountTenantSomethingTr = {
|
||||
...managementAccountTr,
|
||||
"tenant/something": "Kiracı Bilgileri",
|
||||
};
|
||||
const managementAccountTenantMainTr = {
|
||||
header: headerManagementAccountTenantSomethingTr,
|
||||
menu: menuManagementAccountTenantSomethingTr,
|
||||
content: contentManagementAccountTenantSomethingTr,
|
||||
footer: footerManagementAccountTenantSomethingTr,
|
||||
};
|
||||
|
||||
export {
|
||||
contentManagementAccountTenantSomethingTr,
|
||||
footerManagementAccountTenantSomethingTr,
|
||||
headerManagementAccountTenantSomethingTr,
|
||||
menuManagementAccountTenantSomethingTr,
|
||||
managementAccountTenantMainTr,
|
||||
};
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
import { footerDefaultEn } from "@/languages/mutual/footer/english";
|
||||
import { headerDefaultEn } from "@/languages/mutual/header/english";
|
||||
import { contentDefaultEn } from "@/languages/mutual/content/english";
|
||||
import { managementAccountEn, managementAccountFieldsEn } from "../../english";
|
||||
|
||||
const contentManagementAccountTenantSomethingSecondEn = {
|
||||
...contentDefaultEn,
|
||||
...managementAccountFieldsEn,
|
||||
title: "Management Account Tenant Something",
|
||||
content: "Management Account Tenant Something Content",
|
||||
button: "Management Account Tenant Something Button",
|
||||
};
|
||||
const footerManagementAccountTenantSomethingSecondEn = {
|
||||
...footerDefaultEn,
|
||||
page: "Management Account Tenant Something Second Footer",
|
||||
};
|
||||
const headerManagementAccountTenantSomethingSecondEn = {
|
||||
...headerDefaultEn,
|
||||
page: "Management Account Tenant Something Second Header",
|
||||
};
|
||||
|
||||
const menuManagementAccountTenantSomethingSecondEn = {
|
||||
...managementAccountEn,
|
||||
"tenant/somethingSecond": "Tenant Info Second",
|
||||
};
|
||||
|
||||
const managementAccountTenantMainSecondEn = {
|
||||
header: headerManagementAccountTenantSomethingSecondEn,
|
||||
menu: menuManagementAccountTenantSomethingSecondEn,
|
||||
content: contentManagementAccountTenantSomethingSecondEn,
|
||||
footer: footerManagementAccountTenantSomethingSecondEn,
|
||||
};
|
||||
|
||||
export {
|
||||
contentManagementAccountTenantSomethingSecondEn,
|
||||
footerManagementAccountTenantSomethingSecondEn,
|
||||
headerManagementAccountTenantSomethingSecondEn,
|
||||
menuManagementAccountTenantSomethingSecondEn,
|
||||
managementAccountTenantMainSecondEn,
|
||||
};
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
import { managementAccountTenantMainSecondTr } from "./turkish";
|
||||
import { managementAccountTenantMainSecondEn } from "./english";
|
||||
|
||||
const managementAccountTenantMainSecond = {
|
||||
tr: managementAccountTenantMainSecondTr,
|
||||
en: managementAccountTenantMainSecondEn,
|
||||
};
|
||||
|
||||
export { managementAccountTenantMainSecond };
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
import { footerDefaultTr } from "@/languages/mutual/footer/turkish";
|
||||
import { headerDefaultTr } from "@/languages/mutual/header/turkish";
|
||||
import { managementAccountTr, managementAccountFieldsTr } from "../../turkish";
|
||||
|
||||
const contentManagementAccountTenantSomethingSecondTr = {
|
||||
...managementAccountFieldsTr,
|
||||
title: "Yönetim Hesap Kiracı Bilgileri",
|
||||
description: "Yönetim Hesap Kiracı Bilgileri",
|
||||
button: "Yönetim Hesap Kiracı Bilgileri Buton",
|
||||
};
|
||||
const footerManagementAccountTenantSomethingSecondTr = {
|
||||
...footerDefaultTr,
|
||||
page: "Yönetim Hesap Kiracı Bilgileri Footer",
|
||||
};
|
||||
const headerManagementAccountTenantSomethingSecondTr = {
|
||||
...headerDefaultTr,
|
||||
page: "Yönetim Hesap Kiracı Bilgileri Header",
|
||||
};
|
||||
|
||||
const menuManagementAccountTenantSomethingSecondTr = {
|
||||
...managementAccountTr,
|
||||
"tenant/somethingSecond": "İkinci Kiracı Bilgileri",
|
||||
};
|
||||
const managementAccountTenantMainSecondTr = {
|
||||
header: headerManagementAccountTenantSomethingSecondTr,
|
||||
menu: menuManagementAccountTenantSomethingSecondTr,
|
||||
content: contentManagementAccountTenantSomethingSecondTr,
|
||||
footer: footerManagementAccountTenantSomethingSecondTr,
|
||||
};
|
||||
|
||||
export {
|
||||
contentManagementAccountTenantSomethingSecondTr,
|
||||
footerManagementAccountTenantSomethingSecondTr,
|
||||
headerManagementAccountTenantSomethingSecondTr,
|
||||
menuManagementAccountTenantSomethingSecondTr,
|
||||
managementAccountTenantMainSecondTr,
|
||||
};
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
const managementEn = {
|
||||
management: "Management First Layer Label",
|
||||
};
|
||||
|
||||
const managementAccountEn = {
|
||||
...managementEn,
|
||||
account: "Account Second Layer Label",
|
||||
};
|
||||
|
||||
const managementAccountFieldsEn = {
|
||||
"User.firstName": "First Name",
|
||||
"User.lastName": "Last Name",
|
||||
"User.email": "Email",
|
||||
"User.phoneNumber": "Phone Number",
|
||||
"User.country": "Country",
|
||||
"User.description": "Description",
|
||||
"User.isDeleted": "Is Deleted",
|
||||
"User.isConfirmed": "Is Confirmed",
|
||||
"User.createdAt": "Created At",
|
||||
"User.updatedAt": "Updated At",
|
||||
};
|
||||
|
||||
export { managementEn, managementAccountEn, managementAccountFieldsEn };
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
const managementTr = {
|
||||
management: "Management Birinci Seviye",
|
||||
};
|
||||
const managementAccountTr = {
|
||||
...managementTr,
|
||||
account: "Account İkinci Seviye",
|
||||
};
|
||||
|
||||
const managementAccountFieldsTr = {
|
||||
"User.firstName": "Ad",
|
||||
"User.lastName": "Soyad",
|
||||
"User.email": "Email",
|
||||
"User.phoneNumber": "Telefon Numarası",
|
||||
"User.country": "Ülke",
|
||||
"User.description": "Açıklama",
|
||||
"User.isDeleted": "Silindi",
|
||||
"User.isConfirmed": "Onaylandı",
|
||||
"User.createdAt": "Oluşturulma Tarihi",
|
||||
"User.updatedAt": "Güncellenme Tarihi",
|
||||
};
|
||||
|
||||
export { managementTr, managementAccountTr, managementAccountFieldsTr };
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
const contentDefaultEn = {
|
||||
title: "Content Default",
|
||||
content: "Content Default",
|
||||
button: "Content Default",
|
||||
rows: "Rows",
|
||||
};
|
||||
|
||||
export { contentDefaultEn };
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
import { contentDefaultTr } from "./turkish";
|
||||
import { contentDefaultEn } from "./english";
|
||||
|
||||
const contentDefault = {
|
||||
tr: contentDefaultTr,
|
||||
en: contentDefaultEn,
|
||||
};
|
||||
|
||||
export { contentDefault };
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
const contentDefaultTr = {
|
||||
title: "İçerik Varsayılan",
|
||||
content: "İçerik Varsayılan",
|
||||
button: "İçerik Varsayılan",
|
||||
rows: "Satır",
|
||||
};
|
||||
|
||||
export { contentDefaultTr };
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
const dashboardTranslationEn = {
|
||||
title: "Dashboard Panel",
|
||||
};
|
||||
|
||||
export { dashboardTranslationEn };
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
import { dashboardTranslationEn } from "./english";
|
||||
import { dashboardTranslationTr } from "./turkish";
|
||||
|
||||
export const dashboardTranslation = {
|
||||
en: dashboardTranslationEn,
|
||||
tr: dashboardTranslationTr,
|
||||
};
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
const dashboardTranslationTr = {
|
||||
title: "Yönetim Panosu",
|
||||
};
|
||||
|
||||
export { dashboardTranslationTr };
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
const footerDefaultEn = {
|
||||
description: "Footer Default",
|
||||
footer: "Footer Info",
|
||||
};
|
||||
|
||||
export { footerDefaultEn };
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
const footerDefaultTr = {
|
||||
description: "Footer Bilgi",
|
||||
footer: "Alt Bilgi",
|
||||
};
|
||||
|
||||
export { footerDefaultTr };
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
const headerDefaultEn = {
|
||||
selectedPage: "Selected Page",
|
||||
};
|
||||
|
||||
export { headerDefaultEn };
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
const headerDefaultTr = {
|
||||
selectedPage: "Seçili Sayfa",
|
||||
};
|
||||
|
||||
export { headerDefaultTr };
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
const languageSelectionTranslationEn = {
|
||||
title: "Language Selection",
|
||||
english: "English",
|
||||
turkish: "Turkish",
|
||||
};
|
||||
|
||||
export { languageSelectionTranslationEn };
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
import { languageSelectionTranslationEn } from "./english";
|
||||
import { languageSelectionTranslationTr } from "./turkish";
|
||||
|
||||
export const languageSelectionTranslation = {
|
||||
en: languageSelectionTranslationEn,
|
||||
tr: languageSelectionTranslationTr
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
const languageSelectionTranslationTr = {
|
||||
title: "Dil Seçimi",
|
||||
english: "İngilizce",
|
||||
turkish: "Türkçe",
|
||||
};
|
||||
|
||||
export { languageSelectionTranslationTr };
|
||||
|
|
@ -0,0 +1,176 @@
|
|||
// const menuTranslationEn = {
|
||||
// "/definitions/identifications/people": "People",
|
||||
// "/definitions/identifications/users": "Users",
|
||||
|
||||
// "/definitions/building/parts": "Build Parts",
|
||||
// "/definitions/building/areas": "Building Areas",
|
||||
|
||||
// "/building/accounts/managment/accounts": "Management Accounts",
|
||||
// "/building/accounts/managment/budgets": "Management Budgets",
|
||||
// "/building/accounts/parts/accounts": "Parts Accounts",
|
||||
// "/building/accounts/parts/budgets": "Parts Budgets",
|
||||
|
||||
// "/building/meetings/regular/actions": "Regular Meeting Actions",
|
||||
// "/building/meetings/regular/accounts": "Regular Meeting Accounts",
|
||||
// "/building/meetings/ergunt/actions": "Ergunt Meeting Actions",
|
||||
// "/building/meetings/ergunt/accounts": "Ergunt Meeting Accounts",
|
||||
// "/building/meetings/invited/attendance": "Meeting Invited Attendance",
|
||||
// };
|
||||
|
||||
const menuTranslationEn = {
|
||||
// New menu
|
||||
"/dashboard": [
|
||||
{ value: "Dashboard", key: "dashboard" },
|
||||
{ value: "Dashboard", key: "dashboard" },
|
||||
{ value: "Dashboard", key: "dashboard" },
|
||||
],
|
||||
"/individual": [
|
||||
{ value: "Individual", key: "individual" },
|
||||
{ value: "Individual", key: "individual" },
|
||||
{ value: "Individual", key: "individual" },
|
||||
],
|
||||
"/user": [
|
||||
{ value: "User", key: "user" },
|
||||
{ value: "User", key: "user" },
|
||||
{ value: "User", key: "user" },
|
||||
],
|
||||
"/build": [
|
||||
{ value: "Build", key: "build" },
|
||||
{ value: "Build", key: "build" },
|
||||
{ value: "Build", key: "build" },
|
||||
],
|
||||
"/build/parts": [
|
||||
{ value: "Build", key: "build" },
|
||||
{ value: "Parts", key: "parts" },
|
||||
{ value: "Build", key: "build" },
|
||||
],
|
||||
"/management/budget/actions": [
|
||||
{ value: "Management", key: "management" },
|
||||
{ value: "Budget", key: "budget" },
|
||||
{ value: "Actions", key: "actions" },
|
||||
],
|
||||
"/management/budget": [
|
||||
{ value: "Management", key: "management" },
|
||||
{ value: "Budget", key: "budget" },
|
||||
{ value: "Budget", key: "budget" },
|
||||
],
|
||||
"/annual/meeting/close": [
|
||||
{ value: "Annual", key: "annual" },
|
||||
{ value: "Meeting", key: "meeting" },
|
||||
{ value: "Close", key: "close" },
|
||||
],
|
||||
"/emergency/meeting": [
|
||||
{ value: "Emergency", key: "emergency" },
|
||||
{ value: "Meeting", key: "meeting" },
|
||||
{ value: "Meeting", key: "meeting" },
|
||||
],
|
||||
"/emergency/meeting/close": [
|
||||
{ value: "Emergency", key: "emergency" },
|
||||
{ value: "Meeting", key: "meeting" },
|
||||
{ value: "Close", key: "close" },
|
||||
],
|
||||
"/tenant/accounting": [
|
||||
{ value: "Tenant", key: "tenant" },
|
||||
{ value: "Accounting", key: "accounting" },
|
||||
{ value: "Accounting", key: "accounting" },
|
||||
],
|
||||
"/meeting/participation": [
|
||||
{ value: "Meeting", key: "meeting" },
|
||||
{ value: "Participation", key: "participation" },
|
||||
{ value: "Participation", key: "participation" },
|
||||
],
|
||||
"/tenant/messageToBM": [
|
||||
{ value: "Tenant", key: "tenant" },
|
||||
{ value: "Message To BM", key: "messageToBM" },
|
||||
{ value: "Message To BM", key: "messageToBM" },
|
||||
],
|
||||
"/tenant/messageToOwner": [
|
||||
{ value: "Tenant", key: "tenant" },
|
||||
{ value: "Message To Owner", key: "messageToOwner" },
|
||||
{ value: "Message To Owner", key: "messageToOwner" },
|
||||
],
|
||||
"/management/accounting": [
|
||||
{ value: "Management", key: "management" },
|
||||
{ value: "Accounting", key: "accounting" },
|
||||
{ value: "Accounting", key: "accounting" },
|
||||
],
|
||||
"/build/area": [
|
||||
{ value: "Build", key: "build" },
|
||||
{ value: "Area", key: "area" },
|
||||
{ value: "Area", key: "area" },
|
||||
],
|
||||
"/management/budget/status": [
|
||||
{ value: "Management", key: "management" },
|
||||
{ value: "Budget", key: "budget" },
|
||||
{ value: "Status", key: "status" },
|
||||
],
|
||||
|
||||
// Early menu
|
||||
"/definitions/identifications/people": [
|
||||
{ value: "Definitions", key: "definitions" },
|
||||
{ value: "Identifications", key: "identifications" },
|
||||
{ value: "People", key: "people" },
|
||||
],
|
||||
"/definitions/identifications/users": [
|
||||
{ value: "Definitions", key: "definitions" },
|
||||
{ value: "Identifications", key: "identifications" },
|
||||
{ value: "Users", key: "users" },
|
||||
],
|
||||
"/definitions/building/parts": [
|
||||
{ value: "Definitions", key: "definitions" },
|
||||
{ value: "Building", key: "building" },
|
||||
{ value: "Parts", key: "parts" },
|
||||
],
|
||||
"/definitions/building/areas": [
|
||||
{ value: "Definitions", key: "definitions" },
|
||||
{ value: "Building", key: "building" },
|
||||
{ value: "Areas", key: "areas" },
|
||||
],
|
||||
"/building/accounts/managment/accounts": [
|
||||
{ value: "Building", key: "building" },
|
||||
{ value: "Accounts", key: "accounts" },
|
||||
{ value: "Managment", key: "managment" },
|
||||
],
|
||||
"/building/accounts/managment/budgets": [
|
||||
{ value: "Building", key: "building" },
|
||||
{ value: "Accounts", key: "accounts" },
|
||||
{ value: "Managment", key: "managment" },
|
||||
],
|
||||
"/building/accounts/parts/accounts": [
|
||||
{ value: "Building", key: "building" },
|
||||
{ value: "Accounts", key: "accounts" },
|
||||
{ value: "Parts", key: "parts" },
|
||||
],
|
||||
"/building/accounts/parts/budgets": [
|
||||
{ value: "Building", key: "building" },
|
||||
{ value: "Accounts", key: "accounts" },
|
||||
{ value: "Parts", key: "parts" },
|
||||
],
|
||||
"/building/meetings/regular/actions": [
|
||||
{ value: "Building", key: "building" },
|
||||
{ value: "Meetings", key: "meetings" },
|
||||
{ value: "Regular", key: "regular" },
|
||||
],
|
||||
"/building/meetings/regular/accounts": [
|
||||
{ value: "Building", key: "building" },
|
||||
{ value: "Meetings", key: "meetings" },
|
||||
{ value: "Regular", key: "regular" },
|
||||
],
|
||||
"/building/meetings/ergunt/actions": [
|
||||
{ value: "Building", key: "building" },
|
||||
{ value: "Meetings", key: "meetings" },
|
||||
{ value: "Ergunt", key: "ergunt" },
|
||||
],
|
||||
"/building/meetings/ergunt/accounts": [
|
||||
{ value: "Building", key: "building" },
|
||||
{ value: "Meetings", key: "meetings" },
|
||||
{ value: "Ergunt", key: "ergunt" },
|
||||
],
|
||||
"/building/meetings/invited/attendance": [
|
||||
{ value: "Building", key: "building" },
|
||||
{ value: "Meetings", key: "meetings" },
|
||||
{ value: "Invited", key: "invited" },
|
||||
],
|
||||
};
|
||||
|
||||
export { menuTranslationEn };
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
import { menuTranslationEn } from "./english";
|
||||
import { menuTranslationTr } from "./turkish";
|
||||
|
||||
export const menuTranslation = {
|
||||
en: menuTranslationEn,
|
||||
tr: menuTranslationTr,
|
||||
};
|
||||
|
|
@ -0,0 +1,176 @@
|
|||
// const menuTranslationTr = {
|
||||
// "/definitions/identifications/people": "Kişiler",
|
||||
// "/definitions/identifications/users": "Kullanıcılar",
|
||||
|
||||
// "/definitions/building/parts": "Daireler",
|
||||
// "/definitions/building/areas": "Bina Alanları",
|
||||
|
||||
// "/building/accounts/managment/accounts": "Bina Hesapları",
|
||||
// "/building/accounts/managment/budgets": "Bina Bütçesi",
|
||||
// "/building/accounts/parts/accounts": "Daire Hesapları",
|
||||
// "/building/accounts/parts/budgets": "Daire Bütçesi",
|
||||
|
||||
// "/building/meetings/regular/actions": "Düzenli Toplantı Eylemleri",
|
||||
// "/building/meetings/regular/accounts": "Düzenli Toplantı Accounts",
|
||||
// "/building/meetings/ergunt/actions": "Ergunt Toplantı Eylemleri",
|
||||
// "/building/meetings/ergunt/accounts": "Ergunt Toplantı Accounts",
|
||||
// "/building/meetings/invited/attendance": "Toplantı Davetli Katılımlar",
|
||||
// };
|
||||
|
||||
const menuTranslationTr = {
|
||||
// New menu
|
||||
"/dashboard": [
|
||||
{ value: "Dashboard", key: "dashboard" },
|
||||
{ value: "Dashboard", key: "dashboard" },
|
||||
{ value: "Dashboard", key: "dashboard" },
|
||||
],
|
||||
"/individual": [
|
||||
{ value: "Individual", key: "individual" },
|
||||
{ value: "Individual", key: "individual" },
|
||||
{ value: "Individual", key: "individual" },
|
||||
],
|
||||
"/user": [
|
||||
{ value: "User", key: "user" },
|
||||
{ value: "User", key: "user" },
|
||||
{ value: "User", key: "user" },
|
||||
],
|
||||
"/build": [
|
||||
{ value: "Build", key: "build" },
|
||||
{ value: "Build", key: "build" },
|
||||
{ value: "Build", key: "build" },
|
||||
],
|
||||
"/build/parts": [
|
||||
{ value: "Build", key: "build" },
|
||||
{ value: "Parts", key: "parts" },
|
||||
{ value: "Build", key: "build" },
|
||||
],
|
||||
"/management/budget/actions": [
|
||||
{ value: "Management", key: "management" },
|
||||
{ value: "Budget", key: "budget" },
|
||||
{ value: "Actions", key: "actions" },
|
||||
],
|
||||
"/management/budget": [
|
||||
{ value: "Management", key: "management" },
|
||||
{ value: "Budget", key: "budget" },
|
||||
{ value: "Budget", key: "budget" },
|
||||
],
|
||||
"/annual/meeting/close": [
|
||||
{ value: "Annual", key: "annual" },
|
||||
{ value: "Meeting", key: "meeting" },
|
||||
{ value: "Close", key: "close" },
|
||||
],
|
||||
"/emergency/meeting": [
|
||||
{ value: "Emergency", key: "emergency" },
|
||||
{ value: "Meeting", key: "meeting" },
|
||||
{ value: "Meeting", key: "meeting" },
|
||||
],
|
||||
"/emergency/meeting/close": [
|
||||
{ value: "Emergency", key: "emergency" },
|
||||
{ value: "Meeting", key: "meeting" },
|
||||
{ value: "Close", key: "close" },
|
||||
],
|
||||
"/tenant/accounting": [
|
||||
{ value: "Tenant", key: "tenant" },
|
||||
{ value: "Accounting", key: "accounting" },
|
||||
{ value: "Accounting", key: "accounting" },
|
||||
],
|
||||
"/meeting/participation": [
|
||||
{ value: "Meeting", key: "meeting" },
|
||||
{ value: "Participation", key: "participation" },
|
||||
{ value: "Participation", key: "participation" },
|
||||
],
|
||||
"/tenant/messageToBM": [
|
||||
{ value: "Tenant", key: "tenant" },
|
||||
{ value: "Message To BM", key: "messageToBM" },
|
||||
{ value: "Message To BM", key: "messageToBM" },
|
||||
],
|
||||
"/tenant/messageToOwner": [
|
||||
{ value: "Tenant", key: "tenant" },
|
||||
{ value: "Message To Owner", key: "messageToOwner" },
|
||||
{ value: "Message To Owner", key: "messageToOwner" },
|
||||
],
|
||||
"/management/accounting": [
|
||||
{ value: "Management", key: "management" },
|
||||
{ value: "Accounting", key: "accounting" },
|
||||
{ value: "Accounting", key: "accounting" },
|
||||
],
|
||||
"/build/area": [
|
||||
{ value: "Build", key: "build" },
|
||||
{ value: "Area", key: "area" },
|
||||
{ value: "Area", key: "area" },
|
||||
],
|
||||
"/management/budget/status": [
|
||||
{ value: "Management", key: "management" },
|
||||
{ value: "Budget", key: "budget" },
|
||||
{ value: "Status", key: "status" },
|
||||
],
|
||||
|
||||
// Early menu
|
||||
"/definitions/identifications/people": [
|
||||
{ value: "Tanımlamalar", key: "definitions" },
|
||||
{ value: "Tanımlamalar", key: "identifications" },
|
||||
{ value: "Kişiler", key: "people" },
|
||||
],
|
||||
"/definitions/identifications/users": [
|
||||
{ value: "Tanımlamalar", key: "definitions" },
|
||||
{ value: "Tanımlamalar", key: "identifications" },
|
||||
{ value: "Kullanıcılar", key: "users" },
|
||||
],
|
||||
"/definitions/building/parts": [
|
||||
{ value: "Tanımlamalar", key: "definitions" },
|
||||
{ value: "Bina", key: "building" },
|
||||
{ value: "Daireler", key: "parts" },
|
||||
],
|
||||
"/definitions/building/areas": [
|
||||
{ value: "Tanımlamalar", key: "definitions" },
|
||||
{ value: "Bina", key: "building" },
|
||||
{ value: "Bina Alanları", key: "areas" },
|
||||
],
|
||||
"/building/accounts/managment/accounts": [
|
||||
{ value: "Bina", key: "building" },
|
||||
{ value: "Hesap Eylemleri", key: "accounts" },
|
||||
{ value: "Yönetim", key: "managment" },
|
||||
],
|
||||
"/building/accounts/managment/budgets": [
|
||||
{ value: "Bina", key: "building" },
|
||||
{ value: "Hesap Eylemleri", key: "accounts" },
|
||||
{ value: "Yönetim", key: "managment" },
|
||||
],
|
||||
"/building/accounts/parts/accounts": [
|
||||
{ value: "Bina", key: "building" },
|
||||
{ value: "Hesap Eylemleri", key: "accounts" },
|
||||
{ value: "Daireler", key: "parts" },
|
||||
],
|
||||
"/building/accounts/parts/budgets": [
|
||||
{ value: "Bina", key: "building" },
|
||||
{ value: "Hesap Eylemleri", key: "accounts" },
|
||||
{ value: "Daireler", key: "parts" },
|
||||
],
|
||||
"/building/meetings/regular/actions": [
|
||||
{ value: "Bina", key: "building" },
|
||||
{ value: "Toplantılar", key: "meetings" },
|
||||
{ value: "Düzenli", key: "regular" },
|
||||
],
|
||||
"/building/meetings/regular/accounts": [
|
||||
{ value: "Bina", key: "building" },
|
||||
{ value: "Toplantılar", key: "meetings" },
|
||||
{ value: "Düzenli", key: "regular" },
|
||||
],
|
||||
"/building/meetings/ergunt/actions": [
|
||||
{ value: "Bina", key: "building" },
|
||||
{ value: "Toplantılar", key: "meetings" },
|
||||
{ value: "Acil", key: "ergunt" },
|
||||
],
|
||||
"/building/meetings/ergunt/accounts": [
|
||||
{ value: "Bina", key: "building" },
|
||||
{ value: "Toplantılar", key: "meetings" },
|
||||
{ value: "Acil", key: "ergunt" },
|
||||
],
|
||||
"/building/meetings/invited/attendance": [
|
||||
{ value: "Bina", key: "building" },
|
||||
{ value: "Toplantılar", key: "meetings" },
|
||||
{ value: "Davetli", key: "invited" },
|
||||
],
|
||||
};
|
||||
|
||||
export { menuTranslationTr };
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
const dashboardTranslationEn = {
|
||||
title: "Dashboard Panel",
|
||||
};
|
||||
|
||||
export { dashboardTranslationEn };
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
import { dashboardTranslationEn } from "./english";
|
||||
import { dashboardTranslationTr } from "./turkish";
|
||||
|
||||
export const dashboardTranslation = {
|
||||
en: dashboardTranslationEn,
|
||||
tr: dashboardTranslationTr,
|
||||
};
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
const dashboardTranslationTr = {
|
||||
title: "Yönetim Panosu",
|
||||
};
|
||||
|
||||
export { dashboardTranslationTr };
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
'use server';
|
||||
import { FC, Suspense } from "react";
|
||||
import { AuthLayoutProps } from "@/validations/mutual/auth/props";
|
||||
|
||||
const AuthLayout: FC<AuthLayoutProps> = async ({ lang, page, activePageUrl }) => {
|
||||
return (
|
||||
<div className="min-h-screen min-w-screen flex h-screen w-screen overflow-hidden">
|
||||
<div className="w-1/4">
|
||||
<div className="flex flex-col items-center justify-center h-screen bg-purple-600">
|
||||
<div className="text-2xl font-bold">WAG Frontend</div>
|
||||
<div className="text-sm text-gray-500 mt-4">Welcome to the WAG Frontend Application</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-3/4 text-black">
|
||||
<Suspense fallback={<div>Loading...</div>}>{page}</Suspense>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { AuthLayout };
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
'use client';
|
||||
import React, { FC } from 'react';
|
||||
import { ClientProviders } from "@/components/mutual/providers/client-providers";
|
||||
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";
|
||||
import { 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";
|
||||
|
||||
interface ClientLayoutProps { activePageUrl: string, searchParams: Record<string, any> }
|
||||
|
||||
const ClientLayout: FC<ClientLayoutProps> = ({ activePageUrl, searchParams }) => {
|
||||
|
||||
const { onlineData, isLoading: onlineLoading, error: onlineError, refreshOnline, updateOnline } = useOnline();
|
||||
const { userData, isLoading: userLoading, error: userError, refreshUser, updateUser } = useUser();
|
||||
const { availableApplications, isLoading: menuLoading, error: menuError, menuData, refreshMenu, updateMenu } = useMenu();
|
||||
const { selectionData, isLoading: selectionLoading, error: selectionError, refreshSelection, updateSelection } = useSelection();
|
||||
const { configData, isLoading: configLoading, error: configError, refreshConfig, updateConfig } = useConfig();
|
||||
const prefix = "/panel"
|
||||
const mode = (searchParams?.mode as ModeTypes) || 'shortList';
|
||||
console.log("onlineData", onlineData)
|
||||
|
||||
return (
|
||||
<ClientProviders>
|
||||
<div className="flex flex-col min-w-screen">
|
||||
<HeaderComponent activePageUrl={activePageUrl} searchParams={searchParams}
|
||||
onlineData={onlineData} onlineLoading={onlineLoading} onlineError={onlineError} refreshOnline={refreshOnline} updateOnline={updateOnline}
|
||||
userData={userData} userLoading={userLoading} userError={userError} refreshUser={refreshUser} updateUser={updateUser} />
|
||||
<MenuComponent availableApplications={availableApplications} activePageUrl={activePageUrl} prefix={prefix} searchParams={searchParams}
|
||||
onlineData={onlineData} onlineLoading={onlineLoading} onlineError={onlineError} refreshOnline={refreshOnline} updateOnline={updateOnline}
|
||||
userData={userData} userLoading={userLoading} userError={userError} refreshUser={refreshUser} updateUser={updateUser}
|
||||
selectionData={selectionData} selectionLoading={selectionLoading} selectionError={selectionError} refreshSelection={refreshSelection} updateSelection={updateSelection}
|
||||
menuData={menuData} menuLoading={menuLoading} menuError={menuError} refreshMenu={refreshMenu} updateMenu={updateMenu} />
|
||||
<ContentComponent activePageUrl={activePageUrl} mode={mode} searchParams={searchParams}
|
||||
userData={userData} userLoading={userLoading} userError={userError} refreshUser={refreshUser} updateUser={updateUser}
|
||||
onlineData={onlineData} onlineLoading={onlineLoading} onlineError={onlineError} refreshOnline={refreshOnline} updateOnline={updateOnline} />
|
||||
<FooterComponent activePageUrl={activePageUrl} searchParams={searchParams}
|
||||
configData={configData} configLoading={configLoading} configError={configError} refreshConfig={refreshConfig} updateConfig={updateConfig}
|
||||
onlineData={onlineData} onlineLoading={onlineLoading} onlineError={onlineError} refreshOnline={refreshOnline} updateOnline={updateOnline} />
|
||||
</div>
|
||||
</ClientProviders>
|
||||
);
|
||||
};
|
||||
|
||||
export { ClientLayout }
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue