diff --git a/ServicesWeb/customer/src/app/api/auth/a.txt b/ServicesWeb/customer/src/app/api/auth/a.txt new file mode 100644 index 0000000..e69de29 diff --git a/ServicesWeb/customer/src/app/api/auth/token/check/route.ts b/ServicesWeb/customer/src/app/api/auth/token/check/route.ts new file mode 100644 index 0000000..d55e1df --- /dev/null +++ b/ServicesWeb/customer/src/app/api/auth/token/check/route.ts @@ -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 }); +} diff --git a/ServicesWeb/customer/src/app/api/auth/token/remove/route.ts b/ServicesWeb/customer/src/app/api/auth/token/remove/route.ts new file mode 100644 index 0000000..4eafe3a --- /dev/null +++ b/ServicesWeb/customer/src/app/api/auth/token/remove/route.ts @@ -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 }); + } +} diff --git a/ServicesWeb/customer/src/app/api/context/dash/selection/route.ts b/ServicesWeb/customer/src/app/api/context/dash/selection/route.ts new file mode 100644 index 0000000..898c27e --- /dev/null +++ b/ServicesWeb/customer/src/app/api/context/dash/selection/route.ts @@ -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" }, + } + ); + } +} diff --git a/ServicesWeb/customer/src/app/api/context/dash/settings/route.ts b/ServicesWeb/customer/src/app/api/context/dash/settings/route.ts new file mode 100644 index 0000000..8ec7dfa --- /dev/null +++ b/ServicesWeb/customer/src/app/api/context/dash/settings/route.ts @@ -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" }, + } + ); + } +} diff --git a/ServicesWeb/customer/src/app/api/context/dash/user/route.ts b/ServicesWeb/customer/src/app/api/context/dash/user/route.ts new file mode 100644 index 0000000..38131ff --- /dev/null +++ b/ServicesWeb/customer/src/app/api/context/dash/user/route.ts @@ -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" }, + } + ); + } +} diff --git a/ServicesWeb/customer/src/app/api/context/page/config/route.ts b/ServicesWeb/customer/src/app/api/context/page/config/route.ts new file mode 100644 index 0000000..731c0bc --- /dev/null +++ b/ServicesWeb/customer/src/app/api/context/page/config/route.ts @@ -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" }, + } + ); + } +} diff --git a/ServicesWeb/customer/src/app/api/context/page/menu/route.ts b/ServicesWeb/customer/src/app/api/context/page/menu/route.ts new file mode 100644 index 0000000..f2f59c1 --- /dev/null +++ b/ServicesWeb/customer/src/app/api/context/page/menu/route.ts @@ -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" }, + } + ); + } +} diff --git a/ServicesWeb/customer/src/app/api/context/page/online/route.ts b/ServicesWeb/customer/src/app/api/context/page/online/route.ts new file mode 100644 index 0000000..e412b93 --- /dev/null +++ b/ServicesWeb/customer/src/app/api/context/page/online/route.ts @@ -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" }, + } + ); + } +} diff --git a/ServicesWeb/customer/src/app/api/login/email/route.ts b/ServicesWeb/customer/src/app/api/login/email/route.ts new file mode 100644 index 0000000..15e5b5a --- /dev/null +++ b/ServicesWeb/customer/src/app/api/login/email/route.ts @@ -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 { + 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" }); + } +} diff --git a/ServicesWeb/customer/src/app/api/menu/route.ts b/ServicesWeb/customer/src/app/api/menu/route.ts new file mode 100644 index 0000000..34463e2 --- /dev/null +++ b/ServicesWeb/customer/src/app/api/menu/route.ts @@ -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" }); + } +} diff --git a/ServicesWeb/customer/src/app/api/pages/route.ts b/ServicesWeb/customer/src/app/api/pages/route.ts new file mode 100644 index 0000000..5cfeb99 --- /dev/null +++ b/ServicesWeb/customer/src/app/api/pages/route.ts @@ -0,0 +1,15 @@ +import { NextResponse } from "next/server"; + +export async function POST(): Promise { + async function retrievePageToRender(): Promise { + return new Promise((resolve) => { + resolve("superUserTenantSomething"); + }); + } + + const pageToRender = await retrievePageToRender(); + return NextResponse.json({ + status: 200, + data: pageToRender, + }); +} diff --git a/ServicesWeb/customer/src/app/api/selection/employee/route.ts b/ServicesWeb/customer/src/app/api/selection/employee/route.ts new file mode 100644 index 0000000..0bb6755 --- /dev/null +++ b/ServicesWeb/customer/src/app/api/selection/employee/route.ts @@ -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 { + 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" }); + } +} diff --git a/ServicesWeb/customer/src/app/api/selection/occupant/route.ts b/ServicesWeb/customer/src/app/api/selection/occupant/route.ts new file mode 100644 index 0000000..95fa05b --- /dev/null +++ b/ServicesWeb/customer/src/app/api/selection/occupant/route.ts @@ -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 { + 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" }); + } +} diff --git a/ServicesWeb/customer/src/app/api/utils/apiOperations.ts b/ServicesWeb/customer/src/app/api/utils/apiOperations.ts new file mode 100644 index 0000000..01c55d7 --- /dev/null +++ b/ServicesWeb/customer/src/app/api/utils/apiOperations.ts @@ -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) + ); +} diff --git a/ServicesWeb/customer/src/app/api/utils/index.ts b/ServicesWeb/customer/src/app/api/utils/index.ts new file mode 100644 index 0000000..373c546 --- /dev/null +++ b/ServicesWeb/customer/src/app/api/utils/index.ts @@ -0,0 +1,4 @@ +// Export all utility functions from a single entry point +export * from './responseHandlers'; +export * from './requestHandlers'; +export * from './apiOperations'; diff --git a/ServicesWeb/customer/src/app/api/utils/requestHandlers.ts b/ServicesWeb/customer/src/app/api/utils/requestHandlers.ts new file mode 100644 index 0000000..ec8ecda --- /dev/null +++ b/ServicesWeb/customer/src/app/api/utils/requestHandlers.ts @@ -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 }; +} diff --git a/ServicesWeb/customer/src/app/api/utils/responseHandlers.ts b/ServicesWeb/customer/src/app/api/utils/responseHandlers.ts new file mode 100644 index 0000000..45cfc42 --- /dev/null +++ b/ServicesWeb/customer/src/app/api/utils/responseHandlers.ts @@ -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(data: T, status: number = 200) { + return NextResponse.json( + { + success: true, + data, + } as ApiResponse, + { 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, + { status } + ); +} + +/** + * Standard pagination response format + * @param data Array of items to return + * @param pagination Pagination information + */ +export function paginationResponse(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); +} + +/** + * Create response handler + * @param data The created item data + */ +export function createResponse(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(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); +} diff --git a/ServicesWeb/customer/src/app/api/utils/types.ts b/ServicesWeb/customer/src/app/api/utils/types.ts new file mode 100644 index 0000000..990bf90 --- /dev/null +++ b/ServicesWeb/customer/src/app/api/utils/types.ts @@ -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; +} + +/** + * Pagination response interface + */ +export interface PaginationResponse { + page: number; + size: number; + totalCount: number; + totalItems: number; + totalPages: number; + pageCount: number; + orderField: string[]; + orderType: string[]; + query: Record; + 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 { + success: boolean; + data?: T; + error?: string; +} + +/** + * Paginated API response interface + */ +export interface PaginatedApiResponse { + data: T[]; + pagination: PaginationResponse; +} + +export const collectPaginationFromApiResponse = ( + response: PaginatedApiResponse +): 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; +export type ApiHandlerBodyOnly = (body: any) => Promise; +export type ApiHandler = ApiHandlerWithRequest | ApiHandlerBodyOnly; + +/** + * List function type + */ +export type ListFunction = ( + params: PaginationParams +) => Promise>; + +/** + * Create function type + */ +export type CreateFunction = (data: any) => Promise; + +/** + * Update function type + */ +export type UpdateFunction = (id: any, data: any) => Promise; + +/** + * Delete function type + */ +export type DeleteFunction = (id: any) => Promise; diff --git a/ServicesWeb/customer/src/app/dashboard/page.tsx b/ServicesWeb/customer/src/app/dashboard/page.tsx new file mode 100644 index 0000000..462301f --- /dev/null +++ b/ServicesWeb/customer/src/app/dashboard/page.tsx @@ -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 }) { + const lang: LanguageTypes = 'en'; + return ; +} diff --git a/ServicesWeb/customer/src/app/globals.css b/ServicesWeb/customer/src/app/globals.css index dc98be7..dd7e1cf 100644 --- a/ServicesWeb/customer/src/app/globals.css +++ b/ServicesWeb/customer/src/app/globals.css @@ -1,5 +1,6 @@ @import "tailwindcss"; @import "tw-animate-css"; +@import "../styles/custom-scrollbar.css"; @custom-variant dark (&:is(.dark *)); diff --git a/ServicesWeb/customer/src/components/custom/ClientSelectionSection.tsx b/ServicesWeb/customer/src/components/custom/ClientSelectionSection.tsx new file mode 100644 index 0000000..af35383 --- /dev/null +++ b/ServicesWeb/customer/src/components/custom/ClientSelectionSection.tsx @@ -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 = ({ + selectionData, + initialSelectedClient = null, + onClientSelect +}) => { + const [selectedClient, setSelectedClient] = useState(initialSelectedClient); + const [isDropdownOpen, setIsDropdownOpen] = useState(false); + const dropdownRef = useRef(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 ( +
+ {/* Current selection that acts as dropdown trigger */} +
shouldShowAsDropdown && setIsDropdownOpen(!isDropdownOpen)}> + {selectedClient && renderOneClientSelection({ + item: selectedClient, + isSelected: true, + onClickHandler: () => shouldShowAsDropdown && setIsDropdownOpen(!isDropdownOpen) + })} +
+ + {/* Dropdown menu */} + {shouldShowAsDropdown && isDropdownOpen && ( +
+ {selectionData.selectionList + .filter((client: any) => client.uu_id !== selectedClient?.uu_id) + .map((client: any, index: number) => ( +
handleClientSelection(client)} + > + {renderOneClientSelection({ + item: client, + isSelected: false, + onClickHandler: handleClientSelection + })} +
+ )) + } +
+ )} +
+ ); +}; + +export default ClientSelectionSection; diff --git a/ServicesWeb/customer/src/components/custom/DataTable.tsx b/ServicesWeb/customer/src/components/custom/DataTable.tsx new file mode 100644 index 0000000..fef6fa9 --- /dev/null +++ b/ServicesWeb/customer/src/components/custom/DataTable.tsx @@ -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(null); + + const toggleDropdown = (index: number) => { + setActiveDropdown(prev => prev === index ? null : index); + }; + + return ( +
+
+
{title}
+
+ + +
+
+
+ + + + {columns.map((column, index) => ( + + ))} + + + + + {data.map((row, rowIndex) => ( + + {columns.map((column, colIndex) => ( + + ))} + + + ))} + +
+ {column.header} +
+ {column.cell ? column.cell(row[column.accessor], row) : row[column.accessor]} + +
+ + {activeDropdown === rowIndex && ( + + )} +
+
+
+
+ ); +} diff --git a/ServicesWeb/customer/src/components/custom/EarningsTable.tsx b/ServicesWeb/customer/src/components/custom/EarningsTable.tsx new file mode 100644 index 0000000..a5c9e0a --- /dev/null +++ b/ServicesWeb/customer/src/components/custom/EarningsTable.tsx @@ -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(false); + + // Toggle dropdown + const toggleDropdown = () => { + setActiveDropdown(!activeDropdown); + }; + + return ( +
+
+
Earnings
+
+ + {activeDropdown && ( + + )} +
+
+
+ + + + + + + + + + {items.map((item, index) => ( + + + + + + ))} + +
ServiceEarningStatus
+ + + + {item.isPositive ? '+' : '-'}{item.amount} + + + + {item.status} + +
+
+
+ ); +} diff --git a/ServicesWeb/customer/src/components/custom/OrderStats.tsx b/ServicesWeb/customer/src/components/custom/OrderStats.tsx new file mode 100644 index 0000000..3847254 --- /dev/null +++ b/ServicesWeb/customer/src/components/custom/OrderStats.tsx @@ -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(false); + const chartRef = useRef(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 ( +
+
+
Order Statistics
+
+ + {activeDropdown && ( + + )} +
+
+
+
+
+
{activeOrders}
+ ${activeAmount} +
+ Active +
+
+
+
{completedOrders}
+ +${completedAmount} +
+ Completed +
+
+
+
{canceledOrders}
+ -${canceledAmount} +
+ Canceled +
+
+
+ +
+
+ ); +} + +import { useState } from 'react'; diff --git a/ServicesWeb/customer/src/components/custom/Sidebar.tsx b/ServicesWeb/customer/src/components/custom/Sidebar.tsx new file mode 100644 index 0000000..bba1394 --- /dev/null +++ b/ServicesWeb/customer/src/components/custom/Sidebar.tsx @@ -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>({}); + + // 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 ( + <> +
+ {/* Fixed header section */} +
+ +
+ + {/* Scrollable content section */} +
+ {/* Client selection menu */} + console.log('Selected client:', client)} + /> + + {/* Add more scrollable content here if needed */} + +
+ +
+ + + {/* Overlay for mobile */} + {isSidebarOpen && ( +
+ )} + + ); +} + + diff --git a/ServicesWeb/customer/src/components/custom/StatCard.tsx b/ServicesWeb/customer/src/components/custom/StatCard.tsx new file mode 100644 index 0000000..7d2e274 --- /dev/null +++ b/ServicesWeb/customer/src/components/custom/StatCard.tsx @@ -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 ( +
+
+
+
+
{count}
+
+ {isPositive ? '+' : '-'}{percentage} +
+
+
{title}
+
+
+ +
+
+
+
+ {percentage} {isPositive ? 'increase' : 'decrease'} +
+
from last week
+
+
+ ); +} diff --git a/ServicesWeb/customer/src/components/custom/UserProfileSection.tsx b/ServicesWeb/customer/src/components/custom/UserProfileSection.tsx new file mode 100644 index 0000000..2fe69a5 --- /dev/null +++ b/ServicesWeb/customer/src/components/custom/UserProfileSection.tsx @@ -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 = ({ userData, onlineData }) => { + if (!userData) return null; + return ( +
+
+
+
+
+ {userData && userData.avatar ? ( + Avatar + ) : ( +
+
{userData?.email ? userData.email.slice(0, 2).toUpperCase() : 'U'}
+
+ )} +
+
+

{userData?.person ? `${userData.person.firstname} ${userData.person.surname}` : 'User'}

+

{userData?.email || 'No email'}

+

{onlineData?.userType || 'guest'}

+
+
+
+
+
+ ); +}; + +export default UserProfileSection; diff --git a/ServicesWeb/customer/src/components/custom/a.txt b/ServicesWeb/customer/src/components/custom/a.txt new file mode 100644 index 0000000..e69de29 diff --git a/ServicesWeb/customer/src/components/custom/content/DashboardPage.tsx b/ServicesWeb/customer/src/components/custom/content/DashboardPage.tsx new file mode 100644 index 0000000..6b37bf5 --- /dev/null +++ b/ServicesWeb/customer/src/components/custom/content/DashboardPage.tsx @@ -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; + 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 = ({ + 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) => ( + + {value} + + ) + } + ]; + + 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 ( +
+ {/* User info section */} + {userData && ( +
+

User Information

+

User Type: {userData.user_tag || 'N/A'}

+ {userData.person && ( +

Name: {userData.person.firstname} {userData.person.surname}

+ )} +
+ )} + + {/* Stat cards */} +
+ {statCardsData.map((card, index) => ( + + ))} +
+ + {/* Tables */} +
+ + +
+ + {/* Order Stats and Earnings */} +
+ + +
+
+ ); +}; diff --git a/ServicesWeb/customer/src/components/custom/content/PageToBeChildrendMulti.tsx b/ServicesWeb/customer/src/components/custom/content/PageToBeChildrendMulti.tsx new file mode 100644 index 0000000..d5a43be --- /dev/null +++ b/ServicesWeb/customer/src/components/custom/content/PageToBeChildrendMulti.tsx @@ -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 = ({ + activePageUrl, + userData, + userLoading, + userError, + onlineData, + onlineLoading, + onlineError, + searchParams, + refreshOnline, + updateOnline, + refreshUser, + updateUser, +}) => { + const pageComponents = pageIndexMulti[activePageUrl]; + if (!pageComponents) { return } + const ComponentKey = Object.keys(pageComponents)[0]; + const PageComponent = pageComponents[ComponentKey]; + if (!PageComponent) { return } + return ; +} + +export default PageToBeChildrendMulti diff --git a/ServicesWeb/customer/src/components/custom/content/PageToBeChildrendSingle.tsx b/ServicesWeb/customer/src/components/custom/content/PageToBeChildrendSingle.tsx new file mode 100644 index 0000000..4cf93a8 --- /dev/null +++ b/ServicesWeb/customer/src/components/custom/content/PageToBeChildrendSingle.tsx @@ -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 = async ({ lang, translations, activePageUrl, mode }) => { + const ApplicationToRender = await resolveWhichPageToRenderSingle({ activePageUrl }) + if (ApplicationToRender) { + return + } + return +} + +export default PageToBeChildrendSingle diff --git a/ServicesWeb/customer/src/components/custom/content/component.tsx b/ServicesWeb/customer/src/components/custom/content/component.tsx new file mode 100644 index 0000000..721b1a6 --- /dev/null +++ b/ServicesWeb/customer/src/components/custom/content/component.tsx @@ -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 }) => ( +
+

{translations[lang].contentLoading}

+

{translations[lang].contentLoadingDescription}

+
+

{translations[lang].pageUrl}: {activePageUrl}

+

{translations[lang].language}: {lang}

+
+
+)); + +const ContentComponent: FC = ({ + searchParams, activePageUrl, mode, userData, userLoading, userError, refreshUser, updateUser, + onlineData, onlineLoading, onlineError, refreshOnline, updateOnline, +}) => { + const loadingContent = ; + 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
{loadingContent}
} + if (userError) { + return ( +
+

{translations[lang].errorLoadingContent}

{userError}

+
+ ) + } + return ( +
+ +
+

{translations[lang].contentArea}

+ {(!userData) && ()} + +
+
+
+ ); +}; + +export default ContentComponent; + + +// {userData && ( +//
+// {/* User Information */} +//

User Information

+//

User Type: {userData.user_tag || 'N/A'}

+// {userData.person && ( +//

Name: {userData.person.firstname} {userData.person.surname}

+// )} +//
+// )} + +// {selectionData && ( +//
+// {/* Selection Information */} +//

Selection Information

+//

Current Page: {activePageUrl || 'Home'}

+//

Mode: {modeFromQuery}

+//
+// )} \ No newline at end of file diff --git a/ServicesWeb/customer/src/components/custom/footer/component.tsx b/ServicesWeb/customer/src/components/custom/footer/component.tsx new file mode 100644 index 0000000..fbdd45b --- /dev/null +++ b/ServicesWeb/customer/src/components/custom/footer/component.tsx @@ -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 = ({ + activePageUrl, configData, configLoading, configError, + onlineData, onlineLoading, onlineError +}) => { + // Use the config context hook + const lang = onlineData?.lang as LanguageTypes || 'en'; + + return ( +
+
+
+ {!configLoading && configData && ( + Theme: {configData.theme || 'Default'} + )} +
+
+

{langGetKey(translations[lang], "footer")}: {langGetKey(translations[lang], "page")}

+
+
+ {!configLoading && configData && ( + Text Size: {configData.textFont || 'Default'} + )} +
+
+
+ ); +}; + +export default FooterComponent; diff --git a/ServicesWeb/customer/src/components/custom/header/Navbar.tsx b/ServicesWeb/customer/src/components/custom/header/Navbar.tsx new file mode 100644 index 0000000..140dbf3 --- /dev/null +++ b/ServicesWeb/customer/src/components/custom/header/Navbar.tsx @@ -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 ( +
+ + +
    + {/* Search dropdown */} +
  • + + + + + +
    +
    + + +
    +
    +
    +
    +
  • + + {/* Notifications dropdown */} +
  • + + + + + +
    + + +
    +
    + {activeNotificationTab === 'notifications' ? ( +
    + {[...Array(5)].map((_, i) => ( + +
    +
    N
    +
    +
    New order
    +
    from John Doe
    +
    +
    5 min ago
    +
    +
    + ))} +
    + ) : ( +
    + {[...Array(5)].map((_, i) => ( + +
    +
    J
    +
    +
    John Doe
    +
    Hello there!
    +
    +
    5 min ago
    +
    +
    + ))} +
    + )} +
    +
    +
    +
  • + + {/* Fullscreen button */} +
  • + +
  • + + {/* Profile dropdown */} +
  • + + + + + + Profile + Settings + + Logout + + +
  • +
+
+ ); +} diff --git a/ServicesWeb/customer/src/components/custom/header/component.tsx b/ServicesWeb/customer/src/components/custom/header/component.tsx new file mode 100644 index 0000000..0c14b3c --- /dev/null +++ b/ServicesWeb/customer/src/components/custom/header/component.tsx @@ -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 = ({ + activePageUrl, onlineData, onlineLoading, onlineError, refreshOnline, updateOnline, + userData, userLoading, userError, refreshUser, updateUser +}) => { + const lang = onlineData?.lang as LanguageTypes || 'en'; + return ( +
+
+

{langGetKey(translations[lang], 'selectedPage')} :

+

{activePageUrl || langGetKey(translations[lang], 'page')}

+
+
+ {!onlineLoading && onlineData && onlineData.userType && ( +
+ {lang} + {onlineData.userType} +
+ )} +
+
+ ); +}; + +export default HeaderComponent; diff --git a/ServicesWeb/customer/src/components/custom/menu/clientSelectionSection.tsx b/ServicesWeb/customer/src/components/custom/menu/clientSelectionSection.tsx new file mode 100644 index 0000000..adf588f --- /dev/null +++ b/ServicesWeb/customer/src/components/custom/menu/clientSelectionSection.tsx @@ -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 = ({ + selectionData, + initialSelectedClient = null, + onClientSelect +}) => { + const [selectedClient, setSelectedClient] = useState(initialSelectedClient); + if (!selectionData || !selectionData.selectionList || selectionData.selectionList.length === 0) { return null } + const handleClientSelection = (client: any) => { setSelectedClient(client); if (onClientSelect) { onClientSelect(client) } }; + + return ( +
+ {selectionData.selectionList.map((client: any, index: number) => { + return ( +
handleClientSelection(client)} + > + {client && renderOneClientSelection({ + item: client, + isSelected: selectedClient?.uu_id === client.uu_id, + onClickHandler: handleClientSelection + })} +
+ ); + })} +
+ ); +}; + +export default ClientSelectionSection; diff --git a/ServicesWeb/customer/src/components/custom/menu/component.tsx b/ServicesWeb/customer/src/components/custom/menu/component.tsx new file mode 100644 index 0000000..03d11d0 --- /dev/null +++ b/ServicesWeb/customer/src/components/custom/menu/component.tsx @@ -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 = ({ + activePageUrl, availableApplications, prefix, + onlineData, onlineLoading, onlineError, + userData, userLoading, userError, + selectionData, selectionLoading, selectionError, + menuData, menuLoading, menuError +}) => { + if (menuLoading) { return } // Render loading state + if (menuError) { return ; } // Render error state + if (availableApplications.length === 0) { return ; } // Render empty state + function handleClientSelection(client: any) { console.log('Client selected:', client) } + const lang = onlineData?.lang as LanguageTypes || 'en'; + return ( +
+
+ {/* User Profile Section */} +
}> + + + {/* Client Selection Section */} +
}> + + + {/* Menu Items Section */} + }> + + + + + ); +}; + +export default MenuComponent; diff --git a/ServicesWeb/customer/src/components/custom/menu/firstLayerComponent.tsx b/ServicesWeb/customer/src/components/custom/menu/firstLayerComponent.tsx new file mode 100644 index 0000000..8101850 --- /dev/null +++ b/ServicesWeb/customer/src/components/custom/menu/firstLayerComponent.tsx @@ -0,0 +1,41 @@ +'use client'; +import { FC } from "react"; + +interface FirstLayerDropdownProps { + isActive: boolean; + isExpanded: boolean; + innerText: string; + onClick: () => void; +} + +const FirstLayerDropdown: FC = ({ 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 ( +
+ {innerText} + {isExpanded ? ( + + + + ) : ( + + + + )} +
+ ); +}; + +export default FirstLayerDropdown; diff --git a/ServicesWeb/customer/src/components/custom/menu/menuEmptyState.tsx b/ServicesWeb/customer/src/components/custom/menu/menuEmptyState.tsx new file mode 100644 index 0000000..4ca56dd --- /dev/null +++ b/ServicesWeb/customer/src/components/custom/menu/menuEmptyState.tsx @@ -0,0 +1,18 @@ +'use client'; + +import { FC } from "react"; + +const MenuEmptyState: FC = () => { + return ( +
+
+
+

No menu items available

+

Please check your permissions or contact an administrator

+
+
+
+ ); +}; + +export default MenuEmptyState; diff --git a/ServicesWeb/customer/src/components/custom/menu/menuErrorState.tsx b/ServicesWeb/customer/src/components/custom/menu/menuErrorState.tsx new file mode 100644 index 0000000..b694634 --- /dev/null +++ b/ServicesWeb/customer/src/components/custom/menu/menuErrorState.tsx @@ -0,0 +1,28 @@ +'use client'; + +import { FC } from "react"; + +interface MenuErrorStateProps { + error: string | null; +} + +const MenuErrorState: FC = ({ error }) => { + return ( +
+
+
+ Error loading menu: + {error} + +
+
+
+ ); +}; + +export default MenuErrorState; diff --git a/ServicesWeb/customer/src/components/custom/menu/menuItemsSection.tsx b/ServicesWeb/customer/src/components/custom/menu/menuItemsSection.tsx new file mode 100644 index 0000000..4d25a1c --- /dev/null +++ b/ServicesWeb/customer/src/components/custom/menu/menuItemsSection.tsx @@ -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; +type SecondLayerItems = Record; +type FirstLayerItems = Record; +type MenuStructure = FirstLayerItems; + +interface MenuItemsSectionProps { + availableApplications: string[]; + activePageUrl: string; + lang: string; + prefix?: string; +} + +const menuStaticTranslation = { + tr: { menu: "Menü" }, + en: { menu: "Menu" } +} + +const MenuItemsSection: FC = ({ availableApplications, activePageUrl, lang, prefix }) => { + const [expandedFirstLayer, setExpandedFirstLayer] = useState(null); + const [expandedSecondLayer, setExpandedSecondLayer] = useState(null); + const [menuStructure, setMenuStructure] = useState({}); + + 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
; + }); + }; + 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 ( +
+ handleSecondLayerClick(secondLayerKey)} /> + {isExpanded &&
{renderThirdLayerItems(firstLayerKey, secondLayerKey, thirdLayerItems)}
} +
+ ); + }); + }; + 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 ( +
+ handleFirstLayerClick(firstLayerKey)} /> + {isExpanded &&
{renderSecondLayerItems(firstLayerKey, secondLayerItems)}
} +
+ ); + }); + }; + return

{menuStaticTranslation[lang as keyof typeof menuStaticTranslation].menu}

{renderFirstLayerItems()}
; +}; + +export default MenuItemsSection; diff --git a/ServicesWeb/customer/src/components/custom/menu/menuLoadingState.tsx b/ServicesWeb/customer/src/components/custom/menu/menuLoadingState.tsx new file mode 100644 index 0000000..451a4b5 --- /dev/null +++ b/ServicesWeb/customer/src/components/custom/menu/menuLoadingState.tsx @@ -0,0 +1,20 @@ +'use client'; + +import { FC } from "react"; + +const MenuLoadingState: FC = () => { + return ( +
+
+
+
+
+
+
Loading menu...
+
+
+
+ ); +}; + +export default MenuLoadingState; diff --git a/ServicesWeb/customer/src/components/custom/menu/renderOneClientSelection.tsx b/ServicesWeb/customer/src/components/custom/menu/renderOneClientSelection.tsx new file mode 100644 index 0000000..413638d --- /dev/null +++ b/ServicesWeb/customer/src/components/custom/menu/renderOneClientSelection.tsx @@ -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 = ({ item, isSelected, onClickHandler }) => { + if (isSelected) { + return ( +
{ onClickHandler(item) }} + className="w-full text-xs bg-white shadow rounded-lg overflow-hidden transition-all hover:shadow-md mb-2 cursor-pointer"> +
+
+
+
+ {item.avatar ? (Company) : + (
{(item.public_name || "No Name").slice(0, 2)}
)} +
+
+ +
+
+
+

{item.public_name} {item.company_type}

+

{item.duty}

+
+
+
+
+ ) + } + return ( +
+
+
+
+
+ {item.avatar ? (Company) : + (
{(item.duty || "No Duty").slice(0, 2)}
)} +
+
+ +
+
+
+

{item.public_name} {item.company_type}

+

{item.duty}

+
+
+
+
+ ) +} + +export default RenderOneClientSelection; diff --git a/ServicesWeb/customer/src/components/custom/menu/secondLayerComponent.tsx b/ServicesWeb/customer/src/components/custom/menu/secondLayerComponent.tsx new file mode 100644 index 0000000..b6abf98 --- /dev/null +++ b/ServicesWeb/customer/src/components/custom/menu/secondLayerComponent.tsx @@ -0,0 +1,41 @@ +'use client'; +import { FC } from "react"; + +interface SecondLayerDropdownProps { + isActive: boolean; + isExpanded: boolean; + innerText: string; + onClick: () => void; +} + +const SecondLayerDropdown: FC = ({ 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 ( +
+ {innerText} + {isExpanded ? ( + + + + ) : ( + + + + )} +
+ ); +}; + +export default SecondLayerDropdown; \ No newline at end of file diff --git a/ServicesWeb/customer/src/components/custom/menu/thirdLayerComponent.tsx b/ServicesWeb/customer/src/components/custom/menu/thirdLayerComponent.tsx new file mode 100644 index 0000000..304a69e --- /dev/null +++ b/ServicesWeb/customer/src/components/custom/menu/thirdLayerComponent.tsx @@ -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 = ({ isActive, innerText, url }) => { + const [isLoading, setIsLoading] = useState(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 ( +
+ {innerText} +
+ ); + } else if (isLoading) { + return ( +
+ + {innerText} +
+ ); + } else { + return ( + setIsLoading(true)} className="block"> +
+ {innerText} +
+ + ); + } +}; + +export default ThirdLayerDropdown; diff --git a/ServicesWeb/customer/src/components/custom/menu/type.ts b/ServicesWeb/customer/src/components/custom/menu/type.ts new file mode 100644 index 0000000..6f5c88c --- /dev/null +++ b/ServicesWeb/customer/src/components/custom/menu/type.ts @@ -0,0 +1,5 @@ +interface IntrerfaceLayerDropdown { + isActive: boolean; + innerText: string; + onClick: () => void; +} diff --git a/ServicesWeb/customer/src/components/custom/menu/userProfileComponent.tsx b/ServicesWeb/customer/src/components/custom/menu/userProfileComponent.tsx new file mode 100644 index 0000000..07e41ed --- /dev/null +++ b/ServicesWeb/customer/src/components/custom/menu/userProfileComponent.tsx @@ -0,0 +1,36 @@ + +import { ClientUser } from "@/types/mutual/context/validations"; +import { FC } from "react"; + +interface Props { + userProfile: ClientUser; +} + +const UserProfile: FC = ({ userProfile }) => { + if (!userProfile || !userProfile.person) return; + const profileuser: ClientUser = JSON.parse(JSON.stringify(userProfile)); + return ( +
+
+
+
+ Avatar +
+

Profile Info

+
+
+
+
+ Email: + {profileuser.email} +
+
+ Full Name: + {profileuser.person.firstname} {profileuser.person.surname} +
+
+
+ ) +} + +export default UserProfile; diff --git a/ServicesWeb/customer/src/components/custom/menu/userProfileSection.tsx b/ServicesWeb/customer/src/components/custom/menu/userProfileSection.tsx new file mode 100644 index 0000000..a833251 --- /dev/null +++ b/ServicesWeb/customer/src/components/custom/menu/userProfileSection.tsx @@ -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 = ({ userData, onlineData }) => { + if (!userData) return null; + return ( +
+
+
+
+
+ {userData && userData.avatar ? (Avatar) : ( +
+
{userData?.email ? userData.email.slice(0, 2).toUpperCase() : 'U'}
+
+ )} +
+
+

{userData?.person ? `${userData.person.firstname} ${userData.person.surname}` : 'User'}

+

{userData?.email || 'No email'}

+

{onlineData?.userType || 'guest'}

+
+
+
+
+
+ ); +}; + +export default UserProfileSection; diff --git a/ServicesWeb/customer/src/components/custom/renderOneClientSelection.tsx b/ServicesWeb/customer/src/components/custom/renderOneClientSelection.tsx new file mode 100644 index 0000000..e9f5db5 --- /dev/null +++ b/ServicesWeb/customer/src/components/custom/renderOneClientSelection.tsx @@ -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 ( +
onClickHandler(item)} + > +
+
+
+ {item.logo ? ( + Logo + ) : ( +
+
{item.name ? item.name.slice(0, 2).toUpperCase() : 'C'}
+
+ )} +
+
+

{item.name || 'Client'}

+

{item.description || 'No description'}

+
+ {isSelected && ( +
+
+ + + +
+
+ )} +
+
+
+ ); +}; + +export default renderOneClientSelection; diff --git a/ServicesWeb/customer/src/components/mutual/context/config/context.ts b/ServicesWeb/customer/src/components/mutual/context/config/context.ts new file mode 100644 index 0000000..7da98c3 --- /dev/null +++ b/ServicesWeb/customer/src/components/mutual/context/config/context.ts @@ -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 { + 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({ + 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; + updateConfig: (newConfig: ClientPageConfig) => Promise; +} + +// 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 }; diff --git a/ServicesWeb/customer/src/components/mutual/context/hookFactory.ts b/ServicesWeb/customer/src/components/mutual/context/hookFactory.ts new file mode 100644 index 0000000..819e985 --- /dev/null +++ b/ServicesWeb/customer/src/components/mutual/context/hookFactory.ts @@ -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 { + // 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; + + // Custom update function for setting data + customUpdate?: (newData: T) => Promise; + + // Default value to use when data is not available + defaultValue?: T; +} + +interface UseContextHookResult { + data: T | null; + availableItems: string[]; + isLoading: boolean; + error: string | null; + refresh: () => Promise; + update: (newData: T) => Promise; +} + +/** + * Factory function to create a custom hook for any context type + */ +export function createContextHook(options: UseContextHookOptions) { + 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 { + 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 { + 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 { + const [data, setData] = useState(defaultValue || null); + const [availableItems, setAvailableItems] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + + const [selectedClient, setSelectedClient] = useState(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 => { + 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, + }; + }; +} diff --git a/ServicesWeb/customer/src/components/mutual/context/menu/context.ts b/ServicesWeb/customer/src/components/mutual/context/menu/context.ts new file mode 100644 index 0000000..842e494 --- /dev/null +++ b/ServicesWeb/customer/src/components/mutual/context/menu/context.ts @@ -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({ + 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; + updateMenu: (newMenu: ClientMenu) => Promise; +} + +// 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 }; diff --git a/ServicesWeb/customer/src/components/mutual/context/online/context.ts b/ServicesWeb/customer/src/components/mutual/context/online/context.ts new file mode 100644 index 0000000..57ab813 --- /dev/null +++ b/ServicesWeb/customer/src/components/mutual/context/online/context.ts @@ -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 { + 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 { + 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({ + 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; + updateOnline: (newOnline: ClientOnline) => Promise; +} + +// 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 }; diff --git a/ServicesWeb/customer/src/components/mutual/context/online/provider.tsx b/ServicesWeb/customer/src/components/mutual/context/online/provider.tsx new file mode 100644 index 0000000..40e405b --- /dev/null +++ b/ServicesWeb/customer/src/components/mutual/context/online/provider.tsx @@ -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; + isLoading: boolean; + error: string | null; + retryFetch: () => Promise; +} + +const OnlineContext = createContext({ + 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 = ({ children }) => { + const [online, setOnline] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + const [retryCount, setRetryCount] = useState(0); + const [lastRetryTime, setLastRetryTime] = useState(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 => { + 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 ( + + {children} + + ); +}; + +// Export as default for backward compatibility +export default OnlineProvider; diff --git a/ServicesWeb/customer/src/components/mutual/context/selection/context.ts b/ServicesWeb/customer/src/components/mutual/context/selection/context.ts new file mode 100644 index 0000000..3276a4e --- /dev/null +++ b/ServicesWeb/customer/src/components/mutual/context/selection/context.ts @@ -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({ + 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; + updateSelection: (newSelection: ClientSelection) => Promise; +} + +// 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 }; diff --git a/ServicesWeb/customer/src/components/mutual/context/user/context.ts b/ServicesWeb/customer/src/components/mutual/context/user/context.ts new file mode 100644 index 0000000..1582dff --- /dev/null +++ b/ServicesWeb/customer/src/components/mutual/context/user/context.ts @@ -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 { + 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 { + 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({ + 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; + updateUser: (newUser: ClientUser) => Promise; +} + +// 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 }; diff --git a/ServicesWeb/customer/src/components/mutual/languageSelection/component.tsx b/ServicesWeb/customer/src/components/mutual/languageSelection/component.tsx new file mode 100644 index 0000000..bf3b9cd --- /dev/null +++ b/ServicesWeb/customer/src/components/mutual/languageSelection/component.tsx @@ -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; + updateOnline: (newOnline: any) => Promise; +} + +const LanguageSelectionComponent: React.FC = ({ 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 ( +
+ + + + + + {languageButtons.map((props, index) => ( + + ))} + + +
+ ); +}; + +export default LanguageSelectionComponent; diff --git a/ServicesWeb/customer/src/components/mutual/languageSelection/languageItem.tsx b/ServicesWeb/customer/src/components/mutual/languageSelection/languageItem.tsx new file mode 100644 index 0000000..baa10e6 --- /dev/null +++ b/ServicesWeb/customer/src/components/mutual/languageSelection/languageItem.tsx @@ -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, + updateOnline: (newOnline: any) => Promise +} + +const RenderButtonComponent: FC = ( + { 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 ( + + {innerText} + + ) +} + +const LanguageSelectionItem: React.FC = ({ 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 ? ( +
+ ) : ( + + {innerText} + + )} + + ) +} + +export default LanguageSelectionItem; diff --git a/ServicesWeb/customer/src/components/mutual/loader/component.tsx b/ServicesWeb/customer/src/components/mutual/loader/component.tsx new file mode 100644 index 0000000..8f74631 --- /dev/null +++ b/ServicesWeb/customer/src/components/mutual/loader/component.tsx @@ -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 <> +
+
+
+} + +export default LoadingContent \ No newline at end of file diff --git a/ServicesWeb/customer/src/components/mutual/providers/client-providers.tsx b/ServicesWeb/customer/src/components/mutual/providers/client-providers.tsx new file mode 100644 index 0000000..3df6cda --- /dev/null +++ b/ServicesWeb/customer/src/components/mutual/providers/client-providers.tsx @@ -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 ( + + {children} + + ); +} + +export default ClientProviders; diff --git a/ServicesWeb/customer/src/fetchers/types/context/complete/validations.ts b/ServicesWeb/customer/src/fetchers/types/context/complete/validations.ts index 118b204..de6660a 100644 --- a/ServicesWeb/customer/src/fetchers/types/context/complete/validations.ts +++ b/ServicesWeb/customer/src/fetchers/types/context/complete/validations.ts @@ -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; diff --git a/ServicesWeb/customer/src/hooks/a.txt b/ServicesWeb/customer/src/hooks/a.txt new file mode 100644 index 0000000..e69de29 diff --git a/ServicesWeb/customer/src/hooks/application/hook.tsx b/ServicesWeb/customer/src/hooks/application/hook.tsx new file mode 100644 index 0000000..c0a4a79 --- /dev/null +++ b/ServicesWeb/customer/src/hooks/application/hook.tsx @@ -0,0 +1,29 @@ +export async function retrieveAvailableApplications(data: any): Promise { + 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 { + 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 } +} diff --git a/ServicesWeb/customer/src/languages/a.txt b/ServicesWeb/customer/src/languages/a.txt new file mode 100644 index 0000000..e69de29 diff --git a/ServicesWeb/customer/src/languages/custom/building/english.ts b/ServicesWeb/customer/src/languages/custom/building/english.ts new file mode 100644 index 0000000..e38d477 --- /dev/null +++ b/ServicesWeb/customer/src/languages/custom/building/english.ts @@ -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 }; diff --git a/ServicesWeb/customer/src/languages/custom/building/turkish.ts b/ServicesWeb/customer/src/languages/custom/building/turkish.ts new file mode 100644 index 0000000..bd65725 --- /dev/null +++ b/ServicesWeb/customer/src/languages/custom/building/turkish.ts @@ -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 }; diff --git a/ServicesWeb/customer/src/languages/custom/index.ts b/ServicesWeb/customer/src/languages/custom/index.ts new file mode 100644 index 0000000..f1ea90c --- /dev/null +++ b/ServicesWeb/customer/src/languages/custom/index.ts @@ -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> = { + "/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 }; diff --git a/ServicesWeb/customer/src/languages/custom/management/a.txt b/ServicesWeb/customer/src/languages/custom/management/a.txt new file mode 100644 index 0000000..e69de29 diff --git a/ServicesWeb/customer/src/languages/custom/management/account/a copy.txt b/ServicesWeb/customer/src/languages/custom/management/account/a copy.txt new file mode 100644 index 0000000..e69de29 diff --git a/ServicesWeb/customer/src/languages/custom/management/account/tenantSomething/english.ts b/ServicesWeb/customer/src/languages/custom/management/account/tenantSomething/english.ts new file mode 100644 index 0000000..4b7f805 --- /dev/null +++ b/ServicesWeb/customer/src/languages/custom/management/account/tenantSomething/english.ts @@ -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, +}; diff --git a/ServicesWeb/customer/src/languages/custom/management/account/tenantSomething/index.ts b/ServicesWeb/customer/src/languages/custom/management/account/tenantSomething/index.ts new file mode 100644 index 0000000..c9da0fe --- /dev/null +++ b/ServicesWeb/customer/src/languages/custom/management/account/tenantSomething/index.ts @@ -0,0 +1,9 @@ +import { managementAccountTenantMainTr } from "./turkish"; +import { managementAccountTenantMainEn } from "./english"; + +const managementAccountTenantMain = { + tr: managementAccountTenantMainTr, + en: managementAccountTenantMainEn, +} + +export { managementAccountTenantMain } diff --git a/ServicesWeb/customer/src/languages/custom/management/account/tenantSomething/turkish.ts b/ServicesWeb/customer/src/languages/custom/management/account/tenantSomething/turkish.ts new file mode 100644 index 0000000..7253171 --- /dev/null +++ b/ServicesWeb/customer/src/languages/custom/management/account/tenantSomething/turkish.ts @@ -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, +}; diff --git a/ServicesWeb/customer/src/languages/custom/management/account/tenantSomethingSecond/english.ts b/ServicesWeb/customer/src/languages/custom/management/account/tenantSomethingSecond/english.ts new file mode 100644 index 0000000..3c4a97b --- /dev/null +++ b/ServicesWeb/customer/src/languages/custom/management/account/tenantSomethingSecond/english.ts @@ -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, +}; diff --git a/ServicesWeb/customer/src/languages/custom/management/account/tenantSomethingSecond/index.ts b/ServicesWeb/customer/src/languages/custom/management/account/tenantSomethingSecond/index.ts new file mode 100644 index 0000000..2fb2256 --- /dev/null +++ b/ServicesWeb/customer/src/languages/custom/management/account/tenantSomethingSecond/index.ts @@ -0,0 +1,9 @@ +import { managementAccountTenantMainSecondTr } from "./turkish"; +import { managementAccountTenantMainSecondEn } from "./english"; + +const managementAccountTenantMainSecond = { + tr: managementAccountTenantMainSecondTr, + en: managementAccountTenantMainSecondEn, +}; + +export { managementAccountTenantMainSecond }; diff --git a/ServicesWeb/customer/src/languages/custom/management/account/tenantSomethingSecond/turkish.ts b/ServicesWeb/customer/src/languages/custom/management/account/tenantSomethingSecond/turkish.ts new file mode 100644 index 0000000..5b29633 --- /dev/null +++ b/ServicesWeb/customer/src/languages/custom/management/account/tenantSomethingSecond/turkish.ts @@ -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, +}; diff --git a/ServicesWeb/customer/src/languages/custom/management/english.ts b/ServicesWeb/customer/src/languages/custom/management/english.ts new file mode 100644 index 0000000..d511705 --- /dev/null +++ b/ServicesWeb/customer/src/languages/custom/management/english.ts @@ -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 }; diff --git a/ServicesWeb/customer/src/languages/custom/management/turkish.ts b/ServicesWeb/customer/src/languages/custom/management/turkish.ts new file mode 100644 index 0000000..eda9cac --- /dev/null +++ b/ServicesWeb/customer/src/languages/custom/management/turkish.ts @@ -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 }; diff --git a/ServicesWeb/customer/src/languages/mutual/content/english.ts b/ServicesWeb/customer/src/languages/mutual/content/english.ts new file mode 100644 index 0000000..f96ec3a --- /dev/null +++ b/ServicesWeb/customer/src/languages/mutual/content/english.ts @@ -0,0 +1,8 @@ +const contentDefaultEn = { + title: "Content Default", + content: "Content Default", + button: "Content Default", + rows: "Rows", +}; + +export { contentDefaultEn }; diff --git a/ServicesWeb/customer/src/languages/mutual/content/index.ts b/ServicesWeb/customer/src/languages/mutual/content/index.ts new file mode 100644 index 0000000..3cbbc5d --- /dev/null +++ b/ServicesWeb/customer/src/languages/mutual/content/index.ts @@ -0,0 +1,9 @@ +import { contentDefaultTr } from "./turkish"; +import { contentDefaultEn } from "./english"; + +const contentDefault = { + tr: contentDefaultTr, + en: contentDefaultEn, +}; + +export { contentDefault }; diff --git a/ServicesWeb/customer/src/languages/mutual/content/turkish.ts b/ServicesWeb/customer/src/languages/mutual/content/turkish.ts new file mode 100644 index 0000000..537a002 --- /dev/null +++ b/ServicesWeb/customer/src/languages/mutual/content/turkish.ts @@ -0,0 +1,8 @@ +const contentDefaultTr = { + title: "İçerik Varsayılan", + content: "İçerik Varsayılan", + button: "İçerik Varsayılan", + rows: "Satır", +}; + +export { contentDefaultTr }; diff --git a/ServicesWeb/customer/src/languages/mutual/dashboard/english.ts b/ServicesWeb/customer/src/languages/mutual/dashboard/english.ts new file mode 100644 index 0000000..540f3d7 --- /dev/null +++ b/ServicesWeb/customer/src/languages/mutual/dashboard/english.ts @@ -0,0 +1,5 @@ +const dashboardTranslationEn = { + title: "Dashboard Panel", +}; + +export { dashboardTranslationEn }; diff --git a/ServicesWeb/customer/src/languages/mutual/dashboard/index.ts b/ServicesWeb/customer/src/languages/mutual/dashboard/index.ts new file mode 100644 index 0000000..250eff2 --- /dev/null +++ b/ServicesWeb/customer/src/languages/mutual/dashboard/index.ts @@ -0,0 +1,7 @@ +import { dashboardTranslationEn } from "./english"; +import { dashboardTranslationTr } from "./turkish"; + +export const dashboardTranslation = { + en: dashboardTranslationEn, + tr: dashboardTranslationTr, +}; diff --git a/ServicesWeb/customer/src/languages/mutual/dashboard/turkish.ts b/ServicesWeb/customer/src/languages/mutual/dashboard/turkish.ts new file mode 100644 index 0000000..a7e112d --- /dev/null +++ b/ServicesWeb/customer/src/languages/mutual/dashboard/turkish.ts @@ -0,0 +1,5 @@ +const dashboardTranslationTr = { + title: "Yönetim Panosu", +}; + +export { dashboardTranslationTr }; diff --git a/ServicesWeb/customer/src/languages/mutual/footer/english.ts b/ServicesWeb/customer/src/languages/mutual/footer/english.ts new file mode 100644 index 0000000..f6968ef --- /dev/null +++ b/ServicesWeb/customer/src/languages/mutual/footer/english.ts @@ -0,0 +1,6 @@ +const footerDefaultEn = { + description: "Footer Default", + footer: "Footer Info", +}; + +export { footerDefaultEn }; diff --git a/ServicesWeb/customer/src/languages/mutual/footer/turkish.ts b/ServicesWeb/customer/src/languages/mutual/footer/turkish.ts new file mode 100644 index 0000000..0a4f822 --- /dev/null +++ b/ServicesWeb/customer/src/languages/mutual/footer/turkish.ts @@ -0,0 +1,6 @@ +const footerDefaultTr = { + description: "Footer Bilgi", + footer: "Alt Bilgi", +}; + +export { footerDefaultTr }; diff --git a/ServicesWeb/customer/src/languages/mutual/header/english.ts b/ServicesWeb/customer/src/languages/mutual/header/english.ts new file mode 100644 index 0000000..707862f --- /dev/null +++ b/ServicesWeb/customer/src/languages/mutual/header/english.ts @@ -0,0 +1,5 @@ +const headerDefaultEn = { + selectedPage: "Selected Page", +}; + +export { headerDefaultEn }; diff --git a/ServicesWeb/customer/src/languages/mutual/header/turkish.ts b/ServicesWeb/customer/src/languages/mutual/header/turkish.ts new file mode 100644 index 0000000..659956c --- /dev/null +++ b/ServicesWeb/customer/src/languages/mutual/header/turkish.ts @@ -0,0 +1,5 @@ +const headerDefaultTr = { + selectedPage: "Seçili Sayfa", +}; + +export { headerDefaultTr }; diff --git a/ServicesWeb/customer/src/languages/mutual/languageSelection/english.ts b/ServicesWeb/customer/src/languages/mutual/languageSelection/english.ts new file mode 100644 index 0000000..beda5b8 --- /dev/null +++ b/ServicesWeb/customer/src/languages/mutual/languageSelection/english.ts @@ -0,0 +1,7 @@ +const languageSelectionTranslationEn = { + title: "Language Selection", + english: "English", + turkish: "Turkish", +}; + +export { languageSelectionTranslationEn }; diff --git a/ServicesWeb/customer/src/languages/mutual/languageSelection/index.ts b/ServicesWeb/customer/src/languages/mutual/languageSelection/index.ts new file mode 100644 index 0000000..211ff40 --- /dev/null +++ b/ServicesWeb/customer/src/languages/mutual/languageSelection/index.ts @@ -0,0 +1,7 @@ +import { languageSelectionTranslationEn } from "./english"; +import { languageSelectionTranslationTr } from "./turkish"; + +export const languageSelectionTranslation = { + en: languageSelectionTranslationEn, + tr: languageSelectionTranslationTr +} \ No newline at end of file diff --git a/ServicesWeb/customer/src/languages/mutual/languageSelection/turkish.ts b/ServicesWeb/customer/src/languages/mutual/languageSelection/turkish.ts new file mode 100644 index 0000000..d350721 --- /dev/null +++ b/ServicesWeb/customer/src/languages/mutual/languageSelection/turkish.ts @@ -0,0 +1,7 @@ +const languageSelectionTranslationTr = { + title: "Dil Seçimi", + english: "İngilizce", + turkish: "Türkçe", +}; + +export { languageSelectionTranslationTr }; diff --git a/ServicesWeb/customer/src/languages/mutual/menu/english.ts b/ServicesWeb/customer/src/languages/mutual/menu/english.ts new file mode 100644 index 0000000..677625e --- /dev/null +++ b/ServicesWeb/customer/src/languages/mutual/menu/english.ts @@ -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 }; diff --git a/ServicesWeb/customer/src/languages/mutual/menu/index.ts b/ServicesWeb/customer/src/languages/mutual/menu/index.ts new file mode 100644 index 0000000..df0b19e --- /dev/null +++ b/ServicesWeb/customer/src/languages/mutual/menu/index.ts @@ -0,0 +1,7 @@ +import { menuTranslationEn } from "./english"; +import { menuTranslationTr } from "./turkish"; + +export const menuTranslation = { + en: menuTranslationEn, + tr: menuTranslationTr, +}; diff --git a/ServicesWeb/customer/src/languages/mutual/menu/turkish.ts b/ServicesWeb/customer/src/languages/mutual/menu/turkish.ts new file mode 100644 index 0000000..11c59d9 --- /dev/null +++ b/ServicesWeb/customer/src/languages/mutual/menu/turkish.ts @@ -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 }; diff --git a/ServicesWeb/customer/src/languages/mutual/pagination/english.ts b/ServicesWeb/customer/src/languages/mutual/pagination/english.ts new file mode 100644 index 0000000..540f3d7 --- /dev/null +++ b/ServicesWeb/customer/src/languages/mutual/pagination/english.ts @@ -0,0 +1,5 @@ +const dashboardTranslationEn = { + title: "Dashboard Panel", +}; + +export { dashboardTranslationEn }; diff --git a/ServicesWeb/customer/src/languages/mutual/pagination/index.ts b/ServicesWeb/customer/src/languages/mutual/pagination/index.ts new file mode 100644 index 0000000..250eff2 --- /dev/null +++ b/ServicesWeb/customer/src/languages/mutual/pagination/index.ts @@ -0,0 +1,7 @@ +import { dashboardTranslationEn } from "./english"; +import { dashboardTranslationTr } from "./turkish"; + +export const dashboardTranslation = { + en: dashboardTranslationEn, + tr: dashboardTranslationTr, +}; diff --git a/ServicesWeb/customer/src/languages/mutual/pagination/turkish.ts b/ServicesWeb/customer/src/languages/mutual/pagination/turkish.ts new file mode 100644 index 0000000..a7e112d --- /dev/null +++ b/ServicesWeb/customer/src/languages/mutual/pagination/turkish.ts @@ -0,0 +1,5 @@ +const dashboardTranslationTr = { + title: "Yönetim Panosu", +}; + +export { dashboardTranslationTr }; diff --git a/ServicesWeb/customer/src/layouts/a.txt b/ServicesWeb/customer/src/layouts/a.txt new file mode 100644 index 0000000..e69de29 diff --git a/ServicesWeb/customer/src/layouts/auth/layout.tsx b/ServicesWeb/customer/src/layouts/auth/layout.tsx new file mode 100644 index 0000000..1723117 --- /dev/null +++ b/ServicesWeb/customer/src/layouts/auth/layout.tsx @@ -0,0 +1,21 @@ +'use server'; +import { FC, Suspense } from "react"; +import { AuthLayoutProps } from "@/validations/mutual/auth/props"; + +const AuthLayout: FC = async ({ lang, page, activePageUrl }) => { + return ( +
+
+
+
WAG Frontend
+
Welcome to the WAG Frontend Application
+
+
+
+ Loading...
}>{page} +
+ + ); +} + +export { AuthLayout }; \ No newline at end of file diff --git a/ServicesWeb/customer/src/layouts/dashboard/client.tsx b/ServicesWeb/customer/src/layouts/dashboard/client.tsx new file mode 100644 index 0000000..344a4a7 --- /dev/null +++ b/ServicesWeb/customer/src/layouts/dashboard/client.tsx @@ -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 } + +const ClientLayout: FC = ({ 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 ( + +
+ + + + +
+
+ ); +}; + +export { ClientLayout } diff --git a/ServicesWeb/customer/src/layouts/dashboard/layout.tsx b/ServicesWeb/customer/src/layouts/dashboard/layout.tsx new file mode 100644 index 0000000..386facf --- /dev/null +++ b/ServicesWeb/customer/src/layouts/dashboard/layout.tsx @@ -0,0 +1,10 @@ +'use server'; +import { FC } from "react"; +import { DashboardLayoutProps } from "@/validations/mutual/dashboard/props"; +import { ClientLayout } from "./client"; + +const DashboardLayout: FC = async ({ params, searchParams }) => { + const activePageUrl = `/${params.page?.join('/')}`; return +} + +export { DashboardLayout }; diff --git a/ServicesWeb/customer/src/pages/multi/index.ts b/ServicesWeb/customer/src/pages/multi/index.ts new file mode 100644 index 0000000..3e5ff0c --- /dev/null +++ b/ServicesWeb/customer/src/pages/multi/index.ts @@ -0,0 +1,10 @@ +import { DashboardPage } from '@/components/custom/content/DashboardPage'; + +const pageIndexMulti: Record>> = { + '/dashboard': { + 'DashboardPage': DashboardPage + }, + // Add more pages as needed +}; + +export default pageIndexMulti; diff --git a/ServicesWeb/customer/src/pages/mutual/noContent/page.tsx b/ServicesWeb/customer/src/pages/mutual/noContent/page.tsx new file mode 100644 index 0000000..a84706f --- /dev/null +++ b/ServicesWeb/customer/src/pages/mutual/noContent/page.tsx @@ -0,0 +1,37 @@ +'use client'; +import { FC } from 'react'; +import { LanguageTypes } from '@/validations/mutual/language/validations'; + +interface ContentToRenderNoPageProps { + lang: LanguageTypes; +} + +const translations = { + en: { + pageNotFound: "Page Not Found", + pageNotFoundDescription: "The requested page could not be found or is not available.", + goBack: "Go Back" + }, + tr: { + pageNotFound: "Sayfa Bulunamadı", + pageNotFoundDescription: "İstenen sayfa bulunamadı veya kullanılamıyor.", + goBack: "Geri Dön" + } +}; + +const ContentToRenderNoPage: FC = ({ lang = 'en' }) => { + return ( +
+

{translations[lang].pageNotFound}

+

{translations[lang].pageNotFoundDescription}

+ +
+ ); +}; + +export default ContentToRenderNoPage; diff --git a/ServicesWeb/customer/src/styles/custom-scrollbar.css b/ServicesWeb/customer/src/styles/custom-scrollbar.css new file mode 100644 index 0000000..51a651e --- /dev/null +++ b/ServicesWeb/customer/src/styles/custom-scrollbar.css @@ -0,0 +1,25 @@ +/* Custom scrollbar styling */ +.custom-scrollbar::-webkit-scrollbar { + width: 6px; + height: 6px; +} + +.custom-scrollbar::-webkit-scrollbar-track { + background: rgba(0, 0, 0, 0.05); + border-radius: 3px; +} + +.custom-scrollbar::-webkit-scrollbar-thumb { + background: rgba(0, 0, 0, 0.15); + border-radius: 3px; +} + +.custom-scrollbar::-webkit-scrollbar-thumb:hover { + background: rgba(0, 0, 0, 0.25); +} + +/* For Firefox */ +.custom-scrollbar { + scrollbar-width: thin; + scrollbar-color: rgba(0, 0, 0, 0.15) rgba(0, 0, 0, 0.05); +} diff --git a/ServicesWeb/customer/src/types/mutual/context/complete/validations.ts b/ServicesWeb/customer/src/types/mutual/context/complete/validations.ts new file mode 100644 index 0000000..de6660a --- /dev/null +++ b/ServicesWeb/customer/src/types/mutual/context/complete/validations.ts @@ -0,0 +1,44 @@ +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"; +import { defaultValuesHeader } from "@/fetchers/types/context/header/validations"; +import { defaultValuesSelection } from "@/fetchers/types/context/selection/validations"; +import { defaultValuesUser } from "@/fetchers/types/context/user/validations"; +import { defaultValuesSettings } from "@/fetchers/types/context/settings/validations"; + +interface ClientRedisToken { + online: ClientOnline; + pageConfig: ClientPageConfig; + menu: ClientMenu; + header: ClientHeader; + selection: ClientSelection; + user: ClientUser; + settings: ClientSettings; + chatRoom: string[]; + notifications: string[]; + messages: string[]; +} + +const defaultClientRedisToken: ClientRedisToken = { + online: defaultValuesOnline, + pageConfig: defaultValuesPageConfig, + menu: defaultValuesMenu, + header: defaultValuesHeader, + selection: defaultValuesSelection, + user: defaultValuesUser, + settings: defaultValuesSettings, + chatRoom: [], + notifications: [], + messages: [], +}; + +export type { ClientRedisToken }; +export { defaultClientRedisToken }; diff --git a/ServicesWeb/customer/src/types/mutual/context/header/validations.ts b/ServicesWeb/customer/src/types/mutual/context/header/validations.ts new file mode 100644 index 0000000..74fab2d --- /dev/null +++ b/ServicesWeb/customer/src/types/mutual/context/header/validations.ts @@ -0,0 +1,16 @@ +interface ClientHeader { + header: string[]; + activeDomain: string; + listOfDomains: string[]; + connections: string[]; +} + +const defaultValuesHeader: ClientHeader = { + header: [], + activeDomain: "", + listOfDomains: [], + connections: [], +}; + +export type { ClientHeader }; +export { defaultValuesHeader }; diff --git a/ServicesWeb/customer/src/types/mutual/context/index.ts b/ServicesWeb/customer/src/types/mutual/context/index.ts new file mode 100644 index 0000000..a68eae5 --- /dev/null +++ b/ServicesWeb/customer/src/types/mutual/context/index.ts @@ -0,0 +1,62 @@ +import { + ClientRedisToken, + defaultClientRedisToken, +} from "@/fetchers/types/context/complete/validations"; +import { + ClientPageConfig, + defaultValuesPageConfig, +} from "@/fetchers/types/context/pageConfig/validations"; +import { + ClientMenu, + defaultValuesMenu, +} from "@/fetchers/types/context/menu/validations"; +import { + ClientHeader, + defaultValuesHeader, +} from "@/fetchers/types/context/header/validations"; +import { + ClientSelection, + defaultValuesSelection, +} from "@/fetchers/types/context/selection/validations"; +import { + ClientUser, + defaultValuesUser, +} from "@/fetchers/types/context/user/validations"; +import { + ClientSettings, + defaultValuesSettings, +} from "@/fetchers/types/context/settings/validations"; +import { + ClientOnline, + defaultValuesOnline, +} from "@/fetchers/types/context/online/validations"; + +class AuthError extends Error { + constructor(message: string) { + super(message); + this.name = "AuthError"; + } +} + +export type { + ClientRedisToken, + ClientOnline, + ClientPageConfig, + ClientMenu, + ClientHeader, + ClientSelection, + ClientUser, + ClientSettings, +}; + +export { + defaultClientRedisToken, + defaultValuesOnline, + defaultValuesPageConfig, + defaultValuesMenu, + defaultValuesHeader, + defaultValuesSelection, + defaultValuesUser, + defaultValuesSettings, + AuthError, +}; diff --git a/ServicesWeb/customer/src/types/mutual/context/menu/validations.ts b/ServicesWeb/customer/src/types/mutual/context/menu/validations.ts new file mode 100644 index 0000000..7cc25b7 --- /dev/null +++ b/ServicesWeb/customer/src/types/mutual/context/menu/validations.ts @@ -0,0 +1,12 @@ +interface ClientMenu { + selectionList: string[]; + activeSelection: string; +} + +const defaultValuesMenu: ClientMenu = { + selectionList: ["/dashboard"], + activeSelection: "/dashboard", +}; + +export type { ClientMenu }; +export { defaultValuesMenu }; diff --git a/ServicesWeb/customer/src/types/mutual/context/online/validations.ts b/ServicesWeb/customer/src/types/mutual/context/online/validations.ts new file mode 100644 index 0000000..b2ec0b5 --- /dev/null +++ b/ServicesWeb/customer/src/types/mutual/context/online/validations.ts @@ -0,0 +1,22 @@ +interface ClientOnline { + lastLogin: Date; + lastLogout: Date; + lastAction: Date; + lastPage: string; + userType: string; + lang: string; + timezone: string; +} + +const defaultValuesOnline: ClientOnline = { + lastLogin: new Date(), + lastLogout: new Date(), + lastAction: new Date(), + lastPage: "/dashboard", + userType: "employee", + lang: "tr", + timezone: "GMT+3", +}; + +export type { ClientOnline }; +export { defaultValuesOnline }; diff --git a/ServicesWeb/customer/src/types/mutual/context/pageConfig/validations.ts b/ServicesWeb/customer/src/types/mutual/context/pageConfig/validations.ts new file mode 100644 index 0000000..4b2dbf5 --- /dev/null +++ b/ServicesWeb/customer/src/types/mutual/context/pageConfig/validations.ts @@ -0,0 +1,14 @@ +interface ClientPageConfig { + mode: string; + textFont: number; + theme: string; +} + +const defaultValuesPageConfig: ClientPageConfig = { + mode: "light", + textFont: 14, + theme: "default", +}; + +export type { ClientPageConfig }; +export { defaultValuesPageConfig }; diff --git a/ServicesWeb/customer/src/types/mutual/context/selection/validations.ts b/ServicesWeb/customer/src/types/mutual/context/selection/validations.ts new file mode 100644 index 0000000..d06627b --- /dev/null +++ b/ServicesWeb/customer/src/types/mutual/context/selection/validations.ts @@ -0,0 +1,12 @@ +interface ClientSelection { + selectionList: string[]; + activeSelection: Record; +} + +const defaultValuesSelection: ClientSelection = { + selectionList: ["/dashboard"], + activeSelection: {}, +}; + +export type { ClientSelection }; +export { defaultValuesSelection }; diff --git a/ServicesWeb/customer/src/types/mutual/context/settings/validations.ts b/ServicesWeb/customer/src/types/mutual/context/settings/validations.ts new file mode 100644 index 0000000..35ac81f --- /dev/null +++ b/ServicesWeb/customer/src/types/mutual/context/settings/validations.ts @@ -0,0 +1,12 @@ +interface ClientSettings { + lastOnline: Date; + token: string; +} + +const defaultValuesSettings: ClientSettings = { + lastOnline: new Date(), + token: "", +}; + +export type { ClientSettings }; +export { defaultValuesSettings }; diff --git a/ServicesWeb/customer/src/types/mutual/context/user/validations.ts b/ServicesWeb/customer/src/types/mutual/context/user/validations.ts new file mode 100644 index 0000000..8ccc85a --- /dev/null +++ b/ServicesWeb/customer/src/types/mutual/context/user/validations.ts @@ -0,0 +1,44 @@ +interface Person { + uuid: string; + firstname: string; + surname: string; + middle_name: string; + sex_code: string; + person_tag: string; + country_code: string; + birth_date: string; +} + +interface ClientUser { + uuid: string; + avatar: string; + email: string; + phone_number: string; + user_tag: string; + password_expiry_begins: string; + person: Person; +} + +const defaultValuesPerson: Person = { + uuid: "", + firstname: "", + surname: "", + middle_name: "", + sex_code: "", + person_tag: "", + country_code: "", + birth_date: "", +}; + +const defaultValuesUser: ClientUser = { + uuid: "", + avatar: "", + email: "", + phone_number: "", + user_tag: "", + password_expiry_begins: new Date().toISOString(), + person: defaultValuesPerson, +}; + +export type { ClientUser }; +export { defaultValuesUser }; diff --git a/ServicesWeb/customer/src/validations/mutual/dashboard/props.ts b/ServicesWeb/customer/src/validations/mutual/dashboard/props.ts new file mode 100644 index 0000000..33ccf99 --- /dev/null +++ b/ServicesWeb/customer/src/validations/mutual/dashboard/props.ts @@ -0,0 +1,101 @@ +import { LanguageTypes } from "@/validations/mutual/language/validations"; +import { ParamsType } from "@/validations/mutual/pages/props"; + +interface MaindasboardPageProps { + params: Promise; + searchParams: Promise<{ [key: string]: string | string[] | undefined }>; +} + +interface DashboardLayoutProps { + params: ParamsType; + searchParams: { [key: string]: string | string[] | undefined }; + lang: LanguageTypes; +} + +type ModeTypes = "shortList" | "fullList" | "create" | "update" | "view"; +const ModeTypesList = ["shortList", "fullList", "create", "update", "view"]; + +interface ContentProps { + activePageUrl: string; + mode?: ModeTypes; + searchParams: Record; + onlineData: any; + onlineLoading: boolean; + onlineError: any; + refreshOnline: () => Promise; + updateOnline: (newOnline: any) => Promise; + userData: any; + userLoading: boolean; + userError: any; + refreshUser: () => Promise; + updateUser: (newUser: any) => Promise; +} + +interface MenuProps { + availableApplications: string[]; + searchParams: Record; + activePageUrl: string; + prefix?: string; + onlineData: any; + onlineLoading: boolean; + onlineError: any; + refreshOnline: () => Promise; + updateOnline: (newOnline: any) => Promise; + userData: any; + userLoading: boolean; + userError: any; + refreshUser: () => Promise; + updateUser: (newUser: any) => Promise; + selectionData: any; + selectionLoading: boolean; + selectionError: any; + refreshSelection: () => Promise; + updateSelection: (newSelection: any) => Promise; + menuData: any; + menuLoading: boolean; + menuError: any; + refreshMenu: () => Promise; + updateMenu: (newMenu: any) => Promise; +} + +interface FooterProps { + searchParams: Record; + activePageUrl: string; + configData: any; + configLoading: boolean; + configError: any; + refreshConfig: () => Promise; + updateConfig: (newConfig: any) => Promise; + onlineData: any; + onlineLoading: boolean; + onlineError: any; + refreshOnline: () => Promise; + updateOnline: (newOnline: any) => Promise; +} + +interface HeaderProps { + activePageUrl: string; + searchParams: Record; + onlineData: any; + onlineLoading: boolean; + onlineError: any; + refreshOnline: () => Promise; + updateOnline: (newOnline: any) => Promise; + userData: any; + userLoading: boolean; + userError: any; + refreshUser: () => Promise; + updateUser: (newUser: any) => Promise; +} + +export type { + MaindasboardPageProps, + DashboardLayoutProps, + ContentProps, + MenuProps, + FooterProps, + HeaderProps, + ModeTypes, +}; + +export { ModeTypesList }; diff --git a/ServicesWeb/customer/src/validations/mutual/language/validations.ts b/ServicesWeb/customer/src/validations/mutual/language/validations.ts new file mode 100644 index 0000000..31a558f --- /dev/null +++ b/ServicesWeb/customer/src/validations/mutual/language/validations.ts @@ -0,0 +1,3 @@ +type LanguageTypes = "en" | "tr"; + +export type { LanguageTypes };